@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.
Files changed (54) hide show
  1. package/README.md +29 -11
  2. package/dist/image.js +5 -4
  3. package/dist/llm.d.ts +2 -0
  4. package/dist/llm.js +4 -7
  5. package/dist/protocols/anthropic-messages.js +7 -5
  6. package/dist/protocols/bedrock-converse.d.ts +1 -0
  7. package/dist/protocols/bedrock-converse.js +17 -7
  8. package/dist/protocols/bedrock-event-stream.js +16 -6
  9. package/dist/protocols/gemini.d.ts +1 -0
  10. package/dist/protocols/gemini.js +13 -1
  11. package/dist/protocols/google-images.js +8 -18
  12. package/dist/protocols/open-responses-channel.js +20 -9
  13. package/dist/protocols/open-responses-continuation.js +9 -8
  14. package/dist/protocols/open-responses.d.ts +9 -3
  15. package/dist/protocols/open-responses.js +139 -78
  16. package/dist/protocols/openai-chat.d.ts +3 -1
  17. package/dist/protocols/openai-chat.js +39 -28
  18. package/dist/protocols/openai-compatible-chat.js +1 -2
  19. package/dist/protocols/openai-images.js +11 -16
  20. package/dist/protocols/openai-responses.js +1 -1
  21. package/dist/protocols/shared.d.ts +11 -7
  22. package/dist/protocols/shared.js +31 -18
  23. package/dist/protocols/utils/image-input.d.ts +4 -4
  24. package/dist/protocols/utils/image-input.js +6 -8
  25. package/dist/protocols/utils/lifecycle.d.ts +6 -2
  26. package/dist/protocols/utils/lifecycle.js +11 -7
  27. package/dist/protocols/utils/tool-stream.d.ts +6 -0
  28. package/dist/protocols/xai-images.js +7 -12
  29. package/dist/protocols/zai-images.js +5 -10
  30. package/dist/provider-error.d.ts +4 -4
  31. package/dist/provider-error.js +39 -55
  32. package/dist/providers/groq.d.ts +1 -1
  33. package/dist/providers/groq.js +1 -2
  34. package/dist/providers/openrouter.d.ts +1 -1
  35. package/dist/providers/openrouter.js +1 -2
  36. package/dist/route/auth.js +3 -5
  37. package/dist/route/client.d.ts +2 -0
  38. package/dist/route/client.js +29 -11
  39. package/dist/route/executor.d.ts +9 -5
  40. package/dist/route/executor.js +33 -66
  41. package/dist/route/framing.d.ts +6 -2
  42. package/dist/route/framing.js +5 -0
  43. package/dist/route/transport/http.js +7 -2
  44. package/dist/route/transport/index.d.ts +3 -1
  45. package/dist/route/transport/websocket-channel.d.ts +2 -1
  46. package/dist/route/transport/websocket.d.ts +2 -1
  47. package/dist/route/transport/websocket.js +70 -30
  48. package/dist/schema/errors.d.ts +71 -89
  49. package/dist/schema/errors.js +35 -87
  50. package/dist/schema/events.d.ts +2835 -351
  51. package/dist/schema/events.js +32 -14
  52. package/dist/testing.d.ts +41 -2
  53. package/dist/testing.js +39 -18
  54. 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
- const test = yield* TestLLM.Service
225
- console.log(test.requests)
221
+ console.log(yield* test.requests())
226
222
  return result
227
- }).pipe(Effect.provide(TestLLM.clientLayer), Effect.provide(testLLM))
223
+ }).pipe(Effect.provide(TestLLM.testLayer()))
228
224
  ```
229
225
 
230
- `TestLLM.push(...)` scripts one-shot responses, `TestLLM.always(...)` changes the fallback, and
231
- `TestLLM.wait(...)` lets concurrent tests wait until a request has arrived. Every received canonical request is
232
- available on the yielded `TestLLM.Service`.
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, InvalidRequestReason, AIError, ModelID, ProviderID, ProviderMetadata, Usage, } from "./schema/index.js";
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
- module: "Image",
94
- method: "generate",
95
- reason: new InvalidRequestReason({ message: error instanceof Error ? error.message : String(error) }),
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, InvalidProviderOutputReason, AIError, LLMEvent, LLMRequest, LLMResponse, Message, LanguageModel, SystemPart, ToolChoice, ToolDefinition, } from "./schema/index.js";
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
- module: "LLM",
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
- module: "LLM",
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) => Effect.fail(new AIError({
1171
- module: ADAPTER,
1172
- method: "stream",
1173
- reason: classifyProviderFailure({ message: providerErrorMessage(event), code: event.error?.type }),
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
- messages.push({ role: "assistant", content });
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: event.exception.details.message ??
515
- event.exception.details.originalMessage ??
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(messageType === "exception" ? { exception: { type: eventType, details: parsed } } : { [eventType]: parsed });
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;
@@ -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 { InvalidProviderOutputReason, AIError, Usage, mergeHttpOptions, mergeJsonRecords, } from "../schema/index.js";
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 payload = yield* response.json.pipe(Effect.mapError(() => invalidOutput("Failed to read the Google Images response")));
94
- const decoded = yield* Schema.decodeUnknownEffect(GoogleImageResponse)(payload).pipe(Effect.mapError(() => invalidOutput("Google Images returned an invalid response")));
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(() => invalidOutput(`Google Images candidate ${item.candidateIndex} part ${item.partIndex} contains invalid base64 data`)), Effect.map((data) => new GeneratedImage({
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* invalidOutput(`Google Images returned no final images${finishReasons.length === 0 ? "" : ` (finish reasons: ${finishReasons.join(", ")})`}; inspect reason.providerMetadata.google for prompt feedback and candidate details`, {
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, ADAPTER).pipe(Effect.flatMap((decoded) => {
170
+ return ImageInputs.decodeDataUrl(image.url).pipe(Effect.flatMap((decoded) => {
181
171
  if (decoded === undefined)
182
- return Effect.fail(ImageInputs.invalid(ADAPTER, "Google generateContent does not fetch public image URLs; use bytes, a data URL, or a Gemini file URI"));
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(ADAPTER, "Google generateContent requires Gemini file URIs rather than provider file IDs"));
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(options.id, event, `${options.name} stream error`),
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(options.id, event, `${options.name} response failed`),
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) => execution.frames))),
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
- return executeOptions.webSocket.execute(exchange);
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, TransportReason } from "../schema/index.js";
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 = (input, observation, recovery) => ({
84
+ const rejected = (observation, recovery) => ({
85
85
  type: "rejected",
86
86
  recovery,
87
87
  error: new AIError({
88
- module: input.id,
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(input, observation, "retry-full");
124
+ return rejected(observation, "retry-full");
124
125
  if (rejection === "websocket_connection_limit_reached")
125
- return rejected(input, observation, "rotate-and-retry-full");
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 messageItems: ReadonlySet<string>;
572
- readonly messagePhases: Readonly<Record<string, MessagePhase | null>>;
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: (id: string, event: Event, fallback: string) => AIError;
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;