What happened?
When running qwen in non-interactive mode (-p / --prompt) against any OpenAI-compatible endpoint, an upstream 4xx response produces three lines on stderr — the second of which is double-wrapped — followed by a stack trace under "An unexpected critical error occurred:". This makes routine upstream errors look like a CLI crash.
Reproduction
A real provider (DeepSeek) cleanly reproduces it with an invalid model name. Any provider returning a 4xx works the same way:
OPENAI_API_KEY=$DEEPSEEK_KEY \
OPENAI_BASE_URL=https://api.deepseek.com/v1 \
qwen --auth-type openai --model "nonexistent-model-12345" -p "say hi"
Observed (qwen 0.15.3, Node v24.7.0, macOS 26.2):
[API Error: 400 The supported API model names are deepseek-v4-pro or deepseek-v4-flash, but you passed nonexistent-model-12345.]
[API Error: 400 The supported API model names are deepseek-v4-pro or deepseek-v4-flash, but you passed nonexistent-model-12345.]
[API Error: [API Error: 400 The supported API model names are deepseek-v4-pro or deepseek-v4-flash, but you passed nonexistent-model-12345.]]
An unexpected critical error occurred:
Error: [API Error: 400 The supported API model names are deepseek-v4-pro or deepseek-v4-flash, but you passed nonexistent-model-12345.]
at file:///.../dist/cli.js:485490:19
...
The same shape reproduces against any OpenAI-compatible gateway that returns a 4xx (I first ran into it with a 402 from a self-hosted endpoint, then confirmed with DeepSeek's 400).
What did you expect to happen?
A single, cleanly-formatted line and a non-zero exit code:
[API Error: 400 The supported API model names are deepseek-v4-pro or deepseek-v4-flash, but you passed nonexistent-model-12345.]
Root cause (from reading the code)
Three things compound on the non-interactive path; interactive mode is unaffected:
packages/cli/src/nonInteractiveCli.ts (~line 385–396) — the stream-error handler calls parseAndFormatApiError, writes the formatted line to stderr, then throw new Error(errorText) so the thrown Error's .message is the already-formatted string.
packages/cli/src/utils/errors.ts (handleError, ~line 119–147) — re-runs parseAndFormatApiError(error, …) on that thrown Error, which doesn't detect the existing wrap and produces "[API Error: [API Error: ...]]". Writes a second stderr line.
packages/cli/index.ts (~line 86–101) — top-level .catch treats the rethrown error as if it were a programmer-level bug and prints "An unexpected critical error occurred:" plus a stack trace.
JsonOutputAdapter.emitResult in TEXT mode also writes errorMessage to stderr (packages/cli/src/nonInteractive/io/JsonOutputAdapter.ts:70), which adds a third copy of the same line on top of (1) and (2).
Proposed fix (happy to PR)
- Introduce a small marker class
AlreadyReportedError that producers can throw after they've formatted and printed their own message.
handleError short-circuits on that marker — no second print, no re-format.
- Top-level handler short-circuits on that marker — no "critical" framing or stack trace, just propagate
exitCode.
- In the non-interactive catch, skip
adapter.emitResult in TEXT mode when the thrown error is AlreadyReportedError so we don't get the third copy.
- Defensively,
parseAndFormatApiError becomes idempotent: if the input already starts with [API Error: and ends with ], return it unchanged. Acts as a safety net so future call sites that forget the marker don't regress the double-wrap symptom.
I have a working patch with unit tests covering all three pieces (idempotency in errorParsing, the marker short-circuit in handleError, and a regression test on the non-interactive runner that asserts no [API Error: [API Error: ever appears on stderr). Happy to open a PR if this direction sounds right.
Client information
Client Information
Qwen Code: 0.15.3
Node: v24.7.0
Platform: macOS 26.2 (darwin arm64)
What happened?
When running
qwenin non-interactive mode (-p/--prompt) against any OpenAI-compatible endpoint, an upstream 4xx response produces three lines on stderr — the second of which is double-wrapped — followed by a stack trace under "An unexpected critical error occurred:". This makes routine upstream errors look like a CLI crash.Reproduction
A real provider (DeepSeek) cleanly reproduces it with an invalid model name. Any provider returning a 4xx works the same way:
Observed (qwen 0.15.3, Node v24.7.0, macOS 26.2):
The same shape reproduces against any OpenAI-compatible gateway that returns a 4xx (I first ran into it with a 402 from a self-hosted endpoint, then confirmed with DeepSeek's 400).
What did you expect to happen?
A single, cleanly-formatted line and a non-zero exit code:
Root cause (from reading the code)
Three things compound on the non-interactive path; interactive mode is unaffected:
packages/cli/src/nonInteractiveCli.ts(~line 385–396) — the stream-error handler callsparseAndFormatApiError, writes the formatted line to stderr, thenthrow new Error(errorText)so the thrown Error's.messageis the already-formatted string.packages/cli/src/utils/errors.ts(handleError, ~line 119–147) — re-runsparseAndFormatApiError(error, …)on that thrown Error, which doesn't detect the existing wrap and produces"[API Error: [API Error: ...]]". Writes a second stderr line.packages/cli/index.ts(~line 86–101) — top-level.catchtreats the rethrown error as if it were a programmer-level bug and prints "An unexpected critical error occurred:" plus a stack trace.JsonOutputAdapter.emitResultin TEXT mode also writeserrorMessageto stderr (packages/cli/src/nonInteractive/io/JsonOutputAdapter.ts:70), which adds a third copy of the same line on top of (1) and (2).Proposed fix (happy to PR)
AlreadyReportedErrorthat producers can throw after they've formatted and printed their own message.handleErrorshort-circuits on that marker — no second print, no re-format.exitCode.adapter.emitResultin TEXT mode when the thrown error isAlreadyReportedErrorso we don't get the third copy.parseAndFormatApiErrorbecomes idempotent: if the input already starts with[API Error:and ends with], return it unchanged. Acts as a safety net so future call sites that forget the marker don't regress the double-wrap symptom.I have a working patch with unit tests covering all three pieces (idempotency in
errorParsing, the marker short-circuit inhandleError, and a regression test on the non-interactive runner that asserts no[API Error: [API Error:ever appears on stderr). Happy to open a PR if this direction sounds right.Client information
Client Information