| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126 |
- using Invercargill;
- using Invercargill.DataStructures;
- using Invercargill.Expressions;
- using InvercargillSql.Orm;
- using Inversion;
- using ClankerSaddle.Data;
- namespace ClankerSaddle.Services {
- /**
- * The deployment's model configuration (the Admin → Model page):
- * the ONE source for the OpenAI-compatible endpoint every saddle
- * assignment is sent to and the advisor's (optionally overridden)
- * endpoint. Stored as the `model_config` singleton row.
- *
- * A SINGLETON (the advisor service — itself a singleton — and the
- * dispatcher both read it outside request scopes), so every
- * operation opens its own container scope for the ORM session
- * (the Dispatcher's create_scope discipline).
- *
- * BOOTSTRAP: the very first read on an empty table seeds the row
- * from the legacy environment (SADDLE_LLM_BASE_URL / _API_KEY /
- * _MODEL, ADVISOR_LLM_* the same way) so a pre-page deployment
- * keeps dispatching through its upgrade — once, ever. From then on
- * the database is the only source: environment changes are
- * ignored, and the page is where configuration changes land.
- */
- public class ModelConfigService : Object {
- private Container container = inject<Container>();
- /**
- * The configuration row, creating and seeding it from the
- * legacy environment when the table is still empty (the
- * one-time upgrade bridge — never again afterwards).
- */
- public async ModelConfig current() throws Error {
- var scope = container.create_scope();
- try {
- var db = scope.resolve<OrmSession>();
- var row = yield db.query<ModelConfig>()
- .where(expr("id == $0", elem<int64?>(1)))
- .first_async();
- if (row != null) {
- return (!)row;
- }
- var seeded = new ModelConfig() {
- id = 1,
- base_url = legacy_env("SADDLE_LLM_BASE_URL"),
- api_key = legacy_env("SADDLE_LLM_API_KEY"),
- model = legacy_env("SADDLE_LLM_MODEL"),
- advisor_base_url = legacy_env("ADVISOR_LLM_BASE_URL"),
- advisor_api_key = legacy_env("ADVISOR_LLM_API_KEY"),
- advisor_model = legacy_env("ADVISOR_LLM_MODEL"),
- updated_at = new DateTime.now_local()
- };
- yield db.insert_async<ModelConfig>(seeded);
- return seeded;
- } finally {
- scope.release();
- }
- }
- /** One legacy environment value, trimmed ("" when unset). */
- private static string legacy_env(string name) {
- return (Environment.get_variable(name) ?? "").strip();
- }
- /**
- * Persists the page's form fields. The two API keys use the
- * keep-when-blank contract (the page never echoes a key back):
- * a blank field keeps the stored value unless its explicit
- * clear checkbox is set.
- */
- public async ModelConfig save(string base_url, string api_key, string model,
- string advisor_base_url, string advisor_api_key, string advisor_model,
- bool clear_api_key, bool clear_advisor_api_key) throws Error {
- var row = yield current();
- row.base_url = base_url.strip();
- if (api_key.strip().length > 0 || clear_api_key) {
- row.api_key = api_key.strip();
- }
- row.model = model.strip();
- row.advisor_base_url = advisor_base_url.strip();
- if (advisor_api_key.strip().length > 0 || clear_advisor_api_key) {
- row.advisor_api_key = advisor_api_key.strip();
- }
- row.advisor_model = advisor_model.strip();
- row.updated_at = new DateTime.now_local();
- var scope = container.create_scope();
- try {
- var db = scope.resolve<OrmSession>();
- yield db.update_async<ModelConfig>(row);
- } finally {
- scope.release();
- }
- return row;
- }
- /**
- * The saddle assignments' endpoint, or null while no base URL
- * is configured (dispatch pauses — the Admin → Model page is
- * where it gets set).
- */
- public async LlmEndpoint? saddle_endpoint() throws Error {
- var row = yield current();
- return LlmEndpoint.from_config(
- row.base_url, row.api_key, row.model);
- }
- /**
- * The advisor's endpoint: the advisor-specific override when
- * its base URL is set, else the main fields; null while
- * neither carries a base URL (the advisor reports itself
- * unconfigured).
- */
- public async AdvisorLlmEndpoint? advisor_endpoint() throws Error {
- var row = yield current();
- if (row.advisor_base_url.length > 0) {
- return AdvisorLlmEndpoint.from_config(row.advisor_base_url,
- row.advisor_api_key, row.advisor_model);
- }
- return AdvisorLlmEndpoint.from_config(row.base_url, row.api_key, row.model);
- }
- }
- }
|