From d9f7cd20bf95293fe9361232e3d9240435a9fee5 Mon Sep 17 00:00:00 2001 From: Matt Rubens <2600+mrubens@users.noreply.github.com> Date: Thu, 16 Jul 2026 13:45:18 -0400 Subject: [PATCH 1/2] fix: apply reasoning effort to launch-time model overrides A per-task model override only received a reasoning effort when it happened to match the deployment's configured coding or code-review model, so launcher and API model picks ran with no reasoning configured at all. Stamp payload.reasoningEffort at launch for override models (inheriting the deployment coding level, falling back to the coding default, and skipping models whose catalog metadata reports no reasoning support), and consume it in the worker with precedence over the role-configured levels. This also wires up the long-documented payload.reasoningEffort field accepted by the public launch API, which nothing consumed before. --- .../run-task/__tests__/create-harness.test.ts | 2 + apps/worker/src/run-task/agent-home.test.ts | 47 ++++++++++ apps/worker/src/run-task/agent-home.ts | 16 +++- apps/worker/src/run-task/create-harness.ts | 10 ++ .../harnesses/opencode-server/bootstrap.ts | 3 + .../lib/harnesses/opencode-server/start.ts | 5 + .../__tests__/harness-model-overrides.test.ts | 90 ++++++++++++++++++ .../src/server/harness-model-overrides.ts | 94 ++++++++++++++++++- .../cloud-agents/src/server/task-run-queue.ts | 28 +++++- 9 files changed, 287 insertions(+), 8 deletions(-) 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 = From 53d9897fe27276f44137a71b1c85335ed7196046 Mon Sep 17 00:00:00 2001 From: Matt Rubens <2600+mrubens@users.noreply.github.com> Date: Thu, 16 Jul 2026 14:10:59 -0400 Subject: [PATCH 2/2] fix: accept reasoning effort in eval harness selection The shared launch validator still rejected any reasoning effort, returning a 400 from the public launch API and Slack !eval --reasoning before the payload stamping and worker plumbing were reached. Accept it now that the worker applies payload.reasoningEffort, and cover the API path with tests asserting the effort reaches the task payload. --- .../tasks/__tests__/launchTask.test.ts | 53 +++++++++++++++++++ .../__tests__/eval-harness-selection.test.ts | 31 ++++++----- packages/types/src/eval-harness-selection.ts | 13 ++--- 3 files changed, 73 insertions(+), 24 deletions(-) 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/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 {