@opencode-ai/ai 0.0.0-beta-17793 → 0.0.0-beta-17887

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.
@@ -26,6 +26,9 @@ const requiresThoughtSignatureFallback = (modelID) => {
26
26
  return false;
27
27
  return !/(^|\/)gemini-robotics-er-1\.5(?:[.-]|$)/i.test(modelID);
28
28
  };
29
+ // Gemini 3 accepts media nested inside function responses; matched Gemini 2.5 variants reject it,
30
+ // so their tool-result attachments lower as a separate user turn instead.
31
+ const routesLegacyToolMedia = (modelID) => /gemini-2[.-]5(?:[.-]|$)/i.test(modelID);
29
32
  // =============================================================================
30
33
  // Request Body Schema
31
34
  // =============================================================================
@@ -197,7 +200,17 @@ const lowerToolCall = (part) => ({
197
200
  });
198
201
  const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request) {
199
202
  const contents = [];
203
+ const legacyToolMedia = routesLegacyToolMedia(request.model.id);
204
+ let pendingMedia;
205
+ const flushMedia = () => {
206
+ if (!pendingMedia)
207
+ return;
208
+ contents.push({ role: "user", parts: [{ text: "Attached media from tool result:" }, ...pendingMedia] });
209
+ pendingMedia = undefined;
210
+ };
200
211
  for (const message of request.messages) {
212
+ if (message.role !== "tool")
213
+ flushMedia();
201
214
  if (message.role === "system") {
202
215
  const part = yield* ProviderShared.wrappedSystemUpdate("Gemini", message);
203
216
  const previous = contents.at(-1);
@@ -227,7 +240,7 @@ const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request) {
227
240
  if (!ProviderShared.supportsContent(part, ["text", "reasoning", "tool-call"]))
228
241
  return yield* ProviderShared.unsupportedContent("Gemini", "assistant", ["text", "reasoning", "tool-call"]);
229
242
  if (part.type === "text") {
230
- parts.push({ text: part.text });
243
+ parts.push({ text: part.text, thoughtSignature: thoughtSignature(part.providerMetadata) });
231
244
  continue;
232
245
  }
233
246
  if (part.type === "reasoning") {
@@ -278,6 +291,8 @@ const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request) {
278
291
  const value = ProviderShared.normalizeToolFile(item);
279
292
  media.push({ inlineData: { mimeType: value.mime, data: value.base64 } });
280
293
  }
294
+ if (legacyToolMedia && media.length > 0)
295
+ (pendingMedia ??= []).push(...media);
281
296
  parts.push({
282
297
  functionResponse: {
283
298
  id: functionCallId(part.providerMetadata),
@@ -286,7 +301,7 @@ const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request) {
286
301
  name: part.name,
287
302
  content: text.join("\n"),
288
303
  },
289
- parts: media.length > 0 ? media : undefined,
304
+ parts: legacyToolMedia || media.length === 0 ? undefined : media,
290
305
  },
291
306
  });
292
307
  }
@@ -298,6 +313,7 @@ const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request) {
298
313
  else
299
314
  contents.push({ role: "user", parts });
300
315
  }
316
+ flushMedia();
301
317
  return contents;
302
318
  });
