@jsonstudio/rcc 0.89.333 → 0.89.524

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 (165) hide show
  1. package/dist/build-info.js +3 -3
  2. package/dist/build-info.js.map +1 -1
  3. package/dist/cli.js +62 -0
  4. package/dist/cli.js.map +1 -1
  5. package/dist/commands/token-daemon.d.ts +2 -0
  6. package/dist/commands/token-daemon.js +183 -0
  7. package/dist/commands/token-daemon.js.map +1 -0
  8. package/dist/index.js +4 -3
  9. package/dist/index.js.map +1 -1
  10. package/dist/modules/llmswitch/bridge.d.ts +1 -1
  11. package/dist/modules/llmswitch/bridge.js +3 -2
  12. package/dist/modules/llmswitch/bridge.js.map +1 -1
  13. package/dist/modules/pipeline/utils/colored-logger.js +3 -1
  14. package/dist/modules/pipeline/utils/colored-logger.js.map +1 -1
  15. package/dist/providers/auth/gemini-cli-userinfo-helper.js +12 -2
  16. package/dist/providers/auth/gemini-cli-userinfo-helper.js.map +1 -1
  17. package/dist/providers/auth/oauth-lifecycle.js +261 -22
  18. package/dist/providers/auth/oauth-lifecycle.js.map +1 -1
  19. package/dist/providers/core/config/oauth-flows.d.ts +23 -0
  20. package/dist/providers/core/config/oauth-flows.js +92 -5
  21. package/dist/providers/core/config/oauth-flows.js.map +1 -1
  22. package/dist/providers/core/config/provider-oauth-configs.js +9 -3
  23. package/dist/providers/core/config/provider-oauth-configs.js.map +1 -1
  24. package/dist/providers/core/config/service-profiles.js +18 -10
  25. package/dist/providers/core/config/service-profiles.js.map +1 -1
  26. package/dist/providers/core/runtime/gemini-cli-http-provider.js +87 -20
  27. package/dist/providers/core/runtime/gemini-cli-http-provider.js.map +1 -1
  28. package/dist/providers/core/runtime/http-request-executor.d.ts +1 -0
  29. package/dist/providers/core/runtime/http-request-executor.js +41 -1
  30. package/dist/providers/core/runtime/http-request-executor.js.map +1 -1
  31. package/dist/providers/core/runtime/http-transport-provider.d.ts +2 -0
  32. package/dist/providers/core/runtime/http-transport-provider.js +37 -2
  33. package/dist/providers/core/runtime/http-transport-provider.js.map +1 -1
  34. package/dist/providers/core/runtime/responses-provider.js +8 -3
  35. package/dist/providers/core/runtime/responses-provider.js.map +1 -1
  36. package/dist/providers/core/runtime/vision-debug-utils.d.ts +13 -0
  37. package/dist/providers/core/runtime/vision-debug-utils.js +114 -0
  38. package/dist/providers/core/runtime/vision-debug-utils.js.map +1 -0
  39. package/dist/providers/core/strategies/oauth-auth-code-flow.js +75 -26
  40. package/dist/providers/core/strategies/oauth-auth-code-flow.js.map +1 -1
  41. package/dist/providers/core/utils/http-client.js +2 -1
  42. package/dist/providers/core/utils/http-client.js.map +1 -1
  43. package/dist/providers/core/utils/snapshot-writer.d.ts +1 -1
  44. package/dist/providers/core/utils/snapshot-writer.js.map +1 -1
  45. package/dist/server/handlers/sse-dispatcher.js +22 -2
  46. package/dist/server/handlers/sse-dispatcher.js.map +1 -1
  47. package/dist/server/runtime/http-server/index.d.ts +9 -0
  48. package/dist/server/runtime/http-server/index.js +512 -144
  49. package/dist/server/runtime/http-server/index.js.map +1 -1
  50. package/dist/server/runtime/http-server/request-executor.d.ts +10 -0
  51. package/dist/server/runtime/http-server/request-executor.js +553 -159
  52. package/dist/server/runtime/http-server/request-executor.js.map +1 -1
  53. package/dist/server/runtime/http-server/routes.d.ts +5 -0
  54. package/dist/server/runtime/http-server/routes.js +69 -0
  55. package/dist/server/runtime/http-server/routes.js.map +1 -1
  56. package/dist/server/runtime/http-server/runtime-manager.js +18 -0
  57. package/dist/server/runtime/http-server/runtime-manager.js.map +1 -1
  58. package/dist/server/utils/utf8-chunk-buffer.d.ts +43 -0
  59. package/dist/server/utils/utf8-chunk-buffer.js +132 -0
  60. package/dist/server/utils/utf8-chunk-buffer.js.map +1 -0
  61. package/dist/token-daemon/index.d.ts +7 -0
  62. package/dist/token-daemon/index.js +242 -0
  63. package/dist/token-daemon/index.js.map +1 -0
  64. package/dist/token-daemon/server-utils.d.ts +33 -0
  65. package/dist/token-daemon/server-utils.js +155 -0
  66. package/dist/token-daemon/server-utils.js.map +1 -0
  67. package/dist/token-daemon/token-daemon.d.ts +20 -0
  68. package/dist/token-daemon/token-daemon.js +144 -0
  69. package/dist/token-daemon/token-daemon.js.map +1 -0
  70. package/dist/token-daemon/token-types.d.ts +44 -0
  71. package/dist/token-daemon/token-types.js +18 -0
  72. package/dist/token-daemon/token-types.js.map +1 -0
  73. package/dist/token-daemon/token-utils.d.ts +17 -0
  74. package/dist/token-daemon/token-utils.js +153 -0
  75. package/dist/token-daemon/token-utils.js.map +1 -0
  76. package/dist/tools/semantic-replay.js +7 -6
  77. package/dist/tools/semantic-replay.js.map +1 -1
  78. package/dist/utils/error-handler-registry.d.ts +36 -0
  79. package/dist/utils/error-handler-registry.js +93 -7
  80. package/dist/utils/error-handler-registry.js.map +1 -1
  81. package/node_modules/@jsonstudio/llms/README.md +2 -0
  82. package/node_modules/@jsonstudio/llms/dist/conversion/codecs/gemini-openai-codec.js +137 -5
  83. package/node_modules/@jsonstudio/llms/dist/conversion/compat/actions/gemini-web-search.d.ts +17 -0
  84. package/node_modules/@jsonstudio/llms/dist/conversion/compat/actions/gemini-web-search.js +68 -0
  85. package/node_modules/@jsonstudio/llms/dist/conversion/compat/actions/glm-image-content.d.ts +2 -0
  86. package/node_modules/@jsonstudio/llms/dist/conversion/compat/actions/glm-image-content.js +83 -0
  87. package/node_modules/@jsonstudio/llms/dist/conversion/compat/actions/glm-vision-prompt.d.ts +11 -0
  88. package/node_modules/@jsonstudio/llms/dist/conversion/compat/actions/glm-vision-prompt.js +177 -0
  89. package/node_modules/@jsonstudio/llms/dist/conversion/compat/actions/glm-web-search.d.ts +2 -0
  90. package/node_modules/@jsonstudio/llms/dist/conversion/compat/actions/glm-web-search.js +63 -0
  91. package/node_modules/@jsonstudio/llms/dist/conversion/compat/actions/universal-shape-filter.js +11 -0
  92. package/node_modules/@jsonstudio/llms/dist/conversion/compat/profiles/chat-gemini.json +17 -0
  93. package/node_modules/@jsonstudio/llms/dist/conversion/compat/profiles/chat-glm.json +190 -181
  94. package/node_modules/@jsonstudio/llms/dist/conversion/compat/profiles/chat-iflow.json +195 -195
  95. package/node_modules/@jsonstudio/llms/dist/conversion/compat/profiles/chat-lmstudio.json +43 -43
  96. package/node_modules/@jsonstudio/llms/dist/conversion/compat/profiles/chat-qwen.json +20 -20
  97. package/node_modules/@jsonstudio/llms/dist/conversion/compat/profiles/responses-c4m.json +42 -42
  98. package/node_modules/@jsonstudio/llms/dist/conversion/config/sample-config.json +1 -1
  99. package/node_modules/@jsonstudio/llms/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +24 -0
  100. package/node_modules/@jsonstudio/llms/dist/conversion/hub/pipeline/compat/compat-types.d.ts +8 -0
  101. package/node_modules/@jsonstudio/llms/dist/conversion/hub/pipeline/hub-pipeline.js +39 -4
  102. package/node_modules/@jsonstudio/llms/dist/conversion/hub/pipeline/target-utils.js +6 -0
  103. package/node_modules/@jsonstudio/llms/dist/conversion/hub/process/chat-process.js +213 -1
  104. package/node_modules/@jsonstudio/llms/dist/conversion/hub/response/provider-response.d.ts +34 -0
  105. package/node_modules/@jsonstudio/llms/dist/conversion/hub/response/provider-response.js +84 -24
  106. package/node_modules/@jsonstudio/llms/dist/conversion/hub/response/server-side-tools.d.ts +26 -0
  107. package/node_modules/@jsonstudio/llms/dist/conversion/hub/response/server-side-tools.js +383 -0
  108. package/node_modules/@jsonstudio/llms/dist/conversion/hub/semantic-mappers/gemini-mapper.js +241 -14
  109. package/node_modules/@jsonstudio/llms/dist/conversion/hub/semantic-mappers/responses-mapper.js +17 -1
  110. package/node_modules/@jsonstudio/llms/dist/conversion/hub/standardized-bridge.js +14 -0
  111. package/node_modules/@jsonstudio/llms/dist/conversion/hub/types/standardized.d.ts +1 -0
  112. package/node_modules/@jsonstudio/llms/dist/conversion/responses/responses-openai-bridge.js +82 -3
  113. package/node_modules/@jsonstudio/llms/dist/conversion/shared/anthropic-message-utils.js +92 -3
  114. package/node_modules/@jsonstudio/llms/dist/conversion/shared/bridge-message-utils.js +137 -10
  115. package/node_modules/@jsonstudio/llms/dist/conversion/shared/responses-output-builder.js +43 -2
  116. package/node_modules/@jsonstudio/llms/dist/conversion/shared/snapshot-utils.js +17 -47
  117. package/node_modules/@jsonstudio/llms/dist/conversion/shared/tool-filter-pipeline.js +1 -0
  118. package/node_modules/@jsonstudio/llms/dist/conversion/shared/tool-mapping.js +25 -2
  119. package/node_modules/@jsonstudio/llms/dist/index.d.ts +1 -0
  120. package/node_modules/@jsonstudio/llms/dist/index.js +1 -0
  121. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/bootstrap.js +308 -43
  122. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/classifier.js +11 -17
  123. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/context-advisor.d.ts +0 -2
  124. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/context-advisor.js +0 -12
  125. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/engine.d.ts +17 -2
  126. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/engine.js +332 -95
  127. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/features.js +1 -1
  128. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/message-utils.js +36 -24
  129. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/provider-registry.js +2 -1
  130. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/token-counter.js +14 -3
  131. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/types.d.ts +66 -2
  132. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/types.js +2 -1
  133. package/node_modules/@jsonstudio/llms/dist/servertool/engine.d.ts +27 -0
  134. package/node_modules/@jsonstudio/llms/dist/servertool/engine.js +60 -0
  135. package/node_modules/@jsonstudio/llms/dist/servertool/flow-types.d.ts +40 -0
  136. package/node_modules/@jsonstudio/llms/dist/servertool/flow-types.js +1 -0
  137. package/node_modules/@jsonstudio/llms/dist/servertool/handlers/vision.d.ts +1 -0
  138. package/node_modules/@jsonstudio/llms/dist/servertool/handlers/vision.js +194 -0
  139. package/node_modules/@jsonstudio/llms/dist/servertool/handlers/web-search.d.ts +1 -0
  140. package/node_modules/@jsonstudio/llms/dist/servertool/handlers/web-search.js +638 -0
  141. package/node_modules/@jsonstudio/llms/dist/servertool/orchestration-types.d.ts +33 -0
  142. package/node_modules/@jsonstudio/llms/dist/servertool/orchestration-types.js +1 -0
  143. package/node_modules/@jsonstudio/llms/dist/servertool/registry.d.ts +18 -0
  144. package/node_modules/@jsonstudio/llms/dist/servertool/registry.js +27 -0
  145. package/node_modules/@jsonstudio/llms/dist/servertool/server-side-tools.d.ts +8 -0
  146. package/node_modules/@jsonstudio/llms/dist/servertool/server-side-tools.js +208 -0
  147. package/node_modules/@jsonstudio/llms/dist/servertool/types.d.ts +88 -0
  148. package/node_modules/@jsonstudio/llms/dist/servertool/types.js +1 -0
  149. package/node_modules/@jsonstudio/llms/dist/servertool/vision-tool.d.ts +2 -0
  150. package/node_modules/@jsonstudio/llms/dist/servertool/vision-tool.js +185 -0
  151. package/node_modules/@jsonstudio/llms/dist/sse/json-to-sse/event-generators/responses.js +15 -3
  152. package/node_modules/@jsonstudio/llms/dist/sse/sse-to-json/builders/response-builder.js +6 -3
  153. package/node_modules/@jsonstudio/llms/dist/sse/sse-to-json/gemini-sse-to-json-converter.js +27 -1
  154. package/node_modules/@jsonstudio/llms/dist/sse/types/gemini-types.d.ts +20 -1
  155. package/node_modules/@jsonstudio/llms/dist/sse/types/responses-types.js +1 -1
  156. package/node_modules/@jsonstudio/llms/dist/telemetry/stats-center.d.ts +73 -0
  157. package/node_modules/@jsonstudio/llms/dist/telemetry/stats-center.js +280 -0
  158. package/node_modules/@jsonstudio/llms/package.json +1 -1
  159. package/package.json +2 -2
  160. package/scripts/pack-mode.mjs +2 -1
  161. package/scripts/publish-rcc.mjs +20 -4
  162. package/scripts/tests/virtual-router-health.mjs +141 -6
  163. package/dist/tools/replay-request.d.ts +0 -0
  164. package/dist/tools/replay-request.js +0 -2
  165. package/dist/tools/replay-request.js.map +0 -1
