better logging around fallbacks

This commit is contained in:
dal 2025-08-21 08:58:27 -06:00
parent fa70d7f1cb
commit 3b266c07e7
No known key found for this signature in database
GPG Key ID: 16F4B0E1E9F61122
6 changed files with 173 additions and 14 deletions

View File

@ -131,6 +131,9 @@ export class FallbackModel implements LanguageModelV2 {
if (!model) { if (!model) {
throw new Error(`No model available at index ${this._currentModelIndex}`); throw new Error(`No model available at index ${this._currentModelIndex}`);
} }
console.info(
`[Fallback] Using model: ${model.modelId} (index: ${this._currentModelIndex}/${this.settings.models.length - 1})`
);
return model; return model;
} }
@ -141,13 +144,19 @@ export class FallbackModel implements LanguageModelV2 {
const now = Date.now(); const now = Date.now();
if (now - this.lastModelReset >= this.modelResetInterval) { if (now - this.lastModelReset >= this.modelResetInterval) {
// Reset to primary model // Reset to primary model
console.info(
`[Fallback] Resetting to primary model after ${this.modelResetInterval}ms timeout`
);
this.currentModelIndex = 0; this.currentModelIndex = 0;
this.lastModelReset = now; this.lastModelReset = now;
} }
} }
private switchToNextModel() { private switchToNextModel() {
const previousModel = this.settings.models[this.currentModelIndex]?.modelId || 'unknown';
this.currentModelIndex = (this.currentModelIndex + 1) % this.settings.models.length; this.currentModelIndex = (this.currentModelIndex + 1) % this.settings.models.length;
const nextModel = this.settings.models[this.currentModelIndex]?.modelId || 'unknown';
console.warn(`Switching from model ${previousModel} to ${nextModel} due to error`);
} }
private async retry<T>(fn: () => PromiseLike<T>): Promise<T> { private async retry<T>(fn: () => PromiseLike<T>): Promise<T> {
@ -161,7 +170,13 @@ export class FallbackModel implements LanguageModelV2 {
// Retry current model up to maxRetriesPerModel times // Retry current model up to maxRetriesPerModel times
while (modelRetryCount < maxRetriesPerModel) { while (modelRetryCount < maxRetriesPerModel) {
try { try {
return await fn(); const result = await fn();
if (modelRetryCount > 0 || this.currentModelIndex !== initialModel) {
console.info(
`[Fallback] Request succeeded on model ${this.modelId} after ${modelRetryCount} retries`
);
}
return result;
} catch (error) { } catch (error) {
lastError = error as RetryableError; lastError = error as RetryableError;
const shouldRetry = this.settings.shouldRetryThisError || defaultShouldRetryThisError; const shouldRetry = this.settings.shouldRetryThisError || defaultShouldRetryThisError;
@ -188,6 +203,9 @@ export class FallbackModel implements LanguageModelV2 {
} }
// All retries for this model exhausted, switch to next model // All retries for this model exhausted, switch to next model
console.warn(
`Model ${this.modelId} exhausted ${maxRetriesPerModel} retries, switching to next model`
);
this.switchToNextModel(); this.switchToNextModel();
if (this.currentModelIndex === initialModel) { if (this.currentModelIndex === initialModel) {
@ -225,6 +243,7 @@ export class FallbackModel implements LanguageModelV2 {
this.checkAndResetModel(); this.checkAndResetModel();
const self = this; const self = this;
const shouldRetry = this.settings.shouldRetryThisError || defaultShouldRetryThisError; const shouldRetry = this.settings.shouldRetryThisError || defaultShouldRetryThisError;
console.info(`[Fallback] Starting stream request...`);
return this.retry(async () => { return this.retry(async () => {
const result = await self.getCurrentModel().doStream(options); const result = await self.getCurrentModel().doStream(options);
@ -235,6 +254,7 @@ export class FallbackModel implements LanguageModelV2 {
try { try {
const reader = result.stream.getReader(); const reader = result.stream.getReader();
let streamedChunks = 0;
while (true) { while (true) {
const result = await reader.read(); const result = await reader.read();
@ -246,8 +266,14 @@ export class FallbackModel implements LanguageModelV2 {
} }
} }
if (done) break; if (done) {
console.info(
`[Fallback] Stream completed successfully. Streamed ${streamedChunks} chunks from ${self.modelId}`
);
break;
}
controller.enqueue(value); controller.enqueue(value);
streamedChunks++;
if (value?.type !== 'stream-start') { if (value?.type !== 'stream-start') {
hasStreamedAny = true; hasStreamedAny = true;
@ -278,10 +304,12 @@ export class FallbackModel implements LanguageModelV2 {
} }
if (!hasStreamedAny || self.retryAfterOutput) { if (!hasStreamedAny || self.retryAfterOutput) {
// If nothing was streamed yet, switch models and retry // If nothing was streamed yet, switch models and retry
console.warn(`Stream error on ${self.modelId}, attempting fallback...`);
self.switchToNextModel(); self.switchToNextModel();
// Prevent infinite recursion - if we've tried all models, fail // Prevent infinite recursion - if we've tried all models, fail
if (self.currentModelIndex === 0) { if (self.currentModelIndex === 0) {
console.error('All models exhausted, failing request');
controller.error(error); controller.error(error);
return; return;
} }

View File

@ -34,7 +34,28 @@ function initializeGPT5() {
models, models,
modelResetInterval: 60000, modelResetInterval: 60000,
retryAfterOutput: true, retryAfterOutput: true,
onError: (err) => console.error(`FALLBACK. Here is the error: ${err}`), onError: (err, modelId) => {
// Handle various error formats
let errorMessage = 'Unknown error';
if (err instanceof Error) {
errorMessage = err.message;
} else if (err && typeof err === 'object') {
const errObj = err as Record<string, unknown>;
if ('message' in errObj) {
errorMessage = String(errObj.message);
}
if ('type' in errObj) {
errorMessage = `${errObj.type}: ${errObj.message || 'No message'}`;
}
} else {
errorMessage = String(err);
}
const errorDetails =
err instanceof Error && err.stack ? err.stack : JSON.stringify(err, null, 2);
console.error(`FALLBACK from model ${modelId}. Error: ${errorMessage}`);
console.error('Error details:', errorDetails);
},
}); });
return _gpt5Instance; return _gpt5Instance;

View File

@ -34,7 +34,28 @@ function initializeGPT5() {
models, models,
modelResetInterval: 60000, modelResetInterval: 60000,
retryAfterOutput: true, retryAfterOutput: true,
onError: (err) => console.error(`FALLBACK. Here is the error: ${err}`), onError: (err, modelId) => {
// Handle various error formats
let errorMessage = 'Unknown error';
if (err instanceof Error) {
errorMessage = err.message;
} else if (err && typeof err === 'object') {
const errObj = err as Record<string, unknown>;
if ('message' in errObj) {
errorMessage = String(errObj.message);
}
if ('type' in errObj) {
errorMessage = `${errObj.type}: ${errObj.message || 'No message'}`;
}
} else {
errorMessage = String(err);
}
const errorDetails =
err instanceof Error && err.stack ? err.stack : JSON.stringify(err, null, 2);
console.error(`FALLBACK from model ${modelId}. Error: ${errorMessage}`);
console.error('Error details:', errorDetails);
},
}); });
return _gpt5Instance; return _gpt5Instance;

View File

@ -34,7 +34,28 @@ function initializeGPT5() {
models, models,
modelResetInterval: 60000, modelResetInterval: 60000,
retryAfterOutput: true, retryAfterOutput: true,
onError: (err) => console.error(`FALLBACK. Here is the error: ${err}`), onError: (err, modelId) => {
// Handle various error formats
let errorMessage = 'Unknown error';
if (err instanceof Error) {
errorMessage = err.message;
} else if (err && typeof err === 'object') {
const errObj = err as Record<string, unknown>;
if ('message' in errObj) {
errorMessage = String(errObj.message);
}
if ('type' in errObj) {
errorMessage = `${errObj.type}: ${errObj.message || 'No message'}`;
}
} else {
errorMessage = String(err);
}
const errorDetails =
err instanceof Error && err.stack ? err.stack : JSON.stringify(err, null, 2);
console.error(`FALLBACK from model ${modelId}. Error: ${errorMessage}`);
console.error('Error details:', errorDetails);
},
}); });
return _gpt5Instance; return _gpt5Instance;

View File

@ -24,14 +24,26 @@ function initializeHaiku35() {
} }
} }
// Only include Vertex if credentials are available // Only include Vertex if all required credentials are available
if (process.env.VERTEX_CLIENT_EMAIL && process.env.VERTEX_PRIVATE_KEY) { if (
process.env.VERTEX_CLIENT_EMAIL &&
process.env.VERTEX_PRIVATE_KEY &&
process.env.VERTEX_PROJECT
) {
try { try {
models.push(vertexModel('claude-3-5-haiku@20241022')); models.push(vertexModel('claude-3-5-haiku@20241022'));
console.info('Haiku35: Vertex AI model added to fallback chain'); console.info('Haiku35: Vertex AI model added to fallback chain (fallback)');
} catch (error) { } catch (error) {
console.warn('Haiku35: Failed to initialize Vertex AI model:', error); console.warn('Haiku35: Failed to initialize Vertex AI model:', error);
} }
} else {
const missing = [];
if (!process.env.VERTEX_CLIENT_EMAIL) missing.push('VERTEX_CLIENT_EMAIL');
if (!process.env.VERTEX_PRIVATE_KEY) missing.push('VERTEX_PRIVATE_KEY');
if (!process.env.VERTEX_PROJECT) missing.push('VERTEX_PROJECT');
console.info(
`Haiku35: Missing Vertex credentials (${missing.join(', ')}), skipping Vertex model`
);
} }
// Ensure we have at least one model // Ensure we have at least one model
@ -47,7 +59,28 @@ function initializeHaiku35() {
models, models,
modelResetInterval: 60000, modelResetInterval: 60000,
retryAfterOutput: true, retryAfterOutput: true,
onError: (err) => console.error(`FALLBACK. Here is the error: ${err}`), onError: (err, modelId) => {
// Handle various error formats
let errorMessage = 'Unknown error';
if (err instanceof Error) {
errorMessage = err.message;
} else if (err && typeof err === 'object') {
const errObj = err as Record<string, unknown>;
if ('message' in errObj) {
errorMessage = String(errObj.message);
}
if ('type' in errObj) {
errorMessage = `${errObj.type}: ${errObj.message || 'No message'}`;
}
} else {
errorMessage = String(err);
}
const errorDetails =
err instanceof Error && err.stack ? err.stack : JSON.stringify(err, null, 2);
console.error(`FALLBACK from model ${modelId}. Error: ${errorMessage}`);
console.error('Error details:', errorDetails);
},
}); });
return _haiku35Instance; return _haiku35Instance;

View File

@ -18,20 +18,34 @@ function initializeSonnet4() {
if (process.env.ANTHROPIC_API_KEY) { if (process.env.ANTHROPIC_API_KEY) {
try { try {
models.push(anthropicModel('claude-4-sonnet-20250514')); models.push(anthropicModel('claude-4-sonnet-20250514'));
console.info('Sonnet4: Anthropic model added to fallback chain'); console.info('Sonnet4: Anthropic model added to fallback chain (primary)');
} catch (error) { } catch (error) {
console.warn('Sonnet4: Failed to initialize Anthropic model:', error); console.warn('Sonnet4: Failed to initialize Anthropic model:', error);
} }
} else {
console.info('Sonnet4: No ANTHROPIC_API_KEY found, skipping Anthropic model');
} }
// Only include Vertex if credentials are available // Only include Vertex if all required credentials are available
if (process.env.VERTEX_CLIENT_EMAIL && process.env.VERTEX_PRIVATE_KEY) { if (
process.env.VERTEX_CLIENT_EMAIL &&
process.env.VERTEX_PRIVATE_KEY &&
process.env.VERTEX_PROJECT
) {
try { try {
models.push(vertexModel('claude-sonnet-4@20250514')); models.push(vertexModel('claude-sonnet-4@20250514'));
console.info('Sonnet4: Vertex AI model added to fallback chain'); console.info('Sonnet4: Vertex AI model added to fallback chain (fallback)');
} catch (error) { } catch (error) {
console.warn('Sonnet4: Failed to initialize Vertex AI model:', error); console.warn('Sonnet4: Failed to initialize Vertex AI model:', error);
} }
} else {
const missing = [];
if (!process.env.VERTEX_CLIENT_EMAIL) missing.push('VERTEX_CLIENT_EMAIL');
if (!process.env.VERTEX_PRIVATE_KEY) missing.push('VERTEX_PRIVATE_KEY');
if (!process.env.VERTEX_PROJECT) missing.push('VERTEX_PROJECT');
console.info(
`Sonnet4: Missing Vertex credentials (${missing.join(', ')}), skipping Vertex model`
);
} }
// Ensure we have at least one model // Ensure we have at least one model
@ -47,7 +61,28 @@ function initializeSonnet4() {
models, models,
modelResetInterval: 60000, modelResetInterval: 60000,
retryAfterOutput: true, retryAfterOutput: true,
onError: (err) => console.error(`FALLBACK. Here is the error: ${err}`), onError: (err, modelId) => {
// Handle various error formats
let errorMessage = 'Unknown error';
if (err instanceof Error) {
errorMessage = err.message;
} else if (err && typeof err === 'object') {
const errObj = err as Record<string, unknown>;
if ('message' in errObj) {
errorMessage = String(errObj.message);
}
if ('type' in errObj) {
errorMessage = `${errObj.type}: ${errObj.message || 'No message'}`;
}
} else {
errorMessage = String(err);
}
const errorDetails =
err instanceof Error && err.stack ? err.stack : JSON.stringify(err, null, 2);
console.error(`FALLBACK from model ${modelId}. Error: ${errorMessage}`);
console.error('Error details:', errorDetails);
},
}); });
return _sonnet4Instance; return _sonnet4Instance;