Files
makelore/shared/managed-model-capabilities.ts

194 lines
8.3 KiB
TypeScript

import type { ImportedModelWebSearchCapability } from './imported-model-profile';
import { normalizeImportedModelWebSearchCapability } from './user-model-config';
function object(value: unknown): Record<string, unknown> {
return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {};
}
function field(value: Record<string, unknown>, snake: string, camel: string): unknown {
return Object.hasOwn(value, snake) ? value[snake] : value[camel];
}
const bool = (v: unknown): boolean | null => typeof v === 'boolean' ? v : null;
const str = (v: unknown): string | null => typeof v === 'string' && v.trim() ? v : null;
const num = (v: unknown): number | null => typeof v === 'number' && Number.isSafeInteger(v) && v >= 0 ? v : null;
const strings = (v: unknown): string[] | null => Array.isArray(v) && v.every(x => typeof x === 'string' && x.trim())
? [...new Set(v as string[])] : null;
export function normalizeManagedModelCatalog(value: unknown, ids?: readonly string[]): ManagedModelCatalog | undefined {
const raw = object(value);
if (field(raw, 'schema_version', 'schemaVersion') !== 2) return undefined;
const models: ManagedModelCatalog['models'] = {};
for (const [id, item] of Object.entries(object(raw.models))) {
if (ids && !ids.includes(id)) continue;
const p = object(item), r = object(p.reasoning), b = object(r.budget), l = object(p.limits);
const format = field(r, 'control_format', 'controlFormat');
const status = field(p, 'resolution_status', 'resolutionStatus');
const webSearch = normalizeImportedModelWebSearchCapability(field(p, 'web_search', 'webSearch'));
models[id] = {
inputModalities: strings(field(p, 'input_modalities', 'inputModalities')),
outputModalities: strings(field(p, 'output_modalities', 'outputModalities')),
reasoning: {
supported: bool(r.supported), canDisable: bool(field(r, 'can_disable', 'canDisable')),
defaultEnabled: bool(field(r, 'default_enabled', 'defaultEnabled')),
effortValues: strings(field(r, 'effort_values', 'effortValues')),
defaultEffort: str(field(r, 'default_effort', 'defaultEffort')),
controlFormat: format === 'qwen' || format === 'deepseek' ? format : null,
budget: r.budget ? {
supported: bool(b.supported), minTokens: num(field(b, 'min_tokens', 'minTokens')),
maxTokens: num(field(b, 'max_tokens', 'maxTokens')), defaultTokens: num(field(b, 'default_tokens', 'defaultTokens')),
exclusiveWithEffort: bool(field(b, 'exclusive_with_effort', 'exclusiveWithEffort')),
} : null,
},
limits: p.limits ? {
contextWindow: num(field(l, 'context_window', 'contextWindow')),
maxInputTokens: num(field(l, 'max_input_tokens', 'maxInputTokens')),
maxOutputTokens: num(field(l, 'max_output_tokens', 'maxOutputTokens')),
} : null,
resolutionStatus: status === 'ready' || status === 'partial' || status === 'conflict' ? status : 'unknown',
issues: Array.isArray(p.issues) ? p.issues.flatMap(item => {
const i = object(item); return typeof i.field === 'string' && typeof i.code === 'string' ? [{ field: i.field, code: i.code }] : [];
}) : [],
updatedAt: str(field(p, 'updated_at', 'updatedAt')),
...(webSearch ? { webSearch } : {}),
};
}
const status = field(raw, 'refresh_status', 'refreshStatus');
return { schemaVersion: 2, models, fetchedAt: str(field(raw, 'fetched_at', 'fetchedAt')),
refreshStatus: status === 'fresh' || status === 'stale' ? status : 'unavailable' };
}
export function parseManagedReasoningChoice(value: unknown): ManagedReasoningChoice | null {
const v = object(value);
if (v.mode === 'default' || v.mode === 'disabled') return { mode: v.mode };
if (v.mode === 'enabled' && (v.effort === undefined || typeof v.effort === 'string' && v.effort.trim())) {
return { mode: 'enabled', ...(typeof v.effort === 'string' ? { effort: v.effort } : {}) };
}
return null;
}
export type ManagedReasoningChoice =
| { mode: 'default' }
| { mode: 'disabled' }
| { mode: 'enabled'; effort?: string };
export interface ManagedModelCapability {
inputModalities: string[] | null;
outputModalities: string[] | null;
reasoning: {
supported: boolean | null;
canDisable: boolean | null;
defaultEnabled: boolean | null;
effortValues: string[] | null;
defaultEffort: string | null;
controlFormat: 'qwen' | 'deepseek' | null;
budget: {
supported: boolean | null;
minTokens: number | null;
maxTokens: number | null;
defaultTokens: number | null;
exclusiveWithEffort: boolean | null;
} | null;
};
limits: {
contextWindow: number | null;
maxInputTokens: number | null;
maxOutputTokens: number | null;
} | null;
resolutionStatus: 'ready' | 'partial' | 'unknown' | 'conflict';
issues: { field: string; code: string }[];
updatedAt: string | null;
webSearch?: ImportedModelWebSearchCapability;
}
export interface ManagedModelCatalog {
schemaVersion: 2;
models: Record<string, ManagedModelCapability>;
fetchedAt: string | null;
refreshStatus: 'fresh' | 'stale' | 'unavailable';
}
export interface ManagedModelRequest {
modelId: string;
choice: ManagedReasoningChoice;
reasoningFields: Record<string, unknown>;
}
export function unknownManagedModelCapability(): ManagedModelCapability {
return {
inputModalities: null,
outputModalities: null,
reasoning: {
supported: null, canDisable: null, defaultEnabled: null,
effortValues: null, defaultEffort: null, controlFormat: null, budget: null,
},
limits: null,
resolutionStatus: 'unknown',
issues: [],
updatedAt: null,
};
}
export function managedReasoningOptions(capability: ManagedModelCapability): {
value: string; label: string; choice: ManagedReasoningChoice;
}[] {
const options: ReturnType<typeof managedReasoningOptions> = [
{ value: 'default', label: '模型默认', choice: { mode: 'default' } },
];
const r = capability.reasoning;
if (r.supported !== true || !r.controlFormat) return options;
if (r.canDisable === true) {
options.push({ value: 'disabled', label: '关闭思考', choice: { mode: 'disabled' } });
options.push({ value: 'enabled', label: '开启(默认强度)', choice: { mode: 'enabled' } });
}
for (const effort of (r.canDisable === true || r.defaultEnabled === true) ? r.effortValues ?? [] : []) {
options.push({ value: 'effort:' + effort, label: effort, choice: { mode: 'enabled', effort } });
}
return options;
}
export function managedReasoningChoiceKey(choice: ManagedReasoningChoice): string {
return choice.mode === 'enabled' && choice.effort !== undefined
? 'effort:' + choice.effort : choice.mode;
}
export function validateManagedReasoningChoice(
choice: ManagedReasoningChoice,
capability: ManagedModelCapability,
): void {
const key = managedReasoningChoiceKey(choice);
if (!managedReasoningOptions(capability).some((option) => option.value === key)) {
throw new Error('该模型的思考选项已不可用,请刷新模型配置后重新选择');
}
}
export function buildManagedModelRequest(
modelId: string,
choice: ManagedReasoningChoice,
capability: ManagedModelCapability,
): ManagedModelRequest {
validateManagedReasoningChoice(choice, capability);
const fields: Record<string, unknown> = {};
const r = capability.reasoning;
if (choice.mode !== 'default') {
if (r.canDisable === true) {
if (r.controlFormat === 'qwen') fields.enable_thinking = choice.mode === 'enabled';
else fields.thinking = { type: choice.mode === 'enabled' ? 'enabled' : 'disabled' };
}
if (choice.mode === 'enabled' && choice.effort !== undefined) {
fields.reasoning_effort = choice.effort;
}
}
return { modelId, choice: { ...choice }, reasoningFields: fields };
}
// Pi's fixed enum is an execution detail. The request hook applies the validated
// native choice; this carrier must never be persisted as the product selection.
export function managedPiThinkingLevel(choice: ManagedReasoningChoice):
'off' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max' {
if (choice.mode !== 'enabled') return 'off';
switch (choice.effort) {
case 'minimal': case 'low': case 'medium': case 'high': case 'xhigh': case 'max':
return choice.effort;
default: return 'low';
}
}