@jsonstudio/rcc 0.89.333 → 0.89.548

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 (191) hide show
  1. package/dist/build-info.js +3 -3
  2. package/dist/build-info.js.map +1 -1
  3. package/dist/cli.js +110 -1
  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 +20 -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 +337 -25
  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/base-provider.d.ts +2 -0
  27. package/dist/providers/core/runtime/base-provider.js +35 -1
  28. package/dist/providers/core/runtime/base-provider.js.map +1 -1
  29. package/dist/providers/core/runtime/gemini-cli-http-provider.js +87 -20
  30. package/dist/providers/core/runtime/gemini-cli-http-provider.js.map +1 -1
  31. package/dist/providers/core/runtime/http-request-executor.d.ts +1 -0
  32. package/dist/providers/core/runtime/http-request-executor.js +75 -1
  33. package/dist/providers/core/runtime/http-request-executor.js.map +1 -1
  34. package/dist/providers/core/runtime/http-transport-provider.d.ts +2 -0
  35. package/dist/providers/core/runtime/http-transport-provider.js +60 -2
  36. package/dist/providers/core/runtime/http-transport-provider.js.map +1 -1
  37. package/dist/providers/core/runtime/iflow-http-provider.d.ts +4 -0
  38. package/dist/providers/core/runtime/iflow-http-provider.js +28 -0
  39. package/dist/providers/core/runtime/iflow-http-provider.js.map +1 -1
  40. package/dist/providers/core/runtime/rate-limit-manager.d.ts +30 -0
  41. package/dist/providers/core/runtime/rate-limit-manager.js +136 -0
  42. package/dist/providers/core/runtime/rate-limit-manager.js.map +1 -0
  43. package/dist/providers/core/runtime/responses-provider.js +8 -3
  44. package/dist/providers/core/runtime/responses-provider.js.map +1 -1
  45. package/dist/providers/core/runtime/vision-debug-utils.d.ts +13 -0
  46. package/dist/providers/core/runtime/vision-debug-utils.js +114 -0
  47. package/dist/providers/core/runtime/vision-debug-utils.js.map +1 -0
  48. package/dist/providers/core/strategies/oauth-auth-code-flow.js +75 -26
  49. package/dist/providers/core/strategies/oauth-auth-code-flow.js.map +1 -1
  50. package/dist/providers/core/utils/http-client.js +2 -1
  51. package/dist/providers/core/utils/http-client.js.map +1 -1
  52. package/dist/providers/core/utils/provider-error-reporter.js +31 -5
  53. package/dist/providers/core/utils/provider-error-reporter.js.map +1 -1
  54. package/dist/providers/core/utils/provider-type-utils.js +1 -1
  55. package/dist/providers/core/utils/provider-type-utils.js.map +1 -1
  56. package/dist/providers/core/utils/snapshot-writer.d.ts +1 -1
  57. package/dist/providers/core/utils/snapshot-writer.js.map +1 -1
  58. package/dist/server/handlers/sse-dispatcher.js +22 -2
  59. package/dist/server/handlers/sse-dispatcher.js.map +1 -1
  60. package/dist/server/runtime/http-server/index.d.ts +9 -0
  61. package/dist/server/runtime/http-server/index.js +512 -144
  62. package/dist/server/runtime/http-server/index.js.map +1 -1
  63. package/dist/server/runtime/http-server/provider-utils.js +1 -1
  64. package/dist/server/runtime/http-server/provider-utils.js.map +1 -1
  65. package/dist/server/runtime/http-server/request-executor.d.ts +10 -0
  66. package/dist/server/runtime/http-server/request-executor.js +553 -159
  67. package/dist/server/runtime/http-server/request-executor.js.map +1 -1
  68. package/dist/server/runtime/http-server/routes.d.ts +5 -0
  69. package/dist/server/runtime/http-server/routes.js +29 -0
  70. package/dist/server/runtime/http-server/routes.js.map +1 -1
  71. package/dist/server/runtime/http-server/runtime-manager.js +33 -0
  72. package/dist/server/runtime/http-server/runtime-manager.js.map +1 -1
  73. package/dist/server/utils/utf8-chunk-buffer.d.ts +43 -0
  74. package/dist/server/utils/utf8-chunk-buffer.js +132 -0
  75. package/dist/server/utils/utf8-chunk-buffer.js.map +1 -0
  76. package/dist/token-daemon/history-store.d.ts +75 -0
  77. package/dist/token-daemon/history-store.js +207 -0
  78. package/dist/token-daemon/history-store.js.map +1 -0
  79. package/dist/token-daemon/index.d.ts +7 -0
  80. package/dist/token-daemon/index.js +336 -0
  81. package/dist/token-daemon/index.js.map +1 -0
  82. package/dist/token-daemon/server-utils.d.ts +33 -0
  83. package/dist/token-daemon/server-utils.js +155 -0
  84. package/dist/token-daemon/server-utils.js.map +1 -0
  85. package/dist/token-daemon/token-daemon.d.ts +23 -0
  86. package/dist/token-daemon/token-daemon.js +249 -0
  87. package/dist/token-daemon/token-daemon.js.map +1 -0
  88. package/dist/token-daemon/token-types.d.ts +44 -0
  89. package/dist/token-daemon/token-types.js +18 -0
  90. package/dist/token-daemon/token-types.js.map +1 -0
  91. package/dist/token-daemon/token-utils.d.ts +17 -0
  92. package/dist/token-daemon/token-utils.js +153 -0
  93. package/dist/token-daemon/token-utils.js.map +1 -0
  94. package/dist/token-portal/local-token-portal.d.ts +1 -0
  95. package/dist/token-portal/local-token-portal.js +89 -0
  96. package/dist/token-portal/local-token-portal.js.map +1 -0
  97. package/dist/token-portal/render.d.ts +10 -0
  98. package/dist/token-portal/render.js +56 -0
  99. package/dist/token-portal/render.js.map +1 -0
  100. package/dist/tools/semantic-replay.js +7 -6
  101. package/dist/tools/semantic-replay.js.map +1 -1
  102. package/dist/utils/error-handler-registry.d.ts +36 -0
  103. package/dist/utils/error-handler-registry.js +93 -7
  104. package/dist/utils/error-handler-registry.js.map +1 -1
  105. package/node_modules/@jsonstudio/llms/README.md +2 -0
  106. package/node_modules/@jsonstudio/llms/dist/conversion/codecs/gemini-openai-codec.js +137 -5
  107. package/node_modules/@jsonstudio/llms/dist/conversion/compat/actions/gemini-web-search.d.ts +17 -0
  108. package/node_modules/@jsonstudio/llms/dist/conversion/compat/actions/gemini-web-search.js +68 -0
  109. package/node_modules/@jsonstudio/llms/dist/conversion/compat/actions/glm-image-content.d.ts +2 -0
  110. package/node_modules/@jsonstudio/llms/dist/conversion/compat/actions/glm-image-content.js +83 -0
  111. package/node_modules/@jsonstudio/llms/dist/conversion/compat/actions/glm-vision-prompt.d.ts +11 -0
  112. package/node_modules/@jsonstudio/llms/dist/conversion/compat/actions/glm-vision-prompt.js +177 -0
  113. package/node_modules/@jsonstudio/llms/dist/conversion/compat/actions/glm-web-search.d.ts +2 -0
  114. package/node_modules/@jsonstudio/llms/dist/conversion/compat/actions/glm-web-search.js +63 -0
  115. package/node_modules/@jsonstudio/llms/dist/conversion/compat/actions/universal-shape-filter.js +11 -0
  116. package/node_modules/@jsonstudio/llms/dist/conversion/compat/profiles/chat-gemini.json +17 -0
  117. package/node_modules/@jsonstudio/llms/dist/conversion/compat/profiles/chat-glm.json +190 -181
  118. package/node_modules/@jsonstudio/llms/dist/conversion/compat/profiles/chat-iflow.json +195 -195
  119. package/node_modules/@jsonstudio/llms/dist/conversion/compat/profiles/chat-lmstudio.json +43 -43
  120. package/node_modules/@jsonstudio/llms/dist/conversion/compat/profiles/chat-qwen.json +20 -20
  121. package/node_modules/@jsonstudio/llms/dist/conversion/compat/profiles/responses-c4m.json +42 -42
  122. package/node_modules/@jsonstudio/llms/dist/conversion/config/sample-config.json +1 -1
  123. package/node_modules/@jsonstudio/llms/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +24 -0
  124. package/node_modules/@jsonstudio/llms/dist/conversion/hub/pipeline/compat/compat-types.d.ts +8 -0
  125. package/node_modules/@jsonstudio/llms/dist/conversion/hub/pipeline/hub-pipeline.js +39 -4
  126. package/node_modules/@jsonstudio/llms/dist/conversion/hub/pipeline/target-utils.js +6 -0
  127. package/node_modules/@jsonstudio/llms/dist/conversion/hub/process/chat-process.js +213 -1
  128. package/node_modules/@jsonstudio/llms/dist/conversion/hub/response/provider-response.d.ts +34 -0
  129. package/node_modules/@jsonstudio/llms/dist/conversion/hub/response/provider-response.js +84 -24
  130. package/node_modules/@jsonstudio/llms/dist/conversion/hub/response/server-side-tools.d.ts +26 -0
  131. package/node_modules/@jsonstudio/llms/dist/conversion/hub/response/server-side-tools.js +383 -0
  132. package/node_modules/@jsonstudio/llms/dist/conversion/hub/semantic-mappers/gemini-mapper.js +241 -14
  133. package/node_modules/@jsonstudio/llms/dist/conversion/hub/semantic-mappers/responses-mapper.js +17 -1
  134. package/node_modules/@jsonstudio/llms/dist/conversion/hub/standardized-bridge.js +14 -0
  135. package/node_modules/@jsonstudio/llms/dist/conversion/hub/types/standardized.d.ts +1 -0
  136. package/node_modules/@jsonstudio/llms/dist/conversion/responses/responses-openai-bridge.js +82 -3
  137. package/node_modules/@jsonstudio/llms/dist/conversion/shared/anthropic-message-utils.js +92 -3
  138. package/node_modules/@jsonstudio/llms/dist/conversion/shared/bridge-message-utils.js +137 -10
  139. package/node_modules/@jsonstudio/llms/dist/conversion/shared/responses-output-builder.js +43 -2
  140. package/node_modules/@jsonstudio/llms/dist/conversion/shared/snapshot-utils.js +17 -47
  141. package/node_modules/@jsonstudio/llms/dist/conversion/shared/tool-filter-pipeline.js +1 -0
  142. package/node_modules/@jsonstudio/llms/dist/conversion/shared/tool-mapping.js +25 -2
  143. package/node_modules/@jsonstudio/llms/dist/index.d.ts +1 -0
  144. package/node_modules/@jsonstudio/llms/dist/index.js +1 -0
  145. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/bootstrap.js +308 -43
  146. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/classifier.js +11 -17
  147. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/context-advisor.d.ts +0 -2
  148. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/context-advisor.js +0 -12
  149. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/engine.d.ts +17 -2
  150. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/engine.js +332 -95
  151. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/features.js +1 -1
  152. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/message-utils.js +36 -24
  153. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/provider-registry.js +2 -1
  154. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/token-counter.js +14 -3
  155. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/types.d.ts +66 -2
  156. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/types.js +2 -1
  157. package/node_modules/@jsonstudio/llms/dist/servertool/engine.d.ts +27 -0
  158. package/node_modules/@jsonstudio/llms/dist/servertool/engine.js +60 -0
  159. package/node_modules/@jsonstudio/llms/dist/servertool/flow-types.d.ts +40 -0
  160. package/node_modules/@jsonstudio/llms/dist/servertool/flow-types.js +1 -0
  161. package/node_modules/@jsonstudio/llms/dist/servertool/handlers/vision.d.ts +1 -0
  162. package/node_modules/@jsonstudio/llms/dist/servertool/handlers/vision.js +194 -0
  163. package/node_modules/@jsonstudio/llms/dist/servertool/handlers/web-search.d.ts +1 -0
  164. package/node_modules/@jsonstudio/llms/dist/servertool/handlers/web-search.js +638 -0
  165. package/node_modules/@jsonstudio/llms/dist/servertool/orchestration-types.d.ts +33 -0
  166. package/node_modules/@jsonstudio/llms/dist/servertool/orchestration-types.js +1 -0
  167. package/node_modules/@jsonstudio/llms/dist/servertool/registry.d.ts +18 -0
  168. package/node_modules/@jsonstudio/llms/dist/servertool/registry.js +27 -0
  169. package/node_modules/@jsonstudio/llms/dist/servertool/server-side-tools.d.ts +8 -0
  170. package/node_modules/@jsonstudio/llms/dist/servertool/server-side-tools.js +208 -0
  171. package/node_modules/@jsonstudio/llms/dist/servertool/types.d.ts +88 -0
  172. package/node_modules/@jsonstudio/llms/dist/servertool/types.js +1 -0
  173. package/node_modules/@jsonstudio/llms/dist/servertool/vision-tool.d.ts +2 -0
  174. package/node_modules/@jsonstudio/llms/dist/servertool/vision-tool.js +185 -0
  175. package/node_modules/@jsonstudio/llms/dist/sse/json-to-sse/event-generators/responses.js +15 -3
  176. package/node_modules/@jsonstudio/llms/dist/sse/sse-to-json/builders/response-builder.js +6 -3
  177. package/node_modules/@jsonstudio/llms/dist/sse/sse-to-json/gemini-sse-to-json-converter.js +27 -1
  178. package/node_modules/@jsonstudio/llms/dist/sse/types/gemini-types.d.ts +20 -1
  179. package/node_modules/@jsonstudio/llms/dist/sse/types/responses-types.js +1 -1
  180. package/node_modules/@jsonstudio/llms/dist/telemetry/stats-center.d.ts +73 -0
  181. package/node_modules/@jsonstudio/llms/dist/telemetry/stats-center.js +280 -0
  182. package/node_modules/@jsonstudio/llms/package.json +1 -1
  183. package/package.json +3 -2
  184. package/scripts/pack-mode.mjs +2 -1
  185. package/scripts/publish-rcc.mjs +20 -4
  186. package/scripts/test-iflow-web-search.mjs +141 -0
  187. package/scripts/test-iflow.mjs +93 -1
  188. package/scripts/tests/virtual-router-health.mjs +141 -6
  189. package/dist/tools/replay-request.d.ts +0 -0
  190. package/dist/tools/replay-request.js +0 -2
  191. package/dist/tools/replay-request.js.map +0 -1
