mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-10 01:39:36 +00:00
refactor(cli): parse variants from model refs
This commit is contained in:
parent
cccd013801
commit
67f2393f83
10 changed files with 79 additions and 34 deletions
|
|
@ -171,7 +171,7 @@ export const Commands = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCO
|
|||
),
|
||||
model: Flag.string("model").pipe(
|
||||
Flag.withAlias("m"),
|
||||
Flag.withDescription("Model to use in the format provider/model"),
|
||||
Flag.withDescription("Model to use in the format provider/model#variant"),
|
||||
Flag.optional,
|
||||
),
|
||||
agent: Flag.string("agent").pipe(Flag.withDescription("Agent to use"), Flag.optional),
|
||||
|
|
@ -185,7 +185,6 @@ export const Commands = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCO
|
|||
Flag.atMost(100),
|
||||
),
|
||||
title: Flag.string("title").pipe(Flag.withDescription("Session title"), Flag.optional),
|
||||
variant: Flag.string("variant").pipe(Flag.withDescription("Model variant"), Flag.optional),
|
||||
thinking: Flag.boolean("thinking").pipe(
|
||||
Flag.withDescription("Show thinking blocks"),
|
||||
Flag.withDefault(false),
|
||||
|
|
@ -195,10 +194,6 @@ export const Commands = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCO
|
|||
Flag.withDefault(false),
|
||||
),
|
||||
yolo: Flag.boolean("yolo").pipe(Flag.withDefault(false), Flag.withHidden),
|
||||
dangerouslySkipPermissions: Flag.boolean("dangerously-skip-permissions").pipe(
|
||||
Flag.withDefault(false),
|
||||
Flag.withHidden,
|
||||
),
|
||||
},
|
||||
}),
|
||||
Spec.make("service", {
|
||||
|
|
|
|||
|
|
@ -23,9 +23,8 @@ export default Runtime.handler(Commands.commands.run, (input) =>
|
|||
format: input.format,
|
||||
file: [...input.file],
|
||||
title: Option.getOrUndefined(input.title),
|
||||
variant: Option.getOrUndefined(input.variant),
|
||||
thinking: input.thinking,
|
||||
dangerouslySkipPermissions: input.auto || input.yolo || input.dangerouslySkipPermissions,
|
||||
auto: input.auto || input.yolo,
|
||||
}),
|
||||
)
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -3,5 +3,6 @@ export {
|
|||
runNonInteractive,
|
||||
mergeInput as mergeNonInteractiveInput,
|
||||
pickRunModel,
|
||||
parseRunModel,
|
||||
type RunCommandInput,
|
||||
} from "./run"
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ type Input = {
|
|||
variant?: string
|
||||
thinking: boolean
|
||||
format: "default" | "json"
|
||||
dangerouslySkipPermissions: boolean
|
||||
auto: boolean
|
||||
/** True when the client is attached to a shared server rather than an exclusive in-process one. */
|
||||
attached: boolean
|
||||
renderTool: (part: ToolPart) => Promise<void>
|
||||
|
|
@ -91,7 +91,7 @@ export async function runNonInteractivePrompt(input: Input) {
|
|||
}
|
||||
|
||||
const replyPermission = async (request: { id: string; action: string; resources: ReadonlyArray<string> }) => {
|
||||
if (!input.dangerouslySkipPermissions) {
|
||||
if (!input.auto) {
|
||||
permissionRejected = true
|
||||
UI.println(
|
||||
UI.Style.TEXT_WARNING_BOLD + "!",
|
||||
|
|
@ -103,10 +103,10 @@ export async function runNonInteractivePrompt(input: Input) {
|
|||
.reply({
|
||||
sessionID: input.sessionID,
|
||||
requestID: request.id,
|
||||
reply: input.dangerouslySkipPermissions ? "once" : "reject",
|
||||
reply: input.auto ? "once" : "reject",
|
||||
})
|
||||
.catch(() => {})
|
||||
if (!input.dangerouslySkipPermissions) {
|
||||
if (!input.auto) {
|
||||
await input.client.session.interrupt({ sessionID: input.sessionID }).catch(() => {})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { Service } from "@opencode-ai/client/effect"
|
||||
import { OpenCode, type OpenCodeClient } from "@opencode-ai/client/promise"
|
||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||
import { Model } from "@opencode-ai/schema/model"
|
||||
import type { ToolPart } from "@opencode-ai/sdk/v2"
|
||||
import { open } from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
|
|
@ -21,9 +22,8 @@ export type RunCommandInput = {
|
|||
format: "default" | "json"
|
||||
file: string[]
|
||||
title?: string
|
||||
variant?: string
|
||||
thinking?: boolean
|
||||
dangerouslySkipPermissions?: boolean
|
||||
auto?: boolean
|
||||
}
|
||||
|
||||
type FilePart = {
|
||||
|
|
@ -62,7 +62,9 @@ async function execute(input: RunCommandInput, prepared: Prepared, endpoint: Ser
|
|||
const session = await selectSession(client, requestedDirectory, input)
|
||||
const cwd = session?.location.directory ?? requestedDirectory
|
||||
const workspace = session?.location.workspaceID
|
||||
const explicitModel = parseModel(input.model)
|
||||
const explicit = parseRunModel(input.model)
|
||||
const explicitModel = explicit?.model
|
||||
const variant = explicit?.variant
|
||||
const sessionModel = session?.model ? { providerID: session.model.providerID, modelID: session.model.id } : undefined
|
||||
const defaultModel =
|
||||
!explicitModel && !sessionModel
|
||||
|
|
@ -70,8 +72,8 @@ async function execute(input: RunCommandInput, prepared: Prepared, endpoint: Ser
|
|||
.default({ location: { directory: cwd, workspace } })
|
||||
.then((result) => (result.data ? { providerID: result.data.providerID, modelID: result.data.id } : undefined))
|
||||
: undefined
|
||||
const model = pickRunModel(explicitModel, input.variant, sessionModel, defaultModel)
|
||||
if (input.variant && !model)
|
||||
const model = pickRunModel(explicitModel, variant, sessionModel, defaultModel)
|
||||
if (variant && !model)
|
||||
return reportError(input, "Cannot select a variant before selecting a model", session?.id)
|
||||
if (model) {
|
||||
await waitForCatalogReady({ sdk: client, directory: cwd, workspace, model })
|
||||
|
|
@ -84,7 +86,7 @@ async function execute(input: RunCommandInput, prepared: Prepared, endpoint: Ser
|
|||
session ??
|
||||
(await client.session.create({
|
||||
agent,
|
||||
model: model ? { providerID: model.providerID, id: model.modelID, variant: input.variant } : undefined,
|
||||
model: model ? { providerID: model.providerID, id: model.modelID, variant } : undefined,
|
||||
location: { directory: cwd },
|
||||
}))
|
||||
if (!session && input.title !== undefined) {
|
||||
|
|
@ -101,10 +103,10 @@ async function execute(input: RunCommandInput, prepared: Prepared, endpoint: Ser
|
|||
files: prepared.files,
|
||||
agent,
|
||||
model,
|
||||
variant: input.variant,
|
||||
variant,
|
||||
thinking: input.thinking ?? false,
|
||||
format: input.format,
|
||||
dangerouslySkipPermissions: input.dangerouslySkipPermissions ?? false,
|
||||
auto: input.auto ?? false,
|
||||
attached: true,
|
||||
renderTool,
|
||||
renderToolError,
|
||||
|
|
@ -142,12 +144,13 @@ function localDirectory(root: string) {
|
|||
}
|
||||
}
|
||||
|
||||
function parseModel(value?: string) {
|
||||
export function parseRunModel(value?: string) {
|
||||
if (!value) return
|
||||
const [providerID, ...rest] = value.split("/")
|
||||
const modelID = rest.join("/")
|
||||
if (!providerID || !modelID) fail("--model must use the format provider/model")
|
||||
return { providerID, modelID }
|
||||
const ref = Model.Ref.parse(value)
|
||||
return {
|
||||
model: { providerID: ref.providerID, modelID: ref.id },
|
||||
variant: ref.variant,
|
||||
}
|
||||
}
|
||||
|
||||
async function validateAgent(client: OpenCodeClient, directory: string, name?: string) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { describe, expect, test } from "bun:test"
|
||||
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||
import path from "node:path"
|
||||
import { mergeInteractiveInput, mergeNonInteractiveInput, pickRunModel } from "../src/mini"
|
||||
import { mergeInteractiveInput, mergeNonInteractiveInput, parseRunModel, pickRunModel } from "../src/mini"
|
||||
|
||||
async function cli(args: string[]) {
|
||||
const child = Bun.spawn([process.execPath, "run", "src/index.ts", ...args], {
|
||||
|
|
@ -39,6 +39,12 @@ describe("mini command", () => {
|
|||
).toEqual({ providerID: "session-provider", modelID: "session-model" })
|
||||
})
|
||||
|
||||
test("parses model variants from the model reference", () => {
|
||||
expect(JSON.stringify(parseRunModel("openrouter/openai/gpt-5#high"))).toBe(
|
||||
JSON.stringify({ model: { providerID: "openrouter", modelID: "openai/gpt-5" }, variant: "high" }),
|
||||
)
|
||||
})
|
||||
|
||||
test("is registered in the preview CLI", async () => {
|
||||
const result = await cli(["--help"])
|
||||
|
||||
|
|
@ -52,6 +58,7 @@ describe("mini command", () => {
|
|||
|
||||
expect(result.exitCode).toBe(0)
|
||||
expect(result.stdout).toContain("--server string")
|
||||
expect(result.stdout).not.toContain("--variant")
|
||||
expect(result.stdout).not.toContain("--attach")
|
||||
expect(result.stdout).not.toContain("--command")
|
||||
})
|
||||
|
|
|
|||
|
|
@ -27,12 +27,10 @@ export const Selection = Schema.Union([Short, Explicit])
|
|||
.annotate({ identifier: "Config.ModelSelection" })
|
||||
|
||||
function parse(input: string): Selection {
|
||||
const providerEnd = input.indexOf("/")
|
||||
const variantStart = input.lastIndexOf("#")
|
||||
const hasVariant = variantStart > providerEnd
|
||||
const ref = Model.Ref.parse(input)
|
||||
return {
|
||||
providerID: Provider.ID.make(input.slice(0, providerEnd)),
|
||||
model: Model.ID.make(input.slice(providerEnd + 1, hasVariant ? variantStart : undefined)),
|
||||
...(hasVariant ? { variant: Model.VariantID.make(input.slice(variantStart + 1)) } : {}),
|
||||
providerID: ref.providerID,
|
||||
model: ref.id,
|
||||
...(ref.variant ? { variant: ref.variant } : {}),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ async function run(input: { turn: (inputID: string) => V2Event[]; pendingForms?:
|
|||
files: [],
|
||||
thinking: false,
|
||||
format: "default",
|
||||
dangerouslySkipPermissions: false,
|
||||
auto: false,
|
||||
attached: input.attached ?? false,
|
||||
renderTool: () => Promise.resolve(),
|
||||
renderToolError: () => Promise.resolve(),
|
||||
|
|
|
|||
|
|
@ -15,7 +15,27 @@ export const Ref = Schema.Struct({
|
|||
id: ID,
|
||||
providerID: Provider.ID,
|
||||
variant: VariantID.pipe(optional),
|
||||
}).annotate({ identifier: "Model.Ref" })
|
||||
})
|
||||
.annotate({ identifier: "Model.Ref" })
|
||||
.pipe(
|
||||
statics((schema) => ({
|
||||
parse: (input: string) => {
|
||||
const providerEnd = input.indexOf("/")
|
||||
if (providerEnd <= 0) throw new Error(`Invalid model reference: ${input}`)
|
||||
const providerID = input.slice(0, providerEnd)
|
||||
const variantStart = input.indexOf("#", providerEnd + 1)
|
||||
const id = input.slice(providerEnd + 1, variantStart === -1 ? undefined : variantStart)
|
||||
const variant = variantStart === -1 ? undefined : input.slice(variantStart + 1)
|
||||
if (!id || providerID.includes("#") || (variant !== undefined && (!variant || variant.includes("#"))))
|
||||
throw new Error(`Invalid model reference: ${input}`)
|
||||
return schema.make({
|
||||
providerID: Provider.ID.make(providerID),
|
||||
id: ID.make(id),
|
||||
...(variant ? { variant: VariantID.make(variant) } : {}),
|
||||
})
|
||||
},
|
||||
})),
|
||||
)
|
||||
export interface Ref extends Schema.Schema.Type<typeof Ref> {}
|
||||
|
||||
export const Family = Schema.String.pipe(Schema.brand("Model.Family"))
|
||||
|
|
|
|||
22
packages/schema/test/model.test.ts
Normal file
22
packages/schema/test/model.test.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { describe, expect, test } from "bun:test"
|
||||
import { Model } from "../src/model.js"
|
||||
|
||||
describe("Model.Ref", () => {
|
||||
test("parses model references with optional variants", () => {
|
||||
const variant = Model.Ref.parse("openrouter/openai/gpt-5#high")
|
||||
expect(String(variant.providerID)).toBe("openrouter")
|
||||
expect(String(variant.id)).toBe("openai/gpt-5")
|
||||
expect(String(variant.variant)).toBe("high")
|
||||
|
||||
const standard = Model.Ref.parse("anthropic/claude-sonnet")
|
||||
expect(String(standard.providerID)).toBe("anthropic")
|
||||
expect(String(standard.id)).toBe("claude-sonnet")
|
||||
expect(standard.variant).toBeUndefined()
|
||||
})
|
||||
|
||||
test("rejects malformed model references", () => {
|
||||
expect(() => Model.Ref.parse("gpt-5")).toThrow()
|
||||
expect(() => Model.Ref.parse("openai/gpt-5#")).toThrow()
|
||||
expect(() => Model.Ref.parse("openai/gpt-5#high#extra")).toThrow()
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue