From 5fec94dfc293d819df73034d21af03a997e1c05e Mon Sep 17 00:00:00 2001 From: purple-poi Date: Thu, 2 Jul 2026 13:27:36 +0800 Subject: [PATCH 1/2] fix: add stepfun interleaved reasoning --- .../src/kilocode/provider/provider.ts | 7 + packages/opencode/src/provider/provider.ts | 15 +- .../provider/stepfun-interleaved.test.ts | 134 ++++++++++++++++++ 3 files changed, 151 insertions(+), 5 deletions(-) create mode 100644 packages/opencode/test/kilocode/provider/stepfun-interleaved.test.ts diff --git a/packages/opencode/src/kilocode/provider/provider.ts b/packages/opencode/src/kilocode/provider/provider.ts index 66e47a8fa59..2a9244d2558 100644 --- a/packages/opencode/src/kilocode/provider/provider.ts +++ b/packages/opencode/src/kilocode/provider/provider.ts @@ -93,6 +93,13 @@ export function patchConfigModel(cfg: any, existing: any) { } } +export function defaultInterleaved(model: string, npm: string) { + if (npm !== "@ai-sdk/openai-compatible") return false + if (model.includes("deepseek")) return { field: "reasoning_content" as const } + if (model.includes("step")) return { field: "reasoning_content" as const } + return false +} + // --------------------------------------------------------------------------- // Custom loaders (new or fully-replaced loaders) // --------------------------------------------------------------------------- diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index c25476a8850..6ca31a806a8 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -39,6 +39,7 @@ import { patchKiloProviderPrivacy, kiloSmallModelPriority, buildTimeoutSignal, + defaultInterleaved, } from "@/kilocode/provider/provider" import * as ModelsRefresh from "@/kilocode/provider/models-refresh" // kilocode_change end @@ -1160,7 +1161,12 @@ function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model video: model.modalities?.output?.includes("video") ?? false, pdf: model.modalities?.output?.includes("pdf") ?? false, }, - interleaved: model.interleaved ?? false, + interleaved: + model.interleaved ?? + defaultInterleaved( + model.id, + model.provider?.npm ?? provider.npm ?? "@ai-sdk/openai-compatible", + ), }, release_date: model.release_date ?? "", variants: {}, @@ -1397,10 +1403,9 @@ export const layer = Layer.effect( }, interleaved: model.interleaved ?? - existingModel?.capabilities.interleaved ?? - (!existingModel && apiNpm === "@ai-sdk/openai-compatible" && apiID.includes("deepseek") - ? { field: "reasoning_content" } - : false), + (existingModel?.capabilities.interleaved + ? existingModel.capabilities.interleaved + : defaultInterleaved(apiID, apiNpm)), }, cost: { input: model?.cost?.input ?? existingModel?.cost?.input ?? 0, diff --git a/packages/opencode/test/kilocode/provider/stepfun-interleaved.test.ts b/packages/opencode/test/kilocode/provider/stepfun-interleaved.test.ts new file mode 100644 index 00000000000..e6cad16e525 --- /dev/null +++ b/packages/opencode/test/kilocode/provider/stepfun-interleaved.test.ts @@ -0,0 +1,134 @@ +import { expect, test } from "bun:test" +import { Effect } from "effect" +import { ModelsDev } from "@opencode-ai/core/models-dev" +import { Provider } from "../../../src/provider/provider" +import { ProviderID } from "../../../src/provider/schema" +import { testEffect } from "../../lib/effect" + +const it = testEffect(Provider.defaultLayer) + +const base = (id: string): ModelsDev.Model => ({ + id, + name: id, + release_date: "2026-01-01", + attachment: false, + reasoning: true, + temperature: true, + tool_call: true, + limit: { + context: 128000, + output: 8192, + }, +}) + +test("StepFun catalog models default to interleaved reasoning_content", () => { + const provider = Provider.fromModelsDevProvider({ + id: "stepfun", + name: "StepFun", + env: ["STEPFUN_API_KEY"], + npm: "@ai-sdk/openai-compatible", + api: "https://api.stepfun.com/v1", + models: { + "step-3.5-flash": base("step-3.5-flash"), + }, + }) + + expect(provider.models["step-3.5-flash"].capabilities.interleaved).toEqual({ + field: "reasoning_content", + }) +}) + +test("StepFun provider does not default when model ID does not include step", () => { + const provider = Provider.fromModelsDevProvider({ + id: "stepfun", + name: "StepFun", + env: ["STEPFUN_API_KEY"], + npm: "@ai-sdk/openai-compatible", + api: "https://api.stepfun.com/v1", + models: { + custom: { + ...base("custom"), + name: "Custom Model", + }, + }, + }) + + expect(provider.models["custom"].capabilities.interleaved).toBe(false) +}) + +test("catalog model IDs containing step default to interleaved reasoning_content", () => { + const provider = Provider.fromModelsDevProvider({ + id: "custom-provider", + name: "Custom Provider", + env: ["CUSTOM_API_KEY"], + npm: "@ai-sdk/openai-compatible", + api: "https://api.custom.com/v1", + models: { + space: base("step 3.5 flash"), + hyphen: base("step-3.5-flash"), + dot: base("step.3.5.flash"), + underscore: base("step_3_5_flash"), + }, + }) + + for (const model of Object.values(provider.models)) { + expect(model.capabilities.interleaved).toEqual({ + field: "reasoning_content", + }) + } +}) + +test("catalog model IDs containing step do not default for non-openai-compatible providers", () => { + const provider = Provider.fromModelsDevProvider({ + id: "custom-provider", + name: "Custom Provider", + env: ["CUSTOM_API_KEY"], + npm: "@kilocode/kilo-gateway", + api: "https://api.custom.com/v1", + models: { + custom: base("step-3.5-flash"), + }, + }) + + expect(provider.models["custom"].capabilities.interleaved).toBe(false) +}) + +it.instance( + "configured StepFun and step model IDs default to interleaved reasoning_content", + Effect.gen(function* () { + const providers = yield* Provider.use.list() + const stepfun = providers[ProviderID.make("stepfun")].models["step-custom"] + const custom = providers[ProviderID.make("custom-provider")].models["custom-step"] + + expect(stepfun.capabilities.interleaved).toEqual({ + field: "reasoning_content", + }) + expect(custom.capabilities.interleaved).toEqual({ + field: "reasoning_content", + }) + }), + { + config: { + provider: { + stepfun: { + name: "StepFun", + npm: "@ai-sdk/openai-compatible", + api: "https://api.stepfun.com/v1", + models: { + "step-custom": { name: "Step Custom" }, + }, + options: { apiKey: "test-key" }, + }, + "custom-provider": { + name: "Custom Provider", + npm: "@ai-sdk/openai-compatible", + api: "https://api.custom.com/v1", + models: { + "custom-step": { id: "step-3.5-flash", name: "Custom Alias" }, + }, + options: { apiKey: "test-key" }, + }, + }, + }, + }, +) From 4f4e331d9d3726ab28c3f4e4ac0372374e7a7d01 Mon Sep 17 00:00:00 2001 From: purple-poi Date: Thu, 2 Jul 2026 14:52:05 +0800 Subject: [PATCH 2/2] fix: minimal update --- .../opencode/src/kilocode/provider/provider.ts | 7 ------- packages/opencode/src/provider/provider.ts | 13 +++++++------ .../kilocode/provider/stepfun-interleaved.test.ts | 15 +++++++++++++++ 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/packages/opencode/src/kilocode/provider/provider.ts b/packages/opencode/src/kilocode/provider/provider.ts index 2a9244d2558..66e47a8fa59 100644 --- a/packages/opencode/src/kilocode/provider/provider.ts +++ b/packages/opencode/src/kilocode/provider/provider.ts @@ -93,13 +93,6 @@ export function patchConfigModel(cfg: any, existing: any) { } } -export function defaultInterleaved(model: string, npm: string) { - if (npm !== "@ai-sdk/openai-compatible") return false - if (model.includes("deepseek")) return { field: "reasoning_content" as const } - if (model.includes("step")) return { field: "reasoning_content" as const } - return false -} - // --------------------------------------------------------------------------- // Custom loaders (new or fully-replaced loaders) // --------------------------------------------------------------------------- diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 6ca31a806a8..cc1b0f719aa 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -39,7 +39,6 @@ import { patchKiloProviderPrivacy, kiloSmallModelPriority, buildTimeoutSignal, - defaultInterleaved, } from "@/kilocode/provider/provider" import * as ModelsRefresh from "@/kilocode/provider/models-refresh" // kilocode_change end @@ -1163,10 +1162,10 @@ function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model }, interleaved: model.interleaved ?? - defaultInterleaved( - model.id, - model.provider?.npm ?? provider.npm ?? "@ai-sdk/openai-compatible", - ), + ((model.provider?.npm ?? provider.npm ?? "@ai-sdk/openai-compatible") === "@ai-sdk/openai-compatible" && + model.id.includes("step") + ? { field: "reasoning_content" as const } + : false), }, release_date: model.release_date ?? "", variants: {}, @@ -1405,7 +1404,9 @@ export const layer = Layer.effect( model.interleaved ?? (existingModel?.capabilities.interleaved ? existingModel.capabilities.interleaved - : defaultInterleaved(apiID, apiNpm)), + : apiNpm === "@ai-sdk/openai-compatible" && (apiID.includes("deepseek") || apiID.includes("step")) + ? { field: "reasoning_content" as const } + : false), }, cost: { input: model?.cost?.input ?? existingModel?.cost?.input ?? 0, diff --git a/packages/opencode/test/kilocode/provider/stepfun-interleaved.test.ts b/packages/opencode/test/kilocode/provider/stepfun-interleaved.test.ts index e6cad16e525..e9069bd8bf9 100644 --- a/packages/opencode/test/kilocode/provider/stepfun-interleaved.test.ts +++ b/packages/opencode/test/kilocode/provider/stepfun-interleaved.test.ts @@ -56,6 +56,21 @@ test("StepFun provider does not default when model ID does not include step", () expect(provider.models["custom"].capabilities.interleaved).toBe(false) }) +test("catalog DeepSeek models do not default to interleaved reasoning_content", () => { + const provider = Provider.fromModelsDevProvider({ + id: "custom-provider", + name: "Custom Provider", + env: ["CUSTOM_API_KEY"], + npm: "@ai-sdk/openai-compatible", + api: "https://api.custom.com/v1", + models: { + "deepseek-r1": base("deepseek-r1"), + }, + }) + + expect(provider.models["deepseek-r1"].capabilities.interleaved).toBe(false) +}) + test("catalog model IDs containing step default to interleaved reasoning_content", () => { const provider = Provider.fromModelsDevProvider({ id: "custom-provider",