Resolve conflicts:
- streamingHandler.js: adopt upstreamResponseHeaders while keeping 0-token detail row avoidance
- capabilities.js: preserve user-asserted caps and globalThis slots without local caching of catalogSource
- AddCustomModelModal.js & providers/[id]/page.js: wire STT transport marker with custom model edits/assertions
- models/custom/route.js & aliasRepo.js: persist custom model transport and invalidate user caps
- usageRepo.js: key byApiKey live stats by full API key and keep tail in maskApiKey
- UsageStats.js: lazy load charts dynamically
Addresses #4006 by letting Gemini STT models that only exist on the Live
API transcribe instead of failing.
transcribeGemini sends audio to :generateContent and that is the only Gemini
path. A model that is realtime-only (exposed by the Live API's
bidiGenerateContent WebSocket) therefore fails outright, even though the account
can transcribe it.
open-sse/handlers/geminiLiveStt.js owns the WebSocket lifecycle: opens
:bidiGenerateContent, sends setup frame, waits for setupComplete, streams
audio as realtimeInput media chunks, and settles on turnComplete.
Dispatch is driven by transport marker 'gemini-live'. Adds custom model transport
persistence and selection on the dashboard.
New /v1/systemone pass-through route for Jev decision models (jev-1.13,
jev-1.13-free) on OpenCode Zen and the free lane. Follows the media-route
pattern: systemoneConfig in the registry drives URL/headers, the handler
mirrors the embeddings account-fallback + usage flow, and the dashboard
gains a System One media-provider kind. No chat-pipeline changes.
Co-Authored-By: Claude Code <noreply@anthropic.com>
- Match code 110 (billing daily count exceeded) alongside 112/10605/pricingUrl
in isBillingBlock, parsing JSON safely and accepting numeric/string codes
- Accept numeric strings for statusCodeValue and object bodies in envelope peek
- Emit structured 403 quota error chunk instead of synthetic assistant text
when a billing envelope appears mid-stream
- Preserve upstream HTTP status in handleForcedSSEToJson when error chunk carries
a valid 400-599 status
- Add unit tests for code-110 detection, mid-stream billing envelopes, and false-positive guard
Cursor-hosted models (cu/composer-2.5, cu/cursor-grok-*, cu/default) returned
HTTP 200 with an empty turn, or hung, whenever a client sent tools.
- Fold system prompts into the current user message. custom_system_prompt
(RunRequest field 8) makes AgentService return an empty turn.
- Send ModelDetails (field 3); thinking variants (Composer, Grok, *-thinking)
return an empty turn when only requested_model (field 9) is set.
- Route tool-call history and declared tool schemas through AgentService:
encode OpenAI tools into mcp_tools (field 4), decode McpArgs and emit real
tool_calls with finish_reason tool_calls.
- Map Composer thinking / Grok thinking_delta (field 4) into visible content
instead of dropping the answer with the unsigned reasoning.
- Ack request_context without echoing MCP tools (double-advertise stalls the
HTTP/2 stream) and ack kv_server_message so the run proceeds.
- Reject IDE builtin execs instead of failing the turn, so the model can
continue with MCP tools or a text answer.
- Add google.protobuf.Value / MCP encoders and a FIXED64 branch to
encodeField in cursorProtobuf.js.
RTK now compresses the source-format body before translation for cursor only:
its translator rewrites role:tool into user XML, so the post-translate pass
missed those tool results. Every other provider keeps the post-translate pass
unchanged.
Replace the retired api-inference.huggingface.co host with the Inference
Providers router (router.huggingface.co): imageConfig.modelMap resolves
Hub ids to provider-resolved ids, image-to-image models receive the
source image in inputs with the prompt under parameters.prompt, and a
new sttConfig wires the hf-inference ASR route. The image catalog grows
to 23 models, dead whisper-small is replaced by whisper-large-v3-turbo,
the unusable "language" param is dropped, and edit models declare the
edit capability so the dashboard offers a source image. Adds unit and
end-to-end coverage plus a model-id guard on custom endpoints.
Free-tier Zen models reject Responses requests with 403 FreeTierError
when client tools are present but the fingerprint quartet is missing.
Apply the fingerprint tools to every OpenCode request, canonicalise
case variants of the quartet (Bash->bash) without duplication, and
restore the caller's original spellings on the response side via a
request-local WeakMap threaded through the existing toolNameMap.
Resolve conflicts:
- package.json / cli/package.json: take 0.5.75
- .gitignore: union both sides (upstream 9router-*/temp files + local state dirs)
- CHANGELOG.md: keep both blocks, v0.5.75 above v0.5.70
- nonStreamingHandler.js: merge imports (unwrapClineEnvelope +
tokensForDetail/shouldPersistRequestDetail); drop dead appendRequestLog
- providers/[id]/page.js: union useState blocks (compatible-model states
+ importingClineModels)
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Nested combos (comboA lists comboB, comboC, …) now stay one slot each:
the inner combo always runs as fallback to produce a single answer.
Failed hops are no longer written to Details/usage, and streaming no
longer inserts a 0-token placeholder row.
- chat.js: comboStack cycle detection; nested combos forced to fallback;
persistUsage="success-only" for combo hops
- combo.js: discardResponse() cancels unused bodies (fusion timeout /
fallback) so dropped streams fire onStreamComplete; getComboModelsFromData
keeps nested names and honors enabled=false
- requestDetail.js: tokensForDetail() canonicalizes Claude/Gemini usage;
shouldPersistRequestDetail() skips streaming-start and non-success hops
- streamingHandler.js: drop the 0-token streaming placeholder write
- RequestDetailsTab.js: read Gemini/Claude token names; show "streaming"
status in amber
- tests: add combo-nested.test.js (13 cases)
- gitignore: ignore local .vitest/ artifacts
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
A stream that stalled or lost its upstream was closed with no terminal frame
at all, so clients saw "200 OK, a few chunks, then nothing" and could not tell
a truncated reply from a finished one. The Responses passthrough path already
synthesized response.failed; every other client format got nothing.
The watchdog now hands its reason ("stream stall timeout" or "upstream
connection lost") to onAbortTerminal, and buildStreamErrorBytes frames it per
client format: OpenAI-compatible clients get data: {"error":{...}} followed by
data: [DONE], Anthropic clients get `event: error`. The error frame always
precedes [DONE] (openai-python raises APIError on any data payload carrying an
error key), and no synthetic finish_reason is ever emitted — a truncated
stream must not look like a clean stop.
Co-Authored-By: Claude Code <noreply@anthropic.com>
A Vertex job id is a base64url-encoded operation name, and base64url decoding
accepts arbitrary bytes without throwing, so the previous decode-and-split
check let a crafted id splice a path traversal into the fetch URL while the
Bearer token stayed attached — e.g. "..%2F..%2Fevil" resolved to
/v1/evil:fetchPredictOperation on the Vertex host. body.model had the same
shape on the create path, where it is interpolated into the URL unescaped.
decodeJobId now requires a charset-only id, a byte-for-byte round-trip, and a
decoded name matching ^projects/{p}/locations/{l}/publishers/{pub}/models/{m}/operations/{op}$
— no field may contain "/", so ".." can never reach the URL. model ids are
restricted to [A-Za-z0-9._-].
Co-Authored-By: Claude Code <noreply@anthropic.com>
The merged Qoder work also rewrote shared translator/handler code so that
/v1/responses clients got token usage on response.completed. That changed
behaviour for every provider, not just Qoder: proxies saw input tokens
rise by the 2000-token context buffer, and the plain token mapping was
replaced by one that always adds input_tokens_details.
A probe confirms the Qoder benefit does not depend on those edits: the
executor's coalescer already emits one include_usage-style finish chunk, so
a Claude client receives input_tokens and cache_read_input_tokens with
every shared file at its original state. Only the Responses path relies on
the shared translator, and that path has no Qoder-owned seam to put it in.
Reverts the shared files to their pre-PR state and drops the Responses
usage test. The Cline envelope unwrap in nonStreamingHandler.js, which
landed after the PR in the same file, is kept.
defaultClaudeToolType() stamped tools[].type = "custom" onto every
Claude-format request carrying tools since e08ac6da. That satisfied
MiniMax (error 2013) but broke Anthropic-compatible endpoints that only
accept the legacy typeless tool shape. DeepSeek's endpoint
(api.deepseek.com/anthropic/v1/messages) whitelists its tool `type` enum
to the web_search_* variants and answers HTTP 400 "unknown variant
`custom`", so every Claude Code request routed to a DeepSeek connection
failed and surfaced as a persistent 503.
Run the defaulting only when the target provider declares the new
requireClaudeToolType quirk (MiniMax, MiniMax-CN). Add
shouldDefaultClaudeToolType(provider, finalFormat, tools, PROVIDERS) in
translator/concerns/toolCall.js so the gate is unit-testable, and cover
MiniMax keeping the explicit type, DeepSeek/Anthropic staying typeless,
non-Claude formats and tool-less requests never defaulting.
Effectively a no-op for MiniMax and a restore of the pre-e08ac6da
behaviour everywhere else. Another strict gateway now only needs the same
one-line quirk instead of a global behavioural change.
Cline (api.cline.bot) wraps non-stream chat completions in
{"success":true,"data":{...choices...}}, which both the dashboard model-test
ping and the proxy non-stream path read at top level, producing "Provider
returned no completion choices for this model" (#3644). Unwrap the envelope
before usage extraction and response translation; the error envelope
({"success":false,...}) never matches and passes through untouched.
Scoped through `transport.quirks.clineEnvelope` so only cline/clinepass opt
in — no other provider's response body is ever rewritten.
Also adds a live Cline catalog: `fetchClineRawModels()` is shared between
`resolveClineModels()` (full catalog, including free-tier ids such as
z-ai/glm-5.3-flash) and `resolveClinepassModels()` (cline-pass/* only), wired
into /v1/models, the per-provider models route, and the combo selector's
model picker with the static catalog kept as fallback.
Refreshes the dead api-airforce free models (anthropic/claude-3.7-sonnet,
moonshot/kimi-k2.6, google/gemini-2.5-flash) with the live gpt-oss-120b,
gpt-oss-20b and kimi-k2.7-code, plus passthroughModels, forceStream and a
suggested-models filter.
- Coalesce Qoder's empty finish-in-delta frame with the later choices:[] usage
frame so OpenAI and Claude clients receive prompt_tokens, completion_tokens
and cache-hit tokens (the dashboard already saw them)
- Upload inlined images through /api/v2/image/upload like qodercli, and stub
oversized non-image files instead of stuffing 30MB+ data URIs into
agent_chat_generation
- Emit response.completed -> response.usage for chat-native upstreams so
/v1/responses clients (Codex CLI, sub2api) no longer log 0/0/0
- Keep Claude message_delta.usage working when usage arrives without choices[0]
- Escalate to the smallest advertised Qoder context tier (200K/400K/1M) when
the estimated prompt no longer fits max_input_tokens
- Pass apiKey for PAT connections and list hidden enable:false catalog keys
from /v1/models
- Add gpt-image-1.5, gpt-image-2, gpt-image-2.5, gpt-image-2.5-flare and
gpt-image-2.5-sunburst as Codex image models with multi-image support
- Add gpt-image-2.5, gpt-image-2.5-flare and gpt-image-2.5-sunburst to the
OpenAI provider catalog
- Route tool-backed image models through the Codex responses model while
passing the selected model to the image_generation tool, pinning
tool_choice and deriving generate/edit from the presence of references
- Cover the Codex gpt-image-2.5 request shape with a unit test
Video generation was xAI-only. Adds an adapter layer under
open-sse/handlers/videoProviders/ so /v1/videos/* can target OpenRouter or
Google Cloud credentials. A provider with no adapter keeps the exact previous
behaviour (raw body to {baseUrl}/{action}, poll {baseUrl}/{id}, verbatim
passthrough), so the xAI path is unchanged.
- openrouter: async job shape identical to xAI; creation POSTs to the /videos
collection root (no /generations suffix) and the registry HTTP-Referer /
X-Title headers are applied. Bodies pass through verbatim.
- vertex: two-way translation, since Veo does not speak the OpenAI-ish videos
shape. create -> :predictLongRunning { instances[], parameters{} }, poll ->
:fetchPredictOperation (Veo has no REST GET poll). The operation resource
name is base64url-encoded into the job id so GET /v1/videos/{id} stays a
flat path. Access tokens are minted from Service Account JSON via the
existing refreshVertexToken; raw API keys are rejected up front. The
operation response maps back onto the { id, status, video, videos } shape
clients already poll.
- videoCore: the request plan is rebuilt per attempt, so the 401 -> refresh
once -> retry once path picks up the refreshed token. Adapter validation
errors return 400 before any upstream call, so a malformed request can never
create a billable job.
- videoGeneration: GET /v1/videos/{id} resolves the provider from the pinned
x-connection-id connection, then ?provider=, then falls back to the xAI
default.
- registry: openrouter and vertex gain videoConfig, the video serviceKind and
video-kind models (Veo 3.1 / 3 / 2, Sora 2 Pro, Seedance 2.0).
The image handler's `version` header was commented out, so Codex image
requests reached chatgpt.com without the Version identity the backend
expects. Restore it and route every Codex identity header through one
constant.
The CLI version now lives on registry codex.transport as `cliVersion`
(the same pattern gemini-cli uses) and is re-exported as CODEX_CLI_VERSION,
so the registry User-Agent, the image handler and the connection test can
no longer drift apart. Bumped 0.136.0 -> 0.154.0 (current stable).
Co-Authored-By: Claude Code <noreply@anthropic.com>
* handleChatCore() referenced capsOverride (line 162) and
streamErrorPatterns (line 475) but the destructured param list
did not include them. Callers that did not pass these (e.g.
open-sse/handlers/responsesHandler.js, unit callers, older
client builds) would trigger 'capsOverride is not defined' /
'streamErrorPatterns is not defined' ReferenceError mid-request.
* Both defaulted to null. capsOverride is read by capability merge
(line 162, already guarded by '|| {}'). streamErrorPatterns is
read in the early-peek hook (line 484) which was null-safe
only because the variable happened to be in scope when chat.js
spread it in; responsesHandler never passed it and would crash.
* Unblocks all callers regardless of which fields they pass.
* Add /api/usage/request-details/raw endpoint serving a single
stored request detail verbatim (raw payloads), with /raw doc
clarifying it stays gated by the dashboard auth layer.
* Add RawDetailModal opened from a new 'Raw' button in
RequestDetailsTab. Modal loads /raw, exposes per-section copy
buttons and a 'Copy all (JSON)' that bundles every section.
* Capture the raw provider SSE text inside the streaming
transform (cap 64KB) and forward it through
onStreamComplete.rawProviderText so handler stores it as the
providerResponse. response.content stays the extracted user
text. Tool-call-only turns remain so the marker.
* Accumulate from translated client-facing chunks instead of
raw provider shapes so Responses, Claude delta types, and
Gemini/Antigravity parts all contribute.
* Drop redaction from the list endpoint; raw access is now via
the dedicated /raw endpoint. Tests cover the new behavior.
- add a dedicated OpenCode Go executor that always sends x-opencode-session
- preserve a valid caller-provided native OpenCode session header
- translate downstream Agent session IDs into opaque, stable, Agent-scoped IDs
- forward the original provider session seed and client tool on both initial and credential-refresh requests
Non-streaming codex traffic recorded cached_tokens: 0 even when upstream
prompt caching worked. The Claude-format branch (which OpenAI Responses
usage also matches) never read input_tokens_details, and the OpenAI
branch ignored a top-level flat cached_tokens. Read both in both
branches; Responses prompts are cache-inclusive so canonicalizeUsage
passes the value through without folding. 5 new regression tests.
Closes four SSRF guard bypasses reported in #3714:
- Block alternate IPv6 encodings (hex format, NAT64, IPv4-compatible, IPv4-mapped) by parsing to 16-bit groups
- Normalize trailing dots on hostnames to prevent FQDN bypasses
- Add assertPublicUrlResolved() with DNS resolution to block wildcard DNS domains resolving to private/metadata IPs
- Add fetchPublic() to safely handle and validate HTTP redirects
The 3000 ms timeout on /v1/compress was fixed, so busy or slow machines
timed out often and sent the LLM an inconsistently compressed body,
hurting prompt caching. Add a headroomTimeoutMs setting, thread it from
the chat handler down to compressWithHeadroom, expose it in the Token
Saver dashboard, and normalize invalid values back to the 3000 ms default.
Prefer the sourceFormat-matched runtime transport over a model's
declared targetFormat when both apply. MiniMax-M3 previously resolved
to a Claude-shaped body while being posted to the already-selected
OpenAI endpoint, silently dropping image_url blocks from OpenAI
clients. Fixes#3418.
Resolve body.size through sizeToAspectRatio and append the ratio as a
-WxH suffix so the executor's parseImageConfig picks it up. Also fall
back to gemini-3.1-flash-image when a non-image model reaches the
image handler.
The separate zai-search entry showed "No connections" on the web search
page because credentials live on the `glm` connection, not on it. Every
other provider that does both chat and search (antigravity, kimi, xai,
gemini) declares webSearch on the provider itself, so do the same here.
- glm gains serviceKinds ["llm", "webSearch"] and the MCP searchConfig
- the request builder / normalizer move from "zai-search" to "glm"
- drop the zai-search registry entry and its svg logo, which also
removes the only need for svg logo support in getProviderIconSrc
ollama-search keeps its own entry and credentialFallback: its search
endpoint is unrelated to the ollama chat transport.
Register two web search providers that reuse an existing chat provider's
API key instead of requiring their own connection:
- ollama-search (POST ollama.com/api/web_search) reuses the `ollama` key
- zai-search (POST api.z.ai MCP web_search_prime) reuses the `glm` key
A new `credentialFallback` registry field drives this: when a search
provider has no connection of its own, the search handler falls back to
the linked chat provider's credentials.
Also teach getProviderIconSrc to serve .svg logos for providers that
ship vector art.
Strict Anthropic-compatible gateways (e.g. MiniMax) reject Claude-format
requests with HTTP 400 when tools[].type is missing. Normalize each
missing/falsy tools[].type to "custom" before dispatch when the final
request format is Claude. Built-in tool types (computer_use, bash,
web_search_*) are passed through untouched.
Route POST /v1/search with provider "antigravity" through Google Search
grounding on v1internal:generateContent, using the existing Antigravity
OAuth account pool. Grounding chunks become citations with the grounded
sentence as snippet and its surrounding answer text as content.
Upstream repeats a source across chunks, so citations are keyed by URL
and their snippets merged. A missing projectId is reported up front —
upstream answers a fabricated or absent project with a misleading
"no valid license" 403.
Based on the approach in #3437 by @Nautilaceae.
Xquik needs a GET request with x-api-key auth and a tweets envelope
normalizer, neither of which the generic search fallback provides. Adds a
dedicated request builder and normalizer, cursor pagination passthrough,
result-based credit usage reporting, and a validateUrl probe so key
validation hits the no-charge credits endpoint.
- Endpoint: per-API-key model allowlist (schema v3) enforced on chat (403)
and /v1/models; Full-access toggle + multi-select picker in Keys UI.
- Providers: honor x-connection-id in /v1/chat/completions — pinned requests
no longer rotate to another account on failure.
- Providers: strategy saves merge into stored enabled:false override;
Test All groups match grid sections; 1-by-1 skips disabled connections.
- Dashboard: provider-card toggle syncs from server on failure; grid toggles
always visible; connection rows get clear-✕ for stale error banners.
- Combo editor: on desktop (xl+) the Add-Model picker opens as a floating
side panel beside the untouched combo popup instead of stacking on top;
mobile keeps the full-screen overlay.
- Long API-key overflow fixed in key rows + provider model sections.
opencode-go hard-coded targetFormat: claude per model, so every client
format was force-routed to /messages (Codex/OpenAI clients paid a lossy
Responses->OpenAI->Claude double translation). Declare the existing
upstream multi-endpoint transports [openai, claude, openai-responses]
and guard per model via registry supportedFormats: kimi/glm/mimo only
support /chat/completions, minimax/qwen add /messages, deepseek adds
/responses. Undeclared models keep the upstream default.
Drop the bespoke OpenCodeGoExecutor (its shared _lastModel cache could
cross auth headers between concurrent requests); DefaultExecutor already
consumes runtimeTransport and injects reasoning content.
Antigravity and gemini-cli wrap their payload in { response: {...} }.
extractUsageFromResponse only tested top-level usageMetadata, so every
non-streaming antigravity request logged zero usage (IN 0 | OUT 0) and
zeroed rows in the usage dashboard. Read the envelope the same way
usageTracking.js and nonStreamingHandler.js already do; top-level
metadata keeps priority and the OpenAI/Claude branches are untouched.
Fixes#3260
Registry entry plus one config-driven FORMAT_HANDLERS handler. The model id
travels in an HTTP `model` header rather than the JSON body, and the voice is
a reference_id (preset or cloned voice model).
Closes#2411
Passthrough kept the client's own cache_control markers, which point at
pre-normalization offsets. Once normalize/dedupe reshaped system and tools,
the breakpoints landed mid-array and the tail was re-cached every request.
- Pin the last system block and last tool at ttl 1h (was the client's 5m)
- Anchor the last assistant turn at 5m, falling back to the final message
so a first turn still gets a breakpoint
- Fold mid-conversation system messages into the neighbouring user turn
instead of hoisting them into body.system, where the volatile token
counters invalidated the prefix on every request
- Run the anchoring after every token saver, at the final body
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Add apiKey column to requestDetails (schema v2 + migration 002)
- Persist masked API key via buildRequestDetail across chatCore handlers
- Add getRequestDetails apiKey filter + distinct models/apiKeys/statuses helpers
- New /api/usage/filters endpoint returning grouped connections per provider
- Group account dropdown by provider using <optgroup>; add Status column
with success/error badge to the details table
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Add Self-hosted STT/TTS/Embedding providers that read baseUrl per connection
instead of a fixed registry endpoint, so 9Router can point at whisper.cpp,
faster-whisper, Kokoro-FastAPI, llama-server, vLLM, Infinity, and similar
OpenAI-compatible local servers.
Self-hosted Embedding refuses to run without a baseUrl rather than falling
back to api.openai.com like openaiCompatNode does, since that fallback would
silently send input text and the API key to OpenAI under a provider named
"Self-hosted". Also fixes embeddingsCore to catch adapter build errors as a
400 instead of letting them escape uncaught, and bounds the upstream fetch
with FETCH_CONNECT_TIMEOUT_MS to avoid hanging forever on a dead endpoint.
Self-hosted TTS treats a bare model value as the model rather than the voice,
since the generic OpenAI TTS convention (bare = voice) is backwards for a
provider where the model is the variable part.
Codex Responses Lite clients routed to a chat-native OpenAI-compatible
provider lost tool use in three places: non-streaming Chat responses
leaked the raw chat.completion envelope instead of Responses output
items, internal reasoning continuity fields leaked into the outbound
Chat body causing some upstreams to reject the request, and the
Responses to Chat request translator ignored additional_tools,
custom_tool_call, and custom_tool_call_output items entirely.
Also fixes apiType (chat vs responses) for openai-compatible nodes
being resolved from the immutable provider ID instead of the stored
node config, so editing a node's API Type had no runtime effect.
Adds mimo-v2.5-tts as a Media Provider TTS through the existing
OpenAI-compatible chat-completions endpoint. Voice is selected via the
top-level audio.voice field, and an optional style/language hint is
threaded through tts.js -> ttsCore.js -> the new adapter.