@jsonstudio/llms 0.6.3551 → 0.6.3686

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 (66) hide show
  1. package/dist/conversion/compat/actions/antigravity-thought-signature-cache.js +4 -115
  2. package/dist/conversion/compat/actions/auto-thinking.js +3 -2
  3. package/dist/conversion/compat/actions/deepseek-web-response.js +15 -49
  4. package/dist/conversion/compat/actions/gemini-cli-request.d.ts +2 -0
  5. package/dist/conversion/compat/actions/gemini-cli-request.js +1 -1
  6. package/dist/conversion/compat/actions/glm-history-image-trim.js +3 -37
  7. package/dist/conversion/compat/actions/glm-image-content.js +3 -32
  8. package/dist/conversion/compat/actions/glm-native-compat.d.ts +6 -0
  9. package/dist/conversion/compat/actions/glm-native-compat.js +34 -0
  10. package/dist/conversion/compat/actions/glm-vision-prompt.js +3 -76
  11. package/dist/conversion/compat/actions/glm-web-search.js +10 -43
  12. package/dist/conversion/compat/actions/iflow-kimi-cli-defaults.js +4 -53
  13. package/dist/conversion/compat/actions/iflow-kimi-history-media-placeholder.js +5 -141
  14. package/dist/conversion/compat/actions/iflow-kimi-thinking-reasoning-fill.js +7 -28
  15. package/dist/conversion/compat/actions/iflow-native-compat.d.ts +6 -0
  16. package/dist/conversion/compat/actions/iflow-native-compat.js +36 -0
  17. package/dist/conversion/compat/actions/iflow-response-body-unwrap.js +4 -119
  18. package/dist/conversion/compat/actions/iflow-web-search.js +14 -55
  19. package/dist/conversion/hub/operation-table/semantic-mappers/archive/chat-mapper.archive.js +5 -0
  20. package/dist/conversion/hub/operation-table/semantic-mappers/responses-mapper.js +31 -18
  21. package/dist/conversion/hub/pipeline/hub-pipeline.js +163 -163
  22. package/dist/conversion/hub/pipeline/hub-stage-timing.d.ts +6 -0
  23. package/dist/conversion/hub/pipeline/hub-stage-timing.js +178 -0
  24. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage1_format_parse/index.d.ts +4 -4
  25. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage1_format_parse/index.js +33 -14
  26. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.d.ts +7 -6
  27. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.js +41 -23
  28. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage1_semantic_map/index.js +44 -1
  29. package/dist/conversion/hub/pipeline/stages/req_process/req_process_stage1_tool_governance/index.js +4 -1
  30. package/dist/conversion/hub/process/chat-process-continue-execution.js +5 -4
  31. package/dist/conversion/hub/process/chat-process-governance-orchestration.js +3 -1
  32. package/dist/conversion/hub/process/chat-process-media.d.ts +3 -1
  33. package/dist/conversion/hub/process/chat-process-media.js +92 -2
  34. package/dist/conversion/hub/process/chat-process-session-usage.d.ts +7 -0
  35. package/dist/conversion/hub/process/chat-process-session-usage.js +147 -0
  36. package/dist/conversion/hub/response/provider-response.js +13 -0
  37. package/dist/conversion/responses/responses-openai-bridge/response-payload.js +0 -12
  38. package/dist/conversion/responses/responses-openai-bridge/types.d.ts +1 -9
  39. package/dist/conversion/responses/responses-openai-bridge.js +77 -44
  40. package/dist/conversion/shared/reasoning-normalizer.js +42 -0
  41. package/dist/conversion/shared/responses-tool-utils.js +2 -3
  42. package/dist/native/router_hotpath_napi.node +0 -0
  43. package/dist/router/virtual-router/bootstrap/profile-builder.js +1 -0
  44. package/dist/router/virtual-router/bootstrap/provider-normalization.d.ts +1 -0
  45. package/dist/router/virtual-router/bootstrap/provider-normalization.js +6 -0
  46. package/dist/router/virtual-router/bootstrap.js +1 -6
  47. package/dist/router/virtual-router/engine-legacy.js +43 -0
  48. package/dist/router/virtual-router/engine-logging.d.ts +3 -0
  49. package/dist/router/virtual-router/engine-logging.js +29 -3
  50. package/dist/router/virtual-router/engine-selection/native-hub-pipeline-edge-stage-semantics.d.ts +2 -2
  51. package/dist/router/virtual-router/engine-selection/native-hub-pipeline-edge-stage-semantics.js +96 -80
  52. package/dist/router/virtual-router/engine-selection/native-hub-pipeline-req-process-semantics.d.ts +1 -0
  53. package/dist/router/virtual-router/engine.js +34 -22
  54. package/dist/router/virtual-router/provider-registry.js +1 -0
  55. package/dist/router/virtual-router/routing-instructions/state.js +35 -3
  56. package/dist/router/virtual-router/routing-instructions/types.d.ts +4 -0
  57. package/dist/router/virtual-router/types.d.ts +7 -0
  58. package/dist/servertool/engine.js +3 -34
  59. package/dist/servertool/handlers/followup-request-builder.js +0 -6
  60. package/dist/servertool/handlers/gemini-empty-reply-continue.js +3 -274
  61. package/dist/servertool/handlers/stop-message-auto/runtime-utils.d.ts +0 -3
  62. package/dist/servertool/handlers/stop-message-auto/runtime-utils.js +0 -29
  63. package/dist/servertool/handlers/stop-message-auto.js +2 -10
  64. package/dist/servertool/handlers/vision.js +4 -1
  65. package/dist/servertool/server-side-tools.js +66 -3
  66. package/package.json +1 -1
