import { type ModelCapabilities, ProviderDriverKind, type ProviderOptionDescriptor, } from "@t3tools/contracts"; import { type CustomModelDefinition, createModelCapabilities } from "@t3tools/shared/model"; /** Editable mirror of a `ProviderOptionChoice`. `key` is only a React key. */ export interface EditorChoice { readonly key: string; readonly id: string; readonly label: string; readonly isDefault: boolean; readonly description?: string; } /** Editable mirror of a `ProviderOptionDescriptor`. `key` is only a React key. */ export interface EditorDescriptor { readonly key: string; readonly type: "select" | "boolean"; readonly id: string; readonly label: string; readonly choices: ReadonlyArray; readonly currentBooleanValue?: boolean | undefined; readonly description?: string | undefined; } export interface CustomModelDraft { readonly slug: string; readonly name: string; readonly descriptors: ReadonlyArray; } export interface DescriptorPreset { readonly id: string; readonly label: string; readonly type: "select" | "boolean"; readonly choices?: ReadonlyArray<{ id: string; label: string; isDefault?: boolean }>; } const EFFORT_CHOICES = [ { id: "low", label: "Low" }, { id: "medium", label: "Medium", isDefault: true }, { id: "high", label: "High" }, { id: "xhigh", label: "Extra High" }, ] as const; /** * Option ids each adapter actually reads off a turn's model selection, with * the usual choices pre-filled. Anything else the user types is stored * verbatim but will be ignored by the driver. */ export const DESCRIPTOR_PRESETS_BY_KIND: Partial< Record> > = { [ProviderDriverKind.make("codex")]: [ { id: "reasoningEffort", label: "Reasoning", type: "select", choices: EFFORT_CHOICES }, { id: "serviceTier", label: "Speed", type: "select", choices: [ { id: "default", label: "Standard", isDefault: true }, { id: "fast", label: "Fast" }, ], }, ], [ProviderDriverKind.make("claudeAgent")]: [ { id: "effort", label: "Reasoning", type: "select", choices: [ { id: "low", label: "Low" }, { id: "medium", label: "Medium" }, { id: "high", label: "High", isDefault: true }, { id: "xhigh", label: "Extra High" }, { id: "max", label: "Max" }, ], }, { id: "fastMode", label: "Fast Mode", type: "boolean" }, { id: "thinking", label: "Thinking", type: "boolean" }, ], [ProviderDriverKind.make("cursor")]: [ { id: "reasoning", label: "Reasoning", type: "select", choices: EFFORT_CHOICES }, { id: "fastMode", label: "Fast Mode", type: "boolean" }, { id: "thinking", label: "Thinking", type: "boolean" }, ], [ProviderDriverKind.make("grok")]: [ { id: "reasoningEffort", label: "Reasoning", type: "select", choices: EFFORT_CHOICES }, ], [ProviderDriverKind.make("opencode")]: [ { id: "variant", label: "Reasoning", type: "select", choices: EFFORT_CHOICES }, { id: "agent", label: "Agent", type: "select", choices: [ { id: "build", label: "Build", isDefault: true }, { id: "plan", label: "Plan" }, ], }, ], }; let nextKey = 0; export function newEditorKey(): string { nextKey += 1; return `k${nextKey}`; } export function choiceFromPreset(choice: { id: string; label: string; isDefault?: boolean; }): EditorChoice { return { key: newEditorKey(), id: choice.id, label: choice.label, isDefault: !!choice.isDefault }; } export function descriptorFromPreset(preset: DescriptorPreset): EditorDescriptor { return { key: newEditorKey(), type: preset.type, id: preset.id, label: preset.label, choices: (preset.choices ?? []).map(choiceFromPreset), }; } export function emptyEditorDescriptor(): EditorDescriptor { return { key: newEditorKey(), type: "select", id: "", label: "", choices: [] }; } export function emptyEditorChoice(): EditorChoice { return { key: newEditorKey(), id: "", label: "", isDefault: false }; } /** * Prompt-injected choices (Claude's `ultrathink`) are delivered as prompt text * by built-in runtime profiles a custom entry does not have, so they are * dropped rather than stored as a plain option value. */ export function descriptorToEditor(descriptor: ProviderOptionDescriptor): EditorDescriptor { const promptInjected = new Set( descriptor.type === "select" ? (descriptor.promptInjectedValues ?? []) : [], ); const choices = descriptor.type === "select" ? descriptor.options.filter((option) => !promptInjected.has(option.id)) : []; const defaultChoice = choices.find((option) => option.id === descriptor.currentValue) ?? choices.find((option) => option.isDefault); return { key: newEditorKey(), type: descriptor.type, id: descriptor.id, label: descriptor.label, ...(descriptor.description !== undefined ? { description: descriptor.description } : {}), ...(descriptor.type === "boolean" && descriptor.currentValue !== undefined ? { currentBooleanValue: descriptor.currentValue } : {}), choices: choices.map((option) => ({ key: newEditorKey(), id: option.id, label: option.label, ...(option.description !== undefined ? { description: option.description } : {}), isDefault: option === defaultChoice, })), }; } export function draftFromDefinition(entry: CustomModelDefinition): CustomModelDraft { return { slug: entry.slug, name: entry.name === entry.slug ? "" : entry.name, descriptors: (entry.capabilities?.optionDescriptors ?? []).map(descriptorToEditor), }; } /** Claude context choices require runtime suffix mappings that custom entries do not carry. */ export function descriptorsFromCapabilities( capabilities: ModelCapabilities | null | undefined, driverKind: ProviderDriverKind | null, ): EditorDescriptor[] { return (capabilities?.optionDescriptors ?? []) .filter((descriptor) => driverKind !== "claudeAgent" || descriptor.id !== "contextWindow") .map(descriptorToEditor); } /** * Validate the draft before saving. Returns the first problem in reading * order so the message is actionable, or `null` when the draft is sound. */ export function validateDraft(draft: CustomModelDraft): string | null { const seenIds = new Set(); for (const [index, descriptor] of draft.descriptors.entries()) { const position = `Option ${index + 1}`; const id = descriptor.id.trim(); if (!id) return `${position} needs an id.`; if (seenIds.has(id)) return `${position}: id "${id}" is used twice.`; seenIds.add(id); if (!descriptor.label.trim()) return `${position} needs a label.`; if (descriptor.type !== "select") continue; if (descriptor.choices.length === 0) return `${position} needs at least one choice.`; const seenChoices = new Set(); for (const choice of descriptor.choices) { const choiceId = choice.id.trim(); if (!choiceId) return `${position} has a choice without a value.`; if (seenChoices.has(choiceId)) { return `${position}: choice "${choiceId}" is used twice.`; } seenChoices.add(choiceId); } } return null; } /** Convert a validated draft back into a definition. Blank name → slug. */ export function definitionFromDraft(draft: CustomModelDraft): CustomModelDefinition { const descriptors: ProviderOptionDescriptor[] = draft.descriptors.map((descriptor) => { const id = descriptor.id.trim(); const label = descriptor.label.trim(); if (descriptor.type === "boolean") { return { id, label, type: "boolean", ...(descriptor.description !== undefined ? { description: descriptor.description } : {}), ...(descriptor.currentBooleanValue !== undefined ? { currentValue: descriptor.currentBooleanValue } : {}), }; } const options = descriptor.choices.map((choice) => ({ id: choice.id.trim(), label: choice.label.trim() || choice.id.trim(), ...(choice.description !== undefined ? { description: choice.description } : {}), ...(choice.isDefault ? { isDefault: true } : {}), })); const currentValue = options.find((option) => option.isDefault)?.id; return { id, label, type: "select", ...(descriptor.description !== undefined ? { description: descriptor.description } : {}), options, ...(currentValue ? { currentValue } : {}), }; }); const name = draft.name.trim(); return { slug: draft.slug, name: name || draft.slug, capabilities: descriptors.length > 0 ? createModelCapabilities({ optionDescriptors: descriptors }) : null, }; }