Skip to content
Closed
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
120 changes: 120 additions & 0 deletions apps/server/src/provider/Layers/ClaudeAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { ServerSettingsService } from "../../serverSettings.ts";
import { ProviderAdapterProcessError, ProviderAdapterValidationError } from "../Errors.ts";
import type { ClaudeAdapterShape } from "../Services/ClaudeAdapter.ts";
import { makeClaudeAdapter, type ClaudeAdapterLiveOptions } from "./ClaudeAdapter.ts";
import { getClaudeModelCapabilities } from "./ClaudeProvider.ts";
const decodeClaudeSettings = Schema.decodeSync(ClaudeSettings);

// Test-local service tag so the rest of the file can keep using `yield* ClaudeAdapter`.
Expand Down Expand Up @@ -273,6 +274,51 @@ const THREAD_ID = ThreadId.make("thread-claude-1");
const RESUME_THREAD_ID = ThreadId.make("thread-claude-resume");

describe("ClaudeAdapterLive", () => {
it("keeps built-in capabilities ahead of custom metadata", () => {
const builtIn = getClaudeModelCapabilities("claude-opus-5");
const resolved = getClaudeModelCapabilities("claude-opus-5", {
"claude-opus-5": { optionDescriptors: [] },
});

assert.deepEqual(resolved, builtIn);
assert.equal(
resolved.optionDescriptors?.some((descriptor) => descriptor.id === "effort"),
true,
);
});

it("keeps arbitrary custom controls in the Claude catalog", () => {
const resolved = getClaudeModelCapabilities("vendor/preview", {
"vendor/preview": {
optionDescriptors: [
{
id: "effort",
label: "Reasoning",
type: "select",
options: [{ id: "high", label: "High" }],
},
{
id: "serviceTier",
label: "Service Tier",
type: "select",
options: [{ id: "priority", label: "Priority" }],
},
],
},
});

assert.deepEqual(
resolved.optionDescriptors?.map((descriptor) => descriptor.id),
["effort", "serviceTier"],
);
});

it("does not read inherited object keys as Claude custom capabilities", () => {
const resolved = getClaudeModelCapabilities("constructor", {});

assert.deepEqual(resolved, { optionDescriptors: [] });
});

it.effect("returns validation error for non-claude provider on startSession", () => {
const harness = makeHarness();
return Effect.gen(function* () {
Expand Down Expand Up @@ -700,6 +746,80 @@ describe("ClaudeAdapterLive", () => {
);
});

it.effect("forwards declared custom-model controls to Claude SDK launch options", () => {
const customModelCapabilities = {
"gateway/model": {
optionDescriptors: [
{
id: "effort",
label: "Reasoning",
type: "select" as const,
options: [
{ id: "low", label: "Low" },
{ id: "xhigh", label: "Extra High" },
{ id: "max", label: "Max", isDefault: true },
],
},
{
id: "fastMode",
label: "Fast Mode",
type: "boolean" as const,
},
{
id: "thinking",
label: "Thinking",
type: "boolean" as const,
},
{
id: "contextWindow",
label: "Context Window",
type: "select" as const,
options: [
{ id: "200k", label: "200k" },
{ id: "1m", label: "1M", isDefault: true },
],
},
],
},
};
const harness = makeHarness({
claudeConfig: {
customModels: ["gateway/model"],
customModelCapabilities,
} as Partial<ClaudeSettings>,
});

return Effect.gen(function* () {
const adapter = yield* ClaudeAdapter;
yield* adapter.startSession({
threadId: THREAD_ID,
provider: ProviderDriverKind.make("claudeAgent"),
modelSelection: createModelSelection(
ProviderInstanceId.make("claudeAgent"),
"gateway/model",
[
{ id: "effort", value: "xhigh" },
{ id: "fastMode", value: false },
{ id: "thinking", value: false },
{ id: "contextWindow", value: "1m" },
],
),
runtimeMode: "full-access",
});

const createInput = harness.getLastCreateQueryInput();
assert.equal(createInput?.options.model, "gateway/model[1m]");
assert.equal(createInput?.options.effort, "xhigh");
assert.deepEqual(createInput?.options.settings, {
alwaysThinkingEnabled: false,
fastMode: false,
});
}).pipe(
Effect.provideService(Random.Random, makeDeterministicRandomService()),
Effect.provide(harness.layer),
);
});

it.effect("treats ultrathink as a prompt keyword instead of a session effort", () => {
const harness = makeHarness();
return Effect.gen(function* () {
Expand Down
57 changes: 41 additions & 16 deletions apps/server/src/provider/Layers/ClaudeAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
ProviderDriverKind,
ProviderInstanceId,
type ModelSelection,
type ModelCapabilities,
ProviderItemId,
type ProviderRuntimeEvent,
type ProviderRuntimeTurnStatus,
Expand Down Expand Up @@ -79,6 +80,7 @@ import { resolveClaudeSdkExecutablePath } from "../Drivers/ClaudeExecutable.ts";
import { makeClaudeEnvironment } from "../Drivers/ClaudeHome.ts";
import {
getClaudeModelCapabilities,
isConfiguredCustomClaudeModel,
isClaudeUltracodeEffort,
normalizeClaudeCliEffort,
resolveClaudeApiModelId,
Expand Down Expand Up @@ -351,8 +353,9 @@ function normalizeClaudeStreamMessages(
function getEffectiveClaudeAgentEffort(
effort: string | null | undefined,
model: string | null | undefined,
isCustomModel = false,
): ClaudeSdkEffort | null {
const normalized = normalizeClaudeCliEffort(effort, model);
const normalized = normalizeClaudeCliEffort(effort, model, isCustomModel);
return normalized ? (normalized as ClaudeSdkEffort) : null;
}

Expand Down Expand Up @@ -439,6 +442,7 @@ function maxClaudeContextWindowFromModelUsage(

function selectedClaudeContextWindow(
modelSelection: ModelSelection | undefined,
customModelCapabilities: Readonly<Record<string, ModelCapabilities>> = {},
): number | undefined {
switch (modelSelection?.model) {
case "claude-opus-4-8":
Expand All @@ -447,7 +451,7 @@ function selectedClaudeContextWindow(
return 1_000_000;
}

switch (resolveClaudeContextWindow(modelSelection)) {
switch (resolveClaudeContextWindow(modelSelection, customModelCapabilities)) {
case "1m":
return 1_000_000;
case "200k":
Expand Down Expand Up @@ -1210,14 +1214,15 @@ const CLAUDE_SETTING_SOURCES = [
function buildPromptText(
input: ProviderSendTurnInput,
boundInstanceId: ProviderInstanceId,
customModelCapabilities: Readonly<Record<string, ModelCapabilities>> = {},
): string {
const rawEffort =
input.modelSelection?.instanceId === boundInstanceId
? getModelSelectionStringOptionValue(input.modelSelection, "effort")
: null;
const claudeModel =
input.modelSelection?.instanceId === boundInstanceId ? input.modelSelection.model : undefined;
const caps = getClaudeModelCapabilities(claudeModel);
const caps = getClaudeModelCapabilities(claudeModel, customModelCapabilities);

const promptEffort = resolvePromptInjectedEffort(caps, rawEffort);
return applyClaudePromptEffortPrefix(input.input?.trim() ?? "", promptEffort);
Expand Down Expand Up @@ -1257,9 +1262,14 @@ const buildUserMessageEffect = Effect.fn("buildUserMessageEffect")(function* (
readonly fileSystem: FileSystem.FileSystem;
readonly attachmentsDir: string;
readonly boundInstanceId: ProviderInstanceId;
readonly customModelCapabilities: Readonly<Record<string, ModelCapabilities>>;
},
) {
const text = buildPromptText(input, dependencies.boundInstanceId);
const text = buildPromptText(
input,
dependencies.boundInstanceId,
dependencies.customModelCapabilities,
);
const sdkContent: Array<Record<string, unknown>> = [];

if (text.length > 0) {
Expand Down Expand Up @@ -1656,6 +1666,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
options?: ClaudeAdapterLiveOptions,
) {
const boundInstanceId = options?.instanceId ?? ProviderInstanceId.make("claudeAgent");
const customModelCapabilities = claudeSettings.customModelCapabilities ?? {};
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const serverConfig = yield* ServerConfig;
Expand Down Expand Up @@ -4111,10 +4122,15 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
const extraArgs = parseCliArgs(claudeSettings.launchArgs).flags;
const modelSelection =
input.modelSelection?.instanceId === boundInstanceId ? input.modelSelection : undefined;
const caps = getClaudeModelCapabilities(modelSelection?.model);
const caps = getClaudeModelCapabilities(modelSelection?.model, customModelCapabilities);
const descriptors = getProviderOptionDescriptors({ caps });
const apiModelId = modelSelection ? resolveClaudeApiModelId(modelSelection) : undefined;
const initialContextWindow = selectedClaudeContextWindow(modelSelection);
const apiModelId = modelSelection
? resolveClaudeApiModelId(modelSelection, customModelCapabilities)
: undefined;
const initialContextWindow = selectedClaudeContextWindow(
modelSelection,
customModelCapabilities,
);
const rawEffort = getModelSelectionStringOptionValue(modelSelection, "effort");
const effort = resolveClaudeEffort(caps, rawEffort) ?? null;
const fastModeSupported = descriptors.some(
Expand All @@ -4123,14 +4139,18 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
const thinkingSupported = descriptors.some(
(descriptor) => descriptor.type === "boolean" && descriptor.id === "thinking",
);
const fastMode =
getModelSelectionBooleanOptionValue(modelSelection, "fastMode") === true &&
fastModeSupported;
const fastMode = fastModeSupported
? getModelSelectionBooleanOptionValue(modelSelection, "fastMode")
: undefined;
const thinking = thinkingSupported
? getModelSelectionBooleanOptionValue(modelSelection, "thinking")
: undefined;
const ultracode = isClaudeUltracodeEffort(effort);
const effectiveEffort = getEffectiveClaudeAgentEffort(effort, modelSelection?.model);
const effectiveEffort = getEffectiveClaudeAgentEffort(
effort,
modelSelection?.model,
isConfiguredCustomClaudeModel(modelSelection?.model, customModelCapabilities),
);
const runtimeModeToPermission: Record<string, PermissionMode> = {
"auto-accept-edits": "acceptEdits",
auto: "auto",
Expand All @@ -4139,7 +4159,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
const permissionMode = runtimeModeToPermission[input.runtimeMode];
const settings = {
...(typeof thinking === "boolean" ? { alwaysThinkingEnabled: thinking } : {}),
...(fastMode ? { fastMode: true } : {}),
...(typeof fastMode === "boolean" ? { fastMode } : {}),
...(ultracode ? { ultracode: true } : {}),
};
const mcpSession = McpProviderSession.readMcpProviderSession(input.threadId);
Expand Down Expand Up @@ -4303,7 +4323,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
...(input.cwd ? { cwd: input.cwd } : {}),
...(effectiveEffort ? { effort: effectiveEffort } : {}),
...(permissionMode ? { permissionMode } : {}),
...(fastMode ? { fastMode: true } : {}),
...(typeof fastMode === "boolean" ? { fastMode } : {}),
},
},
providerRefs: {},
Expand Down Expand Up @@ -4372,7 +4392,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
}

if (modelSelection?.model) {
const apiModelId = resolveClaudeApiModelId(modelSelection);
const apiModelId = resolveClaudeApiModelId(modelSelection, customModelCapabilities);
if (context.currentApiModelId !== apiModelId) {
yield* Effect.tryPromise({
try: () => context.query.setModel(apiModelId),
Expand All @@ -4384,13 +4404,17 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
...context.session,
model: modelSelection.model,
};
const turnCaps = getClaudeModelCapabilities(modelSelection.model);
const turnCaps = getClaudeModelCapabilities(modelSelection.model, customModelCapabilities);
const turnEffort = resolveClaudeEffort(
turnCaps,
getModelSelectionStringOptionValue(modelSelection, "effort"),
);
context.currentEffort =
getEffectiveClaudeAgentEffort(turnEffort ?? null, modelSelection.model) ?? undefined;
getEffectiveClaudeAgentEffort(
turnEffort ?? null,
modelSelection.model,
isConfiguredCustomClaudeModel(modelSelection.model, customModelCapabilities),
) ?? undefined;
}

// Apply interaction mode by switching the SDK's permission mode.
Expand Down Expand Up @@ -4447,6 +4471,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
fileSystem,
attachmentsDir: serverConfig.attachmentsDir,
boundInstanceId,
customModelCapabilities,
});

yield* Queue.offer(context.promptQueue, {
Expand Down
Loading
Loading