@@ -37,30 +37,42 @@ export function detectExtendedThinkingKeyword(text) {
37
37
  export function detectImageAttachment(message) {
38
38
  if (!message)
39
39
  return false;
40
- if (!message.metadata || typeof message.metadata !== 'object') {
41
- return false;
42
- }
43
- const meta = message.metadata;
44
- const attachments = (meta.attachments ?? null);
45
- if (Array.isArray(attachments)) {
46
- return attachments.some((attachment) => {
47
- const candidate = attachment;
48
- const typeValue = typeof candidate.type === 'string' ? candidate.type.toLowerCase() : '';
49
- const urlValue = typeof candidate.url === 'string'
50
- ? candidate.url
51
- : typeof candidate.src === 'string'
52
- ? candidate.src
53
- : typeof candidate.image_url === 'string'
54
- ? candidate.image_url
55
- : typeof candidate.image_url?.url === 'string'
56
- ? candidate.image_url.url
57
- : '';
58
- return typeValue.includes('image') && urlValue.trim().length > 0;
59
- });
60
- }
61
- if (typeof meta.attachmentType === 'string' && meta.attachmentType.toLowerCase().includes('image')) {
62
- const urlCandidate = typeof meta.attachmentUrl === 'string' ? meta.attachmentUrl : '';
63
- return urlCandidate.trim().length > 0;
40
+ // 仅基于标准 Chat 语义判断是否携带图片:
41
+ // - content 为数组时查找 { type: 'image' | 'image_url' | 'input_image', ... } 块;
42
+ // - 不再依赖 metadata.attachments,也不再用纯文本关键字或剪贴板标记作为信号。
43
+ if (Array.isArray(message.content)) {
44
+ for (const part of message.content) {
45
+ if (!part || typeof part !== 'object') {
46
+ continue;
47
+ }
48
+ const record = part;
49
+ const typeValue = typeof record.type === 'string' ? record.type.toLowerCase() : '';
50
+ // For chat/standardized content, images may appear as:
51
+ // - { type: "image_url", image_url: { url } }
52
+ // - { type: "image", uri: "...", data: "...", url: "..." }
53
+ // - { type: "input_image", image_url: "data:..." }
54
+ // Treat any non-empty URL/URI/data on an image-* block as a signal.
55
+ let imageCandidate = '';
56
+ if (typeof record.image_url === 'string') {
57
+ imageCandidate = record.image_url ?? '';
58
+ }
59
+ else if (record.image_url &&
60
+ typeof record.image_url?.url === 'string') {
61
+ imageCandidate = record.image_url?.url ?? '';
62
+ }
63
+ else if (typeof record.url === 'string') {
64
+ imageCandidate = record.url ?? '';
65
+ }
66
+ else if (typeof record.uri === 'string') {
67
+ imageCandidate = record.uri ?? '';
68
+ }
69
+ else if (typeof record.data === 'string') {
70
+ imageCandidate = record.data ?? '';
71
+ }
72
+ if (typeValue.includes('image') && imageCandidate.trim().length > 0) {
73
+ return true;
74
+ }
75
+ }
64
76
  }
65
77
  return false;
66
78
  }
@@ -62,7 +62,8 @@ export class ProviderRegistry {
62
62
  processMode: profile.processMode || 'chat',
63
63
  responsesConfig: profile.responsesConfig,
64
64
  streaming: profile.streaming,
65
- maxContextTokens: profile.maxContextTokens
65
+ maxContextTokens: profile.maxContextTokens,
66
+ ...(profile.serverToolsDisabled ? { serverToolsDisabled: true } : {})
66
67
  };
67
68
  }
68
69
  }
