Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions apps/server/src/git/Layers/ClaudeTextGeneration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,41 @@ it.layer(ClaudeTextGenerationTestLayer)("ClaudeTextGenerationLive", (it) => {
),
);

it.effect("forwards Claude xhigh effort for Opus 4.7", () =>
withFakeClaudeEnv(
{
output: JSON.stringify({
structured_output: {
title: "Improve orchestration flow",
body: "Body",
},
}),
argsMustContain: "--effort xhigh",
},
Effect.gen(function* () {
const textGeneration = yield* TextGeneration;

const generated = yield* textGeneration.generatePrContent({
cwd: process.cwd(),
baseBranch: "main",
headBranch: "feature/claude-effect",
commitSummary: "Improve orchestration",
diffSummary: "1 file changed",
diffPatch: "diff --git a/README.md b/README.md",
modelSelection: {
provider: "claudeAgent",
model: "claude-opus-4-7",
options: {
effort: "xhigh",
},
},
});

expect(generated.title).toBe("Improve orchestration flow");
}),
),
);

it.effect("generates thread titles through the Claude provider", () =>
withFakeClaudeEnv(
{
Expand Down
31 changes: 30 additions & 1 deletion apps/server/src/provider/Layers/ClaudeAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ import { ProviderAdapterValidationError } from "../Errors.ts";
import { ClaudeAdapter } from "../Services/ClaudeAdapter.ts";
import { makeClaudeAdapterLive, type ClaudeAdapterLiveOptions } from "./ClaudeAdapter.ts";

type ClaudeQueryOptionsWithXHigh = Omit<ClaudeQueryOptions, "effort"> & {
readonly effort?: NonNullable<ClaudeQueryOptions["effort"]> | "xhigh";
};

class FakeClaudeQuery implements AsyncIterable<SDKMessage> {
private readonly queue: Array<SDKMessage> = [];
private readonly waiters: Array<{
Expand Down Expand Up @@ -141,7 +145,7 @@ function makeHarness(config?: {
let createInput:
| {
readonly prompt: AsyncIterable<SDKUserMessage>;
readonly options: ClaudeQueryOptions;
readonly options: ClaudeQueryOptionsWithXHigh;
}
| undefined;

Expand Down Expand Up @@ -351,6 +355,31 @@ describe("ClaudeAdapterLive", () => {
);
});

it.effect("forwards xhigh effort for Opus 4.7 into query options", () => {
const harness = makeHarness();
return Effect.gen(function* () {
const adapter = yield* ClaudeAdapter;
yield* adapter.startSession({
threadId: THREAD_ID,
provider: "claudeAgent",
modelSelection: {
provider: "claudeAgent",
model: "claude-opus-4-7",
options: {
effort: "xhigh",
},
},
runtimeMode: "full-access",
});

const createInput = harness.getLastCreateQueryInput();
assert.equal(createInput?.options.effort, "xhigh");
}).pipe(
Effect.provideService(Random.Random, makeDeterministicRandomService()),
Effect.provide(harness.layer),
);
});

it.effect("falls back to default effort when unsupported max is requested for Sonnet 4.6", () => {
const harness = makeHarness();
return Effect.gen(function* () {
Expand Down
17 changes: 13 additions & 4 deletions apps/server/src/provider/Layers/ClaudeAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,10 +173,15 @@ interface ClaudeQueryRuntime extends AsyncIterable<SDKMessage> {
readonly close: () => void;
}

type ClaudeSdkEffort = NonNullable<ClaudeQueryOptions["effort"]> | "xhigh";
type ClaudeQueryOptionsWithXHigh = Omit<ClaudeQueryOptions, "effort"> & {
readonly effort?: ClaudeSdkEffort;
};

export interface ClaudeAdapterLiveOptions {
readonly createQuery?: (input: {
readonly prompt: AsyncIterable<SDKUserMessage>;
readonly options: ClaudeQueryOptions;
readonly options: ClaudeQueryOptionsWithXHigh;
}) => ClaudeQueryRuntime;
readonly nativeEventLogPath?: string;
readonly nativeEventLogger?: EventNdjsonLogger;
Expand Down Expand Up @@ -926,8 +931,12 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
options?.createQuery ??
((input: {
readonly prompt: AsyncIterable<SDKUserMessage>;
readonly options: ClaudeQueryOptions;
}) => query({ prompt: input.prompt, options: input.options }) as ClaudeQueryRuntime);
readonly options: ClaudeQueryOptionsWithXHigh;
}) =>
query({
prompt: input.prompt,
options: input.options as ClaudeQueryOptions,
}) as ClaudeQueryRuntime);

const sessions = new Map<ThreadId, ClaudeSessionContext>();
const runtimeEventQueue = yield* Queue.unbounded<ProviderRuntimeEvent>();
Expand Down Expand Up @@ -2703,7 +2712,7 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
...(fastMode ? { fastMode: true } : {}),
};

const queryOptions: ClaudeQueryOptions = {
const queryOptions: ClaudeQueryOptionsWithXHigh = {
...(input.cwd ? { cwd: input.cwd } : {}),
...(apiModelId ? { model: apiModelId } : {}),
pathToClaudeCodeExecutable: claudeBinaryPath,
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/provider/Layers/ClaudeProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [
{ value: "low", label: "Low" },
{ value: "medium", label: "Medium" },
{ value: "high", label: "High", isDefault: true },
{ value: "xhigh", label: "Extra High" },
{ value: "max", label: "Max" },
{ value: "ultrathink", label: "Ultrathink" },
],
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/chat/TraitsPicker.browser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ async function mountClaudePicker(props?: {
prompt?: string;
options?: ClaudeModelOptions;
fallbackModelOptions?: {
effort?: "low" | "medium" | "high" | "max" | "ultrathink";
effort?: "low" | "medium" | "high" | "xhigh" | "max" | "ultrathink";
thinking?: boolean;
fastMode?: boolean;
} | null;
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/composerDraftStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,7 @@ function normalizeProviderModelOptions(
claudeCandidate?.effort === "low" ||
claudeCandidate?.effort === "medium" ||
claudeCandidate?.effort === "high" ||
claudeCandidate?.effort === "xhigh" ||
claudeCandidate?.effort === "max" ||
claudeCandidate?.effort === "ultrathink"
? claudeCandidate.effort
Expand Down
9 changes: 8 additions & 1 deletion packages/contracts/src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@ import type { ProviderKind } from "./orchestration";

export const CODEX_REASONING_EFFORT_OPTIONS = ["xhigh", "high", "medium", "low"] as const;
export type CodexReasoningEffort = (typeof CODEX_REASONING_EFFORT_OPTIONS)[number];
export const CLAUDE_CODE_EFFORT_OPTIONS = ["low", "medium", "high", "max", "ultrathink"] as const;
export const CLAUDE_CODE_EFFORT_OPTIONS = [
"low",
"medium",
"high",
"xhigh",
"max",
"ultrathink",
] as const;
export type ClaudeCodeEffort = (typeof CLAUDE_CODE_EFFORT_OPTIONS)[number];
export type ProviderReasoningEffort = CodexReasoningEffort | ClaudeCodeEffort;

Expand Down
21 changes: 21 additions & 0 deletions packages/contracts/src/provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,4 +113,25 @@ describe("ProviderSendTurnInput", () => {
expect(parsed.modelSelection.options?.effort).toBe("ultrathink");
expect(parsed.modelSelection.options?.fastMode).toBe(true);
});

it("accepts claude modelSelection including xhigh for Opus 4.7", () => {
const parsed = decodeProviderSendTurnInput({
threadId: "thread-1",
modelSelection: {
provider: "claudeAgent",
model: "claude-opus-4-7",
options: {
effort: "xhigh",
fastMode: true,
},
},
});

expect(parsed.modelSelection?.provider).toBe("claudeAgent");
if (parsed.modelSelection?.provider !== "claudeAgent") {
throw new Error("Expected claude modelSelection");
}
expect(parsed.modelSelection.options?.effort).toBe("xhigh");
expect(parsed.modelSelection.options?.fastMode).toBe(true);
});
});
Loading