@opencode-ai/ai 0.0.0-beta-18414 → 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/llm.d.ts +2 -0
- package/dist/protocols/bedrock-converse.d.ts +1 -0
- package/dist/protocols/bedrock-converse.js +13 -1
- package/dist/protocols/open-responses.d.ts +8 -2
- package/dist/protocols/open-responses.js +132 -62
- package/dist/protocols/openai-chat.d.ts +3 -1
- package/dist/protocols/openai-chat.js +16 -10
- package/dist/protocols/openai-compatible-chat.js +1 -2
- package/dist/protocols/shared.d.ts +4 -4
- package/dist/protocols/shared.js +5 -5
- 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/provider-error.js +19 -34
- 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 +1 -1
- package/dist/route/client.d.ts +2 -0
- package/dist/route/framing.d.ts +4 -2
- package/dist/route/framing.js +5 -0
- package/dist/schema/errors.d.ts +4 -5
- package/dist/schema/errors.js +1 -4
- 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
package/README.md
CHANGED
|
@@ -214,22 +214,40 @@ the requests sent by code under test:
|
|
|
214
214
|
import { Effect } from "effect"
|
|
215
215
|
import { TestLLM } from "@opencode-ai/ai/testing"
|
|
216
216
|
|
|
217
|
-
const testLLM = TestLLM.layer({
|
|
218
|
-
fallback: TestLLM.text("Hello from the test model", "text-1"),
|
|
219
|
-
})
|
|
220
|
-
|
|
221
|
-
// TestLLM.clientLayer provides LLMClient.Service and consumes TestLLM.Service.
|
|
222
217
|
const programWithTestClient = Effect.gen(function* () {
|
|
218
|
+
const test = yield* TestLLM.Test
|
|
219
|
+
yield* test.push(TestLLM.text("Hello from the test model", "text-1"))
|
|
223
220
|
const result = yield* program
|
|
224
|
-
|
|
225
|
-
console.log(test.requests)
|
|
221
|
+
console.log(yield* test.requests())
|
|
226
222
|
return result
|
|
227
|
-
}).pipe(Effect.provide(TestLLM.
|
|
223
|
+
}).pipe(Effect.provide(TestLLM.testLayer()))
|
|
228
224
|
```
|
|
229
225
|
|
|
230
|
-
`
|
|
231
|
-
|
|
232
|
-
|
|
226
|
+
`testLayer()` provides the same object under `LLMClient.Service` and `TestLLM.Test`. Production consumes the
|
|
227
|
+
normal client; tests use the additional controls. Each layer build has fresh state.
|
|
228
|
+
|
|
229
|
+
- `test.push(...)` queues one-shot responses in execution order. Each argument is one response.
|
|
230
|
+
- `test.always(response)` installs a repeatable fallback. The layer's `fallback` option sets its initial value.
|
|
231
|
+
- `test.serve(request => response)` installs a request-dependent fallback. `always` and `serve` replace each
|
|
232
|
+
other without changing queued replies; queued replies take precedence.
|
|
233
|
+
- `test.requests()` returns an array snapshot. `transformRequest` changes only the recorded observation;
|
|
234
|
+
`serve` receives the original canonical request.
|
|
235
|
+
- `test.wait(count)` waits for request arrivals, not output or completion, and supports concurrent waiters.
|
|
236
|
+
- `test.gate()` returns a scoped gate with countable `started` notifications and a `release` Effect. Release
|
|
237
|
+
unblocks all requests captured by that gate; closing its scope also releases it. Effect-aware test runners
|
|
238
|
+
already provide Scope.
|
|
239
|
+
|
|
240
|
+
Constructing `stream()` or `generate()` does not record a request, invoke a responder, or consume a script.
|
|
241
|
+
Each execution does. An exhausted queue without a fallback defects immediately rather than waiting for a
|
|
242
|
+
future reply.
|
|
243
|
+
|
|
244
|
+
Responses remain canonical event arrays or arbitrary `Stream<LLMEvent, AIError>` values. The client consumes
|
|
245
|
+
supplied streams directly, preserving failure identity, finalizers, incomplete output, and post-finish tails;
|
|
246
|
+
it does not repair or truncate them.
|
|
247
|
+
|
|
248
|
+
The published legacy `Service`, `layer`, `clientLayer`, and module-level controls remain available as adapters
|
|
249
|
+
over the same implementation, including the legacy live `requests` array. New tests should use `Test` and
|
|
250
|
+
`testLayer`.
|
|
233
251
|
|
|
234
252
|
## Caching
|
|
235
253
|
|
package/dist/llm.d.ts
CHANGED
|
@@ -45,6 +45,7 @@ export declare class GenerateObjectResponse<T> {
|
|
|
45
45
|
} | {
|
|
46
46
|
readonly id: string;
|
|
47
47
|
readonly type: "text-end";
|
|
48
|
+
readonly text?: string | undefined;
|
|
48
49
|
readonly providerMetadata?: {
|
|
49
50
|
readonly [x: string]: {
|
|
50
51
|
readonly [x: string]: unknown;
|
|
@@ -70,6 +71,7 @@ export declare class GenerateObjectResponse<T> {
|
|
|
70
71
|
} | {
|
|
71
72
|
readonly id: string;
|
|
72
73
|
readonly type: "reasoning-end";
|
|
74
|
+
readonly text?: string | undefined;
|
|
73
75
|
readonly providerMetadata?: {
|
|
74
76
|
readonly [x: string]: {
|
|
75
77
|
readonly [x: string]: unknown;
|
|
@@ -129,6 +129,7 @@ export type BedrockConverseBody = Schema.Schema.Type<typeof BedrockConverseBody>
|
|
|
129
129
|
interface ParserState {
|
|
130
130
|
readonly providerMetadataKey: string;
|
|
131
131
|
readonly tools: ToolStream.State<number>;
|
|
132
|
+
readonly finishedTools: ReadonlySet<number>;
|
|
132
133
|
readonly pendingFinish: {
|
|
133
134
|
readonly reason: FinishReasonDetails;
|
|
134
135
|
readonly usage?: Usage;
|
|
@@ -285,6 +285,13 @@ const lowerMessages = Effect.fn("BedrockConverse.lowerMessages")(function* (requ
|
|
|
285
285
|
content.push({ reasoningContent: { redactedContent: redactedData } });
|
|
286
286
|
continue;
|
|
287
287
|
}
|
|
288
|
+
if (signature === undefined || signature.trim().length === 0) {
|
|
289
|
+
// Interrupted streams and model switches can leave unsigned reasoning.
|
|
290
|
+
// Preserve readable history as text rather than replay invalid reasoningContent.
|
|
291
|
+
if (part.text.trim().length > 0)
|
|
292
|
+
content.push(...textWithCache(breakpoints, part.text, part.cache));
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
288
295
|
content.push({ reasoningContent: { reasoningText: { text: part.text, signature } } });
|
|
289
296
|
continue;
|
|
290
297
|
}
|
|
@@ -293,7 +300,8 @@ const lowerMessages = Effect.fn("BedrockConverse.lowerMessages")(function* (requ
|
|
|
293
300
|
continue;
|
|
294
301
|
}
|
|
295
302
|
}
|
|
296
|
-
|
|
303
|
+
if (content.length > 0)
|
|
304
|
+
messages.push({ role: "assistant", content });
|
|
297
305
|
continue;
|
|
298
306
|
}
|
|
299
307
|
const content = [];
|
|
@@ -447,6 +455,8 @@ const step = (state, event) => Effect.gen(function* () {
|
|
|
447
455
|
}
|
|
448
456
|
if (event.contentBlockDelta?.delta?.toolUse) {
|
|
449
457
|
const index = event.contentBlockDelta.contentBlockIndex;
|
|
458
|
+
if (state.finishedTools.has(index))
|
|
459
|
+
return [state, []];
|
|
450
460
|
const result = ToolStream.appendExisting(ADAPTER, state.tools, index, event.contentBlockDelta.delta.toolUse.input, "Bedrock Converse tool delta is missing its tool call");
|
|
451
461
|
if (ToolStream.isError(result))
|
|
452
462
|
return yield* result;
|
|
@@ -473,6 +483,7 @@ const step = (state, event) => Effect.gen(function* () {
|
|
|
473
483
|
state.hasToolCalls,
|
|
474
484
|
lifecycle,
|
|
475
485
|
tools: result.tools,
|
|
486
|
+
finishedTools: resultEvents.length > 0 ? new Set([...state.finishedTools, index]) : state.finishedTools,
|
|
476
487
|
reasoningSignatures: Object.fromEntries(Object.entries(state.reasoningSignatures).filter(([key]) => key !== String(index))),
|
|
477
488
|
},
|
|
478
489
|
events,
|
|
@@ -552,6 +563,7 @@ export const protocol = Protocol.make({
|
|
|
552
563
|
initial: (request) => ({
|
|
553
564
|
providerMetadataKey: request.model.route.providerMetadataKey ?? String(request.model.provider),
|
|
554
565
|
tools: ToolStream.empty(),
|
|
566
|
+
finishedTools: new Set(),
|
|
555
567
|
pendingFinish: undefined,
|
|
556
568
|
hasToolCalls: false,
|
|
557
569
|
lifecycle: Lifecycle.initial(),
|
|
@@ -565,15 +565,19 @@ export interface ParserState {
|
|
|
565
565
|
readonly name: string;
|
|
566
566
|
readonly providerMetadataKey: string;
|
|
567
567
|
readonly tools: ToolStream.State<string>;
|
|
568
|
+
readonly completedTools: ReadonlySet<string>;
|
|
568
569
|
readonly hasFunctionCall: boolean;
|
|
569
570
|
readonly lifecycle: Lifecycle.State;
|
|
570
571
|
readonly outputItems: Readonly<Record<number, string>>;
|
|
571
|
-
readonly
|
|
572
|
-
|
|
572
|
+
readonly message: {
|
|
573
|
+
readonly id: string;
|
|
574
|
+
readonly phase: MessagePhase | null | undefined;
|
|
575
|
+
} | undefined;
|
|
573
576
|
readonly reasoningItems: Readonly<Record<string, ReasoningStreamItem>>;
|
|
574
577
|
}
|
|
575
578
|
type ReasoningSummaryStatus = "active" | "can-conclude" | "concluded";
|
|
576
579
|
interface ReasoningStreamItem {
|
|
580
|
+
readonly open: boolean;
|
|
577
581
|
readonly encryptedContent: string | null | undefined;
|
|
578
582
|
readonly summaryParts: Readonly<Record<number, ReasoningSummaryStatus>>;
|
|
579
583
|
readonly deltaIndexes: ReadonlySet<number>;
|
|
@@ -828,6 +832,7 @@ export declare const step: (state: ParserState, input: Event) => AIError | Effec
|
|
|
828
832
|
} | {
|
|
829
833
|
readonly id: string;
|
|
830
834
|
readonly type: "text-end";
|
|
835
|
+
readonly text?: string | undefined;
|
|
831
836
|
readonly providerMetadata?: {
|
|
832
837
|
readonly [x: string]: {
|
|
833
838
|
readonly [x: string]: unknown;
|
|
@@ -853,6 +858,7 @@ export declare const step: (state: ParserState, input: Event) => AIError | Effec
|
|
|
853
858
|
} | {
|
|
854
859
|
readonly id: string;
|
|
855
860
|
readonly type: "reasoning-end";
|
|
861
|
+
readonly text?: string | undefined;
|
|
856
862
|
readonly providerMetadata?: {
|
|
857
863
|
readonly [x: string]: {
|
|
858
864
|
readonly [x: string]: unknown;
|
|
@@ -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
|
})
|
|
@@ -979,6 +1044,11 @@ export const step = (state, input) => {
|
|
|
979
1044
|
if (event.type === "response.output_item.added") {
|
|
980
1045
|
if (event.item?.type === "message" && event.item.id === undefined)
|
|
981
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`);
|
|
982
1052
|
const id = event.item?.id ?? (event.item?.type === "function_call" ? event.item.call_id : undefined);
|
|
983
1053
|
return Effect.succeed(onOutputItemAdded(event.output_index !== undefined && id !== undefined
|
|
984
1054
|
? { ...state, outputItems: { ...state.outputItems, [event.output_index]: id } }
|
|
@@ -1014,10 +1084,10 @@ export const initial = (request, extension = BASE) => ({
|
|
|
1014
1084
|
providerMetadataKey: request.model.route.providerMetadataKey ?? "openresponses",
|
|
1015
1085
|
hasFunctionCall: false,
|
|
1016
1086
|
tools: ToolStream.empty(),
|
|
1087
|
+
completedTools: new Set(),
|
|
1017
1088
|
lifecycle: Lifecycle.initial(),
|
|
1018
1089
|
outputItems: {},
|
|
1019
|
-
|
|
1020
|
-
messagePhases: {},
|
|
1090
|
+
message: undefined,
|
|
1021
1091
|
reasoningItems: {},
|
|
1022
1092
|
});
|
|
1023
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,6 +3,7 @@ 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
9
|
import { AIError, AIErrorReason, InvalidProviderOutputError, LLMEvent, ProviderInternalError, UnknownProviderError, Usage, } from "../schema/index.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: {
|
|
@@ -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()) {
|
|
@@ -891,7 +893,9 @@ const finishEvents = Effect.fn("OpenAIChat.finishEvents")(function* (state) {
|
|
|
891
893
|
normalized: state.finishReason.normalized === "stop" && hasToolCalls ? "tool-calls" : state.finishReason.normalized,
|
|
892
894
|
}
|
|
893
895
|
: { normalized: hasToolCalls ? "tool-calls" : "stop" };
|
|
894
|
-
|
|
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);
|
|
895
899
|
const started = state.reasoningDetailsObserved && !state.reasoningEmitted
|
|
896
900
|
? Lifecycle.reasoningStart(state.lifecycle, events, "reasoning-0", reasoningMetadata(state.providerMetadataKey, state.reasoningField))
|
|
897
901
|
: state.lifecycle;
|
|
@@ -917,7 +921,7 @@ export const protocol = Protocol.make({
|
|
|
917
921
|
from: fromRequest,
|
|
918
922
|
},
|
|
919
923
|
stream: {
|
|
920
|
-
event:
|
|
924
|
+
event: OpenAIChatStreamEvent,
|
|
921
925
|
initial: (request) => ({
|
|
922
926
|
providerMetadataKey: request.model.route.providerMetadataKey ?? String(request.model.provider),
|
|
923
927
|
tools: ToolStream.empty(),
|
|
@@ -931,11 +935,13 @@ export const protocol = Protocol.make({
|
|
|
931
935
|
nextToolIndex: 0,
|
|
932
936
|
requireFinishReason: request.model.compatibility?.requireFinishReason ?? true,
|
|
933
937
|
}),
|
|
934
|
-
step,
|
|
938
|
+
step: (state, event) => (event === DONE ? Effect.succeed([state, []]) : step(state, event)),
|
|
939
|
+
terminal: (event) => event === DONE,
|
|
935
940
|
onHalt: finishEvents,
|
|
936
941
|
},
|
|
937
942
|
});
|
|
938
|
-
export const
|
|
943
|
+
export const framing = Framing.sseWithDone;
|
|
944
|
+
export const httpTransport = HttpTransport.sseJson.with().with({ framing });
|
|
939
945
|
export const route = Route.make({
|
|
940
946
|
id: ADAPTER,
|
|
941
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";
|
|
@@ -117,13 +117,13 @@ export declare const toolResultText: (part: ToolResultPart) => string;
|
|
|
117
117
|
export declare const errorText: (error: unknown) => string;
|
|
118
118
|
/**
|
|
119
119
|
* `framing` step for Server-Sent Events. Decodes UTF-8, runs the SSE channel
|
|
120
|
-
* decoder, optionally filters named events, and drops empty
|
|
121
|
-
*
|
|
122
|
-
*
|
|
120
|
+
* decoder, optionally filters named events, and drops empty events. `[DONE]`
|
|
121
|
+
* is dropped by default or retained for protocols that use it as their stream
|
|
122
|
+
* boundary. Retry control events are ignored without interrupting the stream.
|
|
123
123
|
* Decoder failures become provider output errors so the public error channel
|
|
124
124
|
* stays `AIError`.
|
|
125
125
|
*/
|
|
126
|
-
export declare const sseFraming: (bytes: Stream.Stream<Uint8Array, AIError>, events?: ReadonlySet<string
|
|
126
|
+
export declare const sseFraming: (bytes: Stream.Stream<Uint8Array, AIError>, events?: ReadonlySet<string>, includeDone?: boolean) => Stream.Stream<string, AIError>;
|
|
127
127
|
/**
|
|
128
128
|
* Canonical invalid-request constructor shared by protocol lowering.
|
|
129
129
|
*/
|