diff --git a/packages/runtime/src/__tests__/model-adapter.test.ts b/packages/runtime/src/__tests__/model-adapter.test.ts index 0697f0d9b0..7941d1a3f9 100644 --- a/packages/runtime/src/__tests__/model-adapter.test.ts +++ b/packages/runtime/src/__tests__/model-adapter.test.ts @@ -214,6 +214,41 @@ describe('ModelAdapter stream and error normalization', () => { assert.equal(adapter.mapFinishReason('provider-new-reason'), 'end_turn'); }); + test('projects a structured network error to a consistent reason and safe message', () => { + const event = newAdapter().makeErrorEvent('turn-1', { + message: 'fetch failed', + detail: 'token=sk-live-secret-token-value', + }); + + assert.equal(event.reason, 'network'); + assert.equal(event.message, 'Network error'); + assert.equal(JSON.stringify(event).includes('sk-live-secret-token-value'), false); + }); + + test('retains Node connection copy without promoting retry classification', () => { + const adapter = newAdapter(); + const error = new Error('connect ECONNREFUSED 127.0.0.1:443'); + const event = adapter.makeErrorEvent('turn-1', error); + + assert.equal(adapter.classifyError(error), 'Error'); + assert.equal(event.reason, undefined); + assert.equal(event.message, 'Network error'); + }); + + test('projects string provider errors through the same classification', () => { + const event = newAdapter().makeErrorEvent('turn-1', 'fetch failed'); + + assert.equal(event.reason, 'network'); + assert.equal(event.message, 'Network error'); + }); + + test('keeps an unknown structured provider error generic', () => { + const event = newAdapter().makeErrorEvent('turn-1', { message: 'provider exploded' }); + + assert.equal(event.reason, undefined); + assert.equal(event.message, 'Operation failed'); + }); + test('classifies provider context-length overflow errors as ContextLength', () => { const adapter = newAdapter(); const overflow = (message: string, extra: Record = {}) => diff --git a/packages/runtime/src/model-adapter.ts b/packages/runtime/src/model-adapter.ts index 533c454a3a..383e17d8ba 100644 --- a/packages/runtime/src/model-adapter.ts +++ b/packages/runtime/src/model-adapter.ts @@ -12,7 +12,7 @@ import type { ModelMessage } from 'ai'; import type { AsyncEventQueue } from './async-queue.js'; import { resolveModelRuntime } from './model-runtime.js'; -import { classifyError, errorReasonFromClass } from './tool-runtime.js'; +import { classifyError, errorPresentationFromClass } from './tool-runtime.js'; /** * Build an ai-sdk LanguageModel from a single input object. @@ -301,8 +301,9 @@ export class ModelAdapter { } makeErrorEvent(turnId: string, err: unknown): ErrorEvent { - const message = generalizedErrorMessage(err); - const reason = errorReasonFromClass(classifyError(err)); + const errorClass = classifyError(err); + const presentation = errorPresentationFromClass(errorClass); + const message = presentation.message ?? generalizedErrorMessage(err); const code = err instanceof Error && 'code' in err ? String((err as { code?: unknown }).code) : undefined; @@ -313,7 +314,7 @@ export class ModelAdapter { ts: this.input.now(), recoverable: false, ...(code !== undefined ? { code } : {}), - ...(reason !== undefined ? { reason } : {}), + ...(presentation.reason !== undefined ? { reason: presentation.reason } : {}), message, }; } diff --git a/packages/runtime/src/tool-runtime.ts b/packages/runtime/src/tool-runtime.ts index 01231bf270..e3f6f678f0 100644 --- a/packages/runtime/src/tool-runtime.ts +++ b/packages/runtime/src/tool-runtime.ts @@ -1647,24 +1647,27 @@ export function classifyError(error: unknown): string { return error instanceof Error ? (error.name || 'Other') : 'Other'; } -export function errorReasonFromClass(errorClass: string): string | undefined { +export function errorPresentationFromClass(errorClass: string): { + reason?: string; + message?: string; +} { switch (errorClass) { case 'ContextLength': - return 'context_overflow'; + return { reason: 'context_overflow', message: 'Context window exceeded' }; case 'Timeout': - return 'timeout'; + return { reason: 'timeout', message: 'Request timed out' }; case 'Auth': - return 'auth'; + return { reason: 'auth', message: 'Authentication failed' }; case 'ProviderBilling': - return 'provider_billing'; + return { reason: 'provider_billing', message: 'Provider billing required' }; case 'ProviderUnavailable': - return 'provider_unavailable'; + return { reason: 'provider_unavailable', message: 'Provider returned an error' }; case 'RateLimit': - return 'rate_limit'; + return { reason: 'rate_limit', message: 'Rate limit exceeded' }; case 'Network': - return 'network'; + return { reason: 'network', message: 'Network error' }; default: - return undefined; + return {}; } }