Skip to content
343 changes: 326 additions & 17 deletions apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts

Large diffs are not rendered by default.

146 changes: 99 additions & 47 deletions apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts

Large diffs are not rendered by default.

72 changes: 72 additions & 0 deletions apps/server/src/provider/ClaudeModelCatalog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,78 @@ const catalogFromManifest = (data: ModelManifestData) =>
resolveClaudeModelCatalog(resolveProviderCatalog(data, CLAUDE) ?? undefined);

describe("Claude model catalog", () => {
it("opts custom routes into 1M without assuming their default context window", () => {
const catalog = scopeClaudeModelCatalog(catalogFromManifest(manifest()), [
"bare-route",
{
slug: "gateway-route",
capabilities: {
optionDescriptors: [
{
id: "contextWindow",
label: "Context Window",
type: "select",
options: [
{ id: "default", label: "Provider default", isDefault: true },
{ id: "1m", label: "1M" },
{ id: "constructor", label: "Unmapped" },
],
},
],
},
},
]);
const selection = {
instanceId: ProviderInstanceId.make("claudeAgent"),
model: "gateway-route",
};
assert.strictEqual(resolveClaudeCatalogApiModelId(catalog, selection), "gateway-route");
assert.isUndefined(resolveClaudeCatalogContextWindowTokens(catalog, selection));
const unmapped = { ...selection, options: [{ id: "contextWindow", value: "constructor" }] };
assert.strictEqual(resolveClaudeCatalogApiModelId(catalog, unmapped), "gateway-route");
assert.isUndefined(resolveClaudeCatalogContextWindowTokens(catalog, unmapped));
const expanded = { ...selection, options: [{ id: "contextWindow", value: "1m" }] };
assert.strictEqual(resolveClaudeCatalogApiModelId(catalog, expanded), "gateway-route[1m]");
assert.strictEqual(resolveClaudeCatalogContextWindowTokens(catalog, expanded), 1_000_000);
const reset = { ...selection, options: [{ id: "contextWindow", value: "default" }] };
assert.strictEqual(resolveClaudeCatalogApiModelId(catalog, reset), "gateway-route");
assert.isUndefined(resolveClaudeCatalogContextWindowTokens(catalog, reset));
const undeclared = { ...expanded, model: "bare-route" };
assert.strictEqual(resolveClaudeCatalogApiModelId(catalog, undeclared), "bare-route");
assert.isUndefined(resolveClaudeCatalogContextWindowTokens(catalog, undeclared));
});

it("preserves already tagged custom routes and ignores unrecognized context choices", () => {
const catalog = scopeClaudeModelCatalog(catalogFromManifest(manifest()), [
{
slug: "gateway-route[1m]",
capabilities: {
optionDescriptors: [
{
id: "contextWindow",
label: "Context Window",
type: "select",
options: [
{ id: "default", label: "Provider default", isDefault: true },
{ id: "1m", label: "1M" },
{ id: "large", label: "Large" },
],
},
],
},
},
]);
for (const value of ["default", "1m", "large"]) {
const selection = {
instanceId: ProviderInstanceId.make("claudeAgent"),
model: "gateway-route[1m]",
options: [{ id: "contextWindow", value }],
};
assert.strictEqual(resolveClaudeCatalogApiModelId(catalog, selection), "gateway-route[1m]");
assert.strictEqual(resolveClaudeCatalogContextWindowTokens(catalog, selection), 1_000_000);
}
});

it("resolves capacity from selected options and fixed catalog windows without guessing custom models", () => {
const source = manifest();
const profile = source.providers!.claudeAgent!.profiles.synthetic!;
Expand Down
45 changes: 42 additions & 3 deletions apps/server/src/provider/ClaudeModelCatalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,14 +99,26 @@ export function scopeClaudeModelCatalog(
const customCatalogModels: Array<ClaudeCatalogModel> = [];
for (const entry of customEntries) {
if (!entry.capabilities || builtInSlugs.has(entry.slug)) continue;
const hasExtendedContext = entry.capabilities.optionDescriptors?.some(
(descriptor) =>
descriptor.id === "contextWindow" &&
descriptor.type === "select" &&
descriptor.options.some((choice) => choice.id === "1m"),
);
customCatalogModels.push({
model: {
slug: entry.slug,
name: entry.name,
isCustom: true,
capabilities: entry.capabilities,
},
runtime: {},
runtime: hasExtendedContext
? {
modelSuffixes: { contextWindow: { "1m": "[1m]" } },
contextWindowTokens: { "1m": 1_000_000 },
...(/\[1m\]$/i.test(entry.slug) ? { fixedContextWindowTokens: 1_000_000 } : {}),
}
: {},
compatibility: {},
});
}
Expand All @@ -132,6 +144,28 @@ export function resolveClaudeModelSlug(catalog: ClaudeModelCatalog, slugOrAlias:
return resolveClaudeCatalogModel(catalog, slugOrAlias)?.model.slug ?? slugOrAlias;
}

export function resolveClaudeCatalogModelSelection(
catalog: ClaudeModelCatalog,
instanceId: ModelSelection["instanceId"],
model: string,
): ModelSelection {
const exact = resolveClaudeCatalogModel(catalog, model);
if (exact) return { instanceId, model: exact.model.slug };

for (const entry of catalog.models) {
for (const [id, suffixes] of Object.entries(entry.runtime.modelSuffixes ?? {})) {
for (const [value, suffix] of Object.entries(suffixes)) {
if (!suffix || !model.toLowerCase().endsWith(suffix.toLowerCase())) continue;
const base = resolveClaudeCatalogModel(catalog, model.slice(0, -suffix.length));
if (base !== entry) continue;
return { instanceId, model: entry.model.slug, options: [{ id, value }] };
}
}
}

return { instanceId, model };
}

export function getClaudeCatalogModelCapabilities(
catalog: ClaudeModelCatalog,
slugOrAlias: string | null | undefined,
Expand Down Expand Up @@ -268,7 +302,9 @@ export function resolveClaudeCatalogApiModelId(
const value = getProviderOptionCurrentValue(
descriptors.find((descriptor) => descriptor.id === optionId),
);
if (typeof value === "string" && suffixes[value]) return `${slug}${suffixes[value]}`;
const suffix =
typeof value === "string" && Object.hasOwn(suffixes, value) ? suffixes[value] : undefined;
if (suffix && !slug.toLowerCase().endsWith(suffix.toLowerCase())) return `${slug}${suffix}`;
}
return slug;
}
Expand All @@ -281,5 +317,8 @@ export function resolveClaudeCatalogContextWindowTokens(
if (!entry) return undefined;
if (entry.runtime.fixedContextWindowTokens) return entry.runtime.fixedContextWindowTokens;
const contextWindow = resolveClaudeCatalogContextWindow(catalog, modelSelection);
return contextWindow ? entry.runtime.contextWindowTokens?.[contextWindow] : undefined;
const windows = entry.runtime.contextWindowTokens;
return contextWindow && windows && Object.hasOwn(windows, contextWindow)
? windows[contextWindow]
: undefined;
}
30 changes: 20 additions & 10 deletions apps/web/src/components/chat/ComposerModelEffort.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ export function describeComposerEffort(
.effort?.descriptor ?? null;
const ultrathink = visibility.ultrathinkPromptControlled && reasoning === primary;
const glyph =
reasoning?.type === "select"
!display.thinkingOff && reasoning?.type === "select"
? resolveEffortGlyph(
reasoning,
ultrathink
Expand All @@ -117,15 +117,25 @@ export function describeComposerEffort(
)
: null;
// Workflow levels such as Ultracode always show their name; the setting names every level.
const word = glyph
? (glyph.special ?? (effortDisplay === "label" ? glyph.label : null))
: !reasoning
? null
: reasoning.type === "boolean"
? `${reasoning.label} ${reasoning.currentValue === true ? "On" : "Off"}`
: ultrathink
? "Ultrathink"
: getProviderOptionCurrentLabel(reasoning, modelSelection, traits.reportedModelSelection);
const word = display.thinkingOff
? "Off"
: glyph
? (glyph.special ?? (effortDisplay === "label" ? glyph.label : null))
: !reasoning
? null
: reasoning.type === "boolean"
? reasoning.id === "thinking"
? reasoning.currentValue === true
? "Thinking"
: "Off"
: `${reasoning.label} ${reasoning.currentValue === true ? "On" : "Off"}`
: ultrathink
? "Ultrathink"
: getProviderOptionCurrentLabel(
reasoning,
modelSelection,
traits.reportedModelSelection,
);
const fast = display.speedLabel === "Fast" || display.speedLabel === "Ultrafast";
// Agent options such as modes live in the picker; the trigger adds only the context window.
const contextWindow =
Expand Down
6 changes: 6 additions & 0 deletions docs/user/composer.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,12 @@ On web and desktop, use Settings → Providers → **Models** to add an unlisted
name and options. Only options supported by the provider integration affect turns. Antigravity
uses its account catalog and does not support custom models.

For a custom Claude route, add a Choices option with ID `contextWindow` and values `default`
(Provider default) and `1m` (1M). Choosing 1M appends `[1m]` to the model ID for Claude Code.
Provider default leaves the ID unchanged and does not assume a context size. Only choose 1M
when your gateway and model support it. Add a separate Thinking toggle with ID `thinking`
to control thinking independently. Models without these options keep their existing behavior.

To rename a built-in model on every client, edit the server's `settings.json` in its `userdata`
directory. Add a custom model entry with the built-in model ID and a new name under that provider
instance:
Expand Down
57 changes: 57 additions & 0 deletions packages/client-runtime/src/modelTraits.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { describe, expect, it } from "vite-plus/test";
import { ProviderDriverKind, type ProviderOptionDescriptor } from "@t3tools/contracts";

import { buildTraitsTriggerDisplay } from "./modelTraits.ts";

describe("composer thinking summary", () => {
it.each([
{ thinking: true, toggleFirst: false, effort: "effort", label: "Extra High · 1M" },
{ thinking: true, toggleFirst: true, effort: "effort", label: "Extra High · 1M" },
{ thinking: false, toggleFirst: false, effort: "effort", label: "Off · 1M" },
{ thinking: false, toggleFirst: true, effort: "effort", label: "Off · 1M" },
{ thinking: true, toggleFirst: true, effort: "thought_level", label: "Extra High · 1M" },
{ thinking: false, toggleFirst: false, effort: "thought_level", label: "Off · 1M" },
{ thinking: true, toggleFirst: true, effort: null, label: "Thinking · 1M" },
{ thinking: false, toggleFirst: true, effort: null, label: "Off · 1M" },
])("shows $label with thinking=$thinking and toggleFirst=$toggleFirst", (testCase) => {
const thinking: ProviderOptionDescriptor = {
id: "thinking",
label: "Thinking",
type: "boolean",
currentValue: testCase.thinking,
};
const effort: ProviderOptionDescriptor = {
id: testCase.effort ?? "effort",
label: "Reasoning",
type: "select",
...(testCase.effort === "thought_level" ? { role: "reasoning" as const } : {}),
currentValue: "xhigh",
options: [{ id: "xhigh", label: "Extra High" }],
};
const descriptors: Array<ProviderOptionDescriptor> = [
...(testCase.effort
? testCase.toggleFirst
? [thinking, effort]
: [effort, thinking]
: [thinking]),
{
id: "contextWindow",
label: "Context Window",
type: "select",
currentValue: "1m",
options: [{ id: "1m", label: "1M" }],
},
];
expect(
buildTraitsTriggerDisplay({
provider: ProviderDriverKind.make("claudeAgent"),
descriptors,
primarySelectDescriptorId: testCase.effort ?? "contextWindow",
ultrathinkPromptControlled: false,
}),
).toMatchObject({
label: testCase.label,
thinkingOff: !testCase.thinking,
});
});
});
22 changes: 20 additions & 2 deletions packages/client-runtime/src/modelTraits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,12 +120,30 @@ export function buildTraitsTriggerDisplay(input: {
label: string;
/** "Fast" or "Ultrafast" when a speed option is on. */
speedLabel: string | null;
thinkingOff: boolean;
} {
const thinkingOff = input.descriptors.some(
(descriptor) =>
descriptor.id === "thinking" &&
descriptor.type === "boolean" &&
descriptor.currentValue === false,
);
const hasReasoningSelect = input.descriptors.some(isReasoningSelect);
let fastModeFallbackLabel: string | null = null;
let speedLabel: string | null = null;
let reasoningLabelIndex = -1;
const labels: Array<string> = [];
for (const descriptor of input.descriptors) {
if (descriptor.id === "thinking" && descriptor.type === "boolean") {
if (thinkingOff || (!hasReasoningSelect && descriptor.currentValue === true)) {
reasoningLabelIndex = labels.length;
labels.push(thinkingOff ? "Off" : "Thinking");
}
continue;
Comment thread
chisewaguri marked this conversation as resolved.
Resolved
}
if (thinkingOff && isReasoningSelect(descriptor)) {
continue;
}
if (descriptor.id === "fastMode" && descriptor.type === "boolean") {
speedLabel = descriptor.currentValue === true ? "Fast" : null;
fastModeFallbackLabel = speedLabel ?? "Normal";
Expand Down Expand Up @@ -174,7 +192,7 @@ export function buildTraitsTriggerDisplay(input: {
}

if (labels.length === 0 && fastModeFallbackLabel !== null) {
return { label: fastModeFallbackLabel, speedLabel };
return { label: fastModeFallbackLabel, speedLabel, thinkingOff };
}
if (speedLabel) {
if (reasoningLabelIndex >= 0) {
Expand All @@ -183,5 +201,5 @@ export function buildTraitsTriggerDisplay(input: {
labels.push(speedLabel);
}
}
return { label: labels.join(" · "), speedLabel };
return { label: labels.join(" · "), speedLabel, thinkingOff };
}
Loading