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
3 changes: 2 additions & 1 deletion apps/server/src/git/Layers/ClaudeTextGeneration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { Effect, Layer, Option, Schema, Stream } from "effect";
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";

import { ClaudeModelSelection } from "@t3tools/contracts";
import { resolveApiModelId } from "@t3tools/shared/model";
import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@t3tools/shared/git";

import { TextGenerationError } from "../Errors.ts";
Expand Down Expand Up @@ -103,7 +104,7 @@ const makeClaudeTextGeneration = Effect.gen(function* () {
"--json-schema",
jsonSchemaStr,
"--model",
modelSelection.model,
resolveApiModelId(modelSelection),
...(normalizedOptions?.effort ? ["--effort", normalizedOptions.effort] : []),
...(Object.keys(settings).length > 0 ? ["--settings", JSON.stringify(settings)] : []),
"--dangerously-skip-permissions",
Expand Down
6 changes: 3 additions & 3 deletions apps/server/src/provider/Layers/ClaudeAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ describe("ClaudeAdapterLive", () => {
);
});

it.effect("ignores unsupported max effort for Sonnet 4.6", () => {
it.effect("falls back to default effort when unsupported max is requested for Sonnet 4.6", () => {
const harness = makeHarness();
return Effect.gen(function* () {
const adapter = yield* ClaudeAdapter;
Expand All @@ -365,7 +365,7 @@ describe("ClaudeAdapterLive", () => {
});

const createInput = harness.getLastCreateQueryInput();
assert.equal(createInput?.options.effort, undefined);
assert.equal(createInput?.options.effort, "high");
}).pipe(
Effect.provideService(Random.Random, makeDeterministicRandomService()),
Effect.provide(harness.layer),
Expand Down Expand Up @@ -532,7 +532,7 @@ describe("ClaudeAdapterLive", () => {
});

const createInput = harness.getLastCreateQueryInput();
assert.equal(createInput?.options.effort, undefined);
assert.equal(createInput?.options.effort, "high");
const promptText = yield* Effect.promise(() => readFirstPromptText(createInput));
assert.equal(promptText, "Ultrathink:\nInvestigate the edge cases");
}).pipe(
Expand Down
31 changes: 18 additions & 13 deletions apps/server/src/provider/Layers/ClaudeAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,12 @@ import {
type UserInputQuestion,
ClaudeCodeEffort,
} from "@t3tools/contracts";
import { hasEffortLevel, applyClaudePromptEffortPrefix, trimOrNull } from "@t3tools/shared/model";
import {
applyClaudePromptEffortPrefix,
resolveApiModelId,
resolveEffort,
trimOrNull,
} from "@t3tools/shared/model";
import {
Cause,
DateTime,
Expand Down Expand Up @@ -506,16 +511,15 @@ const CLAUDE_SETTING_SOURCES = [
function buildPromptText(input: ProviderSendTurnInput): string {
const rawEffort =
input.modelSelection?.provider === "claudeAgent" ? input.modelSelection.options?.effort : null;
const requestedEffort = trimOrNull(rawEffort);
const claudeModel =
input.modelSelection?.provider === "claudeAgent" ? input.modelSelection.model : undefined;
const caps = getClaudeModelCapabilities(claudeModel);

// For prompt injection, we check if the raw effort is a prompt-injected level (e.g. "ultrathink").
// resolveEffort strips prompt-injected values (returning the default instead), so we check the raw value directly.
const trimmedEffort = trimOrNull(rawEffort);
const promptEffort =
requestedEffort === "ultrathink" && caps.reasoningEffortLevels.length > 0
? "ultrathink"
: requestedEffort && hasEffortLevel(caps, requestedEffort)
? requestedEffort
: null;
trimmedEffort && caps.promptInjectedEffortLevels.includes(trimmedEffort) ? trimmedEffort : null;
return applyClaudePromptEffortPrefix(input.input?.trim() ?? "", promptEffort);
}

Expand Down Expand Up @@ -2727,10 +2731,10 @@ function makeClaudeAdapter(options?: ClaudeAdapterLiveOptions) {
const claudeBinaryPath = claudeSettings.binaryPath;
const modelSelection =
input.modelSelection?.provider === "claudeAgent" ? input.modelSelection : undefined;
const requestedEffort = trimOrNull(modelSelection?.options?.effort ?? null);
const caps = getClaudeModelCapabilities(modelSelection?.model);
const effort =
requestedEffort && hasEffortLevel(caps, requestedEffort) ? requestedEffort : null;
const apiModelId = modelSelection ? resolveApiModelId(modelSelection) : undefined;
const effort = (resolveEffort(caps, modelSelection?.options?.effort) ??
null) as ClaudeCodeEffort | null;
const fastMode = modelSelection?.options?.fastMode === true && caps.supportsFastMode;
const thinking =
typeof modelSelection?.options?.thinking === "boolean" && caps.supportsThinkingToggle
Expand All @@ -2746,7 +2750,7 @@ function makeClaudeAdapter(options?: ClaudeAdapterLiveOptions) {

const queryOptions: ClaudeQueryOptions = {
...(input.cwd ? { cwd: input.cwd } : {}),
...(modelSelection?.model ? { model: modelSelection.model } : {}),
...(apiModelId ? { model: apiModelId } : {}),
pathToClaudeCodeExecutable: claudeBinaryPath,
settingSources: [...CLAUDE_SETTING_SOURCES],
...(effectiveEffort ? { effort: effectiveEffort } : {}),
Expand Down Expand Up @@ -2840,7 +2844,7 @@ function makeClaudeAdapter(options?: ClaudeAdapterLiveOptions) {
threadId,
payload: {
config: {
...(modelSelection?.model ? { model: modelSelection.model } : {}),
...(apiModelId ? { model: apiModelId } : {}),
...(input.cwd ? { cwd: input.cwd } : {}),
...(effectiveEffort ? { effort: effectiveEffort } : {}),
...(permissionMode ? { permissionMode } : {}),
Expand Down Expand Up @@ -2893,8 +2897,9 @@ function makeClaudeAdapter(options?: ClaudeAdapterLiveOptions) {
}

if (modelSelection?.model) {
const apiModelId = resolveApiModelId(modelSelection);
yield* Effect.tryPromise({
try: () => context.query.setModel(modelSelection.model),
try: () => context.query.setModel(apiModelId),
catch: (cause) => toRequestError(input.threadId, "turn/setModel", cause),
});
}
Expand Down
27 changes: 15 additions & 12 deletions apps/server/src/provider/Layers/ClaudeProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type {
} from "@t3tools/contracts";
import { Effect, Equal, Layer, Option, Result, Stream } from "effect";
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
import { getDefaultEffort, hasEffortLevel, trimOrNull } from "@t3tools/shared/model";
import { resolveContextWindow, resolveEffort } from "@t3tools/shared/model";

import {
buildServerProvider,
Expand Down Expand Up @@ -42,6 +42,10 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [
],
supportsFastMode: true,
supportsThinkingToggle: false,
contextWindowOptions: [
{ value: "200k", label: "200k" },
{ value: "1m", label: "1M", isDefault: true },
],
promptInjectedEffortLevels: ["ultrathink"],
} satisfies ModelCapabilities,
},
Expand All @@ -58,6 +62,10 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [
],
supportsFastMode: false,
supportsThinkingToggle: false,
contextWindowOptions: [
{ value: "200k", label: "200k" },
{ value: "1m", label: "1M", isDefault: true },
],
promptInjectedEffortLevels: ["ultrathink"],
} satisfies ModelCapabilities,
},
Expand All @@ -69,6 +77,7 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [
reasoningEffortLevels: [],
supportsFastMode: false,
supportsThinkingToggle: true,
contextWindowOptions: [],
promptInjectedEffortLevels: [],
} satisfies ModelCapabilities,
},
Expand All @@ -81,6 +90,7 @@ export function getClaudeModelCapabilities(model: string | null | undefined): Mo
reasoningEffortLevels: [],
supportsFastMode: false,
supportsThinkingToggle: false,
contextWindowOptions: [],
promptInjectedEffortLevels: [],
}
);
Expand All @@ -91,23 +101,16 @@ export function normalizeClaudeModelOptions(
modelOptions: ClaudeModelOptions | null | undefined,
): ClaudeModelOptions | undefined {
const caps = getClaudeModelCapabilities(model);
const defaultReasoningEffort = getDefaultEffort(caps);
const resolvedEffort = trimOrNull(modelOptions?.effort);
const isPromptInjected = caps.promptInjectedEffortLevels.includes(resolvedEffort ?? "");
const effort =
resolvedEffort &&
!isPromptInjected &&
hasEffortLevel(caps, resolvedEffort) &&
resolvedEffort !== defaultReasoningEffort
? resolvedEffort
: undefined;
const effort = resolveEffort(caps, modelOptions?.effort);
const thinking =
caps.supportsThinkingToggle && modelOptions?.thinking === false ? false : undefined;
const fastMode = caps.supportsFastMode && modelOptions?.fastMode === true ? true : undefined;
const contextWindow = resolveContextWindow(caps, modelOptions?.contextWindow);
const nextOptions: ClaudeModelOptions = {
...(thinking === false ? { thinking: false } : {}),
...(effort ? { effort } : {}),
...(effort ? { effort: effort as ClaudeModelOptions["effort"] } : {}),
...(fastMode ? { fastMode: true } : {}),
...(contextWindow ? { contextWindow } : {}),
};
return Object.keys(nextOptions).length > 0 ? nextOptions : undefined;
}
Expand Down
14 changes: 10 additions & 4 deletions apps/server/src/provider/Layers/CodexProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type {
} from "@t3tools/contracts";
import { Effect, Equal, FileSystem, Layer, Option, Path, Result, Stream } from "effect";
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
import { getDefaultEffort, trimOrNull } from "@t3tools/shared/model";
import { resolveEffort } from "@t3tools/shared/model";

import {
buildServerProvider,
Expand Down Expand Up @@ -48,6 +48,7 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [
],
supportsFastMode: true,
supportsThinkingToggle: false,
contextWindowOptions: [],
promptInjectedEffortLevels: [],
},
},
Expand All @@ -64,6 +65,7 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [
],
supportsFastMode: true,
supportsThinkingToggle: false,
contextWindowOptions: [],
promptInjectedEffortLevels: [],
},
},
Expand All @@ -80,6 +82,7 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [
],
supportsFastMode: true,
supportsThinkingToggle: false,
contextWindowOptions: [],
promptInjectedEffortLevels: [],
},
},
Expand All @@ -96,6 +99,7 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [
],
supportsFastMode: true,
supportsThinkingToggle: false,
contextWindowOptions: [],
promptInjectedEffortLevels: [],
},
},
Expand All @@ -112,6 +116,7 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [
],
supportsFastMode: true,
supportsThinkingToggle: false,
contextWindowOptions: [],
promptInjectedEffortLevels: [],
},
},
Expand All @@ -128,6 +133,7 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [
],
supportsFastMode: true,
supportsThinkingToggle: false,
contextWindowOptions: [],
promptInjectedEffortLevels: [],
},
},
Expand All @@ -140,6 +146,7 @@ export function getCodexModelCapabilities(model: string | null | undefined): Mod
reasoningEffortLevels: [],
supportsFastMode: false,
supportsThinkingToggle: false,
contextWindowOptions: [],
promptInjectedEffortLevels: [],
}
);
Expand All @@ -150,11 +157,10 @@ export function normalizeCodexModelOptions(
modelOptions: CodexModelOptions | null | undefined,
): CodexModelOptions | undefined {
const caps = getCodexModelCapabilities(model);
const defaultReasoningEffort = getDefaultEffort(caps);
const reasoningEffort = trimOrNull(modelOptions?.reasoningEffort) ?? defaultReasoningEffort;
const reasoningEffort = resolveEffort(caps, modelOptions?.reasoningEffort);
const fastModeEnabled = modelOptions?.fastMode === true;
const nextOptions: CodexModelOptions = {
...(reasoningEffort && reasoningEffort !== defaultReasoningEffort
...(reasoningEffort
? { reasoningEffort: reasoningEffort as CodexModelOptions["reasoningEffort"] }
: {}),
...(fastModeEnabled ? { fastMode: true } : {}),
Expand Down
3 changes: 1 addition & 2 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
type MessageId,
type ModelSelection,
type ProjectScript,
type ModelSlug,
type ProviderKind,
type ProjectEntry,
type ProjectId,
Expand Down Expand Up @@ -3105,7 +3104,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
]);

const onProviderModelSelect = useCallback(
(provider: ProviderKind, model: ModelSlug) => {
(provider: ProviderKind, model: string) => {
if (!activeThread) return;
if (lockedProvider !== null && provider !== lockedProvider) {
scheduleComposerFocus();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ async function mountMenu(props?: { modelSelection?: ModelSelection; prompt?: str
],
supportsFastMode: true,
supportsThinkingToggle: false,
contextWindowOptions: [],
promptInjectedEffortLevels: ["ultrathink"],
},
},
Expand All @@ -71,6 +72,7 @@ async function mountMenu(props?: { modelSelection?: ModelSelection; prompt?: str
reasoningEffortLevels: [],
supportsFastMode: false,
supportsThinkingToggle: true,
contextWindowOptions: [],
promptInjectedEffortLevels: [],
},
},
Expand All @@ -87,6 +89,7 @@ async function mountMenu(props?: { modelSelection?: ModelSelection; prompt?: str
],
supportsFastMode: false,
supportsThinkingToggle: false,
contextWindowOptions: [],
promptInjectedEffortLevels: ["ultrathink"],
},
},
Expand All @@ -103,6 +106,7 @@ async function mountMenu(props?: { modelSelection?: ModelSelection; prompt?: str
],
supportsFastMode: true,
supportsThinkingToggle: false,
contextWindowOptions: [],
promptInjectedEffortLevels: [],
},
},
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/components/chat/ComposerCommandMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type ProjectEntry, type ModelSlug, type ProviderKind } from "@t3tools/contracts";
import { type ProjectEntry, type ProviderKind } from "@t3tools/contracts";
import { memo } from "react";
import { type ComposerSlashCommand, type ComposerTriggerKind } from "../../composer-logic";
import { BotIcon } from "lucide-react";
Expand Down Expand Up @@ -27,7 +27,7 @@ export type ComposerCommandItem =
id: string;
type: "model";
provider: ProviderKind;
model: ModelSlug;
model: string;
label: string;
description: string;
};
Expand Down
9 changes: 7 additions & 2 deletions apps/web/src/components/chat/ProviderModelPicker.browser.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type ModelSlug, type ProviderKind, type ServerProvider } from "@t3tools/contracts";
import { type ProviderKind, type ServerProvider } from "@t3tools/contracts";
import { page } from "vitest/browser";
import { afterEach, describe, expect, it, vi } from "vitest";
import { render } from "vitest-browser-react";
Expand Down Expand Up @@ -33,6 +33,7 @@ const TEST_PROVIDERS: ReadonlyArray<ServerProvider> = [
reasoningEffortLevels: [effort("low"), effort("medium", true), effort("high")],
supportsFastMode: true,
supportsThinkingToggle: false,
contextWindowOptions: [],
promptInjectedEffortLevels: [],
},
},
Expand All @@ -44,6 +45,7 @@ const TEST_PROVIDERS: ReadonlyArray<ServerProvider> = [
reasoningEffortLevels: [effort("low"), effort("medium", true), effort("high")],
supportsFastMode: true,
supportsThinkingToggle: false,
contextWindowOptions: [],
promptInjectedEffortLevels: [],
},
},
Expand Down Expand Up @@ -71,6 +73,7 @@ const TEST_PROVIDERS: ReadonlyArray<ServerProvider> = [
],
supportsFastMode: false,
supportsThinkingToggle: true,
contextWindowOptions: [],
promptInjectedEffortLevels: [],
},
},
Expand All @@ -87,6 +90,7 @@ const TEST_PROVIDERS: ReadonlyArray<ServerProvider> = [
],
supportsFastMode: false,
supportsThinkingToggle: true,
contextWindowOptions: [],
promptInjectedEffortLevels: [],
},
},
Expand All @@ -98,6 +102,7 @@ const TEST_PROVIDERS: ReadonlyArray<ServerProvider> = [
reasoningEffortLevels: [effort("low"), effort("medium", true), effort("high")],
supportsFastMode: false,
supportsThinkingToggle: true,
contextWindowOptions: [],
promptInjectedEffortLevels: [],
},
},
Expand All @@ -107,7 +112,7 @@ const TEST_PROVIDERS: ReadonlyArray<ServerProvider> = [

async function mountPicker(props: {
provider: ProviderKind;
model: ModelSlug;
model: string;
lockedProvider: ProviderKind | null;
providers?: ReadonlyArray<ServerProvider>;
triggerVariant?: "ghost" | "outline";
Expand Down
Loading