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
71 changes: 71 additions & 0 deletions apps/server/src/provider/ClaudeModelCatalog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,15 @@ import { hasValidClaudeManifestAdapters } from "./ClaudeModelManifest.ts";
import type { ModelManifestData } from "./ModelManifest.ts";
import {
formatClaudeVersionUpgradeMessage,
getClaudeCatalogModelCapabilities,
isClaudeCatalogCustomEffortProfile,
normalizeClaudeCatalogEffort,
resolveClaudeCatalogApiModelId,
resolveClaudeCatalogEffort,
resolveClaudeModelCatalog,
resolveClaudeModelsForVersion,
resolveClaudeModelSlug,
scopeClaudeModelCatalog,
} from "./ClaudeModelCatalog.ts";

/**
Expand Down Expand Up @@ -114,6 +118,73 @@ describe("Claude model catalog", () => {
);
});

it("scopes custom aliases and attaches configured effort profiles", () => {
const catalog = scopeClaudeModelCatalog(
resolveClaudeModelCatalog(manifest()),
["synthetic", "custom-model", "custom-model"],
{
"custom-model": {
capabilities: { reasoning: { levels: ["low", "xhigh"] } },
},
},
);

assert.strictEqual(resolveClaudeModelSlug(catalog, "synthetic"), "synthetic");
assert.isTrue(isClaudeCatalogCustomEffortProfile(catalog, "custom-model"));
assert.deepStrictEqual(
getClaudeCatalogModelCapabilities(catalog, "custom-model").optionDescriptors?.[0],
{
id: "effort",
label: "Reasoning",
type: "select",
options: [
{ id: "default", label: "Default", isDefault: true },
{ id: "low", label: "Low" },
{ id: "xhigh", label: "Extra High" },
],
currentValue: "default",
},
);
assert.strictEqual(resolveClaudeCatalogEffort(catalog, "custom-model", undefined), "default");
assert.strictEqual(normalizeClaudeCatalogEffort(catalog, "xhigh", "custom-model"), "xhigh");
assert.deepStrictEqual(
catalog.models.filter((entry) => entry.model.slug === "custom-model").length,
1,
);
});

it("keeps built-in catalog capabilities when a custom profile uses the same slug", () => {
const catalog = scopeClaudeModelCatalog(
resolveClaudeModelCatalog(manifest()),
["claude-synthetic-next"],
{
"claude-synthetic-next": {
capabilities: { reasoning: { levels: ["low"] } },
},
},
);

assert.isFalse(isClaudeCatalogCustomEffortProfile(catalog, "claude-synthetic-next"));
assert.strictEqual(
resolveClaudeCatalogEffort(catalog, "claude-synthetic-next", undefined),
"extreme",
);
});

it("does not treat prototype keys as configured profiles", () => {
const catalog = scopeClaudeModelCatalog(resolveClaudeModelCatalog(manifest()), ["toString"], {
"other-model": {
capabilities: { reasoning: { levels: ["high"] } },
},
});

assert.isFalse(isClaudeCatalogCustomEffortProfile(catalog, "toString"));
assert.deepStrictEqual(
getClaudeCatalogModelCapabilities(catalog, "toString").optionDescriptors,
[],
);
});

it("rejects malformed adapter mappings", () => {
const base = manifest();
const malformed: ModelManifestData = {
Expand Down
3 changes: 3 additions & 0 deletions apps/server/src/provider/ClaudeModelCatalog.testFixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export const SYNTHETIC_CLAUDE_MODEL_CATALOG: ClaudeModelCatalog = {
},
runtime,
compatibility: {},
customEffortProfile: false,
},
{
model: {
Expand All @@ -66,6 +67,7 @@ export const SYNTHETIC_CLAUDE_MODEL_CATALOG: ClaudeModelCatalog = {
},
runtime,
compatibility: {},
customEffortProfile: false,
},
{
model: {
Expand All @@ -78,6 +80,7 @@ export const SYNTHETIC_CLAUDE_MODEL_CATALOG: ClaudeModelCatalog = {
},
runtime: {},
compatibility: {},
customEffortProfile: false,
},
],
};
104 changes: 84 additions & 20 deletions apps/server/src/provider/ClaudeModelCatalog.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import {
CUSTOM_MODEL_REASONING_DEFAULT,
type CustomModelProfile,
type CustomModelProfiles,
type ModelCapabilities,
type ModelSelection,
ProviderDriverKind,
Expand Down Expand Up @@ -32,6 +35,7 @@ export interface ClaudeCatalogModel {
readonly model: ServerProviderModel;
readonly runtime: ClaudeCodeProfile;
readonly compatibility: ClaudeCodeCompatibility;
readonly customEffortProfile: boolean;
}

export interface ClaudeModelCatalog {
Expand All @@ -51,6 +55,7 @@ function tryResolveClaudeModelCatalog(manifest: ModelManifestData): ClaudeModelC
model: entry.model,
runtime: profile.value.claudeCode ?? {},
compatibility: adapter.value.claudeCode ?? {},
customEffortProfile: false,
});
}

Expand All @@ -70,33 +75,85 @@ export function resolveClaudeModelCatalog(manifest: ModelManifestData): ClaudeMo

export const BUNDLED_CLAUDE_MODEL_CATALOG = resolveClaudeModelCatalog(BUNDLED_MODEL_MANIFEST);

/** Keeps custom model aliases opaque while preserving canonical built-in models and capabilities. */
const CUSTOM_MODEL_REASONING_LABELS: Readonly<Record<string, string>> = {
low: "Low",
medium: "Medium",
high: "High",
xhigh: "Extra High",
max: "Max",
};

