Summary
The opencode backend never reports a served model, so every opencode run reaches a consumer as "model unknown". This is not cosmetic: on the zai-coding-plan/glm-5.2 route the provider serves a different model than the one requested, and the bridge currently has no channel that can carry that fact.
Measured tonight against the live fleet route.
The provider substitutes the model, and we cannot see it
Direct calls to the same endpoint and credential opencode uses (https://api.z.ai/api/coding/paas/v4/chat/completions, zai-coding-plan key from opencode's auth store), reading the response body's model field:
| requested |
served |
glm-5.2 |
glm-5.3 |
glm-5.3 |
glm-5.3 |
glm-5-turbo |
glm-5-turbo |
glm-4.6 |
glm-4.6 |
Only glm-5.2 is substituted, and glm-5.2 is the model our fleet declares. Every run recorded as glm-5.2 was answered by glm-5.3.
Where the identity is lost
-
opencode does not emit it. opencode run --format json -m zai-coding-plan/glm-5.2 (opencode 1.18.18) produced 3 events for a complete turn — step_start, text, step_finish — and zero model, modelID, or providerID keys anywhere in stdout. The step_finish part carries tokens and cost only. opencode's own AssistantMessage type does declare modelID/providerID (@opencode-ai/sdk types.gen.d.ts:108-109), but those are the model opencode selected, not what the provider served, and the CLI's JSON printer omits them regardless.
-
The bridge backend has no assignment. src/backends/opencode.ts never sets ChatDelta.model. Its only uses of model are route parsing (:83, :137) and matches() (:64).
-
The channel exists and is documented. src/backends/types.ts:350-351:
/** Provider-reported response model. This is distinct from the requested route model. */
model?: string
pi populates it; no other backend does.
-
The bridge then echoes the request as if it were the response. src/streaming/sse.ts:235:
model: responseModel ?? model,
and the same fallback in the SSE writer at sse.ts:83 (delta.model ?? meta.model). For opencode the completion body therefore reports opencode/zai-coding-plan/glm-5.2 in the model field, which is what was asked for — while glm-5.3 answered.
-
Consumers correctly refuse it. agent-runtime treats a response model equal to the requested wire model as no evidence at all (isWireModel), leaves turn.usage.model undefined, and fails closed:
Runtime turn did not report the model actually used; refusing to label the response with the requested model
That guard is right and should not be weakened. Fleet-wide the same gap reads as providerModel unknown on 1,701 of 1,747 children (97.4%).
Why the bridge cannot know it today
pi is the only backend with a bridge-owned transport (src/backends/pi-inference-transport.ts, used solely by pi.ts and pi-native-start.ts). opencode reaches z.ai directly from inside the jail using its own auth store, so the bridge never observes the provider response and has nothing to put in ChatDelta.model.
Proposed fix — extend the scoped inference transport to opencode
Same shape as #137 (route openai-codex traffic through the scoped inference proxy), and simpler than pi's case because no credential resolution is required: opencode already sends its own Authorization header, so the forwarder only needs to relay it.
The injection point already exists. opencode's config supports a per-provider base URL (@opencode-ai/sdk types.gen.d.ts:928-944, provider.<id>.options.baseURL), and the backend already materializes a request-scoped opencode.json and points OPENCODE_CONFIG at it (opencode.ts:108-114).
- Start a request-scoped loopback forwarder to the provider's real base URL (reuse
startScopedProxy's shape).
- Write
provider.<id>.options.baseURL into the request-scoped opencode.json.
- Read the response body's
model (and system_fingerprint) on both the JSON and SSE paths; emit it as ChatDelta.model.
- Add the forwarder to the net-jail allowlist, as the pi transport does.
Secondary, and worth fixing either way
sse.ts:235 presents the requested model in the model field when no backend reported one. A consumer that trusts that field is told glm-5.2 answered when glm-5.3 did. Consider carrying the provider-reported identity in a field that is present only when a provider actually reported it — the way system_fingerprint already behaves — so "unknown" stays distinguishable from "confirmed".
Reproduction
printf 'Reply with exactly: OK' | opencode run --format json -m zai-coding-plan/glm-5.2
# 3 events, no model identity in any of them
Environment
- cli-bridge
main @ f51a06f
- opencode 1.18.18
- route
opencode/zai-coding-plan/glm-5.2
Summary
The opencode backend never reports a served model, so every opencode run reaches a consumer as "model unknown". This is not cosmetic: on the
zai-coding-plan/glm-5.2route the provider serves a different model than the one requested, and the bridge currently has no channel that can carry that fact.Measured tonight against the live fleet route.
The provider substitutes the model, and we cannot see it
Direct calls to the same endpoint and credential opencode uses (
https://api.z.ai/api/coding/paas/v4/chat/completions,zai-coding-plankey from opencode's auth store), reading the response body'smodelfield:glm-5.2glm-5.3glm-5.3glm-5.3glm-5-turboglm-5-turboglm-4.6glm-4.6Only
glm-5.2is substituted, andglm-5.2is the model our fleet declares. Every run recorded asglm-5.2was answered byglm-5.3.Where the identity is lost
opencode does not emit it.
opencode run --format json -m zai-coding-plan/glm-5.2(opencode 1.18.18) produced 3 events for a complete turn —step_start,text,step_finish— and zeromodel,modelID, orproviderIDkeys anywhere in stdout. Thestep_finishpart carriestokensandcostonly. opencode's ownAssistantMessagetype does declaremodelID/providerID(@opencode-ai/sdktypes.gen.d.ts:108-109), but those are the model opencode selected, not what the provider served, and the CLI's JSON printer omits them regardless.The bridge backend has no assignment.
src/backends/opencode.tsnever setsChatDelta.model. Its only uses ofmodelare route parsing (:83,:137) andmatches()(:64).The channel exists and is documented.
src/backends/types.ts:350-351:pipopulates it; no other backend does.The bridge then echoes the request as if it were the response.
src/streaming/sse.ts:235:and the same fallback in the SSE writer at
sse.ts:83(delta.model ?? meta.model). For opencode the completion body therefore reportsopencode/zai-coding-plan/glm-5.2in themodelfield, which is what was asked for — whileglm-5.3answered.Consumers correctly refuse it. agent-runtime treats a response model equal to the requested wire model as no evidence at all (
isWireModel), leavesturn.usage.modelundefined, and fails closed:That guard is right and should not be weakened. Fleet-wide the same gap reads as
providerModelunknown on 1,701 of 1,747 children (97.4%).Why the bridge cannot know it today
piis the only backend with a bridge-owned transport (src/backends/pi-inference-transport.ts, used solely bypi.tsandpi-native-start.ts). opencode reaches z.ai directly from inside the jail using its own auth store, so the bridge never observes the provider response and has nothing to put inChatDelta.model.Proposed fix — extend the scoped inference transport to opencode
Same shape as #137 (
route openai-codex traffic through the scoped inference proxy), and simpler than pi's case because no credential resolution is required: opencode already sends its ownAuthorizationheader, so the forwarder only needs to relay it.The injection point already exists. opencode's config supports a per-provider base URL (
@opencode-ai/sdktypes.gen.d.ts:928-944,provider.<id>.options.baseURL), and the backend already materializes a request-scopedopencode.jsonand pointsOPENCODE_CONFIGat it (opencode.ts:108-114).startScopedProxy's shape).provider.<id>.options.baseURLinto the request-scopedopencode.json.model(andsystem_fingerprint) on both the JSON and SSE paths; emit it asChatDelta.model.Secondary, and worth fixing either way
sse.ts:235presents the requested model in themodelfield when no backend reported one. A consumer that trusts that field is toldglm-5.2answered whenglm-5.3did. Consider carrying the provider-reported identity in a field that is present only when a provider actually reported it — the waysystem_fingerprintalready behaves — so "unknown" stays distinguishable from "confirmed".Reproduction
Environment
main@f51a06fopencode/zai-coding-plan/glm-5.2