mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-29 12:11:09 +00:00
feat: add stopFailure and postCompact (#2825)
This commit is contained in:
parent
732cee2604
commit
dddb56d885
14 changed files with 1484 additions and 9 deletions
|
|
@ -8,7 +8,7 @@
|
|||
import type { Mock, MockInstance } from 'vitest';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { renderHook, act, waitFor } from '@testing-library/react';
|
||||
import { useGeminiStream } from './useGeminiStream.js';
|
||||
import { useGeminiStream, classifyApiError } from './useGeminiStream.js';
|
||||
import * as atCommandProcessor from './atCommandProcessor.js';
|
||||
import type {
|
||||
TrackedToolCall,
|
||||
|
|
@ -3512,3 +3512,115 @@ describe('useGeminiStream', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('classifyApiError', () => {
|
||||
it('should classify rate limit errors by status code 429', () => {
|
||||
expect(classifyApiError({ message: 'error', status: 429 })).toBe(
|
||||
'rate_limit',
|
||||
);
|
||||
});
|
||||
|
||||
it('should classify rate limit errors by message', () => {
|
||||
expect(classifyApiError({ message: 'Rate limit exceeded' })).toBe(
|
||||
'rate_limit',
|
||||
);
|
||||
});
|
||||
|
||||
it('should classify authentication errors by status code 401', () => {
|
||||
expect(classifyApiError({ message: 'error', status: 401 })).toBe(
|
||||
'authentication_failed',
|
||||
);
|
||||
});
|
||||
|
||||
it('should classify authentication errors by message', () => {
|
||||
expect(classifyApiError({ message: 'Unauthorized access' })).toBe(
|
||||
'authentication_failed',
|
||||
);
|
||||
});
|
||||
|
||||
it('should classify billing errors by status code 402', () => {
|
||||
expect(classifyApiError({ message: 'error', status: 402 })).toBe(
|
||||
'billing_error',
|
||||
);
|
||||
});
|
||||
|
||||
it('should classify billing errors by status code 403', () => {
|
||||
expect(classifyApiError({ message: 'error', status: 403 })).toBe(
|
||||
'billing_error',
|
||||
);
|
||||
});
|
||||
|
||||
it('should classify billing errors by message containing billing', () => {
|
||||
expect(classifyApiError({ message: 'Billing issue detected' })).toBe(
|
||||
'billing_error',
|
||||
);
|
||||
});
|
||||
|
||||
it('should classify billing errors by message containing quota', () => {
|
||||
expect(classifyApiError({ message: 'Quota exceeded' })).toBe(
|
||||
'billing_error',
|
||||
);
|
||||
});
|
||||
|
||||
it('should classify invalid request errors by status code 400', () => {
|
||||
expect(classifyApiError({ message: 'error', status: 400 })).toBe(
|
||||
'invalid_request',
|
||||
);
|
||||
});
|
||||
|
||||
it('should classify invalid request errors by message', () => {
|
||||
expect(classifyApiError({ message: 'Invalid request format' })).toBe(
|
||||
'invalid_request',
|
||||
);
|
||||
});
|
||||
|
||||
it('should classify server errors by status code 500', () => {
|
||||
expect(classifyApiError({ message: 'error', status: 500 })).toBe(
|
||||
'server_error',
|
||||
);
|
||||
});
|
||||
|
||||
it('should classify server errors by status code 502', () => {
|
||||
expect(classifyApiError({ message: 'error', status: 502 })).toBe(
|
||||
'server_error',
|
||||
);
|
||||
});
|
||||
|
||||
it('should classify server errors by status code 503', () => {
|
||||
expect(classifyApiError({ message: 'error', status: 503 })).toBe(
|
||||
'server_error',
|
||||
);
|
||||
});
|
||||
|
||||
it('should classify max output tokens errors by message', () => {
|
||||
expect(classifyApiError({ message: 'max_tokens limit reached' })).toBe(
|
||||
'max_output_tokens',
|
||||
);
|
||||
});
|
||||
|
||||
it('should classify token limit errors by message', () => {
|
||||
expect(classifyApiError({ message: 'Token limit exceeded' })).toBe(
|
||||
'max_output_tokens',
|
||||
);
|
||||
});
|
||||
|
||||
it('should return unknown for unrecognized errors', () => {
|
||||
expect(classifyApiError({ message: 'Some random error' })).toBe('unknown');
|
||||
});
|
||||
|
||||
it('should return unknown for empty message', () => {
|
||||
expect(classifyApiError({ message: '' })).toBe('unknown');
|
||||
});
|
||||
|
||||
it('should handle case insensitive matching', () => {
|
||||
expect(classifyApiError({ message: 'RATE LIMIT exceeded' })).toBe(
|
||||
'rate_limit',
|
||||
);
|
||||
expect(classifyApiError({ message: 'UNAUTHORIZED' })).toBe(
|
||||
'authentication_failed',
|
||||
);
|
||||
expect(classifyApiError({ message: 'BILLING error' })).toBe(
|
||||
'billing_error',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import type {
|
|||
ThoughtSummary,
|
||||
ToolCallRequestInfo,
|
||||
GeminiErrorEventValue,
|
||||
StopFailureErrorType,
|
||||
} from '@qwen-code/qwen-code-core';
|
||||
import {
|
||||
GeminiEventType as ServerGeminiEventType,
|
||||
|
|
@ -78,6 +79,43 @@ import { t } from '../../i18n/index.js';
|
|||
|
||||
const debugLogger = createDebugLogger('GEMINI_STREAM');
|
||||
|
||||
/**
|
||||
* Classify API error to StopFailureErrorType
|
||||
* @internal Exported for testing purposes
|
||||
*/
|
||||
export function classifyApiError(error: {
|
||||
message: string;
|
||||
status?: number;
|
||||
}): StopFailureErrorType {
|
||||
const status = error.status;
|
||||
const message = error.message?.toLowerCase() ?? '';
|
||||
|
||||
if (status === 429 || message.includes('rate limit')) {
|
||||
return 'rate_limit';
|
||||
}
|
||||
if (status === 401 || message.includes('unauthorized')) {
|
||||
return 'authentication_failed';
|
||||
}
|
||||
if (
|
||||
status === 402 ||
|
||||
status === 403 ||
|
||||
message.includes('billing') ||
|
||||
message.includes('quota')
|
||||
) {
|
||||
return 'billing_error';
|
||||
}
|
||||
if (status === 400 || message.includes('invalid')) {
|
||||
return 'invalid_request';
|
||||
}
|
||||
if (status !== undefined && status >= 500) {
|
||||
return 'server_error';
|
||||
}
|
||||
if (message.includes('max_tokens') || message.includes('token limit')) {
|
||||
return 'max_output_tokens';
|
||||
}
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if image parts have supported formats and returns unsupported ones
|
||||
*/
|
||||
|
|
@ -795,6 +833,12 @@ export const useGeminiStream = (
|
|||
// (auto-retry countdown is shown when retryCountdownTimerRef is active)
|
||||
const isShowingAutoRetry = retryCountdownTimerRef.current !== null;
|
||||
clearRetryCountdown();
|
||||
|
||||
const formattedErrorText = parseAndFormatApiError(
|
||||
eventValue.error,
|
||||
config.getContentGeneratorConfig()?.authType,
|
||||
);
|
||||
|
||||
if (!isShowingAutoRetry) {
|
||||
const retryHint = t('Press Ctrl+Y to retry');
|
||||
// Store error with hint as a pending item (not in history).
|
||||
|
|
@ -802,14 +846,24 @@ export const useGeminiStream = (
|
|||
// since pending items are in the dynamic rendering area (not <Static>).
|
||||
setPendingRetryErrorItem({
|
||||
type: 'error' as const,
|
||||
text: parseAndFormatApiError(
|
||||
eventValue.error,
|
||||
config.getContentGeneratorConfig()?.authType,
|
||||
),
|
||||
text: formattedErrorText,
|
||||
hint: retryHint,
|
||||
});
|
||||
}
|
||||
setThought(null); // Reset thought when there's an error
|
||||
|
||||
// Fire StopFailure hook (fire-and-forget, replaces Stop event for API errors)
|
||||
const errorType = classifyApiError(eventValue.error);
|
||||
config
|
||||
.getHookSystem()
|
||||
?.fireStopFailureEvent(
|
||||
errorType,
|
||||
eventValue.error.message,
|
||||
formattedErrorText,
|
||||
)
|
||||
.catch((err) => {
|
||||
debugLogger.warn(`StopFailure hook failed: ${err}`);
|
||||
});
|
||||
},
|
||||
[
|
||||
addItem,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue