diff --git a/packages/cli/src/commands/loop/run.test.ts b/packages/cli/src/commands/loop/run.test.ts new file mode 100644 index 000000000..5a2368924 --- /dev/null +++ b/packages/cli/src/commands/loop/run.test.ts @@ -0,0 +1,89 @@ +import { describe, it, expect } from "vitest"; +import { buildLoopRunInput } from "./run.js"; +import type { LoopRunOptions } from "./types.js"; + +describe("buildLoopRunInput", () => { + it("should parse provider/model from provider string", () => { + const options: LoopRunOptions = { + provider: "opencode/hy3-preview-free", + model: undefined, + verifyProvider: undefined, + verifyModel: undefined, + verify: undefined, + verifyCheck: [], + archive: false, + name: undefined, + sleep: undefined, + maxIterations: undefined, + maxTime: undefined, + } as LoopRunOptions; + + const result = buildLoopRunInput("test prompt", options); + + expect(result.provider).toBe("opencode"); + expect(result.model).toBe("hy3-preview-free"); + }); + + it("should use provider only when no model in string", () => { + const options = { + provider: "opencode", + model: undefined, + verifyProvider: undefined, + verifyModel: undefined, + verify: undefined, + verifyCheck: [], + archive: false, + name: undefined, + sleep: undefined, + maxIterations: undefined, + maxTime: undefined, + } as LoopRunOptions; + + const result = buildLoopRunInput("test prompt", options); + + expect(result.provider).toBe("opencode"); + expect(result.model).toBeUndefined(); + }); + + it("should pass verifierProvider with model parsing", () => { + const options = { + provider: undefined, + model: undefined, + verifyProvider: "codex/gpt-5.4", + verifyModel: undefined, + verify: undefined, + verifyCheck: [], + archive: false, + name: undefined, + sleep: undefined, + maxIterations: undefined, + maxTime: undefined, + } as LoopRunOptions; + + const result = buildLoopRunInput("test prompt", options); + + expect(result.verifierProvider).toBe("codex"); + expect(result.verifierModel).toBe("gpt-5.4"); + }); + + it("should prefer explicit model over parsed model", () => { + const options = { + provider: "opencode/hy3-preview-free", + model: "other-model", + verifyProvider: undefined, + verifyModel: undefined, + verify: undefined, + verifyCheck: [], + archive: false, + name: undefined, + sleep: undefined, + maxIterations: undefined, + maxTime: undefined, + } as LoopRunOptions; + + const result = buildLoopRunInput("test prompt", options); + + expect(result.provider).toBe("opencode"); + expect(result.model).toBe("other-model"); + }); +}); diff --git a/packages/cli/src/commands/loop/run.ts b/packages/cli/src/commands/loop/run.ts index ab693d6bc..4db86852d 100644 --- a/packages/cli/src/commands/loop/run.ts +++ b/packages/cli/src/commands/loop/run.ts @@ -8,6 +8,7 @@ import type { } from "../../output/index.js"; import { collectMultiple } from "../../utils/command-options.js"; import { parseDuration } from "../../utils/duration.js"; +import { resolveProviderAndModel } from "../../utils/provider-model.js"; import type { LoopDaemonClient, LoopRecord, LoopRunInput } from "./types.js"; export interface LoopRunRow { @@ -86,7 +87,8 @@ function parseMaxIterations(value: string | undefined): number | undefined { return parsed; } -function buildLoopRunInput(prompt: string, options: LoopRunOptions): LoopRunInput { +// oxlint-disable complexity +export function buildLoopRunInput(prompt: string, options: LoopRunOptions): LoopRunInput { const verifyPrompt = options.verify?.trim(); if (options.verify !== undefined && !verifyPrompt) { throw { @@ -95,24 +97,54 @@ function buildLoopRunInput(prompt: string, options: LoopRunOptions): LoopRunInpu } satisfies CommandError; } - return { + const result: LoopRunInput = { prompt, cwd: process.cwd(), - ...(options.provider ? { provider: options.provider } : {}), - ...(options.model?.trim() ? { model: options.model.trim() } : {}), - ...(options.verifyProvider ? { verifierProvider: options.verifyProvider } : {}), - ...(options.verifyModel?.trim() ? { verifierModel: options.verifyModel.trim() } : {}), - ...(verifyPrompt ? { verifyPrompt } : {}), - ...(options.verifyCheck && options.verifyCheck.length > 0 - ? { verifyChecks: options.verifyCheck } - : {}), - ...(options.archive ? { archive: true } : {}), - ...(options.name?.trim() ? { name: options.name.trim() } : {}), - ...(options.sleep ? { sleepMs: parseDuration(options.sleep) } : {}), - ...(options.maxIterations ? { maxIterations: parseMaxIterations(options.maxIterations) } : {}), - ...(options.maxTime ? { maxTimeMs: parseDuration(options.maxTime) } : {}), }; + + // Resolve provider/model + if (options.provider) { + const { provider, model } = resolveProviderAndModel({ provider: options.provider }); + if (provider) result.provider = provider; + // Explicit --model takes precedence over parsed model + if (options.model?.trim()) { + result.model = options.model.trim(); + } else if (model) { + result.model = model; + } + } else if (options.model?.trim()) { + result.model = options.model.trim(); + } + + // Resolve verifier provider/model + if (options.verifyProvider) { + const { provider, model } = resolveProviderAndModel({ provider: options.verifyProvider }); + if (provider) result.verifierProvider = provider; + // Explicit --verify-model takes precedence over parsed model + if (options.verifyModel?.trim()) { + result.verifierModel = options.verifyModel.trim(); + } else if (model) { + result.verifierModel = model; + } + } else if (options.verifyModel?.trim()) { + result.verifierModel = options.verifyModel.trim(); + } + + if (verifyPrompt) result.verifyPrompt = verifyPrompt; + if (options.verifyCheck && options.verifyCheck.length > 0) { + result.verifyChecks = options.verifyCheck; + } + if (options.archive) result.archive = true; + if (options.name?.trim()) result.name = options.name.trim(); + if (options.sleep) result.sleepMs = parseDuration(options.sleep); + if (options.maxIterations) { + result.maxIterations = parseMaxIterations(options.maxIterations); + } + if (options.maxTime) result.maxTimeMs = parseDuration(options.maxTime); + + return result; } +// oxlint-enable complexity export type LoopRunResult = SingleResult; diff --git a/packages/server/src/client/daemon-client.ts b/packages/server/src/client/daemon-client.ts index 90f88a93f..5a6274fd2 100644 --- a/packages/server/src/client/daemon-client.ts +++ b/packages/server/src/client/daemon-client.ts @@ -451,6 +451,10 @@ export interface WaitForChatMessagesOptions { export interface RunLoopOptions { prompt: string; cwd: string; + provider?: string; + model?: string; + verifierProvider?: string; + verifierModel?: string; verifyPrompt?: string | null; verifyChecks?: string[]; name?: string | null; @@ -3589,6 +3593,10 @@ export class DaemonClient { type: "loop/run", prompt: options.prompt, cwd: options.cwd, + ...(options.provider ? { provider: options.provider } : {}), + ...(options.model ? { model: options.model } : {}), + ...(options.verifierProvider ? { verifierProvider: options.verifierProvider } : {}), + ...(options.verifierModel ? { verifierModel: options.verifierModel } : {}), ...(options.verifyPrompt ? { verifyPrompt: options.verifyPrompt } : {}), ...(options.verifyChecks && options.verifyChecks.length > 0 ? { verifyChecks: options.verifyChecks }