feat(cli): add --model option to run command for model override
Add -m, --model <provider/model> option to oh-my-opencode run command. Allows users to override the model while keeping the agent unchanged. Changes: - Add model?: string to RunOptions interface - Create model-resolver.ts to parse provider/model format - Add model-resolver.test.ts with 7 test cases (TDD) - Add --model CLI option with help text examples - Wire resolveRunModel in runner.ts and pass to promptAsync - Export resolveRunModel from barrel (index.ts) Example usage: bunx oh-my-opencode run --model anthropic/claude-sonnet-4 "Fix the bug" bunx oh-my-opencode run --agent Sisyphus --model openai/gpt-5.4 "Task"
This commit is contained in:
@@ -69,6 +69,7 @@ program
|
|||||||
.passThroughOptions()
|
.passThroughOptions()
|
||||||
.description("Run opencode with todo/background task completion enforcement")
|
.description("Run opencode with todo/background task completion enforcement")
|
||||||
.option("-a, --agent <name>", "Agent to use (default: from CLI/env/config, fallback: Sisyphus)")
|
.option("-a, --agent <name>", "Agent to use (default: from CLI/env/config, fallback: Sisyphus)")
|
||||||
|
.option("-m, --model <provider/model>", "Model override (e.g., anthropic/claude-sonnet-4)")
|
||||||
.option("-d, --directory <path>", "Working directory")
|
.option("-d, --directory <path>", "Working directory")
|
||||||
.option("-p, --port <port>", "Server port (attaches if port already in use)", parseInt)
|
.option("-p, --port <port>", "Server port (attaches if port already in use)", parseInt)
|
||||||
.option("--attach <url>", "Attach to existing opencode server URL")
|
.option("--attach <url>", "Attach to existing opencode server URL")
|
||||||
@@ -86,6 +87,8 @@ Examples:
|
|||||||
$ bunx oh-my-opencode run --json "Fix the bug" | jq .sessionId
|
$ bunx oh-my-opencode run --json "Fix the bug" | jq .sessionId
|
||||||
$ bunx oh-my-opencode run --on-complete "notify-send Done" "Fix the bug"
|
$ bunx oh-my-opencode run --on-complete "notify-send Done" "Fix the bug"
|
||||||
$ bunx oh-my-opencode run --session-id ses_abc123 "Continue the work"
|
$ bunx oh-my-opencode run --session-id ses_abc123 "Continue the work"
|
||||||
|
$ bunx oh-my-opencode run --model anthropic/claude-sonnet-4 "Fix the bug"
|
||||||
|
$ bunx oh-my-opencode run --agent Sisyphus --model openai/gpt-5.4 "Implement feature X"
|
||||||
|
|
||||||
Agent resolution order:
|
Agent resolution order:
|
||||||
1) --agent flag
|
1) --agent flag
|
||||||
@@ -108,6 +111,7 @@ Unlike 'opencode run', this command waits until:
|
|||||||
const runOptions: RunOptions = {
|
const runOptions: RunOptions = {
|
||||||
message,
|
message,
|
||||||
agent: options.agent,
|
agent: options.agent,
|
||||||
|
model: options.model,
|
||||||
directory: options.directory,
|
directory: options.directory,
|
||||||
port: options.port,
|
port: options.port,
|
||||||
attach: options.attach,
|
attach: options.attach,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export { run } from "./runner"
|
export { run } from "./runner"
|
||||||
export { resolveRunAgent } from "./agent-resolver"
|
export { resolveRunAgent } from "./agent-resolver"
|
||||||
|
export { resolveRunModel } from "./model-resolver"
|
||||||
export { createServerConnection } from "./server-connection"
|
export { createServerConnection } from "./server-connection"
|
||||||
export { resolveSession } from "./session-resolver"
|
export { resolveSession } from "./session-resolver"
|
||||||
export { createJsonOutputManager } from "./json-output"
|
export { createJsonOutputManager } from "./json-output"
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
/// <reference types="bun-types" />
|
||||||
|
|
||||||
|
import { describe, it, expect } from "bun:test"
|
||||||
|
import { resolveRunModel } from "./model-resolver"
|
||||||
|
|
||||||
|
describe("resolveRunModel", () => {
|
||||||
|
it("given no model string, when resolved, then returns undefined", () => {
|
||||||
|
// given
|
||||||
|
const modelString = undefined
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = resolveRunModel(modelString)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("given empty string, when resolved, then throws Error", () => {
|
||||||
|
// given
|
||||||
|
const modelString = ""
|
||||||
|
|
||||||
|
// when
|
||||||
|
const resolve = () => resolveRunModel(modelString)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(resolve).toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("given valid 'anthropic/claude-sonnet-4', when resolved, then returns correct object", () => {
|
||||||
|
// given
|
||||||
|
const modelString = "anthropic/claude-sonnet-4"
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = resolveRunModel(modelString)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result).toEqual({ providerID: "anthropic", modelID: "claude-sonnet-4" })
|
||||||
|
})
|
||||||
|
|
||||||
|
it("given nested slashes 'openai/gpt-5.3/preview', when resolved, then modelID is 'gpt-5.3/preview'", () => {
|
||||||
|
// given
|
||||||
|
const modelString = "openai/gpt-5.3/preview"
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = resolveRunModel(modelString)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result).toEqual({ providerID: "openai", modelID: "gpt-5.3/preview" })
|
||||||
|
})
|
||||||
|
|
||||||
|
it("given no slash 'claude-sonnet-4', when resolved, then throws Error", () => {
|
||||||
|
// given
|
||||||
|
const modelString = "claude-sonnet-4"
|
||||||
|
|
||||||
|
// when
|
||||||
|
const resolve = () => resolveRunModel(modelString)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(resolve).toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("given empty provider '/claude-sonnet-4', when resolved, then throws Error", () => {
|
||||||
|
// given
|
||||||
|
const modelString = "/claude-sonnet-4"
|
||||||
|
|
||||||
|
// when
|
||||||
|
const resolve = () => resolveRunModel(modelString)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(resolve).toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("given trailing slash 'anthropic/', when resolved, then throws Error", () => {
|
||||||
|
// given
|
||||||
|
const modelString = "anthropic/"
|
||||||
|
|
||||||
|
// when
|
||||||
|
const resolve = () => resolveRunModel(modelString)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(resolve).toThrow()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
export function resolveRunModel(
|
||||||
|
modelString?: string
|
||||||
|
): { providerID: string; modelID: string } | undefined {
|
||||||
|
if (modelString === undefined) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const trimmed = modelString.trim()
|
||||||
|
if (trimmed.length === 0) {
|
||||||
|
throw new Error("Model string cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = trimmed.split("/")
|
||||||
|
if (parts.length < 2) {
|
||||||
|
throw new Error("Model string must be in 'provider/model' format")
|
||||||
|
}
|
||||||
|
|
||||||
|
const providerID = parts[0]
|
||||||
|
if (providerID.length === 0) {
|
||||||
|
throw new Error("Provider cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
const modelID = parts.slice(1).join("/")
|
||||||
|
if (modelID.length === 0) {
|
||||||
|
throw new Error("Model ID cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
return { providerID, modelID }
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import { resolveSession } from "./session-resolver"
|
|||||||
import { createJsonOutputManager } from "./json-output"
|
import { createJsonOutputManager } from "./json-output"
|
||||||
import { executeOnCompleteHook } from "./on-complete-hook"
|
import { executeOnCompleteHook } from "./on-complete-hook"
|
||||||
import { resolveRunAgent } from "./agent-resolver"
|
import { resolveRunAgent } from "./agent-resolver"
|
||||||
|
import { resolveRunModel } from "./model-resolver"
|
||||||
import { pollForCompletion } from "./poll-for-completion"
|
import { pollForCompletion } from "./poll-for-completion"
|
||||||
import { loadAgentProfileColors } from "./agent-profile-colors"
|
import { loadAgentProfileColors } from "./agent-profile-colors"
|
||||||
import { suppressRunInput } from "./stdin-suppression"
|
import { suppressRunInput } from "./stdin-suppression"
|
||||||
@@ -46,6 +47,7 @@ export async function run(options: RunOptions): Promise<number> {
|
|||||||
|
|
||||||
const pluginConfig = loadPluginConfig(directory, { command: "run" })
|
const pluginConfig = loadPluginConfig(directory, { command: "run" })
|
||||||
const resolvedAgent = resolveRunAgent(options, pluginConfig)
|
const resolvedAgent = resolveRunAgent(options, pluginConfig)
|
||||||
|
const resolvedModel = resolveRunModel(options.model)
|
||||||
const abortController = new AbortController()
|
const abortController = new AbortController()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -78,6 +80,10 @@ export async function run(options: RunOptions): Promise<number> {
|
|||||||
|
|
||||||
console.log(pc.dim(`Session: ${sessionID}`))
|
console.log(pc.dim(`Session: ${sessionID}`))
|
||||||
|
|
||||||
|
if (resolvedModel) {
|
||||||
|
console.log(pc.dim(`Model: ${resolvedModel.providerID}/${resolvedModel.modelID}`))
|
||||||
|
}
|
||||||
|
|
||||||
const ctx: RunContext = {
|
const ctx: RunContext = {
|
||||||
client,
|
client,
|
||||||
sessionID,
|
sessionID,
|
||||||
@@ -96,6 +102,7 @@ export async function run(options: RunOptions): Promise<number> {
|
|||||||
path: { id: sessionID },
|
path: { id: sessionID },
|
||||||
body: {
|
body: {
|
||||||
agent: resolvedAgent,
|
agent: resolvedAgent,
|
||||||
|
...(resolvedModel ? { model: resolvedModel } : {}),
|
||||||
tools: {
|
tools: {
|
||||||
question: false,
|
question: false,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export type { OpencodeClient }
|
|||||||
export interface RunOptions {
|
export interface RunOptions {
|
||||||
message: string
|
message: string
|
||||||
agent?: string
|
agent?: string
|
||||||
|
model?: string
|
||||||
timestamp?: boolean
|
timestamp?: boolean
|
||||||
verbose?: boolean
|
verbose?: boolean
|
||||||
directory?: string
|
directory?: string
|
||||||
|
|||||||
Reference in New Issue
Block a user