* 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]>
12 KiB
Model-level OpenAI API selection
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
openaiselection 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 temporaryQWEN_HOMEdirectories 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.