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
26 changes: 19 additions & 7 deletions apps/server/src/provider/ClaudeModelCatalog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import { hasValidClaudeManifestAdapters } from "./ClaudeModelManifest.ts";
import type { ModelManifestData } from "./ModelManifest.ts";
import {
formatClaudeVersionUpgradeMessage,
getClaudeCatalogModelCapabilities,
normalizeClaudeCatalogEffort,
resolveClaudeCatalogApiModelId,
resolveClaudeCatalogContextWindowTokens,
resolveClaudeCatalogEffort,
resolveClaudeModelCatalog,
resolveClaudeModelsForVersion,
Expand Down Expand Up @@ -80,7 +82,7 @@ describe("Claude model catalog", () => {
);
});

it("resolves aliases and declarative adapter mappings", () => {
it("resolves aliases without exposing provider-managed effort or context options", () => {
const base = manifest();
const input: ModelManifestData = {
...base,
Expand All @@ -107,12 +109,24 @@ describe("Claude model catalog", () => {
"claude-synthetic-next",
);
assert.strictEqual(normalizeClaudeCatalogEffort(catalog, "extreme", "synthetic"), "high");
assert.deepStrictEqual(
getClaudeCatalogModelCapabilities(catalog, "synthetic").optionDescriptors,
[],
);
assert.strictEqual(resolveClaudeCatalogEffort(catalog, "synthetic", "extreme"), undefined);
assert.strictEqual(
resolveClaudeCatalogApiModelId(catalog, {
instanceId: ProviderInstanceId.make("claudeAgent"),
model: "synthetic",
}),
"claude-synthetic-next[large]",
"claude-synthetic-next",
);
assert.strictEqual(
resolveClaudeCatalogContextWindowTokens(catalog, {
instanceId: ProviderInstanceId.make("claudeAgent"),
model: "synthetic",
}),
undefined,
);
});

Expand All @@ -137,7 +151,7 @@ describe("Claude model catalog", () => {
assert.isFalse(hasValidClaudeManifestAdapters(malformed));
});

it("appends custom models with their own descriptors and keeps bare slugs opaque", () => {
it("hides provider-managed options on custom models and keeps bare slugs opaque", () => {
const catalog = scopeClaudeModelCatalog(resolveClaudeModelCatalog(manifest()), [
"synthetic",
{
Expand All @@ -163,15 +177,13 @@ describe("Claude model catalog", () => {
assert.strictEqual(resolveClaudeModelSlug(catalog, "synthetic"), "synthetic");
assert.strictEqual(resolveClaudeCatalogEffort(catalog, "synthetic", "extreme"), undefined);

// The entry with descriptors resolves user-defined effort ids and passes
// them through untouched (no effortMap, no model suffix).
assert.strictEqual(
resolveClaudeCatalogEffort(catalog, "claude-custom-tuned", "brutal"),
"brutal",
undefined,
);
assert.strictEqual(
resolveClaudeCatalogEffort(catalog, "claude-custom-tuned", "bogus"),
"gentle",
undefined,
);
assert.strictEqual(
normalizeClaudeCatalogEffort(catalog, "brutal", "claude-custom-tuned"),
Expand Down
22 changes: 17 additions & 5 deletions apps/server/src/provider/ClaudeModelCatalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,18 @@ import {
const CLAUDE = ProviderDriverKind.make("claudeAgent");
const EMPTY_CAPABILITIES: ModelCapabilities = { optionDescriptors: [] };

function useProviderConfiguredOptions(model: ServerProviderModel): ServerProviderModel {
const descriptors = model.capabilities?.optionDescriptors;
if (!descriptors) return model;
return {
...model,
capabilities: {
...model.capabilities,
optionDescriptors: descriptors.filter(({ id }) => id !== "effort" && id !== "contextWindow"),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropping contextWindow here also drops the manifest's default [1m] suffix: claude-opus-5-5[1m] becomes claude-opus-5-5, and claude-opus-4-6[1m] becomes claude-opus-4-6. That means 200K for Opus 4.6 everywhere and for third-party routes, and settings cannot restore it because T3 passes an explicit --model. See blocker 2.

},
};
}

export interface ClaudeCatalogModel {
readonly model: ServerProviderModel;
readonly runtime: ClaudeCodeProfile;
Expand All @@ -49,7 +61,7 @@ function tryResolveClaudeModelCatalog(manifest: ModelManifestData): ClaudeModelC
const adapter = decodeClaudeModelAdapter(entry.adapter ?? {});
if (Option.isNone(profile) || Option.isNone(adapter)) return null;
models.push({
model: entry.model,
model: useProviderConfiguredOptions(entry.model),
runtime: profile.value.claudeCode ?? {},
compatibility: adapter.value.claudeCode ?? {},
});
Expand All @@ -75,8 +87,8 @@ export const BUNDLED_CLAUDE_MODEL_CATALOG = resolveClaudeModelCatalog(BUNDLED_MO
* Scope the catalog to one instance's settings: custom model slugs stay opaque
* (a built-in alias they shadow is dropped, canonical slugs and capabilities
* are preserved), and custom entries that declare their own capabilities are
* appended so the adapter resolves effort / fast mode / thinking against the
* user's descriptors instead of the empty default. Custom entries carry no
* appended so the adapter resolves fast mode / thinking against the user's
* descriptors instead of the empty default. Custom entries carry no
* runtime profile, so option values pass through to Claude Code verbatim.
*/
export function scopeClaudeModelCatalog(
Expand Down Expand Up @@ -104,12 +116,12 @@ export function scopeClaudeModelCatalog(
for (const entry of customEntries) {
if (!entry.capabilities || builtInSlugs.has(entry.slug)) continue;
customCatalogModels.push({
model: {
model: useProviderConfiguredOptions({
slug: entry.slug,
name: entry.name,
isCustom: true,
capabilities: entry.capabilities,
},
}),
runtime: {},
compatibility: {},
});
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/provider/CodexDeveloperInstructions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ ${browserToolInstructions(browserToolsAvailable)}

export interface CodexRuntimeInfo {
readonly model: string;
readonly reasoningEffort: string;
readonly reasoningEffort?: string;
}

export function buildCodexDeveloperInstructions(
Expand Down
35 changes: 34 additions & 1 deletion apps/server/src/provider/Layers/ClaudeAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import * as TestClock from "effect/testing/TestClock";
import { attachmentRelativePath } from "../../attachmentStore.ts";
import { ServerConfig } from "../../config.ts";
import { ServerSettingsService } from "../../serverSettings.ts";
import { BUNDLED_CLAUDE_MODEL_CATALOG, type ClaudeModelCatalog } from "../ClaudeModelCatalog.ts";
import {
SYNTHETIC_CLAUDE_CAPABLE_MODEL,
SYNTHETIC_CLAUDE_COLLIDING_ALIAS,
Expand Down Expand Up @@ -167,6 +168,7 @@ function makeHarness(config?: {
readonly cwd?: string;
readonly baseDir?: string;
readonly claudeConfig?: Partial<ClaudeSettings>;
readonly modelCatalog?: ClaudeModelCatalog;
readonly instanceId?: ProviderInstanceId;
readonly scopedLimitNames?: ClaudeAdapterLiveOptions["scopedLimitNames"];
readonly environment?: ClaudeAdapterLiveOptions["environment"];
Expand All @@ -186,7 +188,7 @@ function makeHarness(config?: {
...(config?.environment ? { environment: config.environment } : {}),
...(config?.instanceId ? { instanceId: config.instanceId } : {}),
...(config?.scopedLimitNames ? { scopedLimitNames: config.scopedLimitNames } : {}),
modelCatalog: Effect.succeed(SYNTHETIC_CLAUDE_MODEL_CATALOG),
modelCatalog: Effect.succeed(config?.modelCatalog ?? SYNTHETIC_CLAUDE_MODEL_CATALOG),
...(config?.getSessionMessages ? { getSessionMessages: config.getSessionMessages } : {}),
...(config?.forkSession ? { forkSession: config.forkSession } : {}),
createQuery: (input) => {
Expand Down Expand Up @@ -590,6 +592,37 @@ describe("ClaudeAdapterLive", () => {
);
});

it.effect("uses Claude settings for effort and context despite saved T3 choices", () => {
const harness = makeHarness({
modelCatalog: BUNDLED_CLAUDE_MODEL_CATALOG,
environment: { ...process.env, CLAUDE_CODE_DISABLE_1M_CONTEXT: "1" },
});
return Effect.gen(function* () {
const adapter = yield* ClaudeAdapter;
yield* adapter.startSession({
threadId: THREAD_ID,
provider: ProviderDriverKind.make("claudeAgent"),
modelSelection: createModelSelection(
ProviderInstanceId.make("claudeAgent"),
"claude-opus-5-5",
[
{ id: "effort", value: "max" },
{ id: "contextWindow", value: "1m" },
],
),
runtimeMode: "full-access",
});

const options = harness.getLastCreateQueryInput()?.options;
assert.equal(options?.model, "claude-opus-5-5");
assert.equal(options?.effort, undefined);
assert.equal(options?.env?.CLAUDE_CODE_DISABLE_1M_CONTEXT, "1");
}).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
4 changes: 1 addition & 3 deletions apps/server/src/provider/Layers/CodexAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -415,7 +415,7 @@ sessionErrorLayer("CodexAdapterLive session errors", (it) => {
}),
);

it.effect("maps codex model options before sending a turn", () =>
it.effect("keeps Codex effort in config while mapping service tier", () =>
Effect.gen(function* () {
const adapter = yield* CodexAdapter;
yield* adapter.startSession({
Expand All @@ -442,7 +442,6 @@ sessionErrorLayer("CodexAdapterLive session errors", (it) => {
NodeAssert.deepStrictEqual(runtime.sendTurnImpl.mock.calls[0]?.[0], {
input: "hello",
model: "gpt-5.3-codex",
effort: "high",
serviceTier: "priority",
});
}),
Expand Down Expand Up @@ -608,7 +607,6 @@ sessionErrorLayer("CodexAdapterLive session errors", (it) => {
NodeAssert.deepStrictEqual(runtime.sendTurnImpl.mock.calls[0]?.[0], {
input: "hello",
model: "gpt-5.3-codex",
effort: "high",
serviceTier: "flex",
});
}).pipe(Effect.provide(customLayer));
Expand Down
10 changes: 0 additions & 10 deletions apps/server/src/provider/Layers/CodexAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ import { ChildProcessSpawner } from "effect/unstable/process";
import * as CodexErrors from "effect-codex-app-server/errors";
import * as EffectCodexSchema from "effect-codex-app-server/schema";

import { getModelSelectionStringOptionValue } from "@t3tools/shared/model";
import { getCodexServiceTierOptionValue } from "../../codexModelOptions.ts";
import * as McpProviderSession from "../../mcp/McpProviderSession.ts";

Expand Down Expand Up @@ -2527,10 +2526,6 @@ export const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* (
);

const session = yield* requireSession(input.threadId);
const reasoningEffort =
input.modelSelection?.instanceId === boundInstanceId
? getModelSelectionStringOptionValue(input.modelSelection, "reasoningEffort")
: undefined;
const serviceTier =
input.modelSelection?.instanceId === boundInstanceId
? getCodexServiceTierOptionValue(input.modelSelection)
Expand All @@ -2541,11 +2536,6 @@ export const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* (
...(input.modelSelection?.instanceId === boundInstanceId
? { model: input.modelSelection.model }
: {}),
...(reasoningEffort
? {
effort: reasoningEffort as EffectCodexSchema.V2TurnStartParams__ReasoningEffort,
}
: {}),
...(serviceTier ? { serviceTier } : {}),
...(input.interactionMode !== undefined ? { interactionMode: input.interactionMode } : {}),
...(codexAttachments.length > 0 ? { attachments: codexAttachments } : {}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@ import * as NodePath from "node:path";

import * as NodeServices from "@effect/platform-node/NodeServices";
import { it } from "@effect/vitest";
import { type ProviderApprovalDecision, type ProviderEvent, ThreadId } from "@t3tools/contracts";
import {
type ProviderApprovalDecision,
type ProviderEvent,
ThreadId,
TurnId,
} from "@t3tools/contracts";
import * as Deferred from "effect/Deferred";
import * as Effect from "effect/Effect";
import * as Fiber from "effect/Fiber";
Expand Down Expand Up @@ -741,7 +746,7 @@ describe("CodexSessionRuntime collab integration", () => {
yield* runtime.start();
yield* runtime.sendTurn({ input: "keep working" });
yield* runtime.sendTurn({ input: "queued follow-up" });
yield* runtime.interruptTurn();
yield* runtime.interruptTurn(TurnId.make(queuedTurnId));

const interrupts = NodeFS.readFileSync(interruptsPath, "utf8")
.trim()
Expand Down
9 changes: 1 addition & 8 deletions apps/server/src/provider/Layers/CodexProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { assert, it } from "@effect/vitest";

import { applyPreferredCodexDefaultModel, mapCodexModelCapabilities } from "./CodexProvider.ts";

it("maps current Codex model capability fields", () => {
it("exposes service tiers while leaving reasoning effort to Codex config", () => {
const capabilities = mapCodexModelCapabilities({
additionalSpeedTiers: [],
defaultReasoningEffort: "super-high",
Expand Down Expand Up @@ -34,13 +34,6 @@ it("maps current Codex model capability fields", () => {
});

assert.deepStrictEqual(capabilities.optionDescriptors, [
{
id: "reasoningEffort",
label: "Reasoning",
type: "select",
options: [{ id: "super-high", label: "super-high", isDefault: true }],
currentValue: "super-high",
},
{
id: "serviceTier",
label: "Service Tier",
Expand Down
38 changes: 0 additions & 38 deletions apps/server/src/provider/Layers/CodexProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,23 +77,8 @@ export interface CodexAppServerProviderSnapshot {
readonly skills: ReadonlyArray<ServerProviderSkill>;
}

const REASONING_EFFORT_LABELS: Readonly<Record<string, string>> = {
none: "None",
minimal: "Minimal",
low: "Low",
medium: "Medium",
high: "High",
xhigh: "Extra High",
max: "Max",
ultra: "Ultra",
};

const DEFAULT_SERVICE_TIER_ID = "default";

function reasoningEffortLabel(reasoningEffort: string): string {
return REASONING_EFFORT_LABELS[reasoningEffort] ?? reasoningEffort;
}

function codexAccountAuthLabel(account: CodexSchema.V2GetAccountResponse["account"]) {
if (!account) return undefined;
if (account.type === "apiKey") return "OpenAI API Key";
Expand Down Expand Up @@ -145,20 +130,6 @@ function codexAccountEmail(account: CodexSchema.V2GetAccountResponse["account"])
export function mapCodexModelCapabilities(
model: CodexSchema.V2ModelListResponse__Model,
): ModelCapabilities {
const reasoningOptions = model.supportedReasoningEfforts.map(({ reasoningEffort }) =>
reasoningEffort ===
(codexModelFamily(model.model) === "gpt-6-astra" ? "medium" : model.defaultReasoningEffort)
? {
id: reasoningEffort,
label: reasoningEffortLabel(reasoningEffort),
isDefault: true,
}
: {
id: reasoningEffort,
label: reasoningEffortLabel(reasoningEffort),
},
);
const defaultReasoning = reasoningOptions.find((option) => option.isDefault)?.id;
const serviceTiers =
model.serviceTiers && model.serviceTiers.length > 0
? model.serviceTiers
Expand All @@ -175,15 +146,6 @@ export function mapCodexModelCapabilities(
const defaultServiceTier = catalogDefaultServiceTier ?? DEFAULT_SERVICE_TIER_ID;
const optionDescriptors: ProviderOptionDescriptor[] = [];

if (reasoningOptions.length > 0) {
optionDescriptors.push({
id: "reasoningEffort",
label: "Reasoning",
type: "select",
options: reasoningOptions,
...(defaultReasoning ? { currentValue: defaultReasoning } : {}),
});
}
if (serviceTiers.length > 0) {
optionDescriptors.push({
id: "serviceTier",
Expand Down
9 changes: 4 additions & 5 deletions apps/server/src/provider/Layers/CodexSessionRuntime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,17 +277,15 @@ describe("buildTurnStartParams", () => {
mode: "default",
settings: {
model: "gpt-5.3-codex",
reasoning_effort: "medium",
developer_instructions: buildCodexDeveloperInstructions("default", {
model: "gpt-5.3-codex",
reasoningEffort: "medium",
}),
},
},
});
});

it("reports the same fallback model and effort in settings and instructions", () => {
it("leaves effort to Codex config when no override is given", () => {
const params = Effect.runSync(
buildTurnStartParams({
threadId: "provider-thread-1",
Expand All @@ -299,8 +297,9 @@ describe("buildTurnStartParams", () => {

const settings = params.collaborationMode?.settings;
NodeAssert.equal(settings?.model, DEFAULT_MODEL);
NodeAssert.equal(settings?.reasoning_effort, "medium");
NodeAssert.ok(settings?.developer_instructions?.includes(`as ${DEFAULT_MODEL} with medium`));
NodeAssert.equal(settings?.reasoning_effort, undefined);
NodeAssert.ok(settings?.developer_instructions?.includes(`as ${DEFAULT_MODEL}`));
NodeAssert.ok(!settings?.developer_instructions?.includes("reasoning effort"));
});

it.effect("routes approvals to the auto reviewer in auto mode", () =>
Expand Down
Loading
Loading