@@ -1,88 +1,8 @@
1
+ import { runReqOutboundStage3CompatWithNative } from '../../../router/virtual-router/engine-selection/native-hub-pipeline-req-outbound-semantics.js';
2
+ import { buildIflowRequestCompatInput } from './iflow-native-compat.js';
1
3
  function isRecord(value) {
2
4
  return typeof value === 'object' && value !== null && !Array.isArray(value);
3
5
  }
4
- function detectMediaKind(part) {
5
- const typeValue = typeof part.type === 'string' ? part.type.trim().toLowerCase() : '';
6
- if (!typeValue) {
7
- return null;
8
- }
9
- if (typeValue.includes('video')) {
10
- return 'video';
11
- }
12
- if (typeValue.includes('image')) {
13
- return 'image';
14
- }
15
- return null;
16
- }
17
- function isInlineBase64(value) {
18
- const normalized = value.trim().toLowerCase();
19
- if (!normalized) {
20
- return false;
21
- }
22
- if (normalized.startsWith('data:') && normalized.includes(';base64,')) {
23
- return true;
24
- }
25
- if (normalized.startsWith('base64,')) {
26
- return true;
27
- }
28
- return false;
29
- }
30
- function mediaPartCarriesInlineBase64(part) {
31
- const candidates = [];
32
- const imageUrl = part.image_url;
33
- const videoUrl = part.video_url;
34
- if (typeof imageUrl === 'string') {
35
- candidates.push(imageUrl);
36
- }
37
- if (typeof videoUrl === 'string') {
38
- candidates.push(videoUrl);
39
- }
40
- if (isRecord(imageUrl)) {
41
- const url = typeof imageUrl.url === 'string' ? imageUrl.url : '';
42
- const data = typeof imageUrl.data === 'string' ? String(imageUrl.data) : '';
43
- const base64 = typeof imageUrl.base64 === 'string'
44
- ? String(imageUrl.base64)
45
- : '';
46
- if (url)
47
- candidates.push(url);
48
- if (data)
49
- candidates.push(data);
50
- if (base64)
51
- candidates.push(`base64,${base64}`);
52
- }
53
- if (isRecord(videoUrl)) {
54
- const url = typeof videoUrl.url === 'string' ? videoUrl.url : '';
55
- const data = typeof videoUrl.data === 'string' ? String(videoUrl.data) : '';
56
- const base64 = typeof videoUrl.base64 === 'string'
57
- ? String(videoUrl.base64)
58
- : '';
59
- if (url)
60
- candidates.push(url);
61
- if (data)
62
- candidates.push(data);
63
- if (base64)
64
- candidates.push(`base64,${base64}`);
65
- }
66
- const url = typeof part.url === 'string' ? String(part.url) : '';
67
- const uri = typeof part.uri === 'string' ? String(part.uri) : '';
68
- const data = typeof part.data === 'string' ? String(part.data) : '';
69
- const base64 = typeof part.base64 === 'string' ? String(part.base64) : '';
70
- if (url)
71
- candidates.push(url);
72
- if (uri)
73
- candidates.push(uri);
74
- if (data)
75
- candidates.push(data);
76
- if (base64)
77
- candidates.push(`base64,${base64}`);
78
- return candidates.some((value) => isInlineBase64(value));
79
- }
80
- function buildPlaceholderPart(kind) {
81
- return {
82
- type: 'text',
83
- text: kind === 'video' ? '[history_video_base64_omitted]' : '[history_image_base64_omitted]'
84
- };
85
- }
86
6
  /**
87
7
  * iFlow/Kimi multimodal compat:
88
8
  * - Preserve latest user turn media payload.
@@ -93,67 +13,11 @@ export function applyIflowKimiHistoryMediaPlaceholder(payload) {
93
13
  if (!isRecord(payload)) {
94
14
  return payload;
95
15
  }
96
- const root = structuredClone(payload);
97
- const model = typeof root.model === 'string' ? root.model.trim().toLowerCase() : '';
16
+ const model = typeof payload.model === 'string' ? payload.model.trim().toLowerCase() : '';
98
17
  if (model !== 'kimi-k2.5') {
99
- return root;
100
- }
101
- const messagesValue = root.messages;
102
- if (!Array.isArray(messagesValue) || !messagesValue.length) {
103
- return root;
104
- }
105
- const messages = messagesValue.map((item) => (isRecord(item) ? item : null));
106
- let latestUserIndex = -1;
107
- for (let index = messages.length - 1; index >= 0; index -= 1) {
108
- const message = messages[index];
109
- if (!message)
110
- continue;
111
- const role = typeof message.role === 'string' ? message.role.trim().toLowerCase() : '';
112
- if (role === 'user') {
113
- latestUserIndex = index;
114
- break;
115
- }
116
- }
117
- if (latestUserIndex <= 0) {
118
- return root;
119
- }
120
- const nextMessages = [];
121
- for (let index = 0; index < messages.length; index += 1) {
122
- const message = messages[index];
123
- if (!message)
124
- continue;
125
- if (index >= latestUserIndex) {
126
- nextMessages.push(message);
127
- continue;
128
- }
129
- const contentValue = message.content;
130
- if (!Array.isArray(contentValue)) {
131
- nextMessages.push(message);
132
- continue;
133
- }
134
- let changed = false;
135
- const nextContent = contentValue.map((part) => {
136
- if (!isRecord(part)) {
137
- return part;
138
- }
139
- const mediaKind = detectMediaKind(part);
140
- if (!mediaKind) {
141
- return part;
142
- }
143
- if (!mediaPartCarriesInlineBase64(part)) {
144
- return part;
145
- }
146
- changed = true;
147
- return buildPlaceholderPart(mediaKind);
148
- });
149
- if (!changed) {
150
- nextMessages.push(message);
151
- continue;
152
- }
153
- nextMessages.push({ ...message, content: nextContent });
18
+ return payload;
154
19
  }
155
- root.messages = nextMessages;
156
- return root;
20
+ return runReqOutboundStage3CompatWithNative(buildIflowRequestCompatInput(payload)).payload;
157
21
  }
158
22
  catch {
159
23
  return payload;
@@ -1,9 +1,8 @@
1
+ import { runReqOutboundStage3CompatWithNative } from '../../../router/virtual-router/engine-selection/native-hub-pipeline-req-outbound-semantics.js';
2
+ import { buildIflowRequestCompatInput } from './iflow-native-compat.js';
1
3
  function isRecord(value) {
2
4
  return typeof value === 'object' && value !== null && !Array.isArray(value);
3
5
  }
4
- function hasNonEmptyArray(value) {
5
- return Array.isArray(value) && value.length > 0;
6
- }
7
6
  function isThinkingDisabled(value) {
8
7
  if (value === false)
9
8
  return true;
@@ -32,34 +31,14 @@ export function fillIflowKimiThinkingReasoningContent(payload) {
32
31
  if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
33
32
  return payload;
34
33
  }
35
- const root = structuredClone(payload);
36
- const model = typeof root.model === 'string' ? root.model.trim().toLowerCase() : '';
34
+ const model = typeof payload.model === 'string' ? payload.model.trim().toLowerCase() : '';
37
35
  if (!(model === 'kimi-k2.5' || model.startsWith('kimi-k2.5-'))) {
38
- return root;
39
- }
40
- if (isThinkingDisabled(root.thinking)) {
41
- return root;
42
- }
43
- const messages = Array.isArray(root.messages) ? root.messages : [];
44
- if (!messages.length) {
45
- return root;
36
+ return payload;
46
37
  }
47
- for (const msg of messages) {
48
- if (!isRecord(msg))
49
- continue;
50
- const role = typeof msg.role === 'string' ? msg.role.trim().toLowerCase() : '';
51
- if (role !== 'assistant')
52
- continue;
53
- if (!hasNonEmptyArray(msg.tool_calls))
54
- continue;
55
- const rc = msg.reasoning_content;
56
- const rcText = typeof rc === 'string' ? rc : '';
57
- if (!rcText || !rcText.trim()) {
58
- msg.reasoning_content = '.';
59
- }
38
+ if (isThinkingDisabled(payload.thinking)) {
39
+ return payload;
60
40
  }
61
- root.messages = messages;
62
- return root;
41
+ return runReqOutboundStage3CompatWithNative(buildIflowRequestCompatInput(payload)).payload;
63
42
  }
64
43
  catch {
65
44
  return payload;
@@ -0,0 +1,6 @@
1
+ import type { AdapterContext } from '../../hub/types/chat-envelope.js';
2
+ import type { JsonObject } from '../../hub/types/json.js';
3
+ import type { NativeReqOutboundCompatAdapterContextInput, NativeReqOutboundStage3CompatInput, NativeRespInboundStage3CompatInput } from '../../../router/virtual-router/engine-selection/native-hub-pipeline-req-outbound-semantics.js';
4
+ export declare function buildIflowCompatContext(adapterContext?: AdapterContext): NativeReqOutboundCompatAdapterContextInput;
5
+ export declare function buildIflowRequestCompatInput(payload: JsonObject, adapterContext?: AdapterContext): NativeReqOutboundStage3CompatInput;
6
+ export declare function buildIflowResponseCompatInput(payload: JsonObject, adapterContext?: AdapterContext): NativeRespInboundStage3CompatInput;
@@ -0,0 +1,36 @@
1
+ import { buildNativeReqOutboundCompatAdapterContext } from '../../hub/pipeline/compat/native-adapter-context.js';
2
+ const PROFILE = 'chat:iflow';
3
+ const DEFAULT_PROVIDER_PROTOCOL = 'openai-chat';
4
+ const DEFAULT_ENTRY_ENDPOINT = '/v1/chat/completions';
5
+ export function buildIflowCompatContext(adapterContext) {
6
+ const nativeContext = buildNativeReqOutboundCompatAdapterContext(adapterContext);
7
+ const adapterProfile = adapterContext && typeof adapterContext['compatibilityProfile'] === 'string'
8
+ ? String(adapterContext['compatibilityProfile']).trim() || undefined
9
+ : undefined;
10
+ return {
11
+ ...nativeContext,
12
+ compatibilityProfile: nativeContext.compatibilityProfile ??
13
+ adapterProfile ??
14
+ PROFILE,
15
+ providerProtocol: nativeContext.providerProtocol ??
16
+ adapterContext?.providerProtocol ??
17
+ DEFAULT_PROVIDER_PROTOCOL,
18
+ entryEndpoint: nativeContext.entryEndpoint ??
19
+ adapterContext?.entryEndpoint ??
20
+ DEFAULT_ENTRY_ENDPOINT
21
+ };
22
+ }
23
+ export function buildIflowRequestCompatInput(payload, adapterContext) {
24
+ return {
25
+ payload,
26
+ adapterContext: buildIflowCompatContext(adapterContext),
27
+ explicitProfile: PROFILE
28
+ };
29
+ }
30
+ export function buildIflowResponseCompatInput(payload, adapterContext) {
31
+ return {
32
+ payload,
33
+ adapterContext: buildIflowCompatContext(adapterContext),
34
+ explicitProfile: PROFILE
35
+ };
36
+ }
@@ -1,99 +1,6 @@
1
+ import { runRespInboundStage3CompatWithNative } from '../../../router/virtual-router/engine-selection/native-hub-pipeline-req-outbound-semantics.js';
2
+ import { buildIflowResponseCompatInput } from './iflow-native-compat.js';
1
3
  const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
2
- function looksLikeKnownProviderResponseShape(value) {
3
- if (!isRecord(value))
4
- return false;
5
- if (Array.isArray(value.choices))
6
- return true; // openai-chat
7
- if (Array.isArray(value.output) || value.object === 'response')
8
- return true; // openai-responses
9
- if (typeof value.type === 'string' && String(value.type).toLowerCase() === 'message' && Array.isArray(value.content)) {
10
- return true; // anthropic-messages
11
- }
12
- if (Array.isArray(value.candidates))
13
- return true; // gemini-chat
14
- return false;
15
- }
16
- function stripJsonTextPrefix(text) {
17
- let out = String(text || '').trimStart();
18
- // anti-XSSI prefix: ")]}',\n{...}"
19
- out = out.replace(/^\)\]\}',?\s*/u, '');
20
- // single-line SSE-like wrapper sometimes returned as plain text
21
- out = out.replace(/^data:\s*/iu, '');
22
- return out;
23
- }
24
- function tryParseJsonRecord(text) {
25
- try {
26
- const trimmed = stripJsonTextPrefix(text).trim();
27
- if (!trimmed)
28
- return null;
29
- if (trimmed.length > 10 * 1024 * 1024)
30
- return null;
31
- const first = trimmed.charAt(0);
32
- const last = trimmed.charAt(trimmed.length - 1);
33
- if ((first !== '{' && first !== '[') || (last !== '}' && last !== ']')) {
34
- return null;
35
- }
36
- const parsed = JSON.parse(trimmed);
37
- return isRecord(parsed) ? parsed : null;
38
- }
39
- catch {
40
- return null;
41
- }
42
- }
43
- function tryParseJsonRecordFromMaybeSseText(text) {
44
- if (typeof text !== 'string') {
45
- return null;
46
- }
47
- const raw = stripJsonTextPrefix(text);
48
- const direct = tryParseJsonRecord(raw);
49
- if (direct) {
50
- return direct;
51
- }
52
- // Multi-line SSE text occasionally shows up as a plain string inside iFlow envelopes.
53
- // Best-effort: parse the last valid `data:` JSON object chunk.
54
- if (raw.length > 10 * 1024 * 1024) {
55
- return null;
56
- }
57
- const lines = raw.split(/\r?\n/u);
58
- for (let i = lines.length - 1; i >= 0; i -= 1) {
59
- const line = lines[i]?.trim();
60
- if (!line)
61
- continue;
62
- const lower = line.toLowerCase();
63
- if (lower === 'data: [done]' || lower === '[done]')
64
- continue;
65
- const candidate = lower.startsWith('data:') ? line.slice(5).trim() : line;
66
- const parsed = tryParseJsonRecord(candidate);
67
- if (parsed) {
68
- return parsed;
69
- }
70
- }
71
- return null;
72
- }
73
- function tryUnwrapKnownShapeFromBody(value, depth) {
74
- if (depth < 0)
75
- return null;
76
- if (looksLikeKnownProviderResponseShape(value)) {
77
- return value;
78
- }
79
- if (isRecord(value)) {
80
- // nested body/data wrappers
81
- for (const key of ['data', 'body', 'response', 'payload', 'result']) {
82
- const nested = value[key];
83
- const unwrapped = tryUnwrapKnownShapeFromBody(nested, depth - 1);
84
- if (unwrapped)
85
- return unwrapped;
86
- }
87
- return null;
88
- }
89
- if (typeof value === 'string') {
90
- const parsed = tryParseJsonRecord(value) ?? tryParseJsonRecordFromMaybeSseText(value);
91
- if (!parsed)
92
- return null;
93
- return tryUnwrapKnownShapeFromBody(parsed, depth - 1);
94
- }
95
- return null;
96
- }
97
4
  /**
98
5
  * iFlow compatibility: some iFlow backends wrap OpenAI-compatible payloads inside:
99
6
  * { status, msg, body, request_id }
@@ -107,32 +14,10 @@ export function unwrapIflowResponseBodyEnvelope(payload) {
107
14
  return payload;
108
15
  }
109
16
  const root = structuredClone(payload);
110
- // Only unwrap when it looks like the iFlow envelope.
111
17
  if (!('body' in root) || !('status' in root) || !('msg' in root)) {
112
- return root;
113
- }
114
- // If the root is already a known provider response, don't touch it.
115
- if (looksLikeKnownProviderResponseShape(root)) {
116
- return root;
117
- }
118
- const body = root.body;
119
- const unwrapped = tryUnwrapKnownShapeFromBody(body, 6);
120
- if (unwrapped) {
121
- return unwrapped;
122
- }
123
- // Fallback: unwrap to `body` even when we can't yet recognize the nested shape.
124
- // This avoids treating the iFlow envelope as the provider payload and improves
125
- // downstream diagnostics / error handling.
126
- if (isRecord(body)) {
127
- return body;
128
- }
129
- if (typeof body === 'string') {
130
- const parsed = tryParseJsonRecordFromMaybeSseText(body);
131
- if (parsed) {
132
- return parsed;
133
- }
18
+ return payload;
134
19
  }
135
- return root;
20
+ return runRespInboundStage3CompatWithNative(buildIflowResponseCompatInput(payload)).payload;
136
21
  }
137
22
  catch {
138
23
  return payload;
@@ -1,3 +1,5 @@
1
+ import { runReqOutboundStage3CompatWithNative } from '../../../router/virtual-router/engine-selection/native-hub-pipeline-req-outbound-semantics.js';
2
+ import { buildIflowRequestCompatInput } from './iflow-native-compat.js';
1
3
  const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
2
4
  const DEBUG_IFLOW_WEB_SEARCH = (process.env.ROUTECODEX_DEBUG_IFLOW_WEB_SEARCH || '').trim() === '1';
3
5
  /**
@@ -18,63 +20,20 @@ const DEBUG_IFLOW_WEB_SEARCH = (process.env.ROUTECODEX_DEBUG_IFLOW_WEB_SEARCH ||
18
20
  export function applyIflowWebSearchRequestTransform(payload, adapterContext) {
19
21
  const routeId = typeof adapterContext?.routeId === 'string' ? adapterContext.routeId : '';
20
22
  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
23
  if (!normalizedRoute || (!normalizedRoute.startsWith('web_search') && !normalizedRoute.startsWith('search'))) {
24
24
  return payload;
25
25
  }
26
- const root = structuredClone(payload);
27
- const webSearchRaw = root.web_search;
28
- if (!isRecord(webSearchRaw)) {
29
- return root;
30
- }
31
- const webSearch = webSearchRaw;
32
- const queryValue = webSearch.query;
33
- const recencyValue = webSearch.recency;
34
- const countValue = webSearch.count;
35
- const query = typeof queryValue === 'string' ? queryValue.trim() : '';
36
- const recency = typeof recencyValue === 'string' ? recencyValue.trim() : undefined;
37
- let count;
38
- if (typeof countValue === 'number' && Number.isFinite(countValue)) {
39
- const normalized = Math.floor(countValue);
40
- if (normalized >= 1 && normalized <= 50) {
41
- count = normalized;
42
- }
43
- }
44
- if (!query) {
45
- // No meaningful search query, drop the helper object and passthrough.
46
- delete root.web_search;
47
- return root;
48
- }
49
- const tool = {
50
- type: 'function',
51
- function: {
52
- name: 'web_search',
53
- description: 'Perform web search over the public internet and return up-to-date results.',
54
- parameters: {
55
- type: 'object',
56
- properties: {
57
- query: {
58
- type: 'string',
59
- description: 'Search query string.'
60
- },
61
- recency: {
62
- type: 'string',
63
- description: 'Optional recency filter such as "day", "week", or "month".'
64
- },
65
- count: {
66
- type: 'integer',
67
- minimum: 1,
68
- maximum: 50,
69
- description: 'Maximum number of search results to retrieve (1-50).'
70
- }
71
- },
72
- required: ['query']
73
- }
74
- }
75
- };
76
- root.tools = [tool];
77
- delete root.web_search;
26
+ const normalized = runReqOutboundStage3CompatWithNative(buildIflowRequestCompatInput(payload, adapterContext)).payload;
27
+ const query = isRecord(payload) && isRecord(payload.web_search)
28
+ ? typeof (payload.web_search?.query) === 'string'
29
+ ? String(payload.web_search?.query).trim()
30
+ : ''
31
+ : '';
32
+ const recency = isRecord(payload) && isRecord(payload.web_search)
33
+ ? typeof (payload.web_search?.recency) === 'string'
34
+ ? String(payload.web_search?.recency).trim()
35
+ : undefined
36
+ : undefined;
78
37
  if (DEBUG_IFLOW_WEB_SEARCH) {
79
38
  try {
80
39
  // eslint-disable-next-line no-console
@@ -86,5 +45,5 @@ export function applyIflowWebSearchRequestTransform(payload, adapterContext) {
86
45
  // logging best-effort
87
46
  }
88
47
  }
89
- return root;
48
+ return normalized;
90
49
  }
@@ -2,6 +2,11 @@ import { isJsonObject, jsonClone } from '../../../types/json.js';
2
2
  import { normalizeChatMessageContentWithNative, normalizeOpenaiChatMessagesWithNative } from '../../../../../router/virtual-router/engine-selection/native-shared-conversion-semantics.js';
3
3
  import { ensureProtocolState } from '../../../../protocol-state.js';
4
4
  import { mapReqInboundBridgeToolsToChatWithNative } from '../../../../../router/virtual-router/engine-selection/native-hub-pipeline-req-inbound-semantics.js';
5
+ const ALLOW_ARCHIVE_IMPORTS = process.env.LLMSWITCH_ALLOW_ARCHIVE_IMPORTS === '1' ||
6
+ process.env.ROUTECODEX_ALLOW_ARCHIVE_IMPORTS === '1';
7
+ if (!ALLOW_ARCHIVE_IMPORTS) {
8
+ throw new Error('[archive] chat-mapper.archive is fail-closed. Set LLMSWITCH_ALLOW_ARCHIVE_IMPORTS=1 only for explicit parity/compare scripts.');
9
+ }
5
10
  const CHAT_PARAMETER_KEYS = [
6
11
  'model',
7
12
  'temperature',
@@ -1,5 +1,6 @@
1
1
  import { isJsonObject, jsonClone } from '../../types/json.js';
2
2
  import { captureResponsesContext, buildChatRequestFromResponses, buildResponsesRequestFromChat } from '../../../responses/responses-openai-bridge.js';
3
+ import { logHubStageTiming } from '../../pipeline/hub-stage-timing.js';
3
4
  import { maybeAugmentApplyPatchErrorContent } from './chat-mapper.js';
4
5
  import { mapReqInboundBridgeToolsToChatWithNative, mapReqInboundResumeToolOutputsDetailedWithNative } from '../../../../router/virtual-router/engine-selection/native-hub-pipeline-req-inbound-semantics.js';
5
6
  const RESPONSES_PARAMETER_KEYS = [
@@ -337,29 +338,19 @@ function collectSubmitToolOutputs(chat, responsesContext) {
337
338
  }
338
339
  return outputs;
339
340
  }
340
- function resolveSubmitStreamFlag(chat, _ctx, responsesContext) {
341
+ function resolveSubmitStreamFlag(chat, _ctx, _responsesContext) {
341
342
  if (chat.parameters && typeof chat.parameters.stream === 'boolean') {
342
343
  return chat.parameters.stream;
343
344
  }
344
- if (responsesContext && typeof responsesContext.stream === 'boolean') {
345
- return responsesContext.stream;
346
- }
347
345
  return undefined;
348
346
  }
349
- function resolveSubmitModel(chat, responsesContext) {
347
+ function resolveSubmitModel(chat, _responsesContext) {
350
348
  const direct = chat.parameters && typeof chat.parameters.model === 'string'
351
349
  ? chat.parameters.model.trim()
352
350
  : undefined;
353
351
  if (direct) {
354
352
  return direct;
355
353
  }
356
- if (responsesContext && typeof responsesContext.parameters === 'object') {
357
- const params = responsesContext.parameters;
358
- const model = typeof params.model === 'string' ? params.model.trim() : undefined;
359
- if (model) {
360
- return model;
361
- }
362
- }
363
354
  return undefined;
364
355
  }
365
356
  function buildSubmitToolOutputsPayload(chat, ctx, responsesContext) {
@@ -400,13 +391,26 @@ function buildSubmitToolOutputsPayload(chat, ctx, responsesContext) {
400
391
  }
401
392
  export class ResponsesSemanticMapper {
402
393
  async toChat(format, ctx) {
394
+ const requestId = typeof ctx.requestId === 'string' && ctx.requestId.trim().length ? ctx.requestId : 'unknown';
403
395
  const payload = format.payload || {};
396
+ logHubStageTiming(requestId, 'req_inbound.responses.capture_context', 'start');
397
+ const captureStart = Date.now();
404
398
  const responsesContext = captureResponsesContext(payload, { route: { requestId: ctx.requestId } });
399
+ logHubStageTiming(requestId, 'req_inbound.responses.capture_context', 'completed', {
400
+ elapsedMs: Date.now() - captureStart,
401
+ forceLog: true
402
+ });
403
+ logHubStageTiming(requestId, 'req_inbound.responses.build_chat_request', 'start');
404
+ const buildChatStart = Date.now();
405
405
  const { request, toolsNormalized } = buildChatRequestFromResponses(payload, responsesContext);
406
+ logHubStageTiming(requestId, 'req_inbound.responses.build_chat_request', 'completed', {
407
+ elapsedMs: Date.now() - buildChatStart,
408
+ forceLog: true
409
+ });
406
410
  const missingFields = [];
407
411
  const messages = normalizeMessages(request.messages, missingFields);
408
412
  let toolOutputs = mapToolOutputs(payload.tool_outputs, missingFields);
409
- const parameters = collectParameters(payload, responsesContext.stream);
413
+ const parameters = collectParameters(payload, typeof payload.stream === 'boolean' ? payload.stream : undefined);
410
414
  const metadata = { context: ctx };
411
415
  if (missingFields.length) {
412
416
  metadata.missingFields = missingFields;
@@ -423,6 +427,7 @@ export class ResponsesSemanticMapper {
423
427
  };
424
428
  }
425
429
  async fromChat(chat, ctx) {
430
+ const requestId = typeof ctx.requestId === 'string' && ctx.requestId.trim().length ? ctx.requestId : 'unknown';
426
431
  const envelopeMetadata = chat.metadata && isJsonObject(chat.metadata) ? chat.metadata : undefined;
427
432
  const responsesContext = selectResponsesContextSnapshot(chat, envelopeMetadata);
428
433
  if (isSubmitToolOutputsEndpoint(ctx)) {
@@ -447,12 +452,20 @@ export class ResponsesSemanticMapper {
447
452
  messages: chat.messages,
448
453
  tools: chat.tools
449
454
  };
450
- const originalSystemMessages = (chat.messages || [])
451
- .filter((message) => Boolean(message && typeof message === 'object' && message.role === 'system'))
452
- .map(message => serializeSystemContent(message))
453
- .filter((content) => typeof content === 'string' && content.length > 0);
454
- responsesContext.originalSystemMessages = originalSystemMessages;
455
+ if (!Array.isArray(responsesContext.originalSystemMessages) || responsesContext.originalSystemMessages.length === 0) {
456
+ const originalSystemMessages = (chat.messages || [])
457
+ .filter((message) => Boolean(message && typeof message === 'object' && message.role === 'system'))
458
+ .map(message => serializeSystemContent(message))
459
+ .filter((content) => typeof content === 'string' && content.length > 0);
460
+ responsesContext.originalSystemMessages = originalSystemMessages;
461
+ }
462
+ logHubStageTiming(requestId, 'req_outbound.responses.build_request', 'start');
463
+ const buildRequestStart = Date.now();
455
464
  const responsesResult = buildResponsesRequestFromChat(requestShape, responsesContext);
465
+ logHubStageTiming(requestId, 'req_outbound.responses.build_request', 'completed', {
466
+ elapsedMs: Date.now() - buildRequestStart,
467
+ forceLog: true
468
+ });
456
469
  const responses = responsesResult.request;
457
470
  if (chat.parameters && chat.parameters.stream !== undefined) {
458
471
  responses.stream = chat.parameters.stream;