diff --git a/src/codehelp/docs/manual_class_creation.md b/src/codehelp/docs/manual_class_creation.md index 97364cb..5958004 100644 --- a/src/codehelp/docs/manual_class_creation.md +++ b/src/codehelp/docs/manual_class_creation.md @@ -29,7 +29,7 @@ If you don't have an API key yet, you can leave it blank for now, but it will be You can create an API key using an account at openai.com. OpenAI will charge you directly for your students' usage, so you will need to purchase usage credits using a credit card. The cost is low. -One query from CodeHelp costs roughly US$0.01 if using GPT-4 (the recommended model) or $0.001 using GPT-3.5 (which is less accurate). +One query from CodeHelp costs roughly US$0.01 if using GPT-4o (the recommended model) or $0.0004 using GPT-4o-mini (which is less accurate). ## Configuration diff --git a/src/codehelp/templates/landing.html b/src/codehelp/templates/landing.html index 050b168..588eb0a 100644 --- a/src/codehelp/templates/landing.html +++ b/src/codehelp/templates/landing.html @@ -161,7 +161,8 @@

Costs

CodeHelp itself does not take payment, but the OpenAI large language models it uses are not free. We will ask you to provide an OpenAI API key to be used for your students' queries.

-

Costs are low: OpenAI will charge you roughly US$0.01 for each query made with the GPT-4 model (GPT-3.5 is roughly 1/10 the cost, though less accurate). If your students use CodeHelp regularly and average 50 queries each over a semester (higher than the average we've observed), your total costs would be roughly $0.50 per student (or $0.05 per student if using GPT-3.5).

+

Costs are low: OpenAI will charge you roughly US$0.01 for each query made with the default GPT-4o model. If your students use CodeHelp regularly and average 50 queries each over a semester (higher than the average we've observed), your total costs would be roughly $0.50 per student.

+

(The GPT-4o-mini model is also available, and it is much less expensive though also less accurate and helpful. The same scenario above would cost about $0.02 per student if using GPT-4o-mini.)

diff --git a/src/gened/base.py b/src/gened/base.py index 4d7155b..02a27e0 100644 --- a/src/gened/base.py +++ b/src/gened/base.py @@ -116,6 +116,10 @@ def create_app_base(import_name: str, app_config: dict[str, Any], instance_path: SEND_FILE_MAX_AGE_DEFAULT=3*60*60, # 3 hours # Free query tokens given to new users DEFAULT_TOKENS=10, + # Model IDs for various circumstances (see also: models table in DB schema) + DEFAULT_MODEL_ID=2, # Default for new users: GPT-4o + SYSTEM_MODEL_ID=2, # Default for 'system' use: GPT-4o + PYLTI_CONFIG={ # will be loaded from the consumers table in the database "consumers": { } diff --git a/src/gened/classes.py b/src/gened/classes.py index d73c5e2..c3c8a89 100644 --- a/src/gened/classes.py +++ b/src/gened/classes.py @@ -111,8 +111,8 @@ class name from the user class_id = cur.lastrowid assert class_id is not None db.execute( - "INSERT INTO classes_user (class_id, creator_user_id, link_ident, openai_key, link_reg_expires) VALUES (?, ?, ?, ?, ?)", - [class_id, user_id, link_ident, openai_key, dt.date.min] + "INSERT INTO classes_user (class_id, creator_user_id, link_ident, openai_key, link_reg_expires, model_id) VALUES (?, ?, ?, ?, ?, ?)", + [class_id, user_id, link_ident, openai_key, dt.date.min, current_app.config['DEFAULT_MODEL_ID']] ) db.execute( "INSERT INTO roles (user_id, class_id, role) VALUES (?, ?, ?)", diff --git a/src/gened/migrations/20240731--expand-models.sql b/src/gened/migrations/20240731--expand-models.sql new file mode 100644 index 0000000..1f9d161 --- /dev/null +++ b/src/gened/migrations/20240731--expand-models.sql @@ -0,0 +1,31 @@ +-- SPDX-FileCopyrightText: 2024 Mark Liffiton +-- +-- SPDX-License-Identifier: AGPL-3.0-only + +PRAGMA foreign_keys = OFF; + +BEGIN; + +-- Note: Not removing DEFAULT values on model_id in consumer and classes_user. +-- With the default removed in the schema, no code going forward should +-- ever use that default (else it would crash in testing). + +DROP TABLE models; + +CREATE TABLE models ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + shortname TEXT NOT NULL UNIQUE, + model TEXT NOT NULL, + active BOOLEAN NOT NULL CHECK (active IN (0,1)) +); +-- See also: DEFAULT_MODEL_ID in app.config (src/gened/base.py) +INSERT INTO models(name, shortname, model, active) VALUES + ('OpenAI GPT-3.5 Turbo', 'GPT-3.5', 'gpt-3.5-turbo-0125', false), + ('OpenAI GPT-4o', 'GPT-4o', 'gpt-4o', true), + ('OpenAI GPT-4o-mini', 'GPT-4o-mini', 'gpt-4o-mini', true) +; + +COMMIT; + +PRAGMA foreign_keys = ON; diff --git a/src/gened/openai.py b/src/gened/openai.py index 3d92f5c..e60c107 100644 --- a/src/gened/openai.py +++ b/src/gened/openai.py @@ -46,7 +46,7 @@ def _get_llm(*, use_system_key: bool, spend_token: bool) -> LLMConfig: a) LTI class config is in the linked LTI consumer. b) User class config is in the user class. c) If there is a current class but it is disabled or has no key, raise an error. - 3) If the user is a local-auth user, the system API key and GPT-3.5 is used. + 3) If the user is a local-auth user, the system API key and model is used. 4) Otherwise, we use tokens and the system API key / model. If spend_token is True, the user must have 1 or more tokens remaining. If they have 0 tokens, raise an error. @@ -63,8 +63,7 @@ def make_system_client(tokens_remaining: int | None = None) -> LLMConfig: """ Factory function to initialize a default client (using the system key) only if/when needed. """ - # Get the systemwide default model (TODO: better control than just id=2) - model_row = db.execute("SELECT models.model FROM models WHERE models.id=2").fetchone() + model_row = db.execute("SELECT models.model FROM models WHERE models.id=?", [current_app.config['SYSTEM_MODEL_ID']]).fetchone() system_model = model_row['model'] system_key = current_app.config["OPENAI_API_KEY"] return LLMConfig( @@ -183,7 +182,7 @@ def decorated_function(*args: P.args, **kwargs: P.kwargs) -> str | R: def get_models() -> list[Row]: """Enumerate the models available in the database.""" db = get_db() - models = db.execute("SELECT * FROM models ORDER BY id ASC").fetchall() + models = db.execute("SELECT * FROM models WHERE active ORDER BY id ASC").fetchall() return models diff --git a/src/gened/schema_common.sql b/src/gened/schema_common.sql index a88a530..2b0e231 100644 --- a/src/gened/schema_common.sql +++ b/src/gened/schema_common.sql @@ -29,7 +29,7 @@ CREATE TABLE consumers ( lti_consumer TEXT NOT NULL UNIQUE, lti_secret TEXT, openai_key TEXT, - model_id INTEGER NOT NULL DEFAULT 1, -- gpt-3.5 + model_id INTEGER NOT NULL, created DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY(model_id) REFERENCES models(id) ); @@ -111,7 +111,7 @@ CREATE UNIQUE INDEX classes_lti_by_consumer_context ON classes_lti(lti_consumer CREATE TABLE classes_user ( class_id INTEGER PRIMARY KEY, -- references classes.id openai_key TEXT, - model_id INTEGER NOT NULL DEFAULT 1, -- gpt-3.5 + model_id INTEGER NOT NULL, link_ident TEXT NOT NULL UNIQUE, -- random (unguessable) identifier used in access/registration link for this class link_reg_expires DATE NOT NULL, -- registration active for the class link if this date is in the future (anywhere on Earth) creator_user_id INTEGER NOT NULL, -- references users.id @@ -158,11 +158,14 @@ CREATE TABLE models ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, shortname TEXT NOT NULL UNIQUE, - model TEXT NOT NULL + model TEXT NOT NULL, + active BOOLEAN NOT NULL CHECK (active IN (0,1)) ); -INSERT INTO models(name, shortname, model) VALUES - ('OpenAI GPT-3.5 Turbo', 'GPT-3.5', 'gpt-3.5-turbo-0125'), - ('OpenAI GPT-4o', 'GPT-4', 'gpt-4o') +-- See also: DEFAULT_MODEL_ID in app.config (src/gened/base.py) +INSERT INTO models(name, shortname, model, active) VALUES + ('OpenAI GPT-3.5 Turbo', 'GPT-3.5', 'gpt-3.5-turbo-0125', false), + ('OpenAI GPT-4o', 'GPT-4o', 'gpt-4o', true), + ('OpenAI GPT-4o-mini', 'GPT-4o-mini', 'gpt-4o-mini', false) ; diff --git a/src/gened/templates/instructor_class_config.html b/src/gened/templates/instructor_class_config.html index 47651fc..e75c350 100644 --- a/src/gened/templates/instructor_class_config.html +++ b/src/gened/templates/instructor_class_config.html @@ -169,7 +169,6 @@

Language Model

-

Note that GPT-4 produces more accurate results than 3.5 but is also roughly 10 times the cost.

diff --git a/tests/test_data.sql b/tests/test_data.sql index 0af939e..bc5f10c 100644 --- a/tests/test_data.sql +++ b/tests/test_data.sql @@ -2,10 +2,10 @@ -- -- SPDX-License-Identifier: AGPL-3.0-only -INSERT INTO consumers (id, lti_consumer, lti_secret, openai_key) +INSERT INTO consumers (id, lti_consumer, lti_secret, openai_key, model_id) VALUES - (1, 'consumer.domain', 'seecrits1', 'keeeez1'), - (2, 'consumer.otherdomain', 'seecrits2', 'keeeez2'); + (1, 'consumer.domain', 'seecrits1', 'keeeez1', 1), + (2, 'consumer.otherdomain', 'seecrits2', 'keeeez2', 2); INSERT INTO classes (id, name, enabled) VALUES @@ -41,11 +41,11 @@ VALUES (22, 2, null, 'ltiuser2@domain.edu', null, false, false, 0), (23, 2, null, 'ltiuser3@domain.edu', null, false, false, 0); -INSERT INTO classes_user (class_id, openai_key, link_ident, link_reg_expires, creator_user_id) +INSERT INTO classes_user (class_id, openai_key, link_ident, link_reg_expires, creator_user_id, model_id) VALUES - (2, 'nope', 'reg_disabled', '0001-01-01', 11), - (3, 'nope', 'reg_expired', '2023-01-01', 11), - (4, 'nope', 'reg_enabled', '9999-12-31', 11); + (2, 'nope', 'reg_disabled', '0001-01-01', 11, 1), + (3, 'nope', 'reg_expired', '2023-01-01', 11, 2), + (4, 'nope', 'reg_enabled', '9999-12-31', 11, 2); INSERT INTO auth_local (user_id, username, password) VALUES