Skip to content
16 changes: 8 additions & 8 deletions apps/server/src/git/Layers/ClaudeTextGeneration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,10 +202,10 @@ it.layer(ClaudeTextGenerationTestLayer)("ClaudeTextGenerationLive", (it) => {
modelSelection: {
provider: "claudeAgent",
model: "claude-haiku-4-5",
options: {
thinking: false,
effort: "high",
},
options: [
{ id: "thinking", value: false },
{ id: "effort", value: "high" },
],
},
});

Expand Down Expand Up @@ -265,10 +265,10 @@ it.layer(ClaudeTextGenerationTestLayer)("ClaudeTextGenerationLive", (it) => {
modelSelection: {
provider: "claudeAgent",
model: "claude-opus-4-6",
options: {
effort: "max",
fastMode: true,
},
options: [
{ id: "effort", value: "max" },
{ id: "fastMode", value: true },
],
},
});

Expand Down
40 changes: 28 additions & 12 deletions apps/server/src/git/Layers/ClaudeTextGeneration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,17 @@ import {
sanitizeThreadTitle,
toJsonSchemaObject,
} from "../Utils.ts";
import { normalizeClaudeModelOptionsWithCapabilities } from "@marcode/shared/model";
import { resolveClaudeApiModelId } from "../../provider/Layers/ClaudeProvider.ts";
import {
getModelSelectionStringOptionValue,
getProviderOptionDescriptors,
} from "@marcode/shared/model";
import {
getClaudeModelCapabilities,
normalizeClaudeCliEffort,
resolveClaudeApiModelId,
resolveClaudeEffort,
} from "../../provider/Layers/ClaudeProvider.ts";
import { ServerSettingsService } from "../../serverSettings.ts";
import { getClaudeModelCapabilities } from "../../provider/Layers/ClaudeProvider.ts";

const CLAUDE_TIMEOUT_MS = 180_000;