@@ -56,6 +56,19 @@ function enforceBuiltinToolSchema(name, candidate) {
56
56
  const base = asSchema(candidate);
57
57
  return ensureApplyPatchSchema(base);
58
58
  }
59
+ if (normalizedName === 'web_search') {
60
+ // For web_search we currently accept any incoming schema and fall back to a
61
+ // minimal object definition. Server-side web_search execution only relies
62
+ // on the function name + JSON arguments, so tool schema is best-effort.
63
+ const base = asSchema(candidate) ?? {};
64
+ if (!base.type) {
65
+ base.type = 'object';
66
+ }
67
+ if (!Object.prototype.hasOwnProperty.call(base, 'properties')) {
68
+ base.properties = {};
69
+ }
70
+ return base;
71
+ }
59
72
  return asSchema(candidate);
60
73
  }
61
74
  const DEFAULT_SANITIZER = (value) => {
@@ -111,10 +124,20 @@ export function bridgeToolToChatDefinition(rawTool, options) {
111
124
  }
112
125
  const tool = rawTool;
113
126
  const fnNode = tool.function && typeof tool.function === 'object' ? tool.function : undefined;
114
- const name = pickToolName([fnNode?.name, tool.name], options);
127
+ let name = pickToolName([fnNode?.name, tool.name], options);
128
+ // Special case for Responses builtin web_search tools:
129
+ // Codex / Claude‑code may send tools shaped as `{ type: "web_search", ... }`
130
+ // without a nested `function` node. Treat these as a canonical `web_search`
131
+ // function tool so downstream Chat / Standardized layers can reason over a
132
+ // single function-style web_search surface.
115
133
  if (!name) {
116
- return null;
134
+ const rawType = typeof tool.type === 'string' ? tool.type.trim().toLowerCase() : '';
135
+ if (rawType === 'web_search' || rawType.startsWith('web_search')) {
136
+ name = 'web_search';
137
+ }
117
138
  }
139
+ if (!name)
140
+ return null;
118
141
  const description = resolveToolDescription(fnNode?.description ?? tool.description);
119
142
  const parameters = enforceBuiltinToolSchema(name, resolveToolParameters(fnNode, tool));
120
143
  const strict = resolveToolStrict(fnNode, tool);
@@ -7,4 +7,5 @@
7
7
  export * from './conversion/index.js';
8
8
  export * from './router/virtual-router/bootstrap.js';
9
9
  export * from './router/virtual-router/types.js';
10
+ export * from './telemetry/stats-center.js';
10
11
  export declare const VERSION = "0.4.0";
@@ -7,4 +7,5 @@
7
7
  export * from './conversion/index.js';
8
8
  export * from './router/virtual-router/bootstrap.js';
9
9
  export * from './router/virtual-router/types.js';
10
+ export * from './telemetry/stats-center.js';
10
11
  export const VERSION = '0.4.0';
@@ -11,8 +11,7 @@ const DEFAULT_LOAD_BALANCING = { strategy: 'round-robin' };
11
11
  const DEFAULT_HEALTH = { failureThreshold: 3, cooldownMs: 30_000, fatalCooldownMs: 300_000 };
12
12
  const DEFAULT_CONTEXT_ROUTING = {
13
13
  warnRatio: 0.9,
14
- hardLimit: false,
15
- fallbackRoute: 'longcontext'
14
+ hardLimit: false
16
15
  };
17
16
  /**
18
17
  * 将用户提供的 Virtual Router 配置(或包含 virtualrouter 字段的整体配置)
@@ -28,6 +27,8 @@ export function bootstrapVirtualRouterConfig(input) {
28
27
  if (!Object.keys(routingSource).length) {
29
28
  throw new VirtualRouterError('Virtual Router routing table cannot be empty', VirtualRouterErrorCode.CONFIG_ERROR);
30
29
  }
30
+ const webSearch = normalizeWebSearch(section.webSearch, routingSource);
31
+ validateWebSearchRouting(webSearch, routingSource);
31
32
  const { runtimeEntries, aliasIndex } = buildProviderRuntimeEntries(providersSource);
32
33
  const { routing, targetKeys } = expandRoutingTable(routingSource, aliasIndex);
33
34
  if (!routing.default || routing.default.length === 0) {
@@ -44,7 +45,8 @@ export function bootstrapVirtualRouterConfig(input) {
44
45
  classifier,
45
46
  loadBalancing,
46
47
  health,
47
- contextRouting
48
+ contextRouting,
49
+ ...(webSearch ? { webSearch } : {})
48
50
  };
49
51
  return {
50
52
  config,
@@ -65,7 +67,8 @@ function extractVirtualRouterSection(input) {
65
67
  const loadBalancing = normalizeLoadBalancing(section.loadBalancing ?? root.loadBalancing);
66
68
  const health = normalizeHealth(section.health ?? root.health);
67
69
  const contextRouting = normalizeContextRouting(section.contextRouting ?? root.contextRouting);
68
- return { providers, routing, classifier, loadBalancing, health, contextRouting };
70
+ const webSearch = section.webSearch ?? root.webSearch;
71
+ return { providers, routing, classifier, loadBalancing, health, contextRouting, webSearch };
69
72
  }
70
73
  function buildProviderRuntimeEntries(providers) {
71
74
  const runtimeEntries = {};
@@ -95,6 +98,11 @@ function buildProviderRuntimeEntries(providers) {
95
98
  userInfoUrl: entry.auth.userInfoUrl,
96
99
  refreshUrl: entry.auth.refreshUrl
97
100
  };
101
+ // 为 OAuth 类型的 auth 设置 tokenFile 为 alias(如果没有显式配置 tokenFile)
102
+ // 这允许 oauth-lifecycle.ts 的 resolveTokenFilePath 函数正确解析并匹配现有文件
103
+ if (!runtimeAuth.tokenFile && (runtimeAuth.rawType?.includes('oauth') || runtimeAuth.type === 'oauth')) {
104
+ runtimeAuth.tokenFile = entry.keyAlias;
105
+ }
98
106
  if (runtimeAuth.type === 'apiKey' && !runtimeAuth.secretRef) {
99
107
  runtimeAuth.secretRef = `${providerId}.${entry.keyAlias}`;
100
108
  }
@@ -113,7 +121,8 @@ function buildProviderRuntimeEntries(providers) {
113
121
  streaming: normalizedProvider.streaming,
114
122
  modelStreaming: normalizedProvider.modelStreaming,
115
123
  modelContextTokens: normalizedProvider.modelContextTokens,
116
- defaultContextTokens: normalizedProvider.defaultContextTokens
124
+ defaultContextTokens: normalizedProvider.defaultContextTokens,
125
+ ...(normalizedProvider.serverToolsDisabled ? { serverToolsDisabled: true } : {})
117
126
  };
118
127
  }
119
128
  }
@@ -122,28 +131,40 @@ function buildProviderRuntimeEntries(providers) {
122
131
  function expandRoutingTable(routingSource, aliasIndex) {
123
132
  const routing = {};
124
133
  const targetKeys = new Set();
125
- for (const [routeName, entries] of Object.entries(routingSource)) {
126
- const expanded = [];
127
- for (const entry of entries) {
128
- const parsed = parseRouteEntry(entry, aliasIndex);
129
- if (!parsed) {
130
- continue;
131
- }
132
- if (!aliasIndex.has(parsed.providerId)) {
133
- throw new VirtualRouterError(`Route "${routeName}" references unknown provider "${parsed.providerId}"`, VirtualRouterErrorCode.CONFIG_ERROR);
134
+ for (const [routeName, pools] of Object.entries(routingSource)) {
135
+ const expandedPools = [];
136
+ for (const pool of pools) {
137
+ const expandedTargets = [];
138
+ for (const entry of pool.targets) {
139
+ const parsed = parseRouteEntry(entry, aliasIndex);
140
+ if (!parsed) {
141
+ continue;
142
+ }
143
+ if (!aliasIndex.has(parsed.providerId)) {
144
+ throw new VirtualRouterError(`Route "${routeName}" references unknown provider "${parsed.providerId}"`, VirtualRouterErrorCode.CONFIG_ERROR);
145
+ }
146
+ const aliases = parsed.keyAlias ? [parsed.keyAlias] : aliasIndex.get(parsed.providerId);
147
+ if (!aliases.length) {
148
+ throw new VirtualRouterError(`Provider ${parsed.providerId} has no auth aliases but is referenced in routing`, VirtualRouterErrorCode.CONFIG_ERROR);
149
+ }
150
+ for (const alias of aliases) {
151
+ const runtimeKey = buildRuntimeKey(parsed.providerId, alias);
152
+ const targetKey = `${runtimeKey}.${parsed.modelId}`;
153
+ pushUnique(expandedTargets, targetKey);
154
+ targetKeys.add(targetKey);
155
+ }
134
156
  }
135
- const aliases = parsed.keyAlias ? [parsed.keyAlias] : aliasIndex.get(parsed.providerId);
136
- if (!aliases.length) {
137
- throw new VirtualRouterError(`Provider ${parsed.providerId} has no auth aliases but is referenced in routing`, VirtualRouterErrorCode.CONFIG_ERROR);
138
- }
139
- for (const alias of aliases) {
140
- const runtimeKey = buildRuntimeKey(parsed.providerId, alias);
141
- const targetKey = `${runtimeKey}.${parsed.modelId}`;
142
- pushUnique(expanded, targetKey);
143
- targetKeys.add(targetKey);
157
+ if (expandedTargets.length) {
158
+ expandedPools.push({
159
+ id: pool.id,
160
+ priority: pool.priority,
161
+ backup: pool.backup,
162
+ targets: expandedTargets,
163
+ ...(pool.force ? { force: true } : {})
164
+ });
144
165
  }
145
166
  }
146
- routing[routeName] = expanded;
167
+ routing[routeName] = expandedPools;
147
168
  }
148
169
  return { routing, targetKeys };
149
170
  }
@@ -175,7 +196,8 @@ function buildProviderProfiles(targetKeys, runtimeEntries) {
175
196
  processMode: runtime.processMode || 'chat',
176
197
  responsesConfig: runtime.responsesConfig,
177
198
  streaming: streamingPref,
178
- maxContextTokens: contextTokens
199
+ maxContextTokens: contextTokens,
200
+ ...(runtime.serverToolsDisabled ? { serverToolsDisabled: true } : {})
179
201
  };
180
202
  targetRuntime[targetKey] = {
181
203
  ...runtime,
@@ -200,15 +222,138 @@ function resolveContextTokens(runtime, modelId) {
200
222
  function normalizeRouting(source) {
201
223
  const routing = {};
202
224
  for (const [routeName, entries] of Object.entries(source)) {
203
- if (!Array.isArray(entries))
225
+ if (!Array.isArray(entries) || !entries.length) {
226
+ routing[routeName] = [];
204
227
  continue;
205
- const normalized = entries
206
- .map((entry) => (typeof entry === 'string' ? entry.trim() : ''))
207
- .filter(Boolean);
208
- routing[routeName] = Array.from(new Set(normalized));
228
+ }
229
+ const allStrings = entries.every((entry) => typeof entry === 'string' || entry === null || entry === undefined);
230
+ if (allStrings) {
231
+ const targets = normalizeTargetList(entries);
232
+ routing[routeName] = targets.length ? [buildLegacyRoutePool(routeName, targets)] : [];
233
+ continue;
234
+ }
235
+ const normalized = [];
236
+ const total = entries.length || 1;
237
+ for (let index = 0; index < entries.length; index += 1) {
238
+ const entry = entries[index];
239
+ const pool = normalizeRoutePoolEntry(routeName, entry, index, total);
240
+ if (pool && pool.targets.length) {
241
+ normalized.push(pool);
242
+ }
243
+ }
244
+ routing[routeName] = normalized;
209
245
  }
210
246
  return routing;
211
247
  }
248
+ function buildLegacyRoutePool(routeName, targets) {
249
+ return {
250
+ id: `${routeName}:pool0`,
251
+ priority: targets.length,
252
+ backup: false,
253
+ targets
254
+ };
255
+ }
256
+ function normalizeRoutePoolEntry(routeName, entry, index, total) {
257
+ if (typeof entry === 'string') {
258
+ const targets = normalizeTargetList(entry);
259
+ return targets.length
260
+ ? {
261
+ id: `${routeName}:pool${index + 1}`,
262
+ priority: total - index,
263
+ backup: false,
264
+ targets
265
+ }
266
+ : null;
267
+ }
268
+ if (!entry || typeof entry !== 'object') {
269
+ return null;
270
+ }
271
+ const record = entry;
272
+ const id = readOptionalString(record.id) ??
273
+ readOptionalString(record?.poolId) ??
274
+ `${routeName}:pool${index + 1}`;
275
+ const backup = record.backup === true ||
276
+ record.isBackup === true ||
277
+ (typeof record.type === 'string' && record.type.toLowerCase() === 'backup');
278
+ const priority = normalizePriorityValue(record.priority, total - index);
279
+ const targets = normalizeRouteTargets(record);
280
+ const force = record.force === true ||
281
+ (typeof record.force === 'string' && record.force.trim().toLowerCase() === 'true');
282
+ return targets.length
283
+ ? {
284
+ id,
285
+ priority,
286
+ backup,
287
+ targets,
288
+ ...(force ? { force: true } : {})
289
+ }
290
+ : null;
291
+ }
292
+ function normalizeRouteTargets(record) {
293
+ const buckets = [
294
+ record.targets,
295
+ record.providers,
296
+ record.pool,
297
+ record.entries,
298
+ record.items,
299
+ record.routes
300
+ ];
301
+ const normalized = [];
302
+ for (const bucket of buckets) {
303
+ for (const target of normalizeTargetList(bucket)) {
304
+ if (!normalized.includes(target)) {
305
+ normalized.push(target);
306
+ }
307
+ }
308
+ }
309
+ const singular = [record.target, record.provider];
310
+ for (const candidate of singular) {
311
+ for (const target of normalizeTargetList(candidate)) {
312
+ if (!normalized.includes(target)) {
313
+ normalized.push(target);
314
+ }
315
+ }
316
+ }
317
+ return normalized;
318
+ }
319
+ function normalizeTargetList(value) {
320
+ if (Array.isArray(value)) {
321
+ const normalized = [];
322
+ for (const entry of value) {
323
+ if (typeof entry === 'string') {
324
+ const trimmed = entry.trim();
325
+ if (trimmed && !normalized.includes(trimmed)) {
326
+ normalized.push(trimmed);
327
+ }
328
+ }
329
+ }
330
+ return normalized;
331
+ }
332
+ if (typeof value === 'string') {
333
+ const trimmed = value.trim();
334
+ return trimmed ? [trimmed] : [];
335
+ }
336
+ if (typeof value === 'number') {
337
+ const str = String(value).trim();
338
+ return str ? [str] : [];
339
+ }
340
+ return [];
341
+ }
342
+ function normalizePriorityValue(value, fallback) {
343
+ if (typeof value === 'number' && Number.isFinite(value)) {
344
+ return value;
345
+ }
346
+ if (typeof value === 'string') {
347
+ const trimmed = value.trim();
348
+ if (trimmed) {
349
+ const parsed = Number(trimmed);
350
+ if (Number.isFinite(parsed)) {
351
+ return parsed;
352
+ }
353
+ }
354
+ }
355
+ return fallback;
356
+ }
212
357
  function normalizeClassifier(input) {
213
358
  const normalized = asRecord(input);
214
359
  const result = {
@@ -247,6 +392,12 @@ function normalizeProvider(providerId, raw) {
247
392
  const streaming = resolveProviderStreamingPreference(provider, responsesNode);
248
393
  const modelStreaming = normalizeModelStreaming(provider);
249
394
  const { modelContextTokens, defaultContextTokens } = normalizeModelContextTokens(provider);
395
+ const serverToolsDisabled = provider.serverToolsDisabled === true ||
396
+ (typeof provider.serverToolsDisabled === 'string' &&
397
+ provider.serverToolsDisabled.trim().toLowerCase() === 'true') ||
398
+ (provider.serverTools &&
399
+ typeof provider.serverTools === 'object' &&
400
+ provider.serverTools.enabled === false);
250
401
  return {
251
402
  providerId,
252
403
  providerType,
@@ -259,7 +410,8 @@ function normalizeProvider(providerId, raw) {
259
410
  streaming,
260
411
  modelStreaming,
261
412
  modelContextTokens,
262
- defaultContextTokens
413
+ defaultContextTokens,
414
+ ...(serverToolsDisabled ? { serverToolsDisabled: true } : {})
263
415
  };
264
416
  }
265
417
  function normalizeModelStreaming(provider) {
@@ -389,15 +541,110 @@ function normalizeContextRouting(input) {
389
541
  coerceRatio(record?.warn_ratio);
390
542
  const hardLimitCandidate = coerceBoolean(record.hardLimit) ??
391
543
  coerceBoolean(record?.hard_limit);
392
- const fallbackCandidate = readOptionalString(record.fallbackRoute) ??
393
- readOptionalString(record?.fallback_route);
394
544
  const warnRatio = clampWarnRatio(warnCandidate ?? DEFAULT_CONTEXT_ROUTING.warnRatio);
395
545
  const hardLimit = typeof hardLimitCandidate === 'boolean' ? hardLimitCandidate : DEFAULT_CONTEXT_ROUTING.hardLimit;
396
- const fallbackRoute = fallbackCandidate ?? DEFAULT_CONTEXT_ROUTING.fallbackRoute;
397
546
  return {
398
547
  warnRatio,
399
- hardLimit,
400
- fallbackRoute
548
+ hardLimit
549
+ };
550
+ }
551
+ function validateWebSearchRouting(webSearch, routingSource) {
552
+ if (!webSearch) {
553
+ return;
554
+ }
555
+ const routePools = routingSource['web_search'] ?? routingSource['search'];
556
+ if (!Array.isArray(routePools) || !routePools.length) {
557
+ throw new VirtualRouterError('Virtual Router webSearch.engines configured but routing.web_search (or search) route is missing or empty', VirtualRouterErrorCode.CONFIG_ERROR);
558
+ }
559
+ const targets = new Set();
560
+ for (const pool of routePools) {
561
+ if (!pool || !Array.isArray(pool.targets)) {
562
+ continue;
563
+ }
564
+ for (const target of pool.targets) {
565
+ if (typeof target === 'string' && target.trim()) {
566
+ targets.add(target.trim());
567
+ }
568
+ }
569
+ }
570
+ for (const engine of webSearch.engines) {
571
+ if (!targets.has(engine.providerKey)) {
572
+ throw new VirtualRouterError(`Virtual Router webSearch engine "${engine.id}" references providerKey "${engine.providerKey}" which is not present in routing.web_search/search`, VirtualRouterErrorCode.CONFIG_ERROR);
573
+ }
574
+ }
575
+ }
576
+ function normalizeWebSearch(input, routingSource) {
577
+ if (!input || typeof input !== 'object') {
578
+ return undefined;
579
+ }
580
+ const record = input;
581
+ const enginesNode = Array.isArray(record.engines) ? record.engines : [];
582
+ const engines = [];
583
+ for (const raw of enginesNode) {
584
+ if (!raw || typeof raw !== 'object') {
585
+ continue;
586
+ }
587
+ const node = raw;
588
+ const idRaw = node.id;
589
+ const providerKeyRaw = node.providerKey ?? node.provider ?? node.target;
590
+ const id = typeof idRaw === 'string' && idRaw.trim()
591
+ ? idRaw.trim()
592
+ : undefined;
593
+ const providerKey = typeof providerKeyRaw === 'string' && providerKeyRaw.trim()
594
+ ? providerKeyRaw.trim()
595
+ : undefined;
596
+ if (!id || !providerKey) {
597
+ continue;
598
+ }
599
+ const description = typeof node.description === 'string' && node.description.trim()
600
+ ? node.description.trim()
601
+ : undefined;
602
+ const isDefault = node.default === true ||
603
+ (typeof node.default === 'string' && node.default.trim().toLowerCase() === 'true');
604
+ const serverToolsDisabled = node.serverToolsDisabled === true ||
605
+ (typeof node.serverToolsDisabled === 'string' &&
606
+ node.serverToolsDisabled.trim().toLowerCase() === 'true') ||
607
+ (node.serverTools &&
608
+ typeof node.serverTools === 'object' &&
609
+ node.serverTools.enabled === false);
610
+ // Deduplicate by id; first wins, subsequent are ignored.
611
+ if (engines.some((engine) => engine.id === id)) {
612
+ continue;
613
+ }
614
+ engines.push({
615
+ id,
616
+ providerKey,
617
+ description,
618
+ default: isDefault,
619
+ ...(serverToolsDisabled ? { serverToolsDisabled: true } : {})
620
+ });
621
+ }
622
+ if (!engines.length) {
623
+ return undefined;
624
+ }
625
+ let injectPolicy;
626
+ let force;
627
+ const rawPolicy = record.injectPolicy ?? record?.inject_policy;
628
+ if (typeof rawPolicy === 'string') {
629
+ const normalized = rawPolicy.trim().toLowerCase();
630
+ if (normalized === 'always' || normalized === 'selective') {
631
+ injectPolicy = normalized;
632
+ }
633
+ }
634
+ if (record.force === true ||
635
+ (typeof record.force === 'string' && record.force.trim().toLowerCase() === 'true')) {
636
+ force = true;
637
+ }
638
+ else {
639
+ const webSearchPools = routingSource['web_search'] ?? routingSource['search'] ?? [];
640
+ if (Array.isArray(webSearchPools) && webSearchPools.some((pool) => pool.force)) {
641
+ force = true;
642
+ }
643
+ }
644
+ return {
645
+ engines,
646
+ injectPolicy: injectPolicy ?? 'selective',
647
+ ...(force ? { force } : {})
401
648
  };
402
649
  }
403
650
  function extractProviderAuthEntries(providerId, raw) {
@@ -531,19 +778,37 @@ function extractProviderAuthEntries(providerId, raw) {
531
778
  pushEntry(undefined, buildAuthCandidate(baseTypeSource, { value: apiKeyField.trim() }));
532
779
  }
533
780
  // 自动多 token 扫描:仅在未显式声明多 key、且为受支持的 OAuth 提供方时触发
534
- if (!entries.length && baseType === 'oauth') {
535
- const oauthProviderId = baseTypeInfo.oauthProviderId;
536
- if (oauthProviderId && MULTI_TOKEN_OAUTH_PROVIDERS.has(oauthProviderId)) {
537
- const tokenFiles = scanOAuthTokenFiles(oauthProviderId);
781
+ if (baseType === 'oauth') {
782
+ const scanCandidates = new Set();
783
+ const pushCandidate = (value) => {
784
+ if (typeof value === 'string' && value.trim()) {
785
+ scanCandidates.add(value.trim().toLowerCase());
786
+ }
787
+ };
788
+ pushCandidate(auth?.oauthProviderId);
789
+ pushCandidate(baseTypeInfo.oauthProviderId);
790
+ pushCandidate(providerId);
791
+ for (const candidate of scanCandidates) {
792
+ if (!MULTI_TOKEN_OAUTH_PROVIDERS.has(candidate)) {
793
+ continue;
794
+ }
795
+ const tokenFiles = scanOAuthTokenFiles(candidate);
796
+ if (!tokenFiles.length) {
797
+ continue;
798
+ }
799
+ const baseTypeAlias = baseTypeInfo.oauthProviderId?.toLowerCase();
538
800
  for (const match of tokenFiles) {
539
801
  const alias = match.alias && match.alias !== 'default'
540
802
  ? `${match.sequence}-${match.alias}`
541
803
  : String(match.sequence);
804
+ const typeHint = baseTypeSource && baseTypeAlias === candidate
805
+ ? baseTypeSource
806
+ : `${candidate}-oauth`;
542
807
  const authConfig = {
543
808
  ...defaults,
544
- type: baseTypeSource ?? `${oauthProviderId}-oauth`,
809
+ type: typeHint,
545
810
  tokenFile: match.filePath,
546
- oauthProviderId
811
+ oauthProviderId: candidate
547
812
  };
548
813
  pushEntry(alias, authConfig);
549
814
  }
@@ -712,7 +977,7 @@ function mergeScopes(primary, fallback) {
712
977
  }
713
978
  return merged.size ? Array.from(merged) : undefined;
714
979
  }
715
- const MULTI_TOKEN_OAUTH_PROVIDERS = new Set(['iflow']);
980
+ const MULTI_TOKEN_OAUTH_PROVIDERS = new Set(['iflow', 'qwen', 'gemini-cli', 'antigravity']);
716
981
  function interpretAuthType(value) {
717
982
  if (typeof value !== 'string') {
718
983
  return { type: 'apiKey' };
@@ -17,34 +17,28 @@ export class RoutingClassifier {
17
17
  const thinkingContinuation = lastToolCategory === 'read';
18
18
  const searchContinuation = lastToolCategory === 'search';
19
19
  const toolsContinuation = lastToolCategory === 'other';
20
- if (latestMessageFromUser) {
21
- const reasoning = 'thinking:user-input';
22
- const evaluations = {
23
- thinking: { triggered: true, reason: reasoning }
24
- };
25
- const candidates = this.ensureDefaultCandidate(['thinking']);
26
- return this.buildResult('thinking', reasoning, evaluations, candidates);
27
- }
20
+ // 用户输入优先级最高(仅次于 vision),确保每次新的用户输入都走 thinking 路由进行工具检查
21
+ // thinking_continuation 用于区分"工具轮次中的 read 类调用"与"用户新输入"
28
22
  const evaluationMap = {
29
23
  vision: {
30
- triggered: features.hasVisionTool && features.hasImageAttachment,
31
- reason: 'vision:requires-tool+image'
24
+ triggered: features.hasImageAttachment,
25
+ reason: 'vision:image-detected'
26
+ },
27
+ thinking: {
28
+ triggered: latestMessageFromUser,
29
+ reason: 'thinking:user-input'
32
30
  },
33
31
  longcontext: {
34
32
  triggered: reachedLongContext,
35
33
  reason: 'longcontext:token-threshold'
36
34
  },
37
- websearch: {
38
- triggered: features.hasWebTool || searchContinuation,
39
- reason: searchContinuation ? 'websearch:last-tool-search' : 'websearch:web-tools-detected'
40
- },
41
35
  coding: {
42
36
  triggered: codingContinuation,
43
37
  reason: 'coding:last-tool-write'
44
38
  },
45
- thinking: {
46
- triggered: thinkingContinuation || latestMessageFromUser,
47
- reason: thinkingContinuation ? 'thinking:last-tool-read' : 'thinking:user-input'
39
+ thinking_continuation: {
40
+ triggered: thinkingContinuation,
41
+ reason: 'thinking:last-tool-read'
48
42
  },
49
43
  tools: {
50
44
  triggered: toolsContinuation || features.hasTools || features.hasToolCallResponses,
@@ -16,6 +16,4 @@ export declare class ContextAdvisor {
16
16
  private hardLimit;
17
17
  configure(config?: VirtualRouterContextRoutingConfig | null): void;
18
18
  classify(pool: string[], estimatedTokens: number, resolveProfile: (key: string) => ProviderProfile): ContextAdvisorResult;
19
- prefersFallback(result: ContextAdvisorResult): boolean;
20
- allowsOverflow(): boolean;
21
19
  }
@@ -55,18 +55,6 @@ export class ContextAdvisor {
55
55
  allOverflow: safe.length === 0 && risky.length === 0 && overflow.length > 0
56
56
  };
57
57
  }
58
- prefersFallback(result) {
59
- if (result.safe.length > 0) {
60
- return false;
61
- }
62
- if (result.risky.length > 0) {
63
- return true;
64
- }
65
- return result.allOverflow;
66
- }
67
- allowsOverflow() {
68
- return !this.hardLimit;
69
- }
70
58
  }
71
59
  function clampWarnRatio(value) {
72
60
  if (!Number.isFinite(value)) {
@@ -11,6 +11,8 @@ export declare class VirtualRouterEngine {
11
11
  private routeStats;
12
12
  private readonly debug;
13
13
  private healthConfig;
14
+ private readonly statsCenter;
15
+ private webSearchForce;
14
16
  initialize(config: VirtualRouterConfig): void;
15
17
  route(request: StandardizedRequest | ProcessedRequest, metadata: RouterMetadataInput): {
16
18
  target: TargetMetadata;
@@ -27,24 +29,37 @@ export declare class VirtualRouterEngine {
27
29
  }>;
28
30
  health: import("./types.js").ProviderHealthState[];
29
31
  };
32
+ /**
33
+ * 将分类器产生的逻辑路由名直接归一化为配置中的路由键。
34
+ * 不再维护 "websearch" 之类的别名,调用方应显式使用 "web_search" 或 "search" 等实际路由名。
35
+ */
36
+ private normalizeRouteAlias;
30
37
  private validateConfig;
31
38
  private selectProvider;
39
+ private trySelectFromTier;
32
40
  private incrementRouteStat;
33
41
  private providerHealthConfig;
34
42
  private initializeRouteQueue;
35
- private resolveFallbackRoute;
36
- private maybeDeferToFallback;
37
43
  private buildContextCandidatePools;
38
44
  private describeAttempt;
39
45
  private resolveStickyKey;
40
46
  private mapProviderError;
41
47
  private deriveReason;
42
48
  private buildRouteCandidates;
49
+ private reorderForInlineVision;
50
+ private routeSupportsInlineVision;
43
51
  private sortByPriority;
44
52
  private routeWeight;
53
+ private routeHasForceFlag;
54
+ private routeHasTargets;
55
+ private hasPrimaryPool;
56
+ private sortRoutePools;
57
+ private flattenPoolTargets;
45
58
  private buildHitReason;
46
59
  private decorateWithDetail;
47
60
  private formatVirtualRouterHit;
48
61
  private resolveRouteColor;
49
62
  private describeContextUsage;
63
+ private describeTargetProvider;
64
+ private parseProviderKey;
50
65
  }