Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions packages/runtime/src/__tests__/model-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown> = {}) =>
Expand Down
9 changes: 5 additions & 4 deletions packages/runtime/src/model-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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;
Expand All @@ -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,
};
}
Expand Down
21 changes: 12 additions & 9 deletions packages/runtime/src/tool-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {};
}
}

Expand Down
Loading