Expand Down Expand Up @@ -85,15 +92,24 @@ const makeClaudeTextGeneration = Effect.gen(function* () {
modelSelection: ClaudeModelSelection;
}): Effect.fn.Return<S["Type"], TextGenerationError, S["DecodingServices"]> {
const jsonSchemaStr = JSON.stringify(toJsonSchemaObject(outputSchemaJson));
const normalizedOptions = normalizeClaudeModelOptionsWithCapabilities(
getClaudeModelCapabilities(modelSelection.model),
modelSelection.options,
);
const caps = getClaudeModelCapabilities(modelSelection.model);
const descriptors = getProviderOptionDescriptors({
caps,
selections: modelSelection.options,
});
const findDescriptor = (id: string) => descriptors.find((descriptor) => descriptor.id === id);
const rawEffortSelection = getModelSelectionStringOptionValue(modelSelection, "effort");
const resolvedEffort = resolveClaudeEffort(caps, rawEffortSelection);
const cliEffort = normalizeClaudeCliEffort(resolvedEffort);
const thinkingDescriptor = findDescriptor("thinking");
const fastModeDescriptor = findDescriptor("fastMode");
const thinking =
thinkingDescriptor?.type === "boolean" ? thinkingDescriptor.currentValue : undefined;
const fastMode =
fastModeDescriptor?.type === "boolean" ? fastModeDescriptor.currentValue : undefined;
const settings = {
...(typeof normalizedOptions?.thinking === "boolean"
? { alwaysThinkingEnabled: normalizedOptions.thinking }
: {}),
...(normalizedOptions?.fastMode ? { fastMode: true } : {}),
...(typeof thinking === "boolean" ? { alwaysThinkingEnabled: thinking } : {}),
...(fastMode ? { fastMode: true } : {}),
};

const claudeSettings = yield* Effect.map(
Expand All @@ -112,7 +128,7 @@ const makeClaudeTextGeneration = Effect.gen(function* () {
jsonSchemaStr,
"--model",
resolveClaudeApiModelId(modelSelection),
...(normalizedOptions?.effort ? ["--effort", normalizedOptions.effort] : []),
...(cliEffort ? ["--effort", cliEffort] : []),
...(Object.keys(settings).length > 0 ? ["--settings", JSON.stringify(settings)] : []),
"--tools",
"",
Expand Down
8 changes: 4 additions & 4 deletions apps/server/src/git/Layers/CodexTextGeneration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,10 +247,10 @@ it.layer(CodexTextGenerationTestLayer)("CodexTextGenerationLive", (it) => {
modelSelection: {
provider: "codex",
model: "gpt-5.4",
options: {
reasoningEffort: "xhigh",
fastMode: true,
},
options: [
{ id: "reasoningEffort", value: "xhigh" },
{ id: "fastMode", value: true },
],
},
});
}),
Expand Down
10 changes: 8 additions & 2 deletions apps/server/src/git/Layers/CodexTextGeneration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";

import { CodexModelSelection } from "@marcode/contracts";
import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@marcode/shared/git";
import {
getModelSelectionBooleanOptionValue,
getModelSelectionStringOptionValue,
} from "@marcode/shared/model";

import { resolveAttachmentPath } from "../../attachmentStore.ts";
import { ServerConfig } from "../../config.ts";
Expand Down Expand Up @@ -156,7 +160,9 @@ const makeCodexTextGeneration = Effect.gen(function* () {

const runCodexCommand = Effect.fn("runCodexJson.runCodexCommand")(function* () {
const reasoningEffort =
modelSelection.options?.reasoningEffort ?? CODEX_GIT_TEXT_GENERATION_REASONING_EFFORT;
getModelSelectionStringOptionValue(modelSelection, "reasoningEffort") ??
CODEX_GIT_TEXT_GENERATION_REASONING_EFFORT;
const fastMode = getModelSelectionBooleanOptionValue(modelSelection, "fastMode") === true;
const command = ChildProcess.make(
codexSettings?.binaryPath || "codex",
[
Expand All @@ -169,7 +175,7 @@ const makeCodexTextGeneration = Effect.gen(function* () {
modelSelection.model,
"--config",
`model_reasoning_effort="${reasoningEffort}"`,
...(modelSelection.options?.fastMode ? ["--config", `service_tier="fast"`] : []),
...(fastMode ? ["--config", `service_tier="fast"`] : []),
"--output-schema",
schemaPath,
"--output-last-message",
Expand Down
10 changes: 5 additions & 5 deletions apps/server/src/git/Layers/CursorTextGeneration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,11 +137,11 @@ it.layer(CursorTextGenerationTestLayer)("CursorTextGenerationLive", (it) => {
modelSelection: {
provider: "cursor",
model: "gpt-5.4",
options: {
reasoning: "xhigh",
fastMode: true,
contextWindow: "1m",
},
options: [
{ id: "reasoning", value: "xhigh" },
{ id: "fastMode", value: true },
{ id: "contextWindow", value: "1m" },
],
},
});

Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/git/Layers/CursorTextGeneration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ const makeCursorTextGeneration = Effect.gen(function* () {
yield* applyCursorAcpModelSelection({
runtime,
model: modelSelection.model,
modelOptions: modelSelection.options,
selections: modelSelection.options,
mapError: ({ cause, configId, step }) =>
mapCursorAcpError(
operation,
Expand Down
11 changes: 5 additions & 6 deletions apps/server/src/git/Layers/OpenCodeTextGeneration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
type OpenCodeModelSelection,
} from "@marcode/contracts";
import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@marcode/shared/git";
import { getModelSelectionStringOptionValue } from "@marcode/shared/model";

import { ServerConfig } from "../../config.ts";
import { resolveAttachmentPath } from "../../attachmentStore.ts";
Expand Down Expand Up @@ -321,15 +322,13 @@ const makeOpenCodeTextGeneration = Effect.gen(function* () {
throw new Error("OpenCode session.create returned no session payload.");
}

const agent = getModelSelectionStringOptionValue(input.modelSelection, "agent");
const variant = getModelSelectionStringOptionValue(input.modelSelection, "variant");
const result = await client.session.prompt({
sessionID: session.data.id,
model: parsedModel,
...(input.modelSelection.options?.agent
? { agent: input.modelSelection.options.agent }
: {}),
...(input.modelSelection.options?.variant
? { variant: input.modelSelection.options.variant }
: {}),
...(agent ? { agent } : {}),
...(variant ? { variant } : {}),
parts: [{ type: "text", text: input.prompt }, ...fileParts],
});
const info = result.data?.info;
Expand Down
60 changes: 21 additions & 39 deletions apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -577,10 +577,10 @@ describe("ProviderCommandReactor", () => {
modelSelection: {
provider: "codex",
model: "gpt-5.3-codex",
options: {
reasoningEffort: "high",
fastMode: true,
},
options: [
{ id: "reasoningEffort", value: "high" },
{ id: "fastMode", value: true },
],
},
interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
runtimeMode: "approval-required",
Expand All @@ -594,21 +594,21 @@ describe("ProviderCommandReactor", () => {
modelSelection: {
provider: "codex",
model: "gpt-5.3-codex",
options: {
reasoningEffort: "high",
fastMode: true,
},
options: [
{ id: "reasoningEffort", value: "high" },
{ id: "fastMode", value: true },
],
},
});
expect(harness.sendTurn.mock.calls[0]?.[0]).toMatchObject({
threadId: ThreadId.make("thread-1"),
modelSelection: {
provider: "codex",
model: "gpt-5.3-codex",
options: {
reasoningEffort: "high",
fastMode: true,
},
options: [
{ id: "reasoningEffort", value: "high" },
{ id: "fastMode", value: true },
],
},
});
});
Expand All @@ -633,9 +633,7 @@ describe("ProviderCommandReactor", () => {
modelSelection: {
provider: "claudeAgent",
model: "claude-sonnet-4-6",
options: {
effort: "max",
},
options: [{ id: "effort", value: "max" }],
},
interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
runtimeMode: "approval-required",
Expand All @@ -649,19 +647,15 @@ describe("ProviderCommandReactor", () => {
modelSelection: {
provider: "claudeAgent",
model: "claude-sonnet-4-6",
options: {
effort: "max",
},
options: [{ id: "effort", value: "max" }],
},
});
expect(harness.sendTurn.mock.calls[0]?.[0]).toMatchObject({
threadId: ThreadId.make("thread-1"),
modelSelection: {
provider: "claudeAgent",
model: "claude-sonnet-4-6",
options: {
effort: "max",
},
options: [{ id: "effort", value: "max" }],
},
});
});
Expand All @@ -686,9 +680,7 @@ describe("ProviderCommandReactor", () => {
modelSelection: {
provider: "claudeAgent",
model: "claude-opus-4-6",
options: {
fastMode: true,
},
options: [{ id: "fastMode", value: true }],
},
interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
runtimeMode: "approval-required",
Expand All @@ -702,19 +694,15 @@ describe("ProviderCommandReactor", () => {
modelSelection: {
provider: "claudeAgent",
model: "claude-opus-4-6",
options: {
fastMode: true,
},
options: [{ id: "fastMode", value: true }],
},
});
expect(harness.sendTurn.mock.calls[0]?.[0]).toMatchObject({
threadId: ThreadId.make("thread-1"),
modelSelection: {
provider: "claudeAgent",
model: "claude-opus-4-6",
options: {
fastMode: true,
},
options: [{ id: "fastMode", value: true }],
},
});
});
Expand Down Expand Up @@ -1004,9 +992,7 @@ describe("ProviderCommandReactor", () => {
modelSelection: {
provider: "claudeAgent",
model: "claude-sonnet-4-6",
options: {
effort: "medium",
},
options: [{ id: "effort", value: "medium" }],
},
interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
runtimeMode: "approval-required",
Expand All @@ -1031,9 +1017,7 @@ describe("ProviderCommandReactor", () => {
modelSelection: {
provider: "claudeAgent",
model: "claude-sonnet-4-6",
options: {
effort: "max",
},
options: [{ id: "effort", value: "max" }],
},
interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
runtimeMode: "approval-required",
Expand All @@ -1048,9 +1032,7 @@ describe("ProviderCommandReactor", () => {
modelSelection: {
provider: "claudeAgent",
model: "claude-sonnet-4-6",
options: {
effort: "max",
},
options: [{ id: "effort", value: "max" }],
},
});
});
Expand Down
16 changes: 8 additions & 8 deletions apps/server/src/orchestration/decider.projectScripts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,10 +167,10 @@ describe("decider project scripts", () => {
modelSelection: {
provider: "codex",
model: "gpt-5.3-codex",
options: {
reasoningEffort: "high",
fastMode: true,
},
options: [
{ id: "reasoningEffort", value: "high" },
{ id: "fastMode", value: true },
],
},
interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
runtimeMode: "approval-required",
Expand All @@ -196,10 +196,10 @@ describe("decider project scripts", () => {
modelSelection: {
provider: "codex",
model: "gpt-5.3-codex",
options: {
reasoningEffort: "high",
fastMode: true,
},
options: [
{ id: "reasoningEffort", value: "high" },
{ id: "fastMode", value: true },
],
},
runtimeMode: "approval-required",
});
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/persistence/Migrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import Migration0026 from "./Migrations/026_AuthSessionLastConnectedAt.ts";
import Migration0027 from "./Migrations/027_ProjectionThreadShellSummary.ts";
import Migration0028 from "./Migrations/028_BackfillProjectionThreadShellSummary.ts";
import Migration0029 from "./Migrations/029_CleanupInvalidProjectionPendingApprovals.ts";
import Migration0030 from "./Migrations/030_CanonicalizeModelSelectionOptions.ts";

/**
* Migration loader with all migrations defined inline.
Expand Down Expand Up @@ -81,6 +82,7 @@ export const migrationEntries = [
[27, "ProjectionThreadShellSummary", Migration0027],
[28, "BackfillProjectionThreadShellSummary", Migration0028],
[29, "CleanupInvalidProjectionPendingApprovals", Migration0029],
[30, "CanonicalizeModelSelectionOptions", Migration0030],
] as const;

export const makeMigrationLoader = (throughId?: number) =>
Expand Down
Loading
Loading