Skip to content
Closed
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
14 changes: 9 additions & 5 deletions packages/app-bundle/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@
"packages/core/test/location-mutation.test.ts": "2c4c8faa220cc8fabfe12d57bd74ec8dd8aac8485d0ecafd2761cdee1806d6b0",
"packages/core/test/tool-question.test.ts": "4df41393f15104a1d3a9c3d06e30cef515108490b41fcf4d9a7d9399fd4ed945",
"packages/llm/src/cache-policy.ts": "04c1fb5d3d061cfa95d04e54df0a1c2cdaafac921224242446eaa2968bff7857",
"packages/llm/src/provider-error.ts": "65b9124905f0755c1e368df5150aa211bea3cc29afb66845b5ceaccba10042dd",
"packages/llm/test/cache-policy.test.ts": "e921095c4301440c786105063439e49b2fedbaf6d46f30457e0a96388d268f4b",
"packages/llm/test/provider-error.test.ts": "ac3bf099319acdc27ff47ea79fd74357d55e12ab95131a07a72a5a2a9bc0c08f",
"packages/opencode/script/build.ts": "876e7d569aa71ce060c1d4b24c96e9bf13245cbf4ca743828d645560a95aa641",
"packages/opencode/script/httpapi-exercise.ts": "772bd2cf5748ca90c5d5790c2d044237518cc8ede0411fc4ba28b9d7440ba28c",
"packages/opencode/test/amicode-release-workflow.test.ts": "f6dfcb3ed9c572ca8e8b21ff82e038872b635b25c68f9580f6d6f51ee2813189",
Expand Down Expand Up @@ -274,7 +276,7 @@
"packages/opencode/src/project/instance-context.ts": "65a35792e50e96a50ea14cd9f928045c0e2fc15eb49506f5bbceee38c4af1457",
"packages/opencode/src/project/instance-store.ts": "c22d3e2499e63dd8d9395d58189df75444d306774df833e746f22e327edcf2ab",
"packages/opencode/src/provider/auth.ts": "b0ab1322c1039e27176bf9e3500603eae27506065ac141a69710220f3361f6b8",
"packages/opencode/src/provider/error.ts": "7f3c8210c775e2ce98140bf015dc97e61192912af062c75e18ecb09ee2736a0f",
"packages/opencode/src/provider/error.ts": "d72310be26cfae5c2943f9939a6461e848532a5646ff405d628e9a50fc1ebe91",
"packages/opencode/src/provider/provider.ts": "671f6d8de83f919ddaa69b8240cacd82e2b8202ab505d4334175f2858bbd8c32",
"packages/opencode/src/provider/transform.ts": "d35a95a6092b5dd9cc8e37adb4d52f9ad9880d021cb3119c1d4abbc4b4fb0772",
"packages/opencode/src/server/server.ts": "56b3c2047917aa80d1dfb29bfa9839bcc2ad7f5a6f4db12103cd962a98fba866",
Expand Down Expand Up @@ -344,7 +346,7 @@
"packages/opencode/test/session/business-record.test.ts": "7857094479408bcfd6ea8d9f5b425c6384bcf8490f930674bc1c203e82782d75",
"packages/opencode/test/session/instruction.test.ts": "8b4d954eb10afa852a671e6c0a94d19604deacc9a62f9c3651c7456cc32736b7",
"packages/opencode/test/session/llm-native-recorded.test.ts": "7139b58569af0172862af521e8f8d7a78132f40f06dcceb4baf8ca3a2f07d3bd",
"packages/opencode/test/session/message-v2.test.ts": "9edc16ae57b7f2d34aaed7378d82fcf9d07a82075a05730bfb3caae39d70ac7f",
"packages/opencode/test/session/message-v2.test.ts": "d1b930b4b57243387920b76fb68680e507e3a288d50d06c357d83eb7931da735",
"packages/opencode/test/session/mutation.test.ts": "ccb82b6858ac097d2af09a94497d05f6a0ff843b928a69e763fdb510905a10cd",
"packages/opencode/test/session/prompt.test.ts": "e3d0748edb833640ef284857ae725e4b974843480d4128f56239e3fd1e6d481f",
"packages/opencode/test/session/prose-guard.test.ts": "7239a758cbf27df4c3879fa9ee0374964559c1461773c2f5a85196f64632e6c4",
Expand Down Expand Up @@ -849,7 +851,7 @@
"packages/ui/src/v2/components/text-shimmer-v2.tsx"
],
"exceptions": [],
"extracted_at": "2026-09-21T02:29:31.939Z",
"extracted_at": "2026-09-21T17:30:46.454Z",
"per_package": {
"packages/app": {
"A": 171,
Expand All @@ -863,7 +865,7 @@
},
"packages/llm": {
"A": 0,
"M": 4,
"M": 6,
"D": 0
},
"packages/opencode": {
Expand Down Expand Up @@ -893,7 +895,7 @@
}
},
"counts": {
"overlay_total": 827,
"overlay_total": 829,
"deletions": 7,
"server_coupled": 0
},
Expand Down Expand Up @@ -933,7 +935,9 @@
"packages/core/test/location-mutation.test.ts": "M",
"packages/core/test/tool-question.test.ts": "M",
"packages/llm/src/cache-policy.ts": "M",
"packages/llm/src/provider-error.ts": "M",
"packages/llm/test/cache-policy.test.ts": "M",
"packages/llm/test/provider-error.test.ts": "M",
"packages/opencode/script/build.ts": "M",
"packages/opencode/script/httpapi-exercise.ts": "M",
"packages/opencode/test/amicode-release-workflow.test.ts": "A",
Expand Down
55 changes: 55 additions & 0 deletions packages/app-bundle/overlay/packages/llm/src/provider-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Schema } from "effect"
import { LLMError, ProviderErrorEvent } from "./schema"

const patterns = [
/prompt is too long/i,
/request_too_large/i,
/input is too long for requested model/i,
/exceeds the context window/i,
/exceeds (?:the )?(?:model'?s )?maximum context length(?: of [\d,]+ tokens?|\s*\([\d,]+\))/i,
/input token count.*exceeds the maximum/i,
/tokens in request more than max tokens allowed/i,
/maximum prompt length is \d+/i,
/reduce the length of the messages/i,
/maximum context length is \d+ tokens/i,
/exceeds (?:the )?maximum allowed input length of [\d,]+ tokens?/i,
/input \(\d+ tokens\) is longer than the model'?s context length \(\d+ tokens\)/i,
// Tightened: require a context/input/prompt noun so throughput/quota limits
// like "exceeds the limit of 100000 tokens per minute" don't false-positive.
/(?:input|context|prompt|message).*exceeds the limit of \d+/i,
/exceeds the available context size/i,
/greater than the context length/i,
/context window exceeds limit/i,
/exceeded model token limit/i,
/context[_ ]length[_ ]exceeded/i,
/request entity too large/i,
/context length is only \d+ tokens/i,
/input length.*exceeds.*context length/i,
/prompt too long; exceeded (?:max )?context length/i,
/too large for model with \d+ maximum context length/i,
/prompt has [\d,]+ tokens?, but the configured context size is [\d,]+ tokens?/i,
/model_context_window_exceeded/i,
/^too many tokens$/i,
/^token limit exceeded$/i,
]

const exclusions = [
/^(throttling error|service unavailable):/i,
/rate limit/i,
/too many requests/i,
// Bedrock ThrottlingException messages that match broad patterns above but
// are throughput/quota limits, not context-window overflow.
/throughput/i,
/per\s+(minute|second|hour|day)/i,
/invocation.*(limit|quota|exceeded)/i,
/quota/i,
]

export const isContextOverflow = (message: string) =>
!exclusions.some((pattern) => pattern.test(message)) &&
(patterns.some((pattern) => pattern.test(message)) || /^4(00|13)\s*(status code)?\s*\(no body\)/i.test(message))

export const isContextOverflowFailure = (failure: unknown) =>
failure instanceof LLMError
? failure.reason._tag === "InvalidRequest" && failure.reason.classification === "context-overflow"
: Schema.is(ProviderErrorEvent)(failure) && failure.classification === "context-overflow"
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { describe, expect, test } from "bun:test"
import { isContextOverflow } from "../src"

describe("provider error classification", () => {
test("classifies provider token limit messages as context overflow", () => {
const messages = [
"tokens in request more than max tokens allowed",
'{"error":{"type":"request_too_large","message":"Request exceeds the maximum size"}}',
"Requested token count exceeds the model's maximum context length of 131072 tokens.",
"Input length (265330) exceeds model's maximum context length (262144).",
"Input length 131393 exceeds the maximum allowed input length of 131040 tokens.",
"The input (516368 tokens) is longer than the model's context length (262144 tokens).",
"Prompt has 5,958,968 tokens, but the configured context size is 256,000 tokens",
"Too many tokens",
"Token limit exceeded",
]

expect(messages.every(isContextOverflow)).toBe(true)
})

test("does not classify rate limits as context overflow", () => {
const messages = [
"Throttling error: Too many tokens, please wait before trying again.",
"Rate limit exceeded, please retry after 30 seconds.",
"Too many requests. Please slow down.",
"Monthly token limit exceeded",
]

expect(messages.some(isContextOverflow)).toBe(false)
})

test("does not classify Bedrock throttling/quota messages as context overflow", () => {
const messages = [
// ThrottlingException messages that match broad patterns (too many tokens,
// token limit exceeded, exceeds the limit) but are throughput limits.
"Token throughput limit exceeded",
"Too many tokens per minute for model anthropic.claude-opus-4-8-20250808-v1:0",
"You have exceeded the model invocation throughput limit of 100000 tokens per minute",
"Model invocation throughput limit exceeded",
"Insufficient quota for this request",
"Request quota exceeded, please try again later",
"Token throughput quota exceeded: too many tokens per second",
]

expect(messages.some(isContextOverflow)).toBe(false)
})

test("still classifies real Bedrock context overflow as context overflow", () => {
const messages = [
"Input is too long for requested model. Max input token count: 200000, actual token count: 250000",
"Input is too long for requested model",
"Input length 265330 exceeds the context window of 200000 tokens",
]

expect(messages.every(isContextOverflow)).toBe(true)
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,15 @@ export type ParsedAPICallError =
export function parseAPICallError(input: { providerID: ProviderV2.ID; error: APICallError }): ParsedAPICallError {
const m = message(input.providerID, input.error)
const body = json(input.error.responseBody)
if (isContextOverflow(m) || input.error.statusCode === 413 || body?.error?.code === "context_length_exceeded") {
// Status-code guard: 429 (rate limit) and 5xx (server error) are never
// context overflow, even when the error message text happens to match the
// heuristic patterns (e.g. Bedrock ThrottlingException "token throughput
// limit exceeded" matches /token limit exceeded/i but is a rate limit).
// The `undefined` status case (stream errors without HTTP metadata) still
// runs the heuristic — only known non-overflow statuses are excluded.
const status = input.error.statusCode
const overflowEligible = status === undefined || (status >= 400 && status < 500 && status !== 429)
if (overflowEligible && (isContextOverflow(m) || status === 413 || body?.error?.code === "context_length_exceeded")) {
return {
type: "context_overflow",
message: m,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1538,6 +1538,56 @@ describe("session.message-v2.fromError", () => {
expect(SessionV1.APIError.isInstance(result)).toBe(true)
})

test("does not classify Bedrock 429 throttling with overflow-matching body as context overflow", () => {
// Bedrock's @ai-sdk/amazon-bedrock adapter coerces undefined to "undefined",
// stripping the SDK's "429 Too Many Requests" prefix that would otherwise
// trigger the "too many requests" exclusion in isContextOverflow. The status
// code guard in parseAPICallError must prevent the raw Bedrock message
// (which matches /token limit exceeded/i) from being misclassified.
const bedrockMessages = [
"Token throughput limit exceeded",
"Too many tokens per minute for model anthropic.claude-opus-4-8-20250808-v1:0",
"You have exceeded the model invocation throughput limit of 100000 tokens per minute",
]

for (const bodyMessage of bedrockMessages) {
const result = MessageV2.fromError(
new APICallError({
message: "undefined",
url: "https://bedrock-runtime.us-east-1.amazonaws.com/model/anthropic.claude-opus-4-8-20250808-v1%3A0/converse-stream",
requestBodyValues: {},
statusCode: 429,
responseHeaders: { "content-type": "application/json" },
responseBody: JSON.stringify({ message: bodyMessage }),
isRetryable: true,
}),
{ providerID: ProviderV2.ID.make("amazon-bedrock") },
)
expect(SessionV1.ContextOverflowError.isInstance(result)).toBe(false)
expect(SessionV1.APIError.isInstance(result)).toBe(true)
}
})

test("does not classify Bedrock 400 throttling with overflow-matching body as context overflow", () => {
// Bedrock ValidationException (HTTP 400) can carry throughput messages that
// match context-overflow patterns. The broadened exclusions in
// isContextOverflow must prevent these from being misclassified.
const result = MessageV2.fromError(
new APICallError({
message: "undefined",
url: "https://bedrock-runtime.us-east-1.amazonaws.com/model/anthropic.claude-opus-4-8-20250808-v1%3A0/converse-stream",
requestBodyValues: {},
statusCode: 400,
responseHeaders: { "content-type": "application/json" },
responseBody: JSON.stringify({ message: "Token throughput limit exceeded" }),
isRetryable: false,
}),
{ providerID: ProviderV2.ID.make("amazon-bedrock") },
)
expect(SessionV1.ContextOverflowError.isInstance(result)).toBe(false)
expect(SessionV1.APIError.isInstance(result)).toBe(true)
})

test("serializes unknown inputs", () => {
const result = MessageV2.fromError(123, { providerID })

Expand Down
Loading