mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-27 00:31:00 +00:00
test(acp-next): add config option helpers (#29234)
This commit is contained in:
parent
2fce3c1370
commit
fe482fe3dd
2 changed files with 432 additions and 0 deletions
203
packages/opencode/src/acp-next/config-option.ts
Normal file
203
packages/opencode/src/acp-next/config-option.ts
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
import type { SessionConfigOption } from "@agentclientprotocol/sdk"
|
||||
|
||||
export const DEFAULT_VARIANT_VALUE = "default"
|
||||
|
||||
export type ConfigOptionModel = {
|
||||
id: string
|
||||
name: string
|
||||
variants?: Record<string, Record<string, unknown>>
|
||||
}
|
||||
|
||||
export type ConfigOptionProvider = {
|
||||
id: string
|
||||
name: string
|
||||
models: Record<string, ConfigOptionModel>
|
||||
}
|
||||
|
||||
export type ConfigOptionMode = {
|
||||
id: string
|
||||
name: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export type ModelSelection = {
|
||||
model: {
|
||||
providerID: string
|
||||
modelID: string
|
||||
}
|
||||
variant?: string
|
||||
}
|
||||
|
||||
export function buildModelSelectOption(input: {
|
||||
providers: readonly ConfigOptionProvider[]
|
||||
currentModel: ModelSelection["model"]
|
||||
currentVariant?: string
|
||||
includeVariants?: boolean
|
||||
}): SessionConfigOption {
|
||||
return {
|
||||
id: "model",
|
||||
name: "Model",
|
||||
category: "model",
|
||||
type: "select",
|
||||
currentValue: formatCurrentModelId({
|
||||
model: input.currentModel,
|
||||
variant: input.currentVariant,
|
||||
variants: variantsForModel(input.providers, input.currentModel),
|
||||
includeVariant: input.includeVariants ?? false,
|
||||
}),
|
||||
options: buildModelSelectOptions(input.providers, { includeVariants: input.includeVariants ?? false }),
|
||||
}
|
||||
}
|
||||
|
||||
export function buildEffortSelectOption(input: {
|
||||
variants: readonly string[]
|
||||
currentVariant?: string
|
||||
}): SessionConfigOption | undefined {
|
||||
if (input.variants.length === 0) return undefined
|
||||
|
||||
return {
|
||||
id: "effort",
|
||||
name: "Effort",
|
||||
description: "Available effort levels for this model",
|
||||
category: "thought_level",
|
||||
type: "select",
|
||||
currentValue: selectVariant(input.currentVariant, input.variants),
|
||||
options: input.variants.map((variant) => ({
|
||||
value: variant,
|
||||
name: formatVariantName(variant),
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
export function buildModeSelectOption(input: {
|
||||
modes: readonly ConfigOptionMode[]
|
||||
currentModeId: string
|
||||
}): SessionConfigOption {
|
||||
return {
|
||||
id: "mode",
|
||||
name: "Session Mode",
|
||||
category: "mode",
|
||||
type: "select",
|
||||
currentValue: input.currentModeId,
|
||||
options: input.modes.map((mode) => ({
|
||||
value: mode.id,
|
||||
name: mode.name,
|
||||
...(mode.description ? { description: mode.description } : {}),
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
export function buildConfigOptions(input: {
|
||||
providers: readonly ConfigOptionProvider[]
|
||||
currentModel: ModelSelection["model"]
|
||||
currentVariant?: string
|
||||
includeModelVariants?: boolean
|
||||
modes?: readonly ConfigOptionMode[]
|
||||
currentModeId?: string
|
||||
}): SessionConfigOption[] {
|
||||
const variants = variantsForModel(input.providers, input.currentModel)
|
||||
const effort = buildEffortSelectOption({ variants, currentVariant: input.currentVariant })
|
||||
|
||||
return [
|
||||
buildModelSelectOption({
|
||||
providers: input.providers,
|
||||
currentModel: input.currentModel,
|
||||
currentVariant: input.currentVariant,
|
||||
includeVariants: input.includeModelVariants ?? false,
|
||||
}),
|
||||
...(effort ? [effort] : []),
|
||||
...(input.modes && input.currentModeId
|
||||
? [buildModeSelectOption({ modes: input.modes, currentModeId: input.currentModeId })]
|
||||
: []),
|
||||
]
|
||||
}
|
||||
|
||||
export function parseModelSelection(modelId: string, providers: readonly ConfigOptionProvider[]): ModelSelection {
|
||||
const provider = providers.find((item) => modelId.startsWith(`${item.id}/`))
|
||||
if (provider) {
|
||||
const modelID = modelId.slice(provider.id.length + 1)
|
||||
if (provider.models[modelID]) {
|
||||
return { model: { providerID: provider.id, modelID } }
|
||||
}
|
||||
|
||||
const separator = modelID.lastIndexOf("/")
|
||||
if (separator > -1) {
|
||||
const baseModelID = modelID.slice(0, separator)
|
||||
const variant = modelID.slice(separator + 1)
|
||||
if (provider.models[baseModelID]?.variants?.[variant]) {
|
||||
return { model: { providerID: provider.id, modelID: baseModelID }, variant }
|
||||
}
|
||||
}
|
||||
|
||||
return { model: { providerID: provider.id, modelID } }
|
||||
}
|
||||
|
||||
const separator = modelId.indexOf("/")
|
||||
if (separator === -1) {
|
||||
return { model: { providerID: modelId, modelID: "" } }
|
||||
}
|
||||
|
||||
return {
|
||||
model: {
|
||||
providerID: modelId.slice(0, separator),
|
||||
modelID: modelId.slice(separator + 1),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function formatCurrentModelId(input: {
|
||||
model: ModelSelection["model"]
|
||||
variant?: string
|
||||
variants?: readonly string[]
|
||||
includeVariant?: boolean
|
||||
}) {
|
||||
const base = `${input.model.providerID}/${input.model.modelID}`
|
||||
if (!input.includeVariant || !input.variants?.length) return base
|
||||
return `${base}/${selectVariant(input.variant, input.variants)}`
|
||||
}
|
||||
|
||||
export function formatVariantName(variant: string) {
|
||||
return variant
|
||||
.split(/[_-]/)
|
||||
.map((part) => (part ? part.charAt(0).toUpperCase() + part.slice(1) : part))
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
function buildModelSelectOptions(
|
||||
providers: readonly ConfigOptionProvider[],
|
||||
options: { includeVariants: boolean },
|
||||
): Array<{ value: string; name: string }> {
|
||||
return providers.flatMap((provider) =>
|
||||
Object.values(provider.models)
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.flatMap((model) => {
|
||||
const base = {
|
||||
value: `${provider.id}/${model.id}`,
|
||||
name: `${provider.name}/${model.name}`,
|
||||
}
|
||||
if (!options.includeVariants || !model.variants) return [base]
|
||||
|
||||
return [
|
||||
base,
|
||||
...Object.keys(model.variants)
|
||||
.filter((variant) => variant !== DEFAULT_VARIANT_VALUE)
|
||||
.map((variant) => ({
|
||||
value: `${provider.id}/${model.id}/${variant}`,
|
||||
name: `${provider.name}/${model.name} (${formatVariantName(variant)})`,
|
||||
})),
|
||||
]
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function variantsForModel(providers: readonly ConfigOptionProvider[], model: ModelSelection["model"]) {
|
||||
return Object.keys(
|
||||
providers.find((provider) => provider.id === model.providerID)?.models[model.modelID]?.variants ?? {},
|
||||
)
|
||||
}
|
||||
|
||||
function selectVariant(variant: string | undefined, variants: readonly string[]) {
|
||||
if (variant && variants.includes(variant)) return variant
|
||||
if (variants.includes(DEFAULT_VARIANT_VALUE)) return DEFAULT_VARIANT_VALUE
|
||||
return variants[0]
|
||||
}
|
||||
229
packages/opencode/test/acp-next/config-option.test.ts
Normal file
229
packages/opencode/test/acp-next/config-option.test.ts
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
import { describe, expect, test } from "bun:test"
|
||||
import {
|
||||
buildConfigOptions,
|
||||
buildEffortSelectOption,
|
||||
buildModeSelectOption,
|
||||
buildModelSelectOption,
|
||||
formatCurrentModelId,
|
||||
formatVariantName,
|
||||
parseModelSelection,
|
||||
type ConfigOptionProvider,
|
||||
} from "@/acp-next/config-option"
|
||||
|
||||
const providers: ConfigOptionProvider[] = [
|
||||
{
|
||||
id: "anthropic",
|
||||
name: "Anthropic",
|
||||
models: {
|
||||
"claude/sonnet-4": {
|
||||
id: "claude/sonnet-4",
|
||||
name: "Claude Sonnet 4",
|
||||
variants: {
|
||||
default: {},
|
||||
high: {},
|
||||
"very-high": {},
|
||||
},
|
||||
},
|
||||
"claude-haiku": {
|
||||
id: "claude-haiku",
|
||||
name: "Claude Haiku",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "openai",
|
||||
name: "OpenAI",
|
||||
models: {
|
||||
"gpt-5": {
|
||||
id: "gpt-5",
|
||||
name: "GPT-5",
|
||||
variants: {
|
||||
minimal: {},
|
||||
low: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
describe("acp-next config options", () => {
|
||||
test("builds the model select option with ACP verifier category", () => {
|
||||
expect(
|
||||
buildModelSelectOption({
|
||||
providers,
|
||||
currentModel: { providerID: "anthropic", modelID: "claude/sonnet-4" },
|
||||
currentVariant: "high",
|
||||
}),
|
||||
).toEqual({
|
||||
id: "model",
|
||||
name: "Model",
|
||||
category: "model",
|
||||
type: "select",
|
||||
currentValue: "anthropic/claude/sonnet-4",
|
||||
options: [
|
||||
{ value: "anthropic/claude-haiku", name: "Anthropic/Claude Haiku" },
|
||||
{ value: "anthropic/claude/sonnet-4", name: "Anthropic/Claude Sonnet 4" },
|
||||
{ value: "openai/gpt-5", name: "OpenAI/GPT-5" },
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
test("includes variant ids in the model option only when requested", () => {
|
||||
const option = buildModelSelectOption({
|
||||
providers,
|
||||
currentModel: { providerID: "anthropic", modelID: "claude/sonnet-4" },
|
||||
currentVariant: "high",
|
||||
includeVariants: true,
|
||||
})
|
||||
|
||||
expect(option.currentValue).toBe("anthropic/claude/sonnet-4/high")
|
||||
if (option.type !== "select") throw new Error("expected select option")
|
||||
expect(option.options).toContainEqual({
|
||||
value: "anthropic/claude/sonnet-4/high",
|
||||
name: "Anthropic/Claude Sonnet 4 (High)",
|
||||
})
|
||||
expect(option.options).not.toContainEqual({
|
||||
value: "anthropic/claude/sonnet-4/default",
|
||||
name: "Anthropic/Claude Sonnet 4 (Default)",
|
||||
})
|
||||
})
|
||||
|
||||
test("builds effort option from variants and falls back to default when current variant is invalid", () => {
|
||||
expect(buildEffortSelectOption({ variants: ["low", "default", "high"], currentVariant: "missing" })).toEqual({
|
||||
id: "effort",
|
||||
name: "Effort",
|
||||
description: "Available effort levels for this model",
|
||||
category: "thought_level",
|
||||
type: "select",
|
||||
currentValue: "default",
|
||||
options: [
|
||||
{ value: "low", name: "Low" },
|
||||
{ value: "default", name: "Default" },
|
||||
{ value: "high", name: "High" },
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
test("effort fallback uses the first variant when default is absent", () => {
|
||||
expect(buildEffortSelectOption({ variants: ["minimal", "low"], currentVariant: "missing" })?.currentValue).toBe(
|
||||
"minimal",
|
||||
)
|
||||
})
|
||||
|
||||
test("omits effort option when there are no variants", () => {
|
||||
expect(buildEffortSelectOption({ variants: [] })).toBeUndefined()
|
||||
})
|
||||
|
||||
test("builds the mode select option with descriptions when present", () => {
|
||||
expect(
|
||||
buildModeSelectOption({
|
||||
currentModeId: "build",
|
||||
modes: [
|
||||
{ id: "build", name: "Build", description: "Make code changes" },
|
||||
{ id: "plan", name: "Plan" },
|
||||
],
|
||||
}),
|
||||
).toEqual({
|
||||
id: "mode",
|
||||
name: "Session Mode",
|
||||
category: "mode",
|
||||
type: "select",
|
||||
currentValue: "build",
|
||||
options: [
|
||||
{ value: "build", name: "Build", description: "Make code changes" },
|
||||
{ value: "plan", name: "Plan" },
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
test("builds full config options with model, effort, and mode in stable order", () => {
|
||||
const options = buildConfigOptions({
|
||||
providers,
|
||||
currentModel: { providerID: "anthropic", modelID: "claude/sonnet-4" },
|
||||
currentVariant: "very-high",
|
||||
modes: [
|
||||
{ id: "build", name: "Build" },
|
||||
{ id: "plan", name: "Plan" },
|
||||
],
|
||||
currentModeId: "plan",
|
||||
})
|
||||
|
||||
expect(options.map((option) => option.id)).toEqual(["model", "effort", "mode"])
|
||||
expect(options.map((option) => option.category)).toEqual(["model", "thought_level", "mode"])
|
||||
expect(options[1]?.currentValue).toBe("very-high")
|
||||
})
|
||||
|
||||
test("full config options omit effort for models without variants", () => {
|
||||
expect(
|
||||
buildConfigOptions({
|
||||
providers,
|
||||
currentModel: { providerID: "anthropic", modelID: "claude-haiku" },
|
||||
}).map((option) => option.id),
|
||||
).toEqual(["model"])
|
||||
})
|
||||
|
||||
test("parses provider/model selections", () => {
|
||||
expect(parseModelSelection("openai/gpt-5", providers)).toEqual({
|
||||
model: { providerID: "openai", modelID: "gpt-5" },
|
||||
})
|
||||
})
|
||||
|
||||
test("parses provider/model/variant selections when the base model exposes that variant", () => {
|
||||
expect(parseModelSelection("openai/gpt-5/low", providers)).toEqual({
|
||||
model: { providerID: "openai", modelID: "gpt-5" },
|
||||
variant: "low",
|
||||
})
|
||||
})
|
||||
|
||||
test("prefers exact slash-containing model ids before treating the tail as a variant", () => {
|
||||
expect(parseModelSelection("anthropic/claude/sonnet-4", providers)).toEqual({
|
||||
model: { providerID: "anthropic", modelID: "claude/sonnet-4" },
|
||||
})
|
||||
})
|
||||
|
||||
test("parses trailing variants for slash-containing model ids", () => {
|
||||
expect(parseModelSelection("anthropic/claude/sonnet-4/high", providers)).toEqual({
|
||||
model: { providerID: "anthropic", modelID: "claude/sonnet-4" },
|
||||
variant: "high",
|
||||
})
|
||||
})
|
||||
|
||||
test("keeps unknown trailing segments in the model id when they are not valid variants", () => {
|
||||
expect(parseModelSelection("anthropic/claude/sonnet-4/missing", providers)).toEqual({
|
||||
model: { providerID: "anthropic", modelID: "claude/sonnet-4/missing" },
|
||||
})
|
||||
})
|
||||
|
||||
test("formats current model ids with and without selected variants", () => {
|
||||
expect(
|
||||
formatCurrentModelId({
|
||||
model: { providerID: "openai", modelID: "gpt-5" },
|
||||
variant: "low",
|
||||
variants: ["minimal", "low"],
|
||||
}),
|
||||
).toBe("openai/gpt-5")
|
||||
expect(
|
||||
formatCurrentModelId({
|
||||
model: { providerID: "openai", modelID: "gpt-5" },
|
||||
variant: "low",
|
||||
variants: ["minimal", "low"],
|
||||
includeVariant: true,
|
||||
}),
|
||||
).toBe("openai/gpt-5/low")
|
||||
})
|
||||
|
||||
test("formats current model ids with variant fallback", () => {
|
||||
expect(
|
||||
formatCurrentModelId({
|
||||
model: { providerID: "anthropic", modelID: "claude/sonnet-4" },
|
||||
variant: "missing",
|
||||
variants: ["default", "high"],
|
||||
includeVariant: true,
|
||||
}),
|
||||
).toBe("anthropic/claude/sonnet-4/default")
|
||||
})
|
||||
|
||||
test("formats variant names for display", () => {
|
||||
expect(formatVariantName("very_high-effort")).toBe("Very High Effort")
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue