@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.
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
@@ -177,6 +177,47 @@ function normalizeUsage(usageRaw) {
177
177
  }
178
178
  return usageRaw;
179
179
  }
180
+ function extractApplyPatchArguments(rawArgs) {
181
+ // Upstream Responses providers may wrap apply_patch arguments in a JSON object
182
+ // (e.g. { patch: '*** Begin Patch...', input: '...' }). For Codex, the tool
183
+ // expects a FREEFORM patch string obeying the unified diff grammar. Here we
184
+ // best-effort extract such a patch string when available.
185
+ const tryExtractFromObject = (obj) => {
186
+ if (!obj || typeof obj !== 'object' || Array.isArray(obj))
187
+ return null;
188
+ const record = obj;
189
+ const candidates = ['patch', 'input'];
190
+ for (const key of candidates) {
191
+ const value = record[key];
192
+ if (typeof value !== 'string')
193
+ continue;
194
+ const trimmed = value.trimStart();
195
+ if (trimmed.startsWith('*** Begin Patch')) {
196
+ return trimmed;
197
+ }
198
+ }
199
+ return null;
200
+ };
201
+ if (typeof rawArgs === 'string') {
202
+ const trimmed = rawArgs.trimStart();
203
+ if (trimmed.startsWith('*** Begin Patch')) {
204
+ return trimmed;
205
+ }
206
+ try {
207
+ const parsed = JSON.parse(rawArgs);
208
+ const fromObject = tryExtractFromObject(parsed);
209
+ if (fromObject) {
210
+ return fromObject;
211
+ }
212
+ }
213
+ catch {
214
+ // non-JSON string that is not a patch header; leave to caller
215
+ }
216
+ return null;
217
+ }
218
+ const fromObject = tryExtractFromObject(rawArgs);
219
+ return fromObject;
220
+ }
180
221
  function buildFunctionCallOutput(call, allocateOutputId, sanitizeFunctionName, baseCount, offset) {
181
222
  try {
182
223
  const fn = call?.function || {};
@@ -187,16 +228,37 @@ function buildFunctionCallOutput(call, allocateOutputId, sanitizeFunctionName, b
187
228
  if (!sanitized || sanitized.toLowerCase() === 'tool')
188
229
  return null;
189
230
  const rawArgs = fn?.arguments ?? call.arguments ?? {};
190
- const argsStr = typeof rawArgs === 'string'
191
- ? rawArgs
192
- : (() => {
231
+ let argsStr;
232
+ if (sanitized === 'apply_patch') {
233
+ const patch = extractApplyPatchArguments(rawArgs);
234
+ if (patch != null) {
235
+ argsStr = patch;
236
+ }
237
+ else if (typeof rawArgs === 'string') {
238
+ argsStr = rawArgs;
239
+ }
240
+ else {
193
241
  try {
194
- return JSON.stringify(rawArgs ?? {});
242
+ argsStr = JSON.stringify(rawArgs ?? {});
195
243
  }
196
244
  catch {
197
- return '{}';
245
+ argsStr = '{}';
198
246
  }
199
- })();
247
+ }
248
+ }
249
+ else {
250
+ argsStr =
251
+ typeof rawArgs === 'string'
252
+ ? rawArgs
253
+ : (() => {
254
+ try {
255
+ return JSON.stringify(rawArgs ?? {});
256
+ }
257
+ catch {
258
+ return '{}';
259
+ }
260
+ })();
261
+ }
200
262
  const originalCallId = typeof call.id === 'string' && call.id.trim().length
201
263
  ? String(call.id)
202
264
  : (typeof call.call_id === 'string' && call.call_id.trim().length ? String(call.call_id) : undefined);
@@ -0,0 +1,13 @@
1
+ import type { Filter, FilterContext, FilterResult, JsonObject } from '../types.js';
2
+ /**
3
+ * Ensure assistant.tool_calls[].function.arguments is a JSON string containing valid JSON.
4
+ * - If arguments is not a string, JSON.stringify it.
5
+ * - If arguments is a string but not parseable as JSON, wrap it into a JSON object so the
6
+ * provider always receives syntactically valid JSON (e.g. {"input": "<raw>"}).
7
+ * Also set assistant.content=null when tool_calls exist (request-side invariant).
8
+ */
9
+ export declare class RequestToolCallsStringifyFilter implements Filter<JsonObject> {
10
+ readonly name = "request_toolcalls_stringify";
11
+ readonly stage: FilterContext['stage'];
12
+ apply(input: JsonObject): FilterResult<JsonObject>;
13
+ }
@@ -1,5 +1,8 @@
1
1
  /**
2
- * Ensure assistant.tool_calls[].function.arguments is a JSON string.
2
+ * Ensure assistant.tool_calls[].function.arguments is a JSON string containing valid JSON.
3
+ * - If arguments is not a string, JSON.stringify it.
4
+ * - If arguments is a string but not parseable as JSON, wrap it into a JSON object so the
5
+ * provider always receives syntactically valid JSON (e.g. {"input": "<raw>"}).
3
6
  * Also set assistant.content=null when tool_calls exist (request-side invariant).
4
7
  */
5
8
  export class RequestToolCallsStringifyFilter {
@@ -18,14 +21,111 @@ export class RequestToolCallsStringifyFilter {
18
21
  if (!tc || typeof tc !== 'object')
19
22
  continue;
20
23
  const fn = tc.function || {};
21
- if (fn && typeof fn === 'object' && fn.arguments !== undefined && typeof fn.arguments !== 'string') {
24
+ if (!fn || typeof fn !== 'object')
25
+ continue;
26
+ const currentArgs = fn.arguments;
27
+ const fnName = typeof fn.name === 'string' ? fn.name.trim() : '';
28
+ // Case 1: non-string arguments → stringify directly
29
+ if (currentArgs !== undefined && typeof currentArgs !== 'string') {
22
30
  try {
23
- fn.arguments = JSON.stringify(fn.arguments ?? {});
31
+ fn.arguments = JSON.stringify(currentArgs ?? {});
24
32
  }
25
33
  catch {
26
34
  fn.arguments = '{}';
27
35
  }
28
36
  tc.function = fn;
37
+ continue;
38
+ }
39
+ // Case 2: string arguments → ensure it is valid JSON
40
+ if (typeof currentArgs === 'string') {
41
+ const trimmed = currentArgs.trim();
42
+ if (trimmed.length === 0) {
43
+ fn.arguments = '{}';
44
+ tc.function = fn;
45
+ continue;
46
+ }
47
+ let parsedOk = false;
48
+ let parsedValue = undefined;
49
+ try {
50
+ parsedValue = JSON.parse(trimmed);
51
+ parsedOk = true;
52
+ }
53
+ catch {
54
+ parsedOk = false;
55
+ }
56
+ if (!parsedOk) {
57
+ // Wrap raw string into a JSON object to keep payload syntactically valid.
58
+ // For shell, align with GLM/统一工具治理约定,优先映射到 { command },
59
+ // 其余模型仍使用 { input } 形式。
60
+ try {
61
+ if (fnName === 'shell') {
62
+ fn.arguments = JSON.stringify({ command: currentArgs });
63
+ }
64
+ else if (fnName === 'apply_patch') {
65
+ fn.arguments = JSON.stringify({ patch: currentArgs });
66
+ }
67
+ else {
68
+ fn.arguments = JSON.stringify({ input: currentArgs });
69
+ }
70
+ }
71
+ catch {
72
+ fn.arguments = '{}';
73
+ }
74
+ tc.function = fn;
75
+ continue;
76
+ }
77
+ // 已经是合法 JSON 的场景下,仅对特定工具做形状修复。
78
+ if (parsedOk && fnName === 'apply_patch') {
79
+ try {
80
+ let obj = parsedValue;
81
+ // 1) 若整体是字符串,则视为补丁文本
82
+ if (typeof obj === 'string') {
83
+ obj = { patch: obj };
84
+ }
85
+ if (obj && typeof obj === 'object') {
86
+ const container = obj;
87
+ const rawPatch = container.patch;
88
+ const rawInput = container.input;
89
+ // 2) 若 patch 是形如 JSON 的字符串,尝试解包 {"input": "..."} 或 {"patch": "..."}
90
+ if (typeof rawPatch === 'string') {
91
+ const ptrim = rawPatch.trim();
92
+ if (ptrim.startsWith('{') && ptrim.endsWith('}')) {
93
+ try {
94
+ const inner = JSON.parse(ptrim);
95
+ if (typeof inner.patch === 'string') {
96
+ container.patch = inner.patch;
97
+ }
98
+ else if (typeof inner.input === 'string') {
99
+ container.patch = inner.input;
100
+ }
101
+ else {
102
+ container.patch = ptrim;
103
+ }
104
+ }
105
+ catch {
106
+ container.patch = rawPatch;
107
+ }
108
+ }
109
+ }
110
+ else if (rawPatch === undefined && typeof rawInput === 'string') {
111
+ // 3) 若只有 input 字段,则复制一份到 patch,避免双层包装
112
+ container.patch = rawInput;
113
+ }
114
+ fn.arguments = JSON.stringify(container);
115
+ tc.function = fn;
116
+ continue;
117
+ }
118
+ }
119
+ catch {
120
+ // 回退到原始字符串形状
121
+ fn.arguments = trimmed;
122
+ tc.function = fn;
123
+ continue;
124
+ }
125
+ }
126
+ // 其它合法 JSON 场景保持原样
127
+ fn.arguments = trimmed;
128
+ tc.function = fn;
29
129
  }
30
130
  }
31
131
  }
@@ -0,0 +1,16 @@
1
+ import type { Filter, FilterContext, FilterResult, JsonObject } from '../types.js';
2
+ /**
3
+ * Canonicalize assistant textual tool markup into tool_calls (Chat path).
4
+ *
5
+ * 行为分两步:
6
+ * 1. 使用 text-markup-normalizer 将纯文本中的工具标记(含 apply_patch)提升为 tool_calls;
7
+ * 2. 再通过 canonicalizeChatResponseTools 统一 tool_calls 形态(content=null、arguments 为字符串等)。
8
+ *
9
+ * 这样可以保证:无论上游 provider 是哪种协议,只要在文本中输出符合规范的 apply_patch / shell 等片段,
10
+ * 在响应侧都会被折叠为标准的 tool_calls,供后续工具治理与客户端透明消费。
11
+ */
12
+ export declare class ResponseToolTextCanonicalizeFilter implements Filter<JsonObject> {
13
+ readonly name = "response_tool_text_canonicalize";
14
+ readonly stage: FilterContext['stage'];
15
+ apply(input: JsonObject): FilterResult<JsonObject>;
16
+ }
@@ -1,16 +1,40 @@
1
1
  /**
2
2
  * Canonicalize assistant textual tool markup into tool_calls (Chat path).
3
- * Wrapper around existing canonicalizer to fit the Filter pipeline.
3
+ *
4
+ * 行为分两步:
5
+ * 1. 使用 text-markup-normalizer 将纯文本中的工具标记(含 apply_patch)提升为 tool_calls;
6
+ * 2. 再通过 canonicalizeChatResponseTools 统一 tool_calls 形态(content=null、arguments 为字符串等)。
7
+ *
8
+ * 这样可以保证:无论上游 provider 是哪种协议,只要在文本中输出符合规范的 apply_patch / shell 等片段,
9
+ * 在响应侧都会被折叠为标准的 tool_calls,供后续工具治理与客户端透明消费。
4
10
  */
5
11
  export class ResponseToolTextCanonicalizeFilter {
6
12
  name = 'response_tool_text_canonicalize';
7
13
  stage = 'response_pre';
8
14
  apply(input) {
9
15
  try {
10
- // Defer to existing canonicalizer for behavior parity
11
16
  // eslint-disable-next-line @typescript-eslint/no-var-requires
12
17
  const { canonicalizeChatResponseTools } = require('../../conversion/shared/tool-canonicalizer.js');
13
- const out = canonicalizeChatResponseTools(input);
18
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
19
+ const { normalizeAssistantTextToToolCalls } = require('../../conversion/shared/text-markup-normalizer.js');
20
+ // 先在文本层面抽取工具调用(apply_patch / shell / MCP 等)
21
+ let working = input && typeof input === 'object' ? JSON.parse(JSON.stringify(input)) : input;
22
+ try {
23
+ const choices = Array.isArray(working?.choices) ? working.choices : [];
24
+ for (const ch of choices) {
25
+ if (!ch || typeof ch !== 'object')
26
+ continue;
27
+ const msg = ch.message;
28
+ if (msg && typeof msg === 'object') {
29
+ ch.message = normalizeAssistantTextToToolCalls(msg);
30
+ }
31
+ }
32
+ working.choices = choices;
33
+ }
34
+ catch {
35
+ // best-effort:文本解析失败时保留原始 payload
36
+ }
37
+ const out = canonicalizeChatResponseTools(working);
14
38
  return { ok: true, data: out };
15
39
  }
16
40
  catch {
@@ -17,8 +17,6 @@ export class RoutingClassifier {
17
17
  const thinkingContinuation = lastToolCategory === 'read';
18
18
  const searchContinuation = lastToolCategory === 'search';
19
19
  const toolsContinuation = lastToolCategory === 'other';
20
- // 用户输入优先级最高(仅次于 vision),确保每次新的用户输入都走 thinking 路由进行工具检查
21
- // thinking_continuation 用于区分"工具轮次中的 read 类调用"与"用户新输入"
22
20
  const evaluationMap = {
23
21
  vision: {
24
22
  triggered: features.hasImageAttachment,
@@ -40,6 +38,10 @@ export class RoutingClassifier {
40
38
  triggered: thinkingContinuation,
41
39
  reason: 'thinking:last-tool-read'
42
40
  },
41
+ search: {
42
+ triggered: searchContinuation,
43
+ reason: 'search:last-tool-search'
44
+ },
43
45
  tools: {
44
46
  triggered: toolsContinuation || features.hasTools || features.hasToolCallResponses,
45
47
  reason: toolsContinuation ? 'tools:last-tool-other' : 'tools:tool-request-detected'
@@ -13,6 +13,7 @@ export declare class VirtualRouterEngine {
13
13
  private healthConfig;
14
14
  private readonly statsCenter;
15
15
  private webSearchForce;
16
+ private routingInstructionState;
16
17
  initialize(config: VirtualRouterConfig): void;
17
18
  route(request: StandardizedRequest | ProcessedRequest, metadata: RouterMetadataInput): {
18
19
  target: TargetMetadata;
@@ -43,6 +44,33 @@ export declare class VirtualRouterEngine {
43
44
  private buildContextCandidatePools;
44
45
  private describeAttempt;
45
46
  private resolveStickyKey;
47
+ private resolveSessionScope;
48
+ private getRoutingInstructionState;
49
+ private buildMetadataInstructions;
50
+ private parseMetadataDisableDescriptor;
51
+ private resolveRoutingMode;
52
+ private resolveInstructionTarget;
53
+ private filterCandidatesByRoutingState;
54
+ private selectFromCandidates;
55
+ private extractProviderId;
56
+ /**
57
+ * 在已有候选路由集合上,筛选出真正挂载了 sticky 池内 providerKey 的路由,
58
+ * 并按 ROUTE_PRIORITY 进行排序;同时显式排除 tools 路由,保证一旦进入
59
+ * sticky 模式,就不会再命中独立的 tools 池(例如 glm/qwen 工具模型)。
60
+ * 若候选集合中完全没有挂载 sticky key 的路由,则尝试在 default 路由上兜底。
61
+ */
62
+ private buildStickyRouteCandidatesFromFiltered;
63
+ /**
64
+ * 在 sticky 模式下,仅在 sticky 池内选择 Provider:
65
+ * - stickyKeySet 表示已经解析并通过健康检查的 providerKey 集合;
66
+ * - 不再依赖 routing[*].targets 中是否挂载这些 key,避免「未初始化路由池」导致 sticky 池为空;
67
+ * - 仍然尊重 allowed/disabledProviders、disabledKeys、disabledModels 以及上下文长度。
68
+ */
69
+ private selectFromStickyPool;
70
+ private extractKeyAlias;
71
+ private normalizeAliasDescriptor;
72
+ private extractKeyIndex;
73
+ private getProviderModelId;
46
74
  private mapProviderError;
47
75
  private deriveReason;
48
76
  private buildRouteCandidates;
@@ -56,6 +84,8 @@ export declare class VirtualRouterEngine {
56
84
  private sortRoutePools;
57
85
  private flattenPoolTargets;
58
86
  private buildHitReason;
87
+ private isRoutingStateEmpty;
88
+ private persistRoutingInstructionState;
59
89
  private decorateWithDetail;
60
90
  private formatVirtualRouterHit;
61
91
  private resolveRouteColor;