@opencode-ai/ai 0.0.0-dev-18146 → 0.0.0-dev-18150

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({
@@ -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, 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,17 @@ 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* ProviderShared.eventError(ADAPTER, "OpenAI Chat stream ended without finish_reason");
808
831
  const events = [];
809
832
  const toolCallEvents = state.finishReason === undefined && Object.keys(state.tools).length > 0
810
- ? Effect.runSync(ToolStream.finishAll(ADAPTER, state.tools)).events
833
+ ? (yield* ToolStream.finishAll(ADAPTER, state.tools)).events
811
834
  : state.toolCallEvents;
812
835
  const hasToolCalls = toolCallEvents.length > 0;
813
836
  const reason = state.finishReason
@@ -815,7 +838,7 @@ const finishEvents = (state) => {
815
838
  ...state.finishReason,
816
839
  normalized: state.finishReason.normalized === "stop" && hasToolCalls ? "tool-calls" : state.finishReason.normalized,
817
840
  }
818
- : { normalized: hasToolCalls ? "tool-calls" : "unknown" };
841
+ : { normalized: hasToolCalls ? "tool-calls" : "stop" };
819
842
  const metadata = reasoningMetadata(state.reasoningField, state.reasoningDetailsObserved ? state.reasoningDetails : undefined);
820
843
  const started = state.reasoningDetailsObserved && !state.reasoningEmitted
821
844
  ? Lifecycle.reasoningStart(state.lifecycle, events, "reasoning-0", reasoningMetadata(state.reasoningField))
@@ -825,7 +848,7 @@ const finishEvents = (state) => {
825
848
  events.push(...toolCallEvents);
826
849
  Lifecycle.finish(lifecycle, events, { reason, usage: state.usage });
827
850
  return events;
828
- };
851
+ });
829
852
  // =============================================================================
830
853
  // Protocol And OpenAI Route
831
854
  // =============================================================================
@@ -853,6 +876,7 @@ export const protocol = Protocol.make({
853
876
  reasoningDetailsObserved: false,
854
877
  reasoningEmitted: false,
855
878
  nextToolIndex: 0,
879
+ requireFinishReason: request.model.compatibility?.requireFinishReason ?? true,
856
880
  }),
857
881
  step,
858
882
  onHalt: finishEvents,
@@ -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-dev-18146",
3
+ "version": "0.0.0-dev-18150",
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-dev-18146",
33
+ "@opencode-ai/http-recorder": "0.0.0-dev-18150",
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-dev-18146",
42
+ "@opencode-ai/schema": "0.0.0-dev-18150",
43
43
  "aws4fetch": "1.0.20",
44
44
  "effect": "4.0.0-rc.111",
45
45
  "google-auth-library": "10.5.0"