blob: bdfe826c6d64fd4a199c870d4b929f30301cc3f7 [file]
/**
* Controllable thinking level for reasoning-capable models.
*
* A `ThinkingLevel` is a user-facing reasoning-depth knob. It is a per-model
* variant: each model supports a subset of levels (declared here by
* `thinkingVariantsForModel`), and switching models clears the choice so a
* level is never sent to a model that does not understand it. `undefined`
* means "no override" (the model's default behaviour) and is the only value
* persisted-absent — the UI shows it as "默认". `'off'` explicitly disables
* reasoning for providers that expose a true off switch (`reasoningEffort:
* 'none'` for OpenAI gpt-5 / codex, `thinking: { type: 'disabled' }` for
* Anthropic-protocol); providers without a clean off switch do not list it.
*
* The runtime maps a chosen level to the ai-sdk provider option
* (`reasoningEffort` / `thinking.budgetTokens` / `thinkingConfig`) in
* `buildProviderOptions`; this module owns only the vocabulary and the
* per-model supported set, so the UI and runtime share one source of truth.
*/
import type { ProviderType } from './llm-connections.js';
import { lookupModelMetadata } from './model-metadata.js';
/**
* Reasoning-depth variants. Ordered from shallowest to deepest for display.
* Not every model supports every level — call `thinkingVariantsForModel` for
* the model-specific subset.
*/
export type ThinkingLevel = 'off' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max';
export const THINKING_LEVELS: readonly ThinkingLevel[] = [
'off',
'minimal',
'low',
'medium',
'high',
'xhigh',
'max',
];
export function isThinkingLevel(value: unknown): value is ThinkingLevel {
return typeof value === 'string' && (THINKING_LEVELS as readonly string[]).includes(value);
}
/**
* Per-model reasoning controls, mirroring models.dev `reasoning_options` plus
* Maka's adapter knowledge for real disabled wires. `efforts` are provider
* native effort enum values (e.g. `none`, `low`, `high`, `xhigh`, `max`);
* `toggle` records the catalog fact that the model has an on/off switch, but
* UI only exposes `off` when `offBehavior` (or effort `none`) says this adapter
* can actually send a disabled/none/budget-zero request.
*/
export type ThinkingOffBehavior =
| 'anthropic-thinking-disabled'
| 'cohere-thinking-disabled'
| 'cloudflare-chat-template-thinking-false'
| 'google-thinking-budget-zero'
| 'volcengine-thinking-disabled';
export interface ThinkingOptions {
readonly efforts?: readonly string[];
readonly toggle?: boolean;
readonly offBehavior?: ThinkingOffBehavior;
}
/**
* Derive the user-facing thinking-level choices from a model's declared
* `ThinkingOptions`. `none` (OpenAI's off effort) and declared `offBehavior`
* surface as `'off'`; other effort values map to the same-named
* `ThinkingLevel`. Raw `toggle` alone is intentionally not enough because some
* adapters have no real disabled wire. Unknown effort values (not in
* `ThinkingLevel`) are dropped. Returns `[]` for models with no declared
* options (miss → no thinking menu, fallback default).
*/
export function deriveThinkingChoices(
options: ThinkingOptions | undefined,
): readonly ThinkingLevel[] {
if (!options) return [];
const choices = new Set<ThinkingLevel>();
if (options.offBehavior) choices.add('off');
for (const effort of options.efforts ?? []) {
if (effort === 'none') choices.add('off');
else if (isThinkingLevel(effort)) choices.add(effort);
// Unknown effort values (not in ThinkingLevel) are dropped — add the
// level to THINKING_LEVELS if a provider introduces a new effort tier.
}
return THINKING_LEVELS.filter((level) => choices.has(level));
}
/**
* Per-model reasoning options declared in `model-metadata.ts`
* (mirroring models.dev `reasoning_options`). Returns `undefined` for models
* with no declared options (miss → `thinkingVariantsForModel` returns `[]`).
*/
export function thinkingOptionsForModel(
providerType: ProviderType,
modelId: string,
): ThinkingOptions | undefined {
return lookupModelMetadata(providerType, modelId).thinkingOptions;
}
/**
* Levels a model supports, in display order. Returns an empty list for
* non-reasoning models and for provider/model combinations whose reasoning
* support is not declarable from `providerType` + `modelId` alone (e.g.
* `openai-compatible`, where the backing model is user-configured and
* unknown). The UI hides the thinking switcher when this returns `[]`.
*
* Heuristics are intentionally conservative: only patterns known to accept the
* mapped provider option are listed. Refine here as provider support grows —
* this is the single place that decides which models expose the knob.
*/
export function thinkingVariantsForModel(
providerType: ProviderType,
modelId: string,
): readonly ThinkingLevel[] {
return deriveThinkingChoices(thinkingOptionsForModel(providerType, modelId));
}