mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-14 03:56:55 +03:00
fix(app): derive model reasoning capability from variants and compatibility (#60457)
This commit is contained in:
committed by
GitHub
Unverified
parent
fbd8aecaa1
commit
85699b5ec8
@@ -4,7 +4,7 @@ import { directoryKey, normalizeAgentList, normalizeProjectInfo, normalizeProvid
|
||||
|
||||
describe("normalizeAgentList", () => {
|
||||
test("adapts current agents to the app agent shape", () => {
|
||||
const result = normalizeAgentList([
|
||||
const agents: AgentListOutput["data"] = [
|
||||
{
|
||||
id: "build",
|
||||
name: "Build",
|
||||
@@ -16,7 +16,9 @@ describe("normalizeAgentList", () => {
|
||||
system: "Build software",
|
||||
permissions: [{ action: "read", resource: "*", effect: "allow" }],
|
||||
},
|
||||
] as AgentListOutput["data"])
|
||||
]
|
||||
|
||||
const result = normalizeAgentList(agents)
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
@@ -40,37 +42,40 @@ describe("normalizeAgentList", () => {
|
||||
|
||||
describe("normalizeProviderList", () => {
|
||||
test("groups current models into the app provider catalog", () => {
|
||||
const result = normalizeProviderList(
|
||||
[{ id: "openai", name: "OpenAI", package: "@ai-sdk/openai" }] as ProviderListOutput["data"],
|
||||
[
|
||||
{
|
||||
id: "gpt-5",
|
||||
modelID: "gpt-5",
|
||||
providerID: "openai",
|
||||
name: "GPT-5",
|
||||
capabilities: { tools: true, input: ["text", "image"], output: ["text"] },
|
||||
variants: [{ id: "high" }],
|
||||
time: { released: 1 },
|
||||
cost: [{ input: 1, output: 2, cache: { read: 0.1, write: 0.2 } }],
|
||||
status: "active",
|
||||
enabled: true,
|
||||
limit: { context: 128_000, output: 8_192 },
|
||||
},
|
||||
{
|
||||
id: "gpt-old",
|
||||
modelID: "gpt-old",
|
||||
providerID: "openai",
|
||||
name: "GPT Old",
|
||||
capabilities: { tools: false, input: ["text"], output: ["text"] },
|
||||
variants: [],
|
||||
time: { released: 0 },
|
||||
cost: [],
|
||||
status: "deprecated",
|
||||
enabled: true,
|
||||
limit: { context: 1, output: 1 },
|
||||
},
|
||||
] as ModelListOutput["data"],
|
||||
)
|
||||
const providers: ProviderListOutput["data"] = [
|
||||
{ id: "openai", name: "OpenAI", activation: "auto", package: "@ai-sdk/openai" },
|
||||
]
|
||||
|
||||
const models: ModelListOutput["data"] = [
|
||||
{
|
||||
id: "gpt-5",
|
||||
modelID: "gpt-5",
|
||||
providerID: "openai",
|
||||
name: "GPT-5",
|
||||
capabilities: { tools: true, input: ["text", "image"], output: ["text"] },
|
||||
variants: [{ id: "high" }],
|
||||
time: { released: 1 },
|
||||
cost: [{ input: 1, output: 2, cache: { read: 0.1, write: 0.2 } }],
|
||||
status: "active",
|
||||
enabled: true,
|
||||
limit: { context: 128_000, output: 8_192 },
|
||||
},
|
||||
{
|
||||
id: "gpt-old",
|
||||
modelID: "gpt-old",
|
||||
providerID: "openai",
|
||||
name: "GPT Old",
|
||||
capabilities: { tools: false, input: ["text"], output: ["text"] },
|
||||
variants: [],
|
||||
time: { released: 0 },
|
||||
cost: [],
|
||||
status: "deprecated",
|
||||
enabled: true,
|
||||
limit: { context: 1, output: 1 },
|
||||
},
|
||||
]
|
||||
|
||||
const result = normalizeProviderList(providers, models)
|
||||
|
||||
expect(result.connected).toEqual(["openai"])
|
||||
expect(result.default).toEqual({ openai: "gpt-5" })
|
||||
@@ -78,19 +83,58 @@ describe("normalizeProviderList", () => {
|
||||
expect(result.all.get("openai")?.models["gpt-5"]).toMatchObject({
|
||||
id: "gpt-5",
|
||||
providerID: "openai",
|
||||
capabilities: { toolcall: true, attachment: true },
|
||||
capabilities: { toolcall: true, attachment: true, reasoning: true },
|
||||
cost: { input: 1, output: 2 },
|
||||
variants: { high: {} },
|
||||
})
|
||||
})
|
||||
|
||||
test.each([
|
||||
["effort variant", [{ id: "low" }], undefined, true],
|
||||
["toggle variant", [{ id: "none" }, { id: "thinking" }], undefined, true],
|
||||
["reasoningField without variants", [], { reasoningField: "reasoning_content" }, true],
|
||||
["no variants or reasoningField", [], undefined, false],
|
||||
["disabled-only none variant", [{ id: "none" }], undefined, false],
|
||||
] as const)("derives reasoning capability from %s", (_, variants, compatibility, expected) => {
|
||||
const providers: ProviderListOutput["data"] = [
|
||||
{ id: "opencode", name: "OpenCode", activation: "auto", package: "@opencode/ai/providers/openai-compatible" },
|
||||
]
|
||||
|
||||
const models: ModelListOutput["data"] = [
|
||||
{
|
||||
id: "model",
|
||||
modelID: "model",
|
||||
providerID: "opencode",
|
||||
name: "Model",
|
||||
compatibility,
|
||||
capabilities: { tools: true, input: ["text"], output: ["text"] },
|
||||
variants: [...variants],
|
||||
time: { released: 1 },
|
||||
cost: [],
|
||||
status: "active",
|
||||
enabled: true,
|
||||
limit: { context: 128_000, output: 8_192 },
|
||||
},
|
||||
]
|
||||
|
||||
const result = normalizeProviderList(providers, models)
|
||||
|
||||
expect(result.all.get("opencode")?.models.model?.capabilities.reasoning).toBe(expected)
|
||||
})
|
||||
})
|
||||
|
||||
describe("normalizeProjectInfo", () => {
|
||||
test("keeps the project VCS backend", () => {
|
||||
const project = { id: "prj", canonical: "/repo", time: { created: 1, updated: 1, active: 1 }, sandboxes: [] }
|
||||
expect(normalizeProjectInfo({ ...project, vcs: "git" } as Project).vcs).toBe("git")
|
||||
expect(normalizeProjectInfo({ ...project, vcs: "hg" } as Project).vcs).toBe("hg")
|
||||
expect(normalizeProjectInfo(project as Project).vcs).toBeUndefined()
|
||||
const project: Project = {
|
||||
id: "prj",
|
||||
canonical: "/repo",
|
||||
time: { created: 1, updated: 1, active: 1 },
|
||||
sandboxes: [],
|
||||
}
|
||||
|
||||
expect(normalizeProjectInfo({ ...project, vcs: "git" }).vcs).toBe("git")
|
||||
expect(normalizeProjectInfo({ ...project, vcs: "hg" }).vcs).toBe("hg")
|
||||
expect(normalizeProjectInfo(project).vcs).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -10,17 +10,22 @@ const providerCatalogs = new WeakMap<
|
||||
WeakMap<ModelListOutput["data"], ProviderListResponse>
|
||||
>()
|
||||
|
||||
export function normalizeAgentList(input: AgentListOutput["data"] | Agent[]): Agent[] {
|
||||
if (input.every((agent) => !("request" in agent))) return input as Agent[]
|
||||
function isLegacyAgentList(input: AgentListOutput["data"] | Agent[]): input is Agent[] {
|
||||
return input.every((agent) => !("request" in agent))
|
||||
}
|
||||
|
||||
return (input as AgentListOutput["data"]).map((agent) => ({
|
||||
export function normalizeAgentList(input: AgentListOutput["data"] | Agent[]): Agent[] {
|
||||
if (isLegacyAgentList(input)) return input
|
||||
|
||||
return input.map((agent) => ({
|
||||
name: agent.id,
|
||||
description: agent.description,
|
||||
mode: agent.mode,
|
||||
hidden: agent.hidden,
|
||||
temperature:
|
||||
typeof agent.request.settings.temperature === "number" ? agent.request.settings.temperature : undefined,
|
||||
topP: typeof agent.request.settings.topP === "number" ? agent.request.settings.topP : undefined,
|
||||
temperature: Number.isFinite(agent.request.settings.temperature)
|
||||
? Number(agent.request.settings.temperature)
|
||||
: undefined,
|
||||
topP: Number.isFinite(agent.request.settings.topP) ? Number(agent.request.settings.topP) : undefined,
|
||||
color: agent.color,
|
||||
permission: agent.permissions.map((rule) => ({
|
||||
permission: rule.action,
|
||||
@@ -79,7 +84,11 @@ export function normalizeProviderList(
|
||||
family: model.family,
|
||||
capabilities: {
|
||||
temperature: false,
|
||||
reasoning: false,
|
||||
// Always-on reasoning models may expose no selectable variants yet still stream reasoning via reasoningField;
|
||||
// a disabled-only `none` variant does not count as reasoning support.
|
||||
reasoning:
|
||||
model.compatibility?.reasoningField !== undefined ||
|
||||
model.variants.some((variant) => variant.id !== "none"),
|
||||
attachment: model.capabilities.input.some((item) => item !== "text"),
|
||||
toolcall: model.capabilities.tools,
|
||||
input: {
|
||||
|
||||
Reference in New Issue
Block a user