ModelConfigService.vala 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. using Invercargill;
  2. using Invercargill.DataStructures;
  3. using Invercargill.Expressions;
  4. using InvercargillSql.Orm;
  5. using Inversion;
  6. using ClankerSaddle.Data;
  7. namespace ClankerSaddle.Services {
  8. /**
  9. * The deployment's model configuration (the Admin → Model page):
  10. * the ONE source for the OpenAI-compatible endpoint every saddle
  11. * assignment is sent to and the advisor's (optionally overridden)
  12. * endpoint. Stored as the `model_config` singleton row.
  13. *
  14. * A SINGLETON (the advisor service — itself a singleton — and the
  15. * dispatcher both read it outside request scopes), so every
  16. * operation opens its own container scope for the ORM session
  17. * (the Dispatcher's create_scope discipline).
  18. *
  19. * BOOTSTRAP: the very first read on an empty table seeds the row
  20. * from the legacy environment (SADDLE_LLM_BASE_URL / _API_KEY /
  21. * _MODEL, ADVISOR_LLM_* the same way) so a pre-page deployment
  22. * keeps dispatching through its upgrade — once, ever. From then on
  23. * the database is the only source: environment changes are
  24. * ignored, and the page is where configuration changes land.
  25. */
  26. public class ModelConfigService : Object {
  27. private Container container = inject<Container>();
  28. /**
  29. * The configuration row, creating and seeding it from the
  30. * legacy environment when the table is still empty (the
  31. * one-time upgrade bridge — never again afterwards).
  32. */
  33. public async ModelConfig current() throws Error {
  34. var scope = container.create_scope();
  35. try {
  36. var db = scope.resolve<OrmSession>();
  37. var row = yield db.query<ModelConfig>()
  38. .where(expr("id == $0", elem<int64?>(1)))
  39. .first_async();
  40. if (row != null) {
  41. return (!)row;
  42. }
  43. var seeded = new ModelConfig() {
  44. id = 1,
  45. base_url = legacy_env("SADDLE_LLM_BASE_URL"),
  46. api_key = legacy_env("SADDLE_LLM_API_KEY"),
  47. model = legacy_env("SADDLE_LLM_MODEL"),
  48. advisor_base_url = legacy_env("ADVISOR_LLM_BASE_URL"),
  49. advisor_api_key = legacy_env("ADVISOR_LLM_API_KEY"),
  50. advisor_model = legacy_env("ADVISOR_LLM_MODEL"),
  51. updated_at = new DateTime.now_local()
  52. };
  53. yield db.insert_async<ModelConfig>(seeded);
  54. return seeded;
  55. } finally {
  56. scope.release();
  57. }
  58. }
  59. /** One legacy environment value, trimmed ("" when unset). */
  60. private static string legacy_env(string name) {
  61. return (Environment.get_variable(name) ?? "").strip();
  62. }
  63. /**
  64. * Persists the page's form fields. The two API keys use the
  65. * keep-when-blank contract (the page never echoes a key back):
  66. * a blank field keeps the stored value unless its explicit
  67. * clear checkbox is set.
  68. */
  69. public async ModelConfig save(string base_url, string api_key, string model,
  70. string advisor_base_url, string advisor_api_key, string advisor_model,
  71. bool clear_api_key, bool clear_advisor_api_key) throws Error {
  72. var row = yield current();
  73. row.base_url = base_url.strip();
  74. if (api_key.strip().length > 0 || clear_api_key) {
  75. row.api_key = api_key.strip();
  76. }
  77. row.model = model.strip();
  78. row.advisor_base_url = advisor_base_url.strip();
  79. if (advisor_api_key.strip().length > 0 || clear_advisor_api_key) {
  80. row.advisor_api_key = advisor_api_key.strip();
  81. }
  82. row.advisor_model = advisor_model.strip();
  83. row.updated_at = new DateTime.now_local();
  84. var scope = container.create_scope();
  85. try {
  86. var db = scope.resolve<OrmSession>();
  87. yield db.update_async<ModelConfig>(row);
  88. } finally {
  89. scope.release();
  90. }
  91. return row;
  92. }
  93. /**
  94. * The saddle assignments' endpoint, or null while no base URL
  95. * is configured (dispatch pauses — the Admin → Model page is
  96. * where it gets set).
  97. */
  98. public async LlmEndpoint? saddle_endpoint() throws Error {
  99. var row = yield current();
  100. return LlmEndpoint.from_config(
  101. row.base_url, row.api_key, row.model);
  102. }
  103. /**
  104. * The advisor's endpoint: the advisor-specific override when
  105. * its base URL is set, else the main fields; null while
  106. * neither carries a base URL (the advisor reports itself
  107. * unconfigured).
  108. */
  109. public async AdvisorLlmEndpoint? advisor_endpoint() throws Error {
  110. var row = yield current();
  111. if (row.advisor_base_url.length > 0) {
  112. return AdvisorLlmEndpoint.from_config(row.advisor_base_url,
  113. row.advisor_api_key, row.advisor_model);
  114. }
  115. return AdvisorLlmEndpoint.from_config(row.base_url, row.api_key, row.model);
  116. }
  117. }
  118. }