Skip to content

Commit df64612

Browse files
authored
better interleaved thinking support (anomalyco#5298)
1 parent 0aa3e6c commit df64612

File tree

12 files changed

+414
-1219
lines changed

12 files changed

+414
-1219
lines changed

.opencode/opencode.jsonc

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,7 @@
77
"instructions": ["STYLE_GUIDE.md"],
88
"provider": {
99
"opencode": {
10-
"options": {
11-
// "baseURL": "http://localhost:8080",
12-
},
10+
"options": {},
1311
},
1412
},
1513
"mcp": {

packages/opencode/src/acp/agent.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import { Provider } from "../provider/provider"
2525
import { Installation } from "@/installation"
2626
import { MessageV2 } from "@/session/message-v2"
2727
import { Config } from "@/config/config"
28-
import { MCP } from "@/mcp"
2928
import { Todo } from "@/session/todo"
3029
import { z } from "zod"
3130
import { LoadAPIKeyError } from "ai"

packages/opencode/src/lsp/server.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,15 @@ export namespace LSPServer {
211211

212212
export const Biome: Info = {
213213
id: "biome",
214-
root: NearestRoot(["biome.json", "biome.jsonc", "package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
214+
root: NearestRoot([
215+
"biome.json",
216+
"biome.jsonc",
217+
"package-lock.json",
218+
"bun.lockb",
219+
"bun.lock",
220+
"pnpm-lock.yaml",
221+
"yarn.lock",
222+
]),
215223
extensions: [
216224
".ts",
217225
".tsx",

packages/opencode/src/provider/models.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,16 @@ export namespace ModelsDev {
1717
reasoning: z.boolean(),
1818
temperature: z.boolean(),
1919
tool_call: z.boolean(),
20+
interleaved: z
21+
.union([
22+
z.literal(true),
23+
z
24+
.object({
25+
field: z.enum(["reasoning_content", "reasoning_details"]),
26+
})
27+
.strict(),
28+
])
29+
.optional(),
2030
cost: z
2131
.object({
2232
input: z.number(),

packages/opencode/src/provider/provider.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,12 @@ export namespace Provider {
349349
video: z.boolean(),
350350
pdf: z.boolean(),
351351
}),
352+
interleaved: z.union([
353+
z.boolean(),
354+
z.object({
355+
field: z.enum(["reasoning_content", "reasoning_details"]),
356+
}),
357+
]),
352358
}),
353359
cost: z.object({
354360
input: z.number(),
@@ -450,6 +456,7 @@ export namespace Provider {
450456
video: model.modalities?.output?.includes("video") ?? false,
451457
pdf: model.modalities?.output?.includes("pdf") ?? false,
452458
},
459+
interleaved: model.interleaved ?? false,
453460
},
454461
}
455462
}
@@ -567,6 +574,7 @@ export namespace Provider {
567574
video: model.modalities?.output?.includes("video") ?? existingModel?.capabilities.output.video ?? false,
568575
pdf: model.modalities?.output?.includes("pdf") ?? existingModel?.capabilities.output.pdf ?? false,
569576
},
577+
interleaved: model.interleaved ?? false,
570578
},
571579
cost: {
572580
input: model?.cost?.input ?? existingModel?.cost?.input ?? 0,

packages/opencode/src/provider/transform.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,23 @@ export namespace ProviderTransform {
273273
return options
274274
}
275275

276-
export function providerOptions(model: Provider.Model, options: { [x: string]: any }) {
276+
export function providerOptions(model: Provider.Model, options: { [x: string]: any }, messages: ModelMessage[]) {
277+
if (model.capabilities.interleaved && typeof model.capabilities.interleaved === "object") {
278+
const cot = []
279+
const assistantMessages = messages.filter((msg) => msg.role === "assistant")
280+
for (const msg of assistantMessages) {
281+
for (const part of msg.content) {
282+
if (typeof part === "string") {
283+
continue
284+
}
285+
if (part.type === "reasoning") {
286+
cot.push(part)
287+
}
288+
}
289+
}
290+
options[model.capabilities.interleaved.field] = cot
291+
}
292+
277293
switch (model.api.npm) {
278294
case "@ai-sdk/openai":
279295
case "@ai-sdk/azure":

packages/opencode/src/session/compaction.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ export namespace SessionCompaction {
143143
providerOptions: ProviderTransform.providerOptions(
144144
model,
145145
pipe({}, mergeDeep(ProviderTransform.options(model, input.sessionID)), mergeDeep(model.options)),
146+
[],
146147
),
147148
headers: model.headers,
148149
abortSignal: input.abort,

packages/opencode/src/session/prompt.ts

Lines changed: 34 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,37 @@ export namespace SessionPrompt {
515515
})
516516
}
517517

518+
const messages = [
519+
...system.map(
520+
(x): ModelMessage => ({
521+
role: "system",
522+
content: x,
523+
}),
524+
),
525+
...MessageV2.toModelMessage(
526+
msgs.filter((m) => {
527+
if (m.info.role !== "assistant" || m.info.error === undefined) {
528+
return true
529+
}
530+
if (
531+
MessageV2.AbortedError.isInstance(m.info.error) &&
532+
m.parts.some((part) => part.type !== "step-start" && part.type !== "reasoning")
533+
) {
534+
return true
535+
}
536+
537+
return false
538+
}),
539+
),
540+
...(isLastStep
541+
? [
542+
{
543+
role: "assistant" as const,
544+
content: MAX_STEPS,
545+
},
546+
]
547+
: []),
548+
]
518549
const result = await processor.process({
519550
onError(error) {
520551
log.error("stream error", {
@@ -562,42 +593,12 @@ export namespace SessionPrompt {
562593
OUTPUT_TOKEN_MAX,
563594
),
564595
abortSignal: abort,
565-
providerOptions: ProviderTransform.providerOptions(model, params.options),
596+
providerOptions: ProviderTransform.providerOptions(model, params.options, messages),
566597
stopWhen: stepCountIs(1),
567598
temperature: params.temperature,
568599
topP: params.topP,
569600
toolChoice: isLastStep ? "none" : undefined,
570-
messages: [
571-
...system.map(
572-
(x): ModelMessage => ({
573-
role: "system",
574-
content: x,
575-
}),
576-
),
577-
...MessageV2.toModelMessage(
578-
msgs.filter((m) => {
579-
if (m.info.role !== "assistant" || m.info.error === undefined) {
580-
return true
581-
}
582-
if (
583-
MessageV2.AbortedError.isInstance(m.info.error) &&
584-
m.parts.some((part) => part.type !== "step-start" && part.type !== "reasoning")
585-
) {
586-
return true
587-
}
588-
589-
return false
590-
}),
591-
),
592-
...(isLastStep
593-
? [
594-
{
595-
role: "assistant" as const,
596-
content: MAX_STEPS,
597-
},
598-
]
599-
: []),
600-
],
601+
messages,
601602
tools: model.capabilities.toolcall === false ? undefined : tools,
602603
model: wrapLanguageModel({
603604
model: language,
@@ -1464,7 +1465,7 @@ export namespace SessionPrompt {
14641465
await generateText({
14651466
// use higher # for reasoning models since reasoning tokens eat up a lot of the budget
14661467
maxOutputTokens: small.capabilities.reasoning ? 3000 : 20,
1467-
providerOptions: ProviderTransform.providerOptions(small, options),
1468+
providerOptions: ProviderTransform.providerOptions(small, options, []),
14681469
messages: [
14691470
...SystemPrompt.title(small.providerID).map(
14701471
(x): ModelMessage => ({

packages/opencode/src/session/summary.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ export namespace SessionSummary {
9191
if (textPart && !userMsg.summary?.title) {
9292
const result = await generateText({
9393
maxOutputTokens: small.capabilities.reasoning ? 1500 : 20,
94-
providerOptions: ProviderTransform.providerOptions(small, options),
94+
providerOptions: ProviderTransform.providerOptions(small, options, []),
9595
messages: [
9696
...SystemPrompt.title(small.providerID).map(
9797
(x): ModelMessage => ({
@@ -144,7 +144,7 @@ export namespace SessionSummary {
144144
const result = await generateText({
145145
model: language,
146146
maxOutputTokens: 100,
147-
providerOptions: ProviderTransform.providerOptions(small, options),
147+
providerOptions: ProviderTransform.providerOptions(small, options, []),
148148
messages: [
149149
...SystemPrompt.summarize(small.providerID).map(
150150
(x): ModelMessage => ({

packages/opencode/test/provider/transform.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ describe("ProviderTransform.message - DeepSeek reasoning content", () => {
130130
toolcall: true,
131131
input: { text: true, audio: false, image: false, video: false, pdf: false },
132132
output: { text: true, audio: false, image: false, video: false, pdf: false },
133+
interleaved: false,
133134
},
134135
cost: {
135136
input: 0.001,
@@ -184,6 +185,7 @@ describe("ProviderTransform.message - DeepSeek reasoning content", () => {
184185
toolcall: true,
185186
input: { text: true, audio: false, image: false, video: false, pdf: false },
186187
output: { text: true, audio: false, image: false, video: false, pdf: false },
188+
interleaved: false,
187189
},
188190
cost: {
189191
input: 0.001,
@@ -236,6 +238,7 @@ describe("ProviderTransform.message - DeepSeek reasoning content", () => {
236238
toolcall: true,
237239
input: { text: true, audio: false, image: false, video: false, pdf: false },
238240
output: { text: true, audio: false, image: false, video: false, pdf: false },
241+
interleaved: false,
239242
},
240243
cost: {
241244
input: 0.001,
@@ -281,6 +284,7 @@ describe("ProviderTransform.message - DeepSeek reasoning content", () => {
281284
toolcall: true,
282285
input: { text: true, audio: false, image: true, video: false, pdf: false },
283286
output: { text: true, audio: false, image: false, video: false, pdf: false },
287+
interleaved: false,
284288
},
285289
cost: {
286290
input: 0.03,

0 commit comments

Comments
 (0)