194 lines
8.3 KiB
TypeScript
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';
|
|
}
|
|
}
|