Skip to content

Commit 1f3534c

Browse files
committed
fix: smart retry classification, Bedrock 200K context cap, batch error details
- retry.ts: respect isRetryable:false and 4xx status in JSON error bodies (Issue #7) - provider.ts: cap Bedrock Anthropic models at 200K context (Issue #4) - batch.ts: include per-tool error details in failure output (Issue #9) - retry.test.ts: 5 new tests for catch-all retry classification
1 parent 7ede44d commit 1f3534c

File tree

4 files changed

+59
-1
lines changed

4 files changed

+59
-1
lines changed

packages/opencode/src/provider/provider.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -740,6 +740,17 @@ export namespace Provider {
740740

741741
m.variants = mapValues(ProviderTransform.variants(m), (v) => v)
742742

743+
// Bedrock enforces 200K context unless the context-1m beta header is sent.
744+
// models-snapshot.ts (auto-generated) lists capability (1M) not runtime limit.
745+
const BEDROCK_CONTEXT_CAP = 200_000
746+
if (
747+
provider.id === "amazon-bedrock" &&
748+
m.limit.context > BEDROCK_CONTEXT_CAP &&
749+
m.id.includes("anthropic")
750+
) {
751+
m.limit.context = BEDROCK_CONTEXT_CAP
752+
}
753+
743754
return m
744755
}
745756

@@ -885,6 +896,16 @@ export namespace Provider {
885896
pickBy(merged, (v) => !v.disabled),
886897
(v) => omit(v, ["disabled"]),
887898
)
899+
// Bedrock enforces 200K context unless the context-1m beta header is sent.
900+
// models-snapshot.ts (auto-generated) lists capability (1M) not runtime limit.
901+
const BEDROCK_CONTEXT_CAP = 200_000
902+
if (
903+
providerID === "amazon-bedrock" &&
904+
parsedModel.limit.context > BEDROCK_CONTEXT_CAP &&
905+
parsedModel.id.includes("anthropic")
906+
) {
907+
parsedModel.limit.context = BEDROCK_CONTEXT_CAP
908+
}
888909
parsed.models[modelID] = parsedModel
889910
}
890911
database[providerID] = parsed

packages/opencode/src/session/retry.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,11 @@ export namespace SessionRetry {
9393
if (json.type === "error" && json.error?.code?.includes("rate_limit")) {
9494
return "Rate Limited"
9595
}
96+
// Respect explicit isRetryable: false from provider SDKs (e.g. Bedrock)
97+
if (json.isRetryable === false) return undefined
98+
// 4xx errors are client errors — not retryable
99+
const status = typeof json.status === "number" ? json.status : typeof json.statusCode === "number" ? json.statusCode : undefined
100+
if (status !== undefined && status >= 400 && status < 500) return undefined
96101
return JSON.stringify(json)
97102
} catch {
98103
return undefined

packages/opencode/src/tool/batch.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,14 @@ export const BatchTool = Tool.define("batch", async () => {
161161

162162
const outputMessage =
163163
failedCalls > 0
164-
? `Executed ${successfulCalls}/${results.length} tools successfully. ${failedCalls} failed.`
164+
? [
165+
`Executed ${successfulCalls}/${results.length} tools successfully. ${failedCalls} failed.`,
166+
"",
167+
"Failed tools:",
168+
...results
169+
.filter((r) => !r.success)
170+
.map((r) => `- ${r.tool}: ${r.error instanceof Error ? r.error.message : String(r.error)}`),
171+
].join("\n")
165172
: `All ${successfulCalls} tools executed successfully.\n\nKeep using the batch tool for optimal performance in your next response!`
166173

167174
return {

packages/opencode/test/session/retry.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,31 @@ describe("session.retry.retryable", () => {
113113
expect(SessionRetry.retryable(error)).toBeUndefined()
114114
})
115115

116+
test("does not retry when json has isRetryable: false", () => {
117+
const error = wrap(JSON.stringify({ isRetryable: false, message: "prompt is too long" }))
118+
expect(SessionRetry.retryable(error)).toBeUndefined()
119+
})
120+
121+
test("does not retry 400 status in json body", () => {
122+
const error = wrap(JSON.stringify({ status: 400, message: "Bad request" }))
123+
expect(SessionRetry.retryable(error)).toBeUndefined()
124+
})
125+
126+
test("does not retry 403 statusCode in json body", () => {
127+
const error = wrap(JSON.stringify({ statusCode: 403, message: "Forbidden" }))
128+
expect(SessionRetry.retryable(error)).toBeUndefined()
129+
})
130+
131+
test("retries 500 status in json body", () => {
132+
const error = wrap(JSON.stringify({ status: 500, message: "Internal Server Error" }))
133+
expect(SessionRetry.retryable(error)).toBe(JSON.stringify({ status: 500, message: "Internal Server Error" }))
134+
})
135+
136+
test("retries json without status or isRetryable fields", () => {
137+
const error = wrap(JSON.stringify({ error: { message: "some_unknown_error" } }))
138+
expect(SessionRetry.retryable(error)).toBe(JSON.stringify({ error: { message: "some_unknown_error" } }))
139+
})
140+
116141
test("does not retry context overflow errors", () => {
117142
const error = new MessageV2.ContextOverflowError({
118143
message: "Input exceeds context window of this model",

0 commit comments

Comments
 (0)