@miller-tech/uap 1.20.38 → 1.20.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miller-tech/uap",
3
- "version": "1.20.38",
3
+ "version": "1.20.39",
4
4
  "description": "Autonomous AI agent memory system with CLAUDE.md protocol enforcement",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -8206,14 +8206,25 @@ def _parse_anthropic_sse_to_message(raw: bytes) -> dict | None:
8206
8206
 
8207
8207
  @app.get("/v1/models")
8208
8208
  async def models():
8209
- """Return available model list (spoofs Anthropic model IDs for client compatibility)."""
8209
+ """Return available model list.
8210
+
8211
+ Advertises Shannon's three canonical Claude model IDs (haiku 4.5,
8212
+ sonnet 4.6, opus 4.7) for client compatibility — Anthropic SDKs
8213
+ typically check /v1/models for the requested ID before sending a
8214
+ Messages request, and failing that check produces a confusing 404 even
8215
+ though the proxy itself would happily accept the request.
8216
+
8217
+ Whether requests for those Claude IDs actually round-trip to
8218
+ api.anthropic.com depends on ANTHROPIC_PASSTHROUGH_MODELS /
8219
+ DEFAULT_PASSTHROUGH_MODEL_PATTERNS. When the local-only sentinel
8220
+ ANTHROPIC_PASSTHROUGH_MODELS=__local_only__ is set, all IDs (including
8221
+ the Claude ones below) are served by the local llama.cpp backend.
8222
+ """
8210
8223
  return {
8211
8224
  "data": [
8212
- {"id": "claude-opus-4-6-20260101", "object": "model"},
8213
- {"id": "claude-sonnet-4-6-20250514", "object": "model"},
8214
- {"id": "gpt-5.4", "object": "model"},
8215
- {"id": "gpt-5.3-codex", "object": "model"},
8216
- {"id": "claude-opus-4-6-20250616", "object": "model"},
8225
+ {"id": "claude-haiku-4-5-20251001", "object": "model"},
8226
+ {"id": "claude-sonnet-4-6", "object": "model"},
8227
+ {"id": "claude-opus-4-7", "object": "model"},
8217
8228
  {"id": "qwen35-a3b-iq4xs", "object": "model"},
8218
8229
  ]
8219
8230
  }
@@ -5003,3 +5003,57 @@ class TestOpenAIPassthroughConversion(unittest.TestCase):
5003
5003
  self.assertEqual(tc["function"]["name"], "Bash")
5004
5004
  # Arguments are JSON-stringified per OpenAI spec
5005
5005
  self.assertEqual(json.loads(tc["function"]["arguments"]), {"command": "pwd"})
5006
+
5007
+
5008
+ class TestModelsEndpoint(unittest.TestCase):
5009
+ """Tests for the /v1/models discovery endpoint.
5010
+
5011
+ Pins the exact set of model IDs the proxy advertises so an accidental
5012
+ list rewrite that drops a Shannon-required ID fails CI loudly. Driven
5013
+ by Shannon-keygraph's .env defaults (ANTHROPIC_SMALL/MEDIUM/LARGE_MODEL)
5014
+ which expect haiku-4-5, sonnet-4-6, opus-4-7 to be discoverable."""
5015
+
5016
+ def test_models_endpoint_returns_shannon_canonical_set(self):
5017
+ """The advertised list must contain Shannon's canonical Claude IDs
5018
+ plus the local model. Order is not asserted (clients shouldn't
5019
+ depend on it), but membership is."""
5020
+ import asyncio
5021
+ result = asyncio.run(proxy.models())
5022
+
5023
+ self.assertIn("data", result)
5024
+ ids = {entry["id"] for entry in result["data"]}
5025
+
5026
+ # Shannon-required Claude IDs (must include — per project policy)
5027
+ self.assertIn("claude-haiku-4-5-20251001", ids)
5028
+ self.assertIn("claude-sonnet-4-6", ids)
5029
+ self.assertIn("claude-opus-4-7", ids)
5030
+
5031
+ # Local model (kept because requests for it actually route locally
5032
+ # even with __local_only__ passthrough sentinel set)
5033
+ self.assertIn("qwen35-a3b-iq4xs", ids)
5034
+
5035
+ def test_models_endpoint_drops_stale_4_6_dated_variants(self):
5036
+ """The pre-2026-05 list advertised claude-opus-4-6-20260101,
5037
+ claude-sonnet-4-6-20250514, claude-opus-4-6-20250616 and
5038
+ unrelated gpt-5 entries. None of those should reappear: the dated
5039
+ 4-6 variants are superseded by 4-7, and the proxy doesn't route
5040
+ to OpenAI so the gpt-5 entries were noise."""
5041
+ import asyncio
5042
+ result = asyncio.run(proxy.models())
5043
+ ids = {entry["id"] for entry in result["data"]}
5044
+
5045
+ self.assertNotIn("claude-opus-4-6-20260101", ids)
5046
+ self.assertNotIn("claude-sonnet-4-6-20250514", ids)
5047
+ self.assertNotIn("claude-opus-4-6-20250616", ids)
5048
+ self.assertNotIn("gpt-5.4", ids)
5049
+ self.assertNotIn("gpt-5.3-codex", ids)
5050
+
5051
+ def test_models_endpoint_returns_object_model_for_each_entry(self):
5052
+ """Each entry must follow the {id, object: 'model'} shape the
5053
+ Anthropic and OpenAI SDKs both expect."""
5054
+ import asyncio
5055
+ result = asyncio.run(proxy.models())
5056
+
5057
+ for entry in result["data"]:
5058
+ self.assertIn("id", entry)
5059
+ self.assertEqual(entry["object"], "model")