feat: use managed model capabilities for reasoning and image input
This commit is contained in:
@@ -476,11 +476,11 @@ export class CodingConversationService {
|
||||
} catch (error) { runtimeError(error); }
|
||||
}
|
||||
|
||||
async setThinking(conversationId: string, thinkingLevel: ProductModelRef['thinkingLevel']): Promise<ConversationModelState> {
|
||||
async setThinking(conversationId: string, thinkingLevel: ProductModelRef['thinkingLevel'], reasoningChoice?: ProductModelRef['reasoningChoice']): Promise<ConversationModelState> {
|
||||
await this.ensurePrepared(conversationId);
|
||||
const { project } = await this.projects.findActiveConversation(conversationId);
|
||||
try {
|
||||
const state = await this.runtime.setThinking({ conversationId, thinkingLevel });
|
||||
const state = await this.runtime.setThinking({ conversationId, thinkingLevel, reasoningChoice });
|
||||
await persist(() => this.projects.conversationStore(project.path).setModelState(conversationId, state));
|
||||
return state;
|
||||
} catch (error) { runtimeError(error); }
|
||||
|
||||
@@ -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 } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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 || {};
|
||||
|
||||
@@ -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 } : {}),
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user