diff --git a/apps/api/src/handlers/tasks/__tests__/launchTask.test.ts b/apps/api/src/handlers/tasks/__tests__/launchTask.test.ts index 442abdfb8..75dac89f0 100644 --- a/apps/api/src/handlers/tasks/__tests__/launchTask.test.ts +++ b/apps/api/src/handlers/tasks/__tests__/launchTask.test.ts @@ -154,6 +154,59 @@ describe('launchTask', () => { expect(enqueuedTask.task.payload.sourceControlProvider).toBeUndefined(); }); + it('stamps a requested reasoning effort and model override into the payload', async () => { + mockEnqueueTask.mockResolvedValue({ id: 103, taskId: 'task-effort' }); + + const app = createApp(authContext); + const response = await app.request( + new Request('http://localhost/tasks', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ + prompt: 'Investigate this', + model: 'openrouter/z-ai/glm-5.2', + reasoningEffort: 'xhigh', + }), + }), + ); + + expect(response.status).toBe(200); + const enqueuedTask = mockEnqueueTask.mock.calls[0]?.[0] as { + task: { + payload: { + reasoningEffort?: string; + harnessModelOverrides?: Record; + }; + }; + }; + expect(enqueuedTask.task.payload.reasoningEffort).toBe('xhigh'); + expect(enqueuedTask.task.payload.harnessModelOverrides).toEqual({ + 'opencode-server': 'openrouter/z-ai/glm-5.2', + }); + }); + + it('stamps a requested reasoning effort without a model override', async () => { + mockEnqueueTask.mockResolvedValue({ id: 104, taskId: 'task-effort-only' }); + + const app = createApp(authContext); + const response = await app.request( + new Request('http://localhost/tasks', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ + prompt: 'Investigate this', + reasoningEffort: 'low', + }), + }), + ); + + expect(response.status).toBe(200); + const enqueuedTask = mockEnqueueTask.mock.calls[0]?.[0] as { + task: { payload: { reasoningEffort?: string } }; + }; + expect(enqueuedTask.task.payload.reasoningEffort).toBe('low'); + }); + it('stamps the launching run and settle opt-in for run-token launches with notifyOnSettle', async () => { mockEnqueueTask.mockResolvedValue({ id: 102, taskId: 'task-child' }); diff --git a/apps/worker/src/run-task/__tests__/create-harness.test.ts b/apps/worker/src/run-task/__tests__/create-harness.test.ts index 5290494ca..267383b56 100644 --- a/apps/worker/src/run-task/__tests__/create-harness.test.ts +++ b/apps/worker/src/run-task/__tests__/create-harness.test.ts @@ -216,6 +216,7 @@ describe('createHarness', () => { harnessModelOverrides: { 'opencode-server': 'provider-id/model-id', }, + reasoningEffort: 'high', }, } as never, callbacks: {} as never, @@ -226,6 +227,7 @@ describe('createHarness', () => { expect(startOpenCodeServerHarnessMock).toHaveBeenCalledWith( expect.objectContaining({ modelOverride: 'provider-id/model-id', + reasoningEffortOverride: 'high', }), ); expect(getHarnessModelOverrideMock).toHaveBeenCalledTimes(1); diff --git a/apps/worker/src/run-task/agent-home.test.ts b/apps/worker/src/run-task/agent-home.test.ts index b2d8a97f5..c436443c8 100644 --- a/apps/worker/src/run-task/agent-home.test.ts +++ b/apps/worker/src/run-task/agent-home.test.ts @@ -19,6 +19,53 @@ describe('generateOpenCodeConfig provider support', () => { return homeDir; } + it('applies the per-task reasoning effort to a launch-time model override', () => { + const result = generateOpenCodeConfig({ + homeDir: createHomeDir(), + runtimeEnv: { + R_MODEL: 'openrouter/openai/gpt-5.6-terra', + R_MODEL_REASONING_EFFORT: 'medium', + OPENROUTER_API_KEY: 'openrouter-key', + }, + model: 'openrouter/z-ai/glm-5.2', + reasoningEffortOverride: 'high', + }); + const config = JSON.parse(result.configContent) as { + provider: Record; + }; + + expect(config.provider.openrouter).toMatchObject({ + models: { + 'z-ai/glm-5.2': { + options: { reasoning: { effort: 'high' } }, + }, + 'openai/gpt-5.6-terra': { + options: { reasoning: { effort: 'medium' } }, + }, + }, + }); + }); + + it('leaves a model override without reasoning options when no per-task effort is set', () => { + const result = generateOpenCodeConfig({ + homeDir: createHomeDir(), + runtimeEnv: { + R_MODEL: 'openrouter/openai/gpt-5.6-terra', + R_MODEL_REASONING_EFFORT: 'medium', + OPENROUTER_API_KEY: 'openrouter-key', + }, + model: 'openrouter/z-ai/glm-5.2', + }); + const config = JSON.parse(result.configContent) as { + provider: Record; + }; + const openrouter = config.provider.openrouter as { + models?: Record; + }; + + expect(openrouter.models?.['z-ai/glm-5.2']).toBeUndefined(); + }); + it('routes Bedrock models through the Mantle Anthropic endpoint', () => { const result = generateOpenCodeConfig({ homeDir: createHomeDir(), diff --git a/apps/worker/src/run-task/agent-home.ts b/apps/worker/src/run-task/agent-home.ts index d3152207d..09de2f359 100644 --- a/apps/worker/src/run-task/agent-home.ts +++ b/apps/worker/src/run-task/agent-home.ts @@ -426,6 +426,7 @@ interface GenerateOpenCodeConfigOptions { developerInstructionsContent?: string; mcpServers?: OpenCodeConfigMcpServer[]; model?: string; + reasoningEffortOverride?: ReasoningEffort; } interface GenerateOpenCodeConfigResult { @@ -898,6 +899,7 @@ function createExploreAgentConfig(options: { function resolveModelBackedOpenCodeConfig( runtimeEnv: Record, modelOverride?: string, + reasoningEffortOverride?: ReasoningEffort, ): Record | null { const rawModel = runtimeEnv.R_MODEL?.trim(); @@ -1089,13 +1091,16 @@ function resolveModelBackedOpenCodeConfig( // applied when the model in play is the one the role was configured with. // Role precedence for a shared model: effective coding model first, then the // persisted coding model, then a distinct helper model. The vision level is - // scoped to the visual subagent via agent-level options above. + // scoped to the visual subagent via agent-level options above. A per-task + // reasoning effort (stamped at launch for model overrides, or set + // explicitly via the public API) wins over the role-configured levels. const effectiveCodingModelReasoningEffort: ReasoningEffort | null = - effectiveCodingModel === model + reasoningEffortOverride ?? + (effectiveCodingModel === model ? modelReasoningEffort : codeReviewModel && effectiveCodingModel === codeReviewModel ? codeReviewModelReasoningEffort - : null; + : null); let providerReasoningConfig: Record = {}; if (effectiveCodingModelReasoningEffort) { @@ -1155,14 +1160,17 @@ function loadOperatorOpenCodeConfig({ runtimeEnv, openCodeConfigDir, modelOverride, + reasoningEffortOverride, }: { runtimeEnv: Record; openCodeConfigDir: string; modelOverride?: string; + reasoningEffortOverride?: ReasoningEffort; }): Record { const modelConfig = resolveModelBackedOpenCodeConfig( runtimeEnv, modelOverride, + reasoningEffortOverride, ); if (modelConfig) { @@ -1232,6 +1240,7 @@ export function generateOpenCodeConfig({ developerInstructionsContent, mcpServers, model, + reasoningEffortOverride, }: GenerateOpenCodeConfigOptions): GenerateOpenCodeConfigResult { const openCodeConfigDir = path.join( homeDir, @@ -1252,6 +1261,7 @@ export function generateOpenCodeConfig({ runtimeEnv, openCodeConfigDir, modelOverride: resolvedModel, + reasoningEffortOverride, }); const instructions: string[] = []; diff --git a/apps/worker/src/run-task/create-harness.ts b/apps/worker/src/run-task/create-harness.ts index bdd2562e2..22e93e04b 100644 --- a/apps/worker/src/run-task/create-harness.ts +++ b/apps/worker/src/run-task/create-harness.ts @@ -3,6 +3,7 @@ import type { ResultPromise } from 'execa'; import { type DequeuedTaskRun, sdk } from '@roomote/sdk/client'; import { getHarnessModelOverride, + isReasoningEffort, type EnvironmentMcpServers, type LaunchCodingHarness, } from '@roomote/types'; @@ -97,6 +98,14 @@ export async function createHarness({ harnessType, ) : undefined; + // Per-task reasoning effort stamped at launch (or set explicitly via the + // public API). Applied to the effective coding model, which per-role env + // levels do not cover when a launch-time model override is in play. + const reasoningEffortOverride = isReasoningEffort( + taskRun.payload?.reasoningEffort, + ) + ? taskRun.payload.reasoningEffort + : undefined; const commonOptions = { workspacePath, @@ -110,6 +119,7 @@ export async function createHarness({ await prepareQueuedPromptActorScope(userId) : undefined, ...(modelOverride ? { modelOverride } : {}), + ...(reasoningEffortOverride ? { reasoningEffortOverride } : {}), }; return await startOpenCodeServerHarness({ diff --git a/apps/worker/src/sandbox-server/lib/harnesses/opencode-server/bootstrap.ts b/apps/worker/src/sandbox-server/lib/harnesses/opencode-server/bootstrap.ts index 1759c71ff..90f496aa0 100644 --- a/apps/worker/src/sandbox-server/lib/harnesses/opencode-server/bootstrap.ts +++ b/apps/worker/src/sandbox-server/lib/harnesses/opencode-server/bootstrap.ts @@ -12,6 +12,7 @@ import type { HarnessLogger } from '../../../../logging'; import { GOOGLE_APPLICATION_CREDENTIALS_ENV_VAR_NAME, OPENCODE_AUTH_CONTENT_ENV_VAR_NAME, + type ReasoningEffort, } from '@roomote/types'; import { @@ -158,6 +159,7 @@ export async function prepareOpenCodeCommandEnv(options: { workspacePath: string; mcpServers?: Record; model?: string; + reasoningEffortOverride?: ReasoningEffort; developerInstructionsContent?: string; logger: HarnessLogger; }): Promise<{ commandEnv: Record; model?: string }> { @@ -188,6 +190,7 @@ export async function prepareOpenCodeCommandEnv(options: { developerInstructionsContent: options.developerInstructionsContent, mcpServers: normalizeOpenCodeMcpServers(parsedMcpServers, commandEnv), model: options.model, + reasoningEffortOverride: options.reasoningEffortOverride, }); commandEnv.OPENCODE_CONFIG_CONTENT = configContent; commandEnv.ROOMOTE_NODE_EXECUTABLE = process.execPath; diff --git a/apps/worker/src/sandbox-server/lib/harnesses/opencode-server/start.ts b/apps/worker/src/sandbox-server/lib/harnesses/opencode-server/start.ts index fead610dd..a572d1466 100644 --- a/apps/worker/src/sandbox-server/lib/harnesses/opencode-server/start.ts +++ b/apps/worker/src/sandbox-server/lib/harnesses/opencode-server/start.ts @@ -3,6 +3,8 @@ import net from 'node:net'; import { execa, type ResultPromise } from 'execa'; +import type { ReasoningEffort } from '@roomote/types'; + import type { HarnessLogger } from '../../../../logging'; import { createPrefixedLogger, describeUnknownError } from '../logging'; @@ -24,6 +26,7 @@ interface StartOpenCodeServerHarnessOptions { mcpServers: Record; initialSessionId?: string; modelOverride?: string; + reasoningEffortOverride?: ReasoningEffort; developerInstructionsContent?: string; /** * Invoked when the OpenCode server subprocess exits while the task was not @@ -213,6 +216,7 @@ export async function startOpenCodeServerHarness({ mcpServers, initialSessionId, modelOverride, + reasoningEffortOverride, developerInstructionsContent, onUnexpectedExit, onDiagnostic, @@ -225,6 +229,7 @@ export async function startOpenCodeServerHarness({ workspacePath, mcpServers, model: modelOverride, + reasoningEffortOverride, developerInstructionsContent, logger, }); diff --git a/packages/cloud-agents/src/server/__tests__/harness-model-overrides.test.ts b/packages/cloud-agents/src/server/__tests__/harness-model-overrides.test.ts index f690955e1..8109d53b7 100644 --- a/packages/cloud-agents/src/server/__tests__/harness-model-overrides.test.ts +++ b/packages/cloud-agents/src/server/__tests__/harness-model-overrides.test.ts @@ -105,4 +105,94 @@ describe('resolveEffectiveHarnessModelState', () => { expect(model).toBe('openrouter/openai/gpt-5.6-terra'); }); + + it('stamps the default coding reasoning effort for a model override', () => { + const { task } = resolveEffectiveHarnessModelState({ + task: makeTask({ 'opencode-server': 'openrouter/z-ai/glm-5.2' }), + targetHarness: 'opencode-server', + isSnapshotResume: false, + }); + + expect(task.payload.reasoningEffort).toBe('medium'); + }); + + it('inherits the deployment coding reasoning effort for a model override', () => { + const { task } = resolveEffectiveHarnessModelState({ + task: makeTask({ 'opencode-server': 'openrouter/z-ai/glm-5.2' }), + targetHarness: 'opencode-server', + isSnapshotResume: false, + deploymentCodingReasoningEffort: 'xhigh', + }); + + expect(task.payload.reasoningEffort).toBe('xhigh'); + }); + + it('keeps an explicit per-task reasoning effort over the deployment level', () => { + const task = makeTask({ 'opencode-server': 'openrouter/z-ai/glm-5.2' }); + task.payload.reasoningEffort = 'low'; + + const { task: nextTask } = resolveEffectiveHarnessModelState({ + task, + targetHarness: 'opencode-server', + isSnapshotResume: false, + deploymentCodingReasoningEffort: 'xhigh', + }); + + expect(nextTask.payload.reasoningEffort).toBe('low'); + }); + + it('does not stamp a reasoning effort for models without reasoning support', () => { + const { task } = resolveEffectiveHarnessModelState({ + task: makeTask({ + 'opencode-server': 'openrouter/custom/no-reasoning-model', + }), + targetHarness: 'opencode-server', + isSnapshotResume: false, + deploymentTaskModelSettings: { + models: [ + { + id: 'openrouter/custom/no-reasoning-model', + displayName: 'No Reasoning Model', + family: 'Custom', + metadata: { + contextWindow: null, + inputTypes: null, + inputPricePerToken: null, + outputPricePerToken: null, + lastRefreshedAt: null, + supportsReasoning: false, + }, + }, + ], + allowedModelIds: ['openrouter/custom/no-reasoning-model'], + defaultModelId: 'openrouter/custom/no-reasoning-model', + }, + }); + + expect(task.payload.reasoningEffort).toBeUndefined(); + }); + + it('does not stamp a reasoning effort when the deployment default model is applied', () => { + const { task } = resolveEffectiveHarnessModelState({ + task: makeTask(), + targetHarness: 'opencode-server', + isSnapshotResume: false, + }); + + expect(task.payload.reasoningEffort).toBeUndefined(); + }); + + it('stamps a reasoning effort when reusing snapshot-resume model overrides', () => { + const { task } = resolveEffectiveHarnessModelState({ + task: makeTask(), + targetHarness: 'opencode-server', + isSnapshotResume: true, + sourceRunHarnessModelOverrides: { + 'opencode-server': 'openrouter/z-ai/glm-5.2', + }, + deploymentCodingReasoningEffort: 'high', + }); + + expect(task.payload.reasoningEffort).toBe('high'); + }); }); diff --git a/packages/cloud-agents/src/server/harness-model-overrides.ts b/packages/cloud-agents/src/server/harness-model-overrides.ts index d159b7d7c..6ab0e1298 100644 --- a/packages/cloud-agents/src/server/harness-model-overrides.ts +++ b/packages/cloud-agents/src/server/harness-model-overrides.ts @@ -2,10 +2,13 @@ import { type TaskSpec, type CodingHarness, type HarnessModelOverrides, + type ReasoningEffort, type TaskModelSettings, + DEFAULT_MODEL_ROLE_REASONING_EFFORTS, TaskPayloadKind, getDefaultTaskModelId, getHarnessModelOverride, + getTaskModelCatalog, isTaskModelIdAllowed, } from '@roomote/types'; import type { MetadataRecord } from '@roomote/feature-flags'; @@ -64,6 +67,85 @@ function applyHarnessModelOverrides( }; } +/** + * Resolves the reasoning effort a launch-time model override should run + * with. Overrides do not match the worker's per-role reasoning env vars + * (those are scoped to the models each role was configured with), so + * without this the override would run with no reasoning configured at all. + * Inherits the deployment's coding-role level, falling back to the coding + * default; models whose catalog metadata reports no configurable reasoning + * get none (unknown support keeps the default, matching the runtime-env + * resolution). + */ +function resolveOverrideTaskReasoningEffort(options: { + modelId: string; + deploymentTaskModelSettings?: TaskModelSettings | null; + deploymentCodingReasoningEffort?: ReasoningEffort | null; +}): ReasoningEffort | null { + const catalogModel = getTaskModelCatalog( + options.deploymentTaskModelSettings, + ).find((model) => model.id === options.modelId); + + if (catalogModel?.metadata?.supportsReasoning === false) { + return null; + } + + return ( + options.deploymentCodingReasoningEffort ?? + DEFAULT_MODEL_ROLE_REASONING_EFFORTS.coding + ); +} + +/** + * Stamps `payload.reasoningEffort` for a launch that selected a model + * override, so the worker can apply a reasoning level to the override + * model. Explicit per-task efforts (for example from the public launch + * API) always win. + */ +function applyOverrideTaskReasoningEffort( + task: T, + options: { + targetHarness: CodingHarness; + deploymentTaskModelSettings?: TaskModelSettings | null; + deploymentCodingReasoningEffort?: ReasoningEffort | null; + }, +): T { + if (options.targetHarness !== 'opencode-server') { + return task; + } + + if (task.payload.reasoningEffort) { + return task; + } + + const overrideModelId = getHarnessModelOverride( + task.payload.harnessModelOverrides, + 'opencode-server', + ); + + if (!overrideModelId) { + return task; + } + + const reasoningEffort = resolveOverrideTaskReasoningEffort({ + modelId: overrideModelId, + deploymentTaskModelSettings: options.deploymentTaskModelSettings, + deploymentCodingReasoningEffort: options.deploymentCodingReasoningEffort, + }); + + if (!reasoningEffort) { + return task; + } + + return { + ...task, + payload: { + ...task.payload, + reasoningEffort, + }, + }; +} + export function resolveEffectiveHarnessModelState(options: { task: T; targetHarness: CodingHarness; @@ -72,14 +154,18 @@ export function resolveEffectiveHarnessModelState(options: { deploymentMetadata?: MetadataRecord | null; deploymentTaskModelSettings?: TaskModelSettings | null; deploymentCodeReviewModelId?: string | null; + deploymentCodingReasoningEffort?: ReasoningEffort | null; }): { task: T; model: string } { const shouldReuseSourceHarnessModelOverrides = options.isSnapshotResume && Boolean(options.sourceRunHarnessModelOverrides); if (shouldReuseSourceHarnessModelOverrides) { - const nextTask = applyHarnessModelOverrides( - options.task, - options.sourceRunHarnessModelOverrides, + const nextTask = applyOverrideTaskReasoningEffort( + applyHarnessModelOverrides( + options.task, + options.sourceRunHarnessModelOverrides, + ), + options, ); return { @@ -110,7 +196,7 @@ export function resolveEffectiveHarnessModelState(options: { } return { - task: options.task, + task: applyOverrideTaskReasoningEffort(options.task, options), model: resolveTaskModelForHarness( options.targetHarness, options.task.payload.harnessModelOverrides, diff --git a/packages/cloud-agents/src/server/task-run-queue.ts b/packages/cloud-agents/src/server/task-run-queue.ts index 678bc2dbe..79c852c31 100644 --- a/packages/cloud-agents/src/server/task-run-queue.ts +++ b/packages/cloud-agents/src/server/task-run-queue.ts @@ -28,6 +28,7 @@ import { getUserDisplayName, getPrimaryPortFromConfig, isConfiguredEnvValue, + isReasoningEffort, normalizeDeploymentModelConfig, resolveTaskRuntimePolicy, resolveTaskWorkspace, @@ -270,6 +271,9 @@ type ResolvedHarnessSelection = { | import('@roomote/types').TaskModelSettings | null; deploymentCodeReviewModelId?: string | null; + deploymentCodingReasoningEffort?: + | import('@roomote/types').ReasoningEffort + | null; }; const DEFAULT_DEPLOYMENT_ID = 'default'; @@ -299,6 +303,16 @@ function resolveCodeReviewModelId( return envCodeReviewModel ?? persistedConfig.roomoteCodeReviewModel; } +function resolveCodingReasoningEffort( + persistedConfig: import('@roomote/types').DeploymentModelConfig, +): import('@roomote/types').ReasoningEffort | null { + const envEffort = process.env.R_MODEL_REASONING_EFFORT?.trim(); + + return isReasoningEffort(envEffort) + ? envEffort + : persistedConfig.roomoteModelReasoningEffort; +} + async function resolveRequestedHarness( task: TaskSpec, ): Promise { @@ -310,6 +324,9 @@ async function resolveRequestedHarness( runtimeModelConfig: true, }, }); + const deploymentModelConfig = normalizeDeploymentModelConfig( + deployment?.runtimeModelConfig, + ); return { harness: task.harness ?? DEFAULT_LAUNCH_CODING_HARNESS, @@ -317,7 +334,10 @@ async function resolveRequestedHarness( (deployment?.metadata as MetadataRecord | null | undefined) ?? null, deploymentTaskModelSettings: deployment?.taskModelSettings ?? null, deploymentCodeReviewModelId: resolveCodeReviewModelId( - normalizeDeploymentModelConfig(deployment?.runtimeModelConfig), + deploymentModelConfig, + ), + deploymentCodingReasoningEffort: resolveCodingReasoningEffort( + deploymentModelConfig, ), }; } @@ -1271,6 +1291,8 @@ async function enqueueFreshLaunch( deploymentTaskModelSettings: resolvedHarness.deploymentTaskModelSettings, deploymentCodeReviewModelId: resolvedHarness.deploymentCodeReviewModelId ?? null, + deploymentCodingReasoningEffort: + resolvedHarness.deploymentCodingReasoningEffort ?? null, }); const repositoryName = taskWithHarnessOverrides.payload.repo || null; @@ -1663,6 +1685,8 @@ export async function enqueueTaskRelaunch( deploymentTaskModelSettings: resolvedHarness.deploymentTaskModelSettings, deploymentCodeReviewModelId: resolvedHarness.deploymentCodeReviewModelId ?? null, + deploymentCodingReasoningEffort: + resolvedHarness.deploymentCodingReasoningEffort ?? null, }); const resolvedTaskPolicy = resolveTaskRuntimePolicy({ @@ -1856,6 +1880,8 @@ async function enqueueSnapshotResume( deploymentTaskModelSettings: resolvedHarness.deploymentTaskModelSettings, deploymentCodeReviewModelId: resolvedHarness.deploymentCodeReviewModelId ?? null, + deploymentCodingReasoningEffort: + resolvedHarness.deploymentCodingReasoningEffort ?? null, }); const targetComputeProvider = diff --git a/packages/types/src/__tests__/eval-harness-selection.test.ts b/packages/types/src/__tests__/eval-harness-selection.test.ts index 42186a253..e9f260dbb 100644 --- a/packages/types/src/__tests__/eval-harness-selection.test.ts +++ b/packages/types/src/__tests__/eval-harness-selection.test.ts @@ -63,22 +63,25 @@ describe('resolveEvalHarnessSelection', () => { } }); - it('rejects --reasoning on the OpenCode harness', () => { - const result = resolveEvalHarnessSelection({ - harness: 'opencode-server', - reasoningEffort: 'xhigh', - }); - expect(result.ok).toBe(false); - if (!result.ok) { - expect(result.error).toContain('not supported on the OpenCode harness'); - } + it('accepts --reasoning on the OpenCode harness', () => { + expect( + resolveEvalHarnessSelection({ + harness: 'opencode-server', + reasoningEffort: 'xhigh', + }), + ).toEqual({ ok: true, harness: 'opencode-server' }); }); - it('rejects --reasoning when the OpenCode harness is inferred from the model', () => { - const result = resolveEvalHarnessSelection({ - model: OPENCODE_MODEL, - reasoningEffort: 'high', + it('accepts --reasoning when the OpenCode harness is inferred from the model', () => { + expect( + resolveEvalHarnessSelection({ + model: OPENCODE_MODEL, + reasoningEffort: 'high', + }), + ).toEqual({ + ok: true, + harness: 'opencode-server', + harnessModelOverrides: { 'opencode-server': OPENCODE_MODEL }, }); - expect(result.ok).toBe(false); }); }); diff --git a/packages/types/src/eval-harness-selection.ts b/packages/types/src/eval-harness-selection.ts index 5a9d3a6ed..43b2e4516 100644 --- a/packages/types/src/eval-harness-selection.ts +++ b/packages/types/src/eval-harness-selection.ts @@ -35,8 +35,9 @@ export type EvalHarnessSelection = * only launch harness for new work. * - The model becomes the override for the OpenCode harness. OpenCode accepts * provider/model identifiers from the operator's selected config. - * - Reasoning effort is not supported by OpenCode, so it is rejected rather - * than silently dropped. + * - Reasoning effort is accepted as-is: callers stamp it onto + * `payload.reasoningEffort`, which the worker applies to the effective + * coding model of the launch. * * The function is pure and validation-complete so both surfaces can share it: * each calls it to surface errors and to build the launch payload. @@ -65,14 +66,6 @@ export function resolveEvalHarnessSelection(input: { const effectiveHarness: LaunchCodingHarness | undefined = explicitHarness ?? (trimmedModel ? 'opencode-server' : undefined); - if (input.reasoningEffort) { - return { - ok: false, - error: - 'Reasoning effort is not supported on the OpenCode harness; omit it when targeting opencode-server.', - }; - } - if (trimmedModel) { if (!OPENCODE_MODEL_PATTERN.test(trimmedModel)) { return {