Files
qwen-code/docs/design/model-api-selection.md
24dd6ddfd4 feat: select the OpenAI wire API per model (#11538)
* feat: select the OpenAI API per model

* fix(config): report an invalid model api as a config error

A typo in a model's `api` threw a plain Error out of the registry
resolver, so it reached handleCriticalError and printed a stack trace.
Every other settings.json validation failure goes through
FatalConfigError, which prints just the message and the "please fix the
configuration file(s)" hint. Because resolution runs inside
loadCliConfig, the TUI never starts either, leaving a one-character
typo with no in-app way back.

Also routes the new API step's labels through t() so they match their
sibling entries, restores the dropped maxTokens TODO, and notes that the
fixed-specs reasoning branch mirrors the editable one that carries the
rationale.

* fix(core): address review findings on per-model OpenAI API selection (#11538)

- skip provider version metadata when an install's effective route differs
  from the provider protocol, so the drift check no longer prompts a
  spurious model-list update on every launch (accepting it duplicated every
  model)
- keep the user's chosen model when a reinstall switches API routes and the
  plan still offers it, while still re-syncing the session onto the new wire
- gate the legacy openai-responses prune on patch ownership so an install
  cannot delete a route owned by another provider's credentials
- keep the prefilled Responses API when re-authenticating a preset provider
  that has no API step, and derive the baseUrl placeholder from the
  effective route so re-authentication matches the stored identity
- classify invalid model `api` values as FatalConfigError on every startup
  shape, including when no model or auth type is selected
- compare effective endpoints for OpenAI sibling credential reuse so a
  baseUrl-less pair sharing the official origin keeps its key across
  switches
- keep the persisted model selection when DELETE /workspace/models removes
  the active route but an API sibling still resolves it

* fix(core): resolve sibling-wire drift, stale version hashes, and tombstone misfires (#11538)

* fix(core): keep the Responses default endpoint and SDK out of the wrong closure (#11538)

Move normalizeOpenAiWireBaseUrl into the dependency-free
openaiContentGenerator/constants leaf so the models barrel no longer
drags @google/genai into the ACP/serve bundle closure (red Lint &
Static check). Derive blank-baseUrl defaults from the effective wire
(raw protocol + api pick) in the ACP, VS Code, and Web Shell auth
flows; keep ACP re-authentication on the session's current wire when
the resolver throws; tolerate malformed legacy buckets and unrelated
invalid api entries on the install path; let ModelsConfig sibling-wire
adoption tolerate default-endpoint representation differences without
dropping the session key, and let setModel follow a model registered
on the sibling wire.

* fix(models): use canonical wireApi and preserve route identity

* fix(models): enforce the provider configuration boundary

* fix(models): compare the retired provider id literally

* fix(models): preserve explicit wire choices across setup and removal

* fix(models): preserve released Responses configurations

* test(cli): align released Responses compatibility expectations

* fix(cli): import core symbols from their defining modules in loadedSettingsAdapter

The branch added a value import of AuthType and
preserveModelProviderPlaceholders from the @qwen-code/qwen-code-core
package root, which the no-restricted-imports rule that landed via the
main merge (#11798) forbids outside the legacy allowlist, failing the
Lint & Static CI job. Import both from the modules that define them;
the deep specifiers resolve through core's exports wildcard.

Co-authored-by: Qwen-Coder <[email protected]>

* fix: preserve model routes and credentials across reconfiguration

Preserve each preset model's API and the saved active route instead of downgrading Responses or broadcasting one wire across mixed models.

Keep credential placeholders tied to their resolved write snapshots, honor first-wins duplicate routes, and refresh writable-scope providers after key rotation.

Validate User model deletion against the pre-load environment and home settings, excluding workspace overlays while preserving valid local selections. Add regressions and synchronize both design documents.

Co-authored-by: Qwen-Coder <[email protected]>

Co-authored-by: Qwen-Coder <[email protected]>

* test(cli): keep provider update settings mocks consistent

Keep a stable User SettingsFile and update both raw and runtime views so provider template reloads observe the persisted settings. Preserve the existing assertions and verify raw model/version consistency.

Co-authored-by: Qwen-Coder <[email protected]>

Co-authored-by: Qwen-Coder <[email protected]>

* fix(models): batch the round-15 review fixes for routes, previews and deletion

- workspace-models: decide the Workspace `model` pair once after both scope
  views are known, only when the workspace owns neither field; tombstone an
  inherited selection that loses its route, and never pin a credential-bearing
  baseUrl into the shareable workspace file (R15-2, R15-3, R15-4)
- setup flow: keep the review preview total by surfacing a planner refusal as
  `previewError` instead of throwing in render, and mask custom header values
  in the display copy only (R15-6, R15-7); reopen Custom Provider on the
  identified saved route in both renderers (R12-4)
- install: evaluate the removal guards on the bucket the patch rewrites and
  keep the route-aware view for purpose changes only; leave dotted bucket ids
  out of the legacy cleanup so adapters cannot dot-split them (R15-10, R15-11)
- acp: resolve an existing provider key by first-wins identity so the resolver
  agrees with the planner (R15-1)
- hot reload: report a failed re-authentication once with reselection
  guidance, without switching routes (R14-7)
- docs: describe the pin/tombstone, preview masking, dotted-bucket and
  hot-reload behaviors in both design documents

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* fix(models): batch the round-16 review fixes for the voice probe, route tests and preview

- install: the voice-conflict probe rebuilds a registry over every bucket, so
  a hand-edited invalid wireApi elsewhere refused an unrelated voice install
  with a bare Error (a 500 instead of the 400 model_purpose_conflict). Drop
  only the entries whose protocol does not resolve before probing; the
  duplicate-voice refusal keeps its modelPurpose step.
- workspace-models tests: pin os.homedir() to the fixture so a contributor's
  ~/.env or ~/.qwen/.env can no longer reach the DELETE route's selection env.
- provider setup preview: render preserved models in the placeholder form the
  writer persists (any field, not just customHeaders) and mask only header
  values that are still literals. Both auth dialogs pass the raw providers
  map through the new getRawModelProviders helper, which the settings adapter
  now shares.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MWe8PKNMpsKdQruTWkvFFc

* test(cli): stub the raw providers lookup in the OpenTUI auth dialog test

The dialog now reads the raw providers map for the review preview, and the
test replaces the settings-adapter module with a hand-written mock that did
not export it, which failed all 16 cases in CI.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MWe8PKNMpsKdQruTWkvFFc

* test(cli): walk main's new OpenTUI dialog tests through the API step

The bracketed-paste and caret-editing cases that landed with #11806 were
written against a custom-provider wizard without the API selection step,
so they stopped one step short and read main's step numbers. They now walk
through the API step, expect 4/7 and 7/7, and count submits through the
install call rather than plan builds, which the review step also runs for
its preview.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MWe8PKNMpsKdQruTWkvFFc

---------

Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: qwen-code-ci-bot <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: Claude Fable 5.1 <[email protected]>
2026-09-17 08:17:50 +00:00

12 KiB

Model-level OpenAI API selection

English | 简体中文

Problem and scope

OpenAI Chat Completions and Responses share credential configuration, but today Qwen Code exposes them as separate authentication choices and provider buckets. Users must change openai to openai-responses to select the request format.

Add wireApi: "chat-completions" | "responses" to each OpenAI-compatible model, beside id, baseUrl, and envKey. Present one OpenAI-compatible provider choice followed by API selection. Keep internal protocol identities and recorded session routes distinct. This is a Qwen Code feature, not a port from another codebase. Native computer-use behavior, reasoning defaults, transport implementations, automatic protocol detection, and automatic credential migration are out of scope.

Configuration contract

{
  "modelProviders": {
    "openai": [
      {
        "id": "gpt-6-astra",
        "wireApi": "responses",
        "envKey": "IDEALAB_API_KEY",
        "baseUrl": "https://gateway.example.com/v1",
        "generationConfig": {
          "reasoning": { "effort": "xhigh" },
          "contextWindowSize": 272000
        }
      }
    ]
  }
}
Provider protocol Model wireApi Effective internal protocol
openai omitted or chat-completions openai
openai responses openai-responses
Custom provider mapped to openai same rules same rules
Released openai-responses (including mapped providers) omitted openai-responses
Released OpenAI-family declarations specified selected wireApi
Other known protocol specified configuration error

Unknown wireApi values are configuration errors. This field cannot make an unknown provider id valid; existing unknown-provider warnings remain. Setup uses one OpenAI credential namespace and writes both APIs under openai. Explicit envKey values retain their meaning within the supported format. wireApi is routing metadata and is never forwarded in request bodies.

Responses shipped in v0.23.3 before this refactor. Keep reading the released modelProviders.openai-responses bucket and providerProtocol mappings to openai-responses, including empty buckets and unused mappings. Explicit provider mappings retain precedence over bucket names; an explicit model wireApi overrides either OpenAI-family protocol. Missing wireApi retains the declared protocol. Loading and hot reload never rewrite settings or add credential references. Existing explicit references and default credential resolution retain their published meaning. The draft field api is not an alias.

New setup writes openai plus per-model wireApi. Reconfiguration preserves metadata and credential references for the exact selected route. It removes only matching legacy entries in the writable scope, comparing effective API, model id and exact configured URL. Buckets whose provider id contains a dot are left untouched, because the settings adapters would write them as nested paths; if such a stale entry still wins, the install fails before writing. Other endpoints, APIs, models and scopes remain intact; custom mappings are not rewritten. A higher-precedence override of the selected released declaration rejects reconfiguration before writing; reconfigure it in its owning context without that override. Other overrides that prevent the new configuration taking effect cause rollback. Internal AuthType.USE_OPENAI_RESPONSES continues to identify Responses transport and recorded session routes. There is still one OpenAI provider choice.

The name wireApi identifies the request protocol explicitly, following the meaning of Codex's wire_api while retaining this project's camelCase settings. The field remains per-model so one provider can offer both APIs.

Runtime and setup design

Use one shared per-model protocol resolver for registry ingestion, startup configuration, credential lookup, setup inspection, and model editing. Preserve the existing effective (authType, model id, configured baseUrl) identity. Two entries with the same model and URL but different APIs remain distinct. Duplicate entries for the same effective route keep the existing first-wins policy. Provider installation must also compare effective API when merging.

When a model was explicitly selected, raw startup selection of openai can select an explicitly configured Responses model when that model has no matching Chat route. An exact matching effective route takes precedence, including the configured URL discriminator. Once resolved, generator creation, model options, and session recording use the effective protocol. Explicit model switches and recorded session routes remain exact; do not add cross-protocol fallback to general registry lookup. Existing enforced-auth policy is not broadened.

Hot reload is transactional: invalid edits leave the prior registry usable. Changing or removing an active route's API must not combine credentials from the new route with the old generator or silently substitute another API. Keep the existing route-unavailable behavior and require explicit selection of the changed route when necessary. A failed re-authentication after such an edit is reported to the user once, with guidance to reselect the route or restart. Restart may resolve the newly edited startup configuration. Existing session records need no new field because their effective auth type already distinguishes both APIs.

Ink and OpenTUI share the provider setup hook; both must present API selection and show the exact persisted configuration in their preview, with custom header values masked; when the planner refuses the inputs, the review step shows the refusal instead of a preview. VS Code and Web Shell must expose the same choice. Web Shell uses a labelled review summary with masked credentials; it must show the selected API without inventing generated settings or defaults. ACP and daemon installation inputs accept wireApi and validate it before writing settings. ACP authentication labels use one shared OpenAI key method for both runtime APIs. Model removal matches each entry's effective protocol and must not clear the active selection when deleting its other-API sibling.

Initial authentication retries retain the resolved startup wire even after a failed attempt. A successful explicit model selection or provider installation ends that startup mapping, including before the first generator is created. ACP persists User's own OpenAI wire choice independently of Workspace-derived runtime authentication. Deletion validates each writable scope against its own effective settings and preserves valid Workspace selections that inherit fields from a cleared User selection. The Workspace model pair is decided once, as an atomic name/baseUrl pair, and only when the workspace owns neither field: an inherited selection that loses its route in the workspace is tombstoned there, and a preserved one is pinned with a credential-bearing URL replaced by an empty tombstone rather than copied into the shareable workspace file. Both fast and full startup capture the environment before loading workspace values. User selection validation uses that immutable snapshot with User's own settings and home-level .env files, discovered from home rather than the workspace or its ancestors. Shell and home credentials remain valid global inputs; workspace-only values cannot preserve a stale User selection. This snapshot is used only for deletion validation, not to change the daemon or Workspace runtime environment. A survivor must be the first registered route and eligible for conversation use.

Preview and submission use the same canonical view of existing models, including released declarations without modifying the source settings. Preset reconnection without an explicit API input preserves each saved model's API at the exact same endpoint, including both APIs for the same model id. The shared install builder performs this preservation for every entry point. The saved selection determines the plan's active route, not the API of other models; generic custom-provider setup keeps its visible API choice, prefilled from the identified saved route. New models retain the preset defaults. Installs with preserved API stamps retire template-version metadata that an unstamped template cannot reproduce.

Credential rotation preserves ${...} references by comparing each write with the same resolved snapshot that produced its input. Legacy pruning uses its own latest write snapshot; final runtime reads resolve the newly stored environment. The CLI refreshes all provider buckets in the writable scope after a model write, so duplicate mapped routes cannot retain stale headers after credential rotation. Placeholder recovery follows the registry's first-wins ordering across provider buckets while still rejecting ambiguous references within the winning bucket. Explicit credential references are never migrated automatically.

protocolOptions controls SDK protocol selection, not whether a model may use wireApi. Voice transcription currently supports Chat Completions only: Responses voice configurations are rejected before writes, including prebuilt models and preserved service metadata. Conversation and image models retain both wire choices.

Implementation areas: core model types/registry/config and provider install; CLI configuration/auth lookup and hot reload; setup views; ACP and daemon installation contracts; SDK daemon request types; settings schema and user documentation. No daemon route ownership or workspace-resolution rules change.

Validation and acceptance

  • Unit tests cover the configuration table, invalid inputs, mixed API entries, exact endpoint credentials, install merge, and transactional registry reload. Released provider ids and mappings remain readable without disk migration.
  • Configuration tests cover initial openai selection resolving Responses, explicit route precedence, model switching, and recorded session restoration.
  • Setup tests cover API selection, preview/write parity, shared credentials, canonical inspection, request validation, and deletion of only the intended API.
  • Regressions cover failed-auth retries versus explicit selection, opposite User/Workspace auth choices, deletion with inherited model fields, service aliases shadowing conversation routes, saved preview metadata, preset reconnection, and voice/wire validation before persistence.
  • An isolated localhost server records actual CLI endpoint paths and payloads: implicit Chat, explicit Chat, canonical Responses, custom-provider Responses, released Responses declarations, invalid API rejection before any request, and tool continuation for both APIs.
  • Dry-run the plan against global qwen, then verify the built local CLI. Use temporary QWEN_HOME directories and mock keys; do not modify real settings or send test prompts to a remote model.
  • Run build, typecheck, focused unit tests, bundle, formatting/lint checks, two clean self-audit passes, and independent review before declaring completion.

Acceptance requires correct request formats and preserved route identity, not merely successful JSON parsing or a zero exit code. Detailed execution results live in the git-ignored .qwen/e2e-tests/pr11538-canonical-wire-api/ directory; publish the verification report on the PR so reviewers have accessible evidence.