@jsonstudio/llms 0.6.1172 → 0.6.1354

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 (160) hide show
  1. package/dist/conversion/codecs/gemini-openai-codec.d.ts +3 -1
  2. package/dist/conversion/codecs/gemini-openai-codec.js +10 -4
  3. package/dist/conversion/compat/actions/gemini-web-search.d.ts +1 -1
  4. package/dist/conversion/compat/actions/gemini-web-search.js +5 -2
  5. package/dist/conversion/compat/actions/iflow-tool-text-fallback.d.ts +12 -0
  6. package/dist/conversion/compat/actions/iflow-tool-text-fallback.js +199 -0
  7. package/dist/conversion/compat/actions/iflow-web-search.d.ts +1 -1
  8. package/dist/conversion/compat/actions/iflow-web-search.js +5 -2
  9. package/dist/conversion/hub/operation-table/semantic-mappers/anthropic-mapper.js +47 -56
  10. package/dist/conversion/hub/operation-table/semantic-mappers/chat-mapper.js +1 -13
  11. package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.js +523 -50
  12. package/dist/conversion/hub/operation-table/semantic-mappers/responses-mapper.js +18 -38
  13. package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +6 -0
  14. package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +3 -0
  15. package/dist/conversion/hub/pipeline/hub-pipeline/adapter-context.d.ts +10 -0
  16. package/dist/conversion/hub/pipeline/hub-pipeline/adapter-context.js +134 -0
  17. package/dist/conversion/hub/pipeline/hub-pipeline/anthropic-alias-map.d.ts +6 -0
  18. package/dist/conversion/hub/pipeline/hub-pipeline/anthropic-alias-map.js +79 -0
  19. package/dist/conversion/hub/pipeline/hub-pipeline/apply-patch-tool-mode.d.ts +3 -0
  20. package/dist/conversion/hub/pipeline/hub-pipeline/apply-patch-tool-mode.js +46 -0
  21. package/dist/conversion/hub/pipeline/hub-pipeline/execute-chat-process-entry.d.ts +8 -0
  22. package/dist/conversion/hub/pipeline/hub-pipeline/execute-chat-process-entry.js +366 -0
  23. package/dist/conversion/hub/pipeline/hub-pipeline/execute-request-stage.d.ts +9 -0
  24. package/dist/conversion/hub/pipeline/hub-pipeline/execute-request-stage.js +384 -0
  25. package/dist/conversion/hub/pipeline/hub-pipeline/node-results.d.ts +3 -0
  26. package/dist/conversion/hub/pipeline/hub-pipeline/node-results.js +14 -0
  27. package/dist/conversion/hub/pipeline/hub-pipeline/payload-normalize.d.ts +2 -0
  28. package/dist/conversion/hub/pipeline/hub-pipeline/payload-normalize.js +144 -0
  29. package/dist/conversion/hub/pipeline/hub-pipeline/policy.d.ts +4 -0
  30. package/dist/conversion/hub/pipeline/hub-pipeline/policy.js +32 -0
  31. package/dist/conversion/hub/pipeline/hub-pipeline/protocol.d.ts +8 -0
  32. package/dist/conversion/hub/pipeline/hub-pipeline/protocol.js +63 -0
  33. package/dist/conversion/hub/pipeline/hub-pipeline/resolve-protocol-hooks.d.ts +2 -0
  34. package/dist/conversion/hub/pipeline/hub-pipeline/resolve-protocol-hooks.js +43 -0
  35. package/dist/conversion/hub/pipeline/hub-pipeline/semantic-gate.d.ts +1 -0
  36. package/dist/conversion/hub/pipeline/hub-pipeline/semantic-gate.js +29 -0
  37. package/dist/conversion/hub/pipeline/hub-pipeline/servertool-runtime-config.d.ts +2 -0
  38. package/dist/conversion/hub/pipeline/hub-pipeline/servertool-runtime-config.js +16 -0
  39. package/dist/conversion/hub/pipeline/hub-pipeline/types.d.ts +116 -0
  40. package/dist/conversion/hub/pipeline/hub-pipeline/types.js +1 -0
  41. package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +3 -95
  42. package/dist/conversion/hub/pipeline/hub-pipeline.js +19 -1281
  43. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage1_format_parse/index.js +1 -1
  44. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.d.ts +7 -0
  45. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.js +65 -1
  46. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js +25 -22
  47. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage1_semantic_map/index.js +1 -1
  48. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage2_format_build/index.d.ts +1 -1
  49. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage2_format_build/index.js +2 -2
  50. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage3_compat/index.js +2 -2
  51. package/dist/conversion/hub/pipeline/stages/req_process/req_process_stage1_tool_governance/index.js +1 -1
  52. package/dist/conversion/hub/pipeline/stages/req_process/req_process_stage2_route_select/index.js +1 -1
  53. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/index.js +11 -11
  54. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage2_format_parse/index.js +1 -1
  55. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage3_semantic_map/index.d.ts +1 -0
  56. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage3_semantic_map/index.js +4 -2
  57. package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.d.ts +1 -0
  58. package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.js +17 -9
  59. package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage2_sse_stream/index.js +2 -2
  60. package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage1_tool_governance/index.js +40 -2
  61. package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage2_finalize/index.js +1 -1
  62. package/dist/conversion/hub/pipeline/target-utils.js +9 -5
  63. package/dist/conversion/hub/process/chat-process.js +256 -16
  64. package/dist/conversion/hub/response/provider-response.d.ts +8 -0
  65. package/dist/conversion/hub/response/provider-response.js +85 -27
  66. package/dist/conversion/hub/response/response-mappers.d.ts +10 -3
  67. package/dist/conversion/hub/response/response-mappers.js +30 -6
  68. package/dist/conversion/hub/response/response-runtime.js +4 -38
  69. package/dist/conversion/hub/snapshot-recorder.js +5 -1
  70. package/dist/conversion/hub/standardized-bridge.js +23 -15
  71. package/dist/conversion/pipeline/codecs/v2/anthropic-openai-pipeline.js +36 -5
  72. package/dist/conversion/responses/responses-openai-bridge.js +20 -4
  73. package/dist/conversion/shared/gemini-tool-utils.d.ts +8 -1
  74. package/dist/conversion/shared/gemini-tool-utils.js +580 -108
  75. package/dist/conversion/shared/jsonish.js +1 -1
  76. package/dist/conversion/shared/mcp-injection.js +67 -33
  77. package/dist/conversion/shared/openai-finalizer.js +2 -1
  78. package/dist/conversion/shared/openai-message-normalize.js +76 -21
  79. package/dist/conversion/shared/responses-output-builder.js +6 -0
  80. package/dist/conversion/shared/runtime-metadata.d.ts +7 -0
  81. package/dist/conversion/shared/runtime-metadata.js +23 -0
  82. package/dist/conversion/shared/text-markup-normalizer.d.ts +2 -0
  83. package/dist/conversion/shared/text-markup-normalizer.js +284 -4
  84. package/dist/conversion/shared/tool-canonicalizer.js +2 -1
  85. package/dist/conversion/shared/tool-governor.js +3 -3
  86. package/dist/filters/engine.js +5 -5
  87. package/dist/filters/special/request-tool-list-filter.js +194 -60
  88. package/dist/filters/special/request-tools-normalize.js +1 -1
  89. package/dist/filters/special/response-tool-text-canonicalize.d.ts +4 -7
  90. package/dist/filters/special/response-tool-text-canonicalize.js +7 -35
  91. package/dist/filters/special/tool-filter-hooks.js +58 -62
  92. package/dist/guidance/index.js +5 -1
  93. package/dist/http/sse-response.js +6 -6
  94. package/dist/router/virtual-router/bootstrap.js +48 -4
  95. package/dist/router/virtual-router/engine-health.d.ts +1 -1
  96. package/dist/router/virtual-router/engine-health.js +11 -110
  97. package/dist/router/virtual-router/engine-selection/alias-selection.d.ts +15 -0
  98. package/dist/router/virtual-router/engine-selection/alias-selection.js +156 -0
  99. package/dist/router/virtual-router/engine-selection/context-weight-multipliers.d.ts +11 -0
  100. package/dist/router/virtual-router/engine-selection/context-weight-multipliers.js +23 -0
  101. package/dist/router/virtual-router/engine-selection/direct-provider-model.d.ts +9 -0
  102. package/dist/router/virtual-router/engine-selection/direct-provider-model.js +49 -0
  103. package/dist/router/virtual-router/engine-selection/instruction-target.d.ts +6 -0
  104. package/dist/router/virtual-router/engine-selection/instruction-target.js +54 -0
  105. package/dist/router/virtual-router/engine-selection/key-parsing.d.ts +8 -0
  106. package/dist/router/virtual-router/engine-selection/key-parsing.js +64 -0
  107. package/dist/router/virtual-router/engine-selection/route-utils.d.ts +12 -0
  108. package/dist/router/virtual-router/engine-selection/route-utils.js +150 -0
  109. package/dist/router/virtual-router/engine-selection/routing-state-filter.d.ts +4 -0
  110. package/dist/router/virtual-router/engine-selection/routing-state-filter.js +50 -0
  111. package/dist/router/virtual-router/engine-selection/selection-deps.d.ts +39 -0
  112. package/dist/router/virtual-router/engine-selection/selection-deps.js +1 -0
  113. package/dist/router/virtual-router/engine-selection/sticky-pool.d.ts +11 -0
  114. package/dist/router/virtual-router/engine-selection/sticky-pool.js +109 -0
  115. package/dist/router/virtual-router/engine-selection/tier-priority.d.ts +12 -0
  116. package/dist/router/virtual-router/engine-selection/tier-priority.js +55 -0
  117. package/dist/router/virtual-router/engine-selection/tier-selection-select.d.ts +22 -0
  118. package/dist/router/virtual-router/engine-selection/tier-selection-select.js +400 -0
  119. package/dist/router/virtual-router/engine-selection/tier-selection.d.ts +3 -0
  120. package/dist/router/virtual-router/engine-selection/tier-selection.js +225 -0
  121. package/dist/router/virtual-router/engine-selection.d.ts +4 -30
  122. package/dist/router/virtual-router/engine-selection.js +10 -962
  123. package/dist/router/virtual-router/engine.d.ts +1 -0
  124. package/dist/router/virtual-router/engine.js +55 -10
  125. package/dist/router/virtual-router/routing-instructions.js +6 -1
  126. package/dist/router/virtual-router/stop-message-state-sync.d.ts +5 -0
  127. package/dist/router/virtual-router/stop-message-state-sync.js +6 -14
  128. package/dist/router/virtual-router/types.d.ts +25 -1
  129. package/dist/servertool/clock/config.d.ts +8 -0
  130. package/dist/servertool/clock/config.js +22 -0
  131. package/dist/servertool/clock/log.d.ts +3 -0
  132. package/dist/servertool/clock/log.js +13 -0
  133. package/dist/servertool/clock/task-store.d.ts +1 -1
  134. package/dist/servertool/clock/task-store.js +1 -1
  135. package/dist/servertool/clock/tasks.js +1 -1
  136. package/dist/servertool/engine.js +146 -21
  137. package/dist/servertool/handlers/clock-auto.js +11 -6
  138. package/dist/servertool/handlers/clock.js +36 -10
  139. package/dist/servertool/handlers/followup-request-builder.js +8 -2
  140. package/dist/servertool/handlers/gemini-empty-reply-continue.js +15 -9
  141. package/dist/servertool/handlers/iflow-model-error-retry.js +6 -4
  142. package/dist/servertool/handlers/recursive-detection-guard.js +4 -2
  143. package/dist/servertool/handlers/stop-message-auto.js +100 -10
  144. package/dist/servertool/handlers/vision.js +4 -1
  145. package/dist/servertool/handlers/web-search.js +3 -1
  146. package/dist/servertool/pending-session.d.ts +19 -0
  147. package/dist/servertool/pending-session.js +97 -0
  148. package/dist/servertool/reenter-backend.js +5 -3
  149. package/dist/servertool/server-side-tools.js +235 -6
  150. package/dist/servertool/types.d.ts +13 -0
  151. package/dist/sse/json-to-sse/event-generators/responses.js +1 -1
  152. package/dist/sse/shared/chat-serializer.js +2 -2
  153. package/dist/sse/shared/constants.js +1 -1
  154. package/dist/sse/sse-to-json/anthropic-sse-to-json-converter.d.ts +7 -1
  155. package/dist/sse/sse-to-json/builders/response-builder.js +16 -0
  156. package/dist/sse/sse-to-json/responses-sse-to-json-converter.d.ts +1 -1
  157. package/dist/tools/apply-patch/execution-capturer.js +1 -1
  158. package/dist/tools/exec-command/normalize.js +4 -0
  159. package/dist/tools/exec-command/regression-capturer.js +1 -1
  160. package/package.json +10 -5
