feat: use managed model capabilities for reasoning and image input

This commit is contained in:
2026-09-12 21:37:18 +08:00
parent e7701d1f2c
commit 26cbb29aa9
29 changed files with 652 additions and 271 deletions

View File

@@ -30,6 +30,7 @@ const CORE_PRODUCT_TOOL_NAMES = new Set([
]);
interface WorkerRegistrationRecord {
managedModelRequest?: import('../../../shared/managed-model-capabilities').ManagedModelRequest;
token: string;
conversationId: string;
generation: number;
@@ -59,6 +60,7 @@ export interface PiExtensionWorkerRegistration {
}
export interface RegisterPiExtensionWorkerInput {
managedModelRequest?: import('../../../shared/managed-model-capabilities').ManagedModelRequest;
conversationId: string;
generation: number;
projectId: string;
@@ -246,6 +248,7 @@ export class PiManagedExtensionHost {
const token = randomBytes(32).toString('base64url');
const contextFile = path.join(input.extensionsDir, `worker-${randomUUID()}.json`);
const record: WorkerRegistrationRecord = {
managedModelRequest: input.managedModelRequest,
token,
conversationId: input.conversationId,
generation: input.generation,
@@ -292,7 +295,7 @@ export class PiManagedExtensionHost {
};
}
async bindRun(conversationId: string, generation: number, runId: string): Promise<void> {
async bindRun(conversationId: string, generation: number, runId: string, managedModelRequest?: import('../../../shared/managed-model-capabilities').ManagedModelRequest): Promise<void> {
const record = this.findWorker(conversationId, generation);
if (!record) throw new Error('Pi extension worker registration is unavailable');
if (this.productTools && record.projectPath) {
@@ -301,6 +304,7 @@ export class PiManagedExtensionHost {
this.releaseWorkerResources(record);
this.runBindings.set(conversationId, runId);
record.runId = runId;
if (managedModelRequest) record.managedModelRequest = structuredClone(managedModelRequest);
await this.writeContext(record);
}
@@ -664,6 +668,7 @@ export class PiManagedExtensionHost {
tools: record.tools,
projectWriteLeaseToolNames: record.projectWriteLeaseToolNames,
...(record.runId ? { runId: record.runId } : {}),
...(record.managedModelRequest ? { managedModelRequest: record.managedModelRequest } : {}),
});
}

View File

@@ -1,7 +1,7 @@
import path from 'node:path';
import { atomicWriteText } from '../../../coding-projects/atomic-json';
export const MAKELORE_PI_EXTENSION_VERSION = 6;
export const MAKELORE_PI_EXTENSION_VERSION = 7;
export const MAKELORE_PI_EXTENSION_FILENAME = `makelore-runtime-v${MAKELORE_PI_EXTENSION_VERSION}.mjs`;
const BUNDLE_SOURCE = String.raw`
@@ -434,6 +434,20 @@ export function createMakeloreRuntime(runtimeDefaults = {}) {
await registerDynamicProductTools();
pi.on('before_provider_request', async (event) => {
const context = await readWorkerContext();
const managed = context.managedModelRequest;
if (!managed || !context.runId) return;
if (!event.payload || typeof event.payload !== 'object' || Array.isArray(event.payload)) return;
if (event.payload.model !== managed.modelId) return;
const payload = { ...event.payload };
delete payload.thinking;
delete payload.enable_thinking;
delete payload.reasoning_effort;
delete payload.thinking_budget;
return { ...payload, ...managed.reasoningFields };
});
pi.on('tool_call', async (event, ctx) => {
if (!MUTATION_TOOLS.has(event.toolName) && !dynamicLeaseTools.has(event.toolName)) return;
const input = event.input || event.arguments || event.args || {};

View File

@@ -35,8 +35,10 @@ export const PI_PROVIDER_APIS = [
] as const;
export type PiProviderApi = (typeof PI_PROVIDER_APIS)[number];
import { unknownManagedModelCapability } from '../../../shared/managed-model-capabilities';
export interface PiProviderModelDescriptor {
managedCapability?: import('../../../shared/managed-model-capabilities').ManagedModelCapability;
id: string;
name: string;
input: Array<'text' | 'image'>;
@@ -119,6 +121,7 @@ export async function buildPiProviderCatalogFromProviderService(
}
export interface PiProviderSelection {
managedCapability?: import('../../../shared/managed-model-capabilities').ManagedModelCapability;
accountId: string;
runtimeProviderId: string;
modelId: string;
@@ -258,7 +261,7 @@ function normalizeModelId(rawModelId: string | undefined, account: ProviderAccou
const unqualified = modelId.startsWith(runtimePrefix)
? modelId.slice(runtimePrefix.length)
: modelId;
return account.id === NIANCODE_USER_MODEL_ACCOUNT_ID
return account.id === NIANCODE_USER_MODEL_ACCOUNT_ID && !account.metadata?.worksSquareModelCapabilitiesV2
? normalizeImportedUserModelId(unqualified)
: unqualified;
}
@@ -306,41 +309,52 @@ function modelDescriptor(
const backend = backendModels.get(modelId);
const profile = getImportedModelProfile(modelId);
const serverCapability = account.metadata?.worksSquareModelCapabilities?.[modelId];
const managed = account.id === 'niancode-user-models'
? account.metadata?.worksSquareModelCapabilitiesV2?.models[modelId] ?? unknownManagedModelCapability()
: undefined;
const backendInput = Array.isArray(backend?.input)
? backend.input.filter((input): input is 'text' | 'image' => input === 'text' || input === 'image')
: [];
const supportsImage = Boolean(
const supportsImage = managed ? managed.inputModalities?.includes('image') === true : Boolean(
summary?.supportsVision
|| profile?.modalities.input.includes('image')
|| backendInput.includes('image'),
);
const contextWindow = finitePositiveInteger(summary?.contextWindow)
const contextWindow = finitePositiveInteger(managed?.limits?.contextWindow)
?? finitePositiveInteger(summary?.contextWindow)
?? finitePositiveInteger(profile?.limit?.context)
?? finitePositiveInteger(backend?.contextWindow);
const maxOutputTokens = finitePositiveInteger(profile?.limit?.output)
const maxOutputTokens = finitePositiveInteger(managed?.limits?.maxOutputTokens)
?? finitePositiveInteger(profile?.limit?.output)
?? finitePositiveInteger(backend?.maxTokens);
return {
id: modelId,
name: summary?.name || (typeof backend?.name === 'string' && backend.name.trim()) || modelId,
input: supportsImage ? ['text', 'image'] : ['text'],
reasoning: serverCapability
...(managed ? { managedCapability: managed } : {}),
reasoning: managed ? managed.reasoning.supported === true : serverCapability
? serverCapability.reasoningEfforts.length > 0
: summary?.supportsReasoning === true
|| profile?.pi?.reasoning === true
|| backend?.reasoning === true,
...(contextWindow ? { contextWindow } : {}),
...(maxOutputTokens ? { maxOutputTokens } : {}),
...(compat || profile?.pi?.compat || enforcedCompat || serverCapability
...(compat || profile?.pi?.compat || enforcedCompat || serverCapability || managed
? {
compat: {
...compat,
...profile?.pi?.compat,
...enforcedCompat,
...(serverCapability ? { supportsReasoningEffort: true } : {}),
...(managed ? {
supportsReasoningEffort: false,
thinkingFormat: managed.reasoning.controlFormat ?? undefined,
requiresReasoningContentOnAssistantMessages: managed.reasoning.controlFormat !== null,
} : {}),
},
}
: {}),
...(serverCapability
...(managed ? {} : serverCapability
? { thinkingLevelMap: thinkingLevelMapForImportedModelCapability(serverCapability) }
: profile?.pi?.thinkingLevelMap
? { thinkingLevelMap: { ...profile.pi.thinkingLevelMap } }
@@ -498,6 +512,7 @@ export function selectPiProviderModel(
modelId: model.id,
thinkingLevel: modelRef.thinkingLevel,
input: [...model.input],
...(model.managedCapability ? { managedCapability: model.managedCapability } : {}),
...(model.contextWindow ? { contextWindow: model.contextWindow } : {}),
...(model.maxOutputTokens ? { maxOutputTokens: model.maxOutputTokens } : {}),
};

View File

@@ -98,6 +98,7 @@ import {
} from './extension-ui-projector';
type RuntimeIdKind = 'run' | 'queue';
import { buildManagedModelRequest, managedPiThinkingLevel, validateManagedReasoningChoice } from '../../../shared/managed-model-capabilities';
export interface PiConversationRuntimeOptions {
pool: PiWorkerPool;
@@ -306,6 +307,9 @@ export function createPiManagedWorkerOpener(
let extension;
try {
extension = await options.extensionHost.registerWorker({
...(selection.managedCapability ? { managedModelRequest: buildManagedModelRequest(
model.modelId, model.reasoningChoice ?? { mode: 'default' }, selection.managedCapability,
) } : {}),
conversationId: input.conversation.conversationId,
generation: input.generation,
projectId: input.conversation.projectId,
@@ -796,6 +800,7 @@ export class PiConversationRuntime implements CodingConversationRuntime {
}
this.snapshot(input.conversationId);
const managedRequest = await this.prepareManagedRequest(input.conversationId, input.attachments.length > 0);
const images = await this.resolveImages(input.attachments);
const runId = this.id('run');
this.acquireRunBackgroundLease(input.conversationId, runId);
@@ -823,7 +828,7 @@ export class PiConversationRuntime implements CodingConversationRuntime {
let ticket;
try {
if (this.extensionHost && generation) {
await this.extensionHost.bindRun(input.conversationId, generation, runId);
await this.extensionHost.bindRun(input.conversationId, generation, runId, managedRequest);
}
ticket = this.pool.startTopLevel({
conversationId: input.conversationId,
@@ -902,10 +907,12 @@ export class PiConversationRuntime implements CodingConversationRuntime {
async validateModel(model: ProductModelRef): Promise<ProductModelRef> {
const selection = await this.resolveModel(model);
if (selection.managedCapability) validateManagedReasoningChoice(model.reasoningChoice ?? { mode: 'default' }, selection.managedCapability);
return {
accountId: selection.accountId,
modelId: selection.modelId,
thinkingLevel: model.thinkingLevel,
...(selection.managedCapability ? { reasoningChoice: model.reasoningChoice ?? { mode: 'default' as const } } : {}),
};
}
@@ -924,6 +931,7 @@ export class PiConversationRuntime implements CodingConversationRuntime {
accountId: selection.accountId,
modelId: selection.modelId,
thinkingLevel,
...(selection.managedCapability ? { reasoningChoice: { mode: 'default' as const } } : {}),
},
modelResolution: 'resolved',
};
@@ -981,6 +989,16 @@ export class PiConversationRuntime implements CodingConversationRuntime {
true,
);
}
const selection = current.model.accountId === 'niancode-user-models' ? await this.resolveModel(current.model) : undefined;
if (selection?.managedCapability) {
const choice = input.reasoningChoice ?? (input.thinkingLevel === 'off'
? { mode: 'disabled' as const } : { mode: 'enabled' as const, effort: input.thinkingLevel });
validateManagedReasoningChoice(choice, selection.managedCapability);
await this.pool.request(input.conversationId, { type: 'set_thinking_level', level: managedPiThinkingLevel(choice) });
return this.persistEffectiveThinking(input.conversationId, {
model: { ...current.model, thinkingLevel: 'off', reasoningChoice: choice }, modelResolution: 'resolved',
}, {}, {}, true, true);
}
const capabilities = await this.pool.request<{ levels?: unknown }>(
input.conversationId,
{ type: 'get_available_thinking_levels' },
@@ -1025,6 +1043,7 @@ export class PiConversationRuntime implements CodingConversationRuntime {
async compact(conversationId: string): Promise<void> {
await this.waitForProjection(conversationId);
this.assertNoUncertainMutation(conversationId);
const managedRequest = await this.prepareManagedRequest(conversationId, false);
const runId = this.id('run');
this.acquireRunBackgroundLease(conversationId, runId);
const generation = this.pool.getState(conversationId)?.generation;
@@ -1032,7 +1051,7 @@ export class PiConversationRuntime implements CodingConversationRuntime {
let ticket;
try {
if (this.extensionHost && generation) {
await this.extensionHost.bindRun(conversationId, generation, runId);
await this.extensionHost.bindRun(conversationId, generation, runId, managedRequest);
}
ticket = this.pool.startTopLevel({
conversationId,
@@ -1317,6 +1336,7 @@ export class PiConversationRuntime implements CodingConversationRuntime {
if (snapshot.run.runId) {
this.acquireRunBackgroundLease(input.conversationId, snapshot.run.runId);
}
await this.prepareManagedRequest(input.conversationId, input.attachments.length > 0);
const images = await this.resolveImages(input.attachments);
const queuePosition = snapshot.queue.items.length + 1;
const queueId = this.id('queue');
@@ -1809,6 +1829,17 @@ export class PiConversationRuntime implements CodingConversationRuntime {
this.states.set(conversationId, createConversationReducerState(projected));
}
private async prepareManagedRequest(conversationId: string, hasImages: boolean) {
const model = this.snapshot(conversationId).conversation.model.model;
if (!model || model.accountId !== 'niancode-user-models') return undefined;
const selection = await this.resolveModel(model);
if (!selection.managedCapability) return undefined;
if (hasImages && !selection.input.includes('image')) {
throw new CodingRuntimeContractError('CODING_MODEL_UNAVAILABLE', '该模型尚未确认支持图片输入,请选择支持图片的模型', true);
}
return buildManagedModelRequest(model.modelId, model.reasoningChoice ?? { mode: 'default' }, selection.managedCapability);
}
private async persistEffectiveThinking(
conversationId: string,
requested: ConversationModelState,
@@ -1818,6 +1849,19 @@ export class PiConversationRuntime implements CodingConversationRuntime {
forcePersist = false,
): Promise<ConversationModelState> {
if (!requested.model) return clone(requested);
const selection = requested.model.accountId === 'niancode-user-models' ? await this.resolveModel(requested.model) : undefined;
if (selection?.managedCapability) {
const durable: ConversationModelState = {
model: { ...requested.model, thinkingLevel: 'off',
reasoningChoice: requested.model.reasoningChoice ?? { mode: 'default' } },
modelResolution: 'resolved',
};
const persisted = await this.registry.setModel(conversationId, durable);
this.pool.updateConversationModel(conversationId, persisted);
const result = { ...persisted, managedCapability: selection.managedCapability };
if (replaceSnapshot) this.replaceModel(conversationId, result);
return clone(result);
}
const effective = effectiveThinkingLevel(stateValue) ?? requested.model.thinkingLevel;
const available = availableThinkingLevels(capabilitiesValue);
if (!available.includes(effective)) available.push(effective);

View File

@@ -52,7 +52,8 @@ function sameModelState(left: ConversationModelState, right: ConversationModelSt
? left.model === right.model
: left.model.accountId === right.model.accountId
&& left.model.modelId === right.model.modelId
&& left.model.thinkingLevel === right.model.thinkingLevel);
&& left.model.thinkingLevel === right.model.thinkingLevel
&& JSON.stringify(left.model.reasoningChoice) === JSON.stringify(right.model.reasoningChoice));
}
export class PiSessionRegistry {

View File

@@ -1,4 +1,5 @@
import type { ModelSummary, ProviderAccount } from '../../shared/providers/types';
import { buildManagedModelRequest } from '../../../shared/managed-model-capabilities';
import { readCodingProjectConfigV2 } from '../../coding-projects/project-config';
import type { CodingProjectStore } from '../../coding-projects/project-store';
import type { PublicUsage } from '../contracts';
@@ -224,6 +225,9 @@ export function createPiManagedSubagentChildOpener(
: {}),
});
const extension = await options.extensionHost.registerWorker({
...(selection.managedCapability ? { managedModelRequest: buildManagedModelRequest(
agent.model.modelId, agent.model.reasoningChoice ?? { mode: 'default' }, selection.managedCapability,
) } : {}),
conversationId: input.conversationId,
generation: input.workerGeneration,
projectId: input.projectId,