@@ -78,11 +78,22 @@ function encodeContent(content, encoder) {
78
78
  total += encodeText(part, encoder);
79
79
  }
80
80
  else if (part && typeof part === 'object') {
81
- if (typeof part.text === 'string') {
82
- total += encodeText(part.text, encoder);
81
+ const record = part;
82
+ const typeValue = typeof record.type === 'string' ? record.type.toLowerCase() : '';
83
+ // Large binary/image payloads (data URIs, base64, etc.) should not
84
+ // dominate context estimation. For image-like blocks, only count a
85
+ // small textual placeholder instead of the full JSON/body.
86
+ if (typeValue.startsWith('image')) {
87
+ const caption = typeof record.caption === 'string' && record.caption.trim().length
88
+ ? record.caption
89
+ : '[image]';
90
+ total += encodeText(caption, encoder);
91
+ }
92
+ else if (typeof record.text === 'string') {
93
+ total += encodeText(record.text, encoder);
83
94
  }
84
95
  else {
85
- total += encodeText(JSON.stringify(part), encoder);
96
+ total += encodeText(JSON.stringify(record), encoder);
86
97
  }
87
98
  }
88
99
  }
@@ -5,7 +5,20 @@ 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 RoutingPools = Record<string, string[]>;
8
+ export interface RoutePoolTier {
9
+ id: string;
10
+ targets: string[];
11
+ priority: number;
12
+ backup?: boolean;
13
+ /**
14
+ * Optional force flag for this route pool.
15
+ * Currently interpreted for:
16
+ * - routing.vision: force dedicated vision backend handling.
17
+ * - routing.web_search / routing.search: force server-side web_search flow.
18
+ */
19
+ force?: boolean;
20
+ }
21
+ export type RoutingPools = Record<string, RoutePoolTier[]>;
9
22
  export type StreamingPreference = 'auto' | 'always' | 'never';