@@ -2,7 +2,9 @@ import type { ConversionCodec, ConversionContext, ConversionProfile } from '../t
2
2
  export declare function buildOpenAIChatFromGeminiRequest(payload: any): {
3
3
  messages: any[];
4
4
  };
5
- export declare function buildOpenAIChatFromGeminiResponse(payload: any): any;
5
+ export declare function buildOpenAIChatFromGeminiResponse(payload: any, options?: {
6
+ aliasMap?: Record<string, string>;
7
+ }): any;
6
8
  export declare function buildGeminiFromOpenAIChat(chatResp: any): any;
7
9
  export declare class GeminiOpenAIConversionCodec implements ConversionCodec {
8
10
  private readonly _dependencies;
@@ -175,7 +175,7 @@ export function buildOpenAIChatFromGeminiRequest(payload) {
175
175
  }
176
176
  return { messages };
177
177
  }
178
- export function buildOpenAIChatFromGeminiResponse(payload) {
178
+ export function buildOpenAIChatFromGeminiResponse(payload, options) {
179
179
  const candidates = Array.isArray(payload?.candidates) ? payload.candidates : [];
180
180
  const primary = candidates[0] && typeof candidates[0] === 'object' ? candidates[0] : {};
181
181
  const content = primary?.content || {};
@@ -229,9 +229,12 @@ export function buildOpenAIChatFromGeminiResponse(payload) {
229
229
  // 5. Function call (tool call)
230
230
  if (pObj.functionCall && typeof pObj.functionCall === 'object') {
231
231
  const fc = pObj.functionCall;
232
- const name = typeof fc.name === 'string' ? String(fc.name) : undefined;
233
- if (!name)
232
+ const nameRaw = typeof fc.name === 'string' ? String(fc.name) : undefined;
233
+ if (!nameRaw)
234
234
  continue;
235
+ const name = options?.aliasMap && typeof options.aliasMap[nameRaw] === 'string'
236
+ ? String(options.aliasMap[nameRaw])
237
+ : nameRaw;
235
238
  let id = typeof fc.id === 'string' && fc.id.trim().length ? String(fc.id).trim() : undefined;
236
239
  const argsRaw = (fc.args ?? fc.arguments);
237
240
  let argsStr;
@@ -268,7 +271,10 @@ export function buildOpenAIChatFromGeminiResponse(payload) {
268
271
  if (pObj.functionResponse && typeof pObj.functionResponse === 'object') {
269
272
  const fr = pObj.functionResponse;
270
273
  const callId = typeof fr.id === 'string' && fr.id.trim().length ? String(fr.id) : undefined;
271
- const name = typeof fr.name === 'string' && fr.name.trim().length ? String(fr.name) : undefined;
274
+ const nameRaw = typeof fr.name === 'string' && fr.name.trim().length ? String(fr.name) : undefined;
275
+ const name = nameRaw && options?.aliasMap && typeof options.aliasMap[nameRaw] === 'string'
276
+ ? String(options.aliasMap[nameRaw])
277
+ : nameRaw;
272
278
  const resp = fr.response;
273
279
  let contentStr = '';
274
280
  if (typeof resp === 'string') {
@@ -3,7 +3,7 @@ import type { AdapterContext } from '../../hub/types/chat-envelope.js';
3
3
  /**
4
4
  * Gemini web_search 请求适配(作用于 Gemini 请求 payload,而不是 ChatEnvelope):
5
5
  *
6
- * - 仅在 routeId 以 `web_search` 开头时生效(来自 AdapterContext.routeId);
6
+ * - 仅在 routeId 以 `web_search` 或 `search` 开头时生效(来自 AdapterContext.routeId);
7
7
  * - 针对 Gemini 请求中的 `tools` 字段进行清洗:
8
8
  * - 保留 name === 'web_search' 的 functionDeclarations(如果存在);
9
9
  * - 丢弃其它 Codex 自身工具(exec_command / MCP 等),避免 Cloud Code 报
@@ -2,7 +2,7 @@ const isRecord = (value) => typeof value === 'object' && value !== null && !Arra
2
2
  /**
3
3
  * Gemini web_search 请求适配(作用于 Gemini 请求 payload,而不是 ChatEnvelope):
4
4
  *
5
- * - 仅在 routeId 以 `web_search` 开头时生效(来自 AdapterContext.routeId);
5
+ * - 仅在 routeId 以 `web_search` 或 `search` 开头时生效(来自 AdapterContext.routeId);
6
6
  * - 针对 Gemini 请求中的 `tools` 字段进行清洗:
7
7
  * - 保留 name === 'web_search' 的 functionDeclarations(如果存在);
8
8
  * - 丢弃其它 Codex 自身工具(exec_command / MCP 等),避免 Cloud Code 报
@@ -15,7 +15,10 @@ const isRecord = (value) => typeof value === 'object' && value !== null && !Arra
15
15
  */
16
16
  export function applyGeminiWebSearchCompat(payload, adapterContext) {
17
17
  const routeId = typeof adapterContext?.routeId === 'string' ? adapterContext.routeId : '';
18
- if (!routeId || !routeId.toLowerCase().startsWith('web_search')) {
18
+ const normalizedRoute = routeId.trim().toLowerCase();
19
+ // Some hosts/configs use "search" as the route name for web search engines.
20
+ // Treat both "web_search*" and "search*" as web-search routes.
21
+ if (!normalizedRoute || (!normalizedRoute.startsWith('web_search') && !normalizedRoute.startsWith('search'))) {
19
22
  return payload;
20
23
  }
21
24
  const cloned = { ...payload };
@@ -0,0 +1,12 @@
1
+ import type { JsonObject } from '../../hub/types/json.js';
2
+ /**
3
+ * Some iFlow models reject OpenAI `tools`/`tool_choice` fields with a non-standard 200 error envelope.
4
+ *
5
+ * For such models, we fall back to "text tool calls":
6
+ * - remove top-level `tools` / `tool_choice` so upstream accepts the request
7
+ * - inject a system instruction telling the model to emit `<tool:...>` blocks
8
+ * - response pipeline will harvest those blocks into canonical `tool_calls`
9
+ */
10
+ export declare function applyIflowToolTextFallback(payload: JsonObject, options?: {
11
+ models?: string[];
12
+ }): JsonObject;
@@ -0,0 +1,199 @@
1
+ const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
2
+ function normalizeModel(value) {
3
+ return typeof value === 'string' ? value.trim().toLowerCase() : '';
4
+ }
5
+ function hasNonEmptyArray(value) {
6
+ return Array.isArray(value) && value.length > 0;
7
+ }
8
+ function ensureSystemMessage(messages) {
9
+ const first = messages[0];
10
+ if (isRecord(first) && typeof first.role === 'string' && first.role.toLowerCase() === 'system') {
11
+ return first;
12
+ }
13
+ const sys = { role: 'system', content: '' };
14
+ messages.unshift(sys);
15
+ return sys;
16
+ }
17
+ function appendSystemText(sys, extra) {
18
+ const prev = typeof sys.content === 'string' ? sys.content : '';
19
+ if (prev.includes('## Tool Calls (Text Markup Mode)')) {
20
+ return;
21
+ }
22
+ const combined = prev ? `${prev}\n\n${extra}` : extra;
23
+ sys.content = combined;
24
+ }
25
+ function buildToolMarkupInstruction() {
26
+ // Keep this concise. The response-side canonicalizer will harvest these blocks into tool_calls.
27
+ return [
28
+ '## Tool Calls (Text Markup Mode)',
29
+ '',
30
+ 'You MAY call tools by emitting one or more XML blocks with this exact format, and nothing else inside the block:',
31
+ '',
32
+ '<tool:exec_command>',
33
+ '<command>...</command>',
34
+ '<timeout_ms>10000</timeout_ms>',
35
+ '</tool:exec_command>',
36
+ '',
37
+ '<tool:write_stdin>',
38
+ '<session_id>...</session_id>',
39
+ '<input>...</input>',
40
+ '</tool:write_stdin>',
41
+ '',
42
+ 'Rules:',
43
+ '- Use `<tool:exec_command>` to run shell commands.',
44
+ '- Use `<tool:write_stdin>` to send input to an existing session.',
45
+ '- Do NOT wrap these blocks in code fences.',
46
+ '- Do NOT invent tools; only use exec_command and write_stdin.',
47
+ ''
48
+ ].join('\n');
49
+ }
50
+ function stringifyToolOutput(value) {
51
+ if (typeof value === 'string')
52
+ return value;
53
+ if (value === null || value === undefined)
54
+ return '';
55
+ try {
56
+ return JSON.stringify(value, null, 2);
57
+ }
58
+ catch {
59
+ return String(value);
60
+ }
61
+ }
62
+ function coerceToolCallToXmlBlock(toolCall) {
63
+ if (!isRecord(toolCall))
64
+ return null;
65
+ const type = typeof toolCall.type === 'string' ? toolCall.type.trim().toLowerCase() : '';
66
+ if (type && type !== 'function')
67
+ return null;
68
+ const fn = isRecord(toolCall.function) ? toolCall.function : null;
69
+ const name = typeof fn?.name === 'string' ? fn.name.trim() : '';
70
+ if (!name)
71
+ return null;
72
+ const argsRaw = fn?.arguments;
73
+ let args = null;
74
+ if (typeof argsRaw === 'string' && argsRaw.trim().length) {
75
+ try {
76
+ const parsed = JSON.parse(argsRaw);
77
+ args = isRecord(parsed) ? parsed : null;
78
+ }
79
+ catch {
80
+ args = { raw: argsRaw };
81
+ }
82
+ }
83
+ else if (isRecord(argsRaw)) {
84
+ args = argsRaw;
85
+ }
86
+ if (name === 'exec_command') {
87
+ const command = typeof args?.cmd === 'string'
88
+ ? args.cmd
89
+ : typeof args?.command === 'string'
90
+ ? args.command
91
+ : '';
92
+ if (!command)
93
+ return null;
94
+ const timeoutMs = typeof args?.timeout_ms === 'number' && Number.isFinite(args.timeout_ms)
95
+ ? Math.floor(args.timeout_ms)
96
+ : undefined;
97
+ return [
98
+ '<tool:exec_command>',
99
+ `<command>${command}</command>`,
100
+ ...(timeoutMs !== undefined ? [`<timeout_ms>${timeoutMs}</timeout_ms>`] : []),
101
+ '</tool:exec_command>'
102
+ ].join('\n');
103
+ }
104
+ if (name === 'write_stdin') {
105
+ const sessionId = typeof args?.session_id === 'string'
106
+ ? args.session_id
107
+ : typeof args?.sessionId === 'string'
108
+ ? args.sessionId
109
+ : '';
110
+ const input = typeof args?.chars === 'string'
111
+ ? args.chars
112
+ : typeof args?.input === 'string'
113
+ ? args.input
114
+ : '';
115
+ if (!sessionId || !input)
116
+ return null;
117
+ return [
118
+ '<tool:write_stdin>',
119
+ `<session_id>${sessionId}</session_id>`,
120
+ `<input>${input}</input>`,
121
+ '</tool:write_stdin>'
122
+ ].join('\n');
123
+ }
124
+ return null;
125
+ }
126
+ function rewriteMessageToolSurfaceInPlace(message) {
127
+ const role = typeof message.role === 'string' ? message.role.trim().toLowerCase() : '';
128
+ // Tool result messages are not supported by some iFlow models; convert them into plain text.
129
+ if (role === 'tool') {
130
+ const toolName = typeof message.name === 'string' ? message.name.trim() : '';
131
+ const toolCallId = typeof message.tool_call_id === 'string' ? String(message.tool_call_id).trim() : '';
132
+ const content = stringifyToolOutput(message.content);
133
+ message.role = 'user';
134
+ message.content = [
135
+ 'Tool result:',
136
+ ...(toolName ? [`name: ${toolName}`] : []),
137
+ ...(toolCallId ? [`tool_call_id: ${toolCallId}`] : []),
138
+ 'output:',
139
+ content
140
+ ].join('\n');
141
+ delete message.name;
142
+ delete message.tool_call_id;
143
+ return;
144
+ }
145
+ // Assistant tool_calls are not supported by some iFlow models; convert them to XML tool markup.
146
+ const toolCallsRaw = message.tool_calls;
147
+ if (hasNonEmptyArray(toolCallsRaw)) {
148
+ const blocks = toolCallsRaw
149
+ .map(coerceToolCallToXmlBlock)
150
+ .filter((b) => typeof b === 'string' && b.length > 0);
151
+ if (blocks.length) {
152
+ const prev = typeof message.content === 'string' ? message.content : '';
153
+ const joined = blocks.join('\n\n');
154
+ message.content = prev && prev.trim().length ? `${prev}\n\n${joined}` : joined;
155
+ }
156
+ delete message.tool_calls;
157
+ }
158
+ }
159
+ /**
160
+ * Some iFlow models reject OpenAI `tools`/`tool_choice` fields with a non-standard 200 error envelope.
161
+ *
162
+ * For such models, we fall back to "text tool calls":
163
+ * - remove top-level `tools` / `tool_choice` so upstream accepts the request
164
+ * - inject a system instruction telling the model to emit `<tool:...>` blocks
165
+ * - response pipeline will harvest those blocks into canonical `tool_calls`
166
+ */
167
+ export function applyIflowToolTextFallback(payload, options) {
168
+ if (!payload || typeof payload !== 'object') {
169
+ return payload;
170
+ }
171
+ const root = structuredClone(payload);
172
+ const model = normalizeModel(root.model);
173
+ const models = Array.isArray(options?.models) ? options.models.map((m) => normalizeModel(m)).filter(Boolean) : [];
174
+ if (!model || !models.includes(model)) {
175
+ return root;
176
+ }
177
+ // Only apply when we have a message array to carry the injected instruction.
178
+ const messages = root.messages;
179
+ if (!hasNonEmptyArray(messages)) {
180
+ return root;
181
+ }
182
+ // Always strip tool schemas for fallback models (including followups),
183
+ // otherwise the first turn might succeed but the tool loop breaks.
184
+ if ('tools' in root) {
185
+ delete root.tools;
186
+ }
187
+ if ('tool_choice' in root) {
188
+ delete root.tool_choice;
189
+ }
190
+ // Rewrite message-level tool protocol to plain text so upstream accepts it.
191
+ for (const msg of messages) {
192
+ if (!isRecord(msg))
193
+ continue;
194
+ rewriteMessageToolSurfaceInPlace(msg);
195
+ }
196
+ const sys = ensureSystemMessage(messages);
197
+ appendSystemText(sys, buildToolMarkupInstruction());
198
+ return root;
199
+ }
@@ -3,7 +3,7 @@ import type { AdapterContext } from '../../hub/types/chat-envelope.js';
3
3
  /**
4
4
  * IFlow web_search 请求适配(作用于 openai-chat 兼容 payload):
5
5
  *
6
- * - 仅在 routeId 以 `web_search` 开头时生效(来自 AdapterContext.routeId);
6
+ * - 仅在 routeId 以 `web_search` 或 `search` 开头时生效(来自 AdapterContext.routeId);
7
7
  * - 读取顶层的 `web_search` helper 对象 `{ query, recency, count, engine }`;
8
8
  * - 当 query 为空或无效时:删除 helper,原样透传;
9
9
  * - 当 query 有效时:构造一个标准的 OpenAI function tool:
@@ -3,7 +3,7 @@ const DEBUG_IFLOW_WEB_SEARCH = (process.env.ROUTECODEX_DEBUG_IFLOW_WEB_SEARCH ||
3
3
  /**
4
4
  * IFlow web_search 请求适配(作用于 openai-chat 兼容 payload):
5
5
  *
6
- * - 仅在 routeId 以 `web_search` 开头时生效(来自 AdapterContext.routeId);
6
+ * - 仅在 routeId 以 `web_search` 或 `search` 开头时生效(来自 AdapterContext.routeId);
7
7
  * - 读取顶层的 `web_search` helper 对象 `{ query, recency, count, engine }`;
8
8
  * - 当 query 为空或无效时:删除 helper,原样透传;
9
9
  * - 当 query 有效时:构造一个标准的 OpenAI function tool:
@@ -17,7 +17,10 @@ const DEBUG_IFLOW_WEB_SEARCH = (process.env.ROUTECODEX_DEBUG_IFLOW_WEB_SEARCH ||
17
17
  */
18
18
  export function applyIflowWebSearchRequestTransform(payload, adapterContext) {
19
19
  const routeId = typeof adapterContext?.routeId === 'string' ? adapterContext.routeId : '';
20
- if (!routeId || !routeId.toLowerCase().startsWith('web_search')) {
20
+ const normalizedRoute = routeId.trim().toLowerCase();
21
+ // Some hosts/configs use "search" as the route name for web search engines.
22
+ // Treat both "web_search*" and "search*" as web-search routes.
23
+ if (!normalizedRoute || (!normalizedRoute.startsWith('web_search') && !normalizedRoute.startsWith('search'))) {
21
24
  return payload;
22
25
  }
23
26
  const root = structuredClone(payload);
@@ -3,7 +3,6 @@ import { buildOpenAIChatFromAnthropic, buildAnthropicRequestFromOpenAIChat } fro
3
3
  import { encodeMetadataPassthrough, extractMetadataPassthrough } from '../../../shared/metadata-passthrough.js';
4
4
  import { buildAnthropicToolAliasMap } from '../../../shared/anthropic-message-utils.js';
5
5
  import { ChatSemanticMapper } from './chat-mapper.js';
6
- import { ensureProtocolState, getProtocolState } from '../../../shared/protocol-state.js';
7
6
  const ANTHROPIC_PARAMETER_KEYS = [
8
7
  'model',
9
8
  'temperature',
@@ -38,12 +37,12 @@ function ensureSemantics(chat) {
38
37
  }
39
38
  return chat.semantics;
40
39
  }
41
- function ensureAnthropicSemanticsNode(chat) {
40
+ function ensureToolsSemanticsNode(chat) {
42
41
  const semantics = ensureSemantics(chat);
43
- if (!semantics.anthropic || !isJsonObject(semantics.anthropic)) {
44
- semantics.anthropic = {};
42
+ if (!semantics.tools || !isJsonObject(semantics.tools)) {
43
+ semantics.tools = {};
45
44
  }
46
- return semantics.anthropic;
45
+ return semantics.tools;
47
46
  }
48
47
  function markExplicitEmptyTools(chat) {
49
48
  const semantics = ensureSemantics(chat);
@@ -52,11 +51,11 @@ function markExplicitEmptyTools(chat) {
52
51
  }
53
52
  semantics.tools.explicitEmpty = true;
54
53
  }
55
- function readAnthropicSemantics(chat) {
54
+ function readToolsSemantics(chat) {
56
55
  if (!chat.semantics || typeof chat.semantics !== 'object') {
57
56
  return undefined;
58
57
  }
59
- const node = chat.semantics.anthropic;
58
+ const node = chat.semantics.tools;
60
59
  return node && isJsonObject(node) ? node : undefined;
61
60
  }
62
61
  function hasExplicitEmptyToolsSemantics(chat) {
@@ -126,34 +125,24 @@ export class AnthropicSemanticMapper {
126
125
  const metadata = chatEnvelope.metadata ?? { context: canonicalContext };
127
126
  chatEnvelope.metadata = metadata;
128
127
  metadata.context = canonicalContext;
129
- let semanticsNode;
130
- const resolveExtraFields = () => {
131
- if (!isJsonObject(metadata.extraFields)) {
132
- metadata.extraFields = {};
133
- }
134
- return metadata.extraFields;
135
- };
136
- const protocolState = ensureProtocolState(metadata, 'anthropic');
128
+ const semantics = ensureSemantics(chatEnvelope);
129
+ if (!semantics.system || !isJsonObject(semantics.system)) {
130
+ semantics.system = {};
131
+ }
132
+ if (!semantics.providerExtras || !isJsonObject(semantics.providerExtras)) {
133
+ semantics.providerExtras = {};
134
+ }
137
135
  const systemBlocks = cloneAnthropicSystemBlocks(payload.system);
138
136
  if (systemBlocks) {
139
- protocolState.systemBlocks = systemBlocks;
140
- semanticsNode = semanticsNode ?? ensureAnthropicSemanticsNode(chatEnvelope);
141
- semanticsNode.systemBlocks = jsonClone(systemBlocks);
137
+ semantics.system.blocks = jsonClone(systemBlocks);
142
138
  }
143
139
  if (payload.tools && Array.isArray(payload.tools) && payload.tools.length === 0) {
144
- metadata.toolsFieldPresent = true;
145
- resolveExtraFields().toolsFieldPresent = true;
146
140
  markExplicitEmptyTools(chatEnvelope);
147
141
  }
148
142
  const aliasMap = buildAnthropicToolAliasMap(payload.tools);
149
143
  if (aliasMap) {
150
- const extraFields = resolveExtraFields();
151
- ctx.anthropicToolNameMap = aliasMap;
152
- canonicalContext.anthropicToolNameMap = aliasMap;
153
- metadata.anthropicToolNameMap = aliasMap;
154
- extraFields.anthropicToolNameMap = aliasMap;
155
- semanticsNode = semanticsNode ?? ensureAnthropicSemanticsNode(chatEnvelope);
156
- semanticsNode.toolAliasMap = jsonClone(aliasMap);
144
+ const toolsNode = ensureToolsSemanticsNode(chatEnvelope);
145
+ toolsNode.toolNameAliasMap = jsonClone(aliasMap);
157
146
  }
158
147
  if (Array.isArray(payload.messages) && payload.messages.length) {
159
148
  const shapes = payload.messages.map((entry) => {
@@ -172,14 +161,8 @@ export class AnthropicSemanticMapper {
172
161
  }
173
162
  return typeof rawContent;
174
163
  });
175
- const extraFields = resolveExtraFields();
176
- const mirrorNode = extraFields.anthropicMirror && typeof extraFields.anthropicMirror === 'object'
177
- ? extraFields.anthropicMirror
178
- : {};
179
- mirrorNode.messageContentShape = shapes;
180
- extraFields.anthropicMirror = mirrorNode;
181
- semanticsNode = semanticsNode ?? ensureAnthropicSemanticsNode(chatEnvelope);
182
- semanticsNode.mirror = jsonClone(mirrorNode);
164
+ const mirrorNode = { messageContentShape: shapes };
165
+ semantics.providerExtras.anthropicMirror = jsonClone(mirrorNode);
183
166
  }
184
167
  if (missing.length) {
185
168
  metadata.missingFields = Array.isArray(metadata.missingFields)
@@ -189,9 +172,7 @@ export class AnthropicSemanticMapper {
189
172
  const providerMetadata = passthrough.metadata ??
190
173
  (payload.metadata && isJsonObject(payload.metadata) ? jsonClone(payload.metadata) : undefined);
191
174
  if (providerMetadata) {
192
- metadata.providerMetadata = providerMetadata;
193
- semanticsNode = semanticsNode ?? ensureAnthropicSemanticsNode(chatEnvelope);
194
- semanticsNode.providerMetadata = jsonClone(providerMetadata);
175
+ semantics.providerExtras.providerMetadata = jsonClone(providerMetadata);
195
176
  }
196
177
  const mergedParameters = { ...(chatEnvelope.parameters ?? {}) };
197
178
  const mergeParameters = (source) => {
@@ -232,8 +213,7 @@ export class AnthropicSemanticMapper {
232
213
  messages: chat.messages,
233
214
  tools: chat.tools
234
215
  };
235
- const semanticsNode = readAnthropicSemantics(chat);
236
- const explicitEmptyTools = (chat.metadata?.toolsFieldPresent === true) || hasExplicitEmptyToolsSemantics(chat);
216
+ const explicitEmptyTools = hasExplicitEmptyToolsSemantics(chat);
237
217
  const trimmedParameters = chat.parameters && typeof chat.parameters === 'object' ? chat.parameters : undefined;
238
218
  if (trimmedParameters) {
239
219
  for (const [key, value] of Object.entries(trimmedParameters)) {
@@ -268,28 +248,39 @@ export class AnthropicSemanticMapper {
268
248
  if (explicitEmptyTools && (!Array.isArray(chat.tools) || chat.tools.length === 0)) {
269
249
  baseRequest.tools = [];
270
250
  }
271
- const protocolState = getProtocolState(chat.metadata, 'anthropic');
272
- if (protocolState?.systemBlocks !== undefined) {
273
- baseRequest.system = jsonClone(protocolState.systemBlocks);
251
+ try {
252
+ const sysNode = chat.semantics && typeof chat.semantics === 'object' ? chat.semantics.system : undefined;
253
+ if (sysNode && typeof sysNode === 'object' && !Array.isArray(sysNode) && sysNode.blocks !== undefined) {
254
+ baseRequest.system = jsonClone(sysNode.blocks);
255
+ }
274
256
  }
275
- else if (semanticsNode?.systemBlocks !== undefined) {
276
- baseRequest.system = jsonClone(semanticsNode.systemBlocks);
257
+ catch {
258
+ // ignore
277
259
  }
278
- if (chat.metadata &&
279
- typeof chat.metadata === 'object' &&
280
- chat.metadata.extraFields &&
281
- typeof chat.metadata.extraFields === 'object' &&
282
- chat.metadata.extraFields.anthropicMirror) {
283
- baseRequest.__anthropicMirror = jsonClone(chat.metadata.extraFields.anthropicMirror ?? {});
260
+ try {
261
+ const extras = chat.semantics && typeof chat.semantics === 'object' ? chat.semantics.providerExtras : undefined;
262
+ const mirror = extras && typeof extras === 'object' && !Array.isArray(extras) ? extras.anthropicMirror : undefined;
263
+ if (mirror && typeof mirror === 'object' && !Array.isArray(mirror)) {
264
+ baseRequest.__anthropicMirror = jsonClone(mirror);
265
+ }
266
+ const providerMetadata = extras && typeof extras === 'object' && !Array.isArray(extras) ? extras.providerMetadata : undefined;
267
+ if (providerMetadata && typeof providerMetadata === 'object' && !Array.isArray(providerMetadata)) {
268
+ // Only for anthropic-native endpoint: allow restoring anthropic metadata back into outbound.
269
+ const existing = baseRequest.metadata && isJsonObject(baseRequest.metadata)
270
+ ? jsonClone(baseRequest.metadata)
271
+ : {};
272
+ const merged = {
273
+ ...existing,
274
+ ...jsonClone(providerMetadata)
275
+ };
276
+ baseRequest.metadata = merged;
277
+ }
284
278
  }
285
- else if (semanticsNode?.mirror && isJsonObject(semanticsNode.mirror)) {
286
- baseRequest.__anthropicMirror = jsonClone(semanticsNode.mirror);
279
+ catch {
280
+ // ignore
287
281
  }
288
282
  const payloadSource = buildAnthropicRequestFromOpenAIChat(baseRequest);
289
283
  const payload = sanitizeAnthropicPayload(JSON.parse(JSON.stringify(payloadSource)));
290
- if (chat.metadata?.toolsFieldPresent && (!Array.isArray(chat.tools) || chat.tools.length === 0)) {
291
- payload.tools = [];
292
- }
293
284
  sanitizeAnthropicPayload(payload);
294
285
  return {
295
286
  protocol: 'anthropic-messages',
@@ -323,9 +323,6 @@ function applyExtraFields(body, metadata, semantics) {
323
323
  if (semanticsExtras) {
324
324
  sources.push(semanticsExtras);
325
325
  }
326
- if (metadata?.extraFields && isJsonObject(metadata.extraFields)) {
327
- sources.push(metadata.extraFields);
328
- }
329
326
  if (!sources.length) {
330
327
  return;
331
328
  }
@@ -350,9 +347,6 @@ export class ChatSemanticMapper {
350
347
  }
351
348
  }
352
349
  const metadata = { context: ctx };
353
- if (normalized.systemSegments.length) {
354
- metadata.systemInstructions = normalized.systemSegments;
355
- }
356
350
  const rawSystemBlocks = collectSystemRawBlocks(payload.messages);
357
351
  if (rawSystemBlocks) {
358
352
  const protocolState = ensureProtocolState(metadata, 'openai');
@@ -362,13 +356,7 @@ export class ChatSemanticMapper {
362
356
  metadata.missingFields = normalized.missingFields;
363
357
  }
364
358
  const extraFields = collectExtraFields(payload);
365
- if (extraFields) {
366
- metadata.extraFields = extraFields;
367
- }
368
359
  const explicitEmptyTools = Array.isArray(payload.tools) && payload.tools.length === 0;
369
- if (explicitEmptyTools) {
370
- metadata.toolsFieldPresent = true;
371
- }
372
360
  const semantics = buildOpenAISemantics({
373
361
  systemSegments: normalized.systemSegments,
374
362
  extraFields,
@@ -384,7 +372,7 @@ export class ChatSemanticMapper {
384
372
  };
385
373
  }
386
374
  async fromChat(chat, ctx) {
387
- const shouldEmitEmptyTools = hasExplicitEmptyToolsSemantics(chat.semantics) || chat.metadata?.toolsFieldPresent === true;
375
+ const shouldEmitEmptyTools = hasExplicitEmptyToolsSemantics(chat.semantics);
388
376
  const payload = {
389
377
  messages: chat.messages,
390
378
  tools: chat.tools ?? (shouldEmitEmptyTools ? [] : undefined),