PR #3422 ('allow OpenAI usage-only empty choices chunks') reintroduced the
assistant-content injection '[OmniRoute] Upstream returned an empty response.
Please retry.' for empty `choices: []` chunks that carry no valid usage. Clients
(Goose/opencode) feed that text back as a turn and spin in a retry loop -- the
exact regression #3400 had fixed by dropping the chunk.
Restore the drop behavior for the no-usage case while preserving #3422's
standards-compliant forwarding of usage-only `include_usage` final chunks.
Realign the mislabeled stream-utils test (it asserted the injection) and add a
dedicated regression guard.
Reported-by: @mochizzan
Refs: #3502, #3388, #3400, #3422
Codex's model-catalog refresh (codex_models_manager) does
GET /v1/models?client_version=<v> and decodes a JSON object with a
TOP-LEVEL `models` array. OmniRoute answers in the OpenAI-standard
`{object,data}` shape, so codex fails with "missing field `models`"
and logs "failed to refresh available models" on every startup.
Detect codex clients via the `originator` / `user-agent` = `codex_*`
headers they send and add an EMPTY top-level `models: []` so the decode
succeeds. Non-codex OpenAI clients keep the byte-identical `{object,data}`
response.
The array is intentionally empty: codex replaces its built-in per-model
agent prompt (`base_instructions`, ~21k chars) with whatever a populated
entry carries for the selected model, so emitting our catalog would drop
the agent prompt to nothing and break codex's agent behaviour (verified
empirically against codex 0.137). An empty list keeps codex on its
built-in model info — same inference as before, minus the error.
Validated end-to-end with the real handler against codex 0.137:
"failed to refresh available models" → 0 occurrences, instructions
preserved (built-in Codex agent prompt, not empty).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The custom-models loop in getUnifiedModelsResponse gated every model through hasEligibleConnectionForModel(getConnectionsForProvider(...)). no-auth providers (theoldllm, etc.) never create DB connection rows, so that returned [] and the gate dropped every imported/custom model for them — the Playground dropdown showed nothing for imported models while built-in/custom models on auth providers worked. Built-in models survived because they go through providerSupportsModel(), which already has a no-auth bypass (#2798).
The custom-model gate now applies the same no-auth bypass, keeping the eligibility check (with parentProviderType) intact for auth providers.
Co-authored-by: tjengbudi <tjengbudi@users.noreply.github.com>
Co-authored-by: a2belugin <a2belugin@users.noreply.github.com>
Claude Code -> claude-opus-4-8 turns intermittently died with 'tool call could not be parsed (retry also failed)'. OmniRoute's claude identity cloak rebuilt the anthropic-beta header from scratch and unconditionally forced interleaved-thinking-2025-05-14 (+ advanced-tool-use / effort for heavy agents), even when the client never negotiated them. The forced interleaved-thinking conflicts with tool_choice-forced turns, producing malformed opus tool_use streams (and sibling 400 'Thinking may not be enabled when tool_choice forces tool use').
selectBetaFlags now takes the client's inbound anthropic-beta: when present, thinking/effort betas are only emitted if the client requested them. Opaque clients (no header — the OAuth cloak path) keep the full set unchanged, so existing behavior and the #2454 model-tier gating are preserved.
Co-authored-by: Forcerecon <Forcerecon@users.noreply.github.com>
Vertex AI's FunctionCall/FunctionResponse protos have no id field; emitting it made Vertex reject tool calls with 400 'Unknown name id'. The id is now stripped only when the routed provider is vertex/vertex-partner (threaded via credentials._provider), preserving it for the public Gemini API where Gemini 3+ uses it for signature matching.
Co-authored-by: nullbytef0x <nullbytef0x@users.noreply.github.com>
- Unit/integration tests: update hardcoded 42→43 in 7 test files
(agentSkillTools-mcp, agentSkills-catalog, agentSkills-generator,
agent-skills-content, agent-skills-discovery, listCapabilities-a2a)
to match the 43rd skill (config-codex-cli) added in the previous commit.
- Include CONFIG_SKILL_IDS in integration content test ALL_IDS so
skills/config-codex-cli/ is no longer "unexpected".
- listCapabilities.ts: change totalSkills from literal 42 to catalog.length
so it adapts to catalog growth automatically.
- computeCoverage assertions: include config.have in totalSkills check.
- CI: switch E2E artifact from upload-artifact path (ambiguous stripping)
to explicit tar archive. Fixes "Could not find a production build in
./.build/next" — the previous approach's download path was double-nested
(.build/next/next/...) due to upload-artifact LCA computation. tar -czf
stores .build/next/... relative to CWD; tar -xzf restores them verbatim.
- Also exclude .build/next/cache from the tar to keep archive lean.
- feat(translator): strip client_metadata in Responses→Chat translation
(Mistral 422 extra_forbidden fix); add regression test.
- Remove 429 from PROVIDER_BREAKER_FAILURE_STATUSES; 429 belongs to
connection cooldown, not whole-provider breaker (CLAUDE.md §resilience).
PR #3366 correctly added 429 to PROVIDER_FAILURE_ERROR_CODES in
accountFallback.ts (combo infinite-retry fix) but the parallel change
to chat.ts was wrong — the integration test from v3.8.10 confirms this.
- Align stream-utils tests to PR #3399 (SYNTHETIC_CLAUDE_EMPTY_RESPONSE_TEXT
→ "", message.content → null) and PR #3355 (malformed tool-call buffer
now emitted as plain text, not suppressed).
- Align services-branch-hardening test to PR #3399 (pinnedModel always
null from applyComboAgentMiddleware; server-side session pinning replaced
client-side <omniModel> tag extraction).
- Align combo-routing-engine context-cache tests to PR #3399 (no <omniModel>
tag in output, no X-OmniRoute-Model header, priority routing unchanged).