Skip to content

Non-interactive (-p) mode prints API errors three times and double-wraps the message #3748

@umut-polat

Description

@umut-polat

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:

  1. 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.
  2. 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.
  3. 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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    type/bugSomething isn't working as expected

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions