SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
68d7816 verified
Raw History Blame Contribute Delete
8.9 kB
import { isUnknownCapability, type ModelCapability } from '#/llm/capability';
import type { LlmErrorMessage } from '#/llm/errors';
import type { LlmModel } from '#/llm/model';
import { SyntaxRequestFormatError } from '#/llm/syntax-errors';
export type ThinkingEffort = 'off' | 'on' | (string & {});
export interface ThinkingRequestOptions {
readonly effort: ThinkingEffort;
readonly keep?: string;
}
export interface ModelThinkingMetadata {
readonly supportEfforts?: readonly string[];
readonly defaultEffort?: string;
readonly offEffort?: string;
readonly alwaysThinking?: boolean;
readonly adaptiveThinking?: boolean;
}
export interface ThinkingDefaults {
readonly enabled?: boolean;
readonly effort?: string;
}
export type ThinkingConfigErrorCode =
| 'effort-not-supported'
| 'thinking-unsupported'
| 'thinking-cannot-disable'
| 'off-needs-offeffort';
export class ThinkingConfigError extends SyntaxRequestFormatError {
readonly code: ThinkingConfigErrorCode;
constructor(code: ThinkingConfigErrorCode, message: string) {
super(message);
this.name = 'ThinkingConfigError';
this.code = code;
}
override toLlmErrorMessage(): LlmErrorMessage<'syntax'> {
return { kind: 'syntax', code: 'thinking_config', message: this.message };
}
}
export type ThinkingResolution =
| { readonly ok: true; readonly encode: 'silent' }
| { readonly ok: true; readonly encode: 'effort'; readonly value: string }
| { readonly ok: false; readonly error: ThinkingConfigError };
export function thinkingMetadataOf(model: LlmModel): ModelThinkingMetadata | undefined {
const candidate = model as LlmModel & ModelThinkingMetadata;
const { supportEfforts, defaultEffort, offEffort, alwaysThinking, adaptiveThinking } = candidate;
if (
supportEfforts === undefined &&
defaultEffort === undefined &&
offEffort === undefined &&
alwaysThinking === undefined &&
adaptiveThinking === undefined
) {
return undefined;
}
return { supportEfforts, defaultEffort, offEffort, alwaysThinking, adaptiveThinking };
}
function capabilityThinking(capability: ModelCapability): boolean | undefined {
return isUnknownCapability(capability) ? undefined : capability.thinking;
}
function effortList(efforts: readonly string[] | undefined): string | undefined {
return efforts !== undefined && efforts.length > 0 ? efforts.join(', ') : undefined;
}
export function resolveThinkingEffort(
options: ThinkingRequestOptions,
model: LlmModel,
strictValidation = false,
): ThinkingResolution {
const effort = options.effort;
const meta = thinkingMetadataOf(model);
if (effort === 'on') {
return { ok: true, encode: 'silent' };
}
if (effort === 'off') {
if (meta?.offEffort !== undefined) {
return { ok: true, encode: 'effort', value: meta.offEffort };
}
if (meta?.alwaysThinking === true) {
const list = effortList(meta.supportEfforts);
return {
ok: false,
error: new ThinkingConfigError(
'thinking-cannot-disable',
list === undefined
? `Model '${model.model}' always reasons and thinking cannot be turned off. Choose a concrete thinking effort instead of 'off'.`
: `Model '${model.model}' always reasons and thinking cannot be turned off. Choose a concrete thinking effort (${list}) instead of 'off'.`,
),
};
}
if (meta?.supportEfforts !== undefined) {
return {
ok: false,
error: new ThinkingConfigError(
'off-needs-offeffort',
`Model '${model.model}' reasons by default but declares no off effort, so thinking cannot be turned off. Declare offEffort (for example 'none') for this model in the model catalog configuration.`,
),
};
}
return { ok: true, encode: 'silent' };
}
if (
strictValidation &&
meta?.supportEfforts !== undefined &&
!meta.supportEfforts.includes(effort)
) {
return {
ok: false,
error: new ThinkingConfigError(
'effort-not-supported',
`Model '${model.model}' does not support thinking effort '${effort}'. Supported efforts: ${meta.supportEfforts.join(', ')}. Set the thinking effort to one of the supported values.`,
),
};
}
if (meta === undefined && capabilityThinking(model.capability) === false) {
return {
ok: false,
error: new ThinkingConfigError(
'thinking-unsupported',
`Model '${model.model}' does not support thinking, but thinking effort '${effort}' was requested. Remove the thinking effort setting or choose a thinking-capable model.`,
),
};
}
return { ok: true, encode: 'effort', value: effort };
}
export function encodeReasoningEffortFallback(
thinking: ThinkingRequestOptions,
model: LlmModel,
strictValidation = false,
): Record<string, unknown> | undefined {
const resolution = resolveThinkingEffort(thinking, model, strictValidation);
if (!resolution.ok) throw resolution.error;
return resolution.encode === 'effort' ? { reasoning_effort: resolution.value } : undefined;
}
function nonEmpty(value: string | undefined): string | undefined {
const trimmed = value?.trim();
return trimmed === undefined || trimmed.length === 0 ? undefined : trimmed;
}
function middleOf(values: readonly string[]): string {
return values[Math.floor(values.length / 2)]!;
}
function effortsFor(meta: ModelThinkingMetadata | undefined): readonly string[] {
return meta?.supportEfforts?.map(nonEmpty).filter((v): v is string => v !== undefined) ?? [];
}
export function normalizeRequestedThinkingEffort(
requested: string | undefined,
): ThinkingEffort | undefined {
return nonEmpty(requested)?.toLowerCase() as ThinkingEffort | undefined;
}
export function modelSupportsThinking(model: LlmModel): boolean {
const meta = thinkingMetadataOf(model);
return (
meta?.alwaysThinking === true ||
meta?.adaptiveThinking === true ||
capabilityThinking(model.capability) === true
);
}
export function defaultThinkingEffortForModel(model: LlmModel): ThinkingEffort {
const meta = thinkingMetadataOf(model);
if (!modelSupportsThinking(model)) return 'off';
const efforts = effortsFor(meta);
if (efforts.length > 0) {
const declared = nonEmpty(meta?.defaultEffort);
return (declared !== undefined && efforts.includes(declared)
? declared
: middleOf(efforts)) as ThinkingEffort;
}
return 'on';
}
function normalizeThinkingEffortForModel(
effort: ThinkingEffort,
model: LlmModel,
strictValidation: boolean,
): ThinkingEffort {
const meta = thinkingMetadataOf(model);
if (effort === 'off' && meta?.alwaysThinking !== true) return 'off';
const efforts = effortsFor(meta);
if (!strictValidation) {
return effort === 'on' && efforts.length > 0
? defaultThinkingEffortForModel(model)
: effort;
}
if (!modelSupportsThinking(model)) return 'off';
if (efforts.length === 0) return 'on';
if (effort === 'on' || !efforts.includes(effort)) {
return defaultThinkingEffortForModel(model);
}
return effort;
}
export function resolveThinkingEffortForModel(
requested: string | undefined,
defaults: ThinkingDefaults | undefined,
model: LlmModel,
strictValidation = false,
): ThinkingEffort {
const configured = normalizeRequestedThinkingEffort(defaults?.effort);
const normalized = normalizeRequestedThinkingEffort(requested);
let effort: ThinkingEffort;
if (normalized !== undefined) {
effort = normalized;
} else if (defaults?.enabled === false) {
effort = 'off';
} else {
effort = configured ?? defaultThinkingEffortForModel(model);
}
if (effort === 'off' && thinkingMetadataOf(model)?.alwaysThinking === true) {
effort =
configured !== undefined && configured !== 'off'
? configured
: defaultThinkingEffortForModel(model);
}
return normalizeThinkingEffortForModel(effort, model, strictValidation);
}
const KEEP_OFF_VALUES = new Set(['0', 'false', 'no', 'off', 'none', 'null']);
type KeepResolution =
| { readonly specified: false }
| { readonly specified: true; readonly value: string | undefined };
function parseKeepValue(raw: string | undefined): KeepResolution {
const trimmed = raw?.trim();
if (trimmed === undefined || trimmed.length === 0) return { specified: false };
if (KEEP_OFF_VALUES.has(trimmed.toLowerCase())) return { specified: true, value: undefined };
return { specified: true, value: trimmed };
}
export function resolveThinkingKeep(
envKeep: string | undefined,
configKeep: string | undefined,
thinkingEffort: ThinkingEffort,
): string | undefined {
if (thinkingEffort === 'off') return undefined;
const fromEnv = parseKeepValue(envKeep);
if (fromEnv.specified) return fromEnv.value;
const fromConfig = parseKeepValue(configKeep);
if (fromConfig.specified) return fromConfig.value;
return 'all';
}