@opencode-ai/ai 0.0.0-beta-18148 → 0.0.0-beta-18155

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.
@@ -565,7 +565,7 @@ export const protocol = Protocol.make({
565
565
  reasoningSignatures: {},
566
566
  }),
567
567
  step,
568
- onHalt,
568
+ onHalt: (state) => Effect.succeed(onHalt(state)),
569
569
  },
570
570
  });
571
571
  export const route = Route.make({
@@ -574,7 +574,7 @@ export const protocol = Protocol.make({
574
574
  lifecycle: Lifecycle.initial(),
575
575
  }),
576
576
  step,
577
- onHalt: finish,
577
+ onHalt: (state) => Effect.succeed(finish(state)),
578
578
  },
579
579
  });
580
580
  export const route = Route.make({
@@ -384,6 +384,7 @@ export declare const decodeKnownErrorEvent: (event: Event) => Effect.Effect<{
384
384
  export declare const Event: Schema.StructWithRest<Schema.Struct<{
385
385
  readonly type: Schema.String;
386
386
  readonly delta: Schema.optional<Schema.String>;
387
+ readonly arguments: Schema.optional<Schema.String>;
387
388
  readonly text: Schema.optional<Schema.String>;
388
389
  readonly item_id: Schema.optional<Schema.String>;
389
390
  readonly summary_index: Schema.optional<Schema.Number>;
@@ -1009,6 +1010,7 @@ export declare const protocol: Protocol<{
1009
1010
  readonly reason?: string | undefined;
1010
1011
  } | null | undefined;
1011
1012
  } | undefined;
1013
+ readonly arguments?: string | undefined;
1012
1014
  readonly param?: string | null | undefined;
1013
1015
  readonly status_code?: unknown;
1014
1016
  readonly item_id?: string | undefined;
@@ -204,6 +204,7 @@ export const decodeKnownErrorEvent = (event) => decodeWebSocketErrorEvent({
204
204
  export const Event = Schema.StructWithRest(Schema.Struct({
205
205
  type: Schema.String,
206
206
  delta: Schema.optional(Schema.String),
207
+ arguments: Schema.optional(Schema.String),
207
208
  text: Schema.optional(Schema.String),
208
209
  item_id: Schema.optional(Schema.String),
209
210
  summary_index: Schema.optional(Schema.Number),
@@ -756,9 +757,23 @@ const onReasoningSummaryPartDone = (state, event) => {
756
757
  ];
757
758
  };
758
759
  const onFunctionCallArgumentsDelta = Effect.fn("OpenResponses.onFunctionCallArgumentsDelta")(function* (state, event) {
759
- if (!event.item_id || !event.delta || !state.tools[event.item_id])
760
+ if (!event.item_id)
761
+ return [state, NO_EVENTS];
762
+ const tool = state.tools[event.item_id];
763
+ if (!tool)
764
+ return [state, NO_EVENTS];
765
+ const final = event.type === "response.function_call_arguments.done" ? event.arguments : undefined;
766
+ if (event.type === "response.function_call_arguments.done" && final === undefined)
767
+ return [state, NO_EVENTS];
768
+ if (final !== undefined && !final.startsWith(tool.input))
769
+ return [
770
+ { ...state, tools: ToolStream.start(state.tools, event.item_id, { ...tool, input: final }) },
771
+ NO_EVENTS,
772
+ ];
773
+ const delta = final === undefined ? event.delta : final.slice(tool.input.length);
774
+ if (!delta)
760
775
  return [state, NO_EVENTS];
761
- const result = ToolStream.appendExisting(state.id, state.tools, event.item_id, event.delta, `${state.name} tool argument delta is missing its tool call`);
776
+ const result = ToolStream.appendExisting(state.id, state.tools, event.item_id, delta, `${state.name} tool argument delta is missing its tool call`);
762
777
  if (ToolStream.isError(result))
763
778
  return yield* result;
764
779
  const events = [];
@@ -943,7 +958,7 @@ export const step = (state, event) => {
943
958
  return ProviderShared.eventError(state.id, `${event.type} message is missing id`);
944
959
  return Effect.succeed(onOutputItemAdded(state, event));
945
960
  }
946
- if (event.type === "response.function_call_arguments.delta")
961
+ if (event.type === "response.function_call_arguments.delta" || event.type === "response.function_call_arguments.done")
947
962
  return event.item_id
948
963
  ? onFunctionCallArgumentsDelta(state, event)
949
964
  : ProviderShared.eventError(state.id, `${event.type} is missing item_id`);
@@ -204,7 +204,7 @@ declare const OpenAIChatBody: Schema.Struct<{
204
204
  stop: Schema.optional<Schema.$Array<Schema.String>>;
205
205
  }>;
206
206
  export type OpenAIChatBody = Schema.Schema.Type<typeof OpenAIChatBody>;
207
- export declare const OpenAIChatEvent: Schema.Struct<{
207
+ export declare const OpenAIChatEvent: Schema.StructWithRest<Schema.Struct<{
208
208
  readonly choices: Schema.optional<Schema.NullOr<Schema.$Array<Schema.StructWithRest<Schema.Struct<{
209
209
  readonly delta: Schema.optional<Schema.NullOr<Schema.StructWithRest<Schema.Struct<{
210
210
  readonly content: Schema.optional<Schema.NullOr<Schema.String>>;
@@ -257,11 +257,11 @@ export declare const OpenAIChatEvent: Schema.Struct<{
257
257
  readonly rejected_prediction_tokens: Schema.optional<Schema.NullOr<Schema.Number>>;
258
258
  }>, readonly [Schema.$Record<Schema.String, Schema.Unknown>]>>>;
259
259
  }>, readonly [Schema.$Record<Schema.String, Schema.Unknown>]>>>;
260
- readonly error: Schema.optional<Schema.NullOr<Schema.Struct<{
260
+ readonly error: Schema.optional<Schema.NullOr<Schema.StructWithRest<Schema.Struct<{
261
261
  readonly code: Schema.optional<Schema.NullOr<Schema.Union<readonly [Schema.String, Schema.Number]>>>;
262
262
  readonly message: Schema.String;
263
- }>>>;
264
- }>;
263
+ }>, readonly [Schema.$Record<Schema.String, Schema.Unknown>]>>>;
264
+ }>, readonly [Schema.$Record<Schema.String, Schema.Unknown>]>;
265
265
  export type OpenAIChatEvent = Schema.Schema.Type<typeof OpenAIChatEvent>;
266
266
  interface PendingToolDelta {
267
267
  readonly id?: string;
@@ -281,6 +281,7 @@ export interface ParserState {
281
281
  readonly reasoningEmitted: boolean;
282
282
  readonly latestToolIndex?: number;
283
283
  readonly nextToolIndex: number;
284
+ readonly requireFinishReason: boolean;
284
285
  }
285
286
  interface LoweringOptions {
286
287
  readonly cacheControl?: (cache: CacheHint | undefined) => Schema.Schema.Type<typeof OpenAIChatCacheControl> | undefined;
@@ -585,7 +586,9 @@ export declare const protocol: Protocol<{
585
586
  readonly frequency_penalty?: number | undefined;
586
587
  readonly presence_penalty?: number | undefined;
587
588
  }, string, {
589
+ readonly [x: string]: unknown;
588
590
  readonly error?: {
591
+ readonly [x: string]: unknown;
589
592
  readonly message: string;
590
593
  readonly code?: string | number | null | undefined;
591
594
  } | null | undefined;
@@ -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, LLMEvent, Usage, } from "../schema/index.js";
8
+ import { AIError, InvalidProviderOutputReason, LLMEvent, ProviderInternalReason, UnknownProviderReason, 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";
@@ -167,15 +167,15 @@ const OpenAIChatChoice = Schema.StructWithRest(Schema.Struct({
167
167
  // Moonshot streams usage on `choice.usage` instead of top-level `usage`.
168
168
  usage: optionalNull(OpenAIChatUsage),
169
169
  }), [Schema.Record(Schema.String, Schema.Unknown)]);
170
- const OpenAIChatError = Schema.Struct({
170
+ const OpenAIChatError = Schema.StructWithRest(Schema.Struct({
171
171
  code: optionalNull(Schema.Union([Schema.String, Schema.Number])),
172
172
  message: Schema.String,
173
- });
174
- export const OpenAIChatEvent = Schema.Struct({
173
+ }), [Schema.Record(Schema.String, Schema.Unknown)]);
174
+ export const OpenAIChatEvent = Schema.StructWithRest(Schema.Struct({
175
175
  choices: optionalNull(Schema.Array(OpenAIChatChoice)),
176
176
  usage: optionalNull(OpenAIChatUsage),
177
177
  error: optionalNull(OpenAIChatError),
178
- });
178
+ }), [Schema.Record(Schema.String, Schema.Unknown)]);
179
179
  const lowerTool = (tool, inputSchema, options, supportsStrictMode) => ({
180
180
  type: "function",
181
181
  function: {
@@ -577,19 +577,32 @@ export const fromRequest = Effect.fn("OpenAIChat.fromRequest")(function* (reques
577
577
  // Streaming parsers are small state machines: every event returns a new state
578
578
  // plus the common `LLMEvent`s produced by that event. Tool calls are accumulated
579
579
  // because OpenAI streams JSON arguments across multiple deltas.
580
- const mapFinishReason = (reason) => {
581
- if (reason === "stop")
582
- return "stop";
583
- if (reason === "length")
584
- return "length";
585
- if (reason === "content_filter")
586
- return "content-filter";
587
- if (reason === "function_call" || reason === "tool_calls")
588
- return "tool-calls";
589
- if (reason === "error")
590
- return "error";
591
- return "unknown";
592
- };
580
+ const finishReasonError = (event, reason) => new AIError({
581
+ module: ADAPTER,
582
+ method: "stream",
583
+ body: ProviderShared.encodeJson(event),
584
+ reason,
585
+ });
586
+ const mapFinishReason = Effect.fn("OpenAIChat.mapFinishReason")(function* (event, reason) {
587
+ switch (reason) {
588
+ case "error":
589
+ return yield* finishReasonError(event, new UnknownProviderReason({ message: "Provider reported an error (finish_reason: error)" }));
590
+ case "network_error":
591
+ return yield* finishReasonError(event, new ProviderInternalReason({ message: "Provider reported a network error (finish_reason: network_error)" }));
592
+ case "stop":
593
+ case "end":
594
+ return "stop";
595
+ case "length":
596
+ return "length";
597
+ case "content_filter":
598
+ return "content-filter";
599
+ case "function_call":
600
+ case "tool_calls":
601
+ return "tool-calls";
602
+ default:
603
+ return "unknown";
604
+ }
605
+ });
593
606
  // OpenAI Chat reports `prompt_tokens` (inclusive total) with a
594
607
  // cached-read and cache-write subsets, and `completion_tokens` (inclusive
595
608
  // total) with a `reasoning_tokens` subset. We pass the inclusive totals
@@ -686,16 +699,20 @@ const reasoningMetadata = (field, details) => ({
686
699
  },
687
700
  });
688
701
  const step = (state, event) => Effect.gen(function* () {
689
- if (event.error)
702
+ if (event.error) {
703
+ const body = ProviderShared.encodeJson(event);
690
704
  return yield* new AIError({
691
705
  module: ADAPTER,
692
706
  method: "stream",
707
+ body,
693
708
  reason: classifyProviderFailure({
694
709
  message: event.error.message,
695
710
  code: event.error.code === undefined || event.error.code === null ? undefined : String(event.error.code),
696
711
  status: typeof event.error.code === "number" ? event.error.code : undefined,
712
+ rawBody: body,
697
713
  }),
698
714
  });
715
+ }
699
716
  const events = [];
700
717
  const choice = event.choices?.[0];
701
718
  // Moonshot (and a few other OpenAI-compatible providers) attach usage to
@@ -703,8 +720,11 @@ const step = (state, event) => Effect.gen(function* () {
703
720
  const choiceUsage = choice?.usage;
704
721
  const usage = mapUsage(event.usage) ?? (choiceUsage ? mapUsage(choiceUsage) : undefined) ?? state.usage;
705
722
  const rawFinishReason = choice?.finish_reason;
706
- const finishReason = rawFinishReason !== undefined && rawFinishReason !== null
707
- ? { normalized: mapFinishReason(rawFinishReason), raw: choice?.native_finish_reason ?? rawFinishReason }
723
+ const finishReason = rawFinishReason
724
+ ? {
725
+ normalized: yield* mapFinishReason(event, rawFinishReason),
726
+ raw: choice?.native_finish_reason ?? rawFinishReason,
727
+ }
708
728
  : state.finishReason;
709
729
  const delta = choice?.delta;
710
730
  const toolDeltas = delta?.tool_calls ?? [];
@@ -721,7 +741,7 @@ const step = (state, event) => Effect.gen(function* () {
721
741
  toolDeltas.some((tool) => Boolean(tool.id) || Boolean(tool.function?.name) || Boolean(tool.function?.arguments));
722
742
  if (state.finishReason !== undefined) {
723
743
  if (hasLateContent)
724
- return yield* ProviderShared.eventError(ADAPTER, "OpenAI Chat received content after the finish reason");
744
+ return yield* ProviderShared.eventError(ADAPTER, "OpenAI Chat received content after the finish reason", ProviderShared.encodeJson(event));
725
745
  return [{ ...state, usage }, events];
726
746
  }
727
747
  const reasoningField = state.reasoningField ?? reasoning?.field;
@@ -773,14 +793,14 @@ const step = (state, event) => Effect.gen(function* () {
773
793
  }
774
794
  const result = ToolStream.appendOrStart(ADAPTER, tools, index, { id: id || undefined, name: name || undefined, text }, "OpenAI Chat tool call delta is missing id or name");
775
795
  if (ToolStream.isError(result))
776
- return yield* result;
796
+ return yield* ProviderShared.eventError(ADAPTER, result.reason.message, ProviderShared.encodeJson(event));
777
797
  tools = result.tools;
778
798
  if (result.events.length)
779
799
  lifecycle = Lifecycle.stepStart(lifecycle, events);
780
800
  events.push(...result.events);
781
801
  }
782
802
  if (finishReason !== undefined && state.finishReason === undefined && Object.keys(pendingTools).length > 0)
783
- return yield* ProviderShared.eventError(ADAPTER, "OpenAI Chat tool call delta is missing id or name");
803
+ return yield* ProviderShared.eventError(ADAPTER, "OpenAI Chat tool call delta is missing id or name", ProviderShared.encodeJson(event));
784
804
  // Finalize accumulated tool inputs eagerly when finish_reason arrives so
785
805
  // valid calls and malformed local calls settle independently.
786
806
  const finished = finishReason !== undefined && state.finishReason === undefined && Object.keys(tools).length > 0
@@ -800,14 +820,25 @@ const step = (state, event) => Effect.gen(function* () {
800
820
  reasoningEmitted,
801
821
  latestToolIndex,
802
822
  nextToolIndex,
823
+ requireFinishReason: state.requireFinishReason,
803
824
  },
804
825
  events,
805
826
  ];
806
827
  });
807
- const finishEvents = (state) => {
828
+ const finishEvents = Effect.fn("OpenAIChat.finishEvents")(function* (state) {
829
+ if (state.finishReason === undefined && state.requireFinishReason)
830
+ return yield* new AIError({
831
+ module: ADAPTER,
832
+ method: "stream",
833
+ reason: new InvalidProviderOutputReason({
834
+ classification: "incomplete-stream",
835
+ message: "OpenAI Chat stream ended without finish_reason",
836
+ route: ADAPTER,
837
+ }),
838
+ });
808
839
  const events = [];
809
840
  const toolCallEvents = state.finishReason === undefined && Object.keys(state.tools).length > 0
810
- ? Effect.runSync(ToolStream.finishAll(ADAPTER, state.tools)).events
841
+ ? (yield* ToolStream.finishAll(ADAPTER, state.tools)).events
811
842
  : state.toolCallEvents;
812
843
  const hasToolCalls = toolCallEvents.length > 0;
813
844
  const reason = state.finishReason
@@ -815,7 +846,7 @@ const finishEvents = (state) => {
815
846
  ...state.finishReason,
816
847
  normalized: state.finishReason.normalized === "stop" && hasToolCalls ? "tool-calls" : state.finishReason.normalized,
817
848
  }
818
- : { normalized: hasToolCalls ? "tool-calls" : "unknown" };
849
+ : { normalized: hasToolCalls ? "tool-calls" : "stop" };
819
850
  const metadata = reasoningMetadata(state.reasoningField, state.reasoningDetailsObserved ? state.reasoningDetails : undefined);
820
851
  const started = state.reasoningDetailsObserved && !state.reasoningEmitted
821
852
  ? Lifecycle.reasoningStart(state.lifecycle, events, "reasoning-0", reasoningMetadata(state.reasoningField))
@@ -825,7 +856,7 @@ const finishEvents = (state) => {
825
856
  events.push(...toolCallEvents);
826
857
  Lifecycle.finish(lifecycle, events, { reason, usage: state.usage });
827
858
  return events;
828
- };
859
+ });
829
860
  // =============================================================================
830
861
  // Protocol And OpenAI Route
831
862
  // =============================================================================
@@ -853,6 +884,7 @@ export const protocol = Protocol.make({
853
884
  reasoningDetailsObserved: false,
854
885
  reasoningEmitted: false,
855
886
  nextToolIndex: 0,
887
+ requireFinishReason: request.model.compatibility?.requireFinishReason ?? true,
856
888
  }),
857
889
  step,
858
890
  onHalt: finishEvents,
@@ -309,6 +309,7 @@ export declare const protocol: Protocol<{
309
309
  readonly reason?: string | undefined;
310
310
  } | null | undefined;
311
311
  } | undefined;
312
+ readonly arguments?: string | undefined;
312
313
  readonly param?: string | null | undefined;
313
314
  readonly status_code?: unknown;
314
315
  readonly item_id?: string | undefined;
@@ -166,6 +166,7 @@ export declare const protocol: Protocol<{
166
166
  readonly reason?: string | undefined;
167
167
  } | null | undefined;
168
168
  } | undefined;
169
+ readonly arguments?: string | undefined;
169
170
  readonly param?: string | null | undefined;
170
171
  readonly status_code?: unknown;
171
172
  readonly item_id?: string | undefined;
@@ -283,7 +283,9 @@ export declare const protocol: Protocol<{
283
283
  readonly frequency_penalty?: number | undefined;
284
284
  readonly presence_penalty?: number | undefined;
285
285
  }, string, {
286
+ readonly [x: string]: unknown;
286
287
  readonly error?: {
288
+ readonly [x: string]: unknown;
287
289
  readonly message: string;
288
290
  readonly code?: string | number | null | undefined;
289
291
  } | null | undefined;
@@ -123,7 +123,17 @@ function makeFromTransport(input) {
123
123
  const route = `${request.model.provider}/${request.model.route.id}`;
124
124
  return Stream.unwrap(routeInput.transport.execute(prepared, request, runtime, options).pipe(Effect.map((execution) => {
125
125
  const events = execution.frames.pipe(Stream.mapEffect(decodeEvent(route)), protocol.stream.terminal ? Stream.takeUntil(protocol.stream.terminal) : (stream) => stream);
126
- const stream = events.pipe(Stream.mapAccumEffect(() => protocol.stream.initial(request), protocol.stream.step, protocol.stream.onHalt ? { onHalt: protocol.stream.onHalt } : undefined), Stream.catchCause((cause) => Stream.fail(streamError(route, `Failed to read ${route} stream`, cause))), requireTerminalEvent(route));
126
+ const stream = Stream.suspend(() => {
127
+ let state = protocol.stream.initial(request);
128
+ const parsed = events.pipe(Stream.mapEffect((event) => protocol.stream.step(state, event).pipe(Effect.map(([next, output]) => {
129
+ state = next;
130
+ return output;
131
+ }))), Stream.flatMap(Stream.fromIterable));
132
+ const onHalt = protocol.stream.onHalt;
133
+ return onHalt
134
+ ? parsed.pipe(Stream.concat(Stream.suspend(() => Stream.unwrap(onHalt(state).pipe(Effect.map(Stream.fromIterable))))))
135
+ : parsed;
136
+ }).pipe(Stream.catchCause((cause) => Stream.fail(streamError(route, `Failed to read ${route} stream`, cause))), requireTerminalEvent(route));
127
137
  return execution.complete ? stream.pipe(Stream.onEnd(execution.complete)) : stream;
128
138
  })));
129
139
  },
@@ -56,8 +56,8 @@ export interface ProtocolStream<Frame, Event, State> {
56
56
  readonly step: (state: State, event: Event) => Effect.Effect<readonly [State, ReadonlyArray<LLMEvent>], AIError>;
57
57
  /** Optional request-completion signal for transports that do not end naturally. */
58
58
  readonly terminal?: (event: Event) => boolean;
59
- /** Optional flush emitted when the framed stream ends. */
60
- readonly onHalt?: (state: State) => ReadonlyArray<LLMEvent>;
59
+ /** Optional effectful flush emitted when the framed stream ends. */
60
+ readonly onHalt?: (state: State) => Effect.Effect<ReadonlyArray<LLMEvent>, AIError>;
61
61
  }
62
62
  /**
63
63
  * Construct a `Protocol` from its body and stream pieces:
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-18148",
3
+ "version": "0.0.0-beta-18155",
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.111",
33
- "@opencode-ai/http-recorder": "0.0.0-beta-18148",
33
+ "@opencode-ai/http-recorder": "0.0.0-beta-18155",
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-18148",
42
+ "@opencode-ai/schema": "0.0.0-beta-18155",
43
43
  "aws4fetch": "1.0.20",
44
44
  "effect": "4.0.0-rc.111",
45
45
  "google-auth-library": "10.5.0"