modelconfig.py 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. """The model configuration bundle (Admin → Model page): the OpenAI
  2. endpoint every saddle assignment is sent to lives in the model_config
  3. row — environment variables no longer configure it (the harness's
  4. SADDLE_LLM_BASE_URL seeds the row once, on the app's first read; the
  5. page's save action owns it afterwards).
  6. """
  7. from harness import * # noqa: F401,F403 — the shared wire helpers
  8. def run(ctx):
  9. auth, fake, fake_id, fake_token = ctx.auth, ctx.fake, ctx.fake_id, ctx.fake_token
  10. print("== model configuration: the Admin → Model page ==")
  11. fake.ask_on_register = False
  12. fake.hold_state = True
  13. # The bootstrap seed: the app's first configuration read imported
  14. # the harness's SADDLE_LLM_BASE_URL environment into the row.
  15. model = slot_public(auth, "/models", "model")
  16. check(model.get("dispatch_ready") is True,
  17. "the page reports dispatch ready (the env bootstrap seeded the row)")
  18. check(model.get("base_url", "").startswith("http://"),
  19. "the seeded base URL is visible on the page")
  20. # The save action rewrites the row: the next assignment carries the
  21. # DATABASE's endpoint, not the environment's.
  22. task_id = create_queued(auth, {
  23. "project_id": "1", "title": "Model config round trip",
  24. "priority": "2"})
  25. set_model_config(base_url="http://127.0.0.1:9", model="db-model",
  26. api_key="db-key-12345678")
  27. ask_status, assignment = fake.ask_now()
  28. check(ask_status == 200 and int(assignment["task"]["id"]) == task_id,
  29. "the ask claims the task under the database configuration")
  30. llm = assignment.get("llm_config") or {}
  31. check(llm.get("base_url") == "http://127.0.0.1:9"
  32. and llm.get("model") == "db-model"
  33. and llm.get("api_key") == "db-key-12345678",
  34. "the assignment's llm_config carries the database endpoint")
  35. # close the round so the fake saddle is not left busy (a busy
  36. # saddle's ask 204s for its OWN open session, which would mask the
  37. # configuration gate the later checks isolate)
  38. fake.state = "done"
  39. fake.push_results("done", summary="# done",
  40. session_id=assignment.get("session_id"))
  41. wait_for(lambda: task_state(auth, task_id).get("status") == "done"
  42. or False, timeout=30, what="model config task done")
  43. # The page's own state reflects the row (the key is never echoed —
  44. # only that one is set plus a masked hint).
  45. model = slot_public(auth, "/models", "model")
  46. check(model.get("base_url") == "http://127.0.0.1:9"
  47. and model.get("model") == "db-model",
  48. "the page shows the saved endpoint fields")
  49. check(model.get("api_key_set") is True
  50. and "db-key-12345678" not in json.dumps(model),
  51. "the page never echoes the stored key (set flag + masked hint only)")
  52. check(model.get("api_key_hint", "").endswith("5678")
  53. and len(model.get("api_key_hint", "")) <= 5,
  54. "the masked hint carries only the key's tail")
  55. # A save with a BLANK api key keeps the stored one (the page's
  56. # write-only contract); a new base URL and model still land.
  57. model = slot_public(auth, "/models", "model")
  58. act(auth, model["save"], {
  59. "base_url": "http://127.0.0.1:10", "model": "db-model-2",
  60. "api_key": "", "advisor_base_url": "", "advisor_model": "",
  61. "advisor_api_key": ""})
  62. conn = db()
  63. row = conn.execute(
  64. "select base_url, model, api_key from model_config where id = 1").fetchone()
  65. conn.close()
  66. check(row is not None and row[0] == "http://127.0.0.1:10" and row[1] == "db-model-2",
  67. "the save action persists the new endpoint fields")
  68. check(row is not None and row[2] == "db-key-12345678",
  69. "a blank key field KEEPS the stored key")
  70. # The advisor override resolves independently: an advisor base URL
  71. # set through the page overrides, empty falls back to the main
  72. # fields (observable through the advisor-configured flags).
  73. set_model_config(advisor_base_url="http://127.0.0.1:11", advisor_model="adv")
  74. model = slot_public(auth, "/models", "model")
  75. check(model.get("advisor_base_url") == "http://127.0.0.1:11"
  76. and model.get("advisor_model") == "adv",
  77. "the advisor override shows on the page")
  78. set_model_config(advisor_base_url="")
  79. model = slot_public(auth, "/models", "model")
  80. check(model.get("advisor_base_url") == "",
  81. "clearing the advisor override falls back to the main endpoint")
  82. # Validation: a non-http base URL is refused and keeps the row.
  83. model = slot_public(auth, "/models", "model")
  84. directives = act(auth, model["save"], {
  85. "base_url": "ftp://nope", "model": "", "api_key": "",
  86. "advisor_base_url": "", "advisor_model": "", "advisor_api_key": ""})
  87. notified = json.dumps(directives)
  88. check("http://" in notified or "error" in notified,
  89. "a non-http base URL is rejected with a notice")
  90. conn = db()
  91. row = conn.execute("select base_url from model_config where id = 1").fetchone()
  92. conn.close()
  93. check(row is not None and row[0] == "http://127.0.0.1:10",
  94. "the rejected save left the stored configuration untouched")
  95. # Emptying the base URL pauses dispatch: a fresh queued task finds
  96. # no taker while no model endpoint is configured.
  97. set_model_config(base_url="")
  98. model = slot_public(auth, "/models", "model")
  99. check(model.get("dispatch_ready") is False,
  100. "the page reports dispatch paused with no base URL")
  101. paused_task = create_queued(auth, {
  102. "project_id": "1", "title": "Paused until an endpoint exists",
  103. "priority": "1"})
  104. ask_status, assignment = fake.ask_now()
  105. check(ask_status == 204,
  106. "the ask returns 204 while no model endpoint is configured")
  107. # ...and saving one through the PAGE lifts the pause.
  108. model = slot_public(auth, "/models", "model")
  109. act(auth, model["save"], {
  110. "base_url": "http://127.0.0.1:12", "model": "db-model-3",
  111. "api_key": "", "advisor_base_url": "", "advisor_model": "",
  112. "advisor_api_key": ""})
  113. ask_status, assignment = fake.ask_now()
  114. check(ask_status == 200 and int(assignment["task"]["id"]) == paused_task,
  115. "saving an endpoint through the page lifts the dispatch pause")