SIGN IN SIGN UP

fix(llms): translate gateway capabilities in one place (#13584)

* fix(core): stop an empty capability list from stripping image input

`modelHasCapability` documents a missing or empty capability list as
carrying no signal, so each gate declares its own default. Two readers
bypassed it and read `capabilities` directly, where an empty list is not
nullish but `[].includes(x)` is false:

- the session runtime's `modelSupportsImages` metadata used
  `capabilities?.includes("images") ?? true`, so the intended fail-open
  never fired for an empty list and the file-read tool silently dropped
  every image from the request;
- `toProviderModel` projected an empty list onto `false`, telling pickers
  a model definitively lacks vision, attachments, and reasoning when
  nothing had been declared.

Both now route through the shared helpers, which state their unspecified
default explicitly: `modelSupportsImageInput` fails open for a capability
gate, and `declaredCapability` preserves `undefined` for `ProviderModel`'s
tri-state booleans. A populated list stays authoritative in both.

A thinking config now short-circuits `supportsReasoning` instead of being
OR-ed with the capability read, so its absence no longer collapses the
tri-state to `false`.

* fix(llms): translate gateway capabilities in one place

Three producers built gateway model definitions from catalog `ModelInfo`,
and each carried its own hand-written `switch` over the capability list.
Nothing tied them together, so they drifted:

- builtin providers always emitted a capability list, so a model whose
  catalog entry declares no capabilities became `["text"]` where the other
  producers emitted `undefined`. `modelSupportsToolCalling` fails open only
  for an absent or empty list, so that list read as an authoritative denial
  and stripped every tool definition from requests to the affected language
  models (dify, sapaicore, opencode, and the Codex CLI);
- the OpenAI-compatible path mapped an `audio` capability that
  `ModelCapabilitySchema` does not define, while the other two dropped it;
- the pass-through capabilities (`streaming`, `files`, `temperature`, ...)
  were enumerated explicitly in one, folded into `default:` in another,
  and ignored in the third.

One exported `toGatewayModelCapabilities` now serves every producer. It is
built on a `Record<ModelCapability, GatewayModelCapability | null>` rather
than a `switch`, so extending `ModelCapabilitySchema` without deciding the
new capability's mapping fails to compile instead of silently falling
through to a default.

The conformance tests walk the capability state space taken from
`ModelCapabilitySchema` itself and assert the real producers agree with the
translator, so a future producer that maps capabilities on its own fails
even when the translator's own unit tests still pass.

---------

Co-authored-by: Cline Agent <cline-agent@users.noreply.github.com>
Co-authored-by: Ara <arafat.da.khan@gmail.com>
D
Dominic Cooney committed
7a6beb9f0d0f6e4b09747c3e81f1ff0cbf779905
Parent: f5370ad
Committed by GitHub <noreply@github.com> on 8/31/2026, 10:17:31 PM