@miller-tech/uap 1.212.6 → 1.213.0

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.
Files changed (44) hide show
  1. package/config/model-profiles/qwen38.json +70 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/bin/cli.js +2 -2
  4. package/dist/bin/cli.js.map +1 -1
  5. package/dist/cli/self-harness.js +1 -1
  6. package/dist/cli/self-harness.js.map +1 -1
  7. package/dist/cli/self-tuning.js +1 -1
  8. package/dist/config/settings-registry.js +1 -1
  9. package/dist/config/settings-registry.js.map +1 -1
  10. package/dist/delivery/agentic-executor.d.ts.map +1 -1
  11. package/dist/delivery/agentic-executor.js +5 -1
  12. package/dist/delivery/agentic-executor.js.map +1 -1
  13. package/dist/models/local-model.d.ts +59 -0
  14. package/dist/models/local-model.d.ts.map +1 -0
  15. package/dist/models/local-model.js +123 -0
  16. package/dist/models/local-model.js.map +1 -0
  17. package/dist/models/openai-compat-client.d.ts.map +1 -1
  18. package/dist/models/openai-compat-client.js +6 -1
  19. package/dist/models/openai-compat-client.js.map +1 -1
  20. package/dist/models/profile-map.d.ts.map +1 -1
  21. package/dist/models/profile-map.js +9 -2
  22. package/dist/models/profile-map.js.map +1 -1
  23. package/dist/models/types.d.ts.map +1 -1
  24. package/dist/models/types.js +69 -21
  25. package/dist/models/types.js.map +1 -1
  26. package/dist/self-tuning/profiles/index.d.ts +1 -0
  27. package/dist/self-tuning/profiles/index.d.ts.map +1 -1
  28. package/dist/self-tuning/profiles/index.js +3 -0
  29. package/dist/self-tuning/profiles/index.js.map +1 -1
  30. package/dist/self-tuning/profiles/qwen38.d.ts +23 -0
  31. package/dist/self-tuning/profiles/qwen38.d.ts.map +1 -0
  32. package/dist/self-tuning/profiles/qwen38.js +46 -0
  33. package/dist/self-tuning/profiles/qwen38.js.map +1 -0
  34. package/dist/utils/model-slots.d.ts +17 -1
  35. package/dist/utils/model-slots.d.ts.map +1 -1
  36. package/dist/utils/model-slots.js +25 -3
  37. package/dist/utils/model-slots.js.map +1 -1
  38. package/package.json +2 -2
  39. package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
  40. package/templates/hooks/__pycache__/deliver_autoroute.cpython-312.pyc +0 -0
  41. package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
  42. package/tools/agents/scripts/anthropic_proxy.py +306 -7
  43. package/tools/agents/tests/test_anthropic_proxy_streaming.py +12 -6
  44. package/tools/agents/tests/test_thinking_template_kwargs.py +366 -1
@@ -1162,6 +1162,247 @@ PROXY_DISABLE_THINKING_ON_TOOL_TURNS = os.environ.get(
1162
1162
  PROXY_DISABLE_THINKING_ALWAYS = os.environ.get(
1163
1163
  "PROXY_DISABLE_THINKING_ALWAYS", "off"
1164
1164
  ).lower() not in {"0", "false", "off", "no"}
1165
+ # Does the upstream accept `chat_template_kwargs`?
1166
+ #
1167
+ # It is a llama.cpp `--jinja` concept: the Qwen chat template reads its
1168
+ # `enable_thinking` key, and _set_thinking writes it because the top-level flag
1169
+ # alone is silently overridden there (see that function). It is NOT part of the
1170
+ # OpenAI API, and a backend that never had a jinja template has no reason to
1171
+ # accept it. ninfer-serve (the qwen3.8-27b backend, 2026-08-19) REJECTS it:
1172
+ #
1173
+ # HTTP 400 {"error":{"code":"chat_template_option_not_supported",
1174
+ # "message":"chat_template_kwargs.enable_thinking is not supported",
1175
+ # "param":"chat_template_kwargs","type":"invalid_request_error"}}
1176
+ #
1177
+ # which killed every turn that expressed a thinking intent — the operator
1178
+ # switches, the tool-turn breakers, the JSON-verdict grammar, the empty-response
1179
+ # retry and every assistant prefill/continuation.
1180
+ #
1181
+ # 'auto' (default) probes the upstream once and remembers; 'on'/'off' pin it for
1182
+ # an operator who knows their backend and does not want the probe.
1183
+ PROXY_CHAT_TEMPLATE_KWARGS = os.environ.get(
1184
+ "PROXY_CHAT_TEMPLATE_KWARGS", "auto"
1185
+ ).strip().lower()
1186
+
1187
+ # Tri-state: True = accepted, False = rejected, None = not yet known.
1188
+ _ctk_supported: bool | None = (
1189
+ True
1190
+ if PROXY_CHAT_TEMPLATE_KWARGS in {"1", "true", "on", "yes"}
1191
+ else False
1192
+ if PROXY_CHAT_TEMPLATE_KWARGS in {"0", "false", "off", "no"}
1193
+ else None
1194
+ )
1195
+ # 'auto' resolves on first use; a pin is already resolved and must not re-probe.
1196
+ _ctk_probed = _ctk_supported is not None
1197
+
1198
+
1199
+ def _ctk_allowed() -> bool:
1200
+ """May this request carry `chat_template_kwargs`?
1201
+
1202
+ UNKNOWN COUNTS AS YES. The field is load-bearing on llama.cpp — dropping it
1203
+ there does not fail loudly, it silently restores the template default and
1204
+ every thinking control becomes a no-op again, which is the exact bug
1205
+ _set_thinking was written to fix. A backend that cannot take the field says
1206
+ so with a 400 the probe and the error latch both read, so guessing 'yes' is
1207
+ self-correcting while guessing 'no' is not.
1208
+ """
1209
+ return _ctk_supported is not False
1210
+
1211
+
1212
+ def _note_ctk_rejection(error_text: str) -> bool:
1213
+ """Latch 'unsupported' from an upstream error body. Returns True if it did.
1214
+
1215
+ The probe is the primary mechanism; this is the safety net for the case it
1216
+ cannot cover — an upstream swapped underneath a running proxy. Matches the
1217
+ machine-readable error code AND the human message, because only the code is
1218
+ contractual and only the message is guaranteed to survive a proxy in the
1219
+ middle that reshapes the envelope.
1220
+ """
1221
+ global _ctk_supported, _ctk_probed
1222
+ if _ctk_supported is False or PROXY_CHAT_TEMPLATE_KWARGS in {"1", "true", "on", "yes"}:
1223
+ return False
1224
+ blob = (error_text or "").lower()
1225
+ # The contractual error code is the trustworthy signal. The bare parameter
1226
+ # NAME is not: the proxy puts that exact string in its own outbound body, so
1227
+ # any upstream or middlebox that echoes the request in an error envelope
1228
+ # (LiteLLM does, and one sits in front of this) would latch the field off on
1229
+ # an error that says nothing about capability. Demonstrated in review: a
1230
+ # context-overflow error quoting the payload flipped the latch. Require the
1231
+ # name to co-occur with an actual rejection phrase.
1232
+ named = "chat_template_kwargs" in blob and any(
1233
+ phrase in blob for phrase in ("not supported", "unsupported", "unknown field", "unrecognized")
1234
+ )
1235
+ if "chat_template_option_not_supported" not in blob and not named:
1236
+ return False
1237
+ _ctk_supported = False
1238
+ _ctk_probed = True
1239
+ logger.warning(
1240
+ "CHAT_TEMPLATE_KWARGS: upstream rejected the field - disabling it for "
1241
+ "this process (set PROXY_CHAT_TEMPLATE_KWARGS=on to force it back)"
1242
+ )
1243
+ return True
1244
+
1245
+
1246
+ # A probe that keeps re-arming on inconclusive answers would add an upstream
1247
+ # round-trip to EVERY /v1/messages for the life of the process. Bounded.
1248
+ _CTK_PROBE_MAX_ATTEMPTS = 3
1249
+ _ctk_probe_attempts = 0
1250
+
1251
+
1252
+ async def _probe_chat_template_kwargs() -> None:
1253
+ """Ask the upstream, with the cheapest request that can actually answer.
1254
+
1255
+ A 1-token completion, not a GET: whether the field is accepted is a property
1256
+ of the request parser, and no metadata endpoint reports it. Vendor-sniffing
1257
+ /v1/models `owned_by` is deliberately not used as the ANSWER - it tells you
1258
+ which backend this is, a different question that goes stale every time a new
1259
+ backend appears - but it IS used to learn the model name, because a
1260
+ completion without one never reaches the part of the parser under test:
1261
+
1262
+ {"error":{"code":null,"message":"missing required field: model",
1263
+ "param":"model","type":"invalid_request_error"}}
1264
+
1265
+ which is a 400 that says nothing about chat_template_kwargs. Measured: the
1266
+ first version of this probe omitted `model`, got exactly that, correctly
1267
+ declined to latch on an unrelated error, and so learned nothing on every
1268
+ attempt while re-arming itself each time.
1269
+
1270
+ Any answer that is NOT the specific rejection leaves the state UNKNOWN, so a
1271
+ probe that could not run (upstream down, timeout, auth) never disables a
1272
+ field llama.cpp needs.
1273
+ """
1274
+ global _ctk_supported, _ctk_probed, _ctk_probe_attempts
1275
+ if _ctk_probed or http_client is None:
1276
+ return
1277
+ _ctk_probe_attempts += 1
1278
+ _ctk_probed = True # cleared below only if the answer was inconclusive
1279
+ try:
1280
+ model = await _upstream_model_name()
1281
+ if not model:
1282
+ _ctk_probed = _ctk_probe_attempts >= _CTK_PROBE_MAX_ATTEMPTS
1283
+ return
1284
+ resp = await http_client.post(
1285
+ f"{LLAMA_CPP_BASE}/chat/completions",
1286
+ json={
1287
+ "model": model,
1288
+ "messages": [{"role": "user", "content": "hi"}],
1289
+ "max_tokens": 1,
1290
+ "chat_template_kwargs": {"enable_thinking": False},
1291
+ },
1292
+ timeout=20.0,
1293
+ )
1294
+ if resp.status_code == 200:
1295
+ _ctk_supported = True
1296
+ logger.info("CHAT_TEMPLATE_KWARGS: upstream accepts the field")
1297
+ return
1298
+ if _is_loading_model_503(resp):
1299
+ # The backend is still loading its weights. That is not an attempt
1300
+ # at answering the question -- it is the question not being asked
1301
+ # yet. Counting it is how a proxy started alongside its model server
1302
+ # burns all three tries before the server is even up, then runs the
1303
+ # rest of its life with the answer stuck at "unknown".
1304
+ _ctk_probe_attempts -= 1
1305
+ _ctk_probed = False
1306
+ return
1307
+ if not _note_ctk_rejection(resp.text[:1000]):
1308
+ # Some other error (auth, bad model, overload). It says nothing
1309
+ # about this field. Re-open the question, up to the attempt cap.
1310
+ logger.debug(
1311
+ "CHAT_TEMPLATE_KWARGS probe inconclusive (HTTP %d): %s",
1312
+ resp.status_code,
1313
+ resp.text[:200],
1314
+ )
1315
+ _ctk_probed = _ctk_probe_attempts >= _CTK_PROBE_MAX_ATTEMPTS
1316
+ except Exception as e: # noqa: BLE001 - a probe must never take the proxy down
1317
+ _ctk_probed = _ctk_probe_attempts >= _CTK_PROBE_MAX_ATTEMPTS
1318
+ logger.debug("CHAT_TEMPLATE_KWARGS probe failed: %s", e)
1319
+
1320
+
1321
+ # Model ids the upstream actually answers to, or None when not yet known.
1322
+ _upstream_model_ids: list[str] | None = None
1323
+ # Ids we have already reported as unservable, so the log says it once per id.
1324
+ _rewritten_model_ids: set[str] = set()
1325
+
1326
+
1327
+ async def _upstream_model_ids_cached() -> list[str] | None:
1328
+ """The upstream's advertised model ids, fetched at most once per process."""
1329
+ global _upstream_model_ids
1330
+ if _upstream_model_ids is not None:
1331
+ return _upstream_model_ids
1332
+ if http_client is None:
1333
+ return None
1334
+ try:
1335
+ r = await http_client.get(f"{LLAMA_CPP_BASE}/models", timeout=10.0)
1336
+ if r.status_code != 200:
1337
+ return None
1338
+ data = (r.json() or {}).get("data") or []
1339
+ ids = [str(m["id"]) for m in data if isinstance(m, dict) and m.get("id")]
1340
+ if ids:
1341
+ _upstream_model_ids = ids
1342
+ return ids or None
1343
+ except Exception: # noqa: BLE001 - discovery must never take the proxy down
1344
+ return None
1345
+
1346
+
1347
+ async def _reconcile_wire_model(openai_body: dict) -> None:
1348
+ """Point a locally-served request at a model the backend will answer to.
1349
+
1350
+ THE BUG THIS FIXES, measured 2026-08-19. llama.cpp ignores the OpenAI
1351
+ `model` field entirely, so for years the proxy could forward whatever the
1352
+ client asked for. ninfer-serve VALIDATES it. With the local-only sentinel
1353
+ (ANTHROPIC_PASSTHROUGH_MODELS=__local_only__ -- the shipped systemd default)
1354
+ EVERY advertised id is served locally, including the four `claude-*` ones,
1355
+ and every one of them came back:
1356
+
1357
+ HTTP 404 {"code":"model_not_found","message":"model 'claude-sonnet-4-6' not found"}
1358
+
1359
+ So the whole point of advertising those ids for SDK compatibility was
1360
+ defeated by the backend swap, and so was any `.uap.json` still naming a
1361
+ retired local preset (this repo's own `roles.fallback` is one).
1362
+
1363
+ Only rewrites an id the upstream does NOT list, and only when the upstream
1364
+ told us what it serves. A request naming a model the backend knows is left
1365
+ exactly alone, so a genuine multi-model gateway keeps working; a backend we
1366
+ could not interrogate changes nothing. Fails open in both directions.
1367
+ """
1368
+ requested = openai_body.get("model")
1369
+ if not requested:
1370
+ return
1371
+ ids = await _upstream_model_ids_cached()
1372
+ if not ids or requested in ids:
1373
+ return
1374
+ served = ids[0]
1375
+ openai_body["model"] = served
1376
+ if requested not in _rewritten_model_ids:
1377
+ _rewritten_model_ids.add(requested)
1378
+ logger.warning(
1379
+ "MODEL REWRITE: upstream does not serve %r (it serves %s) - sending %r instead. "
1380
+ "Update the advertised/configured id to stop relying on this.",
1381
+ requested, ", ".join(ids[:4]), served,
1382
+ )
1383
+
1384
+
1385
+ async def _upstream_model_name() -> str | None:
1386
+ """First model id the upstream advertises, or None.
1387
+
1388
+ Only used to make the capability probe well-formed. Never cached as
1389
+ configuration: the proxy's own advertised list is a separate, deliberate
1390
+ choice (ADVERTISED_MODEL_IDS) and must not silently follow the backend.
1391
+ """
1392
+ if http_client is None:
1393
+ return None
1394
+ try:
1395
+ r = await http_client.get(f"{LLAMA_CPP_BASE}/models", timeout=10.0)
1396
+ if r.status_code != 200:
1397
+ return None
1398
+ data = (r.json() or {}).get("data") or []
1399
+ first = data[0] if data else {}
1400
+ mid = first.get("id") if isinstance(first, dict) else None
1401
+ return str(mid) if mid else None
1402
+ except Exception: # noqa: BLE001
1403
+ return None
1404
+
1405
+
1165
1406
  # Force tool_choice='required' on the first turn of a fresh session. Originally
1166
1407
  # Qwen-tuned to break out of cold-start "tries to chat instead of calling a tool"
1167
1408
  # behaviour. Gemma 4 doesn't need this — it routes 'auto' correctly and the
@@ -1576,6 +1817,12 @@ def _set_thinking(body: dict, enabled: bool) -> None:
1576
1817
  # with `dict(openai_body)`, a SHALLOW copy that still shares this dict, so
1577
1818
  # mutating in place would silently disable thinking on the original request
1578
1819
  # too. Copying keeps any other template kwargs the server was launched with.
1820
+ if not _ctk_allowed():
1821
+ # Backend rejects the field (see _ctk_allowed). The top-level flag set
1822
+ # above is all this backend can be told; sending the nested form would
1823
+ # 400 the whole turn rather than merely fail to take effect.
1824
+ body.pop("chat_template_kwargs", None)
1825
+ return
1579
1826
  existing = body.get("chat_template_kwargs")
1580
1827
  ctk = dict(existing) if isinstance(existing, dict) else {}
1581
1828
  ctk["enable_thinking"] = enabled
@@ -7335,10 +7582,18 @@ def build_openai_request(
7335
7582
  # continuation, so disable thinking the way the template actually reads.
7336
7583
  _final_msgs = openai_body.get("messages") or []
7337
7584
  if _final_msgs and isinstance(_final_msgs[-1], dict) and _final_msgs[-1].get("role") == "assistant":
7338
- ctk = openai_body.setdefault("chat_template_kwargs", {})
7339
- if isinstance(ctk, dict):
7340
- ctk["enable_thinking"] = False
7341
- openai_body.pop("enable_thinking", None)
7585
+ if _ctk_allowed():
7586
+ ctk = openai_body.setdefault("chat_template_kwargs", {})
7587
+ if isinstance(ctk, dict):
7588
+ ctk["enable_thinking"] = False
7589
+ openai_body.pop("enable_thinking", None)
7590
+ else:
7591
+ # No jinja template to instruct, so there is no prefill
7592
+ # incompatibility to work around either — the whole reason this
7593
+ # block sets the nested key. Say it the only way this backend
7594
+ # understands and move on.
7595
+ openai_body.pop("chat_template_kwargs", None)
7596
+ openai_body["enable_thinking"] = False
7342
7597
 
7343
7598
  return openai_body
7344
7599
 
@@ -12100,12 +12355,21 @@ async def messages(request: Request):
12100
12355
  monitor.tool_state_auto_budget_remaining = 1
12101
12356
  monitor.reset_completion_recovery()
12102
12357
 
12358
+ # Whether this upstream accepts chat_template_kwargs decides what
12359
+ # _set_thinking is allowed to write, so it must be answered BEFORE the body
12360
+ # is built -- but only here, AFTER the passthrough branch has already
12361
+ # returned. A cloud-only session was otherwise blocking on a capability
12362
+ # probe of a local backend it will never send a token to.
12363
+ await _probe_chat_template_kwargs()
12103
12364
  openai_body = build_openai_request(
12104
12365
  body,
12105
12366
  monitor,
12106
12367
  profile_prompt_suffix=profile_prompt_suffix,
12107
12368
  profile_grammar=profile_grammar,
12108
12369
  )
12370
+ # Only the WIRE body is touched. The Anthropic response still echoes the
12371
+ # model the client asked for, which is what clients check.
12372
+ await _reconcile_wire_model(openai_body)
12109
12373
 
12110
12374
  client = http_client
12111
12375
  if client is None:
@@ -12230,6 +12494,7 @@ async def messages(request: Request):
12230
12494
  strict_resp.status_code,
12231
12495
  error_text,
12232
12496
  )
12497
+ _note_ctk_rejection(error_text)
12233
12498
  return Response(
12234
12499
  content=json.dumps(
12235
12500
  {
@@ -12495,6 +12760,13 @@ async def messages(request: Request):
12495
12760
  error_text = error_body.decode("utf-8", errors="replace")[:1000]
12496
12761
 
12497
12762
  logger.error("Upstream HTTP %d: %s", resp.status_code, error_text)
12763
+ # Streaming is the DEFAULT client path, and it was the one handler
12764
+ # of three that never latched. Without this, a proxy whose probe
12765
+ # came back inconclusive (upstream still loading during the first
12766
+ # three requests) keeps sending a field the backend rejects, on
12767
+ # every thinking-intent turn, for the life of the process -- the
12768
+ # exact permanent breakage this capability check exists to prevent.
12769
+ _note_ctk_rejection(error_text)
12498
12770
 
12499
12771
  # Parse the error for a user-friendly message
12500
12772
  error_message = f"Upstream server error (HTTP {resp.status_code})"
@@ -12656,6 +12928,7 @@ async def messages(request: Request):
12656
12928
  logger.error(
12657
12929
  "Upstream HTTP %d (non-stream): %s", resp.status_code, error_text
12658
12930
  )
12931
+ _note_ctk_rejection(error_text)
12659
12932
  return Response(
12660
12933
  content=json.dumps(
12661
12934
  {
@@ -13080,13 +13353,39 @@ def _parse_anthropic_sse_to_message(raw: bytes) -> dict | None:
13080
13353
  }
13081
13354
 
13082
13355
 
13083
- ADVERTISED_MODEL_IDS = (
13356
+ # The Claude ids are a hand-maintained contract: SDK clients check /v1/models for
13357
+ # the id they are about to request, and a discovery miss is a confusing 404 even
13358
+ # though this proxy would have served it.
13359
+ #
13360
+ # The LOCAL id is NOT hand-maintained any more. It was, through three model
13361
+ # generations, and each one went stale the moment the served model changed --
13362
+ # harmlessly under llama.cpp, which ignores the model field, and then all at once
13363
+ # under a backend that validates it. `_advertised_model_ids()` asks the upstream
13364
+ # what it serves and falls back to this constant only when it cannot answer.
13365
+ ADVERTISED_CLAUDE_MODEL_IDS = (
13084
13366
  "claude-haiku-4-5-20251001",
13085
13367
  "claude-sonnet-4-6",
13086
13368
  "claude-sonnet-5-20250514",
13087
13369
  "claude-fable-5",
13088
- "qwen36-35b-a3b-iq4xs",
13089
13370
  )
13371
+ # Last-resort local id, used only when the upstream cannot be interrogated.
13372
+ FALLBACK_LOCAL_MODEL_ID = "qwen3.8-27b"
13373
+ ADVERTISED_MODEL_IDS = ADVERTISED_CLAUDE_MODEL_IDS + (FALLBACK_LOCAL_MODEL_ID,)
13374
+
13375
+
13376
+ async def _advertised_model_ids() -> tuple[str, ...]:
13377
+ """The Claude contract ids plus whatever the upstream actually serves.
13378
+
13379
+ Advertising a local id the backend does not answer to is worse than
13380
+ advertising nothing: clients trust the list and select the dead id. Asking
13381
+ the backend removes the only place that answer was previously guessed.
13382
+ """
13383
+ served = await _upstream_model_ids_cached()
13384
+ local = tuple(m for m in (served or ()) if m not in ADVERTISED_CLAUDE_MODEL_IDS)
13385
+ # Served model FIRST. A client that reads the list as "what does this server
13386
+ # run" takes data[0], and with the Claude contract ids in front of it that
13387
+ # answer was a Claude id -- measured against this very proxy.
13388
+ return (local or (FALLBACK_LOCAL_MODEL_ID,)) + ADVERTISED_CLAUDE_MODEL_IDS
13090
13389
 
13091
13390
  # Keys OpenAI-compatible clients probe for a model's context window. There is no
13092
13391
  # standard, so emit the common spellings rather than betting on one: hermes reads
@@ -13154,7 +13453,7 @@ async def models():
13154
13453
  # that only refreshes on /v1/messages means the number a client keeps for
13155
13454
  # the whole session is the one from before any traffic existed.
13156
13455
  await _maybe_recheck_context_window()
13157
- return {"data": [_model_entry(mid) for mid in ADVERTISED_MODEL_IDS]}
13456
+ return {"data": [_model_entry(mid) for mid in await _advertised_model_ids()]}
13158
13457
 
13159
13458
 
13160
13459
  @app.get("/health")
@@ -5333,12 +5333,18 @@ class TestModelsEndpoint(unittest.TestCase):
5333
5333
  self.assertIn("claude-sonnet-4-6", ids)
5334
5334
  self.assertIn("claude-sonnet-5-20250514", ids)
5335
5335
 
5336
- # Local model — what llama-server actually serves. Tracks the
5337
- # active model: qwen36-35b-a3b-iq4xs as of the 2026-05-17 switch
5338
- # back to Qwen3.6-35B-A3B MoE from the 27B dense (see
5339
- # project_active_server memory). Requests for this ID route
5340
- # locally even with the __local_only__ passthrough sentinel set.
5341
- self.assertIn("qwen36-35b-a3b-iq4xs", ids)
5336
+ # Local model — what the inference server actually serves. Tracks the
5337
+ # active model: qwen3.8-27b as of 2026-08-19, served by ninfer-serve
5338
+ # (NOT llama.cpp). Requests for this ID route locally even with the
5339
+ # __local_only__ passthrough sentinel set.
5340
+ #
5341
+ # This assertion is about AGREEMENT with the backend, not about naming.
5342
+ # Measured on the day of the switch: the list still advertised
5343
+ # qwen36-35b-a3b-iq4xs, and a request for it came back
5344
+ # HTTP 404 model_not_found — SDKs check /v1/models before sending, so
5345
+ # every client dutifully selected an id nothing would answer to.
5346
+ self.assertIn("qwen3.8-27b", ids)
5347
+ self.assertNotIn("qwen36-35b-a3b-iq4xs", ids)
5342
5348
  self.assertNotIn("qwen36-27b-iq4xs", ids)
5343
5349
  self.assertNotIn("qwen35-a3b-iq4xs", ids)
5344
5350