@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
@@ -1,7 +1,7 @@
1
1
  import { Effect, Schema } from "effect";
2
2
  import { HttpTransport } from "../route/transport/index.js";
3
3
  import { Protocol } from "../route/protocol.js";
4
- import { AIError, LLMEvent, ProviderInternalReason, Usage, } from "../schema/index.js";
4
+ import { AIError, LLMEvent, ProviderInternalError, Usage, } from "../schema/index.js";
5
5
  import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared.js";
6
6
  import { classifyProviderFailure } from "../provider-error.js";
7
7
  import { OpenResponsesOptions } from "./utils/open-responses-options.js";
@@ -607,16 +607,16 @@ const NO_EVENTS = [];
607
607
  const TERMINAL_TYPES = new Set(["error", "response.completed", "response.incomplete", "response.failed"]);
608
608
  export const terminal = (event) => TERMINAL_TYPES.has(event.type);
609
609
  const onOutputTextDelta = (state, event, id) => {
610
- if (!event.delta || !state.messageItems.has(id))
610
+ if (!event.delta || state.message?.id !== id)
611
611
  return [state, NO_EVENTS];
612
612
  const events = [];
613
- const phase = state.messagePhases[id];
613
+ const phase = state.message.phase;
614
614
  const metadata = providerMetadata(state, { itemId: id, ...(phase === undefined ? {} : { phase }) });
615
615
  const lifecycle = Lifecycle.textStart(state.lifecycle, events, id, metadata);
616
616
  return [{ ...state, lifecycle: Lifecycle.textDelta(lifecycle, events, id, event.delta) }, events];
617
617
  };