303
319
  const resolveOptions = (request) => {
@@ -422,9 +438,11 @@ const finish = (state) => {
422
438
  if (finishReason === undefined && state.usage === undefined)
423
439
  return [];
424
440
  const events = [];
425
- const lifecycle = state.reasoningSignature
426
- ? Lifecycle.reasoningEnd(state.lifecycle, events, "reasoning-0", googleMetadata({ thoughtSignature: state.reasoningSignature }))
427
- : state.lifecycle;
441
+ let lifecycle = state.lifecycle;
442
+ if (state.reasoningSignature !== undefined)
443
+ lifecycle = Lifecycle.reasoningEnd(lifecycle, events, "reasoning-0", googleMetadata({ thoughtSignature: state.reasoningSignature }));
444
+ if (state.textSignature !== undefined)
445
+ lifecycle = Lifecycle.textEnd(lifecycle, events, "text-0", googleMetadata({ thoughtSignature: state.textSignature }));
428
446
  Lifecycle.finish(lifecycle, events, {
429
447
  reason: {
430
448
  normalized: promptBlockReason === undefined ? mapFinishReason(finishReason, state.hasToolCalls) : "content-filter",
@@ -452,16 +470,23 @@ const step = (state, event) => {
452
470
  let lifecycle = nextState.lifecycle;
453
471
  let nextToolCallId = nextState.nextToolCallId;
454
472
  let reasoningSignature = nextState.reasoningSignature;
473
+ let textSignature = nextState.textSignature;
455
474
  for (const part of candidate.content.parts) {
456
- if ("thoughtSignature" in part && part.thoughtSignature && "thought" in part && part.thought)
457
- reasoningSignature = part.thoughtSignature;
475
+ const signature = "thoughtSignature" in part && part.thoughtSignature ? part.thoughtSignature : undefined;
476
+ // Gemini attaches replay signatures to thought parts, visible text, or function calls;
477
+ // each block kind must retain the signature attached to its own parts.
478
+ if (signature !== undefined && "thought" in part && part.thought)
479
+ reasoningSignature = signature;
480
+ else if (signature !== undefined && "text" in part)
481
+ textSignature = signature;
458
482
  if ("text" in part && part.text.length > 0) {
459
483
  if (part.thought) {
460
- lifecycle = Lifecycle.reasoningDelta(lifecycle, events, "reasoning-0", part.text, part.thoughtSignature ? googleMetadata({ thoughtSignature: part.thoughtSignature }) : undefined);
484
+ lifecycle = Lifecycle.reasoningDelta(lifecycle, events, "reasoning-0", part.text, signature ? googleMetadata({ thoughtSignature: signature }) : undefined);
461
485
  continue;
462
486
  }
463
487
  lifecycle = Lifecycle.reasoningEnd(lifecycle, events, "reasoning-0", reasoningSignature ? googleMetadata({ thoughtSignature: reasoningSignature }) : undefined);
464
- lifecycle = Lifecycle.textDelta(lifecycle, events, "text-0", part.text);
488
+ lifecycle = Lifecycle.textDelta(lifecycle, events, "text-0", part.text, textSignature ? googleMetadata({ thoughtSignature: textSignature }) : undefined);
489
+ textSignature = undefined;
465
490
  continue;
466
491
  }
467
492
  if ("functionCall" in part) {
@@ -489,6 +514,7 @@ const step = (state, event) => {
489
514
  lifecycle,
490
515
  nextToolCallId,
491
516
  reasoningSignature,
517
+ textSignature,
492
518
  finishReason: candidate.finishReason ?? nextState.finishReason,
493
519
  },
494
520
  events,
@@ -4,6 +4,8 @@ export interface Options {
4
4
  readonly id: string;
5
5
  readonly name: string;
6
6
  readonly rotateAfterMs?: number;
7
+ readonly enabled?: (url: string) => boolean;
8
+ readonly url?: (url: string) => string;
7
9
  readonly headers?: (headers: Headers.Headers) => Headers.Headers;
8
10
  readonly driver?: (input: {
9
11
  readonly request: Readonly<Record<string, unknown>>;
@@ -79,12 +79,12 @@ export const transport = (options) => {
79
79
  prepare: (input) => Effect.gen(function* () {
80
80
  const parts = yield* HttpTransport.jsonRequestParts(input);
81
81
  const headers = Headers.remove(options.headers?.(parts.headers) ?? parts.headers, "content-length");
82
- const channel = input.webSocket
82
+ const channel = input.webSocket && (options.enabled?.(parts.url) ?? true)
83
83
  ? yield* Effect.gen(function* () {
84
84
  const create = yield* message(parts.jsonBody);
85
85
  const base = driver(options, create.message);
86
86
  return {
87
- url: yield* WebSocketTransport.toWebSocketUrl(parts.url),
87
+ url: yield* WebSocketTransport.toWebSocketUrl(options.url?.(parts.url) ?? parts.url),
88
88
  headers,
89
89
  rotateAfterMs: options.rotateAfterMs,
90
90
  driver: options.driver?.({ request: create.request, message: create.message, base }) ?? base,
@@ -61,7 +61,7 @@ export const model = (input) => {
61
61
  return Effect.succeed({ data: image.data, mediaType: image.mediaType });
62
62
  if (image.type === "url")
63
63
  return ImageInputs.decodeDataUrl(image.url, ADAPTER);
64
- return Effect.succeed(undefined);
64
+ return Effect.undefined;
65
65
  });
66
66
  const multipartMask = mask === undefined
67
67
  ? undefined
@@ -3,6 +3,7 @@ import { Route } from "../route/client.js";
3
3
  import { Protocol } from "../route/protocol.js";
4
4
  import { HttpTransport } from "../route/transport/index.js";
5
5
  import { OpenResponses } from "./open-responses.js";
6
+ import { type Options } from "./open-responses-channel.js";
6
7
  export declare const DEFAULT_BASE_URL = "https://api.openai.com/v1";
7
8
  export declare const PATH = "/responses";
8
9
  declare const OpenAIResponsesBody: Schema.Struct<{
@@ -461,6 +462,141 @@ export declare const httpTransport: HttpTransport.HttpJsonTransport<{
461
462
  readonly max_tool_calls?: number | undefined;
462
463
  readonly parallel_tool_calls?: boolean | undefined;
463
464
  }, string>;
465
+ export declare const channelTransport: (options: Omit<Options, "driver">) => import("../route/transport/index.js").Transport<{
466
+ readonly input: readonly ({
467
+ readonly type: "reasoning";
468
+ readonly summary: readonly {
469
+ readonly type: "summary_text";
470
+ readonly text: string;
471
+ }[];
472
+ readonly id?: string | undefined;
473
+ readonly encrypted_content?: string | null | undefined;
474
+ } | {
475
+ readonly type: "item_reference";
476
+ readonly id: string;
477
+ } | {
478
+ readonly role: "system";
479
+ readonly content: string;
480
+ } | {
481
+ readonly role: "developer";
482
+ readonly content: string;
483
+ } | {
484
+ readonly role: "user";
485
+ readonly content: readonly ({
486
+ readonly type: "input_image";
487
+ readonly image_url: string;
488
+ } | {
489
+ readonly type: "input_file";
490
+ readonly filename: string;
491
+ readonly file_data: string;
492
+ readonly mime_type?: string | undefined;
493
+ } | {
494
+ readonly type: "input_text";
495
+ readonly text: string;
496
+ })[];
497
+ } | {
498
+ readonly type: "message";
499
+ readonly content: readonly {
500
+ readonly type: "output_text";
501
+ readonly text: string;
502
+ }[];
503
+ readonly role: "assistant";
504
+ readonly id?: string | undefined;
505
+ readonly phase?: "commentary" | "final_answer" | undefined;
506
+ } | {
507
+ readonly type: "function_call";
508
+ readonly name: string;
509
+ readonly arguments: string;
510
+ readonly call_id: string;
511
+ readonly id?: string | undefined;
512
+ } | {
513
+ readonly type: "function_call_output";
514
+ readonly call_id: string;
515
+ readonly output: string | readonly ({
516
+ readonly type: "input_image";
517
+ readonly image_url: string;
518
+ } | {
519
+ readonly type: "input_file";
520
+ readonly filename: string;
521
+ readonly file_data: string;
522
+ readonly mime_type?: string | undefined;
523
+ } | {
524
+ readonly type: "input_text";
525
+ readonly text: string;
526
+ })[];
527
+ } | {
528
+ readonly type: "message";
529
+ readonly content: readonly {
530
+ readonly type: "output_text";
531
+ readonly text: string;
532
+ }[];
533
+ readonly role: "assistant";
534
+ readonly id?: string | undefined;
535
+ readonly phase?: "commentary" | "final_answer" | null | undefined;
536
+ })[];
537
+ readonly model: string;
538
+ readonly stream: true;
539
+ readonly metadata?: {
540
+ readonly [x: string]: string;
541
+ } | undefined;
542
+ readonly instructions?: string | undefined;
543
+ readonly reasoning?: {
544
+ readonly summary?: "auto" | "concise" | "detailed" | undefined;
545
+ readonly effort?: import("./utils/open-responses-options.js").ReasoningEffort | undefined;
546
+ } | undefined;
547
+ readonly tools?: readonly ({
548
+ readonly type: "function";
549
+ readonly description: string;
550
+ readonly name: string;
551
+ readonly parameters: {
552
+ readonly [x: string]: unknown;
553
+ };
554
+ readonly strict?: boolean | undefined;
555
+ } | {
556
+ readonly type: "image_generation";
557
+ readonly size?: string | undefined;
558
+ readonly action?: "generate" | "auto" | "edit" | undefined;
559
+ readonly output_format?: "png" | "jpeg" | "webp" | undefined;
560
+ readonly quality?: "auto" | "low" | "medium" | "high" | undefined;
561
+ readonly background?: "auto" | "opaque" | "transparent" | undefined;
562
+ readonly output_compression?: number | undefined;
563
+ readonly input_fidelity?: "low" | "high" | undefined;
564
+ readonly partial_images?: number | undefined;
565
+ })[] | undefined;
566
+ readonly text?: {
567
+ readonly verbosity?: import("./utils/open-responses-options.js").TextVerbosity | undefined;
568
+ } | undefined;
569
+ readonly temperature?: number | undefined;
570
+ readonly tool_choice?: "required" | "auto" | "none" | {
571
+ readonly type: "function";
572
+ readonly name: string;
573
+ } | {
574
+ readonly type: "allowed_tools";
575
+ readonly mode: "required" | "auto" | "none";
576
+ readonly tools: readonly {
577
+ readonly type: "function";
578
+ readonly name: string;
579
+ }[];
580
+ } | {
581
+ readonly type: "image_generation";
582
+ } | undefined;
583
+ readonly top_p?: number | undefined;
584
+ readonly store?: boolean | undefined;
585
+ readonly include?: readonly import("./utils/open-responses-options.js").ResponseIncludable[] | undefined;
586
+ readonly truncation?: "auto" | "disabled" | undefined;
587
+ readonly stream_options?: {
588
+ readonly include_obfuscation?: boolean | undefined;
589
+ } | undefined;
590
+ readonly prompt_cache_key?: string | undefined;
591
+ readonly frequency_penalty?: number | undefined;
592
+ readonly presence_penalty?: number | undefined;
593
+ readonly safety_identifier?: string | undefined;
594
+ readonly top_logprobs?: number | undefined;
595
+ readonly service_tier?: "default" | "auto" | "flex" | "priority" | undefined;
596
+ readonly max_output_tokens?: number | undefined;
597
+ readonly max_tool_calls?: number | undefined;
598
+ readonly parallel_tool_calls?: boolean | undefined;
599
+ }, import("./open-responses-channel.js").Prepared, string>;
464
600
  export declare const transport: import("../route/transport/index.js").Transport<{
465
601
  readonly input: readonly ({
466
602
  readonly type: "reasoning";
@@ -190,12 +190,15 @@ export const protocol = Protocol.make({
190
190
  const endpoint = Endpoint.path(PATH, { baseURL: DEFAULT_BASE_URL });
191
191
  const auth = Auth.none;
192
192
  export const httpTransport = HttpTransport.sseJson.with();
193
- export const transport = OpenResponsesChannel.transport({
193
+ export const channelTransport = (options) => OpenResponsesChannel.transport({
194
+ ...options,
195
+ driver: (input) => OpenAIResponsesChannel.driver({ id: options.id, name: options.name, ...input }),
196
+ });
197
+ export const transport = channelTransport({
194
198
  id: ADAPTER,
195
199
  name: NAME,
196
200
  rotateAfterMs: WEBSOCKET_ROTATE_AFTER_MS,
197
201
  headers: (headers) => Headers.set(headers, "openai-beta", headers["openai-beta"] ?? WEBSOCKET_PROTOCOL_HEADER),
198
- driver: (input) => OpenAIResponsesChannel.driver({ id: ADAPTER, name: NAME, ...input }),
199
202
  });
200
203
  export const route = Route.make({
201
204
  id: ADAPTER,
@@ -8,7 +8,7 @@ const invalid = (module, message) => new AIError({
8
8
  export const dataUrl = (input) => `data:${input.mediaType};base64,${Encoding.encodeBase64(input.data)}`;
9
9
  export const decodeDataUrl = (url, module) => {
10
10
  if (!url.startsWith("data:"))
11
- return Effect.succeed(undefined);
11
+ return Effect.undefined;
12
12
  const match = /^data:([^;,]+);base64,(.*)$/s.exec(url);
13
13
  if (!match)
14
14
  return Effect.fail(invalid(module, "Image data URLs must contain a MIME type and base64 data"));
@@ -7,7 +7,7 @@ export interface State {
7
7
  export declare const initial: () => State;
8
8
  export declare const stepStart: (state: State, events: LLMEvent[]) => State;
9
9
  export declare const textStart: (state: State, events: LLMEvent[], id: string, providerMetadata?: ProviderMetadata) => State;
10
- export declare const textDelta: (state: State, events: LLMEvent[], id: string, text: string) => State;
10
+ export declare const textDelta: (state: State, events: LLMEvent[], id: string, text: string, providerMetadata?: ProviderMetadata) => State;
11
11
  export declare const reasoningStart: (state: State, events: LLMEvent[], id: string, providerMetadata?: ProviderMetadata) => State;
12
12
  export declare const reasoningDelta: (state: State, events: LLMEvent[], id: string, text: string, providerMetadata?: ProviderMetadata) => State;
13
13
  export declare const reasoningEnd: (state: State, events: LLMEvent[], id: string, providerMetadata?: ProviderMetadata) => State;
@@ -13,9 +13,9 @@ export const textStart = (state, events, id, providerMetadata) => {
13
13
  events.push(LLMEvent.textStart({ id, providerMetadata }));
14
14
  return { ...stepped, text: new Set([...stepped.text, id]) };
15
15
  };
16
- export const textDelta = (state, events, id, text) => {
16
+ export const textDelta = (state, events, id, text, providerMetadata) => {
17
17
  const started = textStart(state, events, id);
18
- events.push(LLMEvent.textDelta({ id, text }));
18
+ events.push(LLMEvent.textDelta({ id, text, providerMetadata }));
19
19
  return started;
20
20
  };
21
21
  export const reasoningStart = (state, events, id, providerMetadata) => {
@@ -1,3 +1,4 @@
1
+ import { Headers } from "effect/unstable/http";
1
2
  import { Auth } from "../route/auth.js";
2
3
  import {} from "../route/auth-options.js";
3
4
  import { ProviderID } from "../schema/index.js";
@@ -7,11 +8,35 @@ import { ProviderShared } from "../protocols/shared.js";
7
8
  import { withOpenAIOptions } from "./openai-options.js";
8
9
  export const id = ProviderID.make("azure");
9
10
  const routeAuth = Auth.remove("authorization");
11
+ const RESPONSES_WEBSOCKET_ROTATE_AFTER_MS = 55 * 60 * 1000;
10
12
  const resourceBaseURL = (resourceName) => `https://${resourceName.trim()}.openai.azure.com/openai`;
11
13
  const responsesRoute = OpenAIResponses.route.with({
12
14
  id: "azure-openai-responses",
13
15
  provider: id,
14
16
  auth: routeAuth,
17
+ transport: OpenAIResponses.channelTransport({
18
+ id: "azure-openai-responses",
19
+ name: "Azure OpenAI Responses",
20
+ rotateAfterMs: RESPONSES_WEBSOCKET_ROTATE_AFTER_MS,
21
+ enabled: (value) => {
22
+ const url = new URL(value);
23
+ return (url.protocol === "https:" &&
24
+ url.hostname.endsWith(".openai.azure.com") &&
25
+ url.pathname.endsWith("/openai/v1/responses") &&
26
+ url.searchParams.get("api-version") === "v1");
27
+ },
28
+ url: (value) => {
29
+ const url = new URL(value);
30
+ url.searchParams.delete("api-version");
31
+ return url.toString();
32
+ },
33
+ headers: (headers) => {
34
+ const apiKey = headers["api-key"];
35
+ if (!apiKey)
36
+ return headers;
37
+ return Headers.remove(Headers.set(headers, "authorization", `Bearer ${apiKey}`), "api-key");
38
+ },
39
+ }),
15
40
  });
16
41
  const chatRoute = OpenAIChat.route.with({
17
42
  id: "azure-openai-chat",
@@ -245,7 +245,7 @@ export declare const routes: (Route<{
245
245
  readonly max_output_tokens?: number | undefined;
246
246
  readonly max_tool_calls?: number | undefined;
247
247
  readonly parallel_tool_calls?: boolean | undefined;
248
- }, import("../route/transport/http.js").HttpPrepared<string>>)[];
248
+ }, import("../protocols/open-responses-channel.js").Prepared>)[];
249
249
  export declare const configure: (input?: LanguageModelOptions) => {
250
250
  id: string & import("effect/Brand").Brand<"AI.ProviderID">;
251
251
  model: (modelID: string | ModelID) => import("../schema/options.js").LanguageModel<import("./open-responses-options.js").OpenResponsesOptionsInput>;
@@ -8,13 +8,18 @@ import * as OpenAIChat from "../protocols/openai-chat.js";
8
8
  import * as OpenAIResponses from "../protocols/openai-responses.js";
9
9
  import { XAIImages } from "../protocols/xai-images.js";
10
10
  export const id = ProviderID.make("xai");
11
+ const RESPONSES_WEBSOCKET_ROTATE_AFTER_MS = 24 * 60 * 1000;
11
12
  const responsesRoute = Route.make({
12
13
  id: "openai-responses",
13
14
  provider: id,
14
15
  providerMetadataKey: "xai",
15
16
  protocol: OpenAIResponses.protocol,
16
17
  endpoint: Endpoint.path("/responses", { baseURL: OpenAICompatibleProfiles.profiles.xai.baseURL }),
17
- transport: OpenAIResponses.httpTransport,
18
+ transport: OpenAIResponses.channelTransport({
19
+ id: "openai-responses",
20
+ name: "xAI Responses",
21
+ rotateAfterMs: RESPONSES_WEBSOCKET_ROTATE_AFTER_MS,
22
+ }),
18
23
  defaults: { providerOptions: { store: false } },
19
24
  });
20
25
  const chatRoute = Route.make({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
- "version": "0.0.0-beta-17793",
3
+ "version": "0.0.0-beta-17887",
4
4
  "name": "@opencode-ai/ai",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -30,7 +30,7 @@
30
30
  "devDependencies": {
31
31
  "@clack/prompts": "1.0.0-alpha.1",
32
32
  "@effect/platform-node": "4.0.0-rc.110",
33
- "@opencode-ai/http-recorder": "0.0.0-beta-17793",
33
+ "@opencode-ai/http-recorder": "0.0.0-beta-17887",
34
34
  "@tsconfig/bun": "1.0.9",
35
35
  "@types/bun": "1.3.13",
36
36
  "@typescript/native-preview": "7.0.0-dev.20251207.1",
@@ -39,7 +39,7 @@
39
39
  "dependencies": {
40
40
  "@smithy/eventstream-codec": "4.2.14",
41
41
  "@smithy/util-utf8": "4.2.2",
42
- "@opencode-ai/schema": "0.0.0-beta-17793",
42
+ "@opencode-ai/schema": "0.0.0-beta-17887",
43
43
  "aws4fetch": "1.0.20",
44
44
  "effect": "4.0.0-rc.110",
45
45
  "google-auth-library": "10.5.0"