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:
xu0o0
2026-03-30 23:26:03 +08:00
committed by GitHub
parent 0c64c1ce07
commit a3bf1eb399
3 changed files with 79 additions and 15 deletions

View File

@@ -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,

View File

@@ -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;
}
}

View File

@@ -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);
});
});