@jsonstudio/llms 0.6.3409 → 0.6.3539

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 (82) hide show
  1. package/dist/conversion/codecs/anthropic-openai-codec.d.ts +12 -3
  2. package/dist/conversion/codecs/anthropic-openai-codec.js +32 -92
  3. package/dist/conversion/codecs/gemini-openai-codec.d.ts +6 -5
  4. package/dist/conversion/codecs/gemini-openai-codec.js +48 -685
  5. package/dist/conversion/codecs/openai-openai-codec.d.ts +1 -1
  6. package/dist/conversion/codecs/openai-openai-codec.js +34 -100
  7. package/dist/conversion/codecs/responses-openai-codec.d.ts +1 -1
  8. package/dist/conversion/codecs/responses-openai-codec.js +47 -159
  9. package/dist/conversion/compat/actions/anthropic-claude-code-system-prompt.d.ts +2 -6
  10. package/dist/conversion/compat/actions/anthropic-claude-code-system-prompt.js +29 -245
  11. package/dist/conversion/compat/actions/anthropic-claude-code-user-id.d.ts +3 -0
  12. package/dist/conversion/compat/actions/anthropic-claude-code-user-id.js +30 -0
  13. package/dist/conversion/compat/actions/antigravity-thought-signature-prepare.js +21 -232
  14. package/dist/conversion/compat/actions/deepseek-web-request.js +41 -276
  15. package/dist/conversion/compat/actions/deepseek-web-response.js +64 -859
  16. package/dist/conversion/compat/actions/gemini-cli-request.d.ts +1 -1
  17. package/dist/conversion/compat/actions/gemini-cli-request.js +20 -613
  18. package/dist/conversion/compat/actions/gemini-web-search.d.ts +1 -15
  19. package/dist/conversion/compat/actions/gemini-web-search.js +22 -69
  20. package/dist/conversion/compat/actions/glm-tool-extraction.d.ts +3 -2
  21. package/dist/conversion/compat/actions/glm-tool-extraction.js +28 -257
  22. package/dist/conversion/compat/actions/iflow-tool-text-fallback.d.ts +0 -8
  23. package/dist/conversion/compat/actions/iflow-tool-text-fallback.js +24 -206
  24. package/dist/conversion/compat/actions/qwen-transform.d.ts +3 -2
  25. package/dist/conversion/compat/actions/qwen-transform.js +30 -271
  26. package/dist/conversion/compat/actions/tool-text-request-guidance.js +3 -173
  27. package/dist/conversion/compat/actions/universal-shape-filter.d.ts +6 -23
  28. package/dist/conversion/compat/actions/universal-shape-filter.js +4 -383
  29. package/dist/conversion/hub/pipeline/compat/native-adapter-context.js +1 -0
  30. package/dist/conversion/pipeline/codecs/v2/anthropic-openai-pipeline.d.ts +1 -2
  31. package/dist/conversion/pipeline/codecs/v2/anthropic-openai-pipeline.js +50 -104
  32. package/dist/conversion/pipeline/codecs/v2/openai-openai-pipeline.js +12 -10
  33. package/dist/conversion/pipeline/codecs/v2/responses-openai-pipeline.d.ts +0 -2
  34. package/dist/conversion/pipeline/codecs/v2/responses-openai-pipeline.js +46 -67
  35. package/dist/conversion/pipeline/codecs/v2/shared/openai-chat-helpers.js +15 -40
  36. package/dist/conversion/responses/responses-openai-bridge/response-payload.js +47 -348
  37. package/dist/conversion/responses/responses-openai-bridge.js +129 -611
  38. package/dist/conversion/shared/chat-output-normalizer.js +6 -0
  39. package/dist/conversion/shared/chat-request-filters.js +1 -1
  40. package/dist/conversion/shared/output-content-normalizer.js +10 -0
  41. package/dist/conversion/shared/responses-conversation-store.js +22 -135
  42. package/dist/conversion/shared/responses-output-builder.d.ts +0 -2
  43. package/dist/conversion/shared/responses-output-builder.js +28 -318
  44. package/dist/conversion/shared/responses-response-utils.js +35 -86
  45. package/dist/conversion/shared/streaming-text-extractor.d.ts +1 -2
  46. package/dist/conversion/shared/streaming-text-extractor.js +13 -14
  47. package/dist/native/router_hotpath_napi.node +0 -0
  48. package/dist/router/virtual-router/bootstrap/routing-config.js +11 -3
  49. package/dist/router/virtual-router/engine-legacy.d.ts +3 -3
  50. package/dist/router/virtual-router/engine-legacy.js +15 -7
  51. package/dist/router/virtual-router/engine-selection/native-compat-action-semantics.d.ts +16 -0
  52. package/dist/router/virtual-router/engine-selection/native-compat-action-semantics.js +434 -46
  53. package/dist/router/virtual-router/engine-selection/native-hub-bridge-action-semantics.d.ts +83 -0
  54. package/dist/router/virtual-router/engine-selection/native-hub-bridge-action-semantics.js +295 -0
  55. package/dist/router/virtual-router/engine-selection/native-hub-pipeline-req-outbound-semantics.d.ts +1 -0
  56. package/dist/router/virtual-router/engine-selection/native-hub-pipeline-resp-semantics.d.ts +7 -0
  57. package/dist/router/virtual-router/engine-selection/native-hub-pipeline-resp-semantics.js +8 -1
  58. package/dist/router/virtual-router/engine-selection/native-router-hotpath-loader.js +383 -298
  59. package/dist/router/virtual-router/engine-selection/native-shared-conversion-semantics.d.ts +20 -0
  60. package/dist/router/virtual-router/engine-selection/native-shared-conversion-semantics.js +201 -0
  61. package/dist/router/virtual-router/engine-selection/native-virtual-router-routing-instructions-semantics.d.ts +1 -0
  62. package/dist/router/virtual-router/engine-selection/native-virtual-router-routing-instructions-semantics.js +37 -0
  63. package/dist/router/virtual-router/engine.js +0 -38
  64. package/dist/router/virtual-router/features.js +44 -3
  65. package/dist/router/virtual-router/routing-instructions/parse.d.ts +0 -12
  66. package/dist/router/virtual-router/routing-instructions/parse.js +9 -389
  67. package/dist/router/virtual-router/stop-message-state-sync.d.ts +3 -6
  68. package/dist/router/virtual-router/stop-message-state-sync.js +50 -21
  69. package/dist/sse/sse-to-json/anthropic-sse-to-json-converter.d.ts +1 -0
  70. package/dist/sse/sse-to-json/anthropic-sse-to-json-converter.js +26 -0
  71. package/dist/sse/sse-to-json/builders/anthropic-response-builder.js +12 -2
  72. package/package.json +1 -1
  73. package/dist/router/virtual-router/engine-legacy/route-finalize.d.ts +0 -9
  74. package/dist/router/virtual-router/engine-legacy/route-finalize.js +0 -84
  75. package/dist/router/virtual-router/engine-legacy/route-selection.d.ts +0 -17
  76. package/dist/router/virtual-router/engine-legacy/route-selection.js +0 -205
  77. package/dist/router/virtual-router/engine-legacy/route-state-allowlist.d.ts +0 -3
  78. package/dist/router/virtual-router/engine-legacy/route-state-allowlist.js +0 -36
  79. package/dist/router/virtual-router/engine-legacy/route-state.d.ts +0 -12
  80. package/dist/router/virtual-router/engine-legacy/route-state.js +0 -386
  81. package/dist/router/virtual-router/engine-legacy/routing.d.ts +0 -8
  82. package/dist/router/virtual-router/engine-legacy/routing.js +0 -8
