@opencode-ai/ai 0.0.0-beta-18148 → 0.0.0-beta-18219
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/dist/image-client.d.ts +1 -1
- package/dist/image-client.js +10 -1
- package/dist/protocols/anthropic-messages.d.ts +2 -21
- package/dist/protocols/anthropic-messages.js +55 -10
- package/dist/protocols/bedrock-converse.d.ts +16 -50
- package/dist/protocols/bedrock-converse.js +5 -16
- package/dist/protocols/bedrock-event-stream.js +1 -1
- package/dist/protocols/gemini.js +1 -1
- package/dist/protocols/open-responses-continuation.js +6 -1
- package/dist/protocols/open-responses.d.ts +25 -2
- package/dist/protocols/open-responses.js +49 -20
- package/dist/protocols/openai-chat.d.ts +7 -4
- package/dist/protocols/openai-chat.js +72 -29
- package/dist/protocols/openai-compatible-responses.js +1 -0
- package/dist/protocols/openai-responses.d.ts +11 -0
- package/dist/protocols/openai-responses.js +2 -2
- package/dist/protocols/shared.d.ts +1 -1
- package/dist/protocols/shared.js +4 -4
- package/dist/protocols/utils/partial-json.js +57 -2
- package/dist/protocols/utils/tool-stream.d.ts +1 -1
- package/dist/protocols/utils/tool-stream.js +15 -21
- package/dist/protocols/xai-responses.d.ts +11 -0
- package/dist/providers/openrouter.d.ts +2 -0
- package/dist/providers/openrouter.js +1 -3
- package/dist/providers/xai.js +1 -1
- package/dist/route/client.js +14 -2
- package/dist/route/protocol.d.ts +2 -2
- package/dist/schema/options.d.ts +1 -0
- package/dist/schema/options.js +1 -0
- package/dist/utils/sanitize.d.ts +1 -0
- package/dist/utils/sanitize.js +12 -0
- package/package.json +3 -3
|
@@ -5,7 +5,7 @@ import { Auth } from "../route/auth.js";
|
|
|
5
5
|
import { Endpoint } from "../route/endpoint.js";
|
|
6
6
|
import { HttpTransport } from "../route/transport/index.js";
|
|
7
7
|
import { Protocol } from "../route/protocol.js";
|
|
8
|
-
import { AIError, LLMEvent, Usage, } from "../schema/index.js";
|
|
8
|
+
import { AIError, InvalidProviderOutputReason, LLMEvent, ProviderInternalReason, UnknownProviderReason, Usage, } from "../schema/index.js";
|
|
9
9
|
import { classifyProviderFailure } from "../provider-error.js";
|
|
10
10
|
import { isRecord, JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared.js";
|
|
11
11
|
import { OpenAIOptions } from "./utils/openai-options.js";
|
|
@@ -167,15 +167,15 @@ const OpenAIChatChoice = Schema.StructWithRest(Schema.Struct({
|
|
|
167
167
|
// Moonshot streams usage on `choice.usage` instead of top-level `usage`.
|
|
168
168
|
usage: optionalNull(OpenAIChatUsage),
|
|
169
169
|
}), [Schema.Record(Schema.String, Schema.Unknown)]);
|
|
170
|
-
const OpenAIChatError = Schema.Struct({
|
|
170
|
+
const OpenAIChatError = Schema.StructWithRest(Schema.Struct({
|
|
171
171
|
code: optionalNull(Schema.Union([Schema.String, Schema.Number])),
|
|
172
172
|
message: Schema.String,
|
|
173
|
-
});
|
|
174
|
-
export const OpenAIChatEvent = Schema.Struct({
|
|
173
|
+
}), [Schema.Record(Schema.String, Schema.Unknown)]);
|
|
174
|
+
export const OpenAIChatEvent = Schema.StructWithRest(Schema.Struct({
|
|
175
175
|
choices: optionalNull(Schema.Array(OpenAIChatChoice)),
|
|
176
176
|
usage: optionalNull(OpenAIChatUsage),
|
|
177
177
|
error: optionalNull(OpenAIChatError),
|
|
178
|
-
});
|
|
178
|
+
}), [Schema.Record(Schema.String, Schema.Unknown)]);
|
|
179
179
|
const lowerTool = (tool, inputSchema, options, supportsStrictMode) => ({
|
|
180
180
|
type: "function",
|
|
181
181
|
function: {
|
|
@@ -349,13 +349,22 @@ const lowerMessages = Effect.fn("OpenAIChat.lowerMessages")(function* (request,
|
|
|
349
349
|
]
|
|
350
350
|
: [{ role: "system", content: ProviderShared.joinText(request.system) }];
|
|
351
351
|
const messages = [...system];
|
|
352
|
+
const requireAssistantAfterTool = request.model.compatibility?.requireAssistantAfterTool ??
|
|
353
|
+
["mistral", "devstral", "codestral", "pixtral", "mixtral"].some((family) => request.model.id.toLowerCase().includes(family));
|
|
354
|
+
const bridgeTools = () => {
|
|
355
|
+
if (requireAssistantAfterTool && messages.at(-1)?.role === "tool")
|
|
356
|
+
messages.push({ role: "assistant", content: "Done." });
|
|
357
|
+
};
|
|
352
358
|
const pendingImages = [];
|
|
353
359
|
const flushImages = () => {
|
|
354
360
|
if (pendingImages.length === 0)
|
|
355
361
|
return;
|
|
362
|
+
bridgeTools();
|
|
356
363
|
messages.push({ role: "user", content: pendingImages.splice(0) });
|
|
357
364
|
};
|
|
358
365
|
for (const message of request.messages) {
|
|
366
|
+
if (message.role === "user")
|
|
367
|
+
bridgeTools();
|
|
359
368
|
if (message.role === "system") {
|
|
360
369
|
const part = yield* ProviderShared.wrappedSystemUpdate("OpenAI Chat", message);
|
|
361
370
|
if (pendingImages.length > 0) {
|
|
@@ -396,6 +405,8 @@ const lowerMessages = Effect.fn("OpenAIChat.lowerMessages")(function* (request,
|
|
|
396
405
|
: { role: "user", content: part.text });
|
|
397
406
|
continue;
|
|
398
407
|
}
|
|
408
|
+
if (message.role === "assistant" && message.content.every((part) => part.type === "text" && part.text.trim() === ""))
|
|
409
|
+
continue;
|
|
399
410
|
if (message.role === "tool") {
|
|
400
411
|
const lowered = yield* lowerToolMessages(message, options);
|
|
401
412
|
messages.push(...lowered.messages);
|
|
@@ -517,7 +528,7 @@ const detectZaiToolStream = (provider, baseURL, modelID) => {
|
|
|
517
528
|
};
|
|
518
529
|
const lowerOptions = (request, supportsStore) => {
|
|
519
530
|
const options = OpenAIOptions.resolve(request);
|
|
520
|
-
const cacheKey = ProviderShared.
|
|
531
|
+
const cacheKey = ProviderShared.promptCacheKey(request);
|
|
521
532
|
return {
|
|
522
533
|
...(supportsStore && options.store !== undefined ? { store: options.store } : {}),
|
|
523
534
|
// For providers that support `store`, ensure stateless `store:false` is sent
|
|
@@ -577,19 +588,32 @@ export const fromRequest = Effect.fn("OpenAIChat.fromRequest")(function* (reques
|
|
|
577
588
|
// Streaming parsers are small state machines: every event returns a new state
|
|
578
589
|
// plus the common `LLMEvent`s produced by that event. Tool calls are accumulated
|
|
579
590
|
// because OpenAI streams JSON arguments across multiple deltas.
|
|
580
|
-
const
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
591
|
+
const finishReasonError = (event, reason) => new AIError({
|
|
592
|
+
module: ADAPTER,
|
|
593
|
+
method: "stream",
|
|
594
|
+
body: ProviderShared.encodeJson(event),
|
|
595
|
+
reason,
|
|
596
|
+
});
|
|
597
|
+
const mapFinishReason = Effect.fn("OpenAIChat.mapFinishReason")(function* (event, reason) {
|
|
598
|
+
switch (reason) {
|
|
599
|
+
case "error":
|
|
600
|
+
return yield* finishReasonError(event, new UnknownProviderReason({ message: "Provider reported an error (finish_reason: error)" }));
|
|
601
|
+
case "network_error":
|
|
602
|
+
return yield* finishReasonError(event, new ProviderInternalReason({ message: "Provider reported a network error (finish_reason: network_error)" }));
|
|
603
|
+
case "stop":
|
|
604
|
+
case "end":
|
|
605
|
+
return "stop";
|
|
606
|
+
case "length":
|
|
607
|
+
return "length";
|
|
608
|
+
case "content_filter":
|
|
609
|
+
return "content-filter";
|
|
610
|
+
case "function_call":
|
|
611
|
+
case "tool_calls":
|
|
612
|
+
return "tool-calls";
|
|
613
|
+
default:
|
|
614
|
+
return "unknown";
|
|
615
|
+
}
|
|
616
|
+
});
|
|
593
617
|
// OpenAI Chat reports `prompt_tokens` (inclusive total) with a
|
|
594
618
|
// cached-read and cache-write subsets, and `completion_tokens` (inclusive
|
|
595
619
|
// total) with a `reasoning_tokens` subset. We pass the inclusive totals
|
|
@@ -686,16 +710,20 @@ const reasoningMetadata = (field, details) => ({
|
|
|
686
710
|
},
|
|
687
711
|
});
|
|
688
712
|
const step = (state, event) => Effect.gen(function* () {
|
|
689
|
-
if (event.error)
|
|
713
|
+
if (event.error) {
|
|
714
|
+
const body = ProviderShared.encodeJson(event);
|
|
690
715
|
return yield* new AIError({
|
|
691
716
|
module: ADAPTER,
|
|
692
717
|
method: "stream",
|
|
718
|
+
body,
|
|
693
719
|
reason: classifyProviderFailure({
|
|
694
720
|
message: event.error.message,
|
|
695
721
|
code: event.error.code === undefined || event.error.code === null ? undefined : String(event.error.code),
|
|
696
722
|
status: typeof event.error.code === "number" ? event.error.code : undefined,
|
|
723
|
+
rawBody: body,
|
|
697
724
|
}),
|
|
698
725
|
});
|
|
726
|
+
}
|
|
699
727
|
const events = [];
|
|
700
728
|
const choice = event.choices?.[0];
|
|
701
729
|
// Moonshot (and a few other OpenAI-compatible providers) attach usage to
|
|
@@ -703,8 +731,11 @@ const step = (state, event) => Effect.gen(function* () {
|
|
|
703
731
|
const choiceUsage = choice?.usage;
|
|
704
732
|
const usage = mapUsage(event.usage) ?? (choiceUsage ? mapUsage(choiceUsage) : undefined) ?? state.usage;
|
|
705
733
|
const rawFinishReason = choice?.finish_reason;
|
|
706
|
-
const finishReason = rawFinishReason
|
|
707
|
-
? {
|
|
734
|
+
const finishReason = rawFinishReason
|
|
735
|
+
? {
|
|
736
|
+
normalized: yield* mapFinishReason(event, rawFinishReason),
|
|
737
|
+
raw: choice?.native_finish_reason ?? rawFinishReason,
|
|
738
|
+
}
|
|
708
739
|
: state.finishReason;
|
|
709
740
|
const delta = choice?.delta;
|
|
710
741
|
const toolDeltas = delta?.tool_calls ?? [];
|
|
@@ -721,7 +752,7 @@ const step = (state, event) => Effect.gen(function* () {
|
|
|
721
752
|
toolDeltas.some((tool) => Boolean(tool.id) || Boolean(tool.function?.name) || Boolean(tool.function?.arguments));
|
|
722
753
|
if (state.finishReason !== undefined) {
|
|
723
754
|
if (hasLateContent)
|
|
724
|
-
return yield* ProviderShared.eventError(ADAPTER, "OpenAI Chat received content after the finish reason");
|
|
755
|
+
return yield* ProviderShared.eventError(ADAPTER, "OpenAI Chat received content after the finish reason", ProviderShared.encodeJson(event));
|
|
725
756
|
return [{ ...state, usage }, events];
|
|
726
757
|
}
|
|
727
758
|
const reasoningField = state.reasoningField ?? reasoning?.field;
|
|
@@ -773,14 +804,14 @@ const step = (state, event) => Effect.gen(function* () {
|
|
|
773
804
|
}
|
|
774
805
|
const result = ToolStream.appendOrStart(ADAPTER, tools, index, { id: id || undefined, name: name || undefined, text }, "OpenAI Chat tool call delta is missing id or name");
|
|
775
806
|
if (ToolStream.isError(result))
|
|
776
|
-
return yield* result;
|
|
807
|
+
return yield* ProviderShared.eventError(ADAPTER, result.reason.message, ProviderShared.encodeJson(event));
|
|
777
808
|
tools = result.tools;
|
|
778
809
|
if (result.events.length)
|
|
779
810
|
lifecycle = Lifecycle.stepStart(lifecycle, events);
|
|
780
811
|
events.push(...result.events);
|
|
781
812
|
}
|
|
782
813
|
if (finishReason !== undefined && state.finishReason === undefined && Object.keys(pendingTools).length > 0)
|
|
783
|
-
return yield* ProviderShared.eventError(ADAPTER, "OpenAI Chat tool call delta is missing id or name");
|
|
814
|
+
return yield* ProviderShared.eventError(ADAPTER, "OpenAI Chat tool call delta is missing id or name", ProviderShared.encodeJson(event));
|
|
784
815
|
// Finalize accumulated tool inputs eagerly when finish_reason arrives so
|
|
785
816
|
// valid calls and malformed local calls settle independently.
|
|
786
817
|
const finished = finishReason !== undefined && state.finishReason === undefined && Object.keys(tools).length > 0
|
|
@@ -800,14 +831,25 @@ const step = (state, event) => Effect.gen(function* () {
|
|
|
800
831
|
reasoningEmitted,
|
|
801
832
|
latestToolIndex,
|
|
802
833
|
nextToolIndex,
|
|
834
|
+
requireFinishReason: state.requireFinishReason,
|
|
803
835
|
},
|
|
804
836
|
events,
|
|
805
837
|
];
|
|
806
838
|
});
|
|
807
|
-
const finishEvents = (state)
|
|
839
|
+
const finishEvents = Effect.fn("OpenAIChat.finishEvents")(function* (state) {
|
|
840
|
+
if (state.finishReason === undefined && state.requireFinishReason)
|
|
841
|
+
return yield* new AIError({
|
|
842
|
+
module: ADAPTER,
|
|
843
|
+
method: "stream",
|
|
844
|
+
reason: new InvalidProviderOutputReason({
|
|
845
|
+
classification: "incomplete-stream",
|
|
846
|
+
message: "OpenAI Chat stream ended without finish_reason",
|
|
847
|
+
route: ADAPTER,
|
|
848
|
+
}),
|
|
849
|
+
});
|
|
808
850
|
const events = [];
|
|
809
851
|
const toolCallEvents = state.finishReason === undefined && Object.keys(state.tools).length > 0
|
|
810
|
-
?
|
|
852
|
+
? (yield* ToolStream.finishAll(ADAPTER, state.tools)).events
|
|
811
853
|
: state.toolCallEvents;
|
|
812
854
|
const hasToolCalls = toolCallEvents.length > 0;
|
|
813
855
|
const reason = state.finishReason
|
|
@@ -815,7 +857,7 @@ const finishEvents = (state) => {
|
|
|
815
857
|
...state.finishReason,
|
|
816
858
|
normalized: state.finishReason.normalized === "stop" && hasToolCalls ? "tool-calls" : state.finishReason.normalized,
|
|
817
859
|
}
|
|
818
|
-
: { normalized: hasToolCalls ? "tool-calls" : "
|
|
860
|
+
: { normalized: hasToolCalls ? "tool-calls" : "stop" };
|
|
819
861
|
const metadata = reasoningMetadata(state.reasoningField, state.reasoningDetailsObserved ? state.reasoningDetails : undefined);
|
|
820
862
|
const started = state.reasoningDetailsObserved && !state.reasoningEmitted
|
|
821
863
|
? Lifecycle.reasoningStart(state.lifecycle, events, "reasoning-0", reasoningMetadata(state.reasoningField))
|
|
@@ -825,7 +867,7 @@ const finishEvents = (state) => {
|
|
|
825
867
|
events.push(...toolCallEvents);
|
|
826
868
|
Lifecycle.finish(lifecycle, events, { reason, usage: state.usage });
|
|
827
869
|
return events;
|
|
828
|
-
};
|
|
870
|
+
});
|
|
829
871
|
// =============================================================================
|
|
830
872
|
// Protocol And OpenAI Route
|
|
831
873
|
// =============================================================================
|
|
@@ -853,6 +895,7 @@ export const protocol = Protocol.make({
|
|
|
853
895
|
reasoningDetailsObserved: false,
|
|
854
896
|
reasoningEmitted: false,
|
|
855
897
|
nextToolIndex: 0,
|
|
898
|
+
requireFinishReason: request.model.compatibility?.requireFinishReason ?? true,
|
|
856
899
|
}),
|
|
857
900
|
step,
|
|
858
901
|
onHalt: finishEvents,
|
|
@@ -13,5 +13,6 @@ export const route = Route.make({
|
|
|
13
13
|
protocol: OpenResponses.protocol,
|
|
14
14
|
endpoint: Endpoint.path(OpenResponses.PATH),
|
|
15
15
|
transport: OpenResponses.httpTransport,
|
|
16
|
+
defaults: { providerOptions: { store: false, include: ["reasoning.encrypted_content"] } },
|
|
16
17
|
});
|
|
17
18
|
export * as OpenAICompatibleResponses from "./openai-compatible-responses.js";
|
|
@@ -286,6 +286,15 @@ export declare const protocol: Protocol<{
|
|
|
286
286
|
readonly response?: {
|
|
287
287
|
readonly [x: string]: unknown;
|
|
288
288
|
readonly id?: string | undefined;
|
|
289
|
+
readonly output?: readonly {
|
|
290
|
+
readonly [x: string]: unknown;
|
|
291
|
+
readonly type: string;
|
|
292
|
+
readonly id?: string | undefined;
|
|
293
|
+
readonly name?: string | undefined;
|
|
294
|
+
readonly arguments?: string | undefined;
|
|
295
|
+
readonly encrypted_content?: string | null | undefined;
|
|
296
|
+
readonly call_id?: string | undefined;
|
|
297
|
+
}[] | undefined;
|
|
289
298
|
readonly error?: {
|
|
290
299
|
readonly type?: string | null | undefined;
|
|
291
300
|
readonly code?: string | null | undefined;
|
|
@@ -309,9 +318,11 @@ export declare const protocol: Protocol<{
|
|
|
309
318
|
readonly reason?: string | undefined;
|
|
310
319
|
} | null | undefined;
|
|
311
320
|
} | undefined;
|
|
321
|
+
readonly arguments?: string | undefined;
|
|
312
322
|
readonly param?: string | null | undefined;
|
|
313
323
|
readonly status_code?: unknown;
|
|
314
324
|
readonly item_id?: string | undefined;
|
|
325
|
+
readonly output_index?: number | undefined;
|
|
315
326
|
readonly summary_index?: number | undefined;
|
|
316
327
|
}, OpenResponses.ParserState>;
|
|
317
328
|
export declare const httpTransport: HttpTransport.HttpJsonTransport<{
|
|
@@ -139,7 +139,7 @@ const HOSTED_TOOLS = {
|
|
|
139
139
|
const step = (state, event) => {
|
|
140
140
|
if (event.type === "response.reasoning_text.delta")
|
|
141
141
|
return event.item_id
|
|
142
|
-
? Effect.succeed(OpenResponses.onReasoningDelta(state, event, event.item_id))
|
|
142
|
+
? Effect.succeed(OpenResponses.onReasoningDelta(state, event, OpenResponses.outputItemID(state, event) ?? event.item_id))
|
|
143
143
|
: ProviderShared.eventError(ADAPTER, `${event.type} is missing item_id`);
|
|
144
144
|
if (event.type === "response.output_item.done" && event.item && ResponsesHostedTools.isItem(event.item, HOSTED_TOOLS))
|
|
145
145
|
return ResponsesHostedTools.onDone(state, event.item, HOSTED_TOOLS);
|
|
@@ -176,6 +176,6 @@ export const route = Route.make({
|
|
|
176
176
|
endpoint,
|
|
177
177
|
auth,
|
|
178
178
|
transport,
|
|
179
|
-
defaults: { providerOptions: { store: false } },
|
|
179
|
+
defaults: { providerOptions: { store: false, include: ["reasoning.encrypted_content"] } },
|
|
180
180
|
});
|
|
181
181
|
export * as OpenAIResponses from "./openai-responses.js";
|
|
@@ -11,7 +11,7 @@ export declare const JsonObject: Schema.$Record<Schema.String, Schema.Unknown>;
|
|
|
11
11
|
export declare const optionalArray: <const S extends Schema.Top>(schema: S) => Schema.optional<Schema.$Array<S>>;
|
|
12
12
|
export declare const optionalNull: <const S extends Schema.Top>(schema: S) => Schema.optional<Schema.NullOr<S>>;
|
|
13
13
|
export declare const OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH = 64;
|
|
14
|
-
export declare const
|
|
14
|
+
export declare const promptCacheKey: (request: LLMRequest) => string | undefined;
|
|
15
15
|
/**
|
|
16
16
|
* Streaming tool-call accumulator. Adapters that build a tool call across
|
|
17
17
|
* multiple `tool-input-delta` chunks store the partial JSON input string here
|
package/dist/protocols/shared.js
CHANGED
|
@@ -16,12 +16,12 @@ export const optionalNull = (schema) => Schema.optional(Schema.NullOr(schema));
|
|
|
16
16
|
export const OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH = 64;
|
|
17
17
|
// OpenAI limits `prompt_cache_key` to 64 chars; DeepSeek and Zai inherit the same
|
|
18
18
|
// limit via their OpenAI-compatible APIs. Clamp with unicode-aware slicing.
|
|
19
|
-
export const
|
|
20
|
-
if (
|
|
19
|
+
export const promptCacheKey = (request) => {
|
|
20
|
+
if (request.cache === "none" || request.promptCacheKey === undefined)
|
|
21
21
|
return undefined;
|
|
22
|
-
const chars = Array.from(
|
|
22
|
+
const chars = Array.from(request.promptCacheKey);
|
|
23
23
|
if (chars.length <= OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH)
|
|
24
|
-
return
|
|
24
|
+
return request.promptCacheKey;
|
|
25
25
|
return chars.slice(0, OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH).join("");
|
|
26
26
|
};
|
|
27
27
|
/**
|
|
@@ -43,8 +43,58 @@ export function parseJSON(jsonString, allowPartial = Allow.ALL) {
|
|
|
43
43
|
return decodeJson(input);
|
|
44
44
|
}
|
|
45
45
|
catch { }
|
|
46
|
-
|
|
46
|
+
const repaired = repairJSON(input);
|
|
47
|
+
if (repaired !== input) {
|
|
48
|
+
try {
|
|
49
|
+
return decodeJson(repaired);
|
|
50
|
+
}
|
|
51
|
+
catch { }
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
return _parseJSON(input, allowPartial);
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
if (repaired !== input)
|
|
58
|
+
return _parseJSON(repaired, allowPartial);
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
47
61
|
}
|
|
62
|
+
const repairJSON = (input) => {
|
|
63
|
+
let repaired = "";
|
|
64
|
+
let quoted = false;
|
|
65
|
+
for (let index = 0; index < input.length; index++) {
|
|
66
|
+
const character = input[index];
|
|
67
|
+
if (!quoted) {
|
|
68
|
+
repaired += character;
|
|
69
|
+
if (character === '"')
|
|
70
|
+
quoted = true;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (character === '"') {
|
|
74
|
+
repaired += character;
|
|
75
|
+
quoted = false;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (character === "\\") {
|
|
79
|
+
const next = input[index + 1];
|
|
80
|
+
if (next === "u" && /^[0-9a-fA-F]{4}$/.test(input.slice(index + 2, index + 6))) {
|
|
81
|
+
repaired += input.slice(index, index + 6);
|
|
82
|
+
index += 5;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (next !== undefined && '"\\/bfnrtu'.includes(next)) {
|
|
86
|
+
repaired += `\\${next}`;
|
|
87
|
+
index++;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
repaired += "\\\\";
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const code = character.charCodeAt(0);
|
|
94
|
+
repaired += code <= 0x1f ? `\\u${code.toString(16).padStart(4, "0")}` : character;
|
|
95
|
+
}
|
|
96
|
+
return repaired;
|
|
97
|
+
};
|
|
48
98
|
const _parseJSON = (jsonString, allow) => {
|
|
49
99
|
const length = jsonString.length;
|
|
50
100
|
let index = 0;
|
|
@@ -138,7 +188,12 @@ const _parseJSON = (jsonString, allow) => {
|
|
|
138
188
|
skipBlank();
|
|
139
189
|
index++;
|
|
140
190
|
try {
|
|
141
|
-
object
|
|
191
|
+
Object.defineProperty(object, key, {
|
|
192
|
+
value: parseAny(),
|
|
193
|
+
enumerable: true,
|
|
194
|
+
configurable: true,
|
|
195
|
+
writable: true,
|
|
196
|
+
});
|
|
142
197
|
}
|
|
143
198
|
catch (error) {
|
|
144
199
|
if (Allow.OBJ & allow)
|
|
@@ -61,7 +61,7 @@ export declare const appendOrStart: <K extends StreamKey>(route: string, tools:
|
|
|
61
61
|
export declare const appendExisting: <K extends StreamKey>(route: string, tools: State<K>, key: K, text: string, missingToolMessage: string) => AppendOutcome<K> | AIError;
|
|
62
62
|
/**
|
|
63
63
|
* Finalize one pending tool call: parse the accumulated raw JSON, remove it
|
|
64
|
-
* from state, and
|
|
64
|
+
* from state, and recover incomplete local arguments when needed.
|
|
65
65
|
* Missing keys are a no-op because some providers emit stop events for
|
|
66
66
|
* non-tool content blocks.
|
|
67
67
|
*/
|
|
@@ -19,34 +19,28 @@ const inputStart = (tool) => LLMEvent.toolInputStart({
|
|
|
19
19
|
providerExecuted: tool.providerExecuted ? true : undefined,
|
|
20
20
|
providerMetadata: tool.providerMetadata,
|
|
21
21
|
});
|
|
22
|
-
const inputDelta = (tool, text) => {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
...(Option.isSome(input) ? { input: input.value } : {}),
|
|
29
|
-
});
|
|
30
|
-
};
|
|
22
|
+
const inputDelta = (tool, text) => LLMEvent.toolInputDelta({
|
|
23
|
+
id: tool.id,
|
|
24
|
+
name: tool.name,
|
|
25
|
+
text,
|
|
26
|
+
input: Option.getOrElse(parsePartialInput(tool.input), () => ({})),
|
|
27
|
+
});
|
|
31
28
|
const toolCall = (route, tool, inputOverride) => {
|
|
32
29
|
const raw = inputOverride ?? tool.input;
|
|
33
|
-
return parseToolInput(route, tool.name, raw).pipe(Effect.
|
|
30
|
+
return parseToolInput(route, tool.name, raw).pipe(Effect.catch((error) => tool.providerExecuted
|
|
31
|
+
? Effect.fail(error)
|
|
32
|
+
: Effect.succeed(Option.getOrElse(Option.map(parsePartialInput(raw), (input) => input ?? {}), () => ({})))), Effect.map((input) => LLMEvent.toolCall({
|
|
34
33
|
id: tool.id,
|
|
35
34
|
name: tool.name,
|
|
36
35
|
input,
|
|
37
36
|
providerExecuted: tool.providerExecuted ? true : undefined,
|
|
38
37
|
providerMetadata: tool.providerMetadata,
|
|
39
|
-
}))
|
|
40
|
-
? Effect.fail(error)
|
|
41
|
-
: Effect.succeed(LLMEvent.toolInputError({
|
|
42
|
-
id: tool.id,
|
|
43
|
-
name: tool.name,
|
|
44
|
-
raw,
|
|
45
|
-
}))));
|
|
38
|
+
})));
|
|
46
39
|
};
|
|
47
|
-
const finishEvents = (tool, event) =>
|
|
48
|
-
|
|
49
|
-
|
|
40
|
+
const finishEvents = (tool, event) => [
|
|
41
|
+
LLMEvent.toolInputEnd({ id: tool.id, name: tool.name, providerMetadata: tool.providerMetadata }),
|
|
42
|
+
event,
|
|
43
|
+
];
|
|
50
44
|
/** Store the updated tool and produce the optional public delta event. */
|
|
51
45
|
const appendTool = (tools, key, tool, text) => {
|
|
52
46
|
const events = [];
|
|
@@ -105,7 +99,7 @@ export const appendExisting = (route, tools, key, text, missingToolMessage) => {
|
|
|
105
99
|
};
|
|
106
100
|
/**
|
|
107
101
|
* Finalize one pending tool call: parse the accumulated raw JSON, remove it
|
|
108
|
-
* from state, and
|
|
102
|
+
* from state, and recover incomplete local arguments when needed.
|
|
109
103
|
* Missing keys are a no-op because some providers emit stop events for
|
|
110
104
|
* non-tool content blocks.
|
|
111
105
|
*/
|
|
@@ -143,6 +143,15 @@ export declare const protocol: Protocol<{
|
|
|
143
143
|
readonly response?: {
|
|
144
144
|
readonly [x: string]: unknown;
|
|
145
145
|
readonly id?: string | undefined;
|
|
146
|
+
readonly output?: readonly {
|
|
147
|
+
readonly [x: string]: unknown;
|
|
148
|
+
readonly type: string;
|
|
149
|
+
readonly id?: string | undefined;
|
|
150
|
+
readonly name?: string | undefined;
|
|
151
|
+
readonly arguments?: string | undefined;
|
|
152
|
+
readonly encrypted_content?: string | null | undefined;
|
|
153
|
+
readonly call_id?: string | undefined;
|
|
154
|
+
}[] | undefined;
|
|
146
155
|
readonly error?: {
|
|
147
156
|
readonly type?: string | null | undefined;
|
|
148
157
|
readonly code?: string | null | undefined;
|
|
@@ -166,9 +175,11 @@ export declare const protocol: Protocol<{
|
|
|
166
175
|
readonly reason?: string | undefined;
|
|
167
176
|
} | null | undefined;
|
|
168
177
|
} | undefined;
|
|
178
|
+
readonly arguments?: string | undefined;
|
|
169
179
|
readonly param?: string | null | undefined;
|
|
170
180
|
readonly status_code?: unknown;
|
|
171
181
|
readonly item_id?: string | undefined;
|
|
182
|
+
readonly output_index?: number | undefined;
|
|
172
183
|
readonly summary_index?: number | undefined;
|
|
173
184
|
}, OpenResponses.ParserState>;
|
|
174
185
|
export * as XAIResponses from "./xai-responses.js";
|
|
@@ -283,7 +283,9 @@ export declare const protocol: Protocol<{
|
|
|
283
283
|
readonly frequency_penalty?: number | undefined;
|
|
284
284
|
readonly presence_penalty?: number | undefined;
|
|
285
285
|
}, string, {
|
|
286
|
+
readonly [x: string]: unknown;
|
|
286
287
|
readonly error?: {
|
|
288
|
+
readonly [x: string]: unknown;
|
|
287
289
|
readonly message: string;
|
|
288
290
|
readonly code?: string | number | null | undefined;
|
|
289
291
|
} | null | undefined;
|
|
@@ -8,7 +8,7 @@ import { ProviderID } from "../schema/index.js";
|
|
|
8
8
|
import * as OpenAICompatibleProfiles from "./openai-compatible-profile.js";
|
|
9
9
|
import * as OpenAIChat from "../protocols/openai-chat.js";
|
|
10
10
|
import { newBreakpoints, ttlBucket } from "../protocols/utils/cache.js";
|
|
11
|
-
import { isRecord
|
|
11
|
+
import { isRecord } from "../protocols/shared.js";
|
|
12
12
|
export const profile = OpenAICompatibleProfiles.profiles.openrouter;
|
|
13
13
|
export const id = ProviderID.make(profile.provider);
|
|
14
14
|
const ADAPTER = "openrouter";
|
|
@@ -39,12 +39,10 @@ export const protocol = Protocol.make({
|
|
|
39
39
|
reasoning_details: reasoningDetails,
|
|
40
40
|
};
|
|
41
41
|
});
|
|
42
|
-
const cacheKey = ProviderShared.clampPromptCacheKey(request.promptCacheKey);
|
|
43
42
|
return {
|
|
44
43
|
...body,
|
|
45
44
|
messages,
|
|
46
45
|
...bodyOptions(request.providerOptions),
|
|
47
|
-
...(cacheKey ? { prompt_cache_key: cacheKey } : {}),
|
|
48
46
|
};
|
|
49
47
|
})),
|
|
50
48
|
},
|
package/dist/providers/xai.js
CHANGED
|
@@ -21,7 +21,7 @@ const responsesRoute = Route.make({
|
|
|
21
21
|
name: "xAI Responses",
|
|
22
22
|
rotateAfterMs: RESPONSES_WEBSOCKET_ROTATE_AFTER_MS,
|
|
23
23
|
}),
|
|
24
|
-
defaults: { providerOptions: { store: false } },
|
|
24
|
+
defaults: { providerOptions: { store: false, include: ["reasoning.encrypted_content"] } },
|
|
25
25
|
});
|
|
26
26
|
const chatRoute = Route.make({
|
|
27
27
|
id: "openai-compatible-chat",
|
package/dist/route/client.js
CHANGED
|
@@ -5,6 +5,7 @@ import { RequestExecutor } from "./executor.js";
|
|
|
5
5
|
import { Framing } from "./framing.js";
|
|
6
6
|
import { HttpTransport } from "./transport/index.js";
|
|
7
7
|
import { applyCachePolicy } from "../cache-policy.js";
|
|
8
|
+
import { sanitizeSurrogates } from "../utils/sanitize.js";
|
|
8
9
|
import * as ProviderShared from "../protocols/shared.js";
|
|
9
10
|
import { AIError, GenerationOptions, HttpOptions, LLMRequest, LLMResponse, LanguageModel, LLMEvent, InvalidProviderOutputReason, ProviderID, mergeGenerationOptions, mergeHttpOptions, mergeProviderOptions, } from "../schema/index.js";
|
|
10
11
|
const makeRouteLanguageModel = (route, mapped) => {
|
|
@@ -123,7 +124,17 @@ function makeFromTransport(input) {
|
|
|
123
124
|
const route = `${request.model.provider}/${request.model.route.id}`;
|
|
124
125
|
return Stream.unwrap(routeInput.transport.execute(prepared, request, runtime, options).pipe(Effect.map((execution) => {
|
|
125
126
|
const events = execution.frames.pipe(Stream.mapEffect(decodeEvent(route)), protocol.stream.terminal ? Stream.takeUntil(protocol.stream.terminal) : (stream) => stream);
|
|
126
|
-
const stream =
|
|
127
|
+
const stream = Stream.suspend(() => {
|
|
128
|
+
let state = protocol.stream.initial(request);
|
|
129
|
+
const parsed = events.pipe(Stream.mapEffect((event) => protocol.stream.step(state, event).pipe(Effect.map(([next, output]) => {
|
|
130
|
+
state = next;
|
|
131
|
+
return output;
|
|
132
|
+
}))), Stream.flatMap(Stream.fromIterable));
|
|
133
|
+
const onHalt = protocol.stream.onHalt;
|
|
134
|
+
return onHalt
|
|
135
|
+
? parsed.pipe(Stream.concat(Stream.suspend(() => Stream.unwrap(onHalt(state).pipe(Effect.map(Stream.fromIterable))))))
|
|
136
|
+
: parsed;
|
|
137
|
+
}).pipe(Stream.catchCause((cause) => Stream.fail(streamError(route, `Failed to read ${route} stream`, cause))), requireTerminalEvent(route));
|
|
127
138
|
return execution.complete ? stream.pipe(Stream.onEnd(execution.complete)) : stream;
|
|
128
139
|
})));
|
|
129
140
|
},
|
|
@@ -149,7 +160,8 @@ export function make(input) {
|
|
|
149
160
|
});
|
|
150
161
|
}
|
|
151
162
|
const compile = Effect.fn("LLM.compile")(function* (request, options) {
|
|
152
|
-
const
|
|
163
|
+
const original = applyCachePolicy(resolveRequestOptions(request));
|
|
164
|
+
const resolved = LLMRequest.update(original, sanitizeSurrogates({ ...LLMRequest.input(original), model: undefined }));
|
|
153
165
|
const route = resolved.model.route;
|
|
154
166
|
const body = yield* route.body
|
|
155
167
|
.from(resolved)
|
package/dist/route/protocol.d.ts
CHANGED
|
@@ -56,8 +56,8 @@ export interface ProtocolStream<Frame, Event, State> {
|
|
|
56
56
|
readonly step: (state: State, event: Event) => Effect.Effect<readonly [State, ReadonlyArray<LLMEvent>], AIError>;
|
|
57
57
|
/** Optional request-completion signal for transports that do not end naturally. */
|
|
58
58
|
readonly terminal?: (event: Event) => boolean;
|
|
59
|
-
/** Optional flush emitted when the framed stream ends. */
|
|
60
|
-
readonly onHalt?: (state: State) => ReadonlyArray<LLMEvent>;
|
|
59
|
+
/** Optional effectful flush emitted when the framed stream ends. */
|
|
60
|
+
readonly onHalt?: (state: State) => Effect.Effect<ReadonlyArray<LLMEvent>, AIError>;
|
|
61
61
|
}
|
|
62
62
|
/**
|
|
63
63
|
* Construct a `Protocol` from its body and stream pieces:
|
package/dist/schema/options.d.ts
CHANGED
|
@@ -74,6 +74,7 @@ declare const LanguageModelCompatibility_base: Schema.Class<LanguageModelCompati
|
|
|
74
74
|
readonly reasoningField: Schema.optional<Schema.String>;
|
|
75
75
|
readonly maxTokensField: Schema.optional<Schema.Literals<readonly ["max_completion_tokens", "max_tokens"]>>;
|
|
76
76
|
readonly requireFinishReason: Schema.optional<Schema.Boolean>;
|
|
77
|
+
readonly requireAssistantAfterTool: Schema.optional<Schema.Boolean>;
|
|
77
78
|
readonly supportsStore: Schema.optional<Schema.Boolean>;
|
|
78
79
|
readonly supportsUsageInStreaming: Schema.optional<Schema.Boolean>;
|
|
79
80
|
readonly supportsStrictMode: Schema.optional<Schema.Boolean>;
|
package/dist/schema/options.js
CHANGED
|
@@ -101,6 +101,7 @@ export class LanguageModelCompatibility extends Schema.Class("LLM.LanguageModelC
|
|
|
101
101
|
reasoningField: Schema.optional(Schema.String),
|
|
102
102
|
maxTokensField: Schema.optional(LanguageModelMaxTokensFieldCompatibility),
|
|
103
103
|
requireFinishReason: Schema.optional(Schema.Boolean),
|
|
104
|
+
requireAssistantAfterTool: Schema.optional(Schema.Boolean),
|
|
104
105
|
supportsStore: Schema.optional(Schema.Boolean),
|
|
105
106
|
supportsUsageInStreaming: Schema.optional(Schema.Boolean),
|
|
106
107
|
supportsStrictMode: Schema.optional(Schema.Boolean),
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const sanitizeSurrogates: <T>(value: T) => T;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { isRecord } from "./record.js";
|
|
2
|
+
export const sanitizeSurrogates = (value) => {
|
|
3
|
+
if (typeof value === "string")
|
|
4
|
+
return value.toWellFormed();
|
|
5
|
+
if (Array.isArray(value))
|
|
6
|
+
return value.map(sanitizeSurrogates);
|
|
7
|
+
if (value instanceof Uint8Array || value instanceof Error)
|
|
8
|
+
return value;
|
|
9
|
+
if (isRecord(value))
|
|
10
|
+
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key.toWellFormed(), sanitizeSurrogates(entry)]));
|
|
11
|
+
return value;
|
|
12
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
|
-
"version": "0.0.0-beta-
|
|
3
|
+
"version": "0.0.0-beta-18219",
|
|
4
4
|
"name": "@opencode-ai/ai",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@clack/prompts": "1.0.0-alpha.1",
|
|
32
32
|
"@effect/platform-node": "4.0.0-rc.111",
|
|
33
|
-
"@opencode-ai/http-recorder": "0.0.0-beta-
|
|
33
|
+
"@opencode-ai/http-recorder": "0.0.0-beta-18219",
|
|
34
34
|
"@tsconfig/bun": "1.0.9",
|
|
35
35
|
"@types/bun": "1.3.13",
|
|
36
36
|
"@typescript/native-preview": "7.0.0-dev.20251207.1",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@smithy/eventstream-codec": "4.2.14",
|
|
41
41
|
"@smithy/util-utf8": "4.2.2",
|
|
42
|
-
"@opencode-ai/schema": "0.0.0-beta-
|
|
42
|
+
"@opencode-ai/schema": "0.0.0-beta-18219",
|
|
43
43
|
"aws4fetch": "1.0.20",
|
|
44
44
|
"effect": "4.0.0-rc.111",
|
|
45
45
|
"google-auth-library": "10.5.0"
|