diff --git a/packages/ai/src/route/executor.ts b/packages/ai/src/route/executor.ts index dce362a644..bd3359e555 100644 --- a/packages/ai/src/route/executor.ts +++ b/packages/ai/src/route/executor.ts @@ -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 = []) => +const redactHeaders = (headers: Headers.Headers, redactedNames: ReadonlyArray) => 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() 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 - readonly body: ReturnType + readonly body: ReturnType 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", diff --git a/packages/core/src/aisdk.ts b/packages/core/src/aisdk.ts index 160f2c92f5..00337d8d23 100644 --- a/packages/core/src/aisdk.ts +++ b/packages/core/src/aisdk.ts @@ -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 | 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 } diff --git a/packages/core/test/aisdk.test.ts b/packages/core/test/aisdk.test.ts index 5a82de2773..a6d9ff2a63 100644 --- a/packages/core/test/aisdk.test.ts +++ b/packages/core/test/aisdk.test.ts @@ -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": "" } }, - body: '{"error":{"code":"insufficient_quota","api_key":""}}', - }, - }, - }) + expect(error).toMatchObject({ reason: { _tag: "QuotaExceeded" } }) }), )