@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
@@ -0,0 +1,383 @@
1
+ import { buildOpenAIChatFromGeminiResponse } from '../../codecs/gemini-openai-codec.js';
2
+ function asObject(value) {
3
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
4
+ }
5
+ function getArray(value) {
6
+ return Array.isArray(value) ? value : [];
7
+ }
8
+ function extractToolCalls(chatResponse) {
9
+ const choices = getArray(chatResponse.choices);
10
+ const calls = [];
11
+ for (const choice of choices) {
12
+ const choiceObj = asObject(choice);
13
+ if (!choiceObj)
14
+ continue;
15
+ const message = asObject(choiceObj.message);
16
+ if (!message)
17
+ continue;
18
+ const toolCalls = getArray(message.tool_calls);
19
+ for (const raw of toolCalls) {
20
+ const tc = asObject(raw);
21
+ if (!tc)
22
+ continue;
23
+ const id = typeof tc.id === 'string' && tc.id.trim() ? tc.id.trim() : '';
24
+ const fn = asObject(tc.function);
25
+ const name = fn && typeof fn.name === 'string' && fn.name.trim() ? fn.name.trim() : '';
26
+ const args = fn && typeof fn.arguments === 'string' ? fn.arguments : '';
27
+ if (!id || !name)
28
+ continue;
29
+ calls.push({ id, name, arguments: args });
30
+ }
31
+ }
32
+ return calls;
33
+ }
34
+ function extractTextFromChatLike(payload) {
35
+ // 1) 解包常见包装层:data / response 节点
36
+ let current = payload;
37
+ const visited = new Set();
38
+ while (current && typeof current === 'object' && !Array.isArray(current) && !visited.has(current)) {
39
+ visited.add(current);
40
+ if (Array.isArray(current.choices) || Array.isArray(current.output)) {
41
+ break;
42
+ }
43
+ const data = current.data;
44
+ if (data && typeof data === 'object' && !Array.isArray(data)) {
45
+ current = data;
46
+ continue;
47
+ }
48
+ const response = current.response;
49
+ if (response && typeof response === 'object' && !Array.isArray(response)) {
50
+ current = response;
51
+ continue;
52
+ }
53
+ break;
54
+ }
55
+ // 2) 优先从 choices[].message.content 提取(OpenAI/GLM 兼容)
56
+ const choices = getArray(current.choices);
57
+ if (!choices.length)
58
+ return '';
59
+ const first = asObject(choices[0]);
60
+ if (!first)
61
+ return '';
62
+ const message = asObject(first.message);
63
+ if (!message)
64
+ return '';
65
+ const content = message.content;
66
+ if (typeof content === 'string')
67
+ return content.trim();
68
+ const parts = getArray(content);
69
+ const texts = [];
70
+ for (const part of parts) {
71
+ if (typeof part === 'string') {
72
+ texts.push(part);
73
+ }
74
+ else if (part && typeof part === 'object') {
75
+ const record = part;
76
+ if (typeof record.text === 'string') {
77
+ texts.push(record.text);
78
+ }
79
+ else if (typeof record.content === 'string') {
80
+ texts.push(record.content);
81
+ }
82
+ }
83
+ }
84
+ const joinedFromChoices = texts.join('\n').trim();
85
+ if (joinedFromChoices) {
86
+ return joinedFromChoices;
87
+ }
88
+ // 3) 回退:从 output[].content[] 中提取(部分 Responses/自定义后端)
89
+ const output = current.output;
90
+ if (Array.isArray(output)) {
91
+ const altTexts = [];
92
+ for (const entry of output) {
93
+ if (!entry || typeof entry !== 'object')
94
+ continue;
95
+ const blocks = entry.content;
96
+ const blockArray = Array.isArray(blocks) ? blocks : [];
97
+ for (const block of blockArray) {
98
+ if (!block || typeof block !== 'object')
99
+ continue;
100
+ const record = block;
101
+ if (typeof record.text === 'string') {
102
+ altTexts.push(record.text);
103
+ }
104
+ else if (typeof record.output_text === 'string') {
105
+ altTexts.push(record.output_text);
106
+ }
107
+ else if (typeof record.content === 'string') {
108
+ altTexts.push(record.content);
109
+ }
110
+ }
111
+ }
112
+ const joined = altTexts.join('\n').trim();
113
+ if (joined) {
114
+ return joined;
115
+ }
116
+ }
117
+ return '';
118
+ }
119
+ function getWebSearchConfig(ctx) {
120
+ const raw = ctx.webSearch;
121
+ const record = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : null;
122
+ if (!record)
123
+ return undefined;
124
+ const enginesRaw = Array.isArray(record.engines) ? record.engines : [];
125
+ const engines = [];
126
+ for (const entry of enginesRaw) {
127
+ const obj = entry && typeof entry === 'object' && !Array.isArray(entry) ? entry : null;
128
+ if (!obj)
129
+ continue;
130
+ const id = typeof obj.id === 'string' && obj.id.trim() ? obj.id.trim() : undefined;
131
+ const providerKey = typeof obj.providerKey === 'string' && obj.providerKey.trim()
132
+ ? obj.providerKey.trim()
133
+ : undefined;
134
+ if (!id || !providerKey)
135
+ continue;
136
+ engines.push({
137
+ id,
138
+ providerKey,
139
+ description: typeof obj.description === 'string' && obj.description.trim() ? obj.description.trim() : undefined,
140
+ default: obj.default === true
141
+ });
142
+ }
143
+ if (!engines.length) {
144
+ return undefined;
145
+ }
146
+ const config = { engines };
147
+ if (typeof record.injectPolicy === 'string') {
148
+ const val = String(record.injectPolicy).trim().toLowerCase();
149
+ if (val === 'always' || val === 'selective') {
150
+ config.injectPolicy = val;
151
+ }
152
+ }
153
+ return config;
154
+ }
155
+ function resolveWebSearchEngine(config, engineId) {
156
+ const trimmedId = engineId && typeof engineId === 'string' && engineId.trim() ? engineId.trim() : undefined;
157
+ if (trimmedId) {
158
+ const byId = config.engines.find((e) => e.id === trimmedId);
159
+ if (byId) {
160
+ return byId;
161
+ }
162
+ }
163
+ const byDefault = config.engines.find((e) => e.default);
164
+ if (byDefault) {
165
+ return byDefault;
166
+ }
167
+ if (config.engines.length === 1) {
168
+ return config.engines[0];
169
+ }
170
+ return undefined;
171
+ }
172
+ function isGeminiWebSearchEngine(engine) {
173
+ const key = engine.providerKey.toLowerCase();
174
+ return key.startsWith('gemini-cli.') || key.startsWith('antigravity.');
175
+ }
176
+ function logServerToolWebSearch(engine, requestId, query) {
177
+ const providerAlias = engine.providerKey.split('.')[0] || engine.providerKey;
178
+ const backendLabel = `${providerAlias}:${engine.id}`;
179
+ const prefix = `[server-tool][web_search][${backendLabel}]`;
180
+ const line = `${prefix} requestId=${requestId} query=${JSON.stringify(query)}`;
181
+ // 深蓝色输出
182
+ // eslint-disable-next-line no-console
183
+ console.log(`\x1b[38;5;27m${line}\x1b[0m`);
184
+ }
185
+ function resolveEnvServerSideToolsEnabled() {
186
+ const raw = (process.env.ROUTECODEX_SERVER_SIDE_TOOLS || process.env.RCC_SERVER_SIDE_TOOLS || '').trim().toLowerCase();
187
+ if (!raw)
188
+ return false;
189
+ if (raw === '1' || raw === 'true' || raw === 'yes')
190
+ return true;
191
+ if (raw === 'web_search')
192
+ return true;
193
+ return false;
194
+ }
195
+ export async function runServerSideToolEngine(options) {
196
+ const base = asObject(options.chatResponse);
197
+ if (!base) {
198
+ return { mode: 'passthrough', finalChatResponse: options.chatResponse };
199
+ }
200
+ // 内建 OpenAI Responses `/v1/responses` 已经支持 server-side web_search。
201
+ // 仅当“入口端点为 /v1/responses 且 providerProtocol 也是 openai-responses”时保持透传,
202
+ // 避免对 Responses→Responses 的链路重复执行搜索回环;
203
+ // 其它场景(例如 /v1/responses → gemini/glm 后端)仍允许 server-side web_search 生效。
204
+ const entry = (options.entryEndpoint || '').toLowerCase();
205
+ if (options.providerProtocol === 'openai-responses' && entry.includes('/v1/responses')) {
206
+ return { mode: 'passthrough', finalChatResponse: base };
207
+ }
208
+ // Feature flag: keep behaviour fully backwards-compatible unless explicitly enabled.
209
+ const toolsEnabled = resolveEnvServerSideToolsEnabled();
210
+ const toolCalls = extractToolCalls(base);
211
+ const webSearchCall = toolCalls.find((tc) => tc.name === 'web_search');
212
+ if (!toolsEnabled || !webSearchCall || !options.providerInvoker) {
213
+ return { mode: 'passthrough', finalChatResponse: base };
214
+ }
215
+ const webSearchConfig = getWebSearchConfig(options.adapterContext);
216
+ if (!webSearchConfig) {
217
+ return { mode: 'passthrough', finalChatResponse: base };
218
+ }
219
+ let parsedArgs = {};
220
+ if (webSearchCall.arguments && typeof webSearchCall.arguments === 'string') {
221
+ try {
222
+ parsedArgs = JSON.parse(webSearchCall.arguments);
223
+ }
224
+ catch {
225
+ parsedArgs = {};
226
+ }
227
+ }
228
+ const query = typeof parsedArgs?.query === 'string' && parsedArgs.query.trim()
229
+ ? parsedArgs.query.trim()
230
+ : undefined;
231
+ const engineId = typeof parsedArgs?.engine === 'string' && parsedArgs.engine.trim()
232
+ ? parsedArgs.engine.trim()
233
+ : undefined;
234
+ const recency = typeof parsedArgs?.recency === 'string' && parsedArgs.recency.trim()
235
+ ? parsedArgs.recency.trim()
236
+ : undefined;
237
+ const count = typeof parsedArgs?.count === 'number' && Number.isFinite(parsedArgs.count)
238
+ ? Math.floor(parsedArgs.count)
239
+ : undefined;
240
+ if (!query) {
241
+ return { mode: 'passthrough', finalChatResponse: base };
242
+ }
243
+ const engine = resolveWebSearchEngine(webSearchConfig, engineId);
244
+ if (!engine) {
245
+ return { mode: 'passthrough', finalChatResponse: base };
246
+ }
247
+ const providerInvoker = options.providerInvoker;
248
+ // 1) Call search backend (secondary request)
249
+ let searchSummary = '';
250
+ try {
251
+ logServerToolWebSearch(engine, options.requestId, query);
252
+ if (isGeminiWebSearchEngine(engine)) {
253
+ const geminiPayload = {
254
+ model: engine.id,
255
+ contents: [
256
+ {
257
+ role: 'user',
258
+ parts: [
259
+ {
260
+ text: query
261
+ }
262
+ ]
263
+ }
264
+ ],
265
+ // Cloud Code search models treat googleSearch as the web search tool.
266
+ tools: [
267
+ {
268
+ googleSearch: {}
269
+ }
270
+ ]
271
+ };
272
+ const backend = await providerInvoker({
273
+ providerKey: engine.providerKey,
274
+ providerType: undefined,
275
+ modelId: engine.id,
276
+ providerProtocol: 'gemini-chat',
277
+ payload: geminiPayload,
278
+ entryEndpoint: '/v1/models/gemini:generateContent',
279
+ requestId: `${options.requestId}:web_search:${engine.id}`
280
+ });
281
+ const backendResponse = asObject(backend.providerResponse);
282
+ if (backendResponse) {
283
+ const chatLike = asObject(buildOpenAIChatFromGeminiResponse(backendResponse));
284
+ if (chatLike) {
285
+ searchSummary = extractTextFromChatLike(chatLike);
286
+ }
287
+ }
288
+ }
289
+ else {
290
+ const backendPayload = {
291
+ model: engine.providerKey,
292
+ messages: [
293
+ {
294
+ role: 'system',
295
+ content: 'You are a web search engine. Answer with up-to-date information based on the open internet.'
296
+ },
297
+ {
298
+ role: 'user',
299
+ content: query
300
+ }
301
+ ],
302
+ stream: false,
303
+ web_search: {
304
+ query,
305
+ ...(recency ? { recency } : {}),
306
+ ...(typeof count === 'number' ? { count } : {}),
307
+ engine: engine.id
308
+ }
309
+ };
310
+ const backend = await providerInvoker({
311
+ providerKey: engine.providerKey,
312
+ providerType: undefined,
313
+ modelId: undefined,
314
+ providerProtocol: options.providerProtocol,
315
+ payload: backendPayload,
316
+ entryEndpoint: '/v1/chat/completions',
317
+ requestId: `${options.requestId}:web_search:${engine.id}`
318
+ });
319
+ const backendResponse = asObject(backend.providerResponse);
320
+ if (backendResponse) {
321
+ searchSummary = extractTextFromChatLike(backendResponse);
322
+ }
323
+ }
324
+ }
325
+ catch {
326
+ // fall back to passthrough if backend fails
327
+ return { mode: 'passthrough', finalChatResponse: base };
328
+ }
329
+ if (!searchSummary) {
330
+ return { mode: 'passthrough', finalChatResponse: base };
331
+ }
332
+ // 2) Call main provider again with synthesized prompt (third request)
333
+ const ctxTarget = options.adapterContext.target;
334
+ const target = ctxTarget && typeof ctxTarget === 'object' && !Array.isArray(ctxTarget)
335
+ ? ctxTarget
336
+ : undefined;
337
+ const mainProviderKey = typeof target?.providerKey === 'string' && target.providerKey.trim()
338
+ ? target.providerKey.trim()
339
+ : undefined;
340
+ const mainModelId = (typeof base.model === 'string' && base.model.trim()
341
+ ? base.model.trim()
342
+ : typeof target?.modelId === 'string' && target.modelId.trim()
343
+ ? target.modelId.trim()
344
+ : undefined) ?? 'gpt-4o-mini';
345
+ if (!mainProviderKey) {
346
+ return { mode: 'passthrough', finalChatResponse: base };
347
+ }
348
+ let finalResponse = base;
349
+ try {
350
+ const followupPayload = {
351
+ model: mainModelId,
352
+ messages: [
353
+ {
354
+ role: 'system',
355
+ content: 'You are an assistant that answers using the provided web_search results. Use them as the primary source.'
356
+ },
357
+ {
358
+ role: 'user',
359
+ content: `User question: ${query}\n\nWeb search results:\n${searchSummary}`
360
+ }
361
+ ],
362
+ stream: false
363
+ };
364
+ const followup = await providerInvoker({
365
+ providerKey: mainProviderKey,
366
+ providerType: undefined,
367
+ modelId: mainModelId,
368
+ providerProtocol: options.providerProtocol,
369
+ payload: followupPayload,
370
+ entryEndpoint: '/v1/chat/completions',
371
+ requestId: `${options.requestId}:web_search_followup`
372
+ });
373
+ const followupResponse = asObject(followup.providerResponse);
374
+ if (followupResponse) {
375
+ finalResponse = followupResponse;
376
+ }
377
+ }
378
+ catch {
379
+ // If follow-up fails, keep original base response to avoid breaking clients.
380
+ finalResponse = base;
381
+ }
382
+ return { mode: 'web_search_flow', finalChatResponse: finalResponse };
383
+ }
@@ -17,6 +17,31 @@ const GENERATION_CONFIG_KEYS = [
17
17
  ];
