refactor(ai): remove SDK error diagnostics

This commit is contained in:
Aiden Cline 2026-07-17 10:00:09 -05:00
commit 7236eb106b
3 changed files with 17 additions and 60 deletions

View file

@ -47,14 +47,15 @@ const isSensitiveHeaderName = (name: string) => SENSITIVE_NAME.test(name)
const isSensitiveQueryName = (name: string) => isSensitiveHeaderName(name) || SHORT_QUERY_NAME.test(name)
export const redactHeaders = (headers: Headers.Input, redactedNames: ReadonlyArray<string | RegExp> = []) =>
const redactHeaders = (headers: Headers.Headers, redactedNames: ReadonlyArray<string | RegExp>) =>
Object.fromEntries(
Object.entries(Headers.redact(Headers.fromInput(headers), [...redactedNames, SENSITIVE_NAME])).map(
([name, value]) => [name, String(value)],
),
Object.entries(Headers.redact(headers, [...redactedNames, SENSITIVE_NAME])).map(([name, value]) => [
name,
String(value),
]),
)
export const redactUrl = (value: string) => {
const redactUrl = (value: string) => {
if (!URL.canParse(value)) return REDACTED
const url = new URL(value)
url.searchParams.forEach((_, key) => {
@ -150,12 +151,7 @@ const responseDetails = (
headers: redactHeaders(response.headers, redactedNames),
})
interface RedactionRequest {
readonly url: string
readonly headers: Headers.Input
}
const secretValues = (request: RedactionRequest) => {
const secretValues = (request: HttpClientRequest.HttpClientRequest) => {
const values = new Set<string>()
const add = (value: string) => {
if (value.length < 4) return
@ -180,13 +176,13 @@ const secretValues = (request: RedactionRequest) => {
// Two passes: structural (redact `"name": "value"` and `name=value` patterns
// for any field name that looks sensitive) plus literal (replace any actual
// secret values we sent in the request, in case the response echoes one back).
const redactBody = (body: string, request: RedactionRequest) =>
const redactBody = (body: string, request: HttpClientRequest.HttpClientRequest) =>
Array.from(secretValues(request)).reduce(
(text, secret) => text.split(secret).join(REDACTED),
body.replace(REDACT_JSON_FIELD, `$1"${REDACTED}"`).replace(REDACT_QUERY_FIELD, `$1${REDACTED}`),
)
export const redactResponseBody = (body: string | void, request: RedactionRequest) => {
const responseBody = (body: string | void, request: HttpClientRequest.HttpClientRequest) => {
if (body === undefined) return {}
const redacted = redactBody(body, request)
if (redacted.length <= BODY_LIMIT) return { body: redacted }
@ -202,7 +198,7 @@ const responseHttp = (input: {
readonly request: HttpClientRequest.HttpClientRequest
readonly response: HttpClientResponse.HttpClientResponse
readonly redactedNames: ReadonlyArray<string | RegExp>
readonly body: ReturnType<typeof redactResponseBody>
readonly body: ReturnType<typeof responseBody>
readonly requestId?: string | undefined
readonly rateLimit?: HttpRateLimitDetails | undefined
}) =>
@ -223,7 +219,7 @@ const statusError =
const headers = normalizedHeaders(response.headers)
const retryAfter = retryAfterMs(headers)
const rateLimit = rateLimitDetails(headers, retryAfter)
const details = redactResponseBody(body, request)
const details = responseBody(body, request)
return yield* new LLMError({
module: "RequestExecutor",
method: "execute",

View file

@ -18,9 +18,6 @@ import {
AuthenticationReason,
classifyProviderFailure,
FinishReason,
HttpContext,
HttpRequestDetails,
HttpResponseDetails,
InvalidRequestReason,
InvalidProviderOutputReason,
LLMEvent,
@ -50,7 +47,7 @@ import {
TypeValidationError,
UnsupportedFunctionalityError,
} from "@ai-sdk/provider"
import { Auth, Endpoint, RequestExecutor, type AnyRoute } from "@opencode-ai/ai/route"
import { Auth, Endpoint, type AnyRoute } from "@opencode-ai/ai/route"
import { Cause, Context, Effect, Layer, Option, Schema, Scope, Stream } from "effect"
import { ModelV2 } from "./model"
import { ProviderV2 } from "./provider"
@ -784,27 +781,13 @@ function apiCallReason(error: APICallError) {
const code = apiFailureCode(error.data) ?? apiFailureCode(error.responseBody)
if (error.statusCode === undefined) {
if (code) return classifyProviderFailure({ message: error.message, code })
if (error.isRetryable)
return new TransportReason({ message: error.message, url: RequestExecutor.redactUrl(error.url) })
if (error.isRetryable) return new TransportReason({ message: error.message })
return new UnknownProviderReason({ message: error.message })
}
const body = RequestExecutor.redactResponseBody(error.responseBody, {
url: error.url,
headers: error.responseHeaders ?? {},
})
return classifyProviderFailure({
message: error.message,
status: error.statusCode,
code,
retryAfterMs: retryAfterMs(error.responseHeaders),
http: new HttpContext({
request: new HttpRequestDetails({ method: "POST", url: RequestExecutor.redactUrl(error.url), headers: {} }),
response: new HttpResponseDetails({
status: error.statusCode,
headers: RequestExecutor.redactHeaders(error.responseHeaders ?? {}),
}),
...body,
}),
})
}
@ -825,18 +808,6 @@ const TRANSPORT_CONNECTION_CODES = new Set([
"UND_ERR_SOCKET",
])
function retryAfterMs(headers: Record<string, string> | undefined) {
if (!headers) return undefined
const millis = Number(headers["retry-after-ms"])
if (Number.isFinite(millis)) return Math.max(0, millis)
const value = headers["retry-after"]
if (!value) return undefined
const seconds = Number(value)
if (Number.isFinite(seconds)) return Math.max(0, seconds * 1000)
const date = Date.parse(value)
return Number.isNaN(date) ? undefined : Math.max(0, date - Date.now())
}
function field(error: unknown, name: string) {
return typeof error === "object" && error !== null ? Reflect.get(error, name) : undefined
}

View file

@ -288,31 +288,21 @@ it.effect("projects replay metadata onto AI SDK prompt parts", () =>
}),
)
it.effect("classifies AI SDK API failures with redacted diagnostics", () =>
it.effect("classifies AI SDK API failures", () =>
Effect.gen(function* () {
const error = yield* streamFailure(
failingLanguage(
new APICallError({
message: "Quota exceeded",
url: "https://provider.test/v1?key=url-secret",
url: "https://provider.test/v1",
requestBodyValues: {},
statusCode: 429,
responseHeaders: { "retry-after-ms": "250", "x-api-key": "header-secret" },
responseBody: '{"error":{"code":"insufficient_quota","api_key":"body-secret"}}',
responseBody: '{"error":{"code":"insufficient_quota"}}',
}),
),
)
expect(error).toMatchObject({
reason: {
_tag: "QuotaExceeded",
http: {
request: { url: "https://provider.test/v1?key=%3Credacted%3E" },
response: { headers: { "x-api-key": "<redacted>" } },
body: '{"error":{"code":"insufficient_quota","api_key":"<redacted>"}}',
},
},
})
expect(error).toMatchObject({ reason: { _tag: "QuotaExceeded" } })
}),
)