refactor(ai): remove SDK error diagnostics
This commit is contained in:
parent
0960ef48fd
commit
7236eb106b
3 changed files with 17 additions and 60 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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" } })
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue