From 16f5e8d6d013f47143d5075acf1b069a73d9ed49 Mon Sep 17 00:00:00 2001 From: Charles Vien Date: Sat, 25 Jul 2026 21:38:50 -0700 Subject: [PATCH 01/47] share reasoning dropdown and add ultra tiers --- .../agent/src/adapters/claude/claude-agent.ts | 35 +++-- .../claude/conversion/acp-to-sdk.test.ts | 37 +++++ .../adapters/claude/conversion/acp-to-sdk.ts | 20 +++ .../adapters/claude/session/models.test.ts | 35 ++++- .../src/adapters/claude/session/models.ts | 41 +++++- .../src/adapters/claude/session/options.ts | 5 +- packages/agent/src/adapters/claude/types.ts | 3 +- .../src/adapters/reasoning-effort.test.ts | 59 +++++++- .../agent/src/adapters/reasoning-effort.ts | 75 +++++++++- packages/core/src/sessions/sessionService.ts | 2 + .../core/src/task-detail/previewConfig.ts | 2 + packages/shared/src/domain-types.ts | 2 + packages/shared/src/index.ts | 4 + packages/shared/src/models.ts | 25 ++++ .../canvas/components/ChannelHomeComposer.tsx | 1 - packages/ui/src/features/loops/loopModels.ts | 11 +- .../components/PromptInput.stories.tsx | 1 - .../pi-sessions/PiSessionControls.tsx | 55 ++------ .../components/ReasoningLevelDropdown.tsx | 128 ++++++++++++++++++ .../ReasoningLevelSelector.test.tsx | 85 ++++++++---- .../components/ReasoningLevelSelector.tsx | 89 ++---------- .../sessions/components/SessionView.tsx | 3 - .../task-detail/components/TaskInput.tsx | 1 - 23 files changed, 547 insertions(+), 172 deletions(-) create mode 100644 packages/ui/src/features/sessions/components/ReasoningLevelDropdown.tsx diff --git a/packages/agent/src/adapters/claude/claude-agent.ts b/packages/agent/src/adapters/claude/claude-agent.ts index 0a9842dee7..20e279ebcc 100644 --- a/packages/agent/src/adapters/claude/claude-agent.ts +++ b/packages/agent/src/adapters/claude/claude-agent.ts @@ -86,7 +86,11 @@ import { estimateSkillsTokens, estimateSystemPrompt, } from "./context-breakdown"; -import { isSteerMeta, promptToClaude } from "./conversion/acp-to-sdk"; +import { + appendUltrathinkKeyword, + isSteerMeta, + promptToClaude, +} from "./conversion/acp-to-sdk"; import { handleResultMessage, handleStreamEvent, @@ -124,6 +128,7 @@ import { supports1MContext, supportsFastMode, supportsMcpInjection, + toSdkEffort, toSdkModelId, } from "./session/models"; import { @@ -477,6 +482,11 @@ export class ClaudeAcpAgent extends BaseAcpAgent { await this.refreshSlashCommandsForPrompt(commandMatch[1]); } + // Slash commands are left untouched so the keyword can't corrupt parsing. + if (this.session.effort === "ultrathink" && !commandMatch) { + appendUltrathinkKeyword(userMessage); + } + if (this.session.queryClosed) { throw RequestError.internalError(undefined, SESSION_ENDED_MESSAGE); } @@ -1756,10 +1766,11 @@ export class ClaudeAcpAgent extends BaseAcpAgent { } else if (params.configId === "effort") { const newEffort = resolvedValue as EffortLevel; this.session.effort = newEffort; - this.session.queryOptions.effort = newEffort; + // @ts-expect-error SDK Options.effort omits "ultracode" but the CLI accepts it + this.session.queryOptions.effort = toSdkEffort(newEffort); await this.session.query.applyFlagSettings({ - // @ts-expect-error SDK Settings.effortLevel omits "max" but runtime accepts it - effortLevel: newEffort, + // @ts-expect-error SDK Settings.effortLevel omits "max"/"ultracode" but runtime accepts them + effortLevel: toSdkEffort(newEffort), }); } else if (params.configId === "fast") { // SDK flag first: a rejected control request leaves state untouched. @@ -2272,10 +2283,11 @@ export class ClaudeAcpAgent extends BaseAcpAgent { const resolvedEffort = resolveEffortForModel(resolvedModelId, effort); if (resolvedEffort && resolvedEffort !== effort) { this.session.effort = resolvedEffort; - this.session.queryOptions.effort = resolvedEffort; + // @ts-expect-error SDK Options.effort omits "ultracode" but the CLI accepts it + this.session.queryOptions.effort = toSdkEffort(resolvedEffort); await this.session.query.applyFlagSettings({ - // @ts-expect-error SDK Settings.effortLevel omits "max" but runtime accepts it - effortLevel: resolvedEffort, + // @ts-expect-error SDK Settings.effortLevel omits "max"/"ultracode" but runtime accepts them + effortLevel: toSdkEffort(resolvedEffort), }); } @@ -2579,10 +2591,13 @@ export class ClaudeAcpAgent extends BaseAcpAgent { // when the prior level is invalid for the newly selected model. if (!this.session.effort || resolvedValue !== currentValue) { this.session.effort = resolvedValue as EffortLevel; - this.session.queryOptions.effort = resolvedValue as EffortLevel; + // @ts-expect-error SDK Options.effort omits "ultracode" but the CLI accepts it + this.session.queryOptions.effort = toSdkEffort( + resolvedValue as EffortLevel, + ); void this.session.query.applyFlagSettings({ - // @ts-expect-error SDK Settings.effortLevel omits "max" but runtime accepts it - effortLevel: resolvedValue, + // @ts-expect-error SDK Settings.effortLevel omits "max"/"ultracode" but runtime accepts them + effortLevel: toSdkEffort(resolvedValue as EffortLevel), }); } diff --git a/packages/agent/src/adapters/claude/conversion/acp-to-sdk.test.ts b/packages/agent/src/adapters/claude/conversion/acp-to-sdk.test.ts index 9585a4f480..1e9118f6c7 100644 --- a/packages/agent/src/adapters/claude/conversion/acp-to-sdk.test.ts +++ b/packages/agent/src/adapters/claude/conversion/acp-to-sdk.test.ts @@ -1,9 +1,11 @@ import type { PromptRequest } from "@agentclientprotocol/sdk"; import { describe, expect, it } from "vitest"; import { + appendUltrathinkKeyword, isSteerMeta, promptToClaude, readToolGuidanceForPath, + ULTRATHINK_KEYWORD, workspacePromptFromFileUri, } from "./acp-to-sdk"; @@ -254,3 +256,38 @@ describe("promptToClaude", () => { ]); }); }); + +describe("appendUltrathinkKeyword", () => { + function messageWithPrompt(text: string) { + return promptToClaude({ + sessionId: "session-1", + prompt: [{ type: "text", text }], + }); + } + + it("pushes the keyword as a trailing text block", () => { + const message = messageWithPrompt("Fix the flaky test"); + appendUltrathinkKeyword(message); + expect(message.message.content).toEqual([ + { type: "text", text: "Fix the flaky test" }, + { type: "text", text: ULTRATHINK_KEYWORD }, + ]); + }); + + it("does not duplicate a keyword the user already typed", () => { + const message = messageWithPrompt("ultrathink about this bug"); + appendUltrathinkKeyword(message); + expect(message.message.content).toEqual([ + { type: "text", text: "ultrathink about this bug" }, + ]); + }); + + it("appends to string content", () => { + const message = messageWithPrompt("placeholder"); + message.message.content = "Fix the flaky test"; + appendUltrathinkKeyword(message); + expect(message.message.content).toBe( + `Fix the flaky test\n\n${ULTRATHINK_KEYWORD}`, + ); + }); +}); diff --git a/packages/agent/src/adapters/claude/conversion/acp-to-sdk.ts b/packages/agent/src/adapters/claude/conversion/acp-to-sdk.ts index d4a8cbf421..502e95a761 100644 --- a/packages/agent/src/adapters/claude/conversion/acp-to-sdk.ts +++ b/packages/agent/src/adapters/claude/conversion/acp-to-sdk.ts @@ -221,3 +221,23 @@ export function promptToClaude(prompt: PromptRequest): SDKUserMessage { return message; } + +export const ULTRATHINK_KEYWORD = "ultrathink"; + +// Claude Code recognizes "ultrathink" as a prompt keyword, so a session pinned +// to the Ultrathink tier requests deep reasoning by carrying it every turn. +export function appendUltrathinkKeyword(message: SDKUserMessage): void { + const content = message.message.content; + if (typeof content === "string") { + if (!content.includes(ULTRATHINK_KEYWORD)) { + message.message.content = `${content}\n\n${ULTRATHINK_KEYWORD}`; + } + return; + } + const hasKeyword = content.some( + (block) => block.type === "text" && block.text.includes(ULTRATHINK_KEYWORD), + ); + if (!hasKeyword) { + content.push(sdkText(ULTRATHINK_KEYWORD)); + } +} diff --git a/packages/agent/src/adapters/claude/session/models.test.ts b/packages/agent/src/adapters/claude/session/models.test.ts index b9f42b2b94..1ea643c09c 100644 --- a/packages/agent/src/adapters/claude/session/models.test.ts +++ b/packages/agent/src/adapters/claude/session/models.test.ts @@ -1,3 +1,4 @@ +import { isDefaultSelectOption, selectOptionDocsUrl } from "@posthog/shared"; import { describe, expect, it } from "vitest"; import { DEFAULT_EFFORT, @@ -8,6 +9,7 @@ import { supportsEffort, supportsMcpInjection, supportsXhighEffort, + toSdkEffort, toSdkModelId, } from "./models"; @@ -161,11 +163,42 @@ describe("getEffortOptions", () => { it.each([ ["claude-sonnet-4-6", ["low", "medium", "high"]], - ["claude-opus-4-7", ["low", "medium", "high", "xhigh", "max"]], + [ + "claude-opus-4-7", + ["low", "medium", "high", "xhigh", "max", "ultracode", "ultrathink"], + ], ["@cf/zai-org/glm-5.2", ["high", "max"]], ])("returns the exact effort levels for %s", (modelId, expected) => { expect(getEffortOptions(modelId)?.map((o) => o.value)).toEqual(expected); }); + + it("marks the default level and links docs for the ultra tiers", () => { + const options = getEffortOptions("claude-opus-5") ?? []; + const byValue = new Map(options.map((o) => [o.value, o])); + expect(isDefaultSelectOption(byValue.get("high")?._meta)).toBe(true); + expect(isDefaultSelectOption(byValue.get("max")?._meta)).toBe(false); + expect(selectOptionDocsUrl(byValue.get("ultracode")?._meta)).toContain( + "workflows", + ); + expect(selectOptionDocsUrl(byValue.get("ultrathink")?._meta)).toContain( + "model-config", + ); + expect(selectOptionDocsUrl(byValue.get("low")?._meta)).toBeUndefined(); + }); +}); + +describe("toSdkEffort", () => { + it.each([ + ["low", "low"], + ["medium", "medium"], + ["high", "high"], + ["xhigh", "xhigh"], + ["max", "max"], + ["ultracode", "ultracode"], + ["ultrathink", "max"], + ] as const)("toSdkEffort(%s) === %s", (effort, expected) => { + expect(toSdkEffort(effort)).toBe(expected); + }); }); describe("resolveModelPreference", () => { diff --git a/packages/agent/src/adapters/claude/session/models.ts b/packages/agent/src/adapters/claude/session/models.ts index 47f2288b71..20ba4e81fc 100644 --- a/packages/agent/src/adapters/claude/session/models.ts +++ b/packages/agent/src/adapters/claude/session/models.ts @@ -1,3 +1,7 @@ +import { + DEFAULT_OPTION_META_KEY, + OPTION_DOCS_URL_META_KEY, +} from "@posthog/shared"; import type { EffortLevel } from "../types"; export const DEFAULT_MODEL = "opus"; @@ -43,6 +47,8 @@ const EXTENDED_EFFORT_LEVELS: readonly EffortLevel[] = [ ...STANDARD_EFFORT_LEVELS, "xhigh", "max", + "ultracode", + "ultrathink", ]; const MODEL_EFFORT_LEVELS: Readonly> = { "claude-opus-4-7": EXTENDED_EFFORT_LEVELS, @@ -90,6 +96,7 @@ export function fastModeStateEnabled(state: string | undefined): boolean { interface EffortOption { value: EffortLevel; name: string; + _meta?: Record; } const EFFORT_LABELS: Record = { @@ -98,15 +105,47 @@ const EFFORT_LABELS: Record = { high: "High", xhigh: "Extra High", max: "Max", + ultracode: "Ultracode", + ultrathink: "Ultrathink", }; +const EFFORT_DOCS_URLS: Partial> = { + ultracode: "https://code.claude.com/docs/en/workflows", + ultrathink: + "https://code.claude.com/docs/en/model-config#use-ultrathink-for-one-off-deep-reasoning", +}; + +function effortOptionMeta( + value: EffortLevel, +): Record | undefined { + const docsUrl = EFFORT_DOCS_URLS[value]; + const meta: Record = { + ...(value === DEFAULT_EFFORT ? { [DEFAULT_OPTION_META_KEY]: true } : {}), + ...(docsUrl ? { [OPTION_DOCS_URL_META_KEY]: docsUrl } : {}), + }; + return Object.keys(meta).length > 0 ? meta : undefined; +} + export function getEffortOptions(modelId: string): EffortOption[] | null { const levels = MODEL_EFFORT_LEVELS[modelId]; return ( - levels?.map((value) => ({ value, name: EFFORT_LABELS[value] })) ?? null + levels?.map((value) => ({ + value, + name: EFFORT_LABELS[value], + _meta: effortOptionMeta(value), + })) ?? null ); } +// The SDK's effort union lags the CLI: "ultracode" is accepted at runtime +// (xhigh plus workflow orchestration) while "ultrathink" is only a prompt +// keyword, so Ultrathink sessions run at max budget and inject the keyword. +export function toSdkEffort( + effort: EffortLevel, +): Exclude { + return effort === "ultrathink" ? "max" : effort; +} + // Model alias resolution — lets callers use human-friendly aliases like // "opus" or "sonnet" instead of full model IDs like "claude-opus-4-8". diff --git a/packages/agent/src/adapters/claude/session/options.ts b/packages/agent/src/adapters/claude/session/options.ts index 7c1def1eec..d61e299070 100644 --- a/packages/agent/src/adapters/claude/session/options.ts +++ b/packages/agent/src/adapters/claude/session/options.ts @@ -30,7 +30,7 @@ import { type CodeExecutionMode, toSdkPermissionMode } from "../tools"; import type { EffortLevel } from "../types"; import { buildAppendedInstructions } from "./instructions"; import { loadUserClaudeJsonMcpServers } from "./mcp-config"; -import { DEFAULT_MODEL, FALLBACK_MODEL } from "./models"; +import { DEFAULT_MODEL, FALLBACK_MODEL, toSdkEffort } from "./models"; import { createRtkRewriteHook, resolveRtkPrefix } from "./rtk"; import type { SettingsManager } from "./settings"; @@ -526,7 +526,8 @@ export function buildSessionOptions(params: BuildOptionsParams): Options { } if (params.effort) { - options.effort = params.effort; + // @ts-expect-error SDK Options.effort omits "ultracode" but the CLI accepts it + options.effort = toSdkEffort(params.effort); } clearStatsigCache(); diff --git a/packages/agent/src/adapters/claude/types.ts b/packages/agent/src/adapters/claude/types.ts index 82317f536a..e530a7f4fe 100644 --- a/packages/agent/src/adapters/claude/types.ts +++ b/packages/agent/src/adapters/claude/types.ts @@ -10,6 +10,7 @@ import type { Query, SDKUserMessage, } from "@anthropic-ai/claude-agent-sdk"; +import type { EffortLevel } from "@posthog/shared/domain-types"; import type { PostHogProductId } from "../../posthog-products"; import type { AgentMode } from "../../types"; import type { Pushable } from "../../utils/streams"; @@ -20,7 +21,7 @@ import type { McpToolApprovals } from "./mcp/tool-metadata"; import type { SettingsManager } from "./session/settings"; import type { CodeExecutionMode } from "./tools"; -export type EffortLevel = "low" | "medium" | "high" | "xhigh" | "max"; +export type { EffortLevel }; export type AccumulatedUsage = { inputTokens: number; diff --git a/packages/agent/src/adapters/reasoning-effort.test.ts b/packages/agent/src/adapters/reasoning-effort.test.ts index cc559f486b..d9cd7e3b19 100644 --- a/packages/agent/src/adapters/reasoning-effort.test.ts +++ b/packages/agent/src/adapters/reasoning-effort.test.ts @@ -1,5 +1,9 @@ import { describe, expect, it } from "vitest"; -import { isSupportedReasoningEffort } from "./reasoning-effort"; +import { + isCloudReasoningEffort, + isSupportedReasoningEffort, + toCloudReasoningEffort, +} from "./reasoning-effort"; describe("isSupportedReasoningEffort", () => { it("accepts xhigh for the codex gpt-5.5 family", () => { @@ -51,4 +55,57 @@ describe("isSupportedReasoningEffort", () => { isSupportedReasoningEffort("claude", "@cf/zai-org/glm-5.2", effort), ).toBe(expected); }); + + it.each([ + ["claude-opus-4-8", "ultracode", true], + ["claude-opus-4-8", "ultrathink", true], + ["claude-sonnet-4-6", "ultracode", false], + ["claude-sonnet-4-6", "ultrathink", false], + ["@cf/zai-org/glm-5.2", "ultracode", false], + ])( + "gates the ultra tiers on Claude model %s (%s)", + (modelId, effort, expected) => { + expect(isSupportedReasoningEffort("claude", modelId, effort)).toBe( + expected, + ); + }, + ); + + it("never offers the ultra tiers on codex models", () => { + expect( + isSupportedReasoningEffort("codex", "gpt-5.6-sol", "ultracode"), + ).toBe(false); + expect( + isSupportedReasoningEffort("codex", "gpt-5.6-sol", "ultrathink"), + ).toBe(false); + }); +}); + +describe("toCloudReasoningEffort", () => { + it.each([ + ["low", "low"], + ["medium", "medium"], + ["high", "high"], + ["xhigh", "xhigh"], + ["max", "max"], + ["ultracode", "xhigh"], + ["ultrathink", "max"], + ] as const)("clamps %s to %s", (effort, expected) => { + expect(toCloudReasoningEffort(effort)).toBe(expected); + }); +}); + +describe("isCloudReasoningEffort", () => { + it.each([ + ["low", true], + ["medium", true], + ["high", true], + ["xhigh", true], + ["max", true], + ["ultracode", false], + ["ultrathink", false], + ["ultra", false], + ])("isCloudReasoningEffort(%s) === %s", (effort, expected) => { + expect(isCloudReasoningEffort(effort)).toBe(expected); + }); }); diff --git a/packages/agent/src/adapters/reasoning-effort.ts b/packages/agent/src/adapters/reasoning-effort.ts index 2cc1da50f8..cc05355a9d 100644 --- a/packages/agent/src/adapters/reasoning-effort.ts +++ b/packages/agent/src/adapters/reasoning-effort.ts @@ -1,6 +1,69 @@ -export { - getReasoningEffortOptions, - isSupportedReasoningEffort, - type ReasoningEffortOption, - type SupportedReasoningEffort, -} from "@posthog/shared"; +import type { Adapter } from "@posthog/shared"; +import { getEffortOptions as getClaudeEffortOptions } from "./claude/session/models"; +import { getReasoningEffortOptions as getCodexReasoningEffortOptions } from "./codex-app-server/models"; + +export type SupportedReasoningEffort = + | "low" + | "medium" + | "high" + | "xhigh" + | "max" + | "ultracode" + | "ultrathink"; + +// The cloud task API only accepts the base tiers; ultracode runs the model at +// xhigh and ultrathink at max, so cloud runs clamp to those equivalents. +export type CloudReasoningEffort = Exclude< + SupportedReasoningEffort, + "ultracode" | "ultrathink" +>; + +export interface ReasoningEffortOption { + value: SupportedReasoningEffort; + name: string; + _meta?: Record; +} + +export function getReasoningEffortOptions( + adapter: Adapter, + modelId: string, +): ReasoningEffortOption[] | null { + const options = + adapter === "codex" + ? getCodexReasoningEffortOptions(modelId) + : getClaudeEffortOptions(modelId); + + return options as ReasoningEffortOption[] | null; +} + +export function isSupportedReasoningEffort( + adapter: Adapter, + modelId: string, + value: string, +): value is SupportedReasoningEffort { + return ( + getReasoningEffortOptions(adapter, modelId)?.some( + (option) => option.value === value, + ) ?? false + ); +} + +export function isCloudReasoningEffort( + value: string, +): value is CloudReasoningEffort { + return ( + value === "low" || + value === "medium" || + value === "high" || + value === "xhigh" || + value === "max" + ); +} + +export function toCloudReasoningEffort( + value: SupportedReasoningEffort, +): CloudReasoningEffort { + if (value === "ultracode") return "xhigh"; + if (value === "ultrathink") return "max"; + return value; +} diff --git a/packages/core/src/sessions/sessionService.ts b/packages/core/src/sessions/sessionService.ts index cd001a33e5..54b860b96d 100644 --- a/packages/core/src/sessions/sessionService.ts +++ b/packages/core/src/sessions/sessionService.ts @@ -5456,6 +5456,8 @@ export class SessionService { high: "High", xhigh: "Extra High", max: "Max", + ultracode: "Ultracode", + ultrathink: "Ultrathink", }; const selectedValue = existingValues.find( (value) => value.value === preferredValue, diff --git a/packages/core/src/task-detail/previewConfig.ts b/packages/core/src/task-detail/previewConfig.ts index 88c81af1bf..7b97f54c7c 100644 --- a/packages/core/src/task-detail/previewConfig.ts +++ b/packages/core/src/task-detail/previewConfig.ts @@ -19,6 +19,8 @@ const EFFORT_RANK: Record = { high: 2, xhigh: 3, max: 4, + ultracode: 5, + ultrathink: 6, }; export function clampEffortToAvailable( diff --git a/packages/shared/src/domain-types.ts b/packages/shared/src/domain-types.ts index 8f1e8dc56d..07c712a026 100644 --- a/packages/shared/src/domain-types.ts +++ b/packages/shared/src/domain-types.ts @@ -26,6 +26,8 @@ export const effortLevelSchema = z.enum([ "high", "xhigh", "max", + "ultracode", + "ultrathink", ]); export type EffortLevel = z.infer; diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index f6d15bc1ae..720d70e4ed 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -223,10 +223,14 @@ export { splitMentionSegments, } from "./mentions"; export { + DEFAULT_OPTION_META_KEY, defaultEligibleModel, + isDefaultSelectOption, isRestrictedModelOption, + OPTION_DOCS_URL_META_KEY, RESTRICTED_MODEL_META_KEY, restrictedModelMeta, + selectOptionDocsUrl, } from "./models"; export { getOauthClientIdFromRegion, diff --git a/packages/shared/src/models.ts b/packages/shared/src/models.ts index 65cb3f6ad9..e45d26056f 100644 --- a/packages/shared/src/models.ts +++ b/packages/shared/src/models.ts @@ -26,3 +26,28 @@ export function isRestrictedModelOption( ): boolean { return meta?.[RESTRICTED_MODEL_META_KEY] === true; } + +/** + * ACP SessionConfigSelectOption `_meta` key marking the adapter's default + * value for a select option, so pickers can render a "Default" badge. + */ +export const DEFAULT_OPTION_META_KEY = "posthog.code/defaultOption"; + +/** + * ACP SessionConfigSelectOption `_meta` key carrying a documentation URL for + * an option value, so pickers can render a help affordance linking to it. + */ +export const OPTION_DOCS_URL_META_KEY = "posthog.code/docsUrl"; + +export function isDefaultSelectOption( + meta: Record | null | undefined, +): boolean { + return meta?.[DEFAULT_OPTION_META_KEY] === true; +} + +export function selectOptionDocsUrl( + meta: Record | null | undefined, +): string | undefined { + const url = meta?.[OPTION_DOCS_URL_META_KEY]; + return typeof url === "string" ? url : undefined; +} diff --git a/packages/ui/src/features/canvas/components/ChannelHomeComposer.tsx b/packages/ui/src/features/canvas/components/ChannelHomeComposer.tsx index d2bdcd10d5..dffe120f4f 100644 --- a/packages/ui/src/features/canvas/components/ChannelHomeComposer.tsx +++ b/packages/ui/src/features/canvas/components/ChannelHomeComposer.tsx @@ -426,7 +426,6 @@ export const ChannelHomeComposer = forwardRef< !isLoading && ( diff --git a/packages/ui/src/features/loops/loopModels.ts b/packages/ui/src/features/loops/loopModels.ts index 080c98a0e5..7a5144f2b5 100644 --- a/packages/ui/src/features/loops/loopModels.ts +++ b/packages/ui/src/features/loops/loopModels.ts @@ -1,5 +1,8 @@ import type { SessionConfigOption } from "@agentclientprotocol/sdk"; -import { getReasoningEffortOptions } from "@posthog/agent/adapters/reasoning-effort"; +import { + getReasoningEffortOptions, + isCloudReasoningEffort, +} from "@posthog/agent/adapters/reasoning-effort"; import type { LoopSchemas } from "@posthog/api-client/loops"; import { flattenSelectOptions, isRestrictedModelOption } from "@posthog/shared"; @@ -120,7 +123,11 @@ export function loopReasoningEffortOptions( ): { value: LoopSchemas.LoopReasoningEffortEnum; label: string }[] { const effectiveModel = model || LOOP_DEFAULT_MODELS[adapter].id; const options = getReasoningEffortOptions(adapter, effectiveModel) ?? []; - return options.map((option) => ({ value: option.value, label: option.name })); + return options.flatMap((option) => + isCloudReasoningEffort(option.value) + ? [{ value: option.value, label: option.name }] + : [], + ); } /** The effort unchanged when the effective model supports it, else null diff --git a/packages/ui/src/features/message-editor/components/PromptInput.stories.tsx b/packages/ui/src/features/message-editor/components/PromptInput.stories.tsx index 9ff3c236b2..c408dde309 100644 --- a/packages/ui/src/features/message-editor/components/PromptInput.stories.tsx +++ b/packages/ui/src/features/message-editor/components/PromptInput.stories.tsx @@ -119,7 +119,6 @@ function PromptInputWithSelectors({ showSelectors ? ( ) : ( diff --git a/packages/ui/src/features/pi-sessions/PiSessionControls.tsx b/packages/ui/src/features/pi-sessions/PiSessionControls.tsx index a295fa5ea4..f8d3cbb222 100644 --- a/packages/ui/src/features/pi-sessions/PiSessionControls.tsx +++ b/packages/ui/src/features/pi-sessions/PiSessionControls.tsx @@ -1,4 +1,4 @@ -import { Brain, CaretDown, Lightning, Stack } from "@phosphor-icons/react"; +import { CaretDown, Lightning, Stack } from "@phosphor-icons/react"; import type { PiModelSelection, PiThinkingLevel, @@ -13,6 +13,7 @@ import { DropdownMenuTrigger, MenuLabel, } from "@posthog/quill"; +import { ReasoningLevelDropdown } from "@posthog/ui/features/sessions/components/ReasoningLevelDropdown"; import type { MessagingMode } from "@posthog/ui/features/sessions/messagingModeStore"; import { Fragment } from "react"; @@ -112,7 +113,7 @@ const thinkingLevelLabels: Record = { low: "Low", medium: "Medium", high: "High", - xhigh: "Extra high", + xhigh: "Extra High", max: "Max", }; @@ -129,48 +130,16 @@ export function PiThinkingLevelSelector({ disabled, onChange, }: PiThinkingLevelSelectorProps) { - const activeLabel = thinkingLevelLabels[level] ?? level; - return ( - - - - {activeLabel} - - - } - /> - - Thinking - onChange(value as PiThinkingLevel)} - > - {levels.map((value) => ( - - {thinkingLevelLabels[value] ?? value} - - ))} - - - + ({ + value, + label: thinkingLevelLabels[value] ?? value, + }))} + onChange={(value) => onChange(value as PiThinkingLevel)} + disabled={disabled} + /> ); } diff --git a/packages/ui/src/features/sessions/components/ReasoningLevelDropdown.tsx b/packages/ui/src/features/sessions/components/ReasoningLevelDropdown.tsx new file mode 100644 index 0000000000..7e68d75008 --- /dev/null +++ b/packages/ui/src/features/sessions/components/ReasoningLevelDropdown.tsx @@ -0,0 +1,128 @@ +import { Brain, CaretDown, Question } from "@phosphor-icons/react"; +import { + Button, + DropdownMenu, + DropdownMenuContent, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuTrigger, + MenuLabel, +} from "@posthog/quill"; +import { Badge } from "@posthog/ui/primitives/Badge"; +import { openUrlInBrowser } from "@posthog/ui/utils/browser"; +import { useRef, useState } from "react"; + +export interface ReasoningLevelOption { + value: string; + label: string; + isDefault?: boolean; + docsUrl?: string; +} + +interface ReasoningLevelDropdownProps { + value: string; + options: ReasoningLevelOption[]; + onChange?: (value: string) => void; + disabled?: boolean; +} + +/** + * The one reasoning dropdown. Every surface that offers reasoning levels + * (channel composer, new task, task page, session views) renders this. + */ +export function ReasoningLevelDropdown({ + value, + options, + onChange, + disabled, +}: ReasoningLevelDropdownProps) { + const [open, setOpen] = useState(false); + const pendingValueRef = useRef(null); + + if (options.length === 0) return null; + + const activeLabel = + options.find((option) => option.value === value)?.label ?? value; + + return ( + { + if (!isOpen && pendingValueRef.current !== null) { + onChange?.(pendingValueRef.current); + pendingValueRef.current = null; + } + }} + > + + + {activeLabel} + + + } + /> + + Reasoning + { + pendingValueRef.current = next; + setOpen(false); + }} + > + {options.map((option) => ( + + {option.label} + {(option.isDefault || option.docsUrl) && ( + + {option.isDefault && Default} + {option.docsUrl && ( + + )} + + )} + + ))} + + + + ); +} + +function DocsLink({ label, docsUrl }: { label: string; docsUrl: string }) { + return ( + + ); +} diff --git a/packages/ui/src/features/sessions/components/ReasoningLevelSelector.test.tsx b/packages/ui/src/features/sessions/components/ReasoningLevelSelector.test.tsx index 9631603355..a38bbd24c0 100644 --- a/packages/ui/src/features/sessions/components/ReasoningLevelSelector.test.tsx +++ b/packages/ui/src/features/sessions/components/ReasoningLevelSelector.test.tsx @@ -1,40 +1,56 @@ import type { SessionConfigOption } from "@agentclientprotocol/sdk"; +import { + DEFAULT_OPTION_META_KEY, + OPTION_DOCS_URL_META_KEY, +} from "@posthog/shared"; import { Theme } from "@radix-ui/themes"; import { render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { describe, expect, it, vi } from "vitest"; import { ReasoningLevelSelector } from "./ReasoningLevelSelector"; -function codexThoughtOption( +const openUrlInBrowser = vi.hoisted(() => vi.fn()); +vi.mock("@posthog/ui/utils/browser", () => ({ openUrlInBrowser })); + +const ultrathinkDocsUrl = + "https://code.claude.com/docs/en/model-config#use-ultrathink-for-one-off-deep-reasoning"; + +function thoughtOption( overrides?: Partial, ): SessionConfigOption { return { type: "select", id: "effort", - name: "Reasoning effort", + name: "Effort", category: "thought_level", currentValue: "high", options: [ - { name: "low", value: "low" }, - { name: "high", value: "high" }, - { name: "max", value: "max" }, + { name: "Low", value: "low" }, + { + name: "High", + value: "high", + _meta: { [DEFAULT_OPTION_META_KEY]: true }, + }, + { name: "Max", value: "max" }, + { + name: "Ultrathink", + value: "ultrathink", + _meta: { [OPTION_DOCS_URL_META_KEY]: ultrathinkDocsUrl }, + }, ], ...overrides, } as unknown as SessionConfigOption; } describe("ReasoningLevelSelector", () => { - it("renders the active level as the trigger label for a codex thought_level option", () => { + it("renders the active level as the trigger label", () => { render( - + , ); expect( - screen.getByRole("button", { name: "Reasoning: high" }), + screen.getByRole("button", { name: "Reasoning: High" }), ).toBeInTheDocument(); }); @@ -44,44 +60,65 @@ describe("ReasoningLevelSelector", () => { render( , ); - await user.click(screen.getByRole("button", { name: "Reasoning: high" })); - const lowItem = await screen.findByRole("menuitemradio", { name: "low" }); + await user.click(screen.getByRole("button", { name: "Reasoning: High" })); + const lowItem = await screen.findByRole("menuitemradio", { name: "Low" }); await user.click(lowItem); await waitFor(() => expect(onChange).toHaveBeenCalledWith("low")); expect(onChange).toHaveBeenCalledTimes(1); }); - it("uses the 'Effort' label for the claude adapter", () => { + it("marks the adapter default level with a Default badge", async () => { + const user = userEvent.setup(); + render( + + + , + ); + + await user.click(screen.getByRole("button", { name: "Reasoning: High" })); + const highItem = await screen.findByRole("menuitemradio", { + name: /High/, + }); + expect(highItem).toHaveTextContent("Default"); + }); + + it("opens the docs link without selecting the level", async () => { + const onChange = vi.fn(); + const user = userEvent.setup(); render( , ); - expect( - screen.getByRole("button", { name: "Effort: medium" }), - ).toBeInTheDocument(); + + await user.click(screen.getByRole("button", { name: "Reasoning: High" })); + const docsButton = await screen.findByRole("button", { + name: "Learn more about Ultrathink", + }); + await user.click(docsButton); + + expect(openUrlInBrowser).toHaveBeenCalledWith(ultrathinkDocsUrl); + expect(onChange).not.toHaveBeenCalled(); }); it.each([ ["undefined option", undefined], - ["non-select type", codexThoughtOption({ type: "boolean" })], - ["empty options", codexThoughtOption({ options: [] })], + ["non-select type", thoughtOption({ type: "boolean" })], + ["empty options", thoughtOption({ options: [] })], ])("renders no trigger for %s", (_label, option) => { render( , ); expect(screen.queryByRole("button")).not.toBeInTheDocument(); diff --git a/packages/ui/src/features/sessions/components/ReasoningLevelSelector.tsx b/packages/ui/src/features/sessions/components/ReasoningLevelSelector.tsx index 02f6140b25..189f4829d3 100644 --- a/packages/ui/src/features/sessions/components/ReasoningLevelSelector.tsx +++ b/packages/ui/src/features/sessions/components/ReasoningLevelSelector.tsx @@ -1,22 +1,11 @@ import type { SessionConfigOption } from "@agentclientprotocol/sdk"; -import { Brain, CaretDown } from "@phosphor-icons/react"; -import { - Button, - DropdownMenu, - DropdownMenuContent, - DropdownMenuRadioGroup, - DropdownMenuRadioItem, - DropdownMenuTrigger, - MenuLabel, -} from "@posthog/quill"; -import type { Adapter } from "@posthog/shared"; -import { useRef, useState } from "react"; +import { isDefaultSelectOption, selectOptionDocsUrl } from "@posthog/shared"; import { flattenSelectOptions } from "../sessionStore"; import { useRetainedConfigOption } from "../useRetainedConfigOption"; +import { ReasoningLevelDropdown } from "./ReasoningLevelDropdown"; interface ReasoningLevelSelectorProps { thoughtOption?: SessionConfigOption; - adapter?: Adapter; onChange?: (value: string) => void; disabled?: boolean; isLoading?: boolean; @@ -24,13 +13,10 @@ interface ReasoningLevelSelectorProps { export function ReasoningLevelSelector({ thoughtOption, - adapter, onChange, disabled, isLoading, }: ReasoningLevelSelectorProps) { - const [open, setOpen] = useState(false); - const pendingValueRef = useRef(null); const displayOption = useRetainedConfigOption(thoughtOption); // Genuinely no reasoning levels for this harness/model: hide. While the @@ -42,66 +28,19 @@ export function ReasoningLevelSelector({ } const isReloading = !thoughtOption; - const isDisabled = disabled || isReloading; - - const options = flattenSelectOptions(displayOption.options); - if (options.length === 0) return null; - const activeLevel = displayOption.currentValue; - const activeLabel = - options.find((opt) => opt.value === activeLevel)?.name ?? activeLevel; - const prefix = adapter === "codex" ? "Reasoning" : "Effort"; + const options = flattenSelectOptions(displayOption.options).map((option) => ({ + value: option.value, + label: option.name, + isDefault: isDefaultSelectOption(option._meta), + docsUrl: selectOptionDocsUrl(option._meta), + })); return ( - { - if (!isOpen && pendingValueRef.current !== null) { - onChange?.(pendingValueRef.current); - pendingValueRef.current = null; - } - }} - > - - - {activeLabel} - - - } - /> - - {adapter === "codex" ? "Reasoning" : "Effort"} - { - pendingValueRef.current = value; - setOpen(false); - }} - > - {options.map((level) => ( - - {level.name} - - ))} - - - + ); } diff --git a/packages/ui/src/features/sessions/components/SessionView.tsx b/packages/ui/src/features/sessions/components/SessionView.tsx index 2fe3e49007..58187fce54 100644 --- a/packages/ui/src/features/sessions/components/SessionView.tsx +++ b/packages/ui/src/features/sessions/components/SessionView.tsx @@ -48,7 +48,6 @@ import { useCancelQueuedMessageEdit } from "@posthog/ui/features/sessions/hooks/ import { useSessionEventsResidency } from "@posthog/ui/features/sessions/hooks/useSessionEventsResidency"; import { useToggleMessagingMode } from "@posthog/ui/features/sessions/hooks/useToggleMessagingMode"; import { - useAdapterForTask, useModeConfigOptionForTask, usePendingPermissionsForTask, useSessionSelector, @@ -187,7 +186,6 @@ export function SessionView({ const pendingPermissions = usePendingPermissionsForTask(taskId); const modeOption = useModeConfigOptionForTask(taskId); const thoughtOption = useThoughtLevelConfigOptionForTask(taskId); - const adapter = useAdapterForTask(taskId); const toggleMessagingMode = useToggleMessagingMode(taskId); const { allowBypassPermissions } = useSettingsStore(); const useNewChatThread = useSettingsStore((s) => s.useNewChatThread); @@ -750,7 +748,6 @@ export function SessionView({ thoughtOption ? ( diff --git a/packages/ui/src/features/task-detail/components/TaskInput.tsx b/packages/ui/src/features/task-detail/components/TaskInput.tsx index 6dd6d4b05f..dad65c3ddb 100644 --- a/packages/ui/src/features/task-detail/components/TaskInput.tsx +++ b/packages/ui/src/features/task-detail/components/TaskInput.tsx @@ -1434,7 +1434,6 @@ export function TaskInput({ ) : ( Date: Sat, 25 Jul 2026 21:55:58 -0700 Subject: [PATCH 02/47] add context window and fast mode to menu --- .../agent/src/adapters/claude/claude-agent.ts | 61 ++++++++++++++- .../adapters/claude/session/models.test.ts | 15 ++++ .../src/adapters/claude/session/models.ts | 19 +++++ .../components/ReasoningLevelDropdown.tsx | 78 ++++++++++++++----- .../ReasoningLevelSelector.test.tsx | 57 ++++++++++++++ .../components/ReasoningLevelSelector.tsx | 45 +++++++++-- .../sessions/components/SessionView.tsx | 14 ++++ 7 files changed, 258 insertions(+), 31 deletions(-) diff --git a/packages/agent/src/adapters/claude/claude-agent.ts b/packages/agent/src/adapters/claude/claude-agent.ts index 20e279ebcc..cefce767ab 100644 --- a/packages/agent/src/adapters/claude/claude-agent.ts +++ b/packages/agent/src/adapters/claude/claude-agent.ts @@ -119,9 +119,12 @@ import { resolveInitialModelId, } from "./session/model-config"; import { + CONTEXT_WINDOW_1M_BETA, + CONTEXT_WINDOW_200K_TOKENS, DEFAULT_EFFORT, DEFAULT_MODEL, fastModeStateEnabled, + getContextWindowOptions, getEffortOptions, resolveEffortForModel, resolveModelPreference, @@ -1762,6 +1765,7 @@ export class ClaudeAcpAgent extends BaseAcpAgent { this.session.lastContextWindowSize = this.getContextWindowForModel(resolvedValue); this.rebuildEffortConfigOption(resolvedValue); + this.rebuildContextWindowConfigOption(resolvedValue); this.rebuildFastModeConfigOption(resolvedValue); } else if (params.configId === "effort") { const newEffort = resolvedValue as EffortLevel; @@ -1772,6 +1776,15 @@ export class ClaudeAcpAgent extends BaseAcpAgent { // @ts-expect-error SDK Settings.effortLevel omits "max"/"ultracode" but runtime accepts them effortLevel: toSdkEffort(newEffort), }); + } else if (params.configId === "context_window") { + const enable1M = resolvedValue === "1m"; + // queryOptions is read per prompt, so this applies from the next turn. + this.session.queryOptions.betas = enable1M + ? [CONTEXT_WINDOW_1M_BETA] + : undefined; + this.session.lastContextWindowSize = enable1M + ? this.getContextWindowForModel(this.session.modelId ?? "") + : CONTEXT_WINDOW_200K_TOKENS; } else if (params.configId === "fast") { // SDK flag first: a rejected control request leaves state untouched. const enabled = resolvedValue === "on"; @@ -2292,7 +2305,7 @@ export class ClaudeAcpAgent extends BaseAcpAgent { } if (supports1MContext(resolvedModelId)) { - options.betas = ["context-1m-2025-08-07"]; + options.betas = [CONTEXT_WINDOW_1M_BETA]; } const availableModes = getAvailableModes(); @@ -2472,6 +2485,13 @@ export class ClaudeAcpAgent extends BaseAcpAgent { }); } + const contextOption = this.contextWindowConfigOption( + modelOptions.currentModelId, + ); + if (contextOption) { + configOptions.push(contextOption); + } + if (supportsFastMode(modelOptions.currentModelId)) { configOptions.push(this.fastModeConfigOption(fastModeEnabled ?? false)); } @@ -2479,16 +2499,53 @@ export class ClaudeAcpAgent extends BaseAcpAgent { return configOptions; } + private contextWindowConfigOption( + modelId: string, + ): SessionConfigOption | null { + const contextOptions = getContextWindowOptions(modelId); + if (!contextOptions) return null; + const is1M = this.session.queryOptions.betas?.includes( + CONTEXT_WINDOW_1M_BETA, + ); + return { + id: "context_window", + name: "Context Window", + type: "select", + currentValue: is1M ? "1m" : "200k", + options: contextOptions, + category: "_context_window" as SessionConfigOptionCategory, + description: "Choose the context window size for this session", + }; + } + + private rebuildContextWindowConfigOption(modelId: string): void { + const withoutContext = this.session.configOptions.filter( + (o) => o.id !== "context_window", + ); + if (!supports1MContext(modelId)) { + this.session.queryOptions.betas = undefined; + this.session.configOptions = withoutContext; + return; + } + // Switching onto a 1M-capable model restores the default 1M window. + this.session.queryOptions.betas = [CONTEXT_WINDOW_1M_BETA]; + const contextOption = this.contextWindowConfigOption(modelId); + this.session.configOptions = contextOption + ? [...withoutContext, contextOption] + : withoutContext; + } + private fastModeConfigOption(enabled: boolean): SessionConfigOption { return { id: "fast", - name: "Fast mode", + name: "Fast Mode", type: "select", currentValue: enabled ? "on" : "off", options: [ { value: "on", name: "On" }, { value: "off", name: "Off" }, ], + category: "_fast_mode" as SessionConfigOptionCategory, description: "Faster responses on supported models", }; } diff --git a/packages/agent/src/adapters/claude/session/models.test.ts b/packages/agent/src/adapters/claude/session/models.test.ts index 1ea643c09c..d31e04fe3e 100644 --- a/packages/agent/src/adapters/claude/session/models.test.ts +++ b/packages/agent/src/adapters/claude/session/models.test.ts @@ -2,6 +2,7 @@ import { isDefaultSelectOption, selectOptionDocsUrl } from "@posthog/shared"; import { describe, expect, it } from "vitest"; import { DEFAULT_EFFORT, + getContextWindowOptions, getEffortOptions, resolveEffortForModel, resolveModelPreference, @@ -187,6 +188,20 @@ describe("getEffortOptions", () => { }); }); +describe("getContextWindowOptions", () => { + it("returns null for models without 1M support", () => { + expect(getContextWindowOptions("claude-haiku-4-5")).toBeNull(); + expect(getContextWindowOptions("@cf/zai-org/glm-5.2")).toBeNull(); + }); + + it("offers 200k and 1M with 1M as the default", () => { + const options = getContextWindowOptions("claude-opus-5") ?? []; + expect(options.map((o) => o.value)).toEqual(["200k", "1m"]); + expect(isDefaultSelectOption(options[1]?._meta)).toBe(true); + expect(isDefaultSelectOption(options[0]?._meta)).toBe(false); + }); +}); + describe("toSdkEffort", () => { it.each([ ["low", "low"], diff --git a/packages/agent/src/adapters/claude/session/models.ts b/packages/agent/src/adapters/claude/session/models.ts index 20ba4e81fc..41c9fef9d5 100644 --- a/packages/agent/src/adapters/claude/session/models.ts +++ b/packages/agent/src/adapters/claude/session/models.ts @@ -38,6 +38,25 @@ export function supports1MContext(modelId: string): boolean { return MODELS_WITH_1M_CONTEXT.has(modelId); } +export const CONTEXT_WINDOW_1M_BETA = "context-1m-2025-08-07"; +export const CONTEXT_WINDOW_200K_TOKENS = 200_000; + +interface ContextWindowOption { + value: string; + name: string; + _meta?: Record; +} + +export function getContextWindowOptions( + modelId: string, +): ContextWindowOption[] | null { + if (!supports1MContext(modelId)) return null; + return [ + { value: "200k", name: "200k" }, + { value: "1m", name: "1M", _meta: { [DEFAULT_OPTION_META_KEY]: true } }, + ]; +} + const STANDARD_EFFORT_LEVELS: readonly EffortLevel[] = [ "low", "medium", diff --git a/packages/ui/src/features/sessions/components/ReasoningLevelDropdown.tsx b/packages/ui/src/features/sessions/components/ReasoningLevelDropdown.tsx index 7e68d75008..8f92e2014d 100644 --- a/packages/ui/src/features/sessions/components/ReasoningLevelDropdown.tsx +++ b/packages/ui/src/features/sessions/components/ReasoningLevelDropdown.tsx @@ -5,12 +5,13 @@ import { DropdownMenuContent, DropdownMenuRadioGroup, DropdownMenuRadioItem, + DropdownMenuSeparator, DropdownMenuTrigger, MenuLabel, } from "@posthog/quill"; import { Badge } from "@posthog/ui/primitives/Badge"; import { openUrlInBrowser } from "@posthog/ui/utils/browser"; -import { useRef, useState } from "react"; +import { Fragment, useRef, useState } from "react"; export interface ReasoningLevelOption { value: string; @@ -19,39 +20,55 @@ export interface ReasoningLevelOption { docsUrl?: string; } +export interface ReasoningMenuSection { + key: string; + label: string; + value: string; + options: ReasoningLevelOption[]; + onChange: (value: string) => void; +} + interface ReasoningLevelDropdownProps { value: string; options: ReasoningLevelOption[]; onChange?: (value: string) => void; + sections?: ReasoningMenuSection[]; disabled?: boolean; } /** * The one reasoning dropdown. Every surface that offers reasoning levels - * (channel composer, new task, task page, session views) renders this. + * (channel composer, new task, task page, session views) renders this. Extra + * session settings (context window, fast mode) render as menu sections below. */ export function ReasoningLevelDropdown({ value, options, onChange, + sections, disabled, }: ReasoningLevelDropdownProps) { const [open, setOpen] = useState(false); - const pendingValueRef = useRef(null); + const pendingChangeRef = useRef<(() => void) | null>(null); if (options.length === 0) return null; const activeLabel = options.find((option) => option.value === value)?.label ?? value; + const selectAndClose = (apply: () => void) => { + pendingChangeRef.current = apply; + setOpen(false); + }; + return ( { - if (!isOpen && pendingValueRef.current !== null) { - onChange?.(pendingValueRef.current); - pendingValueRef.current = null; + if (!isOpen && pendingChangeRef.current !== null) { + pendingChangeRef.current(); + pendingChangeRef.current = null; } }} > @@ -83,30 +100,49 @@ export function ReasoningLevelDropdown({ Reasoning { - pendingValueRef.current = next; - setOpen(false); - }} + onValueChange={(next) => selectAndClose(() => onChange?.(next))} > {options.map((option) => ( - - {option.label} - {(option.isDefault || option.docsUrl) && ( - - {option.isDefault && Default} - {option.docsUrl && ( - - )} - - )} - + ))} + {sections?.map((section) => ( + + + {section.label} + + selectAndClose(() => section.onChange(next)) + } + > + {section.options.map((option) => ( + + ))} + + + ))} ); } +function LevelItem({ option }: { option: ReasoningLevelOption }) { + return ( + + {option.label} + {(option.isDefault || option.docsUrl) && ( + + {option.isDefault && Default} + {option.docsUrl && ( + + )} + + )} + + ); +} + function DocsLink({ label, docsUrl }: { label: string; docsUrl: string }) { return (