@opencode-ai/ai 0.0.0-beta-18387 → 0.0.0-beta-18593
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/README.md +29 -11
- package/dist/image.js +5 -4
- package/dist/llm.d.ts +2 -0
- package/dist/llm.js +4 -7
- package/dist/protocols/anthropic-messages.js +7 -5
- package/dist/protocols/bedrock-converse.d.ts +1 -0
- package/dist/protocols/bedrock-converse.js +17 -7
- 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 +9 -3
- package/dist/protocols/open-responses.js +139 -78
- package/dist/protocols/openai-chat.d.ts +3 -1
- package/dist/protocols/openai-chat.js +39 -28
- package/dist/protocols/openai-compatible-chat.js +1 -2
- package/dist/protocols/openai-images.js +11 -16
- package/dist/protocols/openai-responses.js +1 -1
- package/dist/protocols/shared.d.ts +11 -7
- package/dist/protocols/shared.js +31 -18
- package/dist/protocols/utils/image-input.d.ts +4 -4
- package/dist/protocols/utils/image-input.js +6 -8
- package/dist/protocols/utils/lifecycle.d.ts +6 -2
- package/dist/protocols/utils/lifecycle.js +11 -7
- package/dist/protocols/utils/tool-stream.d.ts +6 -0
- 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 +39 -55
- package/dist/providers/groq.d.ts +1 -1
- package/dist/providers/groq.js +1 -2
- package/dist/providers/openrouter.d.ts +1 -1
- package/dist/providers/openrouter.js +1 -2
- package/dist/route/auth.js +3 -5
- package/dist/route/client.d.ts +2 -0
- 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 +6 -2
- package/dist/route/framing.js +5 -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 -89
- package/dist/schema/errors.js +35 -87
- package/dist/schema/events.d.ts +2835 -351
- package/dist/schema/events.js +32 -14
- package/dist/testing.d.ts +41 -2
- package/dist/testing.js +39 -18
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -214,22 +214,40 @@ the requests sent by code under test:
|
|
|
214
214
|
import { Effect } from "effect"
|
|
215
215
|
import { TestLLM } from "@opencode-ai/ai/testing"
|
|
216
216
|
|
|
217
|
-
const testLLM = TestLLM.layer({
|
|
218
|
-
fallback: TestLLM.text("Hello from the test model", "text-1"),
|
|
219
|
-
})
|
|
220
|
-
|
|
221
|
-
// TestLLM.clientLayer provides LLMClient.Service and consumes TestLLM.Service.
|
|
222
217
|
const programWithTestClient = Effect.gen(function* () {
|
|
218
|
+
const test = yield* TestLLM.Test
|
|
219
|
+
yield* test.push(TestLLM.text("Hello from the test model", "text-1"))
|
|
223
220
|
const result = yield* program
|
|
224
|
-
|
|
225
|
-
console.log(test.requests)
|
|
221
|
+
console.log(yield* test.requests())
|
|
226
222
|
return result
|
|
227
|
-
}).pipe(Effect.provide(TestLLM.
|
|
223
|
+
}).pipe(Effect.provide(TestLLM.testLayer()))
|
|
228
224
|
```
|
|
229
225
|
|
|
230
|
-
`
|
|
231
|
-
|
|
232
|
-
|
|
226
|
+
`testLayer()` provides the same object under `LLMClient.Service` and `TestLLM.Test`. Production consumes the
|
|
227
|
+
normal client; tests use the additional controls. Each layer build has fresh state.
|
|
228
|
+
|
|
229
|
+
- `test.push(...)` queues one-shot responses in execution order. Each argument is one response.
|
|
230
|
+
- `test.always(response)` installs a repeatable fallback. The layer's `fallback` option sets its initial value.
|
|
231
|
+
- `test.serve(request => response)` installs a request-dependent fallback. `always` and `serve` replace each
|
|
232
|
+
other without changing queued replies; queued replies take precedence.
|
|
233
|
+
- `test.requests()` returns an array snapshot. `transformRequest` changes only the recorded observation;
|
|
234
|
+
`serve` receives the original canonical request.
|
|
235
|
+
- `test.wait(count)` waits for request arrivals, not output or completion, and supports concurrent waiters.
|
|
236
|
+
- `test.gate()` returns a scoped gate with countable `started` notifications and a `release` Effect. Release
|
|
237
|
+
unblocks all requests captured by that gate; closing its scope also releases it. Effect-aware test runners
|
|
238
|
+
already provide Scope.
|
|
239
|
+
|
|
240
|
+
Constructing `stream()` or `generate()` does not record a request, invoke a responder, or consume a script.
|
|
241
|
+
Each execution does. An exhausted queue without a fallback defects immediately rather than waiting for a
|
|
242
|
+
future reply.
|
|
243
|
+
|
|
244
|
+
Responses remain canonical event arrays or arbitrary `Stream<LLMEvent, AIError>` values. The client consumes
|
|
245
|
+
supplied streams directly, preserving failure identity, finalizers, incomplete output, and post-finish tails;
|
|
246
|
+
it does not repair or truncate them.
|
|
247
|
+
|
|
248
|
+
The published legacy `Service`, `layer`, `clientLayer`, and module-level controls remain available as adapters
|
|
249
|
+
over the same implementation, including the legacy live `requests` array. New tests should use `Test` and
|
|
250
|
+
`testLayer`.
|
|
233
251
|
|
|
234
252
|
## Caching
|
|
235
253
|
|
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.d.ts
CHANGED
|
@@ -45,6 +45,7 @@ export declare class GenerateObjectResponse<T> {
|
|
|
45
45
|
} | {
|
|
46
46
|
readonly id: string;
|
|
47
47
|
readonly type: "text-end";
|
|
48
|
+
readonly text?: string | undefined;
|
|
48
49
|
readonly providerMetadata?: {
|
|
49
50
|
readonly [x: string]: {
|
|
50
51
|
readonly [x: string]: unknown;
|
|
@@ -70,6 +71,7 @@ export declare class GenerateObjectResponse<T> {
|
|
|
70
71
|
} | {
|
|
71
72
|
readonly id: string;
|
|
72
73
|
readonly type: "reasoning-end";
|
|
74
|
+
readonly text?: string | undefined;
|
|
73
75
|
readonly providerMetadata?: {
|
|
74
76
|
readonly [x: string]: {
|
|
75
77
|
readonly [x: string]: unknown;
|
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" ||
|
|
@@ -129,6 +129,7 @@ export type BedrockConverseBody = Schema.Schema.Type<typeof BedrockConverseBody>
|
|
|
129
129
|
interface ParserState {
|
|
130
130
|
readonly providerMetadataKey: string;
|
|
131
131
|
readonly tools: ToolStream.State<number>;
|
|
132
|
+
readonly finishedTools: ReadonlySet<number>;
|
|
132
133
|
readonly pendingFinish: {
|
|
133
134
|
readonly reason: FinishReasonDetails;
|
|
134
135
|
readonly usage?: Usage;
|
|
@@ -285,6 +285,13 @@ const lowerMessages = Effect.fn("BedrockConverse.lowerMessages")(function* (requ
|
|
|
285
285
|
content.push({ reasoningContent: { redactedContent: redactedData } });
|
|
286
286
|
continue;
|
|
287
287
|
}
|
|
288
|
+
if (signature === undefined || signature.trim().length === 0) {
|
|
289
|
+
// Interrupted streams and model switches can leave unsigned reasoning.
|
|
290
|
+
// Preserve readable history as text rather than replay invalid reasoningContent.
|
|
291
|
+
if (part.text.trim().length > 0)
|
|
292
|
+
content.push(...textWithCache(breakpoints, part.text, part.cache));
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
288
295
|
content.push({ reasoningContent: { reasoningText: { text: part.text, signature } } });
|
|
289
296
|
continue;
|
|
290
297
|
}
|
|
@@ -293,7 +300,8 @@ const lowerMessages = Effect.fn("BedrockConverse.lowerMessages")(function* (requ
|
|
|
293
300
|
continue;
|
|
294
301
|
}
|
|
295
302
|
}
|
|
296
|
-
|
|
303
|
+
if (content.length > 0)
|
|
304
|
+
messages.push({ role: "assistant", content });
|
|
297
305
|
continue;
|
|
298
306
|
}
|
|
299
307
|
const content = [];
|
|
@@ -447,6 +455,8 @@ const step = (state, event) => Effect.gen(function* () {
|
|
|
447
455
|
}
|
|
448
456
|
if (event.contentBlockDelta?.delta?.toolUse) {
|
|
449
457
|
const index = event.contentBlockDelta.contentBlockIndex;
|
|
458
|
+
if (state.finishedTools.has(index))
|
|
459
|
+
return [state, []];
|
|
450
460
|
const result = ToolStream.appendExisting(ADAPTER, state.tools, index, event.contentBlockDelta.delta.toolUse.input, "Bedrock Converse tool delta is missing its tool call");
|
|
451
461
|
if (ToolStream.isError(result))
|
|
452
462
|
return yield* result;
|
|
@@ -473,6 +483,7 @@ const step = (state, event) => Effect.gen(function* () {
|
|
|
473
483
|
state.hasToolCalls,
|
|
474
484
|
lifecycle,
|
|
475
485
|
tools: result.tools,
|
|
486
|
+
finishedTools: resultEvents.length > 0 ? new Set([...state.finishedTools, index]) : state.finishedTools,
|
|
476
487
|
reasoningSignatures: Object.fromEntries(Object.entries(state.reasoningSignatures).filter(([key]) => key !== String(index))),
|
|
477
488
|
},
|
|
478
489
|
events,
|
|
@@ -507,14 +518,12 @@ const step = (state, event) => Effect.gen(function* () {
|
|
|
507
518
|
];
|
|
508
519
|
}
|
|
509
520
|
if (event.exception) {
|
|
521
|
+
const message = event.exception.details.message ?? event.exception.details.originalMessage ?? "Bedrock Converse stream error";
|
|
522
|
+
const body = ProviderShared.encodeJson(event);
|
|
510
523
|
return yield* new AIError({
|
|
511
|
-
module: ADAPTER,
|
|
512
|
-
method: "stream",
|
|
513
524
|
reason: classifyProviderFailure({
|
|
514
|
-
message
|
|
515
|
-
|
|
516
|
-
"Bedrock Converse stream error",
|
|
517
|
-
code: event.exception.type,
|
|
525
|
+
message,
|
|
526
|
+
rawBody: body,
|
|
518
527
|
}),
|
|
519
528
|
});
|
|
520
529
|
}
|
|
@@ -554,6 +563,7 @@ export const protocol = Protocol.make({
|
|
|
554
563
|
initial: (request) => ({
|
|
555
564
|
providerMetadataKey: request.model.route.providerMetadataKey ?? String(request.model.provider),
|
|
556
565
|
tools: ToolStream.empty(),
|
|
566
|
+
finishedTools: new Set(),
|
|
557
567
|
pendingFinish: undefined,
|
|
558
568
|
hasToolCalls: false,
|
|
559
569
|
lifecycle: Lifecycle.initial(),
|
|
@@ -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;
|
|
@@ -565,15 +565,19 @@ export interface ParserState {
|
|
|
565
565
|
readonly name: string;
|
|
566
566
|
readonly providerMetadataKey: string;
|
|
567
567
|
readonly tools: ToolStream.State<string>;
|
|
568
|
+
readonly completedTools: ReadonlySet<string>;
|
|
568
569
|
readonly hasFunctionCall: boolean;
|
|
569
570
|
readonly lifecycle: Lifecycle.State;
|
|
570
571
|
readonly outputItems: Readonly<Record<number, string>>;
|
|
571
|
-
readonly
|
|
572
|
-
|
|
572
|
+
readonly message: {
|
|
573
|
+
readonly id: string;
|
|
574
|
+
readonly phase: MessagePhase | null | undefined;
|
|
575
|
+
} | undefined;
|
|
573
576
|
readonly reasoningItems: Readonly<Record<string, ReasoningStreamItem>>;
|
|
574
577
|
}
|
|
575
578
|
type ReasoningSummaryStatus = "active" | "can-conclude" | "concluded";
|
|
576
579
|
interface ReasoningStreamItem {
|
|
580
|
+
readonly open: boolean;
|
|
577
581
|
readonly encryptedContent: string | null | undefined;
|
|
578
582
|
readonly summaryParts: Readonly<Record<number, ReasoningSummaryStatus>>;
|
|
579
583
|
readonly deltaIndexes: ReadonlySet<number>;
|
|
@@ -804,7 +808,7 @@ export declare const terminal: (event: Event) => boolean;
|
|
|
804
808
|
export declare const outputItemID: (state: ParserState, event: Event) => string | undefined;
|
|
805
809
|
export declare const onReasoningDelta: (state: ParserState, event: Event, itemID: string) => StepResult;
|
|
806
810
|
export declare const onReasoningDone: (state: ParserState, event: Event, itemID: string) => StepResult;
|
|
807
|
-
export declare const providerFailure: (
|
|
811
|
+
export declare const providerFailure: (event: Event, fallback: string, body?: string) => AIError;
|
|
808
812
|
export declare const step: (state: ParserState, input: Event) => AIError | Effect.Effect<[ParserState, readonly ({
|
|
809
813
|
readonly type: "step-start";
|
|
810
814
|
readonly index: number;
|
|
@@ -828,6 +832,7 @@ export declare const step: (state: ParserState, input: Event) => AIError | Effec
|
|
|
828
832
|
} | {
|
|
829
833
|
readonly id: string;
|
|
830
834
|
readonly type: "text-end";
|
|
835
|
+
readonly text?: string | undefined;
|
|
831
836
|
readonly providerMetadata?: {
|
|
832
837
|
readonly [x: string]: {
|
|
833
838
|
readonly [x: string]: unknown;
|
|
@@ -853,6 +858,7 @@ export declare const step: (state: ParserState, input: Event) => AIError | Effec
|
|
|
853
858
|
} | {
|
|
854
859
|
readonly id: string;
|
|
855
860
|
readonly type: "reasoning-end";
|
|
861
|
+
readonly text?: string | undefined;
|
|
856
862
|
readonly providerMetadata?: {
|
|
857
863
|
readonly [x: string]: {
|
|
858
864
|
readonly [x: string]: unknown;
|