18
18
  const PASSTHROUGH_METADATA_PREFIX = 'rcc_passthrough_';
19
19
  const PASSTHROUGH_PARAMETERS = ['tool_choice'];
20
+ const DUMMY_THOUGHT_SIGNATURE = 'skip_thought_signature_validator';
21
+ function coerceThoughtSignature(value) {
22
+ if (typeof value === 'string' && value.trim().length) {
23
+ return value.trim();
24
+ }
25
+ return undefined;
26
+ }
27
+ function extractThoughtSignatureFromToolCall(tc) {
28
+ if (!tc || typeof tc !== 'object') {
29
+ return undefined;
30
+ }
31
+ const node = tc;
32
+ const direct = coerceThoughtSignature(node.thought_signature ?? node.thoughtSignature);
33
+ if (direct) {
34
+ return direct;
35
+ }
36
+ const extra = node.extra_content ?? node.extraContent;
37
+ if (extra && typeof extra === 'object') {
38
+ const googleNode = extra.google ?? extra.Google;
39
+ if (googleNode && typeof googleNode === 'object') {
40
+ return coerceThoughtSignature(googleNode.thought_signature ?? googleNode.thoughtSignature);
41
+ }
42
+ }
43
+ return undefined;
44
+ }
20
45
  function normalizeToolOutputs(messages, missing) {
21
46
  const outputs = [];
22
47
  messages.forEach((msg, index) => {
@@ -35,6 +60,37 @@ function normalizeToolOutputs(messages, missing) {
35
60
  });
36
61
  return outputs.length ? outputs : undefined;
37
62
  }
63
+ function synthesizeToolOutputsFromMessages(messages) {
64
+ if (!Array.isArray(messages)) {
65
+ return [];
66
+ }
67
+ const outputs = [];
68
+ for (const message of messages) {
69
+ if (!message || typeof message !== 'object')
70
+ continue;
71
+ if (message.role !== 'assistant')
72
+ continue;
73
+ const toolCalls = Array.isArray(message.tool_calls)
74
+ ? message.tool_calls
75
+ : [];
76
+ for (const call of toolCalls) {
77
+ const callId = typeof call.id === 'string' ? call.id : undefined;
78
+ if (!callId) {
79
+ continue;
80
+ }
81
+ const existing = outputs.find((entry) => entry.tool_call_id === callId);
82
+ if (existing) {
83
+ continue;
84
+ }
85
+ outputs.push({
86
+ tool_call_id: callId,
87
+ content: '',
88
+ name: (call.function && call.function.name) || undefined
89
+ });
90
+ }
91
+ }
92
+ return outputs;
93
+ }
38
94
  function normalizeToolContent(value) {
39
95
  if (typeof value === 'string')
40
96
  return value;
@@ -47,6 +103,30 @@ function normalizeToolContent(value) {
47
103
  return String(value ?? '');
48
104
  }
49
105
  }
106
+ function convertToolMessageToOutput(message) {
107
+ const rawId = (message.tool_call_id ?? message.id);
108
+ const callId = typeof rawId === 'string' && rawId.trim().length ? rawId.trim() : undefined;
109
+ if (!callId) {
110
+ return null;
111
+ }
112
+ return {
113
+ tool_call_id: callId,
114
+ content: normalizeToolContent(message.content),
115
+ name: typeof message.name === 'string' ? message.name : undefined
116
+ };
117
+ }
118
+ function buildFunctionResponseEntry(output) {
119
+ const parsedPayload = safeParseJson(output.content);
120
+ const normalizedPayload = ensureFunctionResponsePayload(cloneAsJsonValue(parsedPayload));
121
+ const part = {
122
+ functionResponse: {
123
+ name: output.name || 'tool',
124
+ id: output.tool_call_id,
125
+ response: normalizedPayload
126
+ }
127
+ };
128
+ return { role: 'user', parts: [part] };
129
+ }
50
130
  function collectSystemSegments(systemInstruction) {
51
131
  if (!systemInstruction)
52
132
  return [];
@@ -91,22 +171,138 @@ function collectParameters(payload) {
91
171
  }
92
172
  return Object.keys(params).length ? params : undefined;
93
173
  }
174
+ function appendChatContentToGeminiParts(message, targetParts) {
175
+ const content = message.content;
176
+ if (typeof content === 'string') {
177
+ const text = content.trim();
178
+ if (text.length) {
179
+ targetParts.push({ text });
180
+ }
181
+ return;
182
+ }
183
+ if (!Array.isArray(content)) {
184
+ return;
185
+ }
186
+ const items = content;
187
+ for (const block of items) {
188
+ if (block == null)
189
+ continue;
190
+ if (typeof block === 'string') {
191
+ const text = block.trim();
192
+ if (text.length) {
193
+ targetParts.push({ text });
194
+ }
195
+ continue;
196
+ }
197
+ if (typeof block !== 'object') {
198
+ const text = String(block);
199
+ if (text.trim().length) {
200
+ targetParts.push({ text: text.trim() });
201
+ }
202
+ continue;
203
+ }
204
+ const record = block;
205
+ const rawType = record.type;
206
+ const type = typeof rawType === 'string' ? rawType.toLowerCase() : '';
207
+ // Text-style blocks
208
+ if (!type || type === 'text') {
209
+ const textValue = typeof record.text === 'string'
210
+ ? record.text
211
+ : typeof record.content === 'string'
212
+ ? record.content
213
+ : '';
214
+ const text = textValue.trim();
215
+ if (text.length) {
216
+ targetParts.push({ text });
217
+ }
218
+ continue;
219
+ }
220
+ // Image-style blocks -> Gemini inlineData
221
+ if (type === 'image' || type === 'image_url') {
222
+ // Prefer OpenAI-style image_url.url, but also accept uri/url/data.
223
+ let url;
224
+ const imageUrlRaw = record.image_url;
225
+ if (typeof imageUrlRaw === 'string') {
226
+ url = imageUrlRaw;
227
+ }
228
+ else if (imageUrlRaw && typeof imageUrlRaw === 'object' && typeof imageUrlRaw.url === 'string') {
229
+ url = imageUrlRaw.url;
230
+ }
231
+ else if (typeof record.uri === 'string') {
232
+ url = record.uri;
233
+ }
234
+ else if (typeof record.url === 'string') {
235
+ url = record.url;
236
+ }
237
+ else if (typeof record.data === 'string') {
238
+ url = record.data;
239
+ }
240
+ const trimmed = (url ?? '').trim();
241
+ if (!trimmed.length) {
242
+ // Fallback: at least emit a textual marker so内容不会完全丢失
243
+ targetParts.push({ text: '[image]' });
244
+ continue;
245
+ }
246
+ let mimeType;
247
+ let data;
248
+ // data:URL → inlineData { mimeType, data }
249
+ if (trimmed.startsWith('data:')) {
250
+ const match = /^data:([^;,]+)?(?:;base64)?,(.*)$/s.exec(trimmed);
251
+ if (match) {
252
+ mimeType = (match[1] || '').trim() || undefined;
253
+ data = match[2] || '';
254
+ }
255
+ }
256
+ if (data && data.trim().length) {
257
+ const inline = {
258
+ inlineData: {
259
+ data: data.trim()
260
+ }
261
+ };
262
+ if (mimeType && mimeType.length) {
263
+ inline.inlineData.mimeType = mimeType;
264
+ }
265
+ targetParts.push(inline);
266
+ }
267
+ else {
268
+ // 非 data: URL 暂时作为文本 URL 传递,保持语义可见
269
+ targetParts.push({ text: trimmed });
270
+ }
271
+ continue;
272
+ }
273
+ // 默认:回退为文本 JSON 表示,避免静默丢失内容
274
+ try {
275
+ const jsonText = JSON.stringify(record);
276
+ if (jsonText.trim().length) {
277
+ targetParts.push({ text: jsonText });
278
+ }
279
+ }
280
+ catch {
281
+ // ignore malformed block
282
+ }
283
+ }
284
+ }
94
285
  function buildGeminiRequestFromChat(chat, metadata) {
95
286
  const contents = [];
287
+ const emittedToolOutputs = new Set();
96
288
  for (const message of chat.messages) {
97
289
  if (!message || typeof message !== 'object')
98
290
  continue;
99
291
  if (message.role === 'system')
100
292
  continue;
101
- if (message.role === 'tool')
293
+ if (message.role === 'tool') {
294
+ const toolOutput = convertToolMessageToOutput(message);
295
+ if (toolOutput) {
296
+ contents.push(buildFunctionResponseEntry(toolOutput));
297
+ emittedToolOutputs.add(toolOutput.tool_call_id);
298
+ }
102
299
  continue;
300
+ }
103
301
  const entry = {
104
302
  role: mapChatRoleToGemini(message.role),
105
303
  parts: []
106
304
  };
107
- if (typeof message.content === 'string' && message.content.trim().length) {
108
- entry.parts.push({ text: message.content });
109
- }
305
+ appendChatContentToGeminiParts(message, entry.parts);
110
306
  const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
111
307
  for (const tc of toolCalls) {
112
308
  if (!tc || typeof tc !== 'object')
@@ -131,25 +327,37 @@ function buildGeminiRequestFromChat(chat, metadata) {
131
327
  if (typeof tc.id === 'string') {
132
328
  part.functionCall.id = tc.id;
133
329
  }
330
+ const thoughtSignature = extractThoughtSignatureFromToolCall(tc) ?? DUMMY_THOUGHT_SIGNATURE;
331
+ if (thoughtSignature) {
332
+ part.thoughtSignature = thoughtSignature;
333
+ }
134
334
  entry.parts.push(part);
135
335
  }
136
336
  if (entry.parts.length) {
137
337
  contents.push(entry);
138
338
  }
139
339
  }
340
+ const toolOutputMap = new Map();
140
341
  if (Array.isArray(chat.toolOutputs)) {
141
- for (const output of chat.toolOutputs) {
142
- const response = cloneAsJsonValue(safeParseJson(output.content));
143
- const part = {
144
- functionResponse: {
145
- name: output.name || 'tool',
146
- id: output.tool_call_id,
147
- response
148
- }
149
- };
150
- contents.push({ role: 'user', parts: [part] });
342
+ for (const entry of chat.toolOutputs) {
343
+ if (entry && typeof entry.tool_call_id === 'string' && entry.tool_call_id.trim().length) {
344
+ toolOutputMap.set(entry.tool_call_id.trim(), entry);
345
+ }
346
+ }
347
+ }
348
+ if (toolOutputMap.size === 0) {
349
+ const syntheticOutputs = synthesizeToolOutputsFromMessages(chat.messages);
350
+ for (const output of syntheticOutputs) {
351
+ toolOutputMap.set(output.tool_call_id, output);
151
352
  }
152
353
  }
354
+ for (const output of toolOutputMap.values()) {
355
+ if (emittedToolOutputs.has(output.tool_call_id)) {
356
+ continue;
357
+ }
358
+ contents.push(buildFunctionResponseEntry(output));
359
+ emittedToolOutputs.add(output.tool_call_id);
360
+ }
153
361
  const request = {
154
362
  model: chat.parameters?.model || 'models/gemini-pro',
155
363
  contents
@@ -235,6 +443,25 @@ function safeParseJson(value) {
235
443
  return value;
236
444
  }
237
445
  }
446
+ function ensureFunctionResponsePayload(value) {
447
+ // Gemini function_response.response 字段在 CloudCode/Gemini CLI 协议里对应的是
448
+ // protobuf Struct(JSON object),而不是顶层数组。
449
+ // 这里做一层规范化:
450
+ // - 对象:直接透传;
451
+ // - 数组:包一层 { result: [...] } 避免把数组作为 Struct 根节点;
452
+ // - 原始值:包一层 { result: value },并把 undefined 映射为 null。
453
+ if (value && typeof value === 'object') {
454
+ if (Array.isArray(value)) {
455
+ return {
456
+ result: value
457
+ };
458
+ }
459
+ return value;
460
+ }
461
+ return {
462
+ result: value === undefined ? null : value
463
+ };
464
+ }
238
465
  function cloneAsJsonValue(value) {
239
466
  try {
240
467
  return JSON.parse(JSON.stringify(value ?? null));