@@ -1,71 +1,24 @@
1
- const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
2
- /**
3
- * Gemini web_search 请求适配(作用于 Gemini 请求 payload,而不是 ChatEnvelope):
4
- *
5
- * - 仅在 routeId 以 `web_search` 或 `search` 开头时生效(来自 AdapterContext.routeId);
6
- * - 针对 Gemini 请求中的 `tools` 字段进行清洗:
7
- * - 保留 name === 'web_search' 的 functionDeclarations(如果存在);
8
- * - 丢弃其它 Codex 自身工具(exec_command / MCP 等),避免 Cloud Code 报
9
- * “Multiple tools are supported only when they are all search tools.”;
10
- * - 如果清洗后没有任何工具,则删除 `tools` 字段。
11
- *
12
- * 注意:
13
- * - 这里处理的是 llmswitch-core 为 gemini-chat 构造的中间 payload(buildGeminiRequestFromChat 输出);
14
- * - 对于 Gemini CLI 后续在传输层添加的 googleSearch 工具,本适配不负责构造或修改。
15
- */
16
- export function applyGeminiWebSearchCompat(payload, adapterContext) {
17
- const routeId = typeof adapterContext?.routeId === 'string' ? adapterContext.routeId : '';
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'))) {
22
- return payload;
23
- }
24
- const cloned = { ...payload };
25
- // 对于 Gemini Chat 后端,请求根节点不支持自定义 `web_search` 字段,
26
- // 统一在 compat 层移除,避免触发
27
- // "Unknown name \"web_search\" at 'request': Cannot find field." 错误。
28
- if (cloned.web_search !== undefined) {
29
- delete cloned.web_search;
30
- }
31
- const toolsRaw = cloned.tools;
32
- // 当 web_search 路由下完全没有 tools 时,为 Gemini 搜索模型注入最小 googleSearch 工具,
33
- // 保持与 gcli2api 类似的“搜索模型自动启用 Search 工具”行为。
34
- if (!Array.isArray(toolsRaw)) {
35
- cloned.tools = [{ googleSearch: {} }];
36
- return cloned;
37
- }
38
- const nextTools = [];
39
- for (const entry of toolsRaw) {
40
- if (!isRecord(entry))
41
- continue;
42
- // 1) 保留 name === 'web_search' 的 functionDeclarations(单函数声明)。
43
- const decls = Array.isArray(entry.functionDeclarations)
44
- ? entry.functionDeclarations
45
- : [];
46
- const webSearchDecls = decls.filter((fn) => {
47
- if (!isRecord(fn))
48
- return false;
49
- const name = typeof fn.name === 'string' ? fn.name.trim().toLowerCase() : '';
50
- return name === 'web_search';
51
- });
52
- if (webSearchDecls.length) {
53
- nextTools.push({
54
- functionDeclarations: webSearchDecls
55
- });
56
- continue;
57
- }
58
- // 2) 若上游已经构造了 googleSearch 工具,则保留。
59
- if (isRecord(entry.googleSearch)) {
60
- nextTools.push({ googleSearch: entry.googleSearch });
61
- }
62
- }
63
- if (nextTools.length > 0) {
64
- cloned.tools = nextTools;
65
- }
66
- else {
67
- // 3) 清洗后没有任何工具时,再次注入一个最小 googleSearch 工具,确保搜索模型仍然启用 Search。
68
- cloned.tools = [{ googleSearch: {} }];
1
+ import { buildNativeReqOutboundCompatAdapterContext } from '../../hub/pipeline/compat/native-adapter-context.js';
2
+ import { applyGeminiWebSearchRequestCompatWithNative } from '../../../router/virtual-router/engine-selection/native-compat-action-semantics.js';
3
+ const PROFILE = 'chat:gemini';
4
+ const DEFAULT_PROVIDER_PROTOCOL = 'gemini-chat';
5
+ const DEFAULT_ENTRY_ENDPOINT = '/v1beta/models:generateContent';
6
+ function buildGeminiCompatContext(adapterContext) {
7
+ const nativeContext = buildNativeReqOutboundCompatAdapterContext(adapterContext);
8
+ return {
9
+ ...nativeContext,
10
+ compatibilityProfile: PROFILE,
11
+ providerProtocol: nativeContext.providerProtocol ??
12
+ adapterContext?.providerProtocol ??
13
+ DEFAULT_PROVIDER_PROTOCOL,
14
+ entryEndpoint: nativeContext.entryEndpoint ??
15
+ adapterContext?.entryEndpoint ??
16
+ DEFAULT_ENTRY_ENDPOINT,
17
+ };
18
+ }
19
+ export function applyGeminiWebSearchCompat(root, adapterContext) {
20
+ if (!root || typeof root !== 'object' || Array.isArray(root)) {
21
+ return root;
69
22
  }
70
- return cloned;
23
+ return applyGeminiWebSearchRequestCompatWithNative(root, buildGeminiCompatContext(adapterContext));
71
24
  }
@@ -1,2 +1,3 @@
1
- import type { JsonObject } from '../../hub/types/json.js';
2
- export declare function extractGlmToolMarkup(root: JsonObject): void;
1
+ import type { AdapterContext } from "../../hub/types/chat-envelope.js";
2
+ import type { JsonObject } from "../../hub/types/json.js";
3
+ export declare function extractGlmToolMarkup(root: JsonObject, adapterContext?: AdapterContext): void;
@@ -1,264 +1,35 @@
1
- function flattenContent(content, depth = 0) {
2
- if (depth > 4 || content == null) {
3
- return '';
4
- }
5
- if (typeof content === 'string') {
6
- return content;
7
- }
8
- if (Array.isArray(content)) {
9
- return content.map((entry) => flattenContent(entry, depth + 1)).join('');
10
- }
11
- if (typeof content === 'object') {
12
- const record = content;
13
- if (typeof record.text === 'string') {
14
- return record.text;
15
- }
16
- if (record.content !== undefined) {
17
- return flattenContent(record.content, depth + 1);
18
- }
19
- }
20
- return '';
21
- }
22
- const GLM_CUSTOM_TAG = /<tool_call(?:\s+name="([^"]+)")?>([\s\S]*?)<\/tool_call>/gi;
23
- const GLM_TAGGED_SEQUENCE = /<tool_call(?:\s+name="([^"]+)")?\s*>([\s\S]*?)(?:<\/tool_call>|(?=<tool_call)|$)/gi;
24
- const GLM_TAGGED_BLOCK = /<arg_key>([\s\S]*?)<\/arg_key>\s*<arg_value>([\s\S]*?)<\/arg_value>/gi;
25
- const GLM_INLINE_NAME = /^[\s\r\n]*([A-Za-z0-9_.:-]+)/;
26
- const GENERIC_PATTERNS = [
27
- [
28
- /```(?:tool|function|tool_call|function_call)?\s*([\s\S]*?)\s*```/gi,
29
- (match) => ({ body: match[1] ?? '' })
30
- ],
31
- [
32
- /\[(tool_call|function_call)(?:\s+name="([^"]+)")?\]([\s\S]*?)\[\/\1\]/gi,
33
- (match) => ({ body: match[3] ?? '', nameHint: match[2] })
34
- ],
35
- [
36
- /(tool_call|function_call)\s*[:=]\s*({[\s\S]+?})/gi,
37
- (match) => ({ body: match[2] ?? '' })
38
- ]
39
- ];
40
- export function extractGlmToolMarkup(root) {
41
- const choicesRaw = root?.choices;
42
- const choices = Array.isArray(choicesRaw) ? choicesRaw : [];
43
- choices.forEach((choice, index) => {
44
- if (!choice || typeof choice !== 'object') {
45
- return;
46
- }
47
- const message = choice.message;
48
- if (!message || typeof message !== 'object') {
49
- return;
50
- }
51
- const msgRecord = message;
52
- const contentField = msgRecord.content;
53
- const reasoningField = msgRecord.reasoning_content;
54
- const text = contentField !== undefined
55
- ? flattenContent(contentField)
56
- : typeof reasoningField === 'string'
57
- ? reasoningField
58
- : '';
59
- if (!text) {
60
- return;
61
- }
62
- const extraction = extractToolCallsFromText(text, index + 1);
63
- if (!extraction) {
64
- return;
65
- }
66
- if (extraction.toolCalls.length) {
67
- msgRecord.tool_calls = extraction.toolCalls;
68
- if (contentField !== undefined) {
69
- msgRecord.content = null;
70
- }
71
- }
72
- if (extraction.reasoningText) {
73
- msgRecord.reasoning_content = extraction.reasoningText;
74
- }
75
- else if ('reasoning_content' in msgRecord) {
76
- delete msgRecord.reasoning_content;
77
- }
78
- });
79
- }
80
- function extractToolCallsFromText(text, choiceIndex) {
81
- const matches = [];
82
- const applyPattern = (pattern, factory) => {
83
- pattern.lastIndex = 0;
84
- let exec;
85
- while ((exec = pattern.exec(text))) {
86
- const payload = factory(exec);
87
- if (!payload)
88
- continue;
89
- const parsed = parseToolCall(payload.body, payload.nameHint);
90
- if (!parsed)
91
- continue;
92
- matches.push({
93
- start: exec.index,
94
- end: exec.index + exec[0].length,
95
- call: parsed
96
- });
97
- }
98
- };
99
- applyPattern(GLM_CUSTOM_TAG, (match) => ({ body: match[2] ?? '', nameHint: match[1] }));
100
- for (const [pattern, factory] of GENERIC_PATTERNS) {
101
- applyPattern(pattern, factory);
102
- }
103
- applyTaggedArgPatterns(text, matches);
104
- if (!matches.length && typeof text === 'string' && text.includes('<arg_key>')) {
105
- GLM_INLINE_NAME.lastIndex = 0;
106
- const inline = GLM_INLINE_NAME.exec(text);
107
- GLM_INLINE_NAME.lastIndex = 0;
108
- if (inline && inline[1]) {
109
- const name = inline[1].trim();
110
- const block = text.slice(inline[0].length);
111
- const argsRecord = parseTaggedArgBlock(block);
112
- if (name && argsRecord) {
113
- matches.push({
114
- start: 0,
115
- end: text.length,
116
- call: {
117
- name,
118
- args: safeStringify(argsRecord)
119
- }
120
- });
121
- }
122
- }
123
- }
124
- matches.sort((a, b) => a.start - b.start);
125
- const toolCalls = matches.map((entry, idx) => ({
126
- id: `glm_tool_${choiceIndex}_${idx + 1}`,
127
- type: 'function',
128
- function: {
129
- name: entry.call.name,
130
- arguments: entry.call.args
131
- }
132
- }));
133
- matches.sort((a, b) => b.start - a.start);
134
- let cleaned = text;
135
- for (const entry of matches) {
136
- cleaned = cleaned.slice(0, entry.start) + cleaned.slice(entry.end);
137
- }
138
- const reasoningText = cleaned.trim();
1
+ import { buildNativeReqOutboundCompatAdapterContext } from "../../hub/pipeline/compat/native-adapter-context.js";
2
+ import { runRespInboundStage3CompatWithNative } from "../../../router/virtual-router/engine-selection/native-hub-pipeline-req-outbound-semantics.js";
3
+ const PROFILE = "chat:glm";
4
+ const DEFAULT_PROVIDER_PROTOCOL = "openai-chat";
5
+ const DEFAULT_ENTRY_ENDPOINT = "/v1/chat/completions";
6
+ function buildGlmCompatContext(adapterContext) {
7
+ const nativeContext = buildNativeReqOutboundCompatAdapterContext(adapterContext);
139
8
  return {
140
- toolCalls,
141
- reasoningText: reasoningText.length ? reasoningText : undefined
9
+ ...nativeContext,
10
+ compatibilityProfile: PROFILE,
11
+ providerProtocol: nativeContext.providerProtocol ??
12
+ adapterContext?.providerProtocol ??
13
+ DEFAULT_PROVIDER_PROTOCOL,
14
+ entryEndpoint: nativeContext.entryEndpoint ??
15
+ adapterContext?.entryEndpoint ??
16
+ DEFAULT_ENTRY_ENDPOINT,
142
17
  };
143
18
  }
144
- function parseToolCall(body, nameHint) {
145
- if (!body || typeof body !== 'string') {
146
- return null;
147
- }
148
- const trimmed = body.trim();
149
- if (!trimmed.length) {
150
- return null;
151
- }
152
- try {
153
- const parsed = JSON.parse(trimmed);
154
- if (!parsed || typeof parsed !== 'object') {
155
- return null;
156
- }
157
- const record = parsed;
158
- const candidateName = (typeof record.name === 'string' && record.name.trim().length
159
- ? record.name.trim()
160
- : undefined) ??
161
- (typeof record.tool_name === 'string' && record.tool_name.trim().length
162
- ? record.tool_name.trim()
163
- : undefined) ??
164
- (typeof record.tool === 'string' && record.tool.trim().length
165
- ? record.tool.trim()
166
- : undefined) ??
167
- (nameHint && nameHint.trim().length ? nameHint.trim() : undefined);
168
- if (!candidateName) {
169
- return null;
170
- }
171
- const argsSource = record.arguments ??
172
- record.input ??
173
- record.params ??
174
- record.parameters ??
175
- record.payload ??
176
- {};
177
- let args = '{}';
178
- if (typeof argsSource === 'string' && argsSource.trim().length) {
179
- args = argsSource.trim();
180
- }
181
- else {
182
- try {
183
- args = JSON.stringify(argsSource ?? {});
184
- }
185
- catch {
186
- args = '{}';
187
- }
188
- }
189
- return { name: candidateName, args };
190
- }
191
- catch {
192
- return null;
193
- }
19
+ function buildGlmResponseCompatInput(payload, adapterContext) {
20
+ return {
21
+ payload,
22
+ adapterContext: buildGlmCompatContext(adapterContext),
23
+ explicitProfile: PROFILE,
24
+ };
194
25
  }
195
- function applyTaggedArgPatterns(text, matches) {
196
- if (!text || typeof text !== 'string') {
26
+ export function extractGlmToolMarkup(root, adapterContext) {
27
+ if (!root || typeof root !== "object" || Array.isArray(root)) {
197
28
  return;
198
29
  }
199
- GLM_TAGGED_SEQUENCE.lastIndex = 0;
200
- let exec;
201
- while ((exec = GLM_TAGGED_SEQUENCE.exec(text))) {
202
- let name = typeof exec[1] === 'string' ? exec[1].trim() : '';
203
- let block = exec[2] ?? '';
204
- if (!name) {
205
- const inline = GLM_INLINE_NAME.exec(block);
206
- if (inline && inline[1]) {
207
- name = inline[1].trim();
208
- block = block.slice(inline[0].length);
209
- }
210
- }
211
- if (!name) {
212
- continue;
213
- }
214
- const argsRecord = parseTaggedArgBlock(block);
215
- if (!argsRecord) {
216
- continue;
217
- }
218
- matches.push({
219
- start: exec.index,
220
- end: exec.index + exec[0].length,
221
- call: {
222
- name,
223
- args: safeStringify(argsRecord)
224
- }
225
- });
226
- }
227
- }
228
- function parseTaggedArgBlock(block) {
229
- if (!block || typeof block !== 'string') {
230
- return null;
231
- }
232
- const record = {};
233
- GLM_TAGGED_BLOCK.lastIndex = 0;
234
- let exec;
235
- while ((exec = GLM_TAGGED_BLOCK.exec(block))) {
236
- const key = typeof exec[1] === 'string' ? exec[1].trim() : '';
237
- if (!key) {
238
- continue;
239
- }
240
- const rawValue = typeof exec[2] === 'string' ? exec[2].trim() : '';
241
- record[key] = coerceTaggedValue(rawValue);
242
- }
243
- return Object.keys(record).length ? record : null;
244
- }
245
- function coerceTaggedValue(raw) {
246
- if (!raw) {
247
- return '';
248
- }
249
- const trimmed = raw.trim();
250
- try {
251
- return JSON.parse(trimmed);
252
- }
253
- catch {
254
- return trimmed;
255
- }
256
- }
257
- function safeStringify(value) {
258
- try {
259
- return JSON.stringify(value ?? {});
260
- }
261
- catch {
262
- return '{}';
263
- }
30
+ const normalized = runRespInboundStage3CompatWithNative(buildGlmResponseCompatInput(root, adapterContext)).payload;
31
+ Object.keys(root).forEach((key) => {
32
+ delete root[key];
33
+ });
34
+ Object.assign(root, normalized);
264
35
  }
@@ -1,12 +1,4 @@
1
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
2
  export declare function applyIflowToolTextFallback(payload: JsonObject, options?: {
11
3
  models?: string[];
12
4
  routeId?: string;
@@ -1,211 +1,29 @@
1
- const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
2
- function normalizeModel(value) {
3
- return typeof value === 'string' ? value.trim().toLowerCase() : '';
1
+ import { buildNativeReqOutboundCompatAdapterContext } from '../../hub/pipeline/compat/native-adapter-context.js';
2
+ import { applyIflowToolTextFallbackWithNative } from '../../../router/virtual-router/engine-selection/native-compat-action-semantics.js';
3
+ const PROFILE = 'chat:iflow';
4
+ const DEFAULT_PROVIDER_PROTOCOL = 'openai-chat';
5
+ const DEFAULT_ENTRY_ENDPOINT = '/v1/chat/completions';
6
+ function buildIflowCompatContext(adapterContext) {
7
+ const nativeContext = buildNativeReqOutboundCompatAdapterContext(adapterContext);
8
+ return {
9
+ ...nativeContext,
10
+ compatibilityProfile: nativeContext.compatibilityProfile ??
11
+ adapterContext?.compatibilityProfile ??
12
+ PROFILE,
13
+ providerProtocol: nativeContext.providerProtocol ??
14
+ adapterContext?.providerProtocol ??
15
+ DEFAULT_PROVIDER_PROTOCOL,
16
+ entryEndpoint: nativeContext.entryEndpoint ??
17
+ adapterContext?.entryEndpoint ??
18
+ DEFAULT_ENTRY_ENDPOINT,
19
+ };
4
20
  }
5
- function isWebSearchRoute(routeId) {
6
- if (typeof routeId !== 'string') {
7
- return false;
8
- }
9
- const normalized = routeId.trim().toLowerCase();
10
- return normalized.startsWith('web_search') || normalized.startsWith('search');
11
- }
12
- function hasNonEmptyArray(value) {
13
- return Array.isArray(value) && value.length > 0;
14
- }
15
- function ensureSystemMessage(messages) {
16
- const first = messages[0];
17
- if (isRecord(first) && typeof first.role === 'string' && first.role.toLowerCase() === 'system') {
18
- return first;
19
- }
20
- const sys = { role: 'system', content: '' };
21
- messages.unshift(sys);
22
- return sys;
23
- }
24
- function appendSystemText(sys, extra) {
25
- const prev = typeof sys.content === 'string' ? sys.content : '';
26
- if (prev.includes('## Tool Calls (Text Markup Mode)')) {
27
- return;
28
- }
29
- const combined = prev ? `${prev}\n\n${extra}` : extra;
30
- sys.content = combined;
31
- }
32
- function buildToolMarkupInstruction() {
33
- // Keep this concise. The response-side canonicalizer will harvest these blocks into tool_calls.
34
- return [
35
- '## Tool Calls (Text Markup Mode)',
36
- '',
37
- 'You MAY call tools by emitting one or more XML blocks with this exact format, and nothing else inside the block:',
38
- '',
39
- '<tool:exec_command>',
40
- '<command>...</command>',
41
- '<timeout_ms>10000</timeout_ms>',
42
- '</tool:exec_command>',
43
- '',
44
- '<tool:write_stdin>',
45
- '<session_id>...</session_id>',
46
- '<input>...</input>',
47
- '</tool:write_stdin>',
48
- '',
49
- 'Rules:',
50
- '- Use `<tool:exec_command>` to run shell commands.',
51
- '- Use `<tool:write_stdin>` to send input to an existing session.',
52
- '- Do NOT wrap these blocks in code fences.',
53
- '- Do NOT invent tools; only use exec_command and write_stdin.',
54
- ''
55
- ].join('\n');
56
- }
57
- function stringifyToolOutput(value) {
58
- if (typeof value === 'string')
59
- return value;
60
- if (value === null || value === undefined)
61
- return '';
62
- try {
63
- return JSON.stringify(value, null, 2);
64
- }
65
- catch {
66
- return String(value);
67
- }
68
- }
69
- function coerceToolCallToXmlBlock(toolCall) {
70
- if (!isRecord(toolCall))
71
- return null;
72
- const type = typeof toolCall.type === 'string' ? toolCall.type.trim().toLowerCase() : '';
73
- if (type && type !== 'function')
74
- return null;
75
- const fn = isRecord(toolCall.function) ? toolCall.function : null;
76
- const name = typeof fn?.name === 'string' ? fn.name.trim() : '';
77
- if (!name)
78
- return null;
79
- const argsRaw = fn?.arguments;
80
- let args = null;
81
- if (typeof argsRaw === 'string' && argsRaw.trim().length) {
82
- try {
83
- const parsed = JSON.parse(argsRaw);
84
- args = isRecord(parsed) ? parsed : null;
85
- }
86
- catch {
87
- args = { raw: argsRaw };
88
- }
89
- }
90
- else if (isRecord(argsRaw)) {
91
- args = argsRaw;
92
- }
93
- if (name === 'exec_command') {
94
- const command = typeof args?.cmd === 'string'
95
- ? args.cmd
96
- : typeof args?.command === 'string'
97
- ? args.command
98
- : '';
99
- if (!command)
100
- return null;
101
- const timeoutMs = typeof args?.timeout_ms === 'number' && Number.isFinite(args.timeout_ms)
102
- ? Math.floor(args.timeout_ms)
103
- : undefined;
104
- return [
105
- '<tool:exec_command>',
106
- `<command>${command}</command>`,
107
- ...(timeoutMs !== undefined ? [`<timeout_ms>${timeoutMs}</timeout_ms>`] : []),
108
- '</tool:exec_command>'
109
- ].join('\n');
110
- }
111
- if (name === 'write_stdin') {
112
- const sessionId = typeof args?.session_id === 'string'
113
- ? args.session_id
114
- : typeof args?.sessionId === 'string'
115
- ? args.sessionId
116
- : '';
117
- const input = typeof args?.chars === 'string'
118
- ? args.chars
119
- : typeof args?.input === 'string'
120
- ? args.input
121
- : '';
122
- if (!sessionId || !input)
123
- return null;
124
- return [
125
- '<tool:write_stdin>',
126
- `<session_id>${sessionId}</session_id>`,
127
- `<input>${input}</input>`,
128
- '</tool:write_stdin>'
129
- ].join('\n');
130
- }
131
- return null;
132
- }
133
- function rewriteMessageToolSurfaceInPlace(message) {
134
- const role = typeof message.role === 'string' ? message.role.trim().toLowerCase() : '';
135
- // Tool result messages are not supported by some iFlow models; convert them into plain text.
136
- if (role === 'tool') {
137
- const toolName = typeof message.name === 'string' ? message.name.trim() : '';
138
- const toolCallId = typeof message.tool_call_id === 'string' ? String(message.tool_call_id).trim() : '';
139
- const content = stringifyToolOutput(message.content);
140
- message.role = 'user';
141
- message.content = [
142
- 'Tool result:',
143
- ...(toolName ? [`name: ${toolName}`] : []),
144
- ...(toolCallId ? [`tool_call_id: ${toolCallId}`] : []),
145
- 'output:',
146
- content
147
- ].join('\n');
148
- delete message.name;
149
- delete message.tool_call_id;
150
- return;
151
- }
152
- // Assistant tool_calls are not supported by some iFlow models; convert them to XML tool markup.
153
- const toolCallsRaw = message.tool_calls;
154
- if (hasNonEmptyArray(toolCallsRaw)) {
155
- const blocks = toolCallsRaw
156
- .map(coerceToolCallToXmlBlock)
157
- .filter((b) => typeof b === 'string' && b.length > 0);
158
- if (blocks.length) {
159
- const prev = typeof message.content === 'string' ? message.content : '';
160
- const joined = blocks.join('\n\n');
161
- message.content = prev && prev.trim().length ? `${prev}\n\n${joined}` : joined;
162
- }
163
- delete message.tool_calls;
164
- }
165
- }
166
- /**
167
- * Some iFlow models reject OpenAI `tools`/`tool_choice` fields with a non-standard 200 error envelope.
168
- *
169
- * For such models, we fall back to "text tool calls":
170
- * - remove top-level `tools` / `tool_choice` so upstream accepts the request
171
- * - inject a system instruction telling the model to emit `<tool:...>` blocks
172
- * - response pipeline will harvest those blocks into canonical `tool_calls`
173
- */
174
21
  export function applyIflowToolTextFallback(payload, options) {
175
- if (!payload || typeof payload !== 'object') {
22
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
176
23
  return payload;
177
24
  }
178
- const root = structuredClone(payload);
179
- if (isWebSearchRoute(options?.routeId)) {
180
- // web_search/search routes are independent transport paths and must not be coupled
181
- // to model-gated fallback heuristics.
182
- return root;
183
- }
184
- const model = normalizeModel(root.model);
185
- const models = Array.isArray(options?.models) ? options.models.map((m) => normalizeModel(m)).filter(Boolean) : [];
186
- if (!model || !models.includes(model)) {
187
- return root;
188
- }
189
- // Only apply when we have a message array to carry the injected instruction.
190
- const messages = root.messages;
191
- if (!hasNonEmptyArray(messages)) {
192
- return root;
193
- }
194
- // Always strip tool schemas for fallback models (including followups),
195
- // otherwise the first turn might succeed but the tool loop breaks.
196
- if ('tools' in root) {
197
- delete root.tools;
198
- }
199
- if ('tool_choice' in root) {
200
- delete root.tool_choice;
201
- }
202
- // Rewrite message-level tool protocol to plain text so upstream accepts it.
203
- for (const msg of messages) {
204
- if (!isRecord(msg))
205
- continue;
206
- rewriteMessageToolSurfaceInPlace(msg);
207
- }
208
- const sys = ensureSystemMessage(messages);
209
- appendSystemText(sys, buildToolMarkupInstruction());
210
- return root;
25
+ const adapterContext = options?.routeId
26
+ ? { routeId: options.routeId }
27
+ : undefined;
28
+ return applyIflowToolTextFallbackWithNative(payload, buildIflowCompatContext(adapterContext), Array.isArray(options?.models) ? options?.models : []);
211
29
  }
@@ -1,3 +1,4 @@
1
+ import type { AdapterContext } from '../../hub/types/chat-envelope.js';
1
2
  import type { JsonObject } from '../../hub/types/json.js';
2
- export declare function applyQwenRequestTransform(payload: JsonObject): JsonObject;
3
- export declare function applyQwenResponseTransform(payload: JsonObject): JsonObject;
3
+ export declare function applyQwenRequestTransform(payload: JsonObject, adapterContext?: AdapterContext): JsonObject;
4
+ export declare function applyQwenResponseTransform(payload: JsonObject, adapterContext?: AdapterContext): JsonObject;