Compare commits

...

2 Commits

Author SHA1 Message Date
Arvin Xu 5fcf2daa65 🐛 fix: require model context for "does not exist" matches
Previously, any message containing "does not exist" was classified as ModelNotFound, which could misclassify API key, deployment, or unrelated errors. Now require the word "model" to appear before "does not exist" within the same sentence. Periods inside version numbers (e.g. "doubao-seed-2.0-pro") are still allowed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-24 01:10:58 +08:00
Arvin Xu d4efbe1ed1 feat: add isModelNotFoundError pattern matching for model-runtime error classification
Add message-based pattern matching for ModelNotFound errors, similar to existing patterns for context window, quota, and account errors. Previously ModelNotFound was only detected via error code 'model_not_found', missing cases where providers return different codes but include recognizable messages like "does not exist" (Volcengine/doubao).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-24 01:10:58 +08:00
5 changed files with 153 additions and 0 deletions
@@ -22,6 +22,7 @@ import { desensitizeUrl } from '../../utils/desensitizeUrl';
import { getModelPricing } from '../../utils/getModelPricing';
import { isAccountDeactivatedError } from '../../utils/isAccountDeactivatedError';
import { isExceededContextWindowError } from '../../utils/isExceededContextWindowError';
import { isModelNotFoundError } from '../../utils/isModelNotFoundError';
import { isQuotaLimitError } from '../../utils/isQuotaLimitError';
import { MODEL_LIST_CONFIGS, processModelList } from '../../utils/modelParse';
import { StreamingResponse } from '../../utils/response';
@@ -332,6 +333,15 @@ export const handleDefaultAnthropicError = <T extends Record<string, any> = any>
};
}
if (isModelNotFoundError(errorMsg)) {
return {
endpoint: desensitizedEndpoint,
error: errorResult,
errorType: AgentRuntimeErrorType.ModelNotFound,
message,
};
}
if (isExceededContextWindowError(errorMsg)) {
return {
endpoint: desensitizedEndpoint,
@@ -736,6 +746,16 @@ export const createAnthropicCompatibleRuntime = <T extends Record<string, any> =
});
}
if (isModelNotFoundError(errorMsg)) {
return AgentRuntimeError.chat({
endpoint: desensitizedEndpoint,
error: errorResult,
errorType: AgentRuntimeErrorType.ModelNotFound,
message,
provider: this.id,
});
}
if (isExceededContextWindowError(errorMsg)) {
return AgentRuntimeError.chat({
endpoint: desensitizedEndpoint,
@@ -42,6 +42,7 @@ import { handleOpenAIError } from '../../utils/handleOpenAIError';
import { isAccountDeactivatedError } from '../../utils/isAccountDeactivatedError';
import { isExceededContextWindowError } from '../../utils/isExceededContextWindowError';
import { isInsufficientQuotaError } from '../../utils/isInsufficientQuotaError';
import { isModelNotFoundError } from '../../utils/isModelNotFoundError';
import { isQuotaLimitError } from '../../utils/isQuotaLimitError';
import { postProcessModelList } from '../../utils/postProcessModelList';
import {
@@ -1156,6 +1157,17 @@ export const createOpenAICompatibleRuntime = <T extends Record<string, any> = an
});
}
if (isModelNotFoundError(errorMsg)) {
log('model not found error detected from message');
return AgentRuntimeError.chat({
endpoint: desensitizedEndpoint,
error: errorResult,
errorType: AgentRuntimeErrorType.ModelNotFound,
message,
provider: this.id,
});
}
if (isInsufficientQuotaError(errorMsg)) {
log('insufficient quota error detected from message');
return AgentRuntimeError.chat({
@@ -1,6 +1,7 @@
import type { ILobeAgentRuntimeErrorType } from '../types/error';
import { AgentRuntimeErrorType } from '../types/error';
import { isExceededContextWindowError } from './isExceededContextWindowError';
import { isModelNotFoundError } from './isModelNotFoundError';
import { isQuotaLimitError } from './isQuotaLimitError';
export interface ParsedError {
@@ -112,6 +113,10 @@ export function parseGoogleErrorMessage(message: string): ParsedError {
return { error: { message }, errorType: AgentRuntimeErrorType.ProviderNoImageGenerated };
}
if (isModelNotFoundError(message)) {
return { error: { message }, errorType: AgentRuntimeErrorType.ModelNotFound };
}
if (isExceededContextWindowError(message)) {
return { error: { message }, errorType: AgentRuntimeErrorType.ExceededContextWindow };
}
@@ -0,0 +1,84 @@
import { describe, expect, it } from 'vitest';
import { isModelNotFoundError } from './isModelNotFoundError';
describe('isModelNotFoundError', () => {
it('should return false for undefined/empty input', () => {
expect(isModelNotFoundError(undefined)).toBe(false);
expect(isModelNotFoundError('')).toBe(false);
});
it('should detect "model not found" errors', () => {
expect(isModelNotFoundError('The model gpt-5 was not found')).toBe(false);
expect(isModelNotFoundError('model not found: gpt-5')).toBe(true);
});
it('should detect "model_not_found" code in message', () => {
expect(isModelNotFoundError('Error: model_not_found')).toBe(true);
});
it('should detect "model ... does not exist" (OpenAI)', () => {
expect(
isModelNotFoundError('The model `gpt-5` does not exist or you do not have access to it.'),
).toBe(true);
});
it('should detect "model or endpoint ... does not exist" (Volcengine/doubao)', () => {
expect(
isModelNotFoundError(
'The model or endpoint doubao-seed-2.0-pro does not exist or you do not have access to it.',
),
).toBe(true);
});
it('should NOT match "does not exist" without model context', () => {
// API key errors that incidentally say "does not exist"
expect(isModelNotFoundError('Your API key does not exist')).toBe(false);
// Deployment/endpoint errors
expect(isModelNotFoundError('The deployment for this resource does not exist')).toBe(false);
// Generic resource errors
expect(isModelNotFoundError('This user does not exist')).toBe(false);
expect(isModelNotFoundError('The organization does not exist')).toBe(false);
});
it('should NOT match when "model" and "does not exist" are in different sentences', () => {
expect(
isModelNotFoundError(
'This feature does not exist in your plan. Contact support to enable the model.',
),
).toBe(false);
expect(isModelNotFoundError('The model is fine. Your account does not exist.')).toBe(false);
});
it('should detect "no such model" errors', () => {
expect(isModelNotFoundError('no such model: custom-model-v1')).toBe(true);
});
it('should detect "not found model" errors', () => {
expect(isModelNotFoundError('not found model abc-123')).toBe(true);
});
it('should detect "model is not accessible" errors', () => {
expect(isModelNotFoundError('The model is not accessible with your current plan')).toBe(true);
});
it('should detect "model is not available" errors', () => {
expect(isModelNotFoundError('The requested model is not available in this region')).toBe(true);
});
it('should detect "invalid model" errors', () => {
expect(isModelNotFoundError('invalid model: test-model')).toBe(true);
});
it('should be case-insensitive', () => {
expect(isModelNotFoundError('MODEL NOT FOUND')).toBe(true);
expect(isModelNotFoundError('The Model Does Not Exist')).toBe(true);
});
it('should return false for unrelated error messages', () => {
expect(isModelNotFoundError('Insufficient Balance')).toBe(false);
expect(isModelNotFoundError('Invalid API key')).toBe(false);
expect(isModelNotFoundError('Rate limit reached')).toBe(false);
expect(isModelNotFoundError('context length exceeded')).toBe(false);
});
});
@@ -0,0 +1,32 @@
const MODEL_NOT_FOUND_PATTERNS = [
'model not found', // OpenAI / generic
'model_not_found', // OpenAI (code in message)
'no such model', // generic
'not found model', // some providers
'model is not accessible', // access-related model errors
'model is not available', // generic
'invalid model', // generic
];
// "does not exist" on its own is too broad (it can show up in API key,
// deployment, or unrelated errors). Require explicit model context:
// the word "model" must appear before "does not exist" within the same
// sentence. The char class excludes sentence terminators (. ! ?) but
// allows a period when followed by a digit, so version numbers like
// "doubao-seed-2.0-pro" don't accidentally break the match.
//
// Matches:
// - OpenAI: "The model `gpt-5` does not exist or you do not have access to it."
// - Volcengine: "The model or endpoint doubao-seed-2.0-pro does not exist..."
// Correctly ignores:
// - "Your API key does not exist"
// - "The deployment for this resource does not exist"
// - "The model is fine. Your account does not exist." (different sentences)
const MODEL_DOES_NOT_EXIST_REGEX = /\bmodel\b(?:[^!.?\n]|\.(?=\d))+?\bdoes not exist\b/i;
export const isModelNotFoundError = (message?: string): boolean => {
if (!message) return false;
const lower = message.toLowerCase();
if (MODEL_NOT_FOUND_PATTERNS.some((p) => lower.includes(p))) return true;
return MODEL_DOES_NOT_EXIST_REGEX.test(message);
};