feat: use managed model capabilities for reasoning and image input
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import type { CapabilityResultV1 } from './data-service';
|
||||
import type { ManagedReasoningChoice, ManagedModelCapability } from './managed-model-capabilities';
|
||||
import type { ModelToolDetailsV1 } from './model-tools';
|
||||
import type { DevicePackageToolDetailsV1 } from './device-packages';
|
||||
|
||||
@@ -12,12 +13,14 @@ export interface ProductModelRef {
|
||||
accountId: string;
|
||||
modelId: string;
|
||||
thinkingLevel: ConversationThinkingLevel;
|
||||
reasoningChoice?: ManagedReasoningChoice;
|
||||
}
|
||||
|
||||
export interface ConversationModelState {
|
||||
model: ProductModelRef | null;
|
||||
modelResolution: 'resolved' | 'required';
|
||||
availableThinkingLevels?: ConversationThinkingLevel[];
|
||||
managedCapability?: ManagedModelCapability;
|
||||
}
|
||||
|
||||
export interface PublicUsage {
|
||||
@@ -400,6 +403,7 @@ export interface SetConversationModelInput {
|
||||
export interface SetThinkingLevelInput {
|
||||
conversationId: string;
|
||||
thinkingLevel: ConversationThinkingLevel;
|
||||
reasoningChoice?: ManagedReasoningChoice;
|
||||
}
|
||||
|
||||
export interface ForkConversationInput {
|
||||
|
||||
@@ -114,6 +114,8 @@ function isPublicError(value: unknown): value is CodingRuntimePublicError {
|
||||
&& typeof record.recoverable === 'boolean';
|
||||
}
|
||||
|
||||
import { parseManagedReasoningChoice } from './managed-model-capabilities';
|
||||
|
||||
function isModelState(value: unknown): boolean {
|
||||
const record = asRecord(value);
|
||||
if (!record || !['resolved', 'required'].includes(String(record.modelResolution))) return false;
|
||||
@@ -125,6 +127,7 @@ function isModelState(value: unknown): boolean {
|
||||
&& isNonEmptyString(model.accountId)
|
||||
&& isNonEmptyString(model.modelId)
|
||||
&& THINKING_LEVELS.has(String(model.thinkingLevel))
|
||||
&& (model.reasoningChoice === undefined || parseManagedReasoningChoice(model.reasoningChoice) !== null)
|
||||
&& (record.availableThinkingLevels === undefined
|
||||
|| (Array.isArray(record.availableThinkingLevels)
|
||||
&& record.availableThinkingLevels.length > 0
|
||||
|
||||
193
shared/managed-model-capabilities.ts
Normal file
193
shared/managed-model-capabilities.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
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';
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,7 @@ function normalizeImportedModelCapability(value: unknown): ImportedModelCapabili
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeImportedModelWebSearchCapability(
|
||||
export function normalizeImportedModelWebSearchCapability(
|
||||
value: unknown,
|
||||
): ImportedModelWebSearchCapability | null {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
||||
|
||||
Reference in New Issue
Block a user