@jsonstudio/rcc 0.89.552 → 0.89.611
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-info.js +2 -2
- package/dist/modules/llmswitch/bridge.d.ts +43 -0
- package/dist/modules/llmswitch/bridge.js +103 -0
- package/dist/modules/llmswitch/bridge.js.map +1 -1
- package/dist/monitoring/semantic-config-loader.js +3 -1
- package/dist/monitoring/semantic-config-loader.js.map +1 -1
- package/dist/providers/core/runtime/http-transport-provider.d.ts +3 -0
- package/dist/providers/core/runtime/http-transport-provider.js +70 -4
- package/dist/providers/core/runtime/http-transport-provider.js.map +1 -1
- package/dist/providers/core/runtime/responses-provider.d.ts +2 -2
- package/dist/providers/core/runtime/responses-provider.js +33 -28
- package/dist/providers/core/runtime/responses-provider.js.map +1 -1
- package/dist/providers/core/utils/provider-error-reporter.js +7 -7
- package/dist/providers/core/utils/provider-error-reporter.js.map +1 -1
- package/dist/providers/core/utils/snapshot-writer.js +6 -2
- package/dist/providers/core/utils/snapshot-writer.js.map +1 -1
- package/dist/server/runtime/http-server/index.js +59 -47
- package/dist/server/runtime/http-server/index.js.map +1 -1
- package/dist/server/runtime/http-server/llmswitch-loader.d.ts +0 -1
- package/dist/server/runtime/http-server/llmswitch-loader.js +17 -21
- package/dist/server/runtime/http-server/llmswitch-loader.js.map +1 -1
- package/dist/server/runtime/http-server/request-executor.d.ts +6 -0
- package/dist/server/runtime/http-server/request-executor.js +113 -37
- package/dist/server/runtime/http-server/request-executor.js.map +1 -1
- package/node_modules/@jsonstudio/llms/dist/conversion/codecs/gemini-openai-codec.js +15 -1
- package/node_modules/@jsonstudio/llms/dist/conversion/compat/actions/iflow-web-search.d.ts +18 -0
- package/node_modules/@jsonstudio/llms/dist/conversion/compat/actions/iflow-web-search.js +87 -0
- package/node_modules/@jsonstudio/llms/dist/conversion/compat/profiles/chat-gemini.json +14 -15
- package/node_modules/@jsonstudio/llms/dist/conversion/compat/profiles/chat-glm.json +194 -190
- package/node_modules/@jsonstudio/llms/dist/conversion/compat/profiles/chat-iflow.json +199 -195
- package/node_modules/@jsonstudio/llms/dist/conversion/compat/profiles/chat-lmstudio.json +43 -43
- package/node_modules/@jsonstudio/llms/dist/conversion/compat/profiles/chat-qwen.json +20 -20
- package/node_modules/@jsonstudio/llms/dist/conversion/compat/profiles/responses-c4m.json +42 -42
- package/node_modules/@jsonstudio/llms/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +6 -0
- package/node_modules/@jsonstudio/llms/dist/conversion/hub/pipeline/compat/compat-types.d.ts +2 -0
- package/node_modules/@jsonstudio/llms/dist/conversion/hub/pipeline/hub-pipeline.js +5 -1
- package/node_modules/@jsonstudio/llms/dist/conversion/hub/pipeline/session-identifiers.d.ts +9 -0
- package/node_modules/@jsonstudio/llms/dist/conversion/hub/pipeline/session-identifiers.js +76 -0
- package/node_modules/@jsonstudio/llms/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/index.js +31 -2
- package/node_modules/@jsonstudio/llms/dist/conversion/hub/process/chat-process.js +89 -25
- package/node_modules/@jsonstudio/llms/dist/conversion/responses/responses-openai-bridge.js +75 -4
- package/node_modules/@jsonstudio/llms/dist/conversion/shared/anthropic-message-utils.js +41 -6
- package/node_modules/@jsonstudio/llms/dist/conversion/shared/errors.d.ts +20 -0
- package/node_modules/@jsonstudio/llms/dist/conversion/shared/errors.js +28 -0
- package/node_modules/@jsonstudio/llms/dist/conversion/shared/responses-conversation-store.js +30 -3
- package/node_modules/@jsonstudio/llms/dist/conversion/shared/responses-output-builder.js +68 -6
- package/node_modules/@jsonstudio/llms/dist/filters/special/request-toolcalls-stringify.d.ts +13 -0
- package/node_modules/@jsonstudio/llms/dist/filters/special/request-toolcalls-stringify.js +103 -3
- package/node_modules/@jsonstudio/llms/dist/filters/special/response-tool-text-canonicalize.d.ts +16 -0
- package/node_modules/@jsonstudio/llms/dist/filters/special/response-tool-text-canonicalize.js +27 -3
- package/node_modules/@jsonstudio/llms/dist/router/virtual-router/classifier.js +4 -2
- package/node_modules/@jsonstudio/llms/dist/router/virtual-router/engine.d.ts +30 -0
- package/node_modules/@jsonstudio/llms/dist/router/virtual-router/engine.js +618 -42
- package/node_modules/@jsonstudio/llms/dist/router/virtual-router/health-manager.d.ts +23 -0
- package/node_modules/@jsonstudio/llms/dist/router/virtual-router/health-manager.js +14 -0
- package/node_modules/@jsonstudio/llms/dist/router/virtual-router/provider-registry.d.ts +15 -0
- package/node_modules/@jsonstudio/llms/dist/router/virtual-router/provider-registry.js +40 -0
- package/node_modules/@jsonstudio/llms/dist/router/virtual-router/routing-instructions.d.ts +34 -0
- package/node_modules/@jsonstudio/llms/dist/router/virtual-router/routing-instructions.js +393 -0
- package/node_modules/@jsonstudio/llms/dist/router/virtual-router/sticky-session-store.d.ts +3 -0
- package/node_modules/@jsonstudio/llms/dist/router/virtual-router/sticky-session-store.js +110 -0
- package/node_modules/@jsonstudio/llms/dist/router/virtual-router/tool-signals.js +0 -22
- package/node_modules/@jsonstudio/llms/dist/router/virtual-router/types.d.ts +41 -0
- package/node_modules/@jsonstudio/llms/dist/servertool/engine.js +42 -1
- package/node_modules/@jsonstudio/llms/dist/servertool/handlers/web-search.js +157 -4
- package/node_modules/@jsonstudio/llms/dist/servertool/types.d.ts +6 -0
- package/node_modules/@jsonstudio/llms/package.json +1 -1
- package/package.json +8 -5
- package/scripts/mock-provider/run-regressions.mjs +38 -2
- package/scripts/verify-apply-patch.mjs +132 -0
|
@@ -5,6 +5,7 @@ import type { StandardizedRequest } from '../../conversion/hub/types/standardize
|
|
|
5
5
|
export declare const DEFAULT_MODEL_CONTEXT_TOKENS = 200000;
|
|
6
6
|
export declare const DEFAULT_ROUTE = "default";
|
|
7
7
|
export declare const ROUTE_PRIORITY: string[];
|
|
8
|
+
export type RoutingInstructionMode = 'force' | 'sticky' | 'none';
|
|
8
9
|
export interface RoutePoolTier {
|
|
9
10
|
id: string;
|
|
10
11
|
targets: string[];
|
|
@@ -167,6 +168,46 @@ export interface RouterMetadataInput {
|
|
|
167
168
|
* serverToolsDisabled when this flag is true.
|
|
168
169
|
*/
|
|
169
170
|
serverToolRequired?: boolean;
|
|
171
|
+
/**
|
|
172
|
+
* 强制路由模式,从消息中的 <**...**> 指令解析得出
|
|
173
|
+
*/
|
|
174
|
+
routingMode?: RoutingInstructionMode;
|
|
175
|
+
/**
|
|
176
|
+
* 允许的 provider 白名单
|
|
177
|
+
*/
|
|
178
|
+
allowedProviders?: string[];
|
|
179
|
+
/**
|
|
180
|
+
* 强制使用的 provider model (格式: provider.model)
|
|
181
|
+
*/
|
|
182
|
+
forcedProviderModel?: string;
|
|
183
|
+
/**
|
|
184
|
+
* 强制使用的 provider keyAlias
|
|
185
|
+
*/
|
|
186
|
+
forcedProviderKeyAlias?: string;
|
|
187
|
+
/**
|
|
188
|
+
* 强制使用的 provider keyIndex (从 1 开始)
|
|
189
|
+
*/
|
|
190
|
+
forcedProviderKeyIndex?: number;
|
|
191
|
+
/**
|
|
192
|
+
* 禁用的 provider model 列表
|
|
193
|
+
*/
|
|
194
|
+
disabledProviderModels?: string[];
|
|
195
|
+
/**
|
|
196
|
+
* 禁用的 provider keyAlias 列表
|
|
197
|
+
*/
|
|
198
|
+
disabledProviderKeyAliases?: string[];
|
|
199
|
+
/**
|
|
200
|
+
* 禁用的 provider keyIndex 列表 (从 1 开始)
|
|
201
|
+
*/
|
|
202
|
+
disabledProviderKeyIndexes?: number[];
|
|
203
|
+
/**
|
|
204
|
+
* 本次请求内需要临时排除的 providerKey 列表。
|
|
205
|
+
* 与 disabledProviders/disabledKeys 不同,这些 key 仅对当前路由决策生效,
|
|
206
|
+
* 不会写入或持久化到 RoutingInstructionState/sticky 存储中。
|
|
207
|
+
*/
|
|
208
|
+
excludedProviderKeys?: string[];
|
|
209
|
+
sessionId?: string;
|
|
210
|
+
conversationId?: string;
|
|
170
211
|
responsesResume?: {
|
|
171
212
|
previousRequestId?: string;
|
|
172
213
|
restoredFromResponseId?: string;
|
|
@@ -41,12 +41,53 @@ export async function runServerToolOrchestration(options) {
|
|
|
41
41
|
const followupBody = followup.body && typeof followup.body === 'object'
|
|
42
42
|
? followup.body
|
|
43
43
|
: engineResult.finalChatResponse;
|
|
44
|
+
const decorated = decorateFinalChatWithServerToolContext(followupBody, engineResult.execution);
|
|
44
45
|
return {
|
|
45
|
-
chat:
|
|
46
|
+
chat: decorated,
|
|
46
47
|
executed: true,
|
|
47
48
|
flowId: engineResult.execution.flowId
|
|
48
49
|
};
|
|
49
50
|
}
|
|
51
|
+
function decorateFinalChatWithServerToolContext(chat, execution) {
|
|
52
|
+
if (!execution || !execution.context) {
|
|
53
|
+
return chat;
|
|
54
|
+
}
|
|
55
|
+
// 目前仅对 web_search flow 附加原文摘要,避免影响其它 ServerTool。
|
|
56
|
+
if (execution.flowId !== 'web_search_flow') {
|
|
57
|
+
return chat;
|
|
58
|
+
}
|
|
59
|
+
const ctx = execution.context;
|
|
60
|
+
const web = ctx.web_search;
|
|
61
|
+
const summary = web && typeof web.summary === 'string' && web.summary.trim().length
|
|
62
|
+
? web.summary.trim()
|
|
63
|
+
: '';
|
|
64
|
+
if (!summary) {
|
|
65
|
+
return chat;
|
|
66
|
+
}
|
|
67
|
+
const engineId = web && typeof web.engineId === 'string' && web.engineId.trim().length
|
|
68
|
+
? web.engineId.trim()
|
|
69
|
+
: undefined;
|
|
70
|
+
const label = engineId
|
|
71
|
+
? `【web_search 原文 | engine: ${engineId}】`
|
|
72
|
+
: '【web_search 原文】';
|
|
73
|
+
const cloned = JSON.parse(JSON.stringify(chat));
|
|
74
|
+
const choices = Array.isArray(cloned.choices) ? cloned.choices : [];
|
|
75
|
+
if (!choices.length) {
|
|
76
|
+
return cloned;
|
|
77
|
+
}
|
|
78
|
+
const first = choices[0] && typeof choices[0] === 'object' ? choices[0] : null;
|
|
79
|
+
if (!first || !first.message || typeof first.message !== 'object') {
|
|
80
|
+
return cloned;
|
|
81
|
+
}
|
|
82
|
+
const message = first.message;
|
|
83
|
+
const baseContent = typeof message.content === 'string' ? message.content : '';
|
|
84
|
+
const suffix = `${label}\n${summary}`;
|
|
85
|
+
message.content =
|
|
86
|
+
baseContent && baseContent.trim().length
|
|
87
|
+
? `${baseContent}\n\n${suffix}`
|
|
88
|
+
: suffix;
|
|
89
|
+
return cloned;
|
|
90
|
+
}
|
|
50
91
|
function resolveRouteHint(adapterContext, flowId) {
|
|
51
92
|
const rawRoute = adapterContext.routeId;
|
|
52
93
|
const routeId = typeof rawRoute === 'string' && rawRoute.trim() ? rawRoute.trim() : '';
|
|
@@ -62,7 +62,14 @@ const handler = async (ctx) => {
|
|
|
62
62
|
payload: followupPayload,
|
|
63
63
|
metadata: buildFollowupMetadata(ctx.adapterContext, 'web_search')
|
|
64
64
|
}
|
|
65
|
-
: undefined
|
|
65
|
+
: undefined,
|
|
66
|
+
context: {
|
|
67
|
+
web_search: {
|
|
68
|
+
engineId: chosenEngine.id,
|
|
69
|
+
providerKey: chosenEngine.providerKey,
|
|
70
|
+
summary: chosenResult.summary
|
|
71
|
+
}
|
|
72
|
+
}
|
|
66
73
|
};
|
|
67
74
|
return {
|
|
68
75
|
chatResponse: patched,
|
|
@@ -89,7 +96,9 @@ function getWebSearchConfig(ctx) {
|
|
|
89
96
|
const enginesRaw = Array.isArray(record.engines) ? record.engines : [];
|
|
90
97
|
const engines = [];
|
|
91
98
|
for (const entry of enginesRaw) {
|
|
92
|
-
const obj = entry && typeof entry === 'object' && !Array.isArray(entry)
|
|
99
|
+
const obj = entry && typeof entry === 'object' && !Array.isArray(entry)
|
|
100
|
+
? entry
|
|
101
|
+
: null;
|
|
93
102
|
if (!obj)
|
|
94
103
|
continue;
|
|
95
104
|
const id = typeof obj.id === 'string' && obj.id.trim() ? obj.id.trim() : undefined;
|
|
@@ -104,12 +113,26 @@ function getWebSearchConfig(ctx) {
|
|
|
104
113
|
(obj.serverTools &&
|
|
105
114
|
typeof obj.serverTools === 'object' &&
|
|
106
115
|
obj.serverTools.enabled === false);
|
|
116
|
+
let searchEngineList;
|
|
117
|
+
const rawSearchList = obj.searchEngineList;
|
|
118
|
+
if (Array.isArray(rawSearchList)) {
|
|
119
|
+
const normalizedList = [];
|
|
120
|
+
for (const item of rawSearchList) {
|
|
121
|
+
if (typeof item === 'string' && item.trim().length) {
|
|
122
|
+
normalizedList.push(item.trim());
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (normalizedList.length) {
|
|
126
|
+
searchEngineList = normalizedList;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
107
129
|
engines.push({
|
|
108
130
|
id,
|
|
109
131
|
providerKey,
|
|
110
132
|
description: typeof obj.description === 'string' && obj.description.trim() ? obj.description.trim() : undefined,
|
|
111
133
|
default: obj.default === true,
|
|
112
|
-
...(serverToolsDisabled ? { serverToolsDisabled: true } : {})
|
|
134
|
+
...(serverToolsDisabled ? { serverToolsDisabled: true } : {}),
|
|
135
|
+
...(searchEngineList ? { searchEngineList } : {})
|
|
113
136
|
});
|
|
114
137
|
}
|
|
115
138
|
if (!engines.length) {
|
|
@@ -179,6 +202,10 @@ function isGeminiWebSearchEngine(engine) {
|
|
|
179
202
|
key.startsWith('antigravity.') ||
|
|
180
203
|
key.startsWith('gemini.'));
|
|
181
204
|
}
|
|
205
|
+
function isIflowWebSearchEngine(engine) {
|
|
206
|
+
const key = engine.providerKey.toLowerCase();
|
|
207
|
+
return key.startsWith('iflow.');
|
|
208
|
+
}
|
|
182
209
|
function normalizeResultCount(value) {
|
|
183
210
|
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
184
211
|
const normalized = Math.trunc(value);
|
|
@@ -197,7 +224,22 @@ async function executeWebSearchBackend(args) {
|
|
|
197
224
|
try {
|
|
198
225
|
logServerToolWebSearch(engine, ctx.options.requestId, query);
|
|
199
226
|
const requestSuffix = `:web_search:${engine.id}`;
|
|
200
|
-
|
|
227
|
+
// 对于 iFlow,直接通过 providerInvoker 调用 /chat/retrieve,
|
|
228
|
+
// 即使 reenterPipeline 可用,也不走 Chat 模型 + tools。
|
|
229
|
+
if (isIflowWebSearchEngine(engine) && ctx.options.providerInvoker) {
|
|
230
|
+
const backendResult = await executeIflowWebSearchViaProvider({
|
|
231
|
+
ctx,
|
|
232
|
+
engine,
|
|
233
|
+
query,
|
|
234
|
+
recency,
|
|
235
|
+
count: args.resultCount,
|
|
236
|
+
requestSuffix
|
|
237
|
+
});
|
|
238
|
+
summary = backendResult.summary;
|
|
239
|
+
hits = backendResult.hits;
|
|
240
|
+
ok = backendResult.ok;
|
|
241
|
+
}
|
|
242
|
+
else if (ctx.options.reenterPipeline) {
|
|
201
243
|
const payload = buildWebSearchReenterPayload(engine, query, recency, args.resultCount);
|
|
202
244
|
const followup = await ctx.options.reenterPipeline({
|
|
203
245
|
entryEndpoint: '/v1/chat/completions',
|
|
@@ -384,6 +426,117 @@ async function executeWebSearchViaProvider(args) {
|
|
|
384
426
|
}
|
|
385
427
|
return extractTextFromChatLike(providerResponse);
|
|
386
428
|
}
|
|
429
|
+
async function executeIflowWebSearchViaProvider(args) {
|
|
430
|
+
const { ctx, engine, query, count, requestSuffix } = args;
|
|
431
|
+
if (!ctx.options.providerInvoker) {
|
|
432
|
+
return {
|
|
433
|
+
summary: '',
|
|
434
|
+
hits: [],
|
|
435
|
+
ok: false
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
const searchEngineList = Array.isArray(engine.searchEngineList) && engine.searchEngineList.length
|
|
439
|
+
? engine.searchEngineList
|
|
440
|
+
: ['GOOGLE', 'BING', 'SCHOLAR', 'AIPGC', 'PDF'];
|
|
441
|
+
const searchBody = {
|
|
442
|
+
query,
|
|
443
|
+
history: {},
|
|
444
|
+
userId: 2,
|
|
445
|
+
userIp: '42.120.74.197',
|
|
446
|
+
appCode: 'SEARCH_CHATBOT',
|
|
447
|
+
chatId: Date.now(),
|
|
448
|
+
phase: 'UNIFY',
|
|
449
|
+
enableQueryRewrite: false,
|
|
450
|
+
enableRetrievalSecurity: false,
|
|
451
|
+
enableIntention: false,
|
|
452
|
+
searchEngineList
|
|
453
|
+
};
|
|
454
|
+
let providerKey = engine.providerKey;
|
|
455
|
+
try {
|
|
456
|
+
const adapter = ctx.adapterContext && typeof ctx.adapterContext === 'object'
|
|
457
|
+
? ctx.adapterContext
|
|
458
|
+
: null;
|
|
459
|
+
const target = adapter && adapter.target && typeof adapter.target === 'object'
|
|
460
|
+
? adapter.target
|
|
461
|
+
: null;
|
|
462
|
+
const targetProviderKey = target && typeof target.providerKey === 'string' && target.providerKey.trim()
|
|
463
|
+
? target.providerKey.trim()
|
|
464
|
+
: undefined;
|
|
465
|
+
if (targetProviderKey) {
|
|
466
|
+
providerKey = targetProviderKey;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
catch {
|
|
470
|
+
// best-effort: fallback to engine.providerKey
|
|
471
|
+
}
|
|
472
|
+
const payload = {
|
|
473
|
+
data: searchBody,
|
|
474
|
+
metadata: {
|
|
475
|
+
entryEndpoint: '/chat/retrieve',
|
|
476
|
+
iflowWebSearch: true,
|
|
477
|
+
routeName: 'web_search'
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
const backend = await ctx.options.providerInvoker({
|
|
481
|
+
providerKey,
|
|
482
|
+
providerType: undefined,
|
|
483
|
+
modelId: undefined,
|
|
484
|
+
providerProtocol: ctx.options.providerProtocol,
|
|
485
|
+
payload,
|
|
486
|
+
entryEndpoint: '/v1/chat/retrieve',
|
|
487
|
+
requestId: `${ctx.options.requestId}${requestSuffix}`,
|
|
488
|
+
routeHint: 'web_search'
|
|
489
|
+
});
|
|
490
|
+
const providerResponse = backend.providerResponse && typeof backend.providerResponse === 'object'
|
|
491
|
+
? backend.providerResponse
|
|
492
|
+
: null;
|
|
493
|
+
if (!providerResponse) {
|
|
494
|
+
return {
|
|
495
|
+
summary: '',
|
|
496
|
+
hits: [],
|
|
497
|
+
ok: false
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
const container = providerResponse;
|
|
501
|
+
const rawHits = Array.isArray(container.data) ? container.data : [];
|
|
502
|
+
const hits = [];
|
|
503
|
+
for (const item of rawHits) {
|
|
504
|
+
if (!item || typeof item !== 'object' || Array.isArray(item))
|
|
505
|
+
continue;
|
|
506
|
+
const record = item;
|
|
507
|
+
const link = typeof record.url === 'string' && record.url.trim() ? record.url.trim() : '';
|
|
508
|
+
if (!link)
|
|
509
|
+
continue;
|
|
510
|
+
const title = typeof record.title === 'string' && record.title.trim() ? record.title.trim() : undefined;
|
|
511
|
+
const publishDate = typeof record.time === 'string' && record.time.trim() ? record.time.trim() : undefined;
|
|
512
|
+
const content = typeof record.abstractInfo === 'string' && record.abstractInfo.trim()
|
|
513
|
+
? record.abstractInfo.trim()
|
|
514
|
+
: undefined;
|
|
515
|
+
hits.push({
|
|
516
|
+
title,
|
|
517
|
+
link,
|
|
518
|
+
publish_date: publishDate,
|
|
519
|
+
content
|
|
520
|
+
});
|
|
521
|
+
if (hits.length >= count) {
|
|
522
|
+
break;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
let summary = '';
|
|
526
|
+
if (typeof container.message === 'string' && container.message.trim()) {
|
|
527
|
+
summary = container.message.trim();
|
|
528
|
+
}
|
|
529
|
+
if (!summary && hits.length) {
|
|
530
|
+
summary = formatHitsSummary(hits);
|
|
531
|
+
}
|
|
532
|
+
const successField = container.success;
|
|
533
|
+
const ok = typeof successField === 'boolean' ? successField : hits.length > 0;
|
|
534
|
+
return {
|
|
535
|
+
summary,
|
|
536
|
+
hits,
|
|
537
|
+
ok
|
|
538
|
+
};
|
|
539
|
+
}
|
|
387
540
|
function injectWebSearchToolResult(base, toolCall, engine, query, backendResult) {
|
|
388
541
|
const cloned = cloneJson(base);
|
|
389
542
|
const existingOutputs = Array.isArray(cloned.tool_outputs)
|
|
@@ -57,6 +57,12 @@ export interface ServerToolFollowupPlan {
|
|
|
57
57
|
export interface ServerToolExecution {
|
|
58
58
|
flowId: string;
|
|
59
59
|
followup?: ServerToolFollowupPlan;
|
|
60
|
+
/**
|
|
61
|
+
* Optional tool-specific context for the execution result.
|
|
62
|
+
* For example, web_search handler may attach { web_search: { engineId, providerKey, summary } }
|
|
63
|
+
* so that orchestration layer can decorate final Chat response without touching host code.
|
|
64
|
+
*/
|
|
65
|
+
context?: JsonObject;
|
|
60
66
|
}
|
|
61
67
|
/**
|
|
62
68
|
* ServerSideToolEngineResult:ServerTool 引擎出参。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsonstudio/rcc",
|
|
3
|
-
"version": "0.89.
|
|
3
|
+
"version": "0.89.611",
|
|
4
4
|
"description": "Multi-provider OpenAI proxy server with anthropic/responses/chat support (dev)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
},
|
|
33
33
|
"scripts": {
|
|
34
34
|
"build": "npm run llmswitch:ensure && node scripts/build-core.mjs && node scripts/vendor-core.mjs && npm run clean && node scripts/gen-build-info.mjs && tsc && node scripts/copy-compat-assets.mjs && node scripts/copy-modules-config.mjs",
|
|
35
|
-
"build:dev": "BUILD_MODE=dev npm run build && npm run verify:e2e-toolcall && npm run install:global",
|
|
35
|
+
"build:dev": "BUILD_MODE=dev npm run build && npm run verify:e2e-toolcall && npm run verify:apply-patch && npm run test:routing-instructions && npm run install:global",
|
|
36
36
|
"build:min": "npm run llmswitch:ensure && node scripts/build-core.mjs && node scripts/vendor-core.mjs && npm run clean && node scripts/gen-build-info.mjs && tsc && node scripts/copy-compat-assets.mjs && node scripts/copy-modules-config.mjs",
|
|
37
37
|
"prepack": "echo skip-prepack",
|
|
38
38
|
"postbuild": "chmod +x dist/cli.js || true",
|
|
@@ -40,7 +40,8 @@
|
|
|
40
40
|
"start": "npm run -s start:bg",
|
|
41
41
|
"dev": "tsx watch src/index.ts",
|
|
42
42
|
"jest:run": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js",
|
|
43
|
-
"test": "npm run
|
|
43
|
+
"test": "npm run test:routing-instructions && npm run mock:regressions",
|
|
44
|
+
"test:routing-instructions": "npm run jest:run -- --runTestsByPath tests/servertool/routing-instructions.spec.ts tests/servertool/hub-pipeline-session-headers.spec.ts tests/providers/core/runtime/http-transport-provider.headers.test.ts",
|
|
44
45
|
"test:watch": "npm run jest:run -- --watch",
|
|
45
46
|
"test:coverage": "npm run jest:run -- --coverage",
|
|
46
47
|
"test:integration": "npm run jest:run -- --testPathPattern=integration",
|
|
@@ -56,6 +57,7 @@
|
|
|
56
57
|
"prebuild": "echo skip-lint",
|
|
57
58
|
"prepare": "",
|
|
58
59
|
"postinstall": "chmod +x dist/cli.js || true",
|
|
60
|
+
"verify:apply-patch": "node scripts/verify-apply-patch.mjs",
|
|
59
61
|
"install:global": "./scripts/install-global.sh",
|
|
60
62
|
"install:release": "./scripts/install-release.sh",
|
|
61
63
|
"audit:tool-text": "node scripts/audit-tool-text.mjs",
|
|
@@ -121,13 +123,14 @@
|
|
|
121
123
|
"sync:ci-goldens": "node scripts/tools/sync-ci-goldens.mjs",
|
|
122
124
|
"mock:extract": "node scripts/mock-provider/extract.mjs",
|
|
123
125
|
"mock:validate": "node scripts/mock-provider/validate.mjs",
|
|
126
|
+
"mock:regressions": "node scripts/mock-provider/run-regressions.mjs",
|
|
124
127
|
"mock:clean": "node scripts/mock-provider/clean.mjs",
|
|
125
128
|
"publish:rcc": "node scripts/publish-rcc.mjs"
|
|
126
129
|
},
|
|
127
130
|
"dependencies": {
|
|
128
131
|
"@anthropic-ai/sdk": "^0.65.0",
|
|
129
|
-
"@jsonstudio/llms": "^0.6.
|
|
130
|
-
"@jsonstudio/rcc": "^0.89.
|
|
132
|
+
"@jsonstudio/llms": "^0.6.473",
|
|
133
|
+
"@jsonstudio/rcc": "^0.89.555",
|
|
131
134
|
"@lmstudio/sdk": "^1.5.0",
|
|
132
135
|
"@radix-ui/react-switch": "^1.2.6",
|
|
133
136
|
"@types/socket.io": "^3.0.1",
|
|
@@ -13,6 +13,7 @@ const MOCK_SAMPLES_DIR = resolveSamplesDir();
|
|
|
13
13
|
const REGISTRY_PATH = path.join(MOCK_SAMPLES_DIR, '_registry', 'index.json');
|
|
14
14
|
const NAME_REGEX = /^[A-Za-z0-9_-]+$/;
|
|
15
15
|
const ENTRY_FILTER = parseEntryFilter();
|
|
16
|
+
const NPM_CMD = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
16
17
|
|
|
17
18
|
function resolveSamplesDir() {
|
|
18
19
|
const override = String(process.env.ROUTECODEX_MOCK_SAMPLES_DIR || '').trim();
|
|
@@ -36,13 +37,48 @@ function parseEntryFilter() {
|
|
|
36
37
|
|
|
37
38
|
async function ensureCliAvailable() {
|
|
38
39
|
const cliPath = path.join(PROJECT_ROOT, 'dist', 'cli.js');
|
|
40
|
+
if (await fileExists(cliPath)) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
console.warn('[mock:regressions] dist/cli.js missing, running "npm run build:min" automatically...');
|
|
44
|
+
await runBuildForMockRegressions();
|
|
45
|
+
if (!(await fileExists(cliPath))) {
|
|
46
|
+
throw new Error('dist/cli.js missing after automatic build. Please run "npm run build:dev" manually.');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function fileExists(targetPath) {
|
|
39
51
|
try {
|
|
40
|
-
await fs.access(
|
|
52
|
+
await fs.access(targetPath);
|
|
53
|
+
return true;
|
|
41
54
|
} catch {
|
|
42
|
-
|
|
55
|
+
return false;
|
|
43
56
|
}
|
|
44
57
|
}
|
|
45
58
|
|
|
59
|
+
async function runBuildForMockRegressions() {
|
|
60
|
+
await new Promise((resolve, reject) => {
|
|
61
|
+
const child = spawn(NPM_CMD, ['run', 'build:min'], {
|
|
62
|
+
cwd: PROJECT_ROOT,
|
|
63
|
+
env: {
|
|
64
|
+
...process.env,
|
|
65
|
+
ROUTECODEX_VERIFY_SKIP: '1'
|
|
66
|
+
},
|
|
67
|
+
stdio: 'inherit'
|
|
68
|
+
});
|
|
69
|
+
child.on('exit', (code) => {
|
|
70
|
+
if (code === 0) {
|
|
71
|
+
resolve();
|
|
72
|
+
} else {
|
|
73
|
+
reject(new Error(`npm run build:min exited with code ${code}`));
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
child.on('error', (error) => {
|
|
77
|
+
reject(error);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
46
82
|
async function loadRegistry() {
|
|
47
83
|
const raw = await fs.readFile(REGISTRY_PATH, 'utf-8');
|
|
48
84
|
const registry = JSON.parse(raw);
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Minimal apply_patch governance verifier (CI client)
|
|
4
|
+
*
|
|
5
|
+
* 直接调用 llmswitch-core 的文本 → tool_calls → 校验链路,
|
|
6
|
+
* 用统一 diff(*** Begin Patch/*** End Patch)触发 apply_patch。
|
|
7
|
+
*/
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
10
|
+
|
|
11
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const repoRoot = path.resolve(__dirname, '..');
|
|
13
|
+
const coreLoaderPath = path.join(repoRoot, 'dist', 'modules', 'llmswitch', 'core-loader.js');
|
|
14
|
+
const coreLoaderUrl = pathToFileURL(coreLoaderPath).href;
|
|
15
|
+
const { importCoreModule } = await import(coreLoaderUrl);
|
|
16
|
+
|
|
17
|
+
async function loadCoreModule(subpath) {
|
|
18
|
+
return importCoreModule(subpath);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function runApplyPatchTextCase(label, patchText) {
|
|
22
|
+
const { normalizeAssistantTextToToolCalls } = await loadCoreModule(
|
|
23
|
+
'conversion/shared/text-markup-normalizer'
|
|
24
|
+
);
|
|
25
|
+
const { canonicalizeChatResponseTools } = await loadCoreModule(
|
|
26
|
+
'conversion/shared/tool-canonicalizer'
|
|
27
|
+
);
|
|
28
|
+
const { validateToolCall } = await loadCoreModule('tools/tool-registry');
|
|
29
|
+
|
|
30
|
+
const message = {
|
|
31
|
+
role: 'assistant',
|
|
32
|
+
content: patchText
|
|
33
|
+
};
|
|
34
|
+
const normalizedMsg = normalizeAssistantTextToToolCalls(message);
|
|
35
|
+
const toolCalls = normalizedMsg?.tool_calls;
|
|
36
|
+
if (!Array.isArray(toolCalls) || toolCalls.length === 0) {
|
|
37
|
+
throw new Error(`[verify-apply-patch] ${label}: text normalizer did not produce tool_calls`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const chatPayload = {
|
|
41
|
+
id: `chatcmpl_apply_patch_${label}`,
|
|
42
|
+
object: 'chat.completion',
|
|
43
|
+
created: Math.floor(Date.now() / 1000),
|
|
44
|
+
model: 'gpt-4.1',
|
|
45
|
+
choices: [
|
|
46
|
+
{
|
|
47
|
+
index: 0,
|
|
48
|
+
message: normalizedMsg,
|
|
49
|
+
finish_reason: 'tool_calls'
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const canonical = canonicalizeChatResponseTools(chatPayload);
|
|
55
|
+
const tc = canonical?.choices?.[0]?.message?.tool_calls?.[0];
|
|
56
|
+
if (!tc || typeof tc !== 'object') {
|
|
57
|
+
throw new Error(`[verify-apply-patch] ${label}: missing tool_calls after canonicalization`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const fn = tc.function || {};
|
|
61
|
+
if (fn.name !== 'apply_patch') {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`[verify-apply-patch] ${label}: expected apply_patch, got ${JSON.stringify(fn.name)}`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
if (typeof fn.arguments !== 'string' || !fn.arguments.trim()) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`[verify-apply-patch] ${label}: arguments must be non-empty JSON string, got ${typeof fn.arguments}`
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
const validation = validateToolCall(fn.name, fn.arguments);
|
|
72
|
+
if (!validation?.ok) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
`[verify-apply-patch] ${label}: validateToolCall failed with reason=${validation?.reason}`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
let parsed;
|
|
78
|
+
try {
|
|
79
|
+
parsed = JSON.parse(validation.normalizedArgs || fn.arguments);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`[verify-apply-patch] ${label}: normalized arguments not valid JSON: ${(error && error.message) || String(error)}`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
if (typeof parsed.patch !== 'string' || !parsed.patch.includes('*** Begin Patch')) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`[verify-apply-patch] ${label}: normalized arguments missing patch text`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
if (typeof parsed.input !== 'string' || !parsed.input.includes('*** Begin Patch')) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`[verify-apply-patch] ${label}: normalized arguments missing input mirror`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function main() {
|
|
98
|
+
if (String(process.env.ROUTECODEX_VERIFY_SKIP || '').trim() === '1') {
|
|
99
|
+
console.log('[verify-apply-patch] 跳过(ROUTECODEX_VERIFY_SKIP=1)');
|
|
100
|
+
process.exit(0);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const plainPatch =
|
|
105
|
+
'*** Begin Patch\n' +
|
|
106
|
+
'*** Add File: hello.txt\n' +
|
|
107
|
+
'+Hello from apply_patch\n' +
|
|
108
|
+
'*** End Patch\n';
|
|
109
|
+
|
|
110
|
+
const fencedPatch =
|
|
111
|
+
'```patch\n' +
|
|
112
|
+
'*** Begin Patch\n' +
|
|
113
|
+
'*** Add File: hello-fenced.txt\n' +
|
|
114
|
+
'+Hello from apply_patch (fenced)\n' +
|
|
115
|
+
'*** End Patch\n' +
|
|
116
|
+
'```';
|
|
117
|
+
|
|
118
|
+
await runApplyPatchTextCase('plain', plainPatch);
|
|
119
|
+
await runApplyPatchTextCase('fenced', fencedPatch);
|
|
120
|
+
|
|
121
|
+
console.log('✅ verify-apply-patch: text→tool_calls pipeline passed');
|
|
122
|
+
} catch (error) {
|
|
123
|
+
console.error(error);
|
|
124
|
+
console.error(
|
|
125
|
+
'❌ verify-apply-patch 失败:',
|
|
126
|
+
error instanceof Error ? error.message : String(error ?? 'Unknown error')
|
|
127
|
+
);
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
main();
|