fix(ai): fix bedrock throttling misidentification (#2699)
- Add NON_OVERFLOW_PATTERNS to explicitly exclude known non-overflow errors
- Consolidate Cerebras 400/413 no-body check into OVERFLOW_PATTERNS
- Format Bedrock errors as ${error.name}: ${error.message} for pattern matching
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
BedrockRuntimeClient,
|
||||
type BedrockRuntimeClientConfig,
|
||||
BedrockRuntimeServiceException,
|
||||
StopReason as BedrockStopReason,
|
||||
type Tool as BedrockTool,
|
||||
CachePointType,
|
||||
@@ -185,15 +186,15 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream", BedrockOpt
|
||||
} else if (item.metadata) {
|
||||
handleMetadata(item.metadata, model, output);
|
||||
} else if (item.internalServerException) {
|
||||
throw new Error(`Internal server error: ${item.internalServerException.message}`);
|
||||
throw item.internalServerException;
|
||||
} else if (item.modelStreamErrorException) {
|
||||
throw new Error(`Model stream error: ${item.modelStreamErrorException.message}`);
|
||||
throw item.modelStreamErrorException;
|
||||
} else if (item.validationException) {
|
||||
throw new Error(`Validation error: ${item.validationException.message}`);
|
||||
throw item.validationException;
|
||||
} else if (item.throttlingException) {
|
||||
throw new Error(`Throttling error: ${item.throttlingException.message}`);
|
||||
throw item.throttlingException;
|
||||
} else if (item.serviceUnavailableException) {
|
||||
throw new Error(`Service unavailable: ${item.serviceUnavailableException.message}`);
|
||||
throw item.serviceUnavailableException;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,7 +214,7 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream", BedrockOpt
|
||||
delete (block as Block).partialJson;
|
||||
}
|
||||
output.stopReason = options.signal?.aborted ? "aborted" : "error";
|
||||
output.errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
|
||||
output.errorMessage = formatBedrockError(error);
|
||||
stream.push({ type: "error", reason: output.stopReason, error: output });
|
||||
stream.end();
|
||||
}
|
||||
@@ -222,6 +223,36 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream", BedrockOpt
|
||||
return stream;
|
||||
};
|
||||
|
||||
/**
|
||||
* Human-readable prefixes for Bedrock SDK exception names.
|
||||
* The downstream retry logic in agent-session matches patterns like
|
||||
* `server.?error` and `service.?unavailable`, so we preserve the legacy
|
||||
* prefix format rather than using the raw SDK exception name.
|
||||
*/
|
||||
const BEDROCK_ERROR_PREFIXES: Record<string, string> = {
|
||||
InternalServerException: "Internal server error",
|
||||
ModelStreamErrorException: "Model stream error",
|
||||
ValidationException: "Validation error",
|
||||
ThrottlingException: "Throttling error",
|
||||
ServiceUnavailableException: "Service unavailable",
|
||||
};
|
||||
|
||||
/**
|
||||
* Format a Bedrock error with a human-readable prefix.
|
||||
* AWS SDK exceptions (both from `client.send()` and from stream event items)
|
||||
* extend BedrockRuntimeServiceException. We map the `.name` to a stable
|
||||
* human-readable prefix so downstream consumers (retry logic, context-overflow
|
||||
* detection) can distinguish error categories via simple string matching.
|
||||
*/
|
||||
function formatBedrockError(error: unknown): string {
|
||||
const message = error instanceof Error ? error.message : JSON.stringify(error);
|
||||
if (error instanceof BedrockRuntimeServiceException) {
|
||||
const prefix = BEDROCK_ERROR_PREFIXES[error.name] ?? error.name;
|
||||
return `${prefix}: ${message}`;
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
export const streamSimpleBedrock: StreamFunction<"bedrock-converse-stream", SimpleStreamOptions> = (
|
||||
model: Model<"bedrock-converse-stream">,
|
||||
context: Context,
|
||||
|
||||
@@ -19,7 +19,7 @@ import type { AssistantMessage } from "../types.js";
|
||||
* - GitHub Copilot: "prompt token count of X exceeds the limit of Y"
|
||||
* - MiniMax: "invalid params, context window exceeds limit"
|
||||
* - Kimi For Coding: "Your request exceeded model token limit: X (requested: Y)"
|
||||
* - Cerebras: Returns "400/413 status code (no body)" - handled separately below
|
||||
* - Cerebras: "400/413 status code (no body)"
|
||||
* - Mistral: "Prompt contains X tokens ... too large for model with Y maximum context length"
|
||||
* - z.ai: Does NOT error, accepts overflow silently - handled via usage.input > contextWindow
|
||||
* - Ollama: Some deployments truncate silently, others return errors like "prompt too long; exceeded max context length by X tokens"
|
||||
@@ -43,6 +43,22 @@ const OVERFLOW_PATTERNS = [
|
||||
/context[_ ]length[_ ]exceeded/i, // Generic fallback
|
||||
/too many tokens/i, // Generic fallback
|
||||
/token limit exceeded/i, // Generic fallback
|
||||
/^4(?:00|13)\s*(?:status code)?\s*\(no body\)/i, // Cerebras: 400/413 with no body
|
||||
];
|
||||
|
||||
/**
|
||||
* Patterns that indicate non-overflow errors (e.g. rate limiting, server errors).
|
||||
* Error messages matching any of these are excluded from overflow detection
|
||||
* even if they also match an OVERFLOW_PATTERN.
|
||||
*
|
||||
* Example: Bedrock formats throttling errors as "ThrottlingException: Too many tokens,
|
||||
* please wait before trying again." which would match the /too many tokens/i overflow
|
||||
* pattern without this exclusion.
|
||||
*/
|
||||
const NON_OVERFLOW_PATTERNS = [
|
||||
/^(Throttling error|Service unavailable):/i, // AWS Bedrock non-overflow errors (human-readable prefixes from formatBedrockError)
|
||||
/rate limit/i, // Generic rate limiting
|
||||
/too many requests/i, // Generic HTTP 429 style
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -94,14 +110,9 @@ const OVERFLOW_PATTERNS = [
|
||||
export function isContextOverflow(message: AssistantMessage, contextWindow?: number): boolean {
|
||||
// Case 1: Check error message patterns
|
||||
if (message.stopReason === "error" && message.errorMessage) {
|
||||
// Check known patterns
|
||||
if (OVERFLOW_PATTERNS.some((p) => p.test(message.errorMessage!))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Cerebras returns 400/413 with no body for context overflow
|
||||
// Note: 429 is rate limiting (requests/tokens per time), NOT context overflow
|
||||
if (/^4(00|13)\s*(status code)?\s*\(no body\)/i.test(message.errorMessage)) {
|
||||
// Skip messages matching known non-overflow patterns (e.g. throttling / rate-limit)
|
||||
const isNonOverflow = NON_OVERFLOW_PATTERNS.some((p) => p.test(message.errorMessage!));
|
||||
if (!isNonOverflow && OVERFLOW_PATTERNS.some((p) => p.test(message.errorMessage!))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,4 +39,26 @@ describe("isContextOverflow", () => {
|
||||
const message = createErrorMessage("500 `model runner crashed unexpectedly`");
|
||||
expect(isContextOverflow(message, 32768)).toBe(false);
|
||||
});
|
||||
|
||||
it("does not treat Bedrock throttling 'Too many tokens' as overflow", () => {
|
||||
// Bedrock returns this for HTTP 429 rate limiting, NOT context overflow.
|
||||
// formatBedrockError uses a human-readable prefix for ThrottlingException.
|
||||
const message = createErrorMessage("Throttling error: Too many tokens, please wait before trying again.");
|
||||
expect(isContextOverflow(message, 200000)).toBe(false);
|
||||
});
|
||||
|
||||
it("does not treat Bedrock service unavailable as overflow", () => {
|
||||
const message = createErrorMessage("Service unavailable: The service is temporarily unavailable.");
|
||||
expect(isContextOverflow(message, 200000)).toBe(false);
|
||||
});
|
||||
|
||||
it("does not treat generic rate limit errors as overflow", () => {
|
||||
const message = createErrorMessage("Rate limit exceeded, please retry after 30 seconds.");
|
||||
expect(isContextOverflow(message, 200000)).toBe(false);
|
||||
});
|
||||
|
||||
it("does not treat HTTP 429 style errors as overflow", () => {
|
||||
const message = createErrorMessage("Too many requests. Please slow down.");
|
||||
expect(isContextOverflow(message, 200000)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user