618
618
  const onOutputTextDone = (state, event, id) => {
619
- if (state.messageItems.has(id)) {
619
+ if (state.message?.id === id) {
620
620
  if (state.lifecycle.text.has(id) || event.text === undefined)
621
621
  return [state, NO_EVENTS];
622
622
  return onOutputTextDelta(state, { ...event, delta: event.text }, id);
@@ -625,19 +625,51 @@ const onOutputTextDone = (state, event, id) => {
625
625
  return [{ ...state, lifecycle: Lifecycle.textEnd(state.lifecycle, events, id) }, events];
626
626
  };
627
627
  export const outputItemID = (state, event) => event.output_index === undefined ? event.item_id : (state.outputItems[event.output_index] ?? event.item_id);
628
- export const onReasoningDelta = (state, event, itemID) => {
628
+ const startReasoningSummaryPart = (state, itemID, index) => {
629
629
  const item = state.reasoningItems[itemID];
630
- if (!event.delta || !item)
630
+ if (!item?.open || index === 0 || item.summaryParts[index] !== undefined)
631
631
  return [state, NO_EVENTS];
632
- const index = event.summary_index ?? 0;
633
632
  const events = [];
633
+ const lifecycle = Object.entries(item.summaryParts)
634
+ .filter((entry) => entry[1] !== "concluded")
635
+ .reduce((lifecycle, entry) => Lifecycle.reasoningEnd(lifecycle, events, `${itemID}:${entry[0]}`, providerMetadata(state, { itemId: itemID })), state.lifecycle);
634
636
  return [
635
637
  {
636
638
  ...state,
637
- lifecycle: Lifecycle.reasoningDelta(state.lifecycle, events, `${itemID}:${index}`, event.delta),
639
+ lifecycle: Lifecycle.reasoningStart(lifecycle, events, `${itemID}:${index}`, providerMetadata(state, { itemId: itemID, reasoningEncryptedContent: item.encryptedContent ?? null })),
638
640
  reasoningItems: {
639
641
  ...state.reasoningItems,
640
- [itemID]: { ...item, deltaIndexes: new Set([...item.deltaIndexes, index]) },
642
+ [itemID]: {
643
+ ...item,
644
+ summaryParts: {
645
+ ...Object.fromEntries(Object.entries(item.summaryParts).map((entry) => entry[1] === "concluded" ? entry : [entry[0], "concluded"])),
646
+ [index]: "active",
647
+ },
648
+ },
649
+ },
650
+ },
651
+ events,
652
+ ];
653
+ };
654
+ export const onReasoningDelta = (state, event, itemID) => {
655
+ const item = state.reasoningItems[itemID];
656
+ if (!event.delta || !item?.open)
657
+ return [state, NO_EVENTS];
658
+ const index = event.summary_index ?? 0;
659
+ if (item.summaryParts[index] === "concluded")
660
+ return [state, NO_EVENTS];
661
+ const [started, emitted] = startReasoningSummaryPart(state, itemID, index);
662
+ const current = started.reasoningItems[itemID];
663
+ if (!current)
664
+ return [started, emitted];
665
+ const events = [...emitted];
666
+ return [
667
+ {
668
+ ...started,
669
+ lifecycle: Lifecycle.reasoningDelta(started.lifecycle, events, `${itemID}:${index}`, event.delta),
670
+ reasoningItems: {
671
+ ...started.reasoningItems,
672
+ [itemID]: { ...current, deltaIndexes: new Set([...current.deltaIndexes, index]) },
641
673
  },
642
674
  },
643
675
  events,
@@ -648,7 +680,7 @@ export const onReasoningDelta = (state, event, itemID) => {
648
680
  // as a single delta unless that summary index already streamed one.
649
681
  export const onReasoningDone = (state, event, itemID) => {
650
682
  const item = state.reasoningItems[itemID];
651
- if (!item || typeof event.text !== "string")
683
+ if (!item?.open || typeof event.text !== "string")
652
684
  return [state, NO_EVENTS];
653
685
  const index = event.summary_index ?? 0;
654
686
  if (item.deltaIndexes.has(index))
@@ -656,32 +688,44 @@ export const onReasoningDone = (state, event, itemID) => {
656
688
  return onReasoningDelta(state, { ...event, delta: event.text }, itemID);
657
689
  };
658
690
  const reasoningMetadata = (state, item) => providerMetadata(state, { itemId: item.id, reasoningEncryptedContent: item.encrypted_content ?? null });
659
- // Responses APIs stream reasoning items in a stable order:
691
+ // Responses APIs normally stream reasoning items in this order:
660
692
  // `output_item.added` (reasoning) →
661
693
  // `reasoning_summary_part.added` (index=0) →
662
694
  // `reasoning_summary_text.delta` →
663
695
  // `reasoning_summary_part.done` (index=0) →
664
696
  // (repeat for index>0) →
665
697
  // `output_item.done` (reasoning).
666
- // The handlers below rely on this ordering: `onOutputItemAdded` seeds the
667
- // per-item entry, `onReasoningSummaryPartAdded` for `summary_index === 0`
668
- // short-circuits when the entry already exists, and higher-index handlers
669
- // fold against the same entry. Behaviour for out-of-order events is
670
- // best-effort, not guaranteed.
698
+ // `onOutputItemAdded` seeds the per-item entry, while each later part start is
699
+ // also an implicit boundary for the previous part. This keeps the common event
700
+ // lifecycle ordered when a compatible provider omits or delays a part-done event.
671
701
  const onOutputItemAdded = (state, event) => {
672
702
  const item = event.item;
673
703
  if (item?.type === "message" && item.id !== undefined) {
704
+ const itemID = item.id;
674
705
  const phase = messagePhase(item.phase);
706
+ // A new message closes earlier messages, including ones that never streamed.
707
+ const events = [];
708
+ const lifecycle = [...state.lifecycle.text]
709
+ .filter((id) => id !== itemID)
710
+ .reduce((lifecycle, id) => {
711
+ const openPhase = state.message?.id === id ? state.message.phase : undefined;
712
+ return Lifecycle.textEnd(lifecycle, events, id, providerMetadata(state, { itemId: id, ...(openPhase === undefined ? {} : { phase: openPhase }) }));
713
+ }, state.lifecycle);
675
714
  return [
676
715
  {
677
716
  ...state,
678
- messageItems: new Set([...state.messageItems, item.id]),
679
- messagePhases: phase === undefined ? state.messagePhases : { ...state.messagePhases, [item.id]: phase },
717
+ lifecycle,
718
+ message: {
719
+ id: itemID,
720
+ phase: phase === undefined && state.message?.id === itemID ? state.message.phase : phase,
721
+ },
680
722
  },
681
- NO_EVENTS,
723
+ events,
682
724
  ];
683
725
  }
684
726
  if (item && isReasoningItem(item)) {
727
+ if (state.reasoningItems[item.id] !== undefined)
728
+ return [state, NO_EVENTS];
685
729
  const events = [];
686
730
  return [
687
731
  {
@@ -690,6 +734,7 @@ const onOutputItemAdded = (state, event) => {
690
734
  reasoningItems: {
691
735
  ...state.reasoningItems,
692
736
  [item.id]: {
737
+ open: true,
693
738
  encryptedContent: item.encrypted_content,
694
739
  summaryParts: { 0: "active" },
695
740
  deltaIndexes: new Set(),
@@ -702,6 +747,8 @@ const onOutputItemAdded = (state, event) => {
702
747
  if (item?.type !== "function_call" || !item.call_id)
703
748
  return [state, NO_EVENTS];
704
749
  const id = item.id ?? item.call_id;
750
+ if (Object.values(state.tools).some((tool) => tool?.id === item.call_id) || state.completedTools.has(item.call_id))
751
+ return [state, NO_EVENTS];
705
752
  const metadata = item.id !== undefined ? providerMetadata(state, { itemId: item.id }) : undefined;
706
753
  const events = [];
707
754
  const lifecycle = Lifecycle.stepStart(state.lifecycle, events);
@@ -722,38 +769,15 @@ const onOutputItemAdded = (state, event) => {
722
769
  const onReasoningSummaryPartAdded = (state, event) => {
723
770
  if (event.item_id === undefined || event.summary_index === undefined)
724
771
  return [state, NO_EVENTS];
725
- const item = state.reasoningItems[event.item_id];
726
- if (!item)
727
- return [state, NO_EVENTS];
728
- if (event.summary_index === 0)
729
- return [state, NO_EVENTS];
730
- const events = [];
731
- const closed = Object.entries(item.summaryParts)
732
- .filter((entry) => entry[1] === "can-conclude")
733
- .reduce((lifecycle, entry) => Lifecycle.reasoningEnd(lifecycle, events, `${event.item_id}:${entry[0]}`, providerMetadata(state, { itemId: event.item_id })), state.lifecycle);
734
- return [
735
- {
736
- ...state,
737
- lifecycle: Lifecycle.reasoningStart(closed, events, `${event.item_id}:${event.summary_index}`, providerMetadata(state, { itemId: event.item_id, reasoningEncryptedContent: item.encryptedContent ?? null })),
738
- reasoningItems: {
739
- ...state.reasoningItems,
740
- [event.item_id]: {
741
- ...item,
742
- summaryParts: {
743
- ...Object.fromEntries(Object.entries(item.summaryParts).map((entry) => entry[1] === "can-conclude" ? [entry[0], "concluded"] : entry)),
744
- [event.summary_index]: "active",
745
- },
746
- },
747
- },
748
- },
749
- events,
750
- ];
772
+ return startReasoningSummaryPart(state, event.item_id, event.summary_index);
751
773
  };
752
774
  const onReasoningSummaryPartDone = (state, event) => {
753
775
  if (event.item_id === undefined || event.summary_index === undefined)
754
776
  return [state, NO_EVENTS];
755
777
  const item = state.reasoningItems[event.item_id];
756
- if (!item)
778
+ if (!item?.open)
779
+ return [state, NO_EVENTS];
780
+ if (item.summaryParts[event.summary_index] !== "active")
757
781
  return [state, NO_EVENTS];
758
782
  return [
759
783
  {
@@ -803,17 +827,13 @@ const onOutputItemDone = Effect.fn("OpenResponses.onOutputItemDone")(function* (
803
827
  return [state, NO_EVENTS];
804
828
  if (item.type === "message" && item.id !== undefined) {
805
829
  const itemPhase = messagePhase(item.phase);
806
- const phase = itemPhase === undefined ? state.messagePhases[item.id] : itemPhase;
830
+ const phase = itemPhase === undefined && state.message?.id === item.id ? state.message.phase : itemPhase;
807
831
  const events = [];
808
- const messageItems = new Set(state.messageItems);
809
- messageItems.delete(item.id);
810
- const { [item.id]: _phase, ...messagePhases } = state.messagePhases;
811
832
  return [
812
833
  {
813
834
  ...state,
814
835
  lifecycle: Lifecycle.textEnd(state.lifecycle, events, item.id, providerMetadata(state, { itemId: item.id, ...(phase === undefined ? {} : { phase }) })),
815
- messageItems,
816
- messagePhases,
836
+ message: state.message?.id === item.id ? undefined : state.message,
817
837
  },
818
838
  events,
819
839
  ];
@@ -821,19 +841,33 @@ const onOutputItemDone = Effect.fn("OpenResponses.onOutputItemDone")(function* (
821
841
  if (item.type === "function_call") {
822
842
  if (!item.call_id || !item.name)
823
843
  return [state, NO_EVENTS];
824
- const id = item.id ?? item.call_id;
825
- const tools = state.tools[id]
844
+ const callID = item.call_id;
845
+ if (state.completedTools.has(callID))
846
+ return [state, NO_EVENTS];
847
+ const metadata = item.id !== undefined ? providerMetadata(state, { itemId: item.id }) : undefined;
848
+ const fallback = item.id ?? callID;
849
+ // Match the pending tool by call id so item events that disagree on
850
+ // whether `item.id` is present still resolve the same call.
851
+ const registered = state.tools[fallback] !== undefined
852
+ ? fallback
853
+ : Object.keys(state.tools).find((key) => state.tools[key]?.id === callID);
854
+ const id = registered ?? fallback;
855
+ const tools = registered !== undefined
826
856
  ? state.tools
827
857
  : ToolStream.start(state.tools, id, {
828
- id: item.call_id,
858
+ id: callID,
829
859
  name: item.name,
830
- providerMetadata: item.id !== undefined ? providerMetadata(state, { itemId: item.id }) : undefined,
860
+ providerMetadata: metadata,
831
861
  });
832
862
  const result = item.arguments === undefined
833
863
  ? yield* ToolStream.finish(state.id, tools, id)
834
864
  : yield* ToolStream.finishWithInput(state.id, tools, id, item.arguments);
835
865
  const events = [];
836
- const resultEvents = result.events ?? [];
866
+ const finished = result.events ?? [];
867
+ // A done-only call never streamed a start event, so open its lifecycle here.
868
+ const resultEvents = registered !== undefined || finished.length === 0
869
+ ? finished
870
+ : [LLMEvent.toolInputStart({ id: callID, name: item.name, providerMetadata: metadata }), ...finished];
837
871
  const lifecycle = resultEvents.length ? Lifecycle.stepStart(state.lifecycle, events) : state.lifecycle;
838
872
  events.push(...resultEvents);
839
873
  return [
@@ -843,6 +877,7 @@ const onOutputItemDone = Effect.fn("OpenResponses.onOutputItemDone")(function* (
843
877
  hasFunctionCall: resultEvents.some((event) => LLMEvent.is.toolCall(event) || LLMEvent.is.toolInputError(event)) ||
844
878
  state.hasFunctionCall,
845
879
  tools: result.tools,
880
+ completedTools: new Set([...state.completedTools, callID]),
846
881
  },
847
882
  events,
848
883
  ];
@@ -852,17 +887,47 @@ const onOutputItemDone = Effect.fn("OpenResponses.onOutputItemDone")(function* (
852
887
  const metadata = reasoningMetadata(state, item);
853
888
  const reasoningItem = state.reasoningItems[item.id];
854
889
  if (reasoningItem) {
890
+ if (!reasoningItem.open)
891
+ return [state, NO_EVENTS];
855
892
  const lifecycle = Object.entries(reasoningItem.summaryParts)
856
893
  .filter((entry) => entry[1] === "active" || entry[1] === "can-conclude")
857
894
  .reduce((lifecycle, entry) => Lifecycle.reasoningEnd(lifecycle, events, `${item.id}:${entry[0]}`, metadata), state.lifecycle);
858
- const { [item.id]: _removed, ...reasoningItems } = state.reasoningItems;
859
- return [{ ...state, lifecycle, reasoningItems }, events];
895
+ return [
896
+ {
897
+ ...state,
898
+ lifecycle,
899
+ reasoningItems: {
900
+ ...state.reasoningItems,
901
+ [item.id]: {
902
+ ...reasoningItem,
903
+ open: false,
904
+ encryptedContent: item.encrypted_content ?? reasoningItem.encryptedContent,
905
+ },
906
+ },
907
+ },
908
+ events,
909
+ ];
860
910
  }
861
911
  if (!state.lifecycle.reasoning.has(item.id)) {
862
912
  const lifecycle = Lifecycle.stepStart(state.lifecycle, events);
863
913
  events.push(LLMEvent.reasoningStart({ id: item.id, providerMetadata: metadata }));
864
914
  events.push(LLMEvent.reasoningEnd({ id: item.id, providerMetadata: metadata }));
865
- return [{ ...state, lifecycle }, events];
915
+ return [
916
+ {
917
+ ...state,
918
+ lifecycle,
919
+ reasoningItems: {
920
+ ...state.reasoningItems,
921
+ [item.id]: {
922
+ open: false,
923
+ encryptedContent: item.encrypted_content,
924
+ summaryParts: { 0: "concluded" },
925
+ deltaIndexes: new Set(),
926
+ },
927
+ },
928
+ },
929
+ events,
930
+ ];
866
931
  }
867
932
  return [
868
933
  { ...state, lifecycle: Lifecycle.reasoningEnd(state.lifecycle, events, item.id, metadata) },
@@ -877,7 +942,7 @@ const onResponseFinish = Effect.fn("OpenResponses.onResponseFinish")(function* (
877
942
  const id = item.id ?? (item.type === "function_call" ? item.call_id : undefined);
878
943
  if (id === undefined ||
879
944
  ((item.type !== "function_call" || !current.tools[id]) &&
880
- (item.type !== "reasoning" || !current.reasoningItems[id])))
945
+ (item.type !== "reasoning" || !current.reasoningItems[id]?.open)))
881
946
  return Effect.succeed([current, events]);
882
947
  return onOutputItemDone(current, { type: "response.output_item.done", item }).pipe(Effect.map(([next, emitted]) => [next, [...events, ...emitted]]));
883
948
  })
@@ -918,11 +983,8 @@ const providerErrorMessage = (event, nested) => {
918
983
  return `${code}: ${message}`;
919
984
  return message || code;
920
985
  };
921
- export const providerFailure = (id, event, fallback) => {
986
+ export const providerFailure = (event, fallback, body = ProviderShared.encodeJson(event)) => {
922
987
  const nested = event.error ?? event.response?.error ?? undefined;
923
- const code = event.code || nested?.code || undefined;
924
- // Keep the full raw payload on the error even when the message is a summary.
925
- const body = JSON.stringify(nested ?? event) ?? "";
926
988
  const summary = providerErrorMessage(event, nested);
927
989
  const message = summary ?? (body === "{}" ? fallback : body);
928
990
  const status = typeof event.status === "number"
@@ -935,16 +997,10 @@ export const providerFailure = (id, event, fallback) => {
935
997
  event.response === undefined &&
936
998
  summary === undefined &&
937
999
  status === undefined
938
- ? new ProviderInternalReason({ message })
939
- : classifyProviderFailure({ message, code, status, rawBody: body });
940
- return new AIError({
941
- module: id,
942
- method: "stream",
943
- body,
944
- reason,
945
- });
1000
+ ? new ProviderInternalError({ message, body })
1001
+ : classifyProviderFailure({ message, status, rawBody: body });
1002
+ return new AIError({ reason });
946
1003
  };
947
- const providerError = (state, event, fallback) => providerFailure(state.id, event, fallback);
948
1004
  export const step = (state, input) => {
949
1005
  // The OpenAPI requires string IDs but imposes no minLength; empty is not missing.
950
1006
  const event = input.item_id !== undefined && outputItemID(state, input) !== input.item_id
@@ -988,6 +1044,11 @@ export const step = (state, input) => {
988
1044
  if (event.type === "response.output_item.added") {
989
1045
  if (event.item?.type === "message" && event.item.id === undefined)
990
1046
  return ProviderShared.eventError(state.id, `${event.type} message is missing id`);
1047
+ if (event.item &&
1048
+ isReasoningItem(event.item) &&
1049
+ state.reasoningItems[event.item.id] === undefined &&
1050
+ state.lifecycle.reasoning.size > 0)
1051
+ return ProviderShared.eventError(state.id, `${event.type} started reasoning before the previous item ended`);
991
1052
  const id = event.item?.id ?? (event.item?.type === "function_call" ? event.item.call_id : undefined);
992
1053
  return Effect.succeed(onOutputItemAdded(event.output_index !== undefined && id !== undefined
993
1054
  ? { ...state, outputItems: { ...state.outputItems, [event.output_index]: id } }
@@ -1005,9 +1066,9 @@ export const step = (state, input) => {
1005
1066
  if (event.type === "response.completed" || event.type === "response.incomplete")
1006
1067
  return onResponseFinish(state, event);
1007
1068
  if (event.type === "response.failed")
1008
- return providerError(state, event, `${state.name} response failed`);
1069
+ return providerFailure(event, `${state.name} response failed`);
1009
1070
  if (event.type === "error")
1010
- return decodeKnownErrorEvent(event).pipe(Effect.mapError(() => ProviderShared.eventError(state.id, `${state.name} returned a malformed error event`)), Effect.flatMap(() => providerError(state, event, `${state.name} stream error`)));
1071
+ return decodeKnownErrorEvent(event).pipe(Effect.mapError((cause) => ProviderShared.eventError(state.id, `${state.name} returned a malformed error event`, ProviderShared.encodeJson(event), cause)), Effect.flatMap(() => providerFailure(event, `${state.name} stream error`)));
1011
1072
  return Effect.succeed([state, NO_EVENTS]);
1012
1073
  };
1013
1074
  // =============================================================================
@@ -1023,10 +1084,10 @@ export const initial = (request, extension = BASE) => ({
1023
1084
  providerMetadataKey: request.model.route.providerMetadataKey ?? "openresponses",
1024
1085
  hasFunctionCall: false,
1025
1086
  tools: ToolStream.empty(),
1087
+ completedTools: new Set(),
1026
1088
  lifecycle: Lifecycle.initial(),
1027
1089
  outputItems: {},
1028
- messageItems: new Set(),
1029
- messagePhases: {},
1090
+ message: undefined,
1030
1091
  reasoningItems: {},
1031
1092
  });
1032
1093
  export const protocol = Protocol.make({
@@ -1,5 +1,6 @@
1
1
  import { Effect, Schema } from "effect";
2
2
  import { Route } from "../route/client.js";
3
+ import { Framing } from "../route/framing.js";
3
4
  import { HttpTransport } from "../route/transport/index.js";
4
5
  import { Protocol } from "../route/protocol.js";
5
6
  import { AIError, LLMEvent, Usage, type FinishReasonDetails, type CacheHint, type LLMRequest } from "../schema/index.js";
@@ -587,7 +588,7 @@ export declare const protocol: Protocol<{
587
588
  readonly tool_stream?: boolean | undefined;
588
589
  readonly frequency_penalty?: number | undefined;
589
590
  readonly presence_penalty?: number | undefined;
590
- }, string, {
591
+ }, string, "[DONE]" | {
591
592
  readonly [x: string]: unknown;
592
593
  readonly error?: {
593
594
  readonly [x: string]: unknown;
@@ -655,6 +656,7 @@ export declare const protocol: Protocol<{
655
656
  readonly native_finish_reason?: string | null | undefined;
656
657
  }[] | null | undefined;
657
658
  }, ParserState>;
659
+ export declare const framing: Framing.Definition<string>;
658
660
  export declare const httpTransport: HttpTransport.HttpJsonTransport<{
659
661
  readonly model: string;
660
662
  readonly messages: readonly ({
@@ -3,9 +3,10 @@ import { Tool } from "@opencode-ai/schema/tool";
3
3
  import { Route } from "../route/client.js";
4
4
  import { Auth } from "../route/auth.js";
5
5
  import { Endpoint } from "../route/endpoint.js";
6
+ import { Framing } from "../route/framing.js";
6
7
  import { HttpTransport } from "../route/transport/index.js";
7
8
  import { Protocol } from "../route/protocol.js";
8
- import { AIError, InvalidProviderOutputReason, LLMEvent, ProviderInternalReason, UnknownProviderReason, Usage, } from "../schema/index.js";
9
+ import { AIError, AIErrorReason, InvalidProviderOutputError, LLMEvent, ProviderInternalError, UnknownProviderError, Usage, } from "../schema/index.js";
9
10
  import { classifyProviderFailure } from "../provider-error.js";
10
11
  import { isRecord, JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared.js";
11
12
  import { OpenAIOptions } from "./utils/openai-options.js";
@@ -176,6 +177,8 @@ export const OpenAIChatEvent = Schema.StructWithRest(Schema.Struct({
176
177
  usage: optionalNull(OpenAIChatUsage),
177
178
  error: optionalNull(OpenAIChatError),
178
179
  }), [Schema.Record(Schema.String, Schema.Unknown)]);
180
+ const DONE = "[DONE]";
181
+ const OpenAIChatStreamEvent = Schema.Union([Schema.Literal(DONE), Protocol.jsonEvent(OpenAIChatEvent)]);
179
182
  const lowerTool = (tool, inputSchema, options, supportsStrictMode) => ({
180
183
  type: "function",
181
184
  function: {
@@ -613,18 +616,22 @@ export const fromRequest = Effect.fn("OpenAIChat.fromRequest")(function* (reques
613
616
  // Streaming parsers are small state machines: every event returns a new state
614
617
  // plus the common `LLMEvent`s produced by that event. Tool calls are accumulated
615
618
  // because OpenAI streams JSON arguments across multiple deltas.
616
- const finishReasonError = (event, reason) => new AIError({
617
- module: ADAPTER,
618
- method: "stream",
619
- body: ProviderShared.encodeJson(event),
620
- reason,
621
- });
622
619
  const mapFinishReason = Effect.fn("OpenAIChat.mapFinishReason")(function* (event, reason) {
623
620
  switch (reason) {
624
621
  case "error":
625
- return yield* finishReasonError(event, new UnknownProviderReason({ message: "Provider reported an error (finish_reason: error)" }));
622
+ return yield* new AIError({
623
+ reason: new UnknownProviderError({
624
+ message: "Provider reported an error (finish_reason: error)",
625
+ body: ProviderShared.encodeJson(event),
626
+ }),
627
+ });
626
628
  case "network_error":
627
- return yield* finishReasonError(event, new ProviderInternalReason({ message: "Provider reported a network error (finish_reason: network_error)" }));
629
+ return yield* new AIError({
630
+ reason: new ProviderInternalError({
631
+ message: "Provider reported a network error (finish_reason: network_error)",
632
+ body: ProviderShared.encodeJson(event),
633
+ }),
634
+ });
628
635
  case "stop":
629
636
  case "end":
630
637
  return "stop";
@@ -738,12 +745,8 @@ const step = (state, event) => Effect.gen(function* () {
738
745
  if (event.error) {
739
746
  const body = ProviderShared.encodeJson(event);
740
747
  return yield* new AIError({
741
- module: ADAPTER,
742
- method: "stream",
743
- body,
744
748
  reason: classifyProviderFailure({
745
749
  message: event.error.message,
746
- code: event.error.code === undefined || event.error.code === null ? undefined : String(event.error.code),
747
750
  status: typeof event.error.code === "number" ? event.error.code : undefined,
748
751
  rawBody: body,
749
752
  }),
@@ -796,14 +799,13 @@ const step = (state, event) => Effect.gen(function* () {
796
799
  (Boolean(delta?.content) || Boolean(delta?.refusal) || toolDeltas.length > 0))
797
800
  lifecycle = Lifecycle.reasoningStart(lifecycle, events, "reasoning-0", deltaMetadata);
798
801
  const reasoningEmitted = state.reasoningEmitted || lifecycle.reasoning.has("reasoning-0");
799
- if (delta?.content) {
800
- lifecycle = Lifecycle.reasoningEnd(lifecycle, events, "reasoning-0", reasoningMetadata(state.providerMetadataKey, reasoningField, reasoningDetailsObserved ? state.reasoningDetails : undefined));
802
+ // Reasoning is one response-wide channel: it stays open alongside text and
803
+ // refusal output so late reasoning deltas and details join the same block,
804
+ // and `finishEvents` closes it once with the complete metadata.
805
+ if (delta?.content)
801
806
  lifecycle = Lifecycle.textDelta(lifecycle, events, "text-0", delta.content);
802
- }
803
- if (delta?.refusal) {
804
- lifecycle = Lifecycle.reasoningEnd(lifecycle, events, "reasoning-0", reasoningMetadata(state.providerMetadataKey, reasoningField, reasoningDetailsObserved ? state.reasoningDetails : undefined));
807
+ if (delta?.refusal)
805
808
  lifecycle = Lifecycle.textDelta(lifecycle, events, "text-0", delta.refusal);
806
- }
807
809
  // Compatible providers may omit indexes. Prefer durable identity, then use
808
810
  // batch position for parallel deltas or the latest call for sparse chunks.
809
811
  for (const [position, tool] of toolDeltas.entries()) {
@@ -831,7 +833,14 @@ const step = (state, event) => Effect.gen(function* () {
831
833
  }
832
834
  const result = ToolStream.appendOrStart(ADAPTER, tools, index, { id: id || undefined, name: name || undefined, text }, "OpenAI Chat tool call delta is missing id or name");
833
835
  if (ToolStream.isError(result))
834
- return yield* ProviderShared.eventError(ADAPTER, result.reason.message, ProviderShared.encodeJson(event));
836
+ return yield* new AIError({
837
+ reason: AIErrorReason.make({
838
+ ...result.reason,
839
+ message: result.message,
840
+ cause: result.reason.cause,
841
+ body: ProviderShared.encodeJson(event),
842
+ }),
843
+ });
835
844
  tools = result.tools;
836
845
  if (result.events.length)
837
846
  lifecycle = Lifecycle.stepStart(lifecycle, events);
@@ -867,11 +876,9 @@ const step = (state, event) => Effect.gen(function* () {
867
876
  const finishEvents = Effect.fn("OpenAIChat.finishEvents")(function* (state) {
868
877
  if (state.finishReason === undefined && state.requireFinishReason)
869
878
  return yield* new AIError({
870
- module: ADAPTER,
871
- method: "stream",
872
- reason: new InvalidProviderOutputReason({
873
- classification: "incomplete-stream",
879
+ reason: new InvalidProviderOutputError({
874
880
  message: "OpenAI Chat stream ended without finish_reason",
881
+ classification: "incomplete-stream",
875
882
  route: ADAPTER,
876
883
  }),
877
884
  });
@@ -886,7 +893,9 @@ const finishEvents = Effect.fn("OpenAIChat.finishEvents")(function* (state) {
886
893
  normalized: state.finishReason.normalized === "stop" && hasToolCalls ? "tool-calls" : state.finishReason.normalized,
887
894
  }
888
895
  : { normalized: hasToolCalls ? "tool-calls" : "stop" };
889
- const metadata = reasoningMetadata(state.providerMetadataKey, state.reasoningField, state.reasoningDetailsObserved ? state.reasoningDetails : undefined);
896
+ // Snapshot details at publish time so the emitted event never observes later
897
+ // mutation of the accumulated `reasoningDetails` array.
898
+ const metadata = reasoningMetadata(state.providerMetadataKey, state.reasoningField, state.reasoningDetailsObserved ? [...state.reasoningDetails] : undefined);
890
899
  const started = state.reasoningDetailsObserved && !state.reasoningEmitted
891
900
  ? Lifecycle.reasoningStart(state.lifecycle, events, "reasoning-0", reasoningMetadata(state.providerMetadataKey, state.reasoningField))
892
901
  : state.lifecycle;
@@ -912,7 +921,7 @@ export const protocol = Protocol.make({
912
921
  from: fromRequest,
913
922
  },
914
923
  stream: {
915
- event: Protocol.jsonEvent(OpenAIChatEvent),
924
+ event: OpenAIChatStreamEvent,
916
925
  initial: (request) => ({
917
926
  providerMetadataKey: request.model.route.providerMetadataKey ?? String(request.model.provider),
918
927
  tools: ToolStream.empty(),
@@ -926,11 +935,13 @@ export const protocol = Protocol.make({
926
935
  nextToolIndex: 0,
927
936
  requireFinishReason: request.model.compatibility?.requireFinishReason ?? true,
928
937
  }),
929
- step,
938
+ step: (state, event) => (event === DONE ? Effect.succeed([state, []]) : step(state, event)),
939
+ terminal: (event) => event === DONE,
930
940
  onHalt: finishEvents,
931
941
  },
932
942
  });
933
- export const httpTransport = HttpTransport.sseJson.with();
943
+ export const framing = Framing.sseWithDone;
944
+ export const httpTransport = HttpTransport.sseJson.with().with({ framing });
934
945
  export const route = Route.make({
935
946
  id: ADAPTER,
936
947
  provider: "openai",
@@ -1,6 +1,5 @@
1
1
  import { Route } from "../route/client.js";
2
2
  import { Endpoint } from "../route/endpoint.js";
3
- import { Framing } from "../route/framing.js";
4
3
  import * as OpenAIChat from "./openai-chat.js";
5
4
  const ADAPTER = "openai-compatible-chat";
6
5
  /**
@@ -15,6 +14,6 @@ export const route = Route.make({
15
14
  providerMetadataKey: "openai",
16
15
  protocol: OpenAIChat.protocol,
17
16
  endpoint: Endpoint.path("/chat/completions"),
18
- framing: Framing.sse,
17
+ framing: OpenAIChat.framing,
19
18
  });
20
19
  export * as OpenAICompatibleChat from "./openai-compatible-chat.js";