The no-auth OpenCode provider has id "opencode" and alias "oc". The combo
builder built qualifiedModel from the provider id ("opencode/big-pickle"), but
parseModel("opencode/...") resolves to the opencode-zen api-key tier via a
manual ALIAS_TO_PROVIDER_ID override — not the no-auth "opencode" provider.
"oc/<model>" resolves correctly.
Rewrite qualifiedModel to the provider alias for no-auth providers (only when
the alias differs from the id), keeping providerId for getModelIsHidden. Aligned
the route test that previously asserted the buggy "opencode/" prefix.
Closes#2901
Codex Desktop injects an image_generation hosted tool into every Responses API
request (even text-only ones). The tool-type validator threw
unsupportedFeature() (400) for it, breaking every Codex Desktop request.
Mirror the tool_search handling: add IMAGE_GENERATION_TOOL_TYPES, allow it past
the validator guard, and drop it from the tools array before forwarding to Chat
Completions.
Closes#2950
The github provider has format:"openai" (baseUrl .../chat/completions) plus a
separate responsesBaseUrl (.../responses); a model only uses the Responses API
when it sets targetFormat:"openai-responses". GitHub Copilot's Responses API
does not serve Claude/Gemini models, so claude-opus-4.7, claude-opus-4-5-20251101,
gemini-3.1-pro-preview and gemini-3-flash-preview failed with [400]. The working
claude-opus-4.6 carries no targetFormat and uses chat/completions.
Drop targetFormat:"openai-responses" from those four Claude/Gemini entries so
they use the provider default. Native OpenAI gpt-* models (and oswe-vscode-prime)
keep the Responses API.
Closes#2911
big-pickle's OpenCode/Zen upstream runs DeepSeek thinking mode, but the model
id reveals no DeepSeek signal, so requiresReasoningReplay (called with
allowLegacyFallback:false) never triggered. Follow-up/tool-use turns failed
with [400] 'The reasoning_content in the thinking mode must be passed back to
the API'. Note: requiresReasoningReplay does not consume supportsReasoning, so
the registry flag alone would not have fixed it.
Add RegistryModel.interleavedField (mirrors models.dev interleaved_field),
declare interleavedField:'reasoning_content' (+ supportsReasoning:true) on
big-pickle in both opencode and opencode-zen, and surface the registry value
in getResolvedModelCapabilities so requiresReasoningReplay returns true.
Closes#2900
077_api_key_stream_default_mode.sql and 077_quota_pools.sql both claimed
prefix 077, so getMigrationFiles() threw a version-collision error and
getDbInstance() failed at every startup (app would not boot; all DB-touching
unit tests were red on release/v3.8.8).
Renumber the dependency-free, idempotent quota_pools migration 077 -> 085
(no other migration references quota_pools/quota_allocations), keep the
non-idempotent api_key_stream_default_mode ALTER at 077, add a retroactive
isSchemaAlreadyApplied guard (case 085) for DBs that already applied it under
077, and add a regression test enforcing unique migration prefixes.
- Move !enforceScopes guard before MCP_TOOL_MAP lookup in evaluateToolScopes()
- Add inlineScopes parameter for dynamic tool scope resolution
- Add scopes to all 33 dynamic tool definitions across 5 tool files
- Wire toolDef.scopes through withScopeEnforcement in server.ts
- Preserves existing behavior: tool_definition_missing returned when
enforceScopes=true and no scopes found anywhere
The agy provider was registered in providerRegistry.ts with
executor: "antigravity" but the executor map in executors/index.ts
only had an "antigravity" entry. getExecutor("agy") fell through to
DefaultExecutor, which returned undefined for baseUrl (agy only has
baseUrls), causing fetch(undefined) → TypeError: Cannot read properties
of undefined (reading 'toString').
Closesdiegosouzapw/OmniRoute#2932
5 new test files covering all 13 changed production files:
- estimateSizeFast.test.ts: 16 tests for fast size estimator (circular ref
protection, early exit, nested structures, Map safety)
- eviction-guards-apiKeyRotator.test.ts: 5 tests for Map eviction guards
(!has() check prevents evicting existing keys on update)
- eviction-guards-codexQuotaFetcher.test.ts: 4 tests for connectionRegistry
and quotaCache eviction guards
- rateLimitManager-idle-eviction.test.ts: 6 tests for idle limiter cleanup,
limiterLastUsed tracking, and shutdown behavior
- registry-direct-exports.test.ts: 20 tests verifying all 8 registries export
plain objects (no Proxy traps, no lazy getters, mutable entries)
Extract estimateSizeFast/isSmallEnoughForSemanticCache into standalone
open-sse/utils/estimateSize.ts to make them testable without importing
the entire chatCore.ts dependency tree.
- estimateSizeFast: add WeakSet cycle detection to prevent infinite
loop on circular object references
- trace(): wrap JSON.stringify(extra) in try-catch to handle BigInt,
circular refs, or other non-serializable values gracefully
- Registry API change (Comment 3): verified all callers already use
new getter functions — no broken call sites
- Add !has(key) guard before eviction to avoid evicting entries
that are about to be updated (combo.ts, apiKeyRotator.ts,
codexQuotaFetcher.ts)
- Use optional chaining for provider?.toUpperCase() null safety
- Replace Object.values() with for-in in estimateSizeFast hot path
Root cause: model string provider prefix (e.g. "xiaomi" from
"xiaomi/mimo-v2-flash") differs from the credential DB provider ID
(e.g. "opengate") when a combo target has a custom providerId. The
pre-selection and execution flows both looked up credentials using the
model-inferred provider, which didn't match any DB entry.
Fix A (chat.ts): checkModelAvailable now uses target.providerId when
available, falling back to modelInfo.provider.
Fix B (chat.ts): handleSingleModelChat now accepts runtimeOptions.providerId
and preferentially uses it for credential lookup instead of re-resolving
the provider from the model string.
Fix C (model.ts): add "xiaomi" alias for "xiaomi-mimo" so direct
(non-combo) model requests to xiaomi/mimo-* also resolve correctly.
The CPA executor now cloaks non-Claude-Code tool names (not just mcp_*), so the
prior "does not rewrite non-mcp_ tool names" assertion no longer holds:
my_tool is aliased to MyTool and restored on the response via _toolNameMap.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Addresses confirmed findings from an adversarial review of the prior commits:
- schema sanitizer: a truncation placeholder in a SCALAR annotation keyword
(description/title/pattern/format) was coerced to {}, which is itself invalid
draft-2020-12 and re-triggered the exact "input_schema is invalid" 400 the
sanitizer exists to prevent. Placeholders are now only coerced to {} in
subschema-expecting positions; scalar keywords are left untouched.
- schema sanitizer: numeric-string coercion is folded into
stripInvalidSchemaConstructs so it also covers contains / propertyNames /
additionalItems (which coerceSchemaNumericFields never visited).
- schema sanitizer: stop stripping the valid `default` keyword on the Claude
native/passthrough surface (the #1782 default-strip is a translator concern;
tool schemas here were previously forwarded verbatim). sanitizeClaudeToolSchema
is now a single stripInvalidSchemaConstructs pass.
- tool-name cloak: consult TOOL_RENAME_MAP / EXTRA_TOOL_RENAME_MAP before the
generic PascalCase fallback, so the CLIProxyAPI path uses the established
fingerprint-evasion aliases (subagents->SubDispatch, session_status->CheckStatus,
webfetch->WebFetch, ...) identically to the native path instead of weaker
first-letter casing.
- kill-switch: CLAUDE_DISABLE_TOOL_NAME_CLOAK is now honoured inside
cloakThirdPartyToolNames, so BOTH the native and CLIProxyAPI executor paths
respect it (previously only base.ts did); .env.example + ENVIRONMENT.md updated.
Regression tests added for each. Verified end-to-end through the live CPA path:
mixture_of_agents, subagents, and a tool carrying placeholder descriptions and
`default` values all return 200 with original names restored on the response.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The native Claude OAuth guard in executors/base.ts is bypassed when
`upstream_proxy_config.mode = cliproxyapi` routes the request through the
CliproxyAPI executor — it has its own execute()/transformRequest() and never
reaches BaseExecutor.execute(), so the cloak/sanitizer never ran for that
(common) deployment. Wire the same guards into
CliproxyapiExecutor.transformRequest (Anthropic-shape branch), composing with
the existing bisected `mcp_*` reserved-namespace rewrite:
- sanitizeClaudeToolSchemas() on transformed.tools.
- cloakThirdPartyToolNames() with skip = mcp-reserved, so applyMcpToolNameRewrite
keeps authority over `mcp_*` (its bisected `Mcp_X` form) and the two reverse
maps stay disjoint / single-hop. Both merge into the non-enumerable
_toolNameMap the response stream already uses to restore the caller's names.
cloakThirdPartyToolNames is now non-mutating (clones changed entries) to respect
transformRequest's no-input-mutation contract, and takes an optional `skip`
predicate.
Verified end-to-end through the live CPA path: a real ~100-tool harness payload
that returned the "out of extra usage" placeholder now returns 200 with original
tool names restored on the response stream; `mcp_*` tools and genuine PascalCase
Claude Code tools are unaffected.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Follow-up commit on PR #2943 review:
- Preserve boolean schemas in `sanitizeClaudeToolSchemas` (Gemini Code Assist,
high severity). `additionalProperties: false` is the canonical JSON Schema
lock-down for object tools; the previous coercion silently turned it into the
permissive `{}`, which would invite models to hallucinate extra arguments
during tool calling. Same rule now applies to per-property boolean schemas
under `properties`. Placeholder strings still get the permissive `{}` slot —
booleans get preserved verbatim.
- Defensive null guards in `cloakThirdPartyToolNames` for `tools[]` and
`messages[]` entries that might be `null`/`undefined`. Prevents a runtime
`TypeError` if a malformed payload reaches the cloak.
- Document `CLAUDE_DISABLE_TOOL_NAME_CLOAK` in `.env.example` and
`docs/reference/ENVIRONMENT.md` (env/docs contract was failing in CI).
- Regression tests covering all of the above (5 boolean preservation cases,
2 null-tolerance cases).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extract 3 high-value CPU/RAM optimizations from perf branch:
1. estimateSizeFast() — fast object-tree size estimator replacing
JSON.stringify().length in isSmallEnoughForSemanticCache(). Walks
object tree with a stack, zero string allocation, early exit at 256KB.
2. Consolidate settings reads — move getCachedSettings() to a single
early read in handleChatCore(), eliminating a redundant second read
200 lines later. Also removes the isDetailedLoggingEnabled() wrapper
call (reads settings internally) in favor of direct field check.
3. Registry Proxy→direct export — convert 8 registries from lazy
Proxy+getOrCreate pattern to simple exported const objects. Eliminates
Proxy trap overhead on every provider property access during routing.
Affected: audio, embedding, image, moderation, music, rerank, search,
video registries (-451 lines of Proxy boilerplate).
These changes are independent of the CPU leak fix (limiter eviction)
and complement it by reducing per-request CPU overhead.