function customModelCapabilities(profile: CustomModelProfile | undefined): ModelCapabilities {
if (!profile) return EMPTY_CAPABILITIES;
return {
optionDescriptors: [
{
id: "effort",
label: "Reasoning",
type: "select",
options: [
{ id: CUSTOM_MODEL_REASONING_DEFAULT, label: "Default", isDefault: true },
...profile.capabilities.reasoning.levels.map((level) => ({
id: level,
label: CUSTOM_MODEL_REASONING_LABELS[level] ?? level,
})),
],
currentValue: CUSTOM_MODEL_REASONING_DEFAULT,
},
],
};
}

/** Keeps custom model aliases opaque and attaches settings-owned custom model profiles. */
export function scopeClaudeModelCatalog(
catalog: ClaudeModelCatalog,
customModels: ReadonlyArray<string>,
customModelProfiles?: CustomModelProfiles,
): ClaudeModelCatalog {
const customAliases = new Set(
customModels.flatMap((model) => {
const slug = normalizeCustomModelSlug(model);
return slug ? [slug.toLowerCase()] : [];
}),
);
if (customAliases.size === 0) return catalog;
const customSlugs = customModels.flatMap((model) => {
const slug = normalizeCustomModelSlug(model);
return slug ? [slug] : [];
});
if (customSlugs.length === 0) return catalog;

return {
models: catalog.models.map((entry) => {
if (!entry.model.aliases?.some((alias) => customAliases.has(alias.toLowerCase()))) {
return entry;
}
return {
...entry,
const customAliases = new Set(customSlugs.map((slug) => slug.toLowerCase()));
const builtInSlugs = new Set(catalog.models.map((entry) => entry.model.slug));
const seenCustom = new Set<string>();
const builtIns = catalog.models.map((entry) => {
if (!entry.model.aliases?.some((alias) => customAliases.has(alias.toLowerCase()))) {
return entry;
}
return {
...entry,
model: {
...entry.model,
aliases: entry.model.aliases.filter((alias) => !customAliases.has(alias.toLowerCase())),
},
};
});
const custom = customSlugs.flatMap((slug): ReadonlyArray<ClaudeCatalogModel> => {
if (builtInSlugs.has(slug) || seenCustom.has(slug)) return [];
seenCustom.add(slug);
const profile =
customModelProfiles && Object.prototype.hasOwnProperty.call(customModelProfiles, slug)
? customModelProfiles[slug]
: undefined;
return [
{
model: {
...entry.model,
aliases: entry.model.aliases.filter((alias) => !customAliases.has(alias.toLowerCase())),
slug,
name: slug,
isCustom: true,
capabilities: customModelCapabilities(profile),
},
};
}),
};
runtime: {},
compatibility: {},
customEffortProfile: profile !== undefined,
},
];
});

return { models: [...builtIns, ...custom] };
}

export function resolveClaudeCatalogModel(
Expand All @@ -117,6 +174,13 @@ export function resolveClaudeModelSlug(catalog: ClaudeModelCatalog, slugOrAlias:
return resolveClaudeCatalogModel(catalog, slugOrAlias)?.model.slug ?? slugOrAlias;
}

export function isClaudeCatalogCustomEffortProfile(
catalog: ClaudeModelCatalog,
slugOrAlias: string | null | undefined,
): boolean {
return resolveClaudeCatalogModel(catalog, slugOrAlias)?.customEffortProfile === true;
}

export function getClaudeCatalogModelCapabilities(
catalog: ClaudeModelCatalog,
slugOrAlias: string | null | undefined,
Expand Down
64 changes: 64 additions & 0 deletions apps/server/src/provider/Layers/ClaudeAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,70 @@ describe("ClaudeAdapterLive", () => {
);
});

it.effect("forwards custom model effort into query options", () => {
const harness = makeHarness({
claudeConfig: {
customModels: ["custom-model"],
customModelProfiles: {
"custom-model": {
capabilities: { reasoning: { levels: ["xhigh", "max"] } },
},
},
},
});

return Effect.gen(function* () {
const adapter = yield* ClaudeAdapter;
yield* adapter.startSession({
threadId: THREAD_ID,
provider: ProviderDriverKind.make("claudeAgent"),
modelSelection: createModelSelection(
ProviderInstanceId.make("claudeAgent"),
"custom-model",
[{ id: "effort", value: "max" }],
),
runtimeMode: "full-access",
});

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

it.effect("omits effort for custom model Default", () => {
const harness = makeHarness({
claudeConfig: {
customModels: ["custom-model"],
customModelProfiles: {
"custom-model": {
capabilities: { reasoning: { levels: ["low", "xhigh"] } },
},
},
},
});

return Effect.gen(function* () {
const adapter = yield* ClaudeAdapter;
yield* adapter.startSession({
threadId: THREAD_ID,
provider: ProviderDriverKind.make("claudeAgent"),
modelSelection: createModelSelection(
ProviderInstanceId.make("claudeAgent"),
"custom-model",
[{ id: "effort", value: "default" }],
),
runtimeMode: "full-access",
});

assert.equal(harness.getLastCreateQueryInput()?.options.effort, undefined);
}).pipe(
Effect.provideService(Random.Random, makeDeterministicRandomService()),
Effect.provide(harness.layer),
);
});

it.effect("runs Claude SDK sessions with the configured CLAUDE_CONFIG_DIR", () => {
const harness = makeHarness({ claudeConfig: { homePath: "~/.claude-work" } });
return Effect.gen(function* () {
Expand Down
Loading
Loading