@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.
- package/README.md +29 -11
- package/dist/image.js +5 -4
- package/dist/llm.d.ts +2 -0
- package/dist/llm.js +4 -7
- package/dist/protocols/anthropic-messages.js +7 -5
- package/dist/protocols/bedrock-converse.d.ts +1 -0
- package/dist/protocols/bedrock-converse.js +17 -7
- package/dist/protocols/bedrock-event-stream.js +16 -6
- package/dist/protocols/gemini.d.ts +1 -0
- package/dist/protocols/gemini.js +13 -1
- package/dist/protocols/google-images.js +8 -18
- package/dist/protocols/open-responses-channel.js +20 -9
- package/dist/protocols/open-responses-continuation.js +9 -8
- package/dist/protocols/open-responses.d.ts +9 -3
- package/dist/protocols/open-responses.js +139 -78
- package/dist/protocols/openai-chat.d.ts +3 -1
- package/dist/protocols/openai-chat.js +39 -28
- package/dist/protocols/openai-compatible-chat.js +1 -2
- package/dist/protocols/openai-images.js +11 -16
- package/dist/protocols/openai-responses.js +1 -1
- package/dist/protocols/shared.d.ts +11 -7
- package/dist/protocols/shared.js +31 -18
- package/dist/protocols/utils/image-input.d.ts +4 -4
- package/dist/protocols/utils/image-input.js +6 -8
- package/dist/protocols/utils/lifecycle.d.ts +6 -2
- package/dist/protocols/utils/lifecycle.js +11 -7
- package/dist/protocols/utils/tool-stream.d.ts +6 -0
- package/dist/protocols/xai-images.js +7 -12
- package/dist/protocols/zai-images.js +5 -10
- package/dist/provider-error.d.ts +4 -4
- package/dist/provider-error.js +39 -55
- package/dist/providers/groq.d.ts +1 -1
- package/dist/providers/groq.js +1 -2
- package/dist/providers/openrouter.d.ts +1 -1
- package/dist/providers/openrouter.js +1 -2
- package/dist/route/auth.js +3 -5
- package/dist/route/client.d.ts +2 -0
- package/dist/route/client.js +29 -11
- package/dist/route/executor.d.ts +9 -5
- package/dist/route/executor.js +33 -66
- package/dist/route/framing.d.ts +6 -2
- package/dist/route/framing.js +5 -0
- package/dist/route/transport/http.js +7 -2
- package/dist/route/transport/index.d.ts +3 -1
- package/dist/route/transport/websocket-channel.d.ts +2 -1
- package/dist/route/transport/websocket.d.ts +2 -1
- package/dist/route/transport/websocket.js +70 -30
- package/dist/schema/errors.d.ts +71 -89
- package/dist/schema/errors.js +35 -87
- package/dist/schema/events.d.ts +2835 -351
- package/dist/schema/events.js +32 -14
- package/dist/testing.d.ts +41 -2
- package/dist/testing.js +39 -18
- 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,
|
|
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 ||
|
|
610
|
+
if (!event.delta || state.message?.id !== id)
|
|
611
611
|
return [state, NO_EVENTS];
|
|
612
612
|
const events = [];
|
|
613
|
-
const phase = state.
|
|
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.
|
|
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
|
-
|
|
628
|
+
const startReasoningSummaryPart = (state, itemID, index) => {
|
|
629
629
|
const item = state.reasoningItems[itemID];
|
|
630
|
-
if (!
|
|
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.
|
|
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]: {
|
|
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
|
|
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
|
-
//
|
|
667
|
-
//
|
|
668
|
-
//
|
|
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
|
-
|
|
679
|
-
|
|
717
|
+
lifecycle,
|
|
718
|
+
message: {
|
|
719
|
+
id: itemID,
|
|
720
|
+
phase: phase === undefined && state.message?.id === itemID ? state.message.phase : phase,
|
|
721
|
+
},
|
|
680
722
|
},
|
|
681
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
825
|
-
|
|
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:
|
|
858
|
+
id: callID,
|
|
829
859
|
name: item.name,
|
|
830
|
-
providerMetadata:
|
|
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
|
|
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
|
-
|
|
859
|
-
|
|
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 [
|
|
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 = (
|
|
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
|
|
939
|
-
: classifyProviderFailure({ message,
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|
|
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*
|
|
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*
|
|
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
|
-
|
|
800
|
-
|
|
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*
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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:
|
|
17
|
+
framing: OpenAIChat.framing,
|
|
19
18
|
});
|
|
20
19
|
export * as OpenAICompatibleChat from "./openai-compatible-chat.js";
|