feat(ai): support custom reasoning fields (#38227)

This commit is contained in:
Aiden Cline 2026-07-21 23:42:59 -05:00 committed by GitHub
commit 23483ea013
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 306 additions and 67 deletions

View file

@ -419,6 +419,28 @@ describe("Config", () => {
}),
)
it.effect("migrates v1 interleaved fields to compatibility", () =>
Effect.sync(() => {
const migrated = ConfigMigrateV1.migrate({
provider: {
custom: {
models: {
object: { interleaved: { field: "vendor_reasoning" } },
string: { interleaved: "reasoning_text" },
boolean: { interleaved: true },
},
},
},
})
expect(migrated.providers?.custom?.models?.object?.compatibility).toEqual({
reasoningField: "vendor_reasoning",
})
expect(migrated.providers?.custom?.models?.string?.compatibility).toEqual({ reasoningField: "reasoning_text" })
expect(migrated.providers?.custom?.models?.boolean?.compatibility).toBeUndefined()
}),
)
it.effect("migrates v1 command configuration", () =>
Effect.sync(() => {
expect(

View file

@ -170,6 +170,7 @@ describe("ConfigProviderPlugin.Plugin", () => {
models: {
chat: {
name: "First",
compatibility: { reasoningField: "vendor_reasoning" },
capabilities: { tools: true, input: ["text"], output: ["text"] },
disabled: true,
limit: { context: 100, output: 50 },
@ -251,6 +252,7 @@ describe("ConfigProviderPlugin.Plugin", () => {
expect(model.id).toBe(modelID)
expect(model.modelID).toBe(ModelV2.ID.make("api-chat"))
expect(model.name).toBe("Last")
expect(model.compatibility).toEqual({ reasoningField: "vendor_reasoning" })
expect(model.capabilities).toEqual({ tools: true, input: ["text"], output: ["text"] })
expect(model.enabled).toBe(false)
expect(model.limit).toEqual({ context: 100, output: 75 })

View file

@ -1,4 +1,4 @@
import { describe, expect, beforeEach, afterAll } from "bun:test"
import { describe, expect, beforeEach, afterAll, test } from "bun:test"
import { Money } from "@opencode-ai/schema/money"
import { Effect, Layer, Ref } from "effect"
import { HttpClient, HttpClientResponse } from "effect/unstable/http"
@ -15,6 +15,13 @@ import path from "path"
const cacheFile = path.join(Global.Path.cache, "models.json")
test("normalizes permissive interleaved values to compatibility", () => {
expect(ModelV2.compatibility("reasoning_text")).toEqual({ reasoningField: "reasoning_text" })
expect(ModelV2.compatibility({ field: "vendor_reasoning" })).toEqual({ reasoningField: "vendor_reasoning" })
expect(ModelV2.compatibility(true)).toBeUndefined()
expect(ModelV2.compatibility(false)).toBeUndefined()
})
const fixture = {
acme: {
id: "acme",
@ -30,6 +37,7 @@ const fixture = {
reasoning: false,
temperature: true,
tool_call: true,
interleaved: { field: "vendor_reasoning" },
limit: { context: 128000, output: 8192 },
},
},
@ -49,6 +57,7 @@ const fixtureSnapshot = [
modelID: ModelV2.ID.make("acme-1"),
providerID: ProviderV2.ID.make("acme"),
name: "Acme One",
compatibility: { reasoningField: "vendor_reasoning" },
family: undefined,
package: undefined,
settings: undefined,

View file

@ -16,6 +16,7 @@ import { it } from "./lib/effect"
interface ModelOptions {
readonly modelID?: string
readonly compatibility?: ModelV2.Compatibility
readonly settings?: ModelV2.Info["settings"]
readonly headers?: ModelV2.Info["headers"]
readonly body?: ModelV2.Info["body"]
@ -28,6 +29,7 @@ const model = (packageName: string | undefined, options: ModelOptions = {}) =>
modelID: ModelV2.ID.make(options.modelID ?? "api-test-model"),
providerID: ProviderV2.ID.make("test-provider"),
name: "Test model",
compatibility: options.compatibility,
package: packageName,
settings: options.settings ?? {},
headers: options.headers ?? { "x-test": "header" },
@ -101,6 +103,7 @@ describe("SessionRunnerModel", () => {
Effect.gen(function* () {
const resolved = yield* SessionRunnerModel.fromCatalogModel(
model(ProviderV2.aisdk("@ai-sdk/openai-compatible"), {
compatibility: { reasoningField: "vendor_reasoning" },
settings: {
apiKey: "settings-secret",
baseURL: "https://compatible.example/v1",
@ -121,6 +124,7 @@ describe("SessionRunnerModel", () => {
expect(headers.authorization).toBe("Bearer settings-secret")
expect(resolved.route.id).toBe("openai-compatible-chat")
expect(resolved.compatibility?.reasoningField).toBe("vendor_reasoning")
expect(resolved.route.endpoint.baseURL).toBe("https://compatible.example/v1")
expect(resolved.route.defaults.http?.body).toEqual({})
}),