@opencode/ai 0.0.0-dev-20017 → 0.0.0-dev-20020
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.
|
@@ -12,6 +12,37 @@ declare const OpenAIChatCacheControl: Schema.Struct<{
|
|
|
12
12
|
readonly type: Schema.Literal<"ephemeral">;
|
|
13
13
|
readonly ttl: Schema.optional<Schema.String>;
|
|
14
14
|
}>;
|
|
15
|
+
declare const ReasoningDetail: Schema.Union<readonly [Schema.StructWithRest<Schema.Struct<{
|
|
16
|
+
readonly id: Schema.optional<Schema.NullOr<Schema.String>>;
|
|
17
|
+
readonly format: Schema.optional<Schema.String>;
|
|
18
|
+
readonly index: Schema.optional<Schema.Number>;
|
|
19
|
+
readonly signature: Schema.optional<Schema.NullOr<Schema.String>>;
|
|
20
|
+
readonly type: Schema.Literal<"reasoning.text">;
|
|
21
|
+
readonly text: Schema.optional<Schema.String>;
|
|
22
|
+
}>, readonly [Schema.$Record<Schema.String, Schema.Unknown>]>, Schema.StructWithRest<Schema.Struct<{
|
|
23
|
+
readonly id: Schema.optional<Schema.NullOr<Schema.String>>;
|
|
24
|
+
readonly format: Schema.optional<Schema.String>;
|
|
25
|
+
readonly index: Schema.optional<Schema.Number>;
|
|
26
|
+
readonly signature: Schema.optional<Schema.NullOr<Schema.String>>;
|
|
27
|
+
readonly type: Schema.Literal<"reasoning.summary">;
|
|
28
|
+
readonly summary: Schema.optional<Schema.String>;
|
|
29
|
+
}>, readonly [Schema.$Record<Schema.String, Schema.Unknown>]>, Schema.StructWithRest<Schema.Struct<{
|
|
30
|
+
readonly id: Schema.optional<Schema.NullOr<Schema.String>>;
|
|
31
|
+
readonly format: Schema.optional<Schema.String>;
|
|
32
|
+
readonly index: Schema.optional<Schema.Number>;
|
|
33
|
+
readonly signature: Schema.optional<Schema.NullOr<Schema.String>>;
|
|
34
|
+
readonly type: Schema.Literal<"reasoning.encrypted">;
|
|
35
|
+
readonly data: Schema.String;
|
|
36
|
+
}>, readonly [Schema.$Record<Schema.String, Schema.Unknown>]>, Schema.Struct<{
|
|
37
|
+
readonly type: Schema.Literal<"summary">;
|
|
38
|
+
readonly summary: Schema.String;
|
|
39
|
+
readonly index: Schema.optional<Schema.Number>;
|
|
40
|
+
}>, Schema.Struct<{
|
|
41
|
+
readonly type: Schema.Literal<"encrypted">;
|
|
42
|
+
readonly encrypted: Schema.String;
|
|
43
|
+
readonly index: Schema.optional<Schema.Number>;
|
|
44
|
+
}>]>;
|
|
45
|
+
type ReasoningDetail = Schema.Schema.Type<typeof ReasoningDetail>;
|
|
15
46
|
export declare const bodyFields: {
|
|
16
47
|
model: Schema.String;
|
|
17
48
|
messages: Schema.$Array<Schema.toTaggedUnion<"role", readonly [Schema.Struct<{
|
|
@@ -278,7 +309,9 @@ export interface ParserState {
|
|
|
278
309
|
readonly finishReason?: FinishReasonDetails;
|
|
279
310
|
readonly lifecycle: Lifecycle.State;
|
|
280
311
|
readonly reasoningField?: string;
|
|
281
|
-
|
|
312
|
+
/** A scalar reasoning field (`reasoning_content`, ...) has carried text in this stream. */
|
|
313
|
+
readonly reasoningTextObserved: boolean;
|
|
314
|
+
readonly reasoningDetails: Array<ReasoningDetail>;
|
|
282
315
|
readonly reasoningDetailsObserved: boolean;
|
|
283
316
|
readonly reasoningEmitted: boolean;
|
|
284
317
|
readonly latestToolIndex?: number;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Effect, Schema } from "effect";
|
|
1
|
+
import { Effect, Option, Schema } from "effect";
|
|
2
2
|
import { Tool } from "@opencode/schema/tool";
|
|
3
3
|
import { Route } from "../route/client.js";
|
|
4
4
|
import { Auth } from "../route/auth.js";
|
|
@@ -50,6 +50,32 @@ const OpenAIChatAssistantToolCall = Schema.Struct({
|
|
|
50
50
|
arguments: Schema.String,
|
|
51
51
|
}),
|
|
52
52
|
});
|
|
53
|
+
// `reasoning_details` carries two dialects. OpenRouter's `reasoning.*` entries
|
|
54
|
+
// must be replayed unmodified (`index` included), so they keep every field they
|
|
55
|
+
// arrived with. Kimi's OpenAI-compatible surface streams preserved thinking as
|
|
56
|
+
// bare `summary` / `encrypted` entries keyed by a stream-only `index`; Kimi does
|
|
57
|
+
// not document this publicly, so the handling follows Kimi Code (Kimi's own
|
|
58
|
+
// client): merge summary deltas by `index`, replay without `index`, and always
|
|
59
|
+
// send `reasoning_content` alongside. Anything else is dropped at the boundary.
|
|
60
|
+
const OpenRouterDetailFields = {
|
|
61
|
+
id: Schema.optional(Schema.NullOr(Schema.String)),
|
|
62
|
+
format: Schema.optional(Schema.String),
|
|
63
|
+
index: Schema.optional(Schema.Number),
|
|
64
|
+
signature: Schema.optional(Schema.NullOr(Schema.String)),
|
|
65
|
+
};
|
|
66
|
+
const ReasoningDetail = Schema.Union([
|
|
67
|
+
Schema.StructWithRest(Schema.Struct({ type: Schema.Literal("reasoning.text"), text: Schema.optional(Schema.String), ...OpenRouterDetailFields }), [Schema.Record(Schema.String, Schema.Unknown)]),
|
|
68
|
+
Schema.StructWithRest(Schema.Struct({
|
|
69
|
+
type: Schema.Literal("reasoning.summary"),
|
|
70
|
+
summary: Schema.optional(Schema.String),
|
|
71
|
+
...OpenRouterDetailFields,
|
|
72
|
+
}), [Schema.Record(Schema.String, Schema.Unknown)]),
|
|
73
|
+
Schema.StructWithRest(Schema.Struct({ type: Schema.Literal("reasoning.encrypted"), data: Schema.String, ...OpenRouterDetailFields }), [Schema.Record(Schema.String, Schema.Unknown)]),
|
|
74
|
+
Schema.Struct({ type: Schema.Literal("summary"), summary: Schema.String, index: Schema.optional(Schema.Number) }),
|
|
75
|
+
Schema.Struct({ type: Schema.Literal("encrypted"), encrypted: Schema.String, index: Schema.optional(Schema.Number) }),
|
|
76
|
+
]);
|
|
77
|
+
const decodeReasoningDetail = Schema.decodeUnknownOption(ReasoningDetail);
|
|
78
|
+
const knownReasoningDetails = (details) => details.flatMap((detail) => Option.toArray(decodeReasoningDetail(detail)));
|
|
53
79
|
// Intentionally omit Gemini's provider-specific `extra_content.google.thought_signature`
|
|
54
80
|
// extension until direct Google OpenAI-compatible routing is supported here:
|
|
55
81
|
// https://github.com/vercel/ai/issues/11590
|
|
@@ -220,10 +246,20 @@ const reasoningDetails = (parts, native, providerMetadataKey) => {
|
|
|
220
246
|
return Array.isArray(details) ? details : [];
|
|
221
247
|
});
|
|
222
248
|
if (parts.some((part) => Array.isArray(part.providerMetadata?.[providerMetadataKey]?.reasoningDetails)))
|
|
223
|
-
return observed;
|
|
249
|
+
return knownReasoningDetails(observed).map(lowerReasoningDetail);
|
|
224
250
|
if (isRecord(native) && Array.isArray(native.reasoning_details))
|
|
225
|
-
return native.reasoning_details;
|
|
251
|
+
return knownReasoningDetails(native.reasoning_details).map(lowerReasoningDetail);
|
|
252
|
+
};
|
|
253
|
+
// Kimi rejects its stream-only `index` on requests
|
|
254
|
+
// ("the reasoning_details ... must not contain streaming index").
|
|
255
|
+
const lowerReasoningDetail = (detail) => {
|
|
256
|
+
if (detail.type === "summary")
|
|
257
|
+
return { type: detail.type, summary: detail.summary };
|
|
258
|
+
if (detail.type === "encrypted")
|
|
259
|
+
return { type: detail.type, encrypted: detail.encrypted };
|
|
260
|
+
return detail;
|
|
226
261
|
};
|
|
262
|
+
const isKimiDetail = (detail) => detail.type === "summary" || detail.type === "encrypted";
|
|
227
263
|
const lowerUserMessage = Effect.fn("OpenAIChat.lowerUserMessage")(function* (message, options) {
|
|
228
264
|
const content = [];
|
|
229
265
|
for (const part of message.content) {
|
|
@@ -282,6 +318,10 @@ const lowerAssistantMessage = Effect.fn("OpenAIChat.lowerAssistantMessage")(func
|
|
|
282
318
|
return "reasoning_content";
|
|
283
319
|
if (!fullyStructured || requireReasoning)
|
|
284
320
|
return "reasoning_content";
|
|
321
|
+
// Kimi always expects `reasoning_content` on replayed assistant messages,
|
|
322
|
+
// even when thinking arrived only through structured details.
|
|
323
|
+
if (details?.some(isKimiDetail))
|
|
324
|
+
return "reasoning_content";
|
|
285
325
|
})();
|
|
286
326
|
const reasoningText = (() => {
|
|
287
327
|
if (configuredField !== undefined)
|
|
@@ -705,14 +745,16 @@ const reasoningDelta = (delta, configuredField) => {
|
|
|
705
745
|
}
|
|
706
746
|
return undefined;
|
|
707
747
|
};
|
|
708
|
-
const detailText = (details) => {
|
|
748
|
+
const detailText = (details, hideKimiSummary) => {
|
|
709
749
|
const text = details.flatMap((detail) => {
|
|
710
|
-
if (
|
|
711
|
-
return [];
|
|
712
|
-
if (detail.type === "reasoning.
|
|
713
|
-
return [detail.
|
|
714
|
-
|
|
715
|
-
|
|
750
|
+
if (detail.type === "reasoning.text")
|
|
751
|
+
return detail.text ? [detail.text] : [];
|
|
752
|
+
if (detail.type === "reasoning.summary")
|
|
753
|
+
return detail.summary ? [detail.summary] : [];
|
|
754
|
+
// Kimi streams the full thinking through `reasoning_content` and a separate
|
|
755
|
+
// summary through details; show the summary only when nothing else does.
|
|
756
|
+
if (detail.type === "summary")
|
|
757
|
+
return detail.summary && !hideKimiSummary ? [detail.summary] : [];
|
|
716
758
|
return [];
|
|
717
759
|
});
|
|
718
760
|
if (text.length > 0)
|
|
@@ -721,25 +763,48 @@ const detailText = (details) => {
|
|
|
721
763
|
const appendReasoningDetails = (result, details) => {
|
|
722
764
|
for (const detail of details) {
|
|
723
765
|
const previous = result.at(-1);
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
!isRecord(detail) ||
|
|
727
|
-
detail.type !== "reasoning.text" ||
|
|
728
|
-
conflictingReasoningTextDetails(previous, detail)) {
|
|
766
|
+
const merged = previous === undefined ? undefined : mergeReasoningDetails(previous, detail);
|
|
767
|
+
if (merged === undefined) {
|
|
729
768
|
result.push(detail);
|
|
730
769
|
continue;
|
|
731
770
|
}
|
|
732
|
-
result[result.length - 1] =
|
|
771
|
+
result[result.length - 1] = merged;
|
|
772
|
+
}
|
|
773
|
+
};
|
|
774
|
+
// Consecutive text or summary deltas of the same kind accumulate into one
|
|
775
|
+
// entry; encrypted entries are opaque and never merge.
|
|
776
|
+
const mergeReasoningDetails = (previous, detail) => {
|
|
777
|
+
if (conflictingReasoningDetails(previous, detail))
|
|
778
|
+
return undefined;
|
|
779
|
+
if (previous.type === "reasoning.text" && detail.type === "reasoning.text")
|
|
780
|
+
return {
|
|
733
781
|
...previous,
|
|
734
|
-
...
|
|
735
|
-
text: `${
|
|
736
|
-
|
|
737
|
-
format: mergeDetailValue(previous.format, detail.format),
|
|
782
|
+
...detail,
|
|
783
|
+
text: `${previous.text ?? ""}${detail.text ?? ""}`,
|
|
784
|
+
...mergeDetailIdentity(previous, detail),
|
|
738
785
|
};
|
|
739
|
-
|
|
786
|
+
if (previous.type === "reasoning.summary" && detail.type === "reasoning.summary")
|
|
787
|
+
return {
|
|
788
|
+
...previous,
|
|
789
|
+
...detail,
|
|
790
|
+
summary: `${previous.summary ?? ""}${detail.summary ?? ""}`,
|
|
791
|
+
...mergeDetailIdentity(previous, detail),
|
|
792
|
+
};
|
|
793
|
+
if (previous.type === "summary" && detail.type === "summary")
|
|
794
|
+
return { ...previous, ...detail, summary: previous.summary + detail.summary };
|
|
795
|
+
};
|
|
796
|
+
// The first non-empty signature and format win; a later delta may carry the
|
|
797
|
+
// signature for text that streamed earlier.
|
|
798
|
+
const mergeDetailIdentity = (previous, current) => {
|
|
799
|
+
const signature = mergeDetailValue(previous.signature, current.signature);
|
|
800
|
+
const format = mergeDetailValue(previous.format, current.format);
|
|
801
|
+
return {
|
|
802
|
+
...(signature === undefined ? {} : { signature }),
|
|
803
|
+
...(format === undefined ? {} : { format }),
|
|
804
|
+
};
|
|
740
805
|
};
|
|
741
806
|
const mergeDetailValue = (previous, current) => previous || current || (previous !== undefined ? previous : current);
|
|
742
|
-
const
|
|
807
|
+
const conflictingReasoningDetails = (previous, current) => conflictingDetailValue(previous.id, current.id) ||
|
|
743
808
|
conflictingDetailValue(previous.index, current.index) ||
|
|
744
809
|
conflictingDetailValue(previous.format, current.format) ||
|
|
745
810
|
(Boolean(previous.signature) && Boolean(current.signature) && previous.signature !== current.signature);
|
|
@@ -795,12 +860,17 @@ const step = (state, event) => Effect.gen(function* () {
|
|
|
795
860
|
return [{ ...state, usage }, events];
|
|
796
861
|
}
|
|
797
862
|
const reasoningField = state.reasoningField ?? reasoning?.field;
|
|
798
|
-
const
|
|
863
|
+
const reasoningTextObserved = state.reasoningTextObserved || reasoning !== undefined;
|
|
864
|
+
const detailDelta = Array.isArray(delta?.reasoning_details)
|
|
865
|
+
? knownReasoningDetails(delta.reasoning_details)
|
|
866
|
+
: undefined;
|
|
799
867
|
if (detailDelta !== undefined)
|
|
800
868
|
appendReasoningDetails(state.reasoningDetails, detailDelta);
|
|
801
869
|
const reasoningDetailsObserved = state.reasoningDetailsObserved || detailDelta !== undefined;
|
|
802
870
|
const deltaMetadata = reasoningMetadata(state.providerMetadataKey, reasoningField);
|
|
803
|
-
const text = detailDelta?.length
|
|
871
|
+
const text = detailDelta?.length
|
|
872
|
+
? (detailText(detailDelta, reasoningTextObserved) ?? reasoning?.text)
|
|
873
|
+
: reasoning?.text;
|
|
804
874
|
if (text !== undefined)
|
|
805
875
|
lifecycle = Lifecycle.reasoningDelta(lifecycle, events, "reasoning-0", text, deltaMetadata);
|
|
806
876
|
else if (reasoningDetailsObserved &&
|
|
@@ -878,6 +948,7 @@ const step = (state, event) => Effect.gen(function* () {
|
|
|
878
948
|
finishReason,
|
|
879
949
|
lifecycle,
|
|
880
950
|
reasoningField,
|
|
951
|
+
reasoningTextObserved,
|
|
881
952
|
reasoningDetails: state.reasoningDetails,
|
|
882
953
|
reasoningDetailsObserved,
|
|
883
954
|
reasoningEmitted,
|
|
@@ -944,6 +1015,7 @@ export const protocol = Protocol.make({
|
|
|
944
1015
|
toolCallEvents: [],
|
|
945
1016
|
lifecycle: Lifecycle.initial(),
|
|
946
1017
|
reasoningField: request.model.compatibility?.reasoningField,
|
|
1018
|
+
reasoningTextObserved: false,
|
|
947
1019
|
reasoningDetails: [],
|
|
948
1020
|
reasoningDetailsObserved: false,
|
|
949
1021
|
reasoningEmitted: false,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
|
-
"version": "0.0.0-dev-
|
|
3
|
+
"version": "0.0.0-dev-20020",
|
|
4
4
|
"name": "@opencode/ai",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@clack/prompts": "1.0.0-alpha.1",
|
|
36
36
|
"@effect/platform-node": "4.0.0-rc.112",
|
|
37
|
-
"@opencode/http-recorder": "0.0.0-dev-
|
|
37
|
+
"@opencode/http-recorder": "0.0.0-dev-20020",
|
|
38
38
|
"@tsconfig/bun": "1.0.9",
|
|
39
39
|
"@types/bun": "1.4.0",
|
|
40
40
|
"@typescript/native-preview": "7.0.0-dev.20251207.1",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"@aws-sdk/credential-providers": "3.1057.0",
|
|
45
45
|
"@smithy/eventstream-codec": "4.2.14",
|
|
46
46
|
"@smithy/util-utf8": "4.2.2",
|
|
47
|
-
"@opencode/schema": "0.0.0-dev-
|
|
47
|
+
"@opencode/schema": "0.0.0-dev-20020",
|
|
48
48
|
"aws4fetch": "1.0.20",
|
|
49
49
|
"effect": "4.0.0-rc.112",
|
|
50
50
|
"google-auth-library": "10.5.0"
|