10
23
  export interface ProviderAuthConfig {
11
24
  type: 'apiKey' | 'oauth';
@@ -36,6 +49,13 @@ export interface ProviderProfile {
36
49
  responsesConfig?: ResponsesProviderConfig;
37
50
  streaming?: StreamingPreference;
38
51
  maxContextTokens?: number;
52
+ /**
53
+ * When true, this provider must be skipped for any request that
54
+ * requires server-side tool orchestration (e.g. web_search).
55
+ * Normal chat routing (without servertool injection) may still
56
+ * use this provider as usual.
57
+ */
58
+ serverToolsDisabled?: boolean;
39
59
  }
40
60
  export interface ProviderRuntimeProfile {
41
61
  runtimeKey: string;
@@ -55,6 +75,12 @@ export interface ProviderRuntimeProfile {
55
75
  modelContextTokens?: Record<string, number>;
56
76
  defaultContextTokens?: number;
57
77
  maxContextTokens?: number;
78
+ /**
79
+ * Provider-level flag propagated from virtualrouter.providers[*].
80
+ * When true, VirtualRouterEngine will skip this runtime for any
81
+ * request that declares serverToolRequired=true in routing metadata.
82
+ */
83
+ serverToolsDisabled?: boolean;
58
84
  }
59
85
  export interface VirtualRouterClassifierConfig {
60
86
  longContextThresholdTokens?: number;
@@ -72,6 +98,27 @@ export interface ProviderHealthConfig {
72
98
  cooldownMs: number;
73
99
  fatalCooldownMs?: number;
74
100
  }
101
+ export interface VirtualRouterWebSearchEngineConfig {
102
+ id: string;
103
+ providerKey: string;
104
+ description?: string;
105
+ default?: boolean;
106
+ /**
107
+ * When true, this engine will never be used by server-side tools
108
+ * (e.g. web_search). It will also be omitted from injected tool
109
+ * schemas so main models cannot select it for servertool flows.
110
+ */
111
+ serverToolsDisabled?: boolean;
112
+ }
113
+ export interface VirtualRouterWebSearchConfig {
114
+ engines: VirtualRouterWebSearchEngineConfig[];
115
+ injectPolicy?: 'always' | 'selective';
116
+ /**
117
+ * When true, always prefer server-side web_search orchestration
118
+ * over upstream builtin behaviours (e.g. OpenAI Responses builtin web_search).
119
+ */
120
+ force?: boolean;
121
+ }
75
122
  export interface VirtualRouterConfig {
76
123
  routing: RoutingPools;
77
124
  providers: Record<string, ProviderProfile>;
@@ -79,11 +126,11 @@ export interface VirtualRouterConfig {
79
126
  loadBalancing?: LoadBalancingPolicy;
80
127
  health?: ProviderHealthConfig;
81
128
  contextRouting?: VirtualRouterContextRoutingConfig;
129
+ webSearch?: VirtualRouterWebSearchConfig;
82
130
  }
83
131
  export interface VirtualRouterContextRoutingConfig {
84
132
  warnRatio: number;
85
133
  hardLimit?: boolean;
86
- fallbackRoute?: string;
87
134
  }
88
135
  export type VirtualRouterProviderDefinition = Record<string, unknown>;
89
136
  export interface VirtualRouterBootstrapInput extends Record<string, unknown> {
@@ -94,6 +141,7 @@ export interface VirtualRouterBootstrapInput extends Record<string, unknown> {
94
141
  loadBalancing?: LoadBalancingPolicy;
95
142
  health?: ProviderHealthConfig;
96
143
  contextRouting?: VirtualRouterContextRoutingConfig;
144
+ webSearch?: VirtualRouterWebSearchConfig | Record<string, unknown>;
97
145
  }
98
146
  export type ProviderRuntimeMap = Record<string, ProviderRuntimeProfile>;
99
147
  export interface VirtualRouterBootstrapResult {
@@ -112,6 +160,13 @@ export interface RouterMetadataInput {
112
160
  providerProtocol?: string;
113
161
  stage?: 'inbound' | 'outbound' | 'response';
114
162
  routeHint?: string;
163
+ /**
164
+ * Indicates that current routing decision is for a request which
165
+ * expects server-side tools orchestration (e.g. web_search).
166
+ * Virtual Router should skip providers that opt out via
167
+ * serverToolsDisabled when this flag is true.
168
+ */
169
+ serverToolRequired?: boolean;
115
170
  responsesResume?: {
116
171
  previousRequestId?: string;
117
172
  restoredFromResponseId?: string;
@@ -152,6 +207,7 @@ export interface RoutingDecision {
152
207
  reasoning: string;
153
208
  fallback: boolean;
154
209
  pool: string[];
210
+ poolId?: string;
155
211
  }
156
212
  export interface TargetMetadata {
157
213
  providerKey: string;
@@ -164,6 +220,13 @@ export interface TargetMetadata {
164
220
  responsesConfig?: ResponsesProviderConfig;
165
221
  streaming?: StreamingPreference;
166
222
  maxContextTokens?: number;
223
+ /**
224
+ * Route-level flags propagated from the virtual router.
225
+ * These are derived from routing pools and webSearch config and
226
+ * are used by hub pipeline/process layers (web_search / vision).
227
+ */
228
+ forceWebSearch?: boolean;
229
+ forceVision?: boolean;
167
230
  }
168
231
  export interface ResponsesProviderConfig {
169
232
  toolCallIdStyle?: 'fc' | 'preserve';
@@ -185,6 +248,7 @@ export interface RoutingDiagnostics {
185
248
  reasoning: string;
186
249
  fallback: boolean;
187
250
  pool: string[];
251
+ poolId?: string;
188
252
  confidence: number;
189
253
  }
190
254
  export interface RoutingStatusSnapshot {
@@ -6,7 +6,8 @@ export const DEFAULT_ROUTE = 'default';
6
6
  export const ROUTE_PRIORITY = [
7
7
  'vision',
8
8
  'longcontext',
9
- 'websearch',
9
+ 'web_search',
10
+ 'search',
10
11
  'coding',
11
12
  'thinking',
12
13
  'tools',
@@ -0,0 +1,27 @@
1
+ import type { AdapterContext } from '../conversion/hub/types/chat-envelope.js';
2
+ import type { JsonObject } from '../conversion/hub/types/json.js';
3
+ import type { ProviderInvoker } from './types.js';
4
+ export interface ServerToolOrchestrationOptions {
5
+ chat: JsonObject;
6
+ adapterContext: AdapterContext;
7
+ requestId: string;
8
+ entryEndpoint: string;
9
+ providerProtocol: string;
10
+ reenterPipeline?: (options: {
11
+ entryEndpoint: string;
12
+ requestId: string;
13
+ body: JsonObject;
14
+ metadata?: JsonObject;
15
+ }) => Promise<{
16
+ body?: JsonObject;
17
+ __sse_responses?: unknown;
18
+ format?: string;
19
+ }>;
20
+ providerInvoker?: ProviderInvoker;
21
+ }
22
+ export interface ServerToolOrchestrationResult {
23
+ chat: JsonObject;
24
+ executed: boolean;
25
+ flowId?: string;
26
+ }
27
+ export declare function runServerToolOrchestration(options: ServerToolOrchestrationOptions): Promise<ServerToolOrchestrationResult>;
@@ -0,0 +1,60 @@
1
+ import { runServerSideToolEngine } from './server-side-tools.js';
2
+ export async function runServerToolOrchestration(options) {
3
+ const engineOptions = {
4
+ chatResponse: options.chat,
5
+ adapterContext: options.adapterContext,
6
+ entryEndpoint: options.entryEndpoint,
7
+ requestId: options.requestId,
8
+ providerProtocol: options.providerProtocol,
9
+ providerInvoker: options.providerInvoker,
10
+ reenterPipeline: options.reenterPipeline
11
+ };
12
+ const engineResult = await runServerSideToolEngine(engineOptions);
13
+ if (engineResult.mode === 'passthrough' || !engineResult.execution) {
14
+ return {
15
+ chat: engineResult.finalChatResponse,
16
+ executed: false
17
+ };
18
+ }
19
+ if (!engineResult.execution.followup || !options.reenterPipeline) {
20
+ return {
21
+ chat: engineResult.finalChatResponse,
22
+ executed: true,
23
+ flowId: engineResult.execution.flowId
24
+ };
25
+ }
26
+ const routeHint = resolveRouteHint(options.adapterContext, engineResult.execution.flowId);
27
+ const metadata = {
28
+ serverToolFollowup: true,
29
+ stream: false,
30
+ ...(engineResult.execution.followup.metadata ?? {})
31
+ };
32
+ if (routeHint && typeof metadata.routeHint !== 'string') {
33
+ metadata.routeHint = routeHint;
34
+ }
35
+ const followup = await options.reenterPipeline({
36
+ entryEndpoint: '/v1/chat/completions',
37
+ requestId: `${options.requestId}${engineResult.execution.followup.requestIdSuffix}`,
38
+ body: engineResult.execution.followup.payload,
39
+ metadata
40
+ });
41
+ const followupBody = followup.body && typeof followup.body === 'object'
42
+ ? followup.body
43
+ : engineResult.finalChatResponse;
44
+ return {
45
+ chat: followupBody,
46
+ executed: true,
47
+ flowId: engineResult.execution.flowId
48
+ };
49
+ }
50
+ function resolveRouteHint(adapterContext, flowId) {
51
+ const rawRoute = adapterContext.routeId;
52
+ const routeId = typeof rawRoute === 'string' && rawRoute.trim() ? rawRoute.trim() : '';
53
+ if (!routeId) {
54
+ return undefined;
55
+ }
56
+ if (flowId && routeId.toLowerCase() === flowId.toLowerCase()) {
57
+ return undefined;
58
+ }
59
+ return routeId;
60
+ }
@@ -0,0 +1,40 @@
1
+ import type { JsonObject } from '../conversion/hub/types/json.js';
2
+ import type { ServerToolOrchestrationOptions, ServerToolOrchestrationResult } from './orchestration-types.js';
3
+ export interface ServerToolFlowContext {
4
+ options: ServerToolOrchestrationOptions;
5
+ baseChatResponse: JsonObject;
6
+ capturedChatRequest?: JsonObject;
7
+ routeId?: string;
8
+ cache: Record<string, unknown>;
9
+ }
10
+ export interface ServerToolHopResult {
11
+ body?: JsonObject;
12
+ __sse_responses?: unknown;
13
+ format?: string;
14
+ }
15
+ export interface ServerToolReenterCallOptions {
16
+ requestIdSuffix: string;
17
+ body: JsonObject;
18
+ entryEndpoint?: string;
19
+ metadata?: JsonObject;
20
+ }
21
+ export interface ServerToolProviderCallOptions {
22
+ requestIdSuffix: string;
23
+ providerKey: string;
24
+ providerProtocol: string;
25
+ entryEndpoint: string;
26
+ payload: JsonObject;
27
+ modelId?: string;
28
+ routeHint?: string;
29
+ }
30
+ export interface ServerToolFlowHelpers {
31
+ makeRequestId: (suffix: string) => string;
32
+ callReenterHop: (options: ServerToolReenterCallOptions) => Promise<ServerToolHopResult | null>;
33
+ callProviderHop: (options: ServerToolProviderCallOptions) => Promise<ServerToolHopResult | null>;
34
+ getRouteHintForFollowup: (exclude?: string) => string | undefined;
35
+ }
36
+ export interface ServerToolFlow {
37
+ id: string;
38
+ shouldRun: (context: ServerToolFlowContext) => Promise<boolean> | boolean;
39
+ run: (context: ServerToolFlowContext, helpers: ServerToolFlowHelpers) => Promise<ServerToolOrchestrationResult | null>;
40
+ }
@@ -0,0 +1,194 @@
1
+ import { registerServerToolHandler } from '../registry.js';
2
+ import { cloneJson, extractTextFromChatLike } from '../server-side-tools.js';
3
+ const FLOW_ID = 'vision_flow';
4
+ const handler = async (ctx) => {
5
+ if (!ctx.options.reenterPipeline) {
6
+ return null;
7
+ }
8
+ if (!shouldRunVisionFlow(ctx)) {
9
+ return null;
10
+ }
11
+ const captured = getCapturedRequest(ctx.adapterContext);
12
+ if (!captured) {
13
+ return null;
14
+ }
15
+ const analysisPayload = buildVisionAnalysisPayload(captured);
16
+ if (!analysisPayload) {
17
+ return null;
18
+ }
19
+ const visionResponse = await ctx.options.reenterPipeline({
20
+ entryEndpoint: '/v1/chat/completions',
21
+ requestId: `${ctx.options.requestId}:vision`,
22
+ body: analysisPayload,
23
+ metadata: {
24
+ routeHint: 'vision',
25
+ serverToolFollowup: true,
26
+ stream: false
27
+ }
28
+ });
29
+ const visionBody = visionResponse.body && typeof visionResponse.body === 'object'
30
+ ? visionResponse.body
31
+ : null;
32
+ if (!visionBody) {
33
+ return null;
34
+ }
35
+ const visionSummary = extractTextFromChatLike(visionBody);
36
+ if (!visionSummary) {
37
+ return null;
38
+ }
39
+ const followupPayload = buildVisionFollowupPayload(captured, visionSummary);
40
+ if (!followupPayload) {
41
+ return null;
42
+ }
43
+ const execution = {
44
+ flowId: FLOW_ID,
45
+ followup: {
46
+ requestIdSuffix: ':vision_followup',
47
+ payload: followupPayload,
48
+ metadata: buildFollowupMetadata(ctx.adapterContext, 'vision')
49
+ }
50
+ };
51
+ return {
52
+ chatResponse: ctx.base,
53
+ execution
54
+ };
55
+ };
56
+ registerServerToolHandler('vision_auto', handler, { trigger: 'auto' });
57
+ function shouldRunVisionFlow(ctx) {
58
+ const record = ctx.adapterContext;
59
+ const followupFlag = record.serverToolFollowup === true || record.serverToolFollowup === 'true';
60
+ if (followupFlag) {
61
+ return false;
62
+ }
63
+ return record.hasImageAttachment === true || record.hasImageAttachment === 'true';
64
+ }
65
+ function getCapturedRequest(adapterContext) {
66
+ if (!adapterContext || typeof adapterContext !== 'object') {
67
+ return null;
68
+ }
69
+ const captured = adapterContext.capturedChatRequest;
70
+ if (!captured || typeof captured !== 'object' || Array.isArray(captured)) {
71
+ return null;
72
+ }
73
+ return captured;
74
+ }
75
+ function buildVisionAnalysisPayload(source) {
76
+ if (!source || typeof source !== 'object') {
77
+ return null;
78
+ }
79
+ const payload = {};
80
+ if (typeof source.model === 'string' && source.model.trim()) {
81
+ payload.model = source.model.trim();
82
+ }
83
+ if (Array.isArray(source.messages)) {
84
+ payload.messages = cloneJson(source.messages);
85
+ }
86
+ else {
87
+ return null;
88
+ }
89
+ if (Array.isArray(source.tools) && source.tools.length) {
90
+ payload.tools = cloneJson(source.tools);
91
+ }
92
+ const parameters = source.parameters;
93
+ if (parameters && typeof parameters === 'object' && !Array.isArray(parameters)) {
94
+ const params = cloneJson(parameters);
95
+ Object.assign(payload, params);
96
+ }
97
+ return payload;
98
+ }
99
+ function buildVisionFollowupPayload(source, summary) {
100
+ if (!source || typeof source !== 'object') {
101
+ return null;
102
+ }
103
+ const payload = {};
104
+ if (typeof source.model === 'string' && source.model.trim()) {
105
+ payload.model = source.model.trim();
106
+ }
107
+ payload.messages = injectVisionSummary(source.messages, summary);
108
+ if (Array.isArray(source.tools) && source.tools.length) {
109
+ payload.tools = cloneJson(source.tools);
110
+ }
111
+ const parameters = source.parameters;
112
+ if (parameters && typeof parameters === 'object' && !Array.isArray(parameters)) {
113
+ const params = cloneJson(parameters);
114
+ Object.assign(payload, params);
115
+ }
116
+ return payload;
117
+ }
118
+ function injectVisionSummary(source, summary) {
119
+ const messages = Array.isArray(source) ? cloneJson(source) : [];
120
+ let injected = false;
121
+ for (const message of messages) {
122
+ if (!message || typeof message !== 'object')
123
+ continue;
124
+ const content = message.content;
125
+ if (!Array.isArray(content))
126
+ continue;
127
+ const nextParts = [];
128
+ let removed = false;
129
+ for (const part of content) {
130
+ if (part && typeof part === 'object') {
131
+ const typeValue = typeof part.type === 'string'
132
+ ? part.type.toLowerCase()
133
+ : '';
134
+ if (typeValue.includes('image')) {
135
+ removed = true;
136
+ continue;
137
+ }
138
+ }
139
+ nextParts.push(part);
140
+ }
141
+ if (removed) {
142
+ nextParts.push({
143
+ type: 'text',
144
+ text: `[Vision] ${summary}`
145
+ });
146
+ message.content = nextParts;
147
+ injected = true;
148
+ }
149
+ }
150
+ if (!injected) {
151
+ for (let i = messages.length - 1; i >= 0; i -= 1) {
152
+ const msg = messages[i];
153
+ if (!msg || typeof msg !== 'object')
154
+ continue;
155
+ const role = typeof msg.role === 'string'
156
+ ? msg.role.toLowerCase()
157
+ : '';
158
+ if (role !== 'user')
159
+ continue;
160
+ const content = msg.content;
161
+ if (Array.isArray(content)) {
162
+ content.push({
163
+ type: 'text',
164
+ text: `[Vision] ${summary}`
165
+ });
166
+ injected = true;
167
+ break;
168
+ }
169
+ if (typeof content === 'string' && content.length) {
170
+ msg.content = `${content}\n[Vision] ${summary}`;
171
+ }
172
+ else {
173
+ msg.content = `[Vision] ${summary}`;
174
+ }
175
+ injected = true;
176
+ break;
177
+ }
178
+ }
179
+ if (!injected) {
180
+ messages.push({
181
+ role: 'system',
182
+ content: `[Vision] ${summary}`
183
+ });
184
+ }
185
+ return messages;
186
+ }
187
+ function buildFollowupMetadata(adapterContext, toolName) {
188
+ const ctx = adapterContext && typeof adapterContext === 'object' ? adapterContext : null;
189
+ const routeId = ctx && typeof ctx.routeId === 'string' && ctx.routeId.trim() ? ctx.routeId.trim() : '';
190
+ if (!routeId || routeId.toLowerCase() === toolName.toLowerCase()) {
191
+ return undefined;
192
+ }
193
+ return { routeHint: routeId };
194
+ }