mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-08-04 22:00:58 +00:00
fix(autofix): short-circuit 400 as terminal and classify only the last API error (#7247)
This commit is contained in:
parent
55563e37b4
commit
719991a3b8
2 changed files with 51 additions and 5 deletions
|
|
@ -97,8 +97,13 @@ function classifyApiError(render) {
|
|||
const status = Number(code);
|
||||
if (status === 429 || (status >= 500 && status <= 599)) return 'transient';
|
||||
if (status === 401 || status === 402 || status === 403) return 'auth';
|
||||
// Any other code (400, 404, ...) is permanent UNLESS the message itself
|
||||
// names an access/existence problem.
|
||||
// 400 is always a malformed client request — it never self-heals by retry,
|
||||
// regardless of what the message says. Route it terminal unconditionally
|
||||
// so a 'does not exist' in the body (a tool name, a field name) cannot
|
||||
// trigger the auth/access retry path.
|
||||
if (status === 400) return '';
|
||||
// Any other code (404, ...) is permanent UNLESS the message itself names
|
||||
// an access/existence problem.
|
||||
return AUTH_API_ERROR.test(render) ? 'auth' : '';
|
||||
}
|
||||
// Code-less render: fall back to the keyword arms.
|
||||
|
|
@ -113,9 +118,13 @@ function classifyApiError(render) {
|
|||
// failure), but detection is best-effort rather than guaranteed.
|
||||
function recoverableApiError(output) {
|
||||
const wrapped = output.match(/\[API Error:[^\]\n]*\]/g) || [];
|
||||
for (const render of wrapped.reverse()) {
|
||||
const kind = classifyApiError(render);
|
||||
if (kind) return { error: render, kind };
|
||||
if (wrapped.length > 0) {
|
||||
// Classify only the LAST render — it represents the terminal state of the
|
||||
// run. An earlier transient error followed by a permanent one must not
|
||||
// retry: the permanent error reproduces identically on every attempt.
|
||||
const last = wrapped[wrapped.length - 1];
|
||||
const kind = classifyApiError(last);
|
||||
if (kind) return { error: last, kind };
|
||||
}
|
||||
// Some quota errors are never wrapped in [API Error: ...] (e.g. Qwen OAuth
|
||||
// quota returns early before formatting) - catch the known standalone form.
|
||||
|
|
|
|||
|
|
@ -3404,6 +3404,11 @@ describe('qwen-autofix workflow', () => {
|
|||
for (const render of [
|
||||
'[API Error: 400 Invalid value for max_tokens: must be <= 512]',
|
||||
'[API Error: 400 context length exceeded: 40000 > 32768]',
|
||||
// A 400 whose message says 'does not exist' in a NON-access context
|
||||
// (a tool name, a field name) must stay terminal — the AUTH_API_ERROR
|
||||
// keyword 'does not exist' must not promote it to a retried auth error.
|
||||
"[API Error: 400 Tool 'web_search' does not exist]",
|
||||
"[API Error: 400 Field 'temperature' does not exist in schema]",
|
||||
]) {
|
||||
withRunnerDir((dir) => {
|
||||
writeFileSync(join(dir, 'feedback.md'), 'feedback\n');
|
||||
|
|
@ -3445,6 +3450,38 @@ describe('qwen-autofix workflow', () => {
|
|||
}
|
||||
});
|
||||
|
||||
it('classifies only the last API error — a terminal error after a transient one stays terminal', () => {
|
||||
// If the output tail contains a transient error (429) followed by a
|
||||
// permanent one (400), the last error represents the terminal state of
|
||||
// the run. Retrying on the earlier transient error would hit the same
|
||||
// permanent error every time.
|
||||
withRunnerDir((dir) => {
|
||||
writeFileSync(join(dir, 'feedback.md'), 'feedback\n');
|
||||
const stub = writeQwenStub(dir, [
|
||||
"process.stdout.write('[API Error: 429 Too Many Requests]\\n');",
|
||||
"process.stdout.write('[API Error: 400 Bad request: malformed]\\n');",
|
||||
'process.exit(1);',
|
||||
]);
|
||||
expect(runAddressReview(dir, stub).status).not.toBe(0);
|
||||
expect(existsSync(join(dir, 'agent-api-error'))).toBe(false);
|
||||
});
|
||||
// The reverse order (permanent then transient) retries on the transient —
|
||||
// the last error is the one that killed the run.
|
||||
withRunnerDir((dir) => {
|
||||
writeFileSync(join(dir, 'feedback.md'), 'feedback\n');
|
||||
const stub = writeQwenStub(dir, [
|
||||
"process.stdout.write('[API Error: 400 Bad request: malformed]\\n');",
|
||||
"process.stdout.write('[API Error: 429 Too Many Requests]\\n');",
|
||||
'process.exit(1);',
|
||||
]);
|
||||
expect(runAddressReview(dir, stub).status).not.toBe(0);
|
||||
expect(existsSync(join(dir, 'agent-api-error'))).toBe(true);
|
||||
expect(
|
||||
readFileSync(join(dir, 'agent-api-error-kind'), 'utf8').trim(),
|
||||
).toBe('transient');
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps the API-error headline valid UTF-8 when the byte cap splits a CJK render', () => {
|
||||
// `cut -c` counts bytes under GNU coreutils and the classifier deliberately
|
||||
// matches Chinese renders, so the 200-byte cap can split a multi-byte
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue