Skip to content

fix(opencode): report the served model, not the requested one (z.ai serves glm-5.3 for glm-5.2) #175

Description

@drewstone

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

  1. 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.

  2. 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).

  3. 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.

  4. 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.

  5. 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).

  1. Start a request-scoped loopback forwarder to the provider's real base URL (reuse startScopedProxy's shape).
  2. Write provider.<id>.options.baseURL into the request-scoped opencode.json.
  3. Read the response body's model (and system_fingerprint) on both the JSON and SSE paths; emit it as ChatDelta.model.
  4. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions