@opencode-ai/ai
Advanced tools
+5
-4
| import { Effect, Schema } from "effect"; | ||
| import { HttpOptions, InvalidRequestReason, AIError, ModelID, ProviderID, ProviderMetadata, Usage, } from "./schema/index.js"; | ||
| import { HttpOptions, InvalidRequestError, AIError, ModelID, ProviderID, ProviderMetadata, Usage, } from "./schema/index.js"; | ||
| import { ImageClient, Service } from "./image-client.js"; | ||
@@ -93,5 +93,6 @@ export class ImageModel { | ||
| catch: (error) => new AIError({ | ||
| module: "Image", | ||
| method: "generate", | ||
| reason: new InvalidRequestReason({ message: error instanceof Error ? error.message : String(error) }), | ||
| reason: new InvalidRequestError({ | ||
| message: error instanceof Error ? error.message : String(error), | ||
| cause: error, | ||
| }), | ||
| }), | ||
@@ -98,0 +99,0 @@ }).pipe(Effect.flatMap((request) => ImageClient.generate(request))); |
+4
-7
| import { Effect, JsonSchema, Schema } from "effect"; | ||
| import { LLMClient, Service } from "./route/client.js"; | ||
| import { GenerationOptions, HttpOptions, InvalidProviderOutputReason, AIError, LLMEvent, LLMRequest, LLMResponse, Message, LanguageModel, SystemPart, ToolChoice, ToolDefinition, } from "./schema/index.js"; | ||
| import { GenerationOptions, HttpOptions, InvalidProviderOutputError, AIError, LLMEvent, LLMRequest, LLMResponse, Message, LanguageModel, SystemPart, ToolChoice, ToolDefinition, } from "./schema/index.js"; | ||
| import { make as makeTool, toDefinitions } from "./tool.js"; | ||
@@ -46,5 +46,3 @@ export const generate = LLMClient.generate; | ||
| return yield* new AIError({ | ||
| module: "LLM", | ||
| method: "generateObject", | ||
| reason: new InvalidProviderOutputReason({ | ||
| reason: new InvalidProviderOutputError({ | ||
| message: `generateObject: model did not call the forced \`${GENERATE_OBJECT_TOOL_NAME}\` tool`, | ||
@@ -54,6 +52,5 @@ }), | ||
| const object = yield* tool._decode(call.input).pipe(Effect.mapError((error) => new AIError({ | ||
| module: "LLM", | ||
| method: "generateObject", | ||
| reason: new InvalidProviderOutputReason({ | ||
| reason: new InvalidProviderOutputError({ | ||
| message: `generateObject: tool input failed schema decode: ${error.message}`, | ||
| cause: error, | ||
| }), | ||
@@ -60,0 +57,0 @@ }))); |
@@ -1170,7 +1170,9 @@ import { Buffer } from "node:buffer"; | ||
| }; | ||
| const onError = (event) => Effect.fail(new AIError({ | ||
| module: ADAPTER, | ||
| method: "stream", | ||
| reason: classifyProviderFailure({ message: providerErrorMessage(event), code: event.error?.type }), | ||
| })); | ||
| const onError = (event) => { | ||
| const message = providerErrorMessage(event); | ||
| const body = ProviderShared.encodeJson(event); | ||
| return Effect.fail(new AIError({ | ||
| reason: classifyProviderFailure({ message, rawBody: body }), | ||
| })); | ||
| }; | ||
| const isKnownStreamBlockType = (type) => type === "text" || | ||
@@ -1177,0 +1179,0 @@ type === "thinking" || |
@@ -510,10 +510,8 @@ import { Effect, Schema } from "effect"; | ||
| if (event.exception) { | ||
| const message = event.exception.details.message ?? event.exception.details.originalMessage ?? "Bedrock Converse stream error"; | ||
| const body = ProviderShared.encodeJson(event); | ||
| return yield* new AIError({ | ||
| module: ADAPTER, | ||
| method: "stream", | ||
| reason: classifyProviderFailure({ | ||
| message: event.exception.details.message ?? | ||
| event.exception.details.originalMessage ?? | ||
| "Bedrock Converse stream error", | ||
| code: event.exception.type, | ||
| message, | ||
| rawBody: body, | ||
| }), | ||
@@ -520,0 +518,0 @@ }); |
| import { EventStreamCodec } from "@smithy/eventstream-codec"; | ||
| import { fromUtf8, toUtf8 } from "@smithy/util-utf8"; | ||
| import { Effect, Stream } from "effect"; | ||
| import { Effect, Encoding, Stream } from "effect"; | ||
| import { AIError, AIErrorReason } from "../schema/index.js"; | ||
| import { Framing } from "../route/framing.js"; | ||
@@ -33,5 +34,7 @@ import { ProviderShared } from "./shared.js"; | ||
| try: () => eventCodec.decode(view.subarray(0, totalLength)), | ||
| catch: (error) => ProviderShared.eventError(route, `Failed to decode Bedrock Converse event-stream frame: ${error instanceof Error ? error.message : String(error)}`), | ||
| catch: (error) => ProviderShared.eventError(route, `Failed to decode Bedrock Converse event-stream frame: ${error instanceof Error ? error.message : String(error)}`, Encoding.encodeBase64(view.subarray(0, totalLength)), error), | ||
| }); | ||
| cursor = { buffer: cursor.buffer, offset: cursor.offset + totalLength }; | ||
| const payload = utf8.decode(decoded.body); | ||
| const body = ProviderShared.encodeJson({ headers: decoded.headers, body: payload }); | ||
| const messageType = decoded.headers[":message-type"]?.value; | ||
@@ -42,3 +45,3 @@ if (messageType === "error") { | ||
| return yield* ProviderShared.eventError(route, [code, message].filter((value) => typeof value === "string").join(": ") || | ||
| "Bedrock Converse event-stream error"); | ||
| "Bedrock Converse event-stream error", body); | ||
| } | ||
@@ -52,3 +55,2 @@ const eventType = messageType === "event" | ||
| continue; | ||
| const payload = utf8.decode(decoded.body); | ||
| if (!payload) | ||
@@ -60,5 +62,12 @@ continue; | ||
| // against ad-hoc `JSON.parse` calls. | ||
| const parsed = (yield* ProviderShared.parseJson(route, payload, "Failed to parse Bedrock Converse event-stream payload")); | ||
| const parsed = (yield* ProviderShared.parseJson(route, payload, "Failed to parse Bedrock Converse event-stream payload").pipe(Effect.mapError((error) => new AIError({ | ||
| reason: AIErrorReason.make({ ...error.reason, message: error.message, cause: error.reason.cause, body }), | ||
| })))); | ||
| delete parsed.p; | ||
| out.push(messageType === "exception" ? { exception: { type: eventType, details: parsed } } : { [eventType]: parsed }); | ||
| out.push({ | ||
| ...(messageType === "exception" | ||
| ? { exception: { type: eventType, details: parsed } } | ||
| : { [eventType]: parsed }), | ||
| rawBody: body, | ||
| }); | ||
| } | ||
@@ -75,4 +84,5 @@ return [cursor, out]; | ||
| id: "aws-event-stream", | ||
| body: (frame) => ("rawBody" in frame && typeof frame.rawBody === "string" ? frame.rawBody : undefined), | ||
| frame: (bytes) => bytes.pipe(Stream.mapAccumEffect(() => initialFrameBuffer, consumeFrames(route))), | ||
| }); | ||
| export * as BedrockEventStream from "./bedrock-event-stream.js"; |
@@ -178,2 +178,3 @@ import { Schema } from "effect"; | ||
| }, string, { | ||
| readonly error?: unknown; | ||
| readonly candidates?: readonly { | ||
@@ -180,0 +181,0 @@ readonly content?: { |
@@ -8,3 +8,4 @@ import { Effect, Option, Schema } from "effect"; | ||
| import { Protocol } from "../route/protocol.js"; | ||
| import { LLMEvent, Usage, } from "../schema/index.js"; | ||
| import { AIError, LLMEvent, Usage, } from "../schema/index.js"; | ||
| import { classifyProviderFailure } from "../provider-error.js"; | ||
| import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared.js"; | ||
@@ -155,2 +156,3 @@ import { GeminiToolSchema } from "./utils/gemini-tool-schema.js"; | ||
| const GeminiEvent = Schema.Struct({ | ||
| error: Schema.optional(Schema.Unknown), | ||
| candidates: optionalNull(Schema.Array(GeminiCandidate)), | ||
@@ -484,2 +486,12 @@ promptFeedback: optionalNull(GeminiPromptFeedback), | ||
| const step = (state, event) => { | ||
| if (ProviderShared.isRecord(event.error) && typeof event.error.message === "string") { | ||
| const body = ProviderShared.encodeJson(event); | ||
| return Effect.fail(new AIError({ | ||
| reason: classifyProviderFailure({ | ||
| message: event.error.message, | ||
| status: typeof event.error.code === "number" ? event.error.code : undefined, | ||
| rawBody: body, | ||
| }), | ||
| })); | ||
| } | ||
| const nextState = { | ||
@@ -486,0 +498,0 @@ ...state, |
@@ -5,3 +5,3 @@ import { Effect, Encoding, Schema } from "effect"; | ||
| import { Auth } from "../route/auth.js"; | ||
| import { InvalidProviderOutputReason, AIError, Usage, mergeHttpOptions, mergeJsonRecords, } from "../schema/index.js"; | ||
| import { AIError, Usage, mergeHttpOptions, mergeJsonRecords } from "../schema/index.js"; | ||
| import { ProviderShared } from "./shared.js"; | ||
@@ -62,7 +62,2 @@ import { ImageInputs } from "./utils/image-input.js"; | ||
| }; | ||
| const invalidOutput = (message, providerMetadata) => new AIError({ | ||
| module: ADAPTER, | ||
| method: "generate", | ||
| reason: new InvalidProviderOutputReason({ message, route: ADAPTER, providerMetadata }), | ||
| }); | ||
| const applyQuery = (url, query) => { | ||
@@ -95,4 +90,4 @@ if (!query) | ||
| const response = yield* execute(HttpClientRequest.post(url).pipe(HttpClientRequest.setHeaders(headers), HttpClientRequest.bodyText(text, "application/json"))); | ||
| const payload = yield* response.json.pipe(Effect.mapError(() => invalidOutput("Failed to read the Google Images response"))); | ||
| const decoded = yield* Schema.decodeUnknownEffect(GoogleImageResponse)(payload).pipe(Effect.mapError(() => invalidOutput("Google Images returned an invalid response"))); | ||
| const output = yield* ProviderShared.imageResponse(ADAPTER, "Google Images", response); | ||
| const decoded = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(GoogleImageResponse))(output.body).pipe(Effect.mapError((cause) => output.invalid("Google Images returned an invalid response", cause))); | ||
| const candidates = decoded.candidates ?? []; | ||
@@ -123,3 +118,3 @@ const candidateMetadata = candidates.map((candidate, candidateIndex) => ({ | ||
| : [{ candidate, candidateIndex, partIndex, inlineData: part.inlineData }])); | ||
| const images = yield* Effect.forEach(encoded, (item) => Effect.fromResult(Encoding.decodeBase64(item.inlineData.data)).pipe(Effect.mapError(() => invalidOutput(`Google Images candidate ${item.candidateIndex} part ${item.partIndex} contains invalid base64 data`)), Effect.map((data) => new GeneratedImage({ | ||
| const images = yield* Effect.forEach(encoded, (item) => Effect.fromResult(Encoding.decodeBase64(item.inlineData.data)).pipe(Effect.mapError((cause) => output.invalid(`Google Images candidate ${item.candidateIndex} part ${item.partIndex} contains invalid base64 data`, cause)), Effect.map((data) => new GeneratedImage({ | ||
| mediaType: item.inlineData.mimeType, | ||
@@ -141,8 +136,3 @@ data, | ||
| const finishReasons = candidates.flatMap((candidate) => candidate.finishReason === undefined ? [] : [candidate.finishReason]); | ||
| return yield* invalidOutput(`Google Images returned no final images${finishReasons.length === 0 ? "" : ` (finish reasons: ${finishReasons.join(", ")})`}; inspect reason.providerMetadata.google for prompt feedback and candidate details`, { | ||
| google: { | ||
| promptFeedback: decoded.promptFeedback, | ||
| candidates: candidateMetadata, | ||
| }, | ||
| }); | ||
| return yield* output.invalid(`Google Images returned no final images${finishReasons.length === 0 ? "" : ` (finish reasons: ${finishReasons.join(", ")})`}; inspect body for prompt feedback and candidate details`); | ||
| } | ||
@@ -185,5 +175,5 @@ const usage = decoded.usageMetadata; | ||
| if (image.type === "url") | ||
| return ImageInputs.decodeDataUrl(image.url, ADAPTER).pipe(Effect.flatMap((decoded) => { | ||
| return ImageInputs.decodeDataUrl(image.url).pipe(Effect.flatMap((decoded) => { | ||
| if (decoded === undefined) | ||
| return Effect.fail(ImageInputs.invalid(ADAPTER, "Google generateContent does not fetch public image URLs; use bytes, a data URL, or a Gemini file URI")); | ||
| return Effect.fail(ImageInputs.invalid("Google generateContent does not fetch public image URLs; use bytes, a data URL, or a Gemini file URI")); | ||
| return Effect.succeed({ | ||
@@ -193,3 +183,3 @@ inlineData: { mimeType: decoded.mediaType, data: Encoding.encodeBase64(decoded.data) }, | ||
| })); | ||
| return Effect.fail(ImageInputs.invalid(ADAPTER, "Google generateContent requires Gemini file URIs rather than provider file IDs")); | ||
| return Effect.fail(ImageInputs.invalid("Google generateContent requires Gemini file URIs rather than provider file IDs")); | ||
| }; | ||
@@ -196,0 +186,0 @@ export const GoogleImages = { |
@@ -31,3 +31,3 @@ import { Effect, Schema, Stream } from "effect"; | ||
| observe: (_create, frame) => Effect.gen(function* () { | ||
| const event = yield* decodeEvent(frame).pipe(Effect.mapError(() => ProviderShared.eventError(options.id, `Invalid ${options.name} WebSocket event`, frame))); | ||
| const event = yield* decodeEvent(frame).pipe(Effect.mapError((cause) => ProviderShared.eventError(options.id, `Invalid ${options.name} WebSocket event`, frame, cause))); | ||
| if (terminal) | ||
@@ -37,6 +37,6 @@ return yield* ProviderShared.eventError(options.id, `${options.name} emitted ${event.type} after a terminal event`, frame); | ||
| terminal = true; | ||
| yield* OpenResponses.decodeKnownErrorEvent(event).pipe(Effect.mapError(() => ProviderShared.eventError(options.id, `${options.name} returned a malformed error event`, frame))); | ||
| yield* OpenResponses.decodeKnownErrorEvent(event).pipe(Effect.mapError((cause) => ProviderShared.eventError(options.id, `${options.name} returned a malformed error event`, frame, cause))); | ||
| return { | ||
| type: "provider-failure", | ||
| error: OpenResponses.providerFailure(options.id, event, `${options.name} stream error`), | ||
| error: OpenResponses.providerFailure(event, `${options.name} stream error`, frame), | ||
| }; | ||
@@ -50,3 +50,3 @@ } | ||
| type: "provider-failure", | ||
| error: OpenResponses.providerFailure(options.id, event, `${options.name} response failed`), | ||
| error: OpenResponses.providerFailure(event, `${options.name} response failed`, frame), | ||
| }; | ||
@@ -116,5 +116,6 @@ } | ||
| }), | ||
| execute: (prepared, request, runtime, executeOptions) => { | ||
| execute: (prepared, request, runtime, executeOptions) => Effect.gen(function* () { | ||
| if (!executeOptions?.webSocket || !prepared.channel) | ||
| return http.execute(prepared.http, request, runtime); | ||
| return yield* http.execute(prepared.http, request, runtime); | ||
| let fallbackHttp; | ||
| const exchange = { | ||
@@ -127,9 +128,19 @@ id: request.id ?? "request", | ||
| }, | ||
| fallback: () => Stream.unwrap(http.execute(prepared.http, request, runtime).pipe(Effect.map((execution) => execution.frames))), | ||
| fallback: () => Stream.unwrap(http.execute(prepared.http, request, runtime).pipe(Effect.map((execution) => { | ||
| fallbackHttp = execution.http; | ||
| return execution.frames; | ||
| }))), | ||
| driver: prepared.channel.driver, | ||
| }; | ||
| return executeOptions.webSocket.execute(exchange); | ||
| }, | ||
| const execution = yield* executeOptions.webSocket.execute(exchange); | ||
| return { | ||
| frames: execution.frames, | ||
| complete: execution.complete, | ||
| get http() { | ||
| return fallbackHttp ?? execution.http; | ||
| }, | ||
| }; | ||
| }), | ||
| }; | ||
| }; | ||
| export const OpenResponsesChannel = { transport }; |
@@ -1,2 +0,2 @@ | ||
| import { AIError, TransportReason } from "../schema/index.js"; | ||
| import { AIError, TransportError } from "../schema/index.js"; | ||
| import { Effect, Option, Schema } from "effect"; | ||
@@ -84,10 +84,11 @@ import * as ProviderShared from "./shared.js"; | ||
| const code = (event) => event.code || event.error?.code || event.response?.error?.code || undefined; | ||
| const rejected = (input, observation, recovery) => ({ | ||
| const rejected = (observation, recovery) => ({ | ||
| type: "rejected", | ||
| recovery, | ||
| error: new AIError({ | ||
| module: input.id, | ||
| method: "stream", | ||
| reason: new TransportReason({ | ||
| reason: new TransportError({ | ||
| message: observation.error.message, | ||
| body: observation.error.reason.body, | ||
| http: observation.error.reason.http, | ||
| cause: observation.error.reason.cause, | ||
| transport: "websocket", | ||
@@ -117,3 +118,3 @@ operation: "read", | ||
| observe: (create, frame) => Effect.gen(function* () { | ||
| const event = yield* decodeEvent(frame).pipe(Effect.mapError(() => ProviderShared.eventError(input.id, `Invalid ${input.name} WebSocket event`, frame))); | ||
| const event = yield* decodeEvent(frame).pipe(Effect.mapError((cause) => ProviderShared.eventError(input.id, `Invalid ${input.name} WebSocket event`, frame, cause))); | ||
| const observation = yield* input.base.observe(create, frame); | ||
@@ -125,5 +126,5 @@ if (event.type === "response.output_item.done" && event.item) | ||
| if (rejection === "previous_response_not_found") | ||
| return rejected(input, observation, "retry-full"); | ||
| return rejected(observation, "retry-full"); | ||
| if (rejection === "websocket_connection_limit_reached") | ||
| return rejected(input, observation, "rotate-and-retry-full"); | ||
| return rejected(observation, "rotate-and-retry-full"); | ||
| } | ||
@@ -130,0 +131,0 @@ if (observation.type !== "completed") |
@@ -807,3 +807,3 @@ import { Effect, Schema } from "effect"; | ||
| export declare const onReasoningDone: (state: ParserState, event: Event, itemID: string) => StepResult; | ||
| export declare const providerFailure: (id: string, event: Event, fallback: string) => AIError; | ||
| export declare const providerFailure: (event: Event, fallback: string, body?: string) => AIError; | ||
| export declare const step: (state: ParserState, input: Event) => AIError | Effect.Effect<[ParserState, readonly ({ | ||
@@ -810,0 +810,0 @@ readonly type: "step-start"; |
| import { Effect, Schema } from "effect"; | ||
| import { HttpTransport } from "../route/transport/index.js"; | ||
| import { Protocol } from "../route/protocol.js"; | ||
| import { AIError, LLMEvent, ProviderInternalReason, Usage, } from "../schema/index.js"; | ||
| import { AIError, LLMEvent, ProviderInternalError, Usage, } from "../schema/index.js"; | ||
| import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared.js"; | ||
@@ -921,7 +921,4 @@ import { classifyProviderFailure } from "../provider-error.js"; | ||
| }; | ||
| export const providerFailure = (id, event, fallback) => { | ||
| export const providerFailure = (event, fallback, body = ProviderShared.encodeJson(event)) => { | ||
| const nested = event.error ?? event.response?.error ?? undefined; | ||
| const code = event.code || nested?.code || undefined; | ||
| // Keep the full raw payload on the error even when the message is a summary. | ||
| const body = JSON.stringify(nested ?? event) ?? ""; | ||
| const summary = providerErrorMessage(event, nested); | ||
@@ -939,12 +936,6 @@ const message = summary ?? (body === "{}" ? fallback : body); | ||
| status === undefined | ||
| ? new ProviderInternalReason({ message }) | ||
| : classifyProviderFailure({ message, code, status, rawBody: body }); | ||
| return new AIError({ | ||
| module: id, | ||
| method: "stream", | ||
| body, | ||
| reason, | ||
| }); | ||
| ? new ProviderInternalError({ message, body }) | ||
| : classifyProviderFailure({ message, status, rawBody: body }); | ||
| return new AIError({ reason }); | ||
| }; | ||
| const providerError = (state, event, fallback) => providerFailure(state.id, event, fallback); | ||
| export const step = (state, input) => { | ||
@@ -1010,5 +1001,5 @@ // The OpenAPI requires string IDs but imposes no minLength; empty is not missing. | ||
| if (event.type === "response.failed") | ||
| return providerError(state, event, `${state.name} response failed`); | ||
| return providerFailure(event, `${state.name} response failed`); | ||
| if (event.type === "error") | ||
| return decodeKnownErrorEvent(event).pipe(Effect.mapError(() => ProviderShared.eventError(state.id, `${state.name} returned a malformed error event`)), Effect.flatMap(() => providerError(state, event, `${state.name} stream error`))); | ||
| return decodeKnownErrorEvent(event).pipe(Effect.mapError((cause) => ProviderShared.eventError(state.id, `${state.name} returned a malformed error event`, ProviderShared.encodeJson(event), cause)), Effect.flatMap(() => providerFailure(event, `${state.name} stream error`))); | ||
| return Effect.succeed([state, NO_EVENTS]); | ||
@@ -1015,0 +1006,0 @@ }; |
@@ -8,3 +8,3 @@ import { Effect, Schema } from "effect"; | ||
| import { Protocol } from "../route/protocol.js"; | ||
| import { AIError, InvalidProviderOutputReason, LLMEvent, ProviderInternalReason, UnknownProviderReason, Usage, } from "../schema/index.js"; | ||
| import { AIError, AIErrorReason, InvalidProviderOutputError, LLMEvent, ProviderInternalError, UnknownProviderError, Usage, } from "../schema/index.js"; | ||
| import { classifyProviderFailure } from "../provider-error.js"; | ||
@@ -617,14 +617,18 @@ import { isRecord, JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared.js"; | ||
| // because OpenAI streams JSON arguments across multiple deltas. | ||
| const finishReasonError = (event, reason) => new AIError({ | ||
| module: ADAPTER, | ||
| method: "stream", | ||
| body: ProviderShared.encodeJson(event), | ||
| reason, | ||
| }); | ||
| const mapFinishReason = Effect.fn("OpenAIChat.mapFinishReason")(function* (event, reason) { | ||
| switch (reason) { | ||
| case "error": | ||
| return yield* finishReasonError(event, new UnknownProviderReason({ message: "Provider reported an error (finish_reason: error)" })); | ||
| return yield* new AIError({ | ||
| reason: new UnknownProviderError({ | ||
| message: "Provider reported an error (finish_reason: error)", | ||
| body: ProviderShared.encodeJson(event), | ||
| }), | ||
| }); | ||
| case "network_error": | ||
| return yield* finishReasonError(event, new ProviderInternalReason({ message: "Provider reported a network error (finish_reason: network_error)" })); | ||
| return yield* new AIError({ | ||
| reason: new ProviderInternalError({ | ||
| message: "Provider reported a network error (finish_reason: network_error)", | ||
| body: ProviderShared.encodeJson(event), | ||
| }), | ||
| }); | ||
| case "stop": | ||
@@ -743,8 +747,4 @@ case "end": | ||
| return yield* new AIError({ | ||
| module: ADAPTER, | ||
| method: "stream", | ||
| body, | ||
| reason: classifyProviderFailure({ | ||
| message: event.error.message, | ||
| code: event.error.code === undefined || event.error.code === null ? undefined : String(event.error.code), | ||
| status: typeof event.error.code === "number" ? event.error.code : undefined, | ||
@@ -837,3 +837,10 @@ rawBody: body, | ||
| if (ToolStream.isError(result)) | ||
| return yield* ProviderShared.eventError(ADAPTER, result.reason.message, ProviderShared.encodeJson(event)); | ||
| return yield* new AIError({ | ||
| reason: AIErrorReason.make({ | ||
| ...result.reason, | ||
| message: result.message, | ||
| cause: result.reason.cause, | ||
| body: ProviderShared.encodeJson(event), | ||
| }), | ||
| }); | ||
| tools = result.tools; | ||
@@ -874,7 +881,5 @@ if (result.events.length) | ||
| return yield* new AIError({ | ||
| module: ADAPTER, | ||
| method: "stream", | ||
| reason: new InvalidProviderOutputReason({ | ||
| reason: new InvalidProviderOutputError({ | ||
| message: "OpenAI Chat stream ended without finish_reason", | ||
| classification: "incomplete-stream", | ||
| message: "OpenAI Chat stream ended without finish_reason", | ||
| route: ADAPTER, | ||
@@ -881,0 +886,0 @@ }), |
@@ -5,3 +5,3 @@ import { Effect, Encoding, Schema } from "effect"; | ||
| import { Auth } from "../route/auth.js"; | ||
| import { InvalidProviderOutputReason, AIError, Usage, mergeHttpOptions, mergeJsonRecords, } from "../schema/index.js"; | ||
| import { Usage, mergeHttpOptions, mergeJsonRecords } from "../schema/index.js"; | ||
| import { ProviderShared } from "./shared.js"; | ||
@@ -39,7 +39,2 @@ import { ImageInputs } from "./utils/image-input.js"; | ||
| }; | ||
| const invalidOutput = (message) => new AIError({ | ||
| module: ADAPTER, | ||
| method: "generate", | ||
| reason: new InvalidProviderOutputReason({ message, route: ADAPTER }), | ||
| }); | ||
| const applyQuery = (url, query) => { | ||
@@ -58,3 +53,3 @@ if (!query) | ||
| if (mask !== undefined && (request.images?.length ?? 0) === 0) | ||
| return yield* ImageInputs.invalid(ADAPTER, "An OpenAI image mask requires at least one input image"); | ||
| return yield* ImageInputs.invalid("An OpenAI image mask requires at least one input image"); | ||
| const http = mergeHttpOptions(request.model.http, request.http); | ||
@@ -66,3 +61,3 @@ const sourceImages = request.images ?? []; | ||
| if (image.type === "url") | ||
| return ImageInputs.decodeDataUrl(image.url, ADAPTER); | ||
| return ImageInputs.decodeDataUrl(image.url); | ||
| return Effect.undefined; | ||
@@ -75,3 +70,3 @@ }); | ||
| : mask.type === "url" | ||
| ? yield* ImageInputs.decodeDataUrl(mask.url, ADAPTER) | ||
| ? yield* ImageInputs.decodeDataUrl(mask.url) | ||
| : undefined; | ||
@@ -119,3 +114,3 @@ const useMultipart = sourceImages.length > 0 && | ||
| if (references.some((image) => image === undefined)) | ||
| return yield* ImageInputs.invalid(ADAPTER, "OpenAI Images accepts image URLs, data URLs, bytes, and file IDs"); | ||
| return yield* ImageInputs.invalid("OpenAI Images accepts image URLs, data URLs, bytes, and file IDs"); | ||
| const maskReference = mask === undefined | ||
@@ -131,3 +126,3 @@ ? undefined | ||
| if (mask !== undefined && maskReference === undefined) | ||
| return yield* ImageInputs.invalid(ADAPTER, "OpenAI Images accepts masks as URLs, data URLs, bytes, or file IDs"); | ||
| return yield* ImageInputs.invalid("OpenAI Images accepts masks as URLs, data URLs, bytes, or file IDs"); | ||
| const requestBody = mergeJsonRecords({ | ||
@@ -154,4 +149,4 @@ model: request.model.id, | ||
| const parseResponse = Effect.fn("OpenAIImages.parseResponse")(function* (response, options, overlay) { | ||
| const payload = yield* response.json.pipe(Effect.mapError(() => invalidOutput("Failed to read the OpenAI Images response"))); | ||
| const decoded = yield* Schema.decodeUnknownEffect(OpenAIImageResponse)(payload).pipe(Effect.mapError(() => invalidOutput("OpenAI Images returned an invalid response"))); | ||
| const output = yield* ProviderShared.imageResponse(ADAPTER, "OpenAI Images", response); | ||
| const decoded = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(OpenAIImageResponse))(output.body).pipe(Effect.mapError((cause) => output.invalid("OpenAI Images returned an invalid response", cause))); | ||
| const requestBody = mergeJsonRecords(nativeOptions(options), overlay); | ||
@@ -161,3 +156,3 @@ const format = decoded.output_format ?? (typeof requestBody?.output_format === "string" ? requestBody.output_format : "png"); | ||
| if (item.b64_json) | ||
| return Effect.fromResult(Encoding.decodeBase64(item.b64_json)).pipe(Effect.mapError(() => invalidOutput(`OpenAI Images result ${index} contains invalid base64 data`)), Effect.map((data) => new GeneratedImage({ | ||
| return Effect.fromResult(Encoding.decodeBase64(item.b64_json)).pipe(Effect.mapError((cause) => output.invalid(`OpenAI Images result ${index} contains invalid base64 data`, cause)), Effect.map((data) => new GeneratedImage({ | ||
| mediaType: `image/${format}`, | ||
@@ -173,6 +168,6 @@ data, | ||
| })); | ||
| return Effect.fail(invalidOutput(`OpenAI Images result ${index} has neither image data nor a URL`)); | ||
| return Effect.fail(output.invalid(`OpenAI Images result ${index} has neither image data nor a URL`)); | ||
| }); | ||
| if (images.length === 0) | ||
| return yield* invalidOutput("OpenAI Images returned no images"); | ||
| return yield* output.invalid("OpenAI Images returned no images"); | ||
| return new ImageResponse({ | ||
@@ -179,0 +174,0 @@ images, |
@@ -118,3 +118,3 @@ import { Effect, Encoding, Schema } from "effect"; | ||
| if (item.type === "image_generation_call" && item.result) { | ||
| yield* Effect.fromResult(Encoding.decodeBase64(item.result)).pipe(Effect.mapError(() => ProviderShared.eventError(ADAPTER, "OpenAI Responses returned invalid image base64"))); | ||
| yield* Effect.fromResult(Encoding.decodeBase64(item.result)).pipe(Effect.mapError((cause) => ProviderShared.eventError(ADAPTER, "OpenAI Responses returned invalid image base64", undefined, cause))); | ||
| const format = item.output_format ?? "png"; | ||
@@ -121,0 +121,0 @@ return { |
| import { Tool } from "@opencode-ai/schema/tool"; | ||
| import { Effect, Schema, Stream } from "effect"; | ||
| import { Headers, HttpClientRequest } from "effect/unstable/http"; | ||
| import { Headers, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"; | ||
| import { AIError, type ContentPart, type LLMRequest, type MediaPart, type ToolResultPart } from "../schema/index.js"; | ||
@@ -57,3 +57,3 @@ import { isRecord } from "../utils/record.js"; | ||
| export declare const sumTokens: (...values: ReadonlyArray<number | undefined>) => number | undefined; | ||
| export declare const eventError: (route: string, message: string, raw?: string) => AIError; | ||
| export declare const eventError: (route: string, message: string, body?: string, cause?: unknown) => AIError; | ||
| export declare const parseJson: (route: string, input: string, message: string) => Effect.Effect<unknown, AIError, never>; | ||
@@ -131,3 +131,7 @@ /** | ||
| */ | ||
| export declare const invalidRequest: (message: string) => AIError; | ||
| export declare const invalidRequest: (message: string, cause?: unknown) => AIError; | ||
| export declare const imageResponse: (route: string, name: string, response: HttpClientResponse.HttpClientResponse) => Effect.Effect<{ | ||
| body: string; | ||
| invalid: (message: string, cause?: unknown) => AIError; | ||
| }, AIError, never>; | ||
| export declare const matchToolChoice: <Auto, None, Required, Tool>(route: string, toolChoice: NonNullable<LLMRequest["toolChoice"]>, cases: { | ||
@@ -134,0 +138,0 @@ readonly auto: () => Auto; |
+26
-13
@@ -5,4 +5,4 @@ import { Buffer } from "node:buffer"; | ||
| import * as Sse from "effect/unstable/encoding/Sse"; | ||
| import { Headers, HttpClientRequest } from "effect/unstable/http"; | ||
| import { InvalidProviderOutputReason, InvalidRequestReason, AIError, } from "../schema/index.js"; | ||
| import { Headers, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"; | ||
| import { InvalidProviderOutputError, InvalidRequestError, AIError, HttpContext, } from "../schema/index.js"; | ||
| import { isRecord } from "../utils/record.js"; | ||
@@ -76,10 +76,8 @@ export { isRecord }; | ||
| }; | ||
| export const eventError = (route, message, raw) => new AIError({ | ||
| module: "ProviderShared", | ||
| method: "stream", | ||
| reason: new InvalidProviderOutputReason({ route, message, raw }), | ||
| export const eventError = (route, message, body, cause) => new AIError({ | ||
| reason: new InvalidProviderOutputError({ route, message, body, cause }), | ||
| }); | ||
| export const parseJson = (route, input, message) => Effect.try({ | ||
| try: () => decodeJson(input), | ||
| catch: () => eventError(route, message, input), | ||
| catch: (cause) => eventError(route, message, input, cause), | ||
| }); | ||
@@ -183,3 +181,3 @@ /** | ||
| if (error) | ||
| return yield* eventError("sse", error.message); | ||
| return yield* eventError("sse", error.message, chunk, error); | ||
| return [state, state.output.splice(0)]; | ||
@@ -192,7 +190,22 @@ })), Stream.filter((event) => (events === undefined || events.has(event.event)) && | ||
| */ | ||
| export const invalidRequest = (message) => new AIError({ | ||
| module: "ProviderShared", | ||
| method: "request", | ||
| reason: new InvalidRequestReason({ message }), | ||
| export const invalidRequest = (message, cause) => new AIError({ | ||
| reason: new InvalidRequestError({ message, cause }), | ||
| }); | ||
| export const imageResponse = Effect.fn("ProviderShared.imageResponse")(function* (route, name, response) { | ||
| const http = new HttpContext({ url: response.request.url, status: response.status, headers: response.headers }); | ||
| const body = yield* response.text.pipe(Effect.mapError((cause) => new AIError({ | ||
| reason: new InvalidProviderOutputError({ | ||
| route, | ||
| message: `Failed to read the ${name} response`, | ||
| http, | ||
| cause, | ||
| }), | ||
| }))); | ||
| return { | ||
| body, | ||
| invalid: (message, cause) => new AIError({ | ||
| reason: new InvalidProviderOutputError({ route, message, body, http, cause }), | ||
| }), | ||
| }; | ||
| }); | ||
| export const matchToolChoice = (route, toolChoice, cases) => Effect.gen(function* () { | ||
@@ -224,3 +237,3 @@ if (toolChoice.type === "auto") | ||
| */ | ||
| export const validateWith = (decode) => (payload) => decode(payload).pipe(Effect.mapError((error) => invalidRequest(error.message))); | ||
| export const validateWith = (decode) => (payload) => decode(payload).pipe(Effect.mapError((error) => invalidRequest(error.message, error))); | ||
| /** | ||
@@ -227,0 +240,0 @@ * Build an HTTP POST with a JSON body. Sets `content-type: application/json` |
@@ -7,7 +7,7 @@ import { Effect } from "effect"; | ||
| }>) => string; | ||
| export declare const decodeDataUrl: (url: string, module: string) => Effect.Effect<{ | ||
| export declare const decodeDataUrl: (url: string) => Effect.Effect<{ | ||
| readonly mediaType: string; | ||
| readonly data: Uint8Array; | ||
| } | undefined, AIError>; | ||
| export declare const invalidImageInput: (module: string, message: string) => AIError; | ||
| export declare const invalidImageInput: (message: string, cause?: unknown) => AIError; | ||
| export declare const ImageInputs: { | ||
@@ -17,7 +17,7 @@ readonly dataUrl: (input: Extract<ImageInput, { | ||
| }>) => string; | ||
| readonly decodeDataUrl: (url: string, module: string) => Effect.Effect<{ | ||
| readonly decodeDataUrl: (url: string) => Effect.Effect<{ | ||
| readonly mediaType: string; | ||
| readonly data: Uint8Array; | ||
| } | undefined, AIError>; | ||
| readonly invalid: (module: string, message: string) => AIError; | ||
| readonly invalid: (message: string, cause?: unknown) => AIError; | ||
| }; |
| import { Effect, Encoding } from "effect"; | ||
| import { InvalidRequestReason, AIError } from "../../schema/index.js"; | ||
| const invalid = (module, message) => new AIError({ | ||
| module, | ||
| method: "generate", | ||
| reason: new InvalidRequestReason({ message }), | ||
| import { InvalidRequestError, AIError } from "../../schema/index.js"; | ||
| const invalid = (message, cause) => new AIError({ | ||
| reason: new InvalidRequestError({ message, cause }), | ||
| }); | ||
| export const dataUrl = (input) => `data:${input.mediaType};base64,${Encoding.encodeBase64(input.data)}`; | ||
| export const decodeDataUrl = (url, module) => { | ||
| export const decodeDataUrl = (url) => { | ||
| if (!url.startsWith("data:")) | ||
@@ -14,4 +12,4 @@ return Effect.undefined; | ||
| if (!match) | ||
| return Effect.fail(invalid(module, "Image data URLs must contain a MIME type and base64 data")); | ||
| return Effect.fromResult(Encoding.decodeBase64(match[2])).pipe(Effect.mapError(() => invalid(module, "Image data URL contains invalid base64 data")), Effect.map((data) => ({ mediaType: match[1], data }))); | ||
| return Effect.fail(invalid("Image data URLs must contain a MIME type and base64 data")); | ||
| return Effect.fromResult(Encoding.decodeBase64(match[2])).pipe(Effect.mapError((cause) => invalid("Image data URL contains invalid base64 data", cause)), Effect.map((data) => ({ mediaType: match[1], data }))); | ||
| }; | ||
@@ -18,0 +16,0 @@ export const invalidImageInput = invalid; |
@@ -5,3 +5,3 @@ import { Effect, Encoding, Schema } from "effect"; | ||
| import { Auth } from "../route/auth.js"; | ||
| import { InvalidProviderOutputReason, AIError, Usage, mergeHttpOptions, mergeJsonRecords, } from "../schema/index.js"; | ||
| import { Usage, mergeHttpOptions, mergeJsonRecords } from "../schema/index.js"; | ||
| import { ProviderShared, optionalNull } from "./shared.js"; | ||
@@ -32,7 +32,2 @@ import { ImageInputs } from "./utils/image-input.js"; | ||
| }; | ||
| const invalidOutput = (message) => new AIError({ | ||
| module: ADAPTER, | ||
| method: "generate", | ||
| reason: new InvalidProviderOutputReason({ message, route: ADAPTER }), | ||
| }); | ||
| const applyQuery = (url, query) => { | ||
@@ -60,3 +55,3 @@ if (!query) | ||
| if (imageReferences.some((image) => image === undefined)) | ||
| return yield* ImageInputs.invalid(ADAPTER, "xAI Images accepts image URLs, data URLs, bytes, and file IDs"); | ||
| return yield* ImageInputs.invalid("xAI Images accepts image URLs, data URLs, bytes, and file IDs"); | ||
| const requestBody = mergeJsonRecords({ | ||
@@ -78,8 +73,8 @@ model: request.model.id, | ||
| const response = yield* execute(HttpClientRequest.post(url).pipe(HttpClientRequest.setHeaders(headers), HttpClientRequest.bodyText(text, "application/json"))); | ||
| const payload = yield* response.json.pipe(Effect.mapError(() => invalidOutput("Failed to read the xAI Images response"))); | ||
| const decoded = yield* Schema.decodeUnknownEffect(XAIImageResponse)(payload).pipe(Effect.mapError(() => invalidOutput("xAI Images returned an invalid response"))); | ||
| const output = yield* ProviderShared.imageResponse(ADAPTER, "xAI Images", response); | ||
| const decoded = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(XAIImageResponse))(output.body).pipe(Effect.mapError((cause) => output.invalid("xAI Images returned an invalid response", cause))); | ||
| const images = yield* Effect.forEach(decoded.data, (item, index) => { | ||
| const mediaType = item.mime_type ?? "application/octet-stream"; | ||
| if (item.b64_json) | ||
| return Effect.fromResult(Encoding.decodeBase64(item.b64_json)).pipe(Effect.mapError(() => invalidOutput(`xAI Images result ${index} contains invalid base64 data`)), Effect.map((data) => new GeneratedImage({ | ||
| return Effect.fromResult(Encoding.decodeBase64(item.b64_json)).pipe(Effect.mapError((cause) => output.invalid(`xAI Images result ${index} contains invalid base64 data`, cause)), Effect.map((data) => new GeneratedImage({ | ||
| mediaType, | ||
@@ -99,6 +94,6 @@ data, | ||
| })); | ||
| return Effect.fail(invalidOutput(`xAI Images result ${index} has neither image data nor a URL`)); | ||
| return Effect.fail(output.invalid(`xAI Images result ${index} has neither image data nor a URL`)); | ||
| }); | ||
| if (images.length === 0) | ||
| return yield* invalidOutput("xAI Images returned no images"); | ||
| return yield* output.invalid("xAI Images returned no images"); | ||
| const usage = ProviderShared.isRecord(decoded.usage) ? decoded.usage : undefined; | ||
@@ -105,0 +100,0 @@ return new ImageResponse({ |
@@ -5,3 +5,3 @@ import { Effect, Schema } from "effect"; | ||
| import { Auth } from "../route/auth.js"; | ||
| import { InvalidProviderOutputReason, AIError, mergeHttpOptions, mergeJsonRecords, } from "../schema/index.js"; | ||
| import { mergeHttpOptions, mergeJsonRecords } from "../schema/index.js"; | ||
| import { ProviderShared } from "./shared.js"; | ||
@@ -31,7 +31,2 @@ import { ImageInputs } from "./utils/image-input.js"; | ||
| }; | ||
| const invalidOutput = (message) => new AIError({ | ||
| module: ADAPTER, | ||
| method: "generate", | ||
| reason: new InvalidProviderOutputReason({ message, route: ADAPTER }), | ||
| }); | ||
| const applyQuery = (url, query) => { | ||
@@ -49,3 +44,3 @@ if (!query) | ||
| if ((request.images?.length ?? 0) > 0) | ||
| return yield* ImageInputs.invalid(ADAPTER, "Z.ai hosted image generation does not support image inputs"); | ||
| return yield* ImageInputs.invalid("Z.ai hosted image generation does not support image inputs"); | ||
| const http = mergeHttpOptions(request.model.http, request.http); | ||
@@ -63,6 +58,6 @@ const requestBody = mergeJsonRecords({ model: request.model.id, prompt: request.prompt }, nativeOptions(request.options), http?.body); | ||
| const response = yield* execute(HttpClientRequest.post(url).pipe(HttpClientRequest.setHeaders(headers), HttpClientRequest.bodyText(text, "application/json"))); | ||
| const payload = yield* response.json.pipe(Effect.mapError(() => invalidOutput("Failed to read the Z.ai Images response"))); | ||
| const decoded = yield* Schema.decodeUnknownEffect(ZAIImageResponse)(payload).pipe(Effect.mapError(() => invalidOutput("Z.ai Images returned an invalid response"))); | ||
| const output = yield* ProviderShared.imageResponse(ADAPTER, "Z.ai Images", response); | ||
| const decoded = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(ZAIImageResponse))(output.body).pipe(Effect.mapError((cause) => output.invalid("Z.ai Images returned an invalid response", cause))); | ||
| if (decoded.data.length === 0) | ||
| return yield* invalidOutput("Z.ai Images returned no images"); | ||
| return yield* output.invalid("Z.ai Images returned no images"); | ||
| return new ImageResponse({ | ||
@@ -69,0 +64,0 @@ images: decoded.data.map((item) => new GeneratedImage({ |
@@ -1,2 +0,2 @@ | ||
| import { AIError, type HttpContext, type HttpRateLimitDetails, type ProviderMetadata } from "./schema/index.js"; | ||
| import { AIError, type HttpContext, type HttpRateLimitDetails } from "./schema/index.js"; | ||
| export declare const isContextOverflow: (message: string) => boolean; | ||
@@ -8,9 +8,9 @@ export declare const isPayloadTooLarge: (message: string) => boolean; | ||
| readonly status?: number | undefined; | ||
| readonly code?: string | undefined; | ||
| readonly rawBody?: string | undefined; | ||
| readonly data?: unknown; | ||
| readonly http?: HttpContext | undefined; | ||
| readonly cause?: unknown; | ||
| readonly retryAfterMs?: number | undefined; | ||
| readonly rateLimit?: HttpRateLimitDetails | undefined; | ||
| readonly http?: HttpContext | undefined; | ||
| readonly providerMetadata?: ProviderMetadata | undefined; | ||
| } | ||
| export declare function classifyProviderFailure(input: ProviderFailure): AIError["reason"]; |
+31
-32
| import { Option, Schema } from "effect"; | ||
| import { AuthenticationReason, ContentPolicyReason, InvalidRequestReason, AIError, ProviderErrorEvent, ProviderInternalReason, QuotaExceededReason, RateLimitReason, UnknownProviderReason, } from "./schema/index.js"; | ||
| import { AuthenticationError, ContentPolicyError, InvalidRequestError, AIError, ProviderErrorEvent, ProviderInternalError, QuotaExceededError, RateLimitError, UnknownProviderError, } from "./schema/index.js"; | ||
| const patterns = [ | ||
@@ -60,10 +60,8 @@ /prompt is too long/i, | ||
| export function classifyProviderFailure(input) { | ||
| const body = input.http?.body ?? input.rawBody ?? ""; | ||
| const codes = [input.code, ...providerCodes(body), ...providerCodes(input.message)] | ||
| .filter((code) => code !== undefined) | ||
| .map((code) => code.toLowerCase()); | ||
| const details = { message: input.message, body: input.rawBody, http: input.http, cause: input.cause }; | ||
| const body = input.rawBody ?? ""; | ||
| const codes = [...providerCodes(input.data), ...providerCodes(body), ...providerCodes(input.message)].map((code) => code.toLowerCase()); | ||
| // Scan the raw payload too so signals missing from the summary message | ||
| // (e.g. overflow phrases nested in a JSON error body) still classify. | ||
| const text = [input.message, body].filter((value) => value.length > 0).join("\n"); | ||
| const common = { message: input.message, providerMetadata: input.providerMetadata, http: input.http }; | ||
| const clientScoped = input.status === undefined || (input.status >= 400 && input.status < 500); | ||
@@ -75,20 +73,20 @@ if (clientScoped && | ||
| isContextOverflow(text))) | ||
| return new InvalidRequestReason({ ...common, classification: "context-overflow" }); | ||
| return new InvalidRequestError({ ...details, classification: "context-overflow" }); | ||
| if (input.status === 413 || isPayloadTooLarge(text)) | ||
| return new InvalidRequestReason({ ...common, classification: "payload-too-large" }); | ||
| return new InvalidRequestError({ ...details, classification: "payload-too-large" }); | ||
| if (CONTENT_POLICY_TEXT.test(text)) | ||
| return new ContentPolicyReason(common); | ||
| return new ContentPolicyError(details); | ||
| if (codes.some((code) => QUOTA_CODES.has(code)) || (input.status === 429 && QUOTA_TEXT.test(text))) | ||
| return new QuotaExceededReason(common); | ||
| return new QuotaExceededError(details); | ||
| if (input.status === 401) | ||
| return new AuthenticationReason({ ...common, kind: "invalid" }); | ||
| return new AuthenticationError({ ...details, kind: "invalid" }); | ||
| if (input.status === 403) | ||
| return new AuthenticationReason({ ...common, kind: "insufficient-permissions" }); | ||
| return new AuthenticationError({ ...details, kind: "insufficient-permissions" }); | ||
| if (codes.includes("authentication_error")) | ||
| return new AuthenticationReason({ ...common, kind: "invalid" }); | ||
| return new AuthenticationError({ ...details, kind: "invalid" }); | ||
| if (codes.includes("permission_error")) | ||
| return new AuthenticationReason({ ...common, kind: "insufficient-permissions" }); | ||
| return new AuthenticationError({ ...details, kind: "insufficient-permissions" }); | ||
| if (codes.some((code) => code.includes("rate_limit") || code === "too_many_requests" || code === "throttlingexception")) | ||
| return new RateLimitReason({ | ||
| ...common, | ||
| return new RateLimitError({ | ||
| ...details, | ||
| retryAfterMs: input.retryAfterMs, | ||
@@ -98,4 +96,4 @@ rateLimit: input.rateLimit, | ||
| if (RATE_LIMIT_TEXT.test(text)) | ||
| return new RateLimitReason({ | ||
| ...common, | ||
| return new RateLimitError({ | ||
| ...details, | ||
| retryAfterMs: input.retryAfterMs, | ||
@@ -105,12 +103,11 @@ rateLimit: input.rateLimit, | ||
| if (NETWORK_ERROR_TEXT.test(text)) | ||
| return new ProviderInternalReason({ ...common, status: input.status }); | ||
| return new ProviderInternalError(details); | ||
| if (codes.some((code) => SERVER_CODES.has(code) || code.includes("exhausted") || code.includes("unavailable"))) | ||
| return new ProviderInternalReason({ | ||
| ...common, | ||
| status: input.status, | ||
| return new ProviderInternalError({ | ||
| ...details, | ||
| retryAfterMs: input.retryAfterMs, | ||
| }); | ||
| if (input.status === 429) { | ||
| return new RateLimitReason({ | ||
| ...common, | ||
| return new RateLimitError({ | ||
| ...details, | ||
| retryAfterMs: input.retryAfterMs, | ||
@@ -121,19 +118,21 @@ rateLimit: input.rateLimit, | ||
| if (input.status === 408 || input.status === 409 || (input.status !== undefined && input.status >= 500)) | ||
| return new ProviderInternalReason({ | ||
| ...common, | ||
| status: input.status, | ||
| return new ProviderInternalError({ | ||
| ...details, | ||
| retryAfterMs: input.retryAfterMs, | ||
| }); | ||
| if (codes.some((code) => INVALID_REQUEST_CODES.has(code))) | ||
| return new InvalidRequestReason(common); | ||
| return new InvalidRequestError(details); | ||
| if (input.status === 400 || input.status === 404 || input.status === 413 || input.status === 422) | ||
| return new InvalidRequestReason(common); | ||
| return new UnknownProviderReason({ ...common, status: input.status }); | ||
| return new InvalidRequestError(details); | ||
| return new UnknownProviderError(details); | ||
| } | ||
| function providerCodes(value) { | ||
| const decoded = Option.getOrUndefined(decodeJson(value)); | ||
| const decoded = typeof value === "string" ? Option.getOrUndefined(decodeJson(value)) : value; | ||
| if (!isRecord(decoded)) | ||
| return []; | ||
| const error = isRecord(decoded.error) ? decoded.error : undefined; | ||
| return [decoded.code, error?.code, error?.type].filter((value) => typeof value === "string"); | ||
| const response = isRecord(decoded.response) ? decoded.response : undefined; | ||
| const responseError = response && isRecord(response.error) ? response.error : undefined; | ||
| const exception = isRecord(decoded.exception) ? decoded.exception : undefined; | ||
| return [decoded.code, error?.code, error?.type, error?.status, responseError?.code, exception?.type].filter((value) => typeof value === "string"); | ||
| } | ||
@@ -140,0 +139,0 @@ function isRecord(value) { |
| import { Config, Effect, Redacted } from "effect"; | ||
| import { Headers } from "effect/unstable/http"; | ||
| import { AuthenticationReason, InvalidRequestReason, AIError } from "../schema/index.js"; | ||
| import { AuthenticationError, InvalidRequestError, AIError } from "../schema/index.js"; | ||
| export class MissingCredentialError extends Error { | ||
@@ -79,7 +79,5 @@ source; | ||
| return new AIError({ | ||
| module: "Auth", | ||
| method: "apply", | ||
| reason: error instanceof MissingCredentialError | ||
| ? new AuthenticationReason({ message: error.message, kind: "missing" }) | ||
| : new InvalidRequestReason({ message: `Failed to resolve auth config: ${error.message}` }), | ||
| ? new AuthenticationError({ message: error.message, cause: error, kind: "missing" }) | ||
| : new InvalidRequestError({ message: `Failed to resolve auth config: ${error.message}`, cause: error }), | ||
| }); | ||
@@ -86,0 +84,0 @@ } |
+29
-11
@@ -10,3 +10,3 @@ import { Cause, Context, Effect, Layer, Schema, Stream } from "effect"; | ||
| import * as ProviderShared from "../protocols/shared.js"; | ||
| import { AIError, GenerationOptions, HttpOptions, LLMRequest, LLMResponse, LanguageModel, LLMEvent, InvalidProviderOutputReason, ProviderID, mergeGenerationOptions, mergeHttpOptions, mergeProviderOptions, } from "../schema/index.js"; | ||
| import { AIError, AIErrorReason, GenerationOptions, HttpOptions, LLMRequest, LLMResponse, LanguageModel, LLMEvent, InvalidProviderOutputError, ProviderID, mergeGenerationOptions, mergeHttpOptions, mergeProviderOptions, } from "../schema/index.js"; | ||
| const makeRouteLanguageModel = (route, mapped) => { | ||
@@ -64,10 +64,8 @@ const provider = route.provider ?? ("provider" in mapped ? mapped.provider : undefined); | ||
| return failed; | ||
| return ProviderShared.eventError(route, message, Cause.pretty(cause)); | ||
| return ProviderShared.eventError(route, message, undefined, cause); | ||
| }; | ||
| const incompleteStreamError = (route) => new AIError({ | ||
| module: "LLMClient", | ||
| method: "stream", | ||
| reason: new InvalidProviderOutputReason({ | ||
| reason: new InvalidProviderOutputError({ | ||
| message: "The provider response ended unexpectedly.", | ||
| classification: "incomplete-stream", | ||
| message: "The provider response ended unexpectedly.", | ||
| route, | ||
@@ -90,3 +88,3 @@ }), | ||
| const decodeEventEffect = Schema.decodeUnknownEffect(protocol.stream.event); | ||
| const decodeEvent = (route) => (frame) => decodeEventEffect(frame).pipe(Effect.mapError(() => ProviderShared.eventError(input.id, `Invalid ${route} stream event`, typeof frame === "string" ? frame : ProviderShared.encodeJson(frame)))); | ||
| const decodeEvent = (route) => (frame) => decodeEventEffect(frame).pipe(Effect.mapError((cause) => ProviderShared.eventError(input.id, `Invalid ${route} stream event`, typeof frame === "string" ? frame : ProviderShared.encodeJson(frame), cause))); | ||
| const build = (routeInput) => { | ||
@@ -133,9 +131,22 @@ const route = { | ||
| return Stream.unwrap(routeInput.transport.execute(prepared, request, runtime, options).pipe(Effect.map((execution) => { | ||
| const events = execution.frames.pipe(Stream.mapEffect(decodeEvent(route)), protocol.stream.terminal ? Stream.takeUntil(protocol.stream.terminal) : (stream) => stream); | ||
| const terminal = protocol.stream.terminal; | ||
| // Preserve assembled inputs; replace only serialized event fallbacks with their original wire data. | ||
| const frameError = (frame, event = frame) => (error) => new AIError({ | ||
| reason: AIErrorReason.make({ | ||
| ...error.reason, | ||
| message: error.reason.message, | ||
| cause: error.reason.cause, | ||
| body: error.reason.body !== undefined && error.reason.body !== ProviderShared.encodeJson(event) | ||
| ? error.reason.body | ||
| : (execution.body?.(frame) ?? | ||
| (typeof frame === "string" ? frame : ProviderShared.encodeJson(frame))), | ||
| }), | ||
| }); | ||
| const events = execution.frames.pipe(Stream.mapEffect((frame) => decodeEvent(route)(frame).pipe(Effect.catchCause((cause) => Effect.fail(streamError(route, `Failed to decode ${route} event`, cause))), Effect.map((event) => ({ event, frame })), Effect.mapError(frameError(frame)))), terminal ? Stream.takeUntil(({ event }) => terminal(event)) : (stream) => stream); | ||
| const stream = Stream.suspend(() => { | ||
| let state = protocol.stream.initial(request); | ||
| const parsed = events.pipe(Stream.mapEffect((event) => protocol.stream.step(state, event).pipe(Effect.map(([next, output]) => { | ||
| const parsed = events.pipe(Stream.mapEffect(({ event, frame }) => protocol.stream.step(state, event).pipe(Effect.catchCause((cause) => Effect.fail(streamError(route, `Failed to parse ${route} event`, cause))), Effect.map(([next, output]) => { | ||
| state = next; | ||
| return output; | ||
| }))), Stream.flatMap(Stream.fromIterable)); | ||
| }), Effect.mapError(frameError(frame, event)))), Stream.flatMap(Stream.fromIterable)); | ||
| const onHalt = protocol.stream.onHalt; | ||
@@ -145,3 +156,10 @@ return onHalt | ||
| : parsed; | ||
| }).pipe(Stream.catchCause((cause) => Stream.fail(streamError(route, `Failed to read ${route} stream`, cause))), requireTerminalEvent(route)); | ||
| }).pipe(Stream.catchCause((cause) => Stream.fail(streamError(route, `Failed to read ${route} stream`, cause))), requireTerminalEvent(route), Stream.mapError((error) => new AIError({ | ||
| reason: AIErrorReason.make({ | ||
| ...error.reason, | ||
| message: error.reason.message, | ||
| cause: error.reason.cause, | ||
| http: error.reason.http ?? execution.http, | ||
| }), | ||
| }))); | ||
| return execution.complete ? stream.pipe(Stream.onEnd(execution.complete)) : stream; | ||
@@ -148,0 +166,0 @@ }))); |
| import { Context, Effect, Layer, Stream } from "effect"; | ||
| import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"; | ||
| import { AIError, TransportReason } from "../schema/index.js"; | ||
| import { HttpContext, AIError } from "../schema/index.js"; | ||
| export interface Interface { | ||
@@ -12,10 +12,14 @@ readonly execute: (request: HttpClientRequest.HttpClientRequest, middleware?: HttpMiddleware) => Effect.Effect<HttpClientResponse.HttpClientResponse, AIError>; | ||
| } | ||
| export declare const classifyHttpFailure: (input: { | ||
| export declare const responseHttp: (response: HttpClientResponse.HttpClientResponse) => HttpContext; | ||
| /** Preserve HTTP diagnostics for executor and externally captured failures alike. */ | ||
| export declare const httpFailure: (input: { | ||
| readonly message: string; | ||
| readonly url: string; | ||
| readonly url?: string | undefined; | ||
| readonly status?: number | undefined; | ||
| readonly code?: string | undefined; | ||
| readonly data?: unknown; | ||
| readonly responseHeaders?: Record<string, string> | undefined; | ||
| readonly responseBody?: string | undefined; | ||
| }) => import("../schema/errors.js").InvalidRequestReason | import("../schema/errors.js").NoRouteReason | import("../schema/errors.js").AuthenticationReason | import("../schema/errors.js").RateLimitReason | import("../schema/errors.js").QuotaExceededReason | import("../schema/errors.js").ContentPolicyReason | import("../schema/errors.js").ProviderInternalReason | TransportReason | import("../schema/errors.js").InvalidProviderOutputReason | import("../schema/errors.js").UnknownProviderReason; | ||
| readonly cause?: unknown; | ||
| }) => AIError; | ||
| export declare const responseStream: (response: HttpClientResponse.HttpClientResponse) => Stream.Stream<Uint8Array, AIError>; | ||
| export declare const stream: (executor: Interface, request: HttpClientRequest.HttpClientRequest, middleware?: HttpMiddleware) => Stream.Stream<Uint8Array, AIError>; | ||
@@ -22,0 +26,0 @@ export declare const layer: Layer.Layer<Service, never, HttpClient.HttpClient>; |
+33
-66
| import { Cause, Context, Effect, Layer, Option, Schema, Stream } from "effect"; | ||
| import { FetchHttpClient, Headers, HttpClient, HttpClientError, HttpClientRequest, HttpClientResponse, } from "effect/unstable/http"; | ||
| import { HttpContext, HttpRateLimitDetails, HttpRequestDetails, HttpResponseDetails, AIError, TransportReason, } from "../schema/index.js"; | ||
| import { HttpContext, HttpRateLimitDetails, AIError, TransportError } from "../schema/index.js"; | ||
| import { classifyProviderFailure } from "../provider-error.js"; | ||
@@ -63,16 +63,7 @@ export class Service extends Context.Service()("@opencode/AI/RequestExecutor") { | ||
| }; | ||
| const requestDetails = (request) => new HttpRequestDetails({ | ||
| method: request.method, | ||
| url: request.url, | ||
| headers: headerDetails(request.headers), | ||
| }); | ||
| const responseDetails = (response) => new HttpResponseDetails({ | ||
| export const responseHttp = (response) => new HttpContext({ | ||
| url: response.request.url, | ||
| status: response.status, | ||
| headers: headerDetails(response.headers), | ||
| }); | ||
| const responseBody = (body) => { | ||
| if (body === undefined) | ||
| return {}; | ||
| return { body }; | ||
| }; | ||
| const decodeProviderBody = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Struct({ | ||
@@ -87,56 +78,32 @@ message: Schema.optionalKey(Schema.String), | ||
| }; | ||
| const responseHttp = (input) => new HttpContext({ | ||
| request: requestDetails(input.request), | ||
| response: responseDetails(input.response), | ||
| ...input.body, | ||
| rateLimit: input.rateLimit, | ||
| }); | ||
| const statusError = (request) => (response) => Effect.gen(function* () { | ||
| const statusError = (response) => Effect.gen(function* () { | ||
| if (response.status < 400) | ||
| return response; | ||
| const body = yield* response.text.pipe(Effect.catch(() => Effect.void)); | ||
| const headers = normalizedHeaders(response.headers); | ||
| const result = yield* response.text.pipe(Effect.result); | ||
| return yield* httpFailure({ | ||
| message: providerMessage(response.status, result._tag === "Success" ? result.success : undefined), | ||
| url: response.request.url, | ||
| status: response.status, | ||
| responseHeaders: headerDetails(response.headers), | ||
| responseBody: result._tag === "Success" ? result.success : undefined, | ||
| cause: result._tag === "Failure" ? (result.failure.cause ?? result.failure) : undefined, | ||
| }); | ||
| }); | ||
| /** Preserve HTTP diagnostics for executor and externally captured failures alike. */ | ||
| export const httpFailure = (input) => { | ||
| const headers = normalizedHeaders(Headers.fromInput(input.responseHeaders)); | ||
| const retryAfter = retryAfterMs(headers); | ||
| const rateLimit = rateLimitDetails(headers, retryAfter); | ||
| const details = responseBody(body); | ||
| return yield* new AIError({ | ||
| module: "RequestExecutor", | ||
| method: "execute", | ||
| return new AIError({ | ||
| reason: classifyProviderFailure({ | ||
| status: response.status, | ||
| message: providerMessage(response.status, body), | ||
| message: input.message, | ||
| status: input.status, | ||
| data: input.data, | ||
| rawBody: input.responseBody, | ||
| retryAfterMs: retryAfter, | ||
| rateLimit, | ||
| http: responseHttp({ | ||
| request, | ||
| response, | ||
| body: details, | ||
| rateLimit, | ||
| }), | ||
| }), | ||
| }); | ||
| }); | ||
| // Classifies an HTTP failure captured outside the executor (for example by the | ||
| // AI SDK's own fetch) onto the same reason types and HttpContext that | ||
| // executor-driven requests produce. The originating request is not available on | ||
| // that path, so the method is assumed (language model calls are always POST), | ||
| // request headers are empty. | ||
| export const classifyHttpFailure = (input) => { | ||
| const headers = normalizedHeaders(Headers.fromInput(input.responseHeaders)); | ||
| const retryAfter = retryAfterMs(headers); | ||
| const rateLimit = rateLimitDetails(headers, retryAfter); | ||
| const details = responseBody(input.responseBody); | ||
| return classifyProviderFailure({ | ||
| message: input.message, | ||
| status: input.status, | ||
| code: input.code, | ||
| retryAfterMs: retryAfter, | ||
| rateLimit, | ||
| http: new HttpContext({ | ||
| request: new HttpRequestDetails({ method: "POST", url: input.url, headers: {} }), | ||
| response: input.status === undefined | ||
| cause: input.cause, | ||
| http: input.status === undefined || input.url === undefined | ||
| ? undefined | ||
| : new HttpResponseDetails({ status: input.status, headers: headerDetails(Headers.fromInput(headers)) }), | ||
| ...details, | ||
| rateLimit, | ||
| : new HttpContext({ url: input.url, status: input.status, headers }), | ||
| }), | ||
@@ -165,6 +132,6 @@ }); | ||
| const transportError = (failure) => new AIError({ | ||
| module: "RequestExecutor", | ||
| method: input.operation, | ||
| reason: new TransportReason({ | ||
| reason: new TransportError({ | ||
| message: failure.message, | ||
| cause: source, | ||
| http: input.http, | ||
| transport: "http", | ||
@@ -174,7 +141,6 @@ operation: input.operation, | ||
| url: request.url, | ||
| http: new HttpContext({ request: requestDetails(request) }), | ||
| }), | ||
| }); | ||
| const source = HttpClientError.isHttpClientError(input.error) && "cause" in input.error.reason | ||
| ? input.error.reason.cause | ||
| ? (input.error.reason.cause ?? input.error) | ||
| : input.error; | ||
@@ -201,5 +167,6 @@ const native = nativeTransportFailure(source); | ||
| }; | ||
| export const responseStream = (response) => response.stream.pipe(Stream.mapError((error) => httpError({ error, request: response.request, operation: "read", http: responseHttp(response) }))); | ||
| export const stream = (executor, request, middleware) => Stream.unwrap(Effect.gen(function* () { | ||
| const response = yield* executor.execute(request, middleware); | ||
| return response.stream.pipe(Stream.mapError((error) => httpError({ error, request: response.request, operation: "read" }))); | ||
| return responseStream(response); | ||
| })); | ||
@@ -210,7 +177,7 @@ export const layer = Layer.effect(Service, Effect.gen(function* () { | ||
| if (!middleware) | ||
| return yield* http.execute(request).pipe(Effect.mapError((error) => httpError({ error, request, operation: "request" })), Effect.flatMap(statusError(request))); | ||
| return yield* http.execute(request).pipe(Effect.mapError((error) => httpError({ error, request, operation: "request" })), Effect.flatMap(statusError)); | ||
| const response = yield* middleware(request, (input) => http | ||
| .execute(input) | ||
| .pipe(Effect.mapError((cause) => (cause instanceof Error ? cause : new Error(String(cause)))))).pipe(Effect.mapError((error) => httpError({ error, request, operation: "request" }))); | ||
| return yield* statusError(response.request)(response); | ||
| return yield* statusError(response); | ||
| }); | ||
@@ -217,0 +184,0 @@ return Service.of({ |
@@ -20,2 +20,4 @@ import type { Stream } from "effect"; | ||
| readonly frame: (bytes: Stream.Stream<Uint8Array, AIError>) => Stream.Stream<Frame, AIError>; | ||
| /** Original wire representation when framing transforms the provider payload. */ | ||
| readonly body?: (frame: Frame) => string | undefined; | ||
| } | ||
@@ -22,0 +24,0 @@ /** Server-Sent Events framing. Used by every JSON-streaming HTTP provider. */ |
@@ -56,4 +56,9 @@ import { Effect } from "effect"; | ||
| }), | ||
| execute: (prepared, _request, runtime) => Effect.succeed({ | ||
| frames: prepared.framing.frame(RequestExecutor.stream(runtime.http, prepared.request, prepared.middleware)), | ||
| execute: (prepared, _request, runtime) => Effect.gen(function* () { | ||
| const response = yield* runtime.http.execute(prepared.request, prepared.middleware); | ||
| return { | ||
| frames: prepared.framing.frame(RequestExecutor.responseStream(response)), | ||
| http: RequestExecutor.responseHttp(response), | ||
| body: prepared.framing.body, | ||
| }; | ||
| }), | ||
@@ -60,0 +65,0 @@ }); |
@@ -6,3 +6,3 @@ import type { Effect, Scope, Stream } from "effect"; | ||
| import type { WebSocketChannelExecutor } from "./websocket-channel.js"; | ||
| import type { AIError, LLMRequest } from "../../schema/index.js"; | ||
| import type { AIError, HttpContext, LLMRequest } from "../../schema/index.js"; | ||
| export interface TransportRuntime { | ||
@@ -13,2 +13,4 @@ readonly http: RequestExecutorInterface; | ||
| readonly frames: Stream.Stream<Frame, AIError>; | ||
| readonly http?: HttpContext; | ||
| body?(frame: Frame): string | undefined; | ||
| /** Optional successful-consumption acknowledgement. HTTP leaves this absent. */ | ||
@@ -15,0 +17,0 @@ readonly complete?: Effect.Effect<void>; |
| import type { Effect, Scope, Stream } from "effect"; | ||
| import type { Headers } from "effect/unstable/http"; | ||
| import type { AIError } from "../../schema/index.js"; | ||
| import type { AIError, HttpContext } from "../../schema/index.js"; | ||
| export interface WebSocketChannelExecutor { | ||
@@ -9,2 +9,3 @@ readonly execute: (exchange: WebSocketChannelExchange) => Effect.Effect<WebSocketChannelExecution, AIError, Scope.Scope>; | ||
| readonly frames: Stream.Stream<string, AIError>; | ||
| readonly http?: HttpContext; | ||
| /** Commits staged state after the decoded Route stream ends successfully. */ | ||
@@ -11,0 +12,0 @@ readonly complete: Effect.Effect<void>; |
| import { Effect, Stream } from "effect"; | ||
| import { Headers } from "effect/unstable/http"; | ||
| import { Socket } from "effect/unstable/socket"; | ||
| import { AIError } from "../../schema/index.js"; | ||
| import { AIError, type HttpContext } from "../../schema/index.js"; | ||
| import type { Transport } from "./index.js"; | ||
@@ -12,2 +12,3 @@ import type { WebSocketChannelExecutor } from "./websocket-channel.js"; | ||
| export interface WebSocketConnection { | ||
| readonly http?: HttpContext; | ||
| readonly sendText: (message: string) => Effect.Effect<void, AIError>; | ||
@@ -14,0 +15,0 @@ readonly messages: Stream.Stream<string | Uint8Array, AIError>; |
| import { Cause, Effect, Queue, Stream } from "effect"; | ||
| import { Headers } from "effect/unstable/http"; | ||
| import { Socket } from "effect/unstable/socket"; | ||
| import { AIError, TransportReason } from "../../schema/index.js"; | ||
| import { AIError, AIErrorReason, TransportError, } from "../../schema/index.js"; | ||
| import * as HttpTransport from "./http.js"; | ||
| const MAX_FRAME_BYTES = 16 * 1024 * 1024; | ||
| const transportError = (method, message, input) => new AIError({ | ||
| module: "WebSocketConnector", | ||
| method, | ||
| reason: new TransportReason({ | ||
| const transportError = (message, input) => new AIError({ | ||
| reason: new TransportError({ | ||
| message, | ||
| body: input.body, | ||
| cause: input.cause, | ||
| transport: "websocket", | ||
@@ -22,14 +22,8 @@ operation: input.operation, | ||
| ? new AIError({ | ||
| module: error.module, | ||
| method: error.method, | ||
| reason: new TransportReason({ | ||
| reason: new TransportError({ | ||
| ...error.reason, | ||
| message: error.reason.message, | ||
| transport: error.reason.transport, | ||
| operation: error.reason.operation, | ||
| code: error.reason.code, | ||
| url: error.reason.url, | ||
| http: error.reason.http, | ||
| cause: error.reason.cause, | ||
| phase: input.phase, | ||
| delivery: input.delivery, | ||
| recovery: error.reason.recovery, | ||
| }), | ||
@@ -56,3 +50,3 @@ }) | ||
| if (ws.readyState === globalThis.WebSocket.CLOSING || ws.readyState === globalThis.WebSocket.CLOSED) { | ||
| return Effect.fail(transportError("open", `WebSocket closed before opening (state ${ws.readyState})`, { | ||
| return Effect.fail(transportError(`WebSocket closed before opening (state ${ws.readyState})`, { | ||
| url: input.url, | ||
@@ -83,3 +77,4 @@ operation: "request", | ||
| cleanup(); | ||
| resume(Effect.fail(transportError("open", `Failed to open WebSocket: ${eventMessage(event)}`, { | ||
| resume(Effect.fail(transportError(`Failed to open WebSocket: ${eventMessage(event)}`, { | ||
| cause: "error" in event ? (event.error ?? event) : event, | ||
| url: input.url, | ||
@@ -93,3 +88,5 @@ operation: "request", | ||
| cleanup(); | ||
| resume(Effect.fail(transportError("open", `WebSocket closed before opening with code ${event.code}`, { | ||
| resume(Effect.fail(transportError(`WebSocket closed before opening with code ${event.code}`, { | ||
| body: event.reason, | ||
| cause: event, | ||
| url: input.url, | ||
@@ -121,3 +118,4 @@ operation: "request", | ||
| }, | ||
| catch: (error) => transportError("prepare", error instanceof Error ? error.message : "Invalid WebSocket URL", { | ||
| catch: (error) => transportError(error instanceof Error ? error.message : "Invalid WebSocket URL", { | ||
| cause: error, | ||
| url: value, | ||
@@ -139,3 +137,4 @@ operation: "request", | ||
| }), | ||
| catch: (error) => transportError("open", error instanceof Error ? error.message : "Failed to construct WebSocket", { | ||
| catch: (error) => transportError(error instanceof Error ? error.message : "Failed to construct WebSocket", { | ||
| cause: error, | ||
| url: input.url, | ||
@@ -156,3 +155,4 @@ operation: "request", | ||
| return false; | ||
| Queue.failCauseUnsafe(messages, Cause.fail(transportError("message", "WebSocket message exceeds the 16 MiB limit", { | ||
| Queue.failCauseUnsafe(messages, Cause.fail(transportError("WebSocket message exceeds the 16 MiB limit", { | ||
| body: typeof message === "string" ? message : new TextDecoder().decode(message), | ||
| url: input.url, | ||
@@ -172,3 +172,4 @@ operation: "read", | ||
| return; | ||
| Queue.failCauseUnsafe(messages, Cause.fail(transportError("message", "WebSocket inbound queue overflow", { | ||
| Queue.failCauseUnsafe(messages, Cause.fail(transportError("WebSocket inbound queue overflow", { | ||
| body: typeof message === "string" ? message : new TextDecoder().decode(message), | ||
| url: input.url, | ||
@@ -186,3 +187,4 @@ operation: "read", | ||
| return offer(binary); | ||
| Queue.failCauseUnsafe(messages, Cause.fail(transportError("message", "Unsupported WebSocket message payload", { | ||
| Queue.failCauseUnsafe(messages, Cause.fail(transportError("Unsupported WebSocket message payload", { | ||
| cause: event, | ||
| url: input.url, | ||
@@ -195,3 +197,4 @@ operation: "read", | ||
| const onError = (event) => { | ||
| Queue.failCauseUnsafe(messages, Cause.fail(transportError("message", `WebSocket error: ${eventMessage(event)}`, { | ||
| Queue.failCauseUnsafe(messages, Cause.fail(transportError(`WebSocket error: ${eventMessage(event)}`, { | ||
| cause: "error" in event ? (event.error ?? event) : event, | ||
| url: input.url, | ||
@@ -204,3 +207,5 @@ operation: "read", | ||
| const onClose = (event) => { | ||
| Queue.failCauseUnsafe(messages, Cause.fail(transportError("message", `WebSocket closed with code ${event.code}`, { | ||
| Queue.failCauseUnsafe(messages, Cause.fail(transportError(`WebSocket closed with code ${event.code}`, { | ||
| body: event.reason, | ||
| cause: event, | ||
| url: input.url, | ||
@@ -223,3 +228,3 @@ operation: "read", | ||
| if (ws.readyState !== globalThis.WebSocket.OPEN) | ||
| return Effect.fail(transportError("sendText", `WebSocket is not open (state ${ws.readyState})`, { | ||
| return Effect.fail(transportError(`WebSocket is not open (state ${ws.readyState})`, { | ||
| url: input.url, | ||
@@ -232,3 +237,4 @@ operation: "write", | ||
| try: () => ws.send(message), | ||
| catch: (error) => transportError("sendText", error instanceof Error ? error.message : "Failed to send WebSocket message", { | ||
| catch: (error) => transportError(error instanceof Error ? error.message : "Failed to send WebSocket message", { | ||
| cause: error, | ||
| url: input.url, | ||
@@ -262,6 +268,14 @@ operation: "write", | ||
| const create = yield* exchange.driver.create(undefined); | ||
| yield* connection.sendText(create.message); | ||
| yield* connection.sendText(create.message).pipe(Effect.mapError((error) => new AIError({ | ||
| reason: AIErrorReason.make({ | ||
| ...error.reason, | ||
| message: error.reason.message, | ||
| cause: error.reason.cause, | ||
| http: error.reason.http ?? connection.http, | ||
| }), | ||
| }))); | ||
| const decoder = new TextDecoder(); | ||
| let observed = false; | ||
| return { | ||
| http: connection.http, | ||
| frames: connection.messages.pipe(Stream.map((message) => { | ||
@@ -273,3 +287,29 @@ observed = true; | ||
| delivery: observed ? "accepted" : "ambiguous", | ||
| })), Stream.mapEffect((frame) => exchange.driver.observe(create, frame)), Stream.takeUntil(observationTerminal), Stream.mapEffect(observationFrame)), | ||
| })), Stream.mapEffect((frame) => exchange.driver.observe(create, frame).pipe(Effect.mapError((error) => new AIError({ | ||
| reason: AIErrorReason.make({ | ||
| ...error.reason, | ||
| message: error.reason.message, | ||
| cause: error.reason.cause, | ||
| body: frame, | ||
| }), | ||
| })), Effect.map((observation) => "error" in observation | ||
| ? { | ||
| ...observation, | ||
| error: new AIError({ | ||
| reason: AIErrorReason.make({ | ||
| ...observation.error.reason, | ||
| message: observation.error.reason.message, | ||
| cause: observation.error.reason.cause, | ||
| body: frame, | ||
| }), | ||
| }), | ||
| } | ||
| : observation))), Stream.takeUntil(observationTerminal), Stream.mapEffect(observationFrame), Stream.mapError((error) => new AIError({ | ||
| reason: AIErrorReason.make({ | ||
| ...error.reason, | ||
| message: error.reason.message, | ||
| cause: error.reason.cause, | ||
| http: error.reason.http ?? connection.http, | ||
| }), | ||
| }))), | ||
| complete: Effect.void, | ||
@@ -301,3 +341,3 @@ }; | ||
| if (!webSocket) { | ||
| return Effect.fail(transportError("json", "WebSocket JSON transport requires StreamOptions.webSocket", { | ||
| return Effect.fail(transportError("WebSocket JSON transport requires StreamOptions.webSocket", { | ||
| url: prepared.url, | ||
@@ -317,3 +357,3 @@ operation: "request", | ||
| connect: { url: prepared.url, headers: prepared.headers }, | ||
| fallback: () => Stream.fail(transportError("fallback", "WebSocket JSON transport does not provide HTTP fallback", { | ||
| fallback: () => Stream.fail(transportError("WebSocket JSON transport does not provide HTTP fallback", { | ||
| url: prepared.url, | ||
@@ -320,0 +360,0 @@ operation: "request", |
+71
-88
@@ -5,15 +5,9 @@ import { Schema } from "effect"; | ||
| export type ProviderFailureClassification = typeof ProviderFailureClassification.Type; | ||
| declare const HttpRequestDetails_base: Schema.Class<HttpRequestDetails, Schema.Struct<{ | ||
| readonly method: Schema.String; | ||
| declare const HttpContext_base: Schema.Class<HttpContext, Schema.Struct<{ | ||
| readonly url: Schema.String; | ||
| readonly status: Schema.Int; | ||
| readonly headers: Schema.$Record<Schema.String, Schema.String>; | ||
| }>, {}>; | ||
| export declare class HttpRequestDetails extends HttpRequestDetails_base { | ||
| export declare class HttpContext extends HttpContext_base { | ||
| } | ||
| declare const HttpResponseDetails_base: Schema.Class<HttpResponseDetails, Schema.Struct<{ | ||
| readonly status: Schema.Number; | ||
| readonly headers: Schema.$Record<Schema.String, Schema.String>; | ||
| }>, {}>; | ||
| export declare class HttpResponseDetails extends HttpResponseDetails_base { | ||
| } | ||
| declare const HttpRateLimitDetails_base: Schema.Class<HttpRateLimitDetails, Schema.Struct<{ | ||
@@ -27,74 +21,66 @@ readonly retryAfterMs: Schema.optional<Schema.Number>; | ||
| } | ||
| declare const HttpContext_base: Schema.Class<HttpContext, Schema.Struct<{ | ||
| readonly request: typeof HttpRequestDetails; | ||
| readonly response: Schema.optional<typeof HttpResponseDetails>; | ||
| readonly body: Schema.optional<Schema.String>; | ||
| readonly bodyTruncated: Schema.optional<Schema.Boolean>; | ||
| readonly rateLimit: Schema.optional<typeof HttpRateLimitDetails>; | ||
| }>, {}>; | ||
| export declare class HttpContext extends HttpContext_base { | ||
| } | ||
| declare const InvalidRequestReason_base: Schema.Class<InvalidRequestReason, Schema.Struct<{ | ||
| readonly _tag: Schema.tag<"InvalidRequest">; | ||
| readonly message: Schema.String; | ||
| declare const InvalidRequestError_base: Schema.Class<InvalidRequestError, Schema.TaggedStruct<"InvalidRequest", { | ||
| readonly parameter: Schema.optional<Schema.String>; | ||
| readonly classification: Schema.optional<Schema.Literals<readonly ["context-overflow", "payload-too-large"]>>; | ||
| readonly providerMetadata: Schema.optional<Schema.$Record<Schema.String, Schema.$Record<Schema.String, Schema.Unknown>>>; | ||
| readonly message: Schema.String; | ||
| readonly body: Schema.optional<Schema.String>; | ||
| readonly http: Schema.optional<typeof HttpContext>; | ||
| }>, {}>; | ||
| export declare class InvalidRequestReason extends InvalidRequestReason_base { | ||
| readonly cause: Schema.optional<Schema.Defect>; | ||
| }>, import("effect/Cause").YieldableError>; | ||
| export declare class InvalidRequestError extends InvalidRequestError_base { | ||
| } | ||
| declare const NoRouteReason_base: Schema.Class<NoRouteReason, Schema.Struct<{ | ||
| readonly _tag: Schema.tag<"NoRoute">; | ||
| declare const NoRouteError_base: Schema.Class<NoRouteError, Schema.TaggedStruct<"NoRoute", { | ||
| readonly route: Schema.String; | ||
| readonly provider: Schema.brand<Schema.String, "AI.ProviderID">; | ||
| readonly model: Schema.brand<Schema.String, "AI.ModelID">; | ||
| }>, {}>; | ||
| export declare class NoRouteReason extends NoRouteReason_base { | ||
| get message(): string; | ||
| readonly message: Schema.String; | ||
| readonly body: Schema.optional<Schema.String>; | ||
| readonly http: Schema.optional<typeof HttpContext>; | ||
| readonly cause: Schema.optional<Schema.Defect>; | ||
| }>, import("effect/Cause").YieldableError>; | ||
| export declare class NoRouteError extends NoRouteError_base { | ||
| } | ||
| declare const AuthenticationReason_base: Schema.Class<AuthenticationReason, Schema.Struct<{ | ||
| readonly _tag: Schema.tag<"Authentication">; | ||
| declare const AuthenticationError_base: Schema.Class<AuthenticationError, Schema.TaggedStruct<"Authentication", { | ||
| readonly kind: Schema.Literals<readonly ["missing", "invalid", "expired", "insufficient-permissions", "unknown"]>; | ||
| readonly message: Schema.String; | ||
| readonly kind: Schema.Literals<readonly ["missing", "invalid", "expired", "insufficient-permissions", "unknown"]>; | ||
| readonly providerMetadata: Schema.optional<Schema.$Record<Schema.String, Schema.$Record<Schema.String, Schema.Unknown>>>; | ||
| readonly body: Schema.optional<Schema.String>; | ||
| readonly http: Schema.optional<typeof HttpContext>; | ||
| }>, {}>; | ||
| export declare class AuthenticationReason extends AuthenticationReason_base { | ||
| readonly cause: Schema.optional<Schema.Defect>; | ||
| }>, import("effect/Cause").YieldableError>; | ||
| export declare class AuthenticationError extends AuthenticationError_base { | ||
| } | ||
| declare const RateLimitReason_base: Schema.Class<RateLimitReason, Schema.Struct<{ | ||
| readonly _tag: Schema.tag<"RateLimit">; | ||
| readonly message: Schema.String; | ||
| declare const RateLimitError_base: Schema.Class<RateLimitError, Schema.TaggedStruct<"RateLimit", { | ||
| readonly retryAfterMs: Schema.optional<Schema.Number>; | ||
| readonly rateLimit: Schema.optional<typeof HttpRateLimitDetails>; | ||
| readonly providerMetadata: Schema.optional<Schema.$Record<Schema.String, Schema.$Record<Schema.String, Schema.Unknown>>>; | ||
| readonly http: Schema.optional<typeof HttpContext>; | ||
| }>, {}>; | ||
| export declare class RateLimitReason extends RateLimitReason_base { | ||
| } | ||
| declare const QuotaExceededReason_base: Schema.Class<QuotaExceededReason, Schema.Struct<{ | ||
| readonly _tag: Schema.tag<"QuotaExceeded">; | ||
| readonly message: Schema.String; | ||
| readonly providerMetadata: Schema.optional<Schema.$Record<Schema.String, Schema.$Record<Schema.String, Schema.Unknown>>>; | ||
| readonly body: Schema.optional<Schema.String>; | ||
| readonly http: Schema.optional<typeof HttpContext>; | ||
| }>, {}>; | ||
| export declare class QuotaExceededReason extends QuotaExceededReason_base { | ||
| readonly cause: Schema.optional<Schema.Defect>; | ||
| }>, import("effect/Cause").YieldableError>; | ||
| export declare class RateLimitError extends RateLimitError_base { | ||
| } | ||
| declare const ContentPolicyReason_base: Schema.Class<ContentPolicyReason, Schema.Struct<{ | ||
| readonly _tag: Schema.tag<"ContentPolicy">; | ||
| readonly message: Schema.String; | ||
| readonly providerMetadata: Schema.optional<Schema.$Record<Schema.String, Schema.$Record<Schema.String, Schema.Unknown>>>; | ||
| readonly http: Schema.optional<typeof HttpContext>; | ||
| }>, {}>; | ||
| export declare class ContentPolicyReason extends ContentPolicyReason_base { | ||
| declare const QuotaExceededError_base: Schema.Class<QuotaExceededError, Schema.TaggedStruct<"QuotaExceeded", { | ||
| message: Schema.String; | ||
| body: Schema.optional<Schema.String>; | ||
| http: Schema.optional<typeof HttpContext>; | ||
| cause: Schema.optional<Schema.Defect>; | ||
| }>, import("effect/Cause").YieldableError>; | ||
| export declare class QuotaExceededError extends QuotaExceededError_base { | ||
| } | ||
| declare const ProviderInternalReason_base: Schema.Class<ProviderInternalReason, Schema.Struct<{ | ||
| readonly _tag: Schema.tag<"ProviderInternal">; | ||
| declare const ContentPolicyError_base: Schema.Class<ContentPolicyError, Schema.TaggedStruct<"ContentPolicy", { | ||
| message: Schema.String; | ||
| body: Schema.optional<Schema.String>; | ||
| http: Schema.optional<typeof HttpContext>; | ||
| cause: Schema.optional<Schema.Defect>; | ||
| }>, import("effect/Cause").YieldableError>; | ||
| export declare class ContentPolicyError extends ContentPolicyError_base { | ||
| } | ||
| declare const ProviderInternalError_base: Schema.Class<ProviderInternalError, Schema.TaggedStruct<"ProviderInternal", { | ||
| readonly retryAfterMs: Schema.optional<Schema.Number>; | ||
| readonly message: Schema.String; | ||
| readonly status: Schema.optional<Schema.Number>; | ||
| readonly retryAfterMs: Schema.optional<Schema.Number>; | ||
| readonly providerMetadata: Schema.optional<Schema.$Record<Schema.String, Schema.$Record<Schema.String, Schema.Unknown>>>; | ||
| readonly body: Schema.optional<Schema.String>; | ||
| readonly http: Schema.optional<typeof HttpContext>; | ||
| }>, {}>; | ||
| export declare class ProviderInternalReason extends ProviderInternalReason_base { | ||
| readonly cause: Schema.optional<Schema.Defect>; | ||
| }>, import("effect/Cause").YieldableError>; | ||
| export declare class ProviderInternalError extends ProviderInternalError_base { | ||
| } | ||
@@ -105,5 +91,3 @@ export declare const TransportType: Schema.Literals<readonly ["http", "websocket"]>; | ||
| export type TransportOperation = typeof TransportOperation.Type; | ||
| declare const TransportReason_base: Schema.Class<TransportReason, Schema.Struct<{ | ||
| readonly _tag: Schema.tag<"Transport">; | ||
| readonly message: Schema.String; | ||
| declare const TransportError_base: Schema.Class<TransportError, Schema.TaggedStruct<"Transport", { | ||
| readonly transport: Schema.Literals<readonly ["http", "websocket"]>; | ||
@@ -113,38 +97,37 @@ readonly operation: Schema.Literals<readonly ["request", "read", "write"]>; | ||
| readonly url: Schema.optional<Schema.String>; | ||
| readonly http: Schema.optional<typeof HttpContext>; | ||
| readonly phase: Schema.optional<Schema.Literals<readonly ["prepare", "queue", "connect", "send", "receive", "decode", "complete", "fallback", "close"]>>; | ||
| readonly delivery: Schema.optional<Schema.Literals<readonly ["not-sent", "rejected", "ambiguous", "accepted"]>>; | ||
| readonly recovery: Schema.optional<Schema.Literals<readonly ["retry-connect", "retry-full", "rotate-and-retry-full", "fallback-http", "fail"]>>; | ||
| }>, {}>; | ||
| export declare class TransportReason extends TransportReason_base { | ||
| readonly message: Schema.String; | ||
| readonly body: Schema.optional<Schema.String>; | ||
| readonly http: Schema.optional<typeof HttpContext>; | ||
| readonly cause: Schema.optional<Schema.Defect>; | ||
| }>, import("effect/Cause").YieldableError>; | ||
| export declare class TransportError extends TransportError_base { | ||
| } | ||
| declare const InvalidProviderOutputReason_base: Schema.Class<InvalidProviderOutputReason, Schema.Struct<{ | ||
| readonly _tag: Schema.tag<"InvalidProviderOutput">; | ||
| readonly message: Schema.String; | ||
| declare const InvalidProviderOutputError_base: Schema.Class<InvalidProviderOutputError, Schema.TaggedStruct<"InvalidProviderOutput", { | ||
| readonly classification: Schema.optional<Schema.Literals<readonly ["incomplete-stream"]>>; | ||
| readonly route: Schema.optional<Schema.String>; | ||
| readonly raw: Schema.optional<Schema.String>; | ||
| readonly providerMetadata: Schema.optional<Schema.$Record<Schema.String, Schema.$Record<Schema.String, Schema.Unknown>>>; | ||
| }>, {}>; | ||
| export declare class InvalidProviderOutputReason extends InvalidProviderOutputReason_base { | ||
| } | ||
| declare const UnknownProviderReason_base: Schema.Class<UnknownProviderReason, Schema.Struct<{ | ||
| readonly _tag: Schema.tag<"UnknownProvider">; | ||
| readonly message: Schema.String; | ||
| readonly status: Schema.optional<Schema.Number>; | ||
| readonly providerMetadata: Schema.optional<Schema.$Record<Schema.String, Schema.$Record<Schema.String, Schema.Unknown>>>; | ||
| readonly body: Schema.optional<Schema.String>; | ||
| readonly http: Schema.optional<typeof HttpContext>; | ||
| }>, {}>; | ||
| export declare class UnknownProviderReason extends UnknownProviderReason_base { | ||
| readonly cause: Schema.optional<Schema.Defect>; | ||
| }>, import("effect/Cause").YieldableError>; | ||
| export declare class InvalidProviderOutputError extends InvalidProviderOutputError_base { | ||
| } | ||
| export declare const AIErrorReason: Schema.toTaggedUnion<"_tag", readonly [typeof InvalidRequestReason, typeof NoRouteReason, typeof AuthenticationReason, typeof RateLimitReason, typeof QuotaExceededReason, typeof ContentPolicyReason, typeof ProviderInternalReason, typeof TransportReason, typeof InvalidProviderOutputReason, typeof UnknownProviderReason]>; | ||
| declare const UnknownProviderError_base: Schema.Class<UnknownProviderError, Schema.TaggedStruct<"UnknownProvider", { | ||
| message: Schema.String; | ||
| body: Schema.optional<Schema.String>; | ||
| http: Schema.optional<typeof HttpContext>; | ||
| cause: Schema.optional<Schema.Defect>; | ||
| }>, import("effect/Cause").YieldableError>; | ||
| export declare class UnknownProviderError extends UnknownProviderError_base { | ||
| } | ||
| export declare const AIErrorReason: Schema.toTaggedUnion<"_tag", readonly [typeof InvalidRequestError, typeof NoRouteError, typeof AuthenticationError, typeof RateLimitError, typeof QuotaExceededError, typeof ContentPolicyError, typeof ProviderInternalError, typeof TransportError, typeof InvalidProviderOutputError, typeof UnknownProviderError]>; | ||
| export type AIErrorReason = Schema.Schema.Type<typeof AIErrorReason>; | ||
| declare const AIError_base: Schema.Class<AIError, Schema.TaggedStruct<"AI.Error", { | ||
| readonly module: Schema.String; | ||
| readonly method: Schema.String; | ||
| readonly reason: Schema.toTaggedUnion<"_tag", readonly [typeof InvalidRequestReason, typeof NoRouteReason, typeof AuthenticationReason, typeof RateLimitReason, typeof QuotaExceededReason, typeof ContentPolicyReason, typeof ProviderInternalReason, typeof TransportReason, typeof InvalidProviderOutputReason, typeof UnknownProviderReason]>; | ||
| readonly body: Schema.optional<Schema.String>; | ||
| readonly reason: Schema.toTaggedUnion<"_tag", readonly [typeof InvalidRequestError, typeof NoRouteError, typeof AuthenticationError, typeof RateLimitError, typeof QuotaExceededError, typeof ContentPolicyError, typeof ProviderInternalError, typeof TransportError, typeof InvalidProviderOutputError, typeof UnknownProviderError]>; | ||
| }>, import("effect/Cause").YieldableError>; | ||
| export declare class AIError extends AIError_base { | ||
| readonly cause: InvalidRequestReason | NoRouteReason | AuthenticationReason | RateLimitReason | QuotaExceededReason | ContentPolicyReason | ProviderInternalReason | TransportReason | InvalidProviderOutputReason | UnknownProviderReason; | ||
| readonly cause: InvalidRequestError | NoRouteError | AuthenticationError | RateLimitError | QuotaExceededError | ContentPolicyError | ProviderInternalError | TransportError | InvalidProviderOutputError | UnknownProviderError; | ||
| get message(): string; | ||
@@ -151,0 +134,0 @@ } |
+36
-85
| import { Schema } from "effect"; | ||
| import { Tool } from "@opencode-ai/schema/tool"; | ||
| import { ModelID, ProviderID, RouteID } from "./ids.js"; | ||
| import { ProviderMetadata } from "./messages.js"; | ||
| export const ProviderFailureClassification = Schema.Literals(["context-overflow", "payload-too-large"]); | ||
| export class HttpRequestDetails extends Schema.Class("AI.HttpRequestDetails")({ | ||
| method: Schema.String, | ||
| export class HttpContext extends Schema.Class("AI.HttpContext")({ | ||
| url: Schema.String, | ||
| status: Schema.Int.check(Schema.isBetween({ minimum: 100, maximum: 599 })), | ||
| headers: Schema.Record(Schema.String, Schema.String), | ||
| }) { | ||
| } | ||
| export class HttpResponseDetails extends Schema.Class("AI.HttpResponseDetails")({ | ||
| status: Schema.Number, | ||
| headers: Schema.Record(Schema.String, Schema.String), | ||
| }) { | ||
| } | ||
| export class HttpRateLimitDetails extends Schema.Class("AI.HttpRateLimitDetails")({ | ||
@@ -24,21 +18,17 @@ retryAfterMs: Schema.optional(Schema.Number), | ||
| } | ||
| export class HttpContext extends Schema.Class("AI.HttpContext")({ | ||
| request: HttpRequestDetails, | ||
| response: Schema.optional(HttpResponseDetails), | ||
| const ReasonFields = { | ||
| message: Schema.String, | ||
| // Preserve the complete original response or triggering event before decoding narrows it. | ||
| body: Schema.optional(Schema.String), | ||
| bodyTruncated: Schema.optional(Schema.Boolean), | ||
| rateLimit: Schema.optional(HttpRateLimitDetails), | ||
| }) { | ||
| } | ||
| export class InvalidRequestReason extends Schema.Class("AI.Error.InvalidRequest")({ | ||
| _tag: Schema.tag("InvalidRequest"), | ||
| message: Schema.String, | ||
| http: Schema.optional(HttpContext), | ||
| cause: Schema.optional(Schema.Defect({ includeStack: true })), | ||
| }; | ||
| export class InvalidRequestError extends Schema.TaggedError("AI.Error.InvalidRequest")("InvalidRequest", { | ||
| ...ReasonFields, | ||
| parameter: Schema.optional(Schema.String), | ||
| classification: Schema.optional(ProviderFailureClassification), | ||
| providerMetadata: Schema.optional(ProviderMetadata), | ||
| http: Schema.optional(HttpContext), | ||
| }) { | ||
| } | ||
| export class NoRouteReason extends Schema.Class("AI.Error.NoRoute")({ | ||
| _tag: Schema.tag("NoRoute"), | ||
| export class NoRouteError extends Schema.TaggedError("AI.Error.NoRoute")("NoRoute", { | ||
| ...ReasonFields, | ||
| route: RouteID, | ||
@@ -48,44 +38,21 @@ provider: ProviderID, | ||
| }) { | ||
| get message() { | ||
| return `No AI route for ${this.provider}/${this.model} using ${this.route}`; | ||
| } | ||
| } | ||
| export class AuthenticationReason extends Schema.Class("AI.Error.Authentication")({ | ||
| _tag: Schema.tag("Authentication"), | ||
| message: Schema.String, | ||
| export class AuthenticationError extends Schema.TaggedError("AI.Error.Authentication")("Authentication", { | ||
| ...ReasonFields, | ||
| kind: Schema.Literals(["missing", "invalid", "expired", "insufficient-permissions", "unknown"]), | ||
| providerMetadata: Schema.optional(ProviderMetadata), | ||
| http: Schema.optional(HttpContext), | ||
| }) { | ||
| } | ||
| export class RateLimitReason extends Schema.Class("AI.Error.RateLimit")({ | ||
| _tag: Schema.tag("RateLimit"), | ||
| message: Schema.String, | ||
| export class RateLimitError extends Schema.TaggedError("AI.Error.RateLimit")("RateLimit", { | ||
| ...ReasonFields, | ||
| retryAfterMs: Schema.optional(Schema.Number), | ||
| rateLimit: Schema.optional(HttpRateLimitDetails), | ||
| providerMetadata: Schema.optional(ProviderMetadata), | ||
| http: Schema.optional(HttpContext), | ||
| }) { | ||
| } | ||
| export class QuotaExceededReason extends Schema.Class("AI.Error.QuotaExceeded")({ | ||
| _tag: Schema.tag("QuotaExceeded"), | ||
| message: Schema.String, | ||
| providerMetadata: Schema.optional(ProviderMetadata), | ||
| http: Schema.optional(HttpContext), | ||
| }) { | ||
| export class QuotaExceededError extends Schema.TaggedError("AI.Error.QuotaExceeded")("QuotaExceeded", ReasonFields) { | ||
| } | ||
| export class ContentPolicyReason extends Schema.Class("AI.Error.ContentPolicy")({ | ||
| _tag: Schema.tag("ContentPolicy"), | ||
| message: Schema.String, | ||
| providerMetadata: Schema.optional(ProviderMetadata), | ||
| http: Schema.optional(HttpContext), | ||
| }) { | ||
| export class ContentPolicyError extends Schema.TaggedError("AI.Error.ContentPolicy")("ContentPolicy", ReasonFields) { | ||
| } | ||
| export class ProviderInternalReason extends Schema.Class("AI.Error.ProviderInternal")({ | ||
| _tag: Schema.tag("ProviderInternal"), | ||
| message: Schema.String, | ||
| status: Schema.optional(Schema.Number), | ||
| export class ProviderInternalError extends Schema.TaggedError("AI.Error.ProviderInternal")("ProviderInternal", { | ||
| ...ReasonFields, | ||
| retryAfterMs: Schema.optional(Schema.Number), | ||
| providerMetadata: Schema.optional(ProviderMetadata), | ||
| http: Schema.optional(HttpContext), | ||
| }) { | ||
@@ -95,5 +62,4 @@ } | ||
| export const TransportOperation = Schema.Literals(["request", "read", "write"]); | ||
| export class TransportReason extends Schema.Class("AI.Error.Transport")({ | ||
| _tag: Schema.tag("Transport"), | ||
| message: Schema.String, | ||
| export class TransportError extends Schema.TaggedError("AI.Error.Transport")("Transport", { | ||
| ...ReasonFields, | ||
| transport: TransportType, | ||
@@ -103,3 +69,2 @@ operation: TransportOperation, | ||
| url: Schema.optional(Schema.String), | ||
| http: Schema.optional(HttpContext), | ||
| phase: Schema.optional(Schema.Literals(["prepare", "queue", "connect", "send", "receive", "decode", "complete", "fallback", "close"])), | ||
@@ -110,42 +75,28 @@ delivery: Schema.optional(Schema.Literals(["not-sent", "rejected", "ambiguous", "accepted"])), | ||
| } | ||
| export class InvalidProviderOutputReason extends Schema.Class("AI.Error.InvalidProviderOutput")({ | ||
| _tag: Schema.tag("InvalidProviderOutput"), | ||
| message: Schema.String, | ||
| export class InvalidProviderOutputError extends Schema.TaggedError("AI.Error.InvalidProviderOutput")("InvalidProviderOutput", { | ||
| ...ReasonFields, | ||
| classification: Schema.optional(Schema.Literals(["incomplete-stream"])), | ||
| route: Schema.optional(Schema.String), | ||
| raw: Schema.optional(Schema.String), | ||
| providerMetadata: Schema.optional(ProviderMetadata), | ||
| }) { | ||
| } | ||
| export class UnknownProviderReason extends Schema.Class("AI.Error.UnknownProvider")({ | ||
| _tag: Schema.tag("UnknownProvider"), | ||
| message: Schema.String, | ||
| status: Schema.optional(Schema.Number), | ||
| providerMetadata: Schema.optional(ProviderMetadata), | ||
| http: Schema.optional(HttpContext), | ||
| }) { | ||
| export class UnknownProviderError extends Schema.TaggedError("AI.Error.UnknownProvider")("UnknownProvider", ReasonFields) { | ||
| } | ||
| export const AIErrorReason = Schema.Union([ | ||
| InvalidRequestReason, | ||
| NoRouteReason, | ||
| AuthenticationReason, | ||
| RateLimitReason, | ||
| QuotaExceededReason, | ||
| ContentPolicyReason, | ||
| ProviderInternalReason, | ||
| TransportReason, | ||
| InvalidProviderOutputReason, | ||
| UnknownProviderReason, | ||
| InvalidRequestError, | ||
| NoRouteError, | ||
| AuthenticationError, | ||
| RateLimitError, | ||
| QuotaExceededError, | ||
| ContentPolicyError, | ||
| ProviderInternalError, | ||
| TransportError, | ||
| InvalidProviderOutputError, | ||
| UnknownProviderError, | ||
| ]).pipe(Schema.toTaggedUnion("_tag")); | ||
| export class AIError extends Schema.TaggedError()("AI.Error", { | ||
| module: Schema.String, | ||
| method: Schema.String, | ||
| reason: AIErrorReason, | ||
| // Raw provider payload as a string, so classified failures never lose the | ||
| // original error detail even when the pretty message is a summary. | ||
| body: Schema.optional(Schema.String), | ||
| }) { | ||
| cause = this.reason; | ||
| get message() { | ||
| return `${this.module}.${this.method}: ${this.reason.message}`; | ||
| return this.reason.message; | ||
| } | ||
@@ -152,0 +103,0 @@ } |
+3
-3
| { | ||
| "$schema": "https://json.schemastore.org/package.json", | ||
| "version": "0.0.0-dev-18409", | ||
| "version": "0.0.0-dev-18411", | ||
| "name": "@opencode-ai/ai", | ||
@@ -33,3 +33,3 @@ "type": "module", | ||
| "@effect/platform-node": "4.0.0-rc.111", | ||
| "@opencode-ai/http-recorder": "0.0.0-dev-18409", | ||
| "@opencode-ai/http-recorder": "0.0.0-dev-18411", | ||
| "@tsconfig/bun": "1.0.9", | ||
@@ -43,3 +43,3 @@ "@types/bun": "1.3.13", | ||
| "@smithy/util-utf8": "4.2.2", | ||
| "@opencode-ai/schema": "0.0.0-dev-18409", | ||
| "@opencode-ai/schema": "0.0.0-dev-18411", | ||
| "aws4fetch": "1.0.20", | ||
@@ -46,0 +46,0 @@ "effect": "4.0.0-rc.111", |
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
1366828
0.07%31404
-0.03%+ Added
- Removed