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:
YeonGyu-Kim
2026-03-11 17:23:29 +09:00
parent d24ec336e5
commit 0eb447113e
6 changed files with 125 additions and 0 deletions

View File

@@ -69,6 +69,7 @@ program
.passThroughOptions()
.description("Run opencode with todo/background task completion enforcement")
.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("-p, --port <port>", "Server port (attaches if port already in use)", parseInt)
.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 --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 --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:
1) --agent flag
@@ -108,6 +111,7 @@ Unlike 'opencode run', this command waits until:
const runOptions: RunOptions = {
message,
agent: options.agent,
model: options.model,
directory: options.directory,
port: options.port,
attach: options.attach,

View File

@@ -1,5 +1,6 @@
export { run } from "./runner"
export { resolveRunAgent } from "./agent-resolver"
export { resolveRunModel } from "./model-resolver"
export { createServerConnection } from "./server-connection"
export { resolveSession } from "./session-resolver"
export { createJsonOutputManager } from "./json-output"

View File

@@ -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()
})
})

View File

@@ -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 }
}

View File

@@ -7,6 +7,7 @@ import { resolveSession } from "./session-resolver"
import { createJsonOutputManager } from "./json-output"
import { executeOnCompleteHook } from "./on-complete-hook"
import { resolveRunAgent } from "./agent-resolver"
import { resolveRunModel } from "./model-resolver"
import { pollForCompletion } from "./poll-for-completion"
import { loadAgentProfileColors } from "./agent-profile-colors"
import { suppressRunInput } from "./stdin-suppression"
@@ -46,6 +47,7 @@ export async function run(options: RunOptions): Promise<number> {
const pluginConfig = loadPluginConfig(directory, { command: "run" })
const resolvedAgent = resolveRunAgent(options, pluginConfig)
const resolvedModel = resolveRunModel(options.model)
const abortController = new AbortController()
try {
@@ -78,6 +80,10 @@ export async function run(options: RunOptions): Promise<number> {
console.log(pc.dim(`Session: ${sessionID}`))
if (resolvedModel) {
console.log(pc.dim(`Model: ${resolvedModel.providerID}/${resolvedModel.modelID}`))
}
const ctx: RunContext = {
client,
sessionID,
@@ -96,6 +102,7 @@ export async function run(options: RunOptions): Promise<number> {
path: { id: sessionID },
body: {
agent: resolvedAgent,
...(resolvedModel ? { model: resolvedModel } : {}),
tools: {
question: false,
},

View File

@@ -4,6 +4,7 @@ export type { OpencodeClient }
export interface RunOptions {
message: string
agent?: string
model?: string
timestamp?: boolean
verbose?: boolean
directory?: string