fix(core): retry AI SDK transport failures

This commit is contained in:
Aiden Cline
2026-08-09 23:38:40 -05:00
parent 825f13417b
commit ddf2619760
3 changed files with 36 additions and 8 deletions
+1 -1
View File
@@ -86,7 +86,7 @@ export interface ProviderFailure {
// Keep HTTP failures and provider-reported stream failures on one typed path so
// session retry policy never needs provider-specific string matching.
export function classifyProviderFailure(input: ProviderFailure): AIError["reason"] {
export function classifyProviderFailure(input: ProviderFailure) {
const body = input.http?.body ?? ""
const codes = [input.code, ...providerCodes(body), ...providerCodes(input.message)]
.filter((code): code is string => code !== undefined)
+19 -7
View File
@@ -23,6 +23,7 @@ import {
LanguageModel,
ProviderID,
ProviderMetadata,
TransportReason,
ToolResultValue,
UnknownProviderReason,
type ContentPart,
@@ -725,13 +726,7 @@ function llmError(method: string, error: unknown) {
error instanceof AIError
? new InvalidProviderOutputReason({ message: error.message })
: APICallError.isInstance(error)
? RequestExecutor.classifyHttpFailure({
message: unknownErrorMessage(error),
url: error.url,
status: error.statusCode,
responseHeaders: error.responseHeaders,
responseBody: error.responseBody,
})
? apiCallErrorReason(error)
: new UnknownProviderReason({ message: unknownErrorMessage(error) })
return new AIError({
module: "AISDK",
@@ -740,6 +735,23 @@ function llmError(method: string, error: unknown) {
})
}
function apiCallErrorReason(error: APICallError) {
const reason = RequestExecutor.classifyHttpFailure({
message: unknownErrorMessage(error),
url: error.url,
status: error.statusCode,
responseHeaders: error.responseHeaders,
responseBody: error.responseBody,
})
if (error.statusCode !== undefined || !error.isRetryable) return reason
return new TransportReason({
message: reason.message,
kind: error.name,
url: error.url,
http: reason.http,
})
}
const ProviderErrorBody = Schema.Struct({
message: Schema.optionalKey(Schema.String),
code: Schema.optionalKey(Schema.String),
+16
View File
@@ -1,6 +1,7 @@
import { APICallError } from "@ai-sdk/provider"
import type { LanguageModelV3, LanguageModelV3StreamPart } from "@ai-sdk/provider"
import { AISDK } from "@opencode-ai/core/aisdk"
import { SessionRunnerRetry } from "@opencode-ai/core/session/runner/retry"
import { toSessionError } from "@opencode-ai/core/session/to-session-error"
import { Model } from "@opencode-ai/core/model"
import { Provider } from "@opencode-ai/core/provider"
@@ -428,6 +429,20 @@ it.effect("classifies retryable AI SDK failures with retry-after details", () =>
}),
)
it.effect("retries status-less AI SDK transport failures", () =>
Effect.gen(function* () {
const error = yield* streamFailure(
apiCallError({
message: "Cannot connect to API: connection refused",
isRetryable: true,
}),
)
expect(error.reason).toMatchObject({ _tag: "Transport", kind: "AI_APICallError" })
expect(SessionRunnerRetry.isRetryable(error)).toBeTrue()
expect(toSessionError(error).http?.request.url).toBe("https://api.example.com/chat")
}),
)
it.effect("prefers a structured provider message over the code fallback", () =>
Effect.gen(function* () {
const error = yield* streamFailure(
@@ -445,6 +460,7 @@ it.effect("falls back to the status alone for malformed response bodies", () =>
const error = yield* streamFailure(
apiCallError({
statusCode: 502,
isRetryable: false,
responseBody: "<html>Bad Gateway</html>",
}),
)