@opencode-ai/ai 0.0.0-dev-18409 → 0.0.0-dev-18411
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/image.js +5 -4
- package/dist/llm.js +4 -7
- package/dist/protocols/anthropic-messages.js +7 -5
- package/dist/protocols/bedrock-converse.js +4 -6
- package/dist/protocols/bedrock-event-stream.js +16 -6
- package/dist/protocols/gemini.d.ts +1 -0
- package/dist/protocols/gemini.js +13 -1
- package/dist/protocols/google-images.js +8 -18
- package/dist/protocols/open-responses-channel.js +20 -9
- package/dist/protocols/open-responses-continuation.js +9 -8
- package/dist/protocols/open-responses.d.ts +1 -1
- package/dist/protocols/open-responses.js +7 -16
- package/dist/protocols/openai-chat.js +23 -18
- package/dist/protocols/openai-images.js +11 -16
- package/dist/protocols/openai-responses.js +1 -1
- package/dist/protocols/shared.d.ts +7 -3
- package/dist/protocols/shared.js +26 -13
- package/dist/protocols/utils/image-input.d.ts +4 -4
- package/dist/protocols/utils/image-input.js +6 -8
- package/dist/protocols/xai-images.js +7 -12
- package/dist/protocols/zai-images.js +5 -10
- package/dist/provider-error.d.ts +4 -4
- package/dist/provider-error.js +31 -32
- package/dist/route/auth.js +3 -5
- package/dist/route/client.js +29 -11
- package/dist/route/executor.d.ts +9 -5
- package/dist/route/executor.js +33 -66
- package/dist/route/framing.d.ts +2 -0
- package/dist/route/transport/http.js +7 -2
- package/dist/route/transport/index.d.ts +3 -1
- package/dist/route/transport/websocket-channel.d.ts +2 -1
- package/dist/route/transport/websocket.d.ts +2 -1
- package/dist/route/transport/websocket.js +70 -30
- package/dist/schema/errors.d.ts +71 -88
- package/dist/schema/errors.js +36 -85
- package/package.json +3 -3
package/dist/image.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Effect, Schema } from "effect";
|
|
2
|
-
import { HttpOptions,
|
|
2
|
+
import { HttpOptions, InvalidRequestError, AIError, ModelID, ProviderID, ProviderMetadata, Usage, } from "./schema/index.js";
|
|
3
3
|
import { ImageClient, Service } from "./image-client.js";
|
|
4
4
|
export class ImageModel {
|
|
5
5
|
id;
|
|
@@ -90,9 +90,10 @@ export function generate(input) {
|
|
|
90
90
|
return Effect.try({
|
|
91
91
|
try: () => (input instanceof ImageRequest ? input : request(input)),
|
|
92
92
|
catch: (error) => new AIError({
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
reason: new InvalidRequestError({
|
|
94
|
+
message: error instanceof Error ? error.message : String(error),
|
|
95
|
+
cause: error,
|
|
96
|
+
}),
|
|
96
97
|
}),
|
|
97
98
|
}).pipe(Effect.flatMap((request) => ImageClient.generate(request)));
|
|
98
99
|
}
|
package/dist/llm.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Effect, JsonSchema, Schema } from "effect";
|
|
2
2
|
import { LLMClient, Service } from "./route/client.js";
|
|
3
|
-
import { GenerationOptions, HttpOptions,
|
|
3
|
+
import { GenerationOptions, HttpOptions, InvalidProviderOutputError, AIError, LLMEvent, LLMRequest, LLMResponse, Message, LanguageModel, SystemPart, ToolChoice, ToolDefinition, } from "./schema/index.js";
|
|
4
4
|
import { make as makeTool, toDefinitions } from "./tool.js";
|
|
5
5
|
export const generate = LLMClient.generate;
|
|
6
6
|
export const stream = LLMClient.stream;
|
|
@@ -43,17 +43,14 @@ const runGenerateObject = Effect.fn("LLM.generateObject")(function* (options, to
|
|
|
43
43
|
const call = response.toolCalls.find((event) => LLMEvent.is.toolCall(event) && event.name === GENERATE_OBJECT_TOOL_NAME);
|
|
44
44
|
if (!call || !LLMEvent.is.toolCall(call))
|
|
45
45
|
return yield* new AIError({
|
|
46
|
-
|
|
47
|
-
method: "generateObject",
|
|
48
|
-
reason: new InvalidProviderOutputReason({
|
|
46
|
+
reason: new InvalidProviderOutputError({
|
|
49
47
|
message: `generateObject: model did not call the forced \`${GENERATE_OBJECT_TOOL_NAME}\` tool`,
|
|
50
48
|
}),
|
|
51
49
|
});
|
|
52
50
|
const object = yield* tool._decode(call.input).pipe(Effect.mapError((error) => new AIError({
|
|
53
|
-
|
|
54
|
-
method: "generateObject",
|
|
55
|
-
reason: new InvalidProviderOutputReason({
|
|
51
|
+
reason: new InvalidProviderOutputError({
|
|
56
52
|
message: `generateObject: tool input failed schema decode: ${error.message}`,
|
|
53
|
+
cause: error,
|
|
57
54
|
}),
|
|
58
55
|
})));
|
|
59
56
|
return new GenerateObjectResponse(object, response);
|
|
@@ -1167,11 +1167,13 @@ const providerErrorMessage = (event) => {
|
|
|
1167
1167
|
return `${type}: ${message}`;
|
|
1168
1168
|
return message || type || "Anthropic Messages stream error";
|
|
1169
1169
|
};
|
|
1170
|
-
const onError = (event) =>
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
})
|
|
1170
|
+
const onError = (event) => {
|
|
1171
|
+
const message = providerErrorMessage(event);
|
|
1172
|
+
const body = ProviderShared.encodeJson(event);
|
|
1173
|
+
return Effect.fail(new AIError({
|
|
1174
|
+
reason: classifyProviderFailure({ message, rawBody: body }),
|
|
1175
|
+
}));
|
|
1176
|
+
};
|
|
1175
1177
|
const isKnownStreamBlockType = (type) => type === "text" ||
|
|
1176
1178
|
type === "thinking" ||
|
|
1177
1179
|
type === "redacted_thinking" ||
|
|
@@ -507,14 +507,12 @@ const step = (state, event) => Effect.gen(function* () {
|
|
|
507
507
|
];
|
|
508
508
|
}
|
|
509
509
|
if (event.exception) {
|
|
510
|
+
const message = event.exception.details.message ?? event.exception.details.originalMessage ?? "Bedrock Converse stream error";
|
|
511
|
+
const body = ProviderShared.encodeJson(event);
|
|
510
512
|
return yield* new AIError({
|
|
511
|
-
module: ADAPTER,
|
|
512
|
-
method: "stream",
|
|
513
513
|
reason: classifyProviderFailure({
|
|
514
|
-
message
|
|
515
|
-
|
|
516
|
-
"Bedrock Converse stream error",
|
|
517
|
-
code: event.exception.type,
|
|
514
|
+
message,
|
|
515
|
+
rawBody: body,
|
|
518
516
|
}),
|
|
519
517
|
});
|
|
520
518
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { EventStreamCodec } from "@smithy/eventstream-codec";
|
|
2
2
|
import { fromUtf8, toUtf8 } from "@smithy/util-utf8";
|
|
3
|
-
import { Effect, Stream } from "effect";
|
|
3
|
+
import { Effect, Encoding, Stream } from "effect";
|
|
4
|
+
import { AIError, AIErrorReason } from "../schema/index.js";
|
|
4
5
|
import { Framing } from "../route/framing.js";
|
|
5
6
|
import { ProviderShared } from "./shared.js";
|
|
6
7
|
// Bedrock streams responses using the AWS event stream binary protocol — each
|
|
@@ -30,15 +31,17 @@ const consumeFrames = (route) => (state, chunk) => Effect.gen(function* () {
|
|
|
30
31
|
break;
|
|
31
32
|
const decoded = yield* Effect.try({
|
|
32
33
|
try: () => eventCodec.decode(view.subarray(0, totalLength)),
|
|
33
|
-
catch: (error) => ProviderShared.eventError(route, `Failed to decode Bedrock Converse event-stream frame: ${error instanceof Error ? error.message : String(error)}
|
|
34
|
+
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),
|
|
34
35
|
});
|
|
35
36
|
cursor = { buffer: cursor.buffer, offset: cursor.offset + totalLength };
|
|
37
|
+
const payload = utf8.decode(decoded.body);
|
|
38
|
+
const body = ProviderShared.encodeJson({ headers: decoded.headers, body: payload });
|
|
36
39
|
const messageType = decoded.headers[":message-type"]?.value;
|
|
37
40
|
if (messageType === "error") {
|
|
38
41
|
const code = decoded.headers[":error-code"]?.value;
|
|
39
42
|
const message = decoded.headers[":error-message"]?.value;
|
|
40
43
|
return yield* ProviderShared.eventError(route, [code, message].filter((value) => typeof value === "string").join(": ") ||
|
|
41
|
-
"Bedrock Converse event-stream error");
|
|
44
|
+
"Bedrock Converse event-stream error", body);
|
|
42
45
|
}
|
|
43
46
|
const eventType = messageType === "event"
|
|
44
47
|
? decoded.headers[":event-type"]?.value
|
|
@@ -47,16 +50,22 @@ const consumeFrames = (route) => (state, chunk) => Effect.gen(function* () {
|
|
|
47
50
|
: undefined;
|
|
48
51
|
if (typeof eventType !== "string")
|
|
49
52
|
continue;
|
|
50
|
-
const payload = utf8.decode(decoded.body);
|
|
51
53
|
if (!payload)
|
|
52
54
|
continue;
|
|
53
55
|
// The AWS event stream pads short payloads with a `p` field. Drop it
|
|
54
56
|
// before handing the object to the chunk schema. JSON decode goes
|
|
55
57
|
// through the shared Schema-driven codec to satisfy the package rule
|
|
56
58
|
// against ad-hoc `JSON.parse` calls.
|
|
57
|
-
const parsed = (yield* ProviderShared.parseJson(route, payload, "Failed to parse Bedrock Converse event-stream payload"))
|
|
59
|
+
const parsed = (yield* ProviderShared.parseJson(route, payload, "Failed to parse Bedrock Converse event-stream payload").pipe(Effect.mapError((error) => new AIError({
|
|
60
|
+
reason: AIErrorReason.make({ ...error.reason, message: error.message, cause: error.reason.cause, body }),
|
|
61
|
+
}))));
|
|
58
62
|
delete parsed.p;
|
|
59
|
-
out.push(
|
|
63
|
+
out.push({
|
|
64
|
+
...(messageType === "exception"
|
|
65
|
+
? { exception: { type: eventType, details: parsed } }
|
|
66
|
+
: { [eventType]: parsed }),
|
|
67
|
+
rawBody: body,
|
|
68
|
+
});
|
|
60
69
|
}
|
|
61
70
|
return [cursor, out];
|
|
62
71
|
});
|
|
@@ -68,6 +77,7 @@ const consumeFrames = (route) => (state, chunk) => Effect.gen(function* () {
|
|
|
68
77
|
*/
|
|
69
78
|
export const framing = (route) => ({
|
|
70
79
|
id: "aws-event-stream",
|
|
80
|
+
body: (frame) => ("rawBody" in frame && typeof frame.rawBody === "string" ? frame.rawBody : undefined),
|
|
71
81
|
frame: (bytes) => bytes.pipe(Stream.mapAccumEffect(() => initialFrameBuffer, consumeFrames(route))),
|
|
72
82
|
});
|
|
73
83
|
export * as BedrockEventStream from "./bedrock-event-stream.js";
|
|
@@ -175,6 +175,7 @@ export declare const protocol: Protocol<{
|
|
|
175
175
|
readonly maxOutputTokens?: number | undefined;
|
|
176
176
|
} | undefined;
|
|
177
177
|
}, string, {
|
|
178
|
+
readonly error?: unknown;
|
|
178
179
|
readonly candidates?: readonly {
|
|
179
180
|
readonly content?: {
|
|
180
181
|
readonly parts?: readonly unknown[] | null | undefined;
|
package/dist/protocols/gemini.js
CHANGED
|
@@ -5,7 +5,8 @@ import { Auth } from "../route/auth.js";
|
|
|
5
5
|
import { Endpoint } from "../route/endpoint.js";
|
|
6
6
|
import { Framing } from "../route/framing.js";
|
|
7
7
|
import { Protocol } from "../route/protocol.js";
|
|
8
|
-
import { LLMEvent, Usage, } from "../schema/index.js";
|
|
8
|
+
import { AIError, LLMEvent, Usage, } from "../schema/index.js";
|
|
9
|
+
import { classifyProviderFailure } from "../provider-error.js";
|
|
9
10
|
import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared.js";
|
|
10
11
|
import { GeminiToolSchema } from "./utils/gemini-tool-schema.js";
|
|
11
12
|
import { Lifecycle } from "./utils/lifecycle.js";
|
|
@@ -151,6 +152,7 @@ const GeminiPromptFeedback = Schema.StructWithRest(Schema.Struct({
|
|
|
151
152
|
safetyRatings: optionalNull(Schema.Unknown),
|
|
152
153
|
}), [Schema.Record(Schema.String, Schema.Unknown)]);
|
|
153
154
|
const GeminiEvent = Schema.Struct({
|
|
155
|
+
error: Schema.optional(Schema.Unknown),
|
|
154
156
|
candidates: optionalNull(Schema.Array(GeminiCandidate)),
|
|
155
157
|
promptFeedback: optionalNull(GeminiPromptFeedback),
|
|
156
158
|
usageMetadata: optionalNull(GeminiUsage),
|
|
@@ -479,6 +481,16 @@ const finish = (state) => {
|
|
|
479
481
|
return events;
|
|
480
482
|
};
|
|
481
483
|
const step = (state, event) => {
|
|
484
|
+
if (ProviderShared.isRecord(event.error) && typeof event.error.message === "string") {
|
|
485
|
+
const body = ProviderShared.encodeJson(event);
|
|
486
|
+
return Effect.fail(new AIError({
|
|
487
|
+
reason: classifyProviderFailure({
|
|
488
|
+
message: event.error.message,
|
|
489
|
+
status: typeof event.error.code === "number" ? event.error.code : undefined,
|
|
490
|
+
rawBody: body,
|
|
491
|
+
}),
|
|
492
|
+
}));
|
|
493
|
+
}
|
|
482
494
|
const nextState = {
|
|
483
495
|
...state,
|
|
484
496
|
promptFeedback: event.promptFeedback ?? state.promptFeedback,
|
|
@@ -2,7 +2,7 @@ import { Effect, Encoding, Schema } from "effect";
|
|
|
2
2
|
import { Headers, HttpClientRequest } from "effect/unstable/http";
|
|
3
3
|
import { GeneratedImage, ImageModel, ImageResponse, } from "../image.js";
|
|
4
4
|
import { Auth } from "../route/auth.js";
|
|
5
|
-
import {
|
|
5
|
+
import { AIError, Usage, mergeHttpOptions, mergeJsonRecords } from "../schema/index.js";
|
|
6
6
|
import { ProviderShared } from "./shared.js";
|
|
7
7
|
import { ImageInputs } from "./utils/image-input.js";
|
|
8
8
|
const ADAPTER = "google-images";
|
|
@@ -58,11 +58,6 @@ const nativeOptions = (options) => {
|
|
|
58
58
|
thinkingConfig: Object.values(thinkingConfig).some((value) => value !== undefined) ? thinkingConfig : undefined,
|
|
59
59
|
}, native) ?? { responseModalities: ["IMAGE"] });
|
|
60
60
|
};
|
|
61
|
-
const invalidOutput = (message, providerMetadata) => new AIError({
|
|
62
|
-
module: ADAPTER,
|
|
63
|
-
method: "generate",
|
|
64
|
-
reason: new InvalidProviderOutputReason({ message, route: ADAPTER, providerMetadata }),
|
|
65
|
-
});
|
|
66
61
|
const applyQuery = (url, query) => {
|
|
67
62
|
if (!query)
|
|
68
63
|
return url;
|
|
@@ -90,8 +85,8 @@ export const model = (input) => {
|
|
|
90
85
|
headers: Headers.fromInput({ ...input.headers, ...http?.headers }),
|
|
91
86
|
});
|
|
92
87
|
const response = yield* execute(HttpClientRequest.post(url).pipe(HttpClientRequest.setHeaders(headers), HttpClientRequest.bodyText(text, "application/json")));
|
|
93
|
-
const
|
|
94
|
-
const decoded = yield* Schema.decodeUnknownEffect(GoogleImageResponse)(
|
|
88
|
+
const output = yield* ProviderShared.imageResponse(ADAPTER, "Google Images", response);
|
|
89
|
+
const decoded = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(GoogleImageResponse))(output.body).pipe(Effect.mapError((cause) => output.invalid("Google Images returned an invalid response", cause)));
|
|
95
90
|
const candidates = decoded.candidates ?? [];
|
|
96
91
|
const candidateMetadata = candidates.map((candidate, candidateIndex) => ({
|
|
97
92
|
index: candidate.index ?? candidateIndex,
|
|
@@ -117,7 +112,7 @@ export const model = (input) => {
|
|
|
117
112
|
const encoded = candidates.flatMap((candidate, candidateIndex) => (candidate.content?.parts ?? []).flatMap((part, partIndex) => part.inlineData === undefined || part.thought === true
|
|
118
113
|
? []
|
|
119
114
|
: [{ candidate, candidateIndex, partIndex, inlineData: part.inlineData }]));
|
|
120
|
-
const images = yield* Effect.forEach(encoded, (item) => Effect.fromResult(Encoding.decodeBase64(item.inlineData.data)).pipe(Effect.mapError(() =>
|
|
115
|
+
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({
|
|
121
116
|
mediaType: item.inlineData.mimeType,
|
|
122
117
|
data,
|
|
123
118
|
providerMetadata: {
|
|
@@ -134,12 +129,7 @@ export const model = (input) => {
|
|
|
134
129
|
}))));
|
|
135
130
|
if (images.length === 0) {
|
|
136
131
|
const finishReasons = candidates.flatMap((candidate) => candidate.finishReason === undefined ? [] : [candidate.finishReason]);
|
|
137
|
-
return yield*
|
|
138
|
-
google: {
|
|
139
|
-
promptFeedback: decoded.promptFeedback,
|
|
140
|
-
candidates: candidateMetadata,
|
|
141
|
-
},
|
|
142
|
-
});
|
|
132
|
+
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`);
|
|
143
133
|
}
|
|
144
134
|
const usage = decoded.usageMetadata;
|
|
145
135
|
const outputTokens = usage?.candidatesTokenCount === undefined
|
|
@@ -177,14 +167,14 @@ const googleImagePart = (image) => {
|
|
|
177
167
|
if (image.type === "file-uri")
|
|
178
168
|
return Effect.succeed({ fileData: { mimeType: image.mediaType, fileUri: image.uri } });
|
|
179
169
|
if (image.type === "url")
|
|
180
|
-
return ImageInputs.decodeDataUrl(image.url
|
|
170
|
+
return ImageInputs.decodeDataUrl(image.url).pipe(Effect.flatMap((decoded) => {
|
|
181
171
|
if (decoded === undefined)
|
|
182
|
-
return Effect.fail(ImageInputs.invalid(
|
|
172
|
+
return Effect.fail(ImageInputs.invalid("Google generateContent does not fetch public image URLs; use bytes, a data URL, or a Gemini file URI"));
|
|
183
173
|
return Effect.succeed({
|
|
184
174
|
inlineData: { mimeType: decoded.mediaType, data: Encoding.encodeBase64(decoded.data) },
|
|
185
175
|
});
|
|
186
176
|
}));
|
|
187
|
-
return Effect.fail(ImageInputs.invalid(
|
|
177
|
+
return Effect.fail(ImageInputs.invalid("Google generateContent requires Gemini file URIs rather than provider file IDs"));
|
|
188
178
|
};
|
|
189
179
|
export const GoogleImages = {
|
|
190
180
|
model,
|
|
@@ -28,15 +28,15 @@ const driver = (options, body) => {
|
|
|
28
28
|
return { message: body, mode: "full" };
|
|
29
29
|
}),
|
|
30
30
|
observe: (_create, frame) => Effect.gen(function* () {
|
|
31
|
-
const event = yield* decodeEvent(frame).pipe(Effect.mapError(() => ProviderShared.eventError(options.id, `Invalid ${options.name} WebSocket event`, frame)));
|
|
31
|
+
const event = yield* decodeEvent(frame).pipe(Effect.mapError((cause) => ProviderShared.eventError(options.id, `Invalid ${options.name} WebSocket event`, frame, cause)));
|
|
32
32
|
if (terminal)
|
|
33
33
|
return yield* ProviderShared.eventError(options.id, `${options.name} emitted ${event.type} after a terminal event`, frame);
|
|
34
34
|
if (event.type === "error") {
|
|
35
35
|
terminal = true;
|
|
36
|
-
yield* OpenResponses.decodeKnownErrorEvent(event).pipe(Effect.mapError(() => ProviderShared.eventError(options.id, `${options.name} returned a malformed error event`, frame)));
|
|
36
|
+
yield* OpenResponses.decodeKnownErrorEvent(event).pipe(Effect.mapError((cause) => ProviderShared.eventError(options.id, `${options.name} returned a malformed error event`, frame, cause)));
|
|
37
37
|
return {
|
|
38
38
|
type: "provider-failure",
|
|
39
|
-
error: OpenResponses.providerFailure(
|
|
39
|
+
error: OpenResponses.providerFailure(event, `${options.name} stream error`, frame),
|
|
40
40
|
};
|
|
41
41
|
}
|
|
42
42
|
if (event.type === "response.failed") {
|
|
@@ -45,7 +45,7 @@ const driver = (options, body) => {
|
|
|
45
45
|
return yield* ProviderShared.eventError(options.id, `${options.name} response ID changed during execution`, frame);
|
|
46
46
|
return {
|
|
47
47
|
type: "provider-failure",
|
|
48
|
-
error: OpenResponses.providerFailure(
|
|
48
|
+
error: OpenResponses.providerFailure(event, `${options.name} response failed`, frame),
|
|
49
49
|
};
|
|
50
50
|
}
|
|
51
51
|
if (event.type === "response.created") {
|
|
@@ -110,9 +110,10 @@ export const transport = (options) => {
|
|
|
110
110
|
channel,
|
|
111
111
|
};
|
|
112
112
|
}),
|
|
113
|
-
execute: (prepared, request, runtime, executeOptions) => {
|
|
113
|
+
execute: (prepared, request, runtime, executeOptions) => Effect.gen(function* () {
|
|
114
114
|
if (!executeOptions?.webSocket || !prepared.channel)
|
|
115
|
-
return http.execute(prepared.http, request, runtime);
|
|
115
|
+
return yield* http.execute(prepared.http, request, runtime);
|
|
116
|
+
let fallbackHttp;
|
|
116
117
|
const exchange = {
|
|
117
118
|
id: request.id ?? "request",
|
|
118
119
|
connect: {
|
|
@@ -120,11 +121,21 @@ export const transport = (options) => {
|
|
|
120
121
|
headers: prepared.channel.headers,
|
|
121
122
|
rotateAfterMs: prepared.channel.rotateAfterMs,
|
|
122
123
|
},
|
|
123
|
-
fallback: () => Stream.unwrap(http.execute(prepared.http, request, runtime).pipe(Effect.map((execution) =>
|
|
124
|
+
fallback: () => Stream.unwrap(http.execute(prepared.http, request, runtime).pipe(Effect.map((execution) => {
|
|
125
|
+
fallbackHttp = execution.http;
|
|
126
|
+
return execution.frames;
|
|
127
|
+
}))),
|
|
124
128
|
driver: prepared.channel.driver,
|
|
125
129
|
};
|
|
126
|
-
|
|
127
|
-
|
|
130
|
+
const execution = yield* executeOptions.webSocket.execute(exchange);
|
|
131
|
+
return {
|
|
132
|
+
frames: execution.frames,
|
|
133
|
+
complete: execution.complete,
|
|
134
|
+
get http() {
|
|
135
|
+
return fallbackHttp ?? execution.http;
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}),
|
|
128
139
|
};
|
|
129
140
|
};
|
|
130
141
|
export const OpenResponsesChannel = { transport };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AIError,
|
|
1
|
+
import { AIError, TransportError } from "../schema/index.js";
|
|
2
2
|
import { Effect, Option, Schema } from "effect";
|
|
3
3
|
import * as ProviderShared from "./shared.js";
|
|
4
4
|
import { OpenResponses } from "./open-responses.js";
|
|
@@ -81,14 +81,15 @@ const incremental = (request, checkpoint) => {
|
|
|
81
81
|
return input.slice(baseline.length);
|
|
82
82
|
};
|
|
83
83
|
const code = (event) => event.code || event.error?.code || event.response?.error?.code || undefined;
|
|
84
|
-
const rejected = (
|
|
84
|
+
const rejected = (observation, recovery) => ({
|
|
85
85
|
type: "rejected",
|
|
86
86
|
recovery,
|
|
87
87
|
error: new AIError({
|
|
88
|
-
|
|
89
|
-
method: "stream",
|
|
90
|
-
reason: new TransportReason({
|
|
88
|
+
reason: new TransportError({
|
|
91
89
|
message: observation.error.message,
|
|
90
|
+
body: observation.error.reason.body,
|
|
91
|
+
http: observation.error.reason.http,
|
|
92
|
+
cause: observation.error.reason.cause,
|
|
92
93
|
transport: "websocket",
|
|
93
94
|
operation: "read",
|
|
94
95
|
phase: "receive",
|
|
@@ -113,16 +114,16 @@ export const driver = (input) => {
|
|
|
113
114
|
};
|
|
114
115
|
}),
|
|
115
116
|
observe: (create, frame) => Effect.gen(function* () {
|
|
116
|
-
const event = yield* decodeEvent(frame).pipe(Effect.mapError(() => ProviderShared.eventError(input.id, `Invalid ${input.name} WebSocket event`, frame)));
|
|
117
|
+
const event = yield* decodeEvent(frame).pipe(Effect.mapError((cause) => ProviderShared.eventError(input.id, `Invalid ${input.name} WebSocket event`, frame, cause)));
|
|
117
118
|
const observation = yield* input.base.observe(create, frame);
|
|
118
119
|
if (event.type === "response.output_item.done" && event.item)
|
|
119
120
|
output.push(event.item);
|
|
120
121
|
if (observation.type === "provider-failure") {
|
|
121
122
|
const rejection = code(event);
|
|
122
123
|
if (rejection === "previous_response_not_found")
|
|
123
|
-
return rejected(
|
|
124
|
+
return rejected(observation, "retry-full");
|
|
124
125
|
if (rejection === "websocket_connection_limit_reached")
|
|
125
|
-
return rejected(
|
|
126
|
+
return rejected(observation, "rotate-and-retry-full");
|
|
126
127
|
}
|
|
127
128
|
if (observation.type !== "completed")
|
|
128
129
|
return observation;
|
|
@@ -804,7 +804,7 @@ export declare const terminal: (event: Event) => boolean;
|
|
|
804
804
|
export declare const outputItemID: (state: ParserState, event: Event) => string | undefined;
|
|
805
805
|
export declare const onReasoningDelta: (state: ParserState, event: Event, itemID: string) => StepResult;
|
|
806
806
|
export declare const onReasoningDone: (state: ParserState, event: Event, itemID: string) => StepResult;
|
|
807
|
-
export declare const providerFailure: (
|
|
807
|
+
export declare const providerFailure: (event: Event, fallback: string, body?: string) => AIError;
|
|
808
808
|
export declare const step: (state: ParserState, input: Event) => AIError | Effect.Effect<[ParserState, readonly ({
|
|
809
809
|
readonly type: "step-start";
|
|
810
810
|
readonly index: number;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Effect, Schema } from "effect";
|
|
2
2
|
import { HttpTransport } from "../route/transport/index.js";
|
|
3
3
|
import { Protocol } from "../route/protocol.js";
|
|
4
|
-
import { AIError, LLMEvent,
|
|
4
|
+
import { AIError, LLMEvent, ProviderInternalError, Usage, } from "../schema/index.js";
|
|
5
5
|
import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared.js";
|
|
6
6
|
import { classifyProviderFailure } from "../provider-error.js";
|
|
7
7
|
import { OpenResponsesOptions } from "./utils/open-responses-options.js";
|
|
@@ -918,11 +918,8 @@ const providerErrorMessage = (event, nested) => {
|
|
|
918
918
|
return `${code}: ${message}`;
|
|
919
919
|
return message || code;
|
|
920
920
|
};
|
|
921
|
-
export const providerFailure = (
|
|
921
|
+
export const providerFailure = (event, fallback, body = ProviderShared.encodeJson(event)) => {
|
|
922
922
|
const nested = event.error ?? event.response?.error ?? undefined;
|
|
923
|
-
const code = event.code || nested?.code || undefined;
|
|
924
|
-
// Keep the full raw payload on the error even when the message is a summary.
|
|
925
|
-
const body = JSON.stringify(nested ?? event) ?? "";
|
|
926
923
|
const summary = providerErrorMessage(event, nested);
|
|
927
924
|
const message = summary ?? (body === "{}" ? fallback : body);
|
|
928
925
|
const status = typeof event.status === "number"
|
|
@@ -935,16 +932,10 @@ export const providerFailure = (id, event, fallback) => {
|
|
|
935
932
|
event.response === undefined &&
|
|
936
933
|
summary === undefined &&
|
|
937
934
|
status === undefined
|
|
938
|
-
? new
|
|
939
|
-
: classifyProviderFailure({ message,
|
|
940
|
-
return new AIError({
|
|
941
|
-
module: id,
|
|
942
|
-
method: "stream",
|
|
943
|
-
body,
|
|
944
|
-
reason,
|
|
945
|
-
});
|
|
935
|
+
? new ProviderInternalError({ message, body })
|
|
936
|
+
: classifyProviderFailure({ message, status, rawBody: body });
|
|
937
|
+
return new AIError({ reason });
|
|
946
938
|
};
|
|
947
|
-
const providerError = (state, event, fallback) => providerFailure(state.id, event, fallback);
|
|
948
939
|
export const step = (state, input) => {
|
|
949
940
|
// The OpenAPI requires string IDs but imposes no minLength; empty is not missing.
|
|
950
941
|
const event = input.item_id !== undefined && outputItemID(state, input) !== input.item_id
|
|
@@ -1005,9 +996,9 @@ export const step = (state, input) => {
|
|
|
1005
996
|
if (event.type === "response.completed" || event.type === "response.incomplete")
|
|
1006
997
|
return onResponseFinish(state, event);
|
|
1007
998
|
if (event.type === "response.failed")
|
|
1008
|
-
return
|
|
999
|
+
return providerFailure(event, `${state.name} response failed`);
|
|
1009
1000
|
if (event.type === "error")
|
|
1010
|
-
return decodeKnownErrorEvent(event).pipe(Effect.mapError(() => ProviderShared.eventError(state.id, `${state.name} returned a malformed error event
|
|
1001
|
+
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`)));
|
|
1011
1002
|
return Effect.succeed([state, NO_EVENTS]);
|
|
1012
1003
|
};
|
|
1013
1004
|
// =============================================================================
|
|
@@ -5,7 +5,7 @@ import { Auth } from "../route/auth.js";
|
|
|
5
5
|
import { Endpoint } from "../route/endpoint.js";
|
|
6
6
|
import { HttpTransport } from "../route/transport/index.js";
|
|
7
7
|
import { Protocol } from "../route/protocol.js";
|
|
8
|
-
import { AIError,
|
|
8
|
+
import { AIError, AIErrorReason, InvalidProviderOutputError, LLMEvent, ProviderInternalError, UnknownProviderError, Usage, } from "../schema/index.js";
|
|
9
9
|
import { classifyProviderFailure } from "../provider-error.js";
|
|
10
10
|
import { isRecord, JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared.js";
|
|
11
11
|
import { OpenAIOptions } from "./utils/openai-options.js";
|
|
@@ -613,18 +613,22 @@ export const fromRequest = Effect.fn("OpenAIChat.fromRequest")(function* (reques
|
|
|
613
613
|
// Streaming parsers are small state machines: every event returns a new state
|
|
614
614
|
// plus the common `LLMEvent`s produced by that event. Tool calls are accumulated
|
|
615
615
|
// because OpenAI streams JSON arguments across multiple deltas.
|
|
616
|
-
const finishReasonError = (event, reason) => new AIError({
|
|
617
|
-
module: ADAPTER,
|
|
618
|
-
method: "stream",
|
|
619
|
-
body: ProviderShared.encodeJson(event),
|
|
620
|
-
reason,
|
|
621
|
-
});
|
|
622
616
|
const mapFinishReason = Effect.fn("OpenAIChat.mapFinishReason")(function* (event, reason) {
|
|
623
617
|
switch (reason) {
|
|
624
618
|
case "error":
|
|
625
|
-
return yield*
|
|
619
|
+
return yield* new AIError({
|
|
620
|
+
reason: new UnknownProviderError({
|
|
621
|
+
message: "Provider reported an error (finish_reason: error)",
|
|
622
|
+
body: ProviderShared.encodeJson(event),
|
|
623
|
+
}),
|
|
624
|
+
});
|
|
626
625
|
case "network_error":
|
|
627
|
-
return yield*
|
|
626
|
+
return yield* new AIError({
|
|
627
|
+
reason: new ProviderInternalError({
|
|
628
|
+
message: "Provider reported a network error (finish_reason: network_error)",
|
|
629
|
+
body: ProviderShared.encodeJson(event),
|
|
630
|
+
}),
|
|
631
|
+
});
|
|
628
632
|
case "stop":
|
|
629
633
|
case "end":
|
|
630
634
|
return "stop";
|
|
@@ -738,12 +742,8 @@ const step = (state, event) => Effect.gen(function* () {
|
|
|
738
742
|
if (event.error) {
|
|
739
743
|
const body = ProviderShared.encodeJson(event);
|
|
740
744
|
return yield* new AIError({
|
|
741
|
-
module: ADAPTER,
|
|
742
|
-
method: "stream",
|
|
743
|
-
body,
|
|
744
745
|
reason: classifyProviderFailure({
|
|
745
746
|
message: event.error.message,
|
|
746
|
-
code: event.error.code === undefined || event.error.code === null ? undefined : String(event.error.code),
|
|
747
747
|
status: typeof event.error.code === "number" ? event.error.code : undefined,
|
|
748
748
|
rawBody: body,
|
|
749
749
|
}),
|
|
@@ -831,7 +831,14 @@ const step = (state, event) => Effect.gen(function* () {
|
|
|
831
831
|
}
|
|
832
832
|
const result = ToolStream.appendOrStart(ADAPTER, tools, index, { id: id || undefined, name: name || undefined, text }, "OpenAI Chat tool call delta is missing id or name");
|
|
833
833
|
if (ToolStream.isError(result))
|
|
834
|
-
return yield*
|
|
834
|
+
return yield* new AIError({
|
|
835
|
+
reason: AIErrorReason.make({
|
|
836
|
+
...result.reason,
|
|
837
|
+
message: result.message,
|
|
838
|
+
cause: result.reason.cause,
|
|
839
|
+
body: ProviderShared.encodeJson(event),
|
|
840
|
+
}),
|
|
841
|
+
});
|
|
835
842
|
tools = result.tools;
|
|
836
843
|
if (result.events.length)
|
|
837
844
|
lifecycle = Lifecycle.stepStart(lifecycle, events);
|
|
@@ -867,11 +874,9 @@ const step = (state, event) => Effect.gen(function* () {
|
|
|
867
874
|
const finishEvents = Effect.fn("OpenAIChat.finishEvents")(function* (state) {
|
|
868
875
|
if (state.finishReason === undefined && state.requireFinishReason)
|
|
869
876
|
return yield* new AIError({
|
|
870
|
-
|
|
871
|
-
method: "stream",
|
|
872
|
-
reason: new InvalidProviderOutputReason({
|
|
873
|
-
classification: "incomplete-stream",
|
|
877
|
+
reason: new InvalidProviderOutputError({
|
|
874
878
|
message: "OpenAI Chat stream ended without finish_reason",
|
|
879
|
+
classification: "incomplete-stream",
|
|
875
880
|
route: ADAPTER,
|
|
876
881
|
}),
|
|
877
882
|
});
|
|
@@ -2,7 +2,7 @@ import { Effect, Encoding, Schema } from "effect";
|
|
|
2
2
|
import { Headers, HttpClientRequest, HttpClientResponse } from "effect/unstable/http";
|
|
3
3
|
import { ImageModel, GeneratedImage, ImageResponse, } from "../image.js";
|
|
4
4
|
import { Auth } from "../route/auth.js";
|
|
5
|
-
import {
|
|
5
|
+
import { Usage, mergeHttpOptions, mergeJsonRecords } from "../schema/index.js";
|
|
6
6
|
import { ProviderShared } from "./shared.js";
|
|
7
7
|
import { ImageInputs } from "./utils/image-input.js";
|
|
8
8
|
import { OpenAIImage } from "./utils/openai-image.js";
|
|
@@ -35,11 +35,6 @@ const nativeOptions = (options) => {
|
|
|
35
35
|
...native,
|
|
36
36
|
};
|
|
37
37
|
};
|
|
38
|
-
const invalidOutput = (message) => new AIError({
|
|
39
|
-
module: ADAPTER,
|
|
40
|
-
method: "generate",
|
|
41
|
-
reason: new InvalidProviderOutputReason({ message, route: ADAPTER }),
|
|
42
|
-
});
|
|
43
38
|
const applyQuery = (url, query) => {
|
|
44
39
|
if (!query)
|
|
45
40
|
return url;
|
|
@@ -53,14 +48,14 @@ export const model = (input) => {
|
|
|
53
48
|
generate: Effect.fn("OpenAIImages.generate")(function* (request, execute) {
|
|
54
49
|
const mask = request.options?.mask;
|
|
55
50
|
if (mask !== undefined && (request.images?.length ?? 0) === 0)
|
|
56
|
-
return yield* ImageInputs.invalid(
|
|
51
|
+
return yield* ImageInputs.invalid("An OpenAI image mask requires at least one input image");
|
|
57
52
|
const http = mergeHttpOptions(request.model.http, request.http);
|
|
58
53
|
const sourceImages = request.images ?? [];
|
|
59
54
|
const multipartImages = yield* Effect.forEach(sourceImages, (image) => {
|
|
60
55
|
if (image.type === "bytes")
|
|
61
56
|
return Effect.succeed({ data: image.data, mediaType: image.mediaType });
|
|
62
57
|
if (image.type === "url")
|
|
63
|
-
return ImageInputs.decodeDataUrl(image.url
|
|
58
|
+
return ImageInputs.decodeDataUrl(image.url);
|
|
64
59
|
return Effect.undefined;
|
|
65
60
|
});
|
|
66
61
|
const multipartMask = mask === undefined
|
|
@@ -68,7 +63,7 @@ export const model = (input) => {
|
|
|
68
63
|
: mask.type === "bytes"
|
|
69
64
|
? { data: mask.data, mediaType: mask.mediaType }
|
|
70
65
|
: mask.type === "url"
|
|
71
|
-
? yield* ImageInputs.decodeDataUrl(mask.url
|
|
66
|
+
? yield* ImageInputs.decodeDataUrl(mask.url)
|
|
72
67
|
: undefined;
|
|
73
68
|
const useMultipart = sourceImages.length > 0 &&
|
|
74
69
|
multipartImages.every((image) => image !== undefined) &&
|
|
@@ -111,7 +106,7 @@ export const model = (input) => {
|
|
|
111
106
|
return undefined;
|
|
112
107
|
});
|
|
113
108
|
if (references.some((image) => image === undefined))
|
|
114
|
-
return yield* ImageInputs.invalid(
|
|
109
|
+
return yield* ImageInputs.invalid("OpenAI Images accepts image URLs, data URLs, bytes, and file IDs");
|
|
115
110
|
const maskReference = mask === undefined
|
|
116
111
|
? undefined
|
|
117
112
|
: mask.type === "bytes"
|
|
@@ -122,7 +117,7 @@ export const model = (input) => {
|
|
|
122
117
|
? { file_id: mask.id }
|
|
123
118
|
: undefined;
|
|
124
119
|
if (mask !== undefined && maskReference === undefined)
|
|
125
|
-
return yield* ImageInputs.invalid(
|
|
120
|
+
return yield* ImageInputs.invalid("OpenAI Images accepts masks as URLs, data URLs, bytes, or file IDs");
|
|
126
121
|
const requestBody = mergeJsonRecords({
|
|
127
122
|
model: request.model.id,
|
|
128
123
|
prompt: request.prompt,
|
|
@@ -144,13 +139,13 @@ export const model = (input) => {
|
|
|
144
139
|
return ImageModel.make({ id: input.id, provider: "openai", route, http: input.http });
|
|
145
140
|
};
|
|
146
141
|
const parseResponse = Effect.fn("OpenAIImages.parseResponse")(function* (response, options, overlay) {
|
|
147
|
-
const
|
|
148
|
-
const decoded = yield* Schema.decodeUnknownEffect(OpenAIImageResponse)(
|
|
142
|
+
const output = yield* ProviderShared.imageResponse(ADAPTER, "OpenAI Images", response);
|
|
143
|
+
const decoded = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(OpenAIImageResponse))(output.body).pipe(Effect.mapError((cause) => output.invalid("OpenAI Images returned an invalid response", cause)));
|
|
149
144
|
const requestBody = mergeJsonRecords(nativeOptions(options), overlay);
|
|
150
145
|
const format = decoded.output_format ?? (typeof requestBody?.output_format === "string" ? requestBody.output_format : "png");
|
|
151
146
|
const images = yield* Effect.forEach(decoded.data, (item, index) => {
|
|
152
147
|
if (item.b64_json)
|
|
153
|
-
return Effect.fromResult(Encoding.decodeBase64(item.b64_json)).pipe(Effect.mapError(() =>
|
|
148
|
+
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({
|
|
154
149
|
mediaType: `image/${format}`,
|
|
155
150
|
data,
|
|
156
151
|
providerMetadata: item.revised_prompt === undefined ? undefined : { openai: { revisedPrompt: item.revised_prompt } },
|
|
@@ -161,10 +156,10 @@ const parseResponse = Effect.fn("OpenAIImages.parseResponse")(function* (respons
|
|
|
161
156
|
data: item.url,
|
|
162
157
|
providerMetadata: item.revised_prompt === undefined ? undefined : { openai: { revisedPrompt: item.revised_prompt } },
|
|
163
158
|
}));
|
|
164
|
-
return Effect.fail(
|
|
159
|
+
return Effect.fail(output.invalid(`OpenAI Images result ${index} has neither image data nor a URL`));
|
|
165
160
|
});
|
|
166
161
|
if (images.length === 0)
|
|
167
|
-
return yield*
|
|
162
|
+
return yield* output.invalid("OpenAI Images returned no images");
|
|
168
163
|
return new ImageResponse({
|
|
169
164
|
images,
|
|
170
165
|
usage: decoded.usage === undefined
|
|
@@ -115,7 +115,7 @@ const fromRequest = Effect.fn("OpenAIResponses.fromRequest")(function* (request)
|
|
|
115
115
|
const hostedToolResult = Effect.fn("OpenAIResponses.hostedToolResult")(function* (item) {
|
|
116
116
|
const isError = item.error !== undefined && item.error !== null;
|
|
117
117
|
if (item.type === "image_generation_call" && item.result) {
|
|
118
|
-
yield* Effect.fromResult(Encoding.decodeBase64(item.result)).pipe(Effect.mapError(() => ProviderShared.eventError(ADAPTER, "OpenAI Responses returned invalid image base64")));
|
|
118
|
+
yield* Effect.fromResult(Encoding.decodeBase64(item.result)).pipe(Effect.mapError((cause) => ProviderShared.eventError(ADAPTER, "OpenAI Responses returned invalid image base64", undefined, cause)));
|
|
119
119
|
const format = item.output_format ?? "png";
|
|
120
120
|
return {
|
|
121
121
|
type: "content",
|