@jsonstudio/rcc 0.89.555 → 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.
Files changed (70) hide show
  1. package/dist/build-info.js +2 -2
  2. package/dist/modules/llmswitch/bridge.d.ts +43 -0
  3. package/dist/modules/llmswitch/bridge.js +103 -0
  4. package/dist/modules/llmswitch/bridge.js.map +1 -1
  5. package/dist/monitoring/semantic-config-loader.js +3 -1
  6. package/dist/monitoring/semantic-config-loader.js.map +1 -1
  7. package/dist/providers/core/runtime/http-transport-provider.d.ts +3 -0
  8. package/dist/providers/core/runtime/http-transport-provider.js +70 -4
  9. package/dist/providers/core/runtime/http-transport-provider.js.map +1 -1
  10. package/dist/providers/core/runtime/responses-provider.d.ts +2 -2
  11. package/dist/providers/core/runtime/responses-provider.js +33 -28
  12. package/dist/providers/core/runtime/responses-provider.js.map +1 -1
  13. package/dist/providers/core/utils/provider-error-reporter.js +7 -7
  14. package/dist/providers/core/utils/provider-error-reporter.js.map +1 -1
  15. package/dist/providers/core/utils/snapshot-writer.js +6 -2
  16. package/dist/providers/core/utils/snapshot-writer.js.map +1 -1
  17. package/dist/server/runtime/http-server/index.js +59 -47
  18. package/dist/server/runtime/http-server/index.js.map +1 -1
  19. package/dist/server/runtime/http-server/llmswitch-loader.d.ts +0 -1
  20. package/dist/server/runtime/http-server/llmswitch-loader.js +17 -21
  21. package/dist/server/runtime/http-server/llmswitch-loader.js.map +1 -1
  22. package/dist/server/runtime/http-server/request-executor.d.ts +6 -0
  23. package/dist/server/runtime/http-server/request-executor.js +113 -37
  24. package/dist/server/runtime/http-server/request-executor.js.map +1 -1
  25. package/node_modules/@jsonstudio/llms/dist/conversion/codecs/gemini-openai-codec.js +15 -1
  26. package/node_modules/@jsonstudio/llms/dist/conversion/compat/actions/iflow-web-search.d.ts +18 -0
  27. package/node_modules/@jsonstudio/llms/dist/conversion/compat/actions/iflow-web-search.js +87 -0
  28. package/node_modules/@jsonstudio/llms/dist/conversion/compat/profiles/chat-gemini.json +14 -15
  29. package/node_modules/@jsonstudio/llms/dist/conversion/compat/profiles/chat-glm.json +194 -190
  30. package/node_modules/@jsonstudio/llms/dist/conversion/compat/profiles/chat-iflow.json +199 -195
  31. package/node_modules/@jsonstudio/llms/dist/conversion/compat/profiles/chat-lmstudio.json +43 -43
  32. package/node_modules/@jsonstudio/llms/dist/conversion/compat/profiles/chat-qwen.json +20 -20
  33. package/node_modules/@jsonstudio/llms/dist/conversion/compat/profiles/responses-c4m.json +42 -42
  34. package/node_modules/@jsonstudio/llms/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +6 -0
  35. package/node_modules/@jsonstudio/llms/dist/conversion/hub/pipeline/compat/compat-types.d.ts +2 -0
  36. package/node_modules/@jsonstudio/llms/dist/conversion/hub/pipeline/hub-pipeline.js +5 -1
  37. package/node_modules/@jsonstudio/llms/dist/conversion/hub/pipeline/session-identifiers.d.ts +9 -0
  38. package/node_modules/@jsonstudio/llms/dist/conversion/hub/pipeline/session-identifiers.js +76 -0
  39. package/node_modules/@jsonstudio/llms/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/index.js +31 -2
  40. package/node_modules/@jsonstudio/llms/dist/conversion/hub/process/chat-process.js +89 -25
  41. package/node_modules/@jsonstudio/llms/dist/conversion/responses/responses-openai-bridge.js +75 -4
  42. package/node_modules/@jsonstudio/llms/dist/conversion/shared/anthropic-message-utils.js +41 -6
  43. package/node_modules/@jsonstudio/llms/dist/conversion/shared/errors.d.ts +20 -0
  44. package/node_modules/@jsonstudio/llms/dist/conversion/shared/errors.js +28 -0
  45. package/node_modules/@jsonstudio/llms/dist/conversion/shared/responses-conversation-store.js +30 -3
  46. package/node_modules/@jsonstudio/llms/dist/conversion/shared/responses-output-builder.js +68 -6
  47. package/node_modules/@jsonstudio/llms/dist/filters/special/request-toolcalls-stringify.d.ts +13 -0
  48. package/node_modules/@jsonstudio/llms/dist/filters/special/request-toolcalls-stringify.js +103 -3
  49. package/node_modules/@jsonstudio/llms/dist/filters/special/response-tool-text-canonicalize.d.ts +16 -0
  50. package/node_modules/@jsonstudio/llms/dist/filters/special/response-tool-text-canonicalize.js +27 -3
  51. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/classifier.js +4 -2
  52. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/engine.d.ts +30 -0
  53. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/engine.js +618 -42
  54. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/health-manager.d.ts +23 -0
  55. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/health-manager.js +14 -0
  56. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/provider-registry.d.ts +15 -0
  57. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/provider-registry.js +40 -0
  58. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/routing-instructions.d.ts +34 -0
  59. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/routing-instructions.js +393 -0
  60. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/sticky-session-store.d.ts +3 -0
  61. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/sticky-session-store.js +110 -0
  62. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/tool-signals.js +0 -22
  63. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/types.d.ts +41 -0
  64. package/node_modules/@jsonstudio/llms/dist/servertool/engine.js +42 -1
  65. package/node_modules/@jsonstudio/llms/dist/servertool/handlers/web-search.js +157 -4
  66. package/node_modules/@jsonstudio/llms/dist/servertool/types.d.ts +6 -0
  67. package/node_modules/@jsonstudio/llms/package.json +1 -1
  68. package/package.json +8 -5
  69. package/scripts/mock-provider/run-regressions.mjs +38 -2
  70. 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: followupBody,
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) ? entry : null;
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
- if (ctx.options.reenterPipeline) {
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 引擎出参。
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsonstudio/llms",
3
- "version": "0.6.375",
3
+ "version": "0.6.473",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsonstudio/rcc",
3
- "version": "0.89.555",
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 jest: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.375",
130
- "@jsonstudio/rcc": "^0.89.524",
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(cliPath);
52
+ await fs.access(targetPath);
53
+ return true;
41
54
  } catch {
42
- throw new Error('dist/cli.js missing. Please run "npm run build:dev" before mock regressions.');
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();