Files
majordomo/docs/adr/0027-qwen-builtin.md
T
steveandClaude Opus 5 02cd561eaf
Gadfly review (reusable) / review (pull_request) Successful in 5m14s
Adversarial Review (Gadfly) / review (pull_request) Successful in 5m14s
CI / Tidy (pull_request) Successful in 9m24s
CI / Build & Test (pull_request) Successful in 9m53s
feat(qwen): Alibaba Qwen built-in over Model Studio's OpenAI-compatible mode
Adds the `qwen` built-in provider and the `qwen://` DSN scheme, keyed by
QWEN_API_KEY and defaulting to Model Studio's international host. Like kimi
(ADR-0026) it is `provider/openai` pointed elsewhere — no new client.

Model Studio serves the same models over two protocols, so the real decision
was which wire format to speak. ADR-0027 records why it is the OpenAI one:
down the anthropic client `ReasoningEffort` is ignored by design, structured
output rides the first-party `output_config.format` mechanism the shim does
not implement, and cached-token accounting reads Anthropic-only usage fields.
Each of those fails silently rather than loudly, which is what makes the
choice worth writing down. The shim stays reachable ad hoc via an
`anthropic://` DSN.

The kimi and qwen DSN factories were byte-identical, so they now share one
`openaiCompatScheme` helper: the "credential comes from the DSN token, and
the missing-key hint names LLM_<NAME>" rules hold by construction instead of
by copy.

Tests are hermetic and break-checked (all six fail on a deliberate mutation),
including the reverse credential leak — a visible QWEN_API_KEY must not
authenticate the openai built-in — and reasoning_effort asserted on the wire
body, which is the ADR's load-bearing claim.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-12 16:07:41 -04:00

5.6 KiB

ADR-0027: Qwen (Alibaba) built-in provider — OpenAI-compat, not Anthropic-compat

Status: Accepted — 2026-08-12

Context

Alibaba's Qwen models (qwen3.8-max, qwen3.7-plus, the qwen3-vl-* vision variants, …) are served from Model Studio / DashScope, and mort wants them as a first-class failover tier with a dedicated QWEN_API_KEY — the same ergonomics ADR-0026 gave Kimi.

Unlike Kimi, Model Studio exposes the same models over two protocols:

OpenAI-compatible Anthropic-compatible
Base URL https://dashscope-intl.aliyuncs.com/compatible-mode/v1 https://dashscope-intl.aliyuncs.com/apps/anthropic
Endpoints full Chat Completions surface /v1/messages only (no /v1/models)
Purpose the documented developer API a shim, documented around hosting Claude Code

So the question this ADR answers is not "which client do we reuse" but "which of Alibaba's two wire protocols does the built-in speak".

Decision

The qwen built-in and the qwen:// DSN scheme speak OpenAI-compat, over provider/openai — no new package, mirroring ADR-0026 (kimi) and ADR-0015 (llama-swap chat). Default base URL is the international host; the China host (dashscope.aliyuncs.com/compatible-mode/v1) and workspace-scoped regional hosts are reachable with a qwen:// DSN.

Credential handling is copied from kimi verbatim, because both of its rules are load-bearing: WithAPIKey is passed unconditionally (even empty) so an unset QWEN_API_KEY can never fall through to openai.New's OPENAI_API_KEY default, and WithAPIKeyName("QWEN_API_KEY") makes the synthetic-401 hint name the variable the operator actually has to set.

The kimi and qwen DSN factories were identical, so they now share one openaiCompatScheme helper — the next OpenAI-compat built-in gets the credential and key-hint rules by construction rather than by copy.

Why not the Anthropic-compatible endpoint

Every concrete difference favors OpenAI-compat for this codebase:

  • Reasoning survives the trip. Model Studio takes reasoning_effort as a top-level field on the OpenAI surface, which provider/openai already sends — llm.WithReasoningEffort works on qwen with zero qwen-specific code (TestQwenReasoningEffortReachesWire asserts it on the wire). Down the anthropic client it would be dropped in silence: provider/anthropic deliberately ignores Request.ReasoningEffort, because first-party Claude has no such knob.
  • Structured output would regress. provider/anthropic implements Request.Schema with the first-party GA output_config.format mechanism. Alibaba's shim does not implement it; a compat endpoint that ignores an unknown field returns unconstrained prose while still reporting success. The OpenAI path sends response_format: json_schema, which Model Studio supports natively on the Max/Plus families.
  • Cache accounting already lands. Model Studio's implicit prefix cache reports hits in usage.prompt_tokens_details.cached_tokens, which the openai client already maps to llm.Usage.CacheReadTokens. The anthropic client reads cache_read_input_tokens, a field the shim has no reason to emit.
  • Thinking content is discarded on the anthropic path anyway. provider/anthropic skips thinking blocks in both the buffered and streaming decoders, so the shim's headline feature — first-class thinking: {type: "enabled", budget_tokens: N} — buys majordomo nothing today.
  • Smaller blast radius. The anthropic client has no WithAPIKeyName option, so a keyless qwen would tell the operator to set ANTHROPIC_API_KEY; fixing that means changing the first-party Anthropic client to serve a third-party shim.
  • It is the less-exercised surface. The Anthropic endpoint is documented as Messages-only, with a temperature range that differs from Anthropic's own ([0, 2) vs [0.0, 1.0]) — i.e. it is Qwen semantics wearing an Anthropic envelope, not an Anthropic-equivalent target.

The one thing the Anthropic surface offers that OpenAI-compat does not is explicit cache_control breakpoints reached through Request.PromptCache. That is not a reason to route Qwen through it: Model Studio's implicit cache is automatic and already metered, and if explicit breakpoints ever matter they belong in provider/openai (Model Studio accepts cache_control on content blocks there too), where every OpenAI-compat target would get them.

Consequences

  • qwen/<model> is first-class in Parse, chains, aliases, and health/failover with no consumer wiring; model ids pass through verbatim (no catalog).
  • Chat, streaming, tools, structured output, reasoning effort, and cached-token accounting all ride the openai client and inherit its fixes.
  • Image inputs work at the client level, but only the qwen-vl-* / qwen3-vl-* models accept them (matrix footnote ³, shared with kimi).
  • Two model-side quirks are Alibaba's, not majordomo's, and are left to the caller rather than papered over: thinking is on by default on some models (e.g. qwen3.7-plus), and Qwen3 open-source models require streaming when thinking is enabled — a buffered Generate against one of those needs a model that supports non-streaming thinking (the Max/Plus families do).
  • If a future consumer genuinely needs the Anthropic surface, it is reachable today without library changes: LLM_QWEN_ANTHROPIC=anthropic://[email protected]/apps/anthropic — with the reasoning/structured-output caveats above.
  • Second third-party built-in after kimi. The ADR-0026 bar still holds: a named consumer needs it in-config. RegisterProvider/LLM_* remain the path for everything else.