@jsonstudio/llms 0.6.1643 → 0.6.1739

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 (96) hide show
  1. package/dist/conversion/compat/actions/harvest-tool-calls-from-text.d.ts +10 -0
  2. package/dist/conversion/compat/actions/harvest-tool-calls-from-text.js +121 -0
  3. package/dist/conversion/compat/actions/iflow-kimi-cli-defaults.d.ts +10 -0
  4. package/dist/conversion/compat/actions/iflow-kimi-cli-defaults.js +80 -0
  5. package/dist/conversion/compat/actions/iflow-kimi-history-media-placeholder.d.ts +7 -0
  6. package/dist/conversion/compat/actions/iflow-kimi-history-media-placeholder.js +161 -0
  7. package/dist/conversion/compat/actions/iflow-kimi-thinking-reasoning-fill.d.ts +12 -0
  8. package/dist/conversion/compat/actions/iflow-kimi-thinking-reasoning-fill.js +67 -0
  9. package/dist/conversion/compat/actions/iflow-response-body-unwrap.d.ts +9 -0
  10. package/dist/conversion/compat/actions/iflow-response-body-unwrap.js +140 -0
  11. package/dist/conversion/compat/actions/lmstudio-responses-fc-ids.d.ts +10 -0
  12. package/dist/conversion/compat/actions/lmstudio-responses-fc-ids.js +59 -0
  13. package/dist/conversion/compat/actions/lmstudio-responses-input-stringify.d.ts +14 -0
  14. package/dist/conversion/compat/actions/lmstudio-responses-input-stringify.js +125 -0
  15. package/dist/conversion/compat/actions/normalize-tool-call-ids.d.ts +11 -0
  16. package/dist/conversion/compat/actions/normalize-tool-call-ids.js +140 -0
  17. package/dist/conversion/compat/actions/strip-orphan-function-calls-tag.d.ts +2 -0
  18. package/dist/conversion/compat/actions/strip-orphan-function-calls-tag.js +152 -0
  19. package/dist/conversion/compat/antigravity-session-signature.d.ts +1 -1
  20. package/dist/conversion/compat/antigravity-session-signature.js +5 -4
  21. package/dist/conversion/compat/profiles/chat-iflow.json +6 -0
  22. package/dist/conversion/compat/profiles/chat-lmstudio.json +7 -1
  23. package/dist/conversion/hub/operation-table/operation-table-runner.js +1 -1
  24. package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.js +19 -2
  25. package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +101 -5
  26. package/dist/conversion/hub/pipeline/compat/compat-profile-resolver.d.ts +2 -0
  27. package/dist/conversion/hub/pipeline/compat/compat-profile-resolver.js +63 -0
  28. package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +18 -0
  29. package/dist/conversion/hub/pipeline/hub-pipeline.js +1 -1
  30. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage1_semantic_map/index.js +8 -5
  31. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage3_compat/index.js +5 -1
  32. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/index.js +113 -0
  33. package/dist/conversion/hub/pipeline/target-utils.js +3 -0
  34. package/dist/conversion/hub/response/provider-response.js +27 -1
  35. package/dist/conversion/responses/responses-openai-bridge.js +32 -6
  36. package/dist/conversion/shared/anthropic-message-utils.js +20 -5
  37. package/dist/conversion/shared/bridge-id-utils.d.ts +2 -0
  38. package/dist/conversion/shared/bridge-id-utils.js +52 -15
  39. package/dist/conversion/shared/responses-conversation-store.js +40 -5
  40. package/dist/conversion/shared/responses-output-builder.js +23 -7
  41. package/dist/conversion/shared/responses-tool-utils.d.ts +1 -0
  42. package/dist/conversion/shared/responses-tool-utils.js +30 -13
  43. package/dist/conversion/shared/text-markup-normalizer.d.ts +1 -0
  44. package/dist/conversion/shared/text-markup-normalizer.js +269 -1
  45. package/dist/router/virtual-router/bootstrap.js +31 -7
  46. package/dist/router/virtual-router/classifier.js +1 -1
  47. package/dist/router/virtual-router/engine/antigravity/alias-lease.d.ts +33 -0
  48. package/dist/router/virtual-router/engine/antigravity/alias-lease.js +247 -0
  49. package/dist/router/virtual-router/engine/health/index.d.ts +23 -0
  50. package/dist/router/virtual-router/engine/health/index.js +720 -0
  51. package/dist/router/virtual-router/engine/provider-key/parse.d.ts +6 -0
  52. package/dist/router/virtual-router/engine/provider-key/parse.js +43 -0
  53. package/dist/router/virtual-router/engine/routing-pools/index.d.ts +13 -0
  54. package/dist/router/virtual-router/engine/routing-pools/index.js +225 -0
  55. package/dist/router/virtual-router/engine/routing-state/keys.d.ts +3 -0
  56. package/dist/router/virtual-router/engine/routing-state/keys.js +30 -0
  57. package/dist/router/virtual-router/engine/routing-state/metadata.d.ts +6 -0
  58. package/dist/router/virtual-router/engine/routing-state/metadata.js +132 -0
  59. package/dist/router/virtual-router/engine/routing-state/store.d.ts +11 -0
  60. package/dist/router/virtual-router/engine/routing-state/store.js +107 -0
  61. package/dist/router/virtual-router/engine-health.d.ts +1 -23
  62. package/dist/router/virtual-router/engine-health.js +1 -720
  63. package/dist/router/virtual-router/engine-selection/route-utils.js +57 -0
  64. package/dist/router/virtual-router/engine-selection/tier-selection-select.js +8 -48
  65. package/dist/router/virtual-router/engine-selection/tier-selection.js +34 -17
  66. package/dist/router/virtual-router/engine-selection.d.ts +1 -13
  67. package/dist/router/virtual-router/engine-selection.js +1 -225
  68. package/dist/router/virtual-router/engine.d.ts +2 -23
  69. package/dist/router/virtual-router/engine.js +130 -603
  70. package/dist/router/virtual-router/message-utils.js +15 -5
  71. package/dist/servertool/engine.js +4 -4
  72. package/dist/servertool/handlers/followup-request-builder.js +46 -0
  73. package/dist/servertool/handlers/gemini-empty-reply-continue.js +48 -47
  74. package/dist/servertool/handlers/stop-message-auto.js +64 -7
  75. package/dist/servertool/handlers/vision.js +10 -0
  76. package/dist/servertool/types.d.ts +3 -0
  77. package/dist/sse/sse-to-json/builders/response-builder.js +6 -0
  78. package/dist/sse/sse-to-json/chat-sse-to-json-converter.js +32 -2
  79. package/dist/sse/sse-to-json/parsers/sse-parser.js +34 -0
  80. package/dist/sse/sse-to-json/responses-sse-to-json-converter.d.ts +1 -0
  81. package/dist/sse/sse-to-json/responses-sse-to-json-converter.js +33 -1
  82. package/dist/tools/apply-patch/args-normalizer/default-actions.d.ts +2 -0
  83. package/dist/tools/apply-patch/args-normalizer/default-actions.js +12 -0
  84. package/dist/tools/apply-patch/args-normalizer/extract-patch.d.ts +2 -0
  85. package/dist/tools/apply-patch/args-normalizer/extract-patch.js +15 -0
  86. package/dist/tools/apply-patch/args-normalizer/index.d.ts +2 -0
  87. package/dist/tools/apply-patch/args-normalizer/index.js +164 -0
  88. package/dist/tools/apply-patch/args-normalizer/structured-builders.d.ts +7 -0
  89. package/dist/tools/apply-patch/args-normalizer/structured-builders.js +85 -0
  90. package/dist/tools/apply-patch/args-normalizer/types.d.ts +54 -0
  91. package/dist/tools/apply-patch/args-normalizer/types.js +1 -0
  92. package/dist/tools/apply-patch/patch-text/looks-like-patch.js +1 -0
  93. package/dist/tools/apply-patch/patch-text/normalize.js +104 -5
  94. package/dist/tools/apply-patch/structured/coercion.js +28 -4
  95. package/dist/tools/apply-patch/validator.js +7 -146
  96. package/package.json +1 -1
@@ -14,6 +14,10 @@ import { applyGlmWebSearchRequestTransform } from '../../../compat/actions/glm-w
14
14
  import { applyGeminiWebSearchCompat } from '../../../compat/actions/gemini-web-search.js';
15
15
  import { applyIflowWebSearchRequestTransform } from '../../../compat/actions/iflow-web-search.js';
16
16
  import { applyIflowToolTextFallback } from '../../../compat/actions/iflow-tool-text-fallback.js';
17
+ import { unwrapIflowResponseBodyEnvelope } from '../../../compat/actions/iflow-response-body-unwrap.js';
18
+ import { applyIflowKimiHistoryMediaPlaceholder } from '../../../compat/actions/iflow-kimi-history-media-placeholder.js';
19
+ import { applyIflowKimiCliDefaults } from '../../../compat/actions/iflow-kimi-cli-defaults.js';
20
+ import { fillIflowKimiThinkingReasoningContent } from '../../../compat/actions/iflow-kimi-thinking-reasoning-fill.js';
17
21
  import { applyGlmImageContentTransform } from '../../../compat/actions/glm-image-content.js';
18
22
  import { applyGlmVisionPromptTransform } from '../../../compat/actions/glm-vision-prompt.js';
19
23
  import { applyClaudeThinkingToolSchemaCompat } from '../../../compat/actions/claude-thinking-tools.js';
@@ -21,6 +25,11 @@ import { wrapGeminiCliRequest } from '../../../compat/actions/gemini-cli-request
21
25
  import { prepareAntigravityThoughtSignatureForGeminiRequest } from '../../../compat/actions/antigravity-thought-signature-prepare.js';
22
26
  import { cacheAntigravityThoughtSignatureFromGeminiResponse } from '../../../compat/actions/antigravity-thought-signature-cache.js';
23
27
  import { applyAnthropicClaudeCodeSystemPromptCompat } from '../../../compat/actions/anthropic-claude-code-system-prompt.js';
28
+ import { normalizeToolCallIdsInPlace } from '../../../compat/actions/normalize-tool-call-ids.js';
29
+ import { harvestToolCallsFromText } from '../../../compat/actions/harvest-tool-calls-from-text.js';
30
+ import { stripOrphanFunctionCallsTag } from '../../../compat/actions/strip-orphan-function-calls-tag.js';
31
+ import { stringifyLmstudioResponsesInput } from '../../../compat/actions/lmstudio-responses-input-stringify.js';
32
+ import { enforceLmstudioResponsesFcToolCallIds } from '../../../compat/actions/lmstudio-responses-fc-ids.js';
24
33
  const RATE_LIMIT_ERROR = 'ERR_COMPAT_RATE_LIMIT_DETECTED';
25
34
  const INTERNAL_STATE = Symbol('compat.internal_state');
26
35
  export function runRequestCompatPipeline(profileId, payload, options) {
@@ -145,6 +154,19 @@ function applyMapping(root, mapping, state) {
145
154
  case 'normalize_tool_choice':
146
155
  normalizeToolChoice(root, mapping);
147
156
  break;
157
+ case 'normalize_tool_call_ids':
158
+ normalizeToolCallIdsInPlace(root);
159
+ break;
160
+ case 'lmstudio_responses_fc_ids':
161
+ if (state.direction === 'request') {
162
+ replaceRoot(root, enforceLmstudioResponsesFcToolCallIds(root));
163
+ }
164
+ break;
165
+ case 'lmstudio_responses_input_stringify':
166
+ if (state.direction === 'request') {
167
+ replaceRoot(root, stringifyLmstudioResponsesInput(root, state.adapterContext));
168
+ }
169
+ break;
148
170
  case 'inject_instruction':
149
171
  injectInstruction(root, mapping);
150
172
  break;
@@ -154,6 +176,31 @@ function applyMapping(root, mapping, state) {
154
176
  case 'extract_glm_tool_markup':
155
177
  extractGlmToolMarkup(root);
156
178
  break;
179
+ case 'harvest_tool_calls_from_text':
180
+ if (state.direction === 'response') {
181
+ replaceRoot(root, harvestToolCallsFromText(root));
182
+ }
183
+ break;
184
+ case 'strip_orphan_function_calls_tag':
185
+ if (state.direction === 'response') {
186
+ replaceRoot(root, stripOrphanFunctionCallsTag(root));
187
+ }
188
+ break;
189
+ case 'iflow_kimi_thinking_reasoning_fill':
190
+ if (state.direction === 'request') {
191
+ replaceRoot(root, fillIflowKimiThinkingReasoningContent(root));
192
+ }
193
+ break;
194
+ case 'iflow_kimi_history_media_placeholder':
195
+ if (state.direction === 'request') {
196
+ replaceRoot(root, applyIflowKimiHistoryMediaPlaceholder(root));
197
+ }
198
+ break;
199
+ case 'iflow_kimi_cli_defaults':
200
+ if (state.direction === 'request') {
201
+ replaceRoot(root, applyIflowKimiCliDefaults(root));
202
+ }
203
+ break;
157
204
  case 'dto_unwrap':
158
205
  dtoUnwrap(root, state);
159
206
  break;
@@ -215,6 +262,11 @@ function applyMapping(root, mapping, state) {
215
262
  replaceRoot(root, applyIflowToolTextFallback(root, { models: mapping.models }));
216
263
  }
217
264
  break;
265
+ case 'iflow_response_body_unwrap':
266
+ if (state.direction === 'response') {
267
+ replaceRoot(root, unwrapIflowResponseBodyEnvelope(root));
268
+ }
269
+ break;
218
270
  case 'claude_thinking_tool_schema':
219
271
  if (state.direction === 'request') {
220
272
  replaceRoot(root, applyClaudeThinkingToolSchemaCompat(root, state.adapterContext));
@@ -290,13 +342,24 @@ function replaceRoot(target, source) {
290
342
  }
291
343
  function dtoUnwrap(root, state) {
292
344
  const original = structuredClone(root);
293
- if (isRecord(original.data)) {
345
+ const data = original.data;
346
+ if (isRecord(data)) {
294
347
  state.dtoEnvelope = { original, isDto: true };
295
- replaceRoot(root, original.data);
348
+ replaceRoot(root, data);
349
+ return;
296
350
  }
297
- else {
298
- state.dtoEnvelope = { original, isDto: false };
351
+ // Transport compatibility: some upstreams return JSON bodies but mislabel the content-type,
352
+ // causing HTTP clients to surface `data` as a string. When a compat profile opts into dto_unwrap,
353
+ // best-effort parse JSON strings so downstream semantic mapping sees canonical objects.
354
+ if (typeof data === 'string') {
355
+ const parsed = tryParseJsonRecord(data);
356
+ if (parsed) {
357
+ state.dtoEnvelope = { original, isDto: true };
358
+ replaceRoot(root, parsed);
359
+ return;
360
+ }
299
361
  }
362
+ state.dtoEnvelope = { original, isDto: false };
300
363
  }
301
364
  function dtoRewrap(root, state) {
302
365
  const envelope = state.dtoEnvelope;
@@ -312,6 +375,35 @@ function dtoRewrap(root, state) {
312
375
  replaceRoot(root, rebuilt);
313
376
  state.dtoEnvelope = undefined;
314
377
  }
378
+ function tryParseJsonRecord(raw) {
379
+ try {
380
+ let text = typeof raw === 'string' ? raw.trim() : '';
381
+ if (!text)
382
+ return null;
383
+ // anti-XSSI prefix: ")]}',\n{...}"
384
+ text = text.replace(/^\)\]\}',?\s*/u, '');
385
+ // single-line SSE-like wrapper sometimes returned as plain text
386
+ text = text.replace(/^data:\s*/iu, '');
387
+ // Quick structural guards to avoid parsing random prose.
388
+ const first = text.charAt(0);
389
+ const last = text.charAt(text.length - 1);
390
+ if (first !== '{' || last !== '}') {
391
+ return null;
392
+ }
393
+ // Guard: avoid unbounded parsing for pathological payloads.
394
+ if (text.length > 10 * 1024 * 1024) {
395
+ return null;
396
+ }
397
+ const parsed = JSON.parse(text);
398
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
399
+ return null;
400
+ }
401
+ return parsed;
402
+ }
403
+ catch {
404
+ return null;
405
+ }
406
+ }
315
407
  function applyShapeFilterMapping(root, mapping, state) {
316
408
  const target = mapping.target ?? state.direction;
317
409
  const filter = new UniversalShapeFilter(mapping.config);
@@ -765,9 +857,13 @@ function convertOutputEntryToChoice(entry, index, root) {
765
857
  (typeof entry.status === 'string' && entry.status) ||
766
858
  (typeof root.status === 'string' && root.status) ||
767
859
  'stop';
860
+ const hasToolCalls = message &&
861
+ typeof message === 'object' &&
862
+ Array.isArray(message.tool_calls) &&
863
+ message.tool_calls.length > 0;
768
864
  return {
769
865
  index,
770
- finish_reason: normalizeFinishReason(finishReasonCandidate),
866
+ finish_reason: hasToolCalls ? 'tool_calls' : normalizeFinishReason(finishReasonCandidate),
771
867
  message
772
868
  };
773
869
  }
@@ -0,0 +1,2 @@
1
+ import type { AdapterContext } from '../../types/chat-envelope.js';
2
+ export declare function resolveCompatProfileForContext(adapterContext: AdapterContext): string | undefined;
@@ -0,0 +1,63 @@
1
+ const PROFILE_BY_PROVIDER_FAMILY = {
2
+ iflow: 'chat:iflow',
3
+ lmstudio: 'chat:lmstudio',
4
+ antigravity: 'chat:gemini-cli',
5
+ 'gemini-cli': 'chat:gemini-cli'
6
+ };
7
+ function readNonEmptyString(value) {
8
+ if (typeof value !== 'string') {
9
+ return undefined;
10
+ }
11
+ const trimmed = value.trim();
12
+ return trimmed.length ? trimmed : undefined;
13
+ }
14
+ function normalizeProviderFamily(value) {
15
+ const normalized = readNonEmptyString(value)?.toLowerCase();
16
+ if (!normalized) {
17
+ return undefined;
18
+ }
19
+ if (normalized in PROFILE_BY_PROVIDER_FAMILY) {
20
+ return normalized;
21
+ }
22
+ if (normalized.includes('.')) {
23
+ const firstToken = normalized.split('.')[0]?.trim();
24
+ if (firstToken && firstToken in PROFILE_BY_PROVIDER_FAMILY) {
25
+ return firstToken;
26
+ }
27
+ }
28
+ if (normalized.includes(':')) {
29
+ const firstToken = normalized.split(':')[0]?.trim();
30
+ if (firstToken && firstToken in PROFILE_BY_PROVIDER_FAMILY) {
31
+ return firstToken;
32
+ }
33
+ }
34
+ if (normalized.includes('/')) {
35
+ const firstToken = normalized.split('/')[0]?.trim();
36
+ if (firstToken && firstToken in PROFILE_BY_PROVIDER_FAMILY) {
37
+ return firstToken;
38
+ }
39
+ }
40
+ return undefined;
41
+ }
42
+ function resolveDefaultProfileFromFamily(adapterContext) {
43
+ const context = adapterContext;
44
+ const providerFamily = normalizeProviderFamily(context.providerFamily);
45
+ if (providerFamily) {
46
+ return PROFILE_BY_PROVIDER_FAMILY[providerFamily];
47
+ }
48
+ for (const key of ['providerId', 'providerKey', 'runtimeKey', 'profileId', 'providerType']) {
49
+ const family = normalizeProviderFamily(context[key]);
50
+ if (family) {
51
+ return PROFILE_BY_PROVIDER_FAMILY[family];
52
+ }
53
+ }
54
+ return undefined;
55
+ }
56
+ export function resolveCompatProfileForContext(adapterContext) {
57
+ const context = adapterContext;
58
+ const explicit = readNonEmptyString(context.compatibilityProfile);
59
+ if (explicit) {
60
+ return explicit;
61
+ }
62
+ return resolveDefaultProfileFromFamily(adapterContext);
63
+ }
@@ -47,6 +47,18 @@ export type MappingInstruction = {
47
47
  action: 'normalize_tool_choice';
48
48
  path?: string;
49
49
  objectReplacement?: string;
50
+ } | {
51
+ action: 'normalize_tool_call_ids';
52
+ } | {
53
+ action: 'lmstudio_responses_fc_ids';
54
+ } | {
55
+ action: 'lmstudio_responses_input_stringify';
56
+ } | {
57
+ action: 'iflow_kimi_history_media_placeholder';
58
+ } | {
59
+ action: 'iflow_kimi_cli_defaults';
60
+ } | {
61
+ action: 'iflow_kimi_thinking_reasoning_fill';
50
62
  } | {
51
63
  action: 'inject_instruction';
52
64
  sourcePath: string;
@@ -63,6 +75,10 @@ export type MappingInstruction = {
63
75
  action: 'convert_responses_output_to_choices';
64
76
  } | {
65
77
  action: 'extract_glm_tool_markup';
78
+ } | {
79
+ action: 'harvest_tool_calls_from_text';
80
+ } | {
81
+ action: 'strip_orphan_function_calls_tag';
66
82
  } | {
67
83
  action: 'dto_unwrap';
68
84
  } | {
@@ -114,6 +130,8 @@ export type MappingInstruction = {
114
130
  } | {
115
131
  action: 'iflow_tool_text_fallback';
116
132
  models?: string[];
133
+ } | {
134
+ action: 'iflow_response_body_unwrap';
117
135
  } | {
118
136
  action: 'claude_thinking_tool_schema';
119
137
  } | {
@@ -543,7 +543,7 @@ export class HubPipeline {
543
543
  //
544
544
  // 注意:这里不再根据 processMode(passthrough/chat) 做分支判断——即使某些
545
545
  // route 将 processMode 标记为 passthrough,我们仍然需要保留一次规范化后的
546
- // Chat 请求快照,供 stopMessage / gemini_empty_reply_continue 等被动触发型
546
+ // Chat 请求快照,供 stopMessage / empty_reply_continue 等被动触发型
547
547
  // servertool 在响应阶段使用。
548
548
  //
549
549
  // 之前这里通过 JSON.stringify/parse 做深拷贝,但在部分 Responses/Gemini
@@ -131,13 +131,16 @@ function normalizeToolOutput(entry) {
131
131
  }
132
132
  function applyToolCallIdStyleMetadata(chatEnvelope, adapterContext, snapshot) {
133
133
  const metadata = chatEnvelope.metadata || (chatEnvelope.metadata = { context: adapterContext });
134
- const hasStyle = typeof metadata.toolCallIdStyle === 'string'
135
- && String(metadata.toolCallIdStyle).trim().length > 0;
136
- if (hasStyle) {
134
+ const current = typeof metadata.toolCallIdStyle === 'string'
135
+ ? String(metadata.toolCallIdStyle).trim()
136
+ : '';
137
+ const resolved = selectToolCallIdStyle(adapterContext, snapshot);
138
+ if (!resolved) {
137
139
  return;
138
140
  }
139
- const resolved = selectToolCallIdStyle(adapterContext, snapshot);
140
- if (resolved) {
141
+ // Always honor the route-selected AdapterContext toolCallIdStyle when present.
142
+ // This prevents cross-provider leakage (e.g. LM Studio "preserve" contaminating OpenAI "fc").
143
+ if (!current || current !== resolved) {
141
144
  metadata.toolCallIdStyle = resolved;
142
145
  }
143
146
  }
@@ -1,7 +1,11 @@
1
1
  import { applyRequestCompat, applyResponseCompat } from '../../../compat/compat-engine.js';
2
+ import { resolveCompatProfileForContext } from '../../../compat/compat-profile-resolver.js';
2
3
  function pickCompatProfile(adapterContext) {
3
4
  const candidate = adapterContext.compatibilityProfile;
4
- return typeof candidate === 'string' && candidate.trim() ? candidate.trim() : undefined;
5
+ if (typeof candidate === 'string' && candidate.trim()) {
6
+ return candidate.trim();
7
+ }
8
+ return resolveCompatProfileForContext(adapterContext);
5
9
  }
6
10
  export async function runReqOutboundStage3Compat(options) {
7
11
  const profile = pickCompatProfile(options.adapterContext);
@@ -38,6 +38,19 @@ function resolveProviderType(protocol) {
38
38
  return undefined;
39
39
  }
40
40
  export async function runRespInboundStage1SseDecode(options) {
41
+ // Transport compatibility: some HTTP clients return JSON bodies as plain strings when the upstream
42
+ // mislabels `Content-Type`. Best-effort parse JSON text early so downstream format adapters and
43
+ // semantic mappers always see canonical objects.
44
+ const maybeJsonText = tryDecodeJsonBodyFromText(options.payload);
45
+ if (maybeJsonText) {
46
+ recordStage(options.stageRecorder, 'chat_process.resp.stage1.sse_decode', {
47
+ streamDetected: false,
48
+ decoded: false,
49
+ protocol: options.providerProtocol,
50
+ reason: 'text_body_is_json'
51
+ });
52
+ return { payload: maybeJsonText, decodedFromSse: false };
53
+ }
41
54
  const wrapperError = extractSseWrapperError(options.payload);
42
55
  const stream = extractSseStream(options.payload);
43
56
  // 某些 mock-provider / 捕获样本在 SSE 连接被异常终止时会携带 error 标记,
@@ -67,6 +80,19 @@ export async function runRespInboundStage1SseDecode(options) {
67
80
  });
68
81
  return { payload: options.payload, decodedFromSse: false };
69
82
  }
83
+ // Compatibility: when an upstream is asked for streaming but responds with a single JSON body
84
+ // (common for mock servers and some OpenAI-compatible implementations), the provider wrapper may
85
+ // still surface a Readable via `__sse_stream`. In that case we should treat it as JSON, not SSE.
86
+ const maybeJson = await tryDecodeJsonBodyFromStream(stream);
87
+ if (maybeJson) {
88
+ recordStage(options.stageRecorder, 'chat_process.resp.stage1.sse_decode', {
89
+ streamDetected: true,
90
+ decoded: false,
91
+ protocol: options.providerProtocol,
92
+ reason: 'stream_body_is_json'
93
+ });
94
+ return { payload: maybeJson, decodedFromSse: false };
95
+ }
70
96
  if (!supportsSseProtocol(options.providerProtocol)) {
71
97
  recordStage(options.stageRecorder, 'chat_process.resp.stage1.sse_decode', {
72
98
  streamDetected: true,
@@ -120,6 +146,93 @@ export async function runRespInboundStage1SseDecode(options) {
120
146
  function supportsSseProtocol(protocol) {
121
147
  return protocol === 'openai-chat' || protocol === 'openai-responses' || protocol === 'anthropic-messages' || protocol === 'gemini-chat';
122
148
  }
149
+ function tryDecodeJsonBodyFromText(payload) {
150
+ try {
151
+ if (typeof payload !== 'string') {
152
+ return null;
153
+ }
154
+ // Normalize common prefixes / wrappers from OpenAI-compatible upstreams.
155
+ let text = payload.trimStart();
156
+ // anti-XSSI prefix: ")]}',\n{...}"
157
+ text = text.replace(/^\)\]\}',?\s*/u, '');
158
+ // single-line SSE-like wrapper sometimes returned as plain text
159
+ text = text.replace(/^data:\s*/iu, '');
160
+ const looksLikeJson = text.startsWith('{') || text.startsWith('[');
161
+ if (!looksLikeJson) {
162
+ return null;
163
+ }
164
+ // Guard: avoid unbounded parsing for pathological payloads.
165
+ if (text.length > 10 * 1024 * 1024) {
166
+ return null;
167
+ }
168
+ const parsed = JSON.parse(text);
169
+ return (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) ? parsed : null;
170
+ }
171
+ catch {
172
+ return null;
173
+ }
174
+ }
175
+ async function tryDecodeJsonBodyFromStream(stream) {
176
+ // Peek the first chunk; if it looks like JSON (starts with `{` or `[`), consume full body and parse.
177
+ const iterator = stream[Symbol.asyncIterator]();
178
+ const first = await iterator.next();
179
+ if (first.done || first.value == null) {
180
+ return null;
181
+ }
182
+ const firstChunk = first.value;
183
+ let prefix = (typeof firstChunk === 'string' ? firstChunk : firstChunk.toString('utf8')).trimStart();
184
+ prefix = prefix.replace(/^\)\]\}',?\s*/u, '');
185
+ prefix = prefix.replace(/^data:\s*/iu, '');
186
+ const looksLikeJson = prefix.startsWith('{') || prefix.startsWith('[');
187
+ if (!looksLikeJson) {
188
+ // Rewind by re-wrapping the iterator so downstream SSE decoder still sees the first chunk.
189
+ // eslint-disable-next-line no-param-reassign
190
+ stream[Symbol.asyncIterator] = () => replayIterator(firstChunk, iterator);
191
+ return null;
192
+ }
193
+ let body = typeof firstChunk === 'string' ? firstChunk : firstChunk.toString('utf8');
194
+ while (true) {
195
+ const next = await iterator.next();
196
+ if (next.done)
197
+ break;
198
+ body += typeof next.value === 'string' ? next.value : Buffer.from(next.value).toString('utf8');
199
+ // Guard: avoid unbounded buffering if the upstream is actually SSE but starts with whitespace.
200
+ if (body.length > 1024 * 1024) {
201
+ return null;
202
+ }
203
+ }
204
+ try {
205
+ const parsed = JSON.parse(body);
206
+ return (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) ? parsed : null;
207
+ }
208
+ catch {
209
+ return null;
210
+ }
211
+ }
212
+ function replayIterator(firstChunk, iterator) {
213
+ let yieldedFirst = false;
214
+ return {
215
+ async next() {
216
+ if (!yieldedFirst) {
217
+ yieldedFirst = true;
218
+ return { done: false, value: firstChunk };
219
+ }
220
+ return iterator.next();
221
+ },
222
+ async return(value) {
223
+ if (typeof iterator.return === 'function') {
224
+ return iterator.return(value);
225
+ }
226
+ return { done: true, value };
227
+ },
228
+ async throw(err) {
229
+ if (typeof iterator.throw === 'function') {
230
+ return iterator.throw(err);
231
+ }
232
+ throw err;
233
+ }
234
+ };
235
+ }
123
236
  function extractSseStream(payload) {
124
237
  if (!payload || typeof payload !== 'object') {
125
238
  return undefined;
@@ -64,6 +64,9 @@ export function applyTargetToSubject(subject, target, originalModel) {
64
64
  metadata.processMode = target.processMode || 'chat';
65
65
  subject.metadata = metadata;
66
66
  const subjectMeta = metadata;
67
+ if (target.responsesConfig?.toolCallIdStyle) {
68
+ subjectMeta.toolCallIdStyle = target.responsesConfig.toolCallIdStyle;
69
+ }
67
70
  if (originalModel && typeof originalModel === 'string' && originalModel.trim()) {
68
71
  const trimmed = originalModel.trim();
69
72
  if (typeof subjectMeta.originalModelId !== 'string' || !subjectMeta.originalModelId) {
@@ -118,6 +118,20 @@ function detectProviderResponseShape(payload) {
118
118
  return 'gemini-chat';
119
119
  return 'unknown';
120
120
  }
121
+ function inferProviderTypeFromProtocol(protocol) {
122
+ const p = typeof protocol === 'string' ? protocol.trim().toLowerCase() : '';
123
+ if (!p)
124
+ return undefined;
125
+ if (p === 'openai-chat')
126
+ return 'openai';
127
+ if (p === 'openai-responses')
128
+ return 'responses';
129
+ if (p === 'anthropic-messages')
130
+ return 'anthropic';
131
+ if (p === 'gemini-chat')
132
+ return 'gemini';
133
+ return undefined;
134
+ }
121
135
  function isCanonicalChatCompletion(payload) {
122
136
  if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
123
137
  return false;
@@ -138,9 +152,18 @@ async function coerceClientPayloadToCanonicalChatCompletionOrThrow(options) {
138
152
  }
139
153
  const detected = detectProviderResponseShape(options.payload);
140
154
  if (detected === 'unknown') {
155
+ const protocol = options.adapterContext?.providerProtocol;
141
156
  throw new ProviderProtocolError(`[hub_response] Non-canonical response payload at ${options.scope}`, {
142
157
  code: 'MALFORMED_RESPONSE',
143
- details: { detected }
158
+ protocol,
159
+ providerType: inferProviderTypeFromProtocol(protocol),
160
+ details: {
161
+ detected,
162
+ payloadType: typeof options.payload,
163
+ payloadKeys: options.payload && typeof options.payload === 'object' && !Array.isArray(options.payload)
164
+ ? Object.keys(options.payload).slice(0, 20)
165
+ : undefined
166
+ }
144
167
  });
145
168
  }
146
169
  const plan = PROVIDER_RESPONSE_REGISTRY[detected];
@@ -149,8 +172,11 @@ async function coerceClientPayloadToCanonicalChatCompletionOrThrow(options) {
149
172
  if (isCanonicalChatCompletion(coerced)) {
150
173
  return coerced;
151
174
  }
175
+ const protocol = options.adapterContext?.providerProtocol;
152
176
  throw new ProviderProtocolError(`[hub_response] Failed to canonicalize response payload at ${options.scope}`, {
153
177
  code: 'MALFORMED_RESPONSE',
178
+ protocol,
179
+ providerType: inferProviderTypeFromProtocol(protocol),
154
180
  details: { detected }
155
181
  });
156
182
  }
@@ -5,6 +5,7 @@ import { createToolCallIdTransformer, enforceToolCallIdStyle, resolveToolCallIdS
5
5
  import { mapBridgeToolsToChat, mapChatToolsToBridge } from '../shared/tool-mapping.js';
6
6
  import { ProviderProtocolError } from '../shared/errors.js';
7
7
  import { readRuntimeMetadata } from '../shared/runtime-metadata.js';
8
+ import { clampResponsesInputItemId } from '../shared/bridge-id-utils.js';
8
9
  // --- Utilities (ported strictly) ---
9
10
  import { canonicalizeChatResponseTools } from '../shared/tool-canonicalizer.js';
10
11
  import { normalizeMessageReasoningTools } from '../shared/reasoning-tool-normalizer.js';
@@ -14,15 +15,31 @@ import { buildResponsesOutputFromChat } from '../shared/responses-output-builder
14
15
  function isObject(v) {
15
16
  return !!v && typeof v === 'object' && !Array.isArray(v);
16
17
  }
17
- function filterBridgeInputForUpstream(input) {
18
+ function filterBridgeInputForUpstream(input, options) {
18
19
  // Upstream `/v1/responses` create only accepts a subset of input item types.
19
20
  // In particular, `type:"reasoning"` entries are output-only artifacts (often
20
21
  // captured from previous responses) and OpenAI rejects them with schema errors
21
22
  // like `input[N].content: array too long (max 0)`.
22
- return (Array.isArray(input) ? input : []).filter((item) => {
23
+ return (Array.isArray(input) ? input : []).flatMap((item) => {
23
24
  if (!item || typeof item !== 'object')
24
- return false;
25
- return item.type !== 'reasoning';
25
+ return [];
26
+ if (item.type === 'reasoning') {
27
+ return [];
28
+ }
29
+ // OpenAI `/v1/responses` request schema uses `call_id` for tool outputs.
30
+ // Some internal carriers may set `tool_call_id`; strip it before sending upstream
31
+ // to avoid strict schema errors (e.g. "Unknown parameter: input[N].tool_call_id").
32
+ const clone = { ...item };
33
+ if (options?.allowToolCallId !== true && clone.tool_call_id !== undefined) {
34
+ delete clone.tool_call_id;
35
+ }
36
+ // OpenAI /v1/responses enforces max length (64) on input item id fields.
37
+ // Keep this as the single outbound guardrail for all bridged input item types.
38
+ const normalizedId = clampResponsesInputItemId(clone.id);
39
+ if (normalizedId) {
40
+ clone.id = normalizedId;
41
+ }
42
+ return [clone];
26
43
  });
27
44
  }
28
45
  // normalizeTools unified in ../shared/args-mapping.ts
@@ -356,7 +373,16 @@ export function buildResponsesRequestFromChat(payload, ctx, extras) {
356
373
  const envelopeMetadata = ctx?.metadata && typeof ctx.metadata === 'object' ? ctx.metadata : undefined;
357
374
  const metadataExtraFields = extractMetadataExtraFields(envelopeMetadata);
358
375
  const contextToolCallIdStyle = readToolCallIdStyleFromContext(ctx);
359
- const toolCallIdStyle = contextToolCallIdStyle ?? resolveToolCallIdStyle(envelopeMetadata);
376
+ const envelopeToolCallIdStyle = resolveToolCallIdStyle(envelopeMetadata);
377
+ const requestMetadata = chat && typeof chat === 'object' && chat.metadata && typeof chat.metadata === 'object'
378
+ ? chat.metadata
379
+ : undefined;
380
+ const routeToolCallIdStyle = requestMetadata
381
+ ? normalizeToolCallIdStyleCandidate(requestMetadata.toolCallIdStyle)
382
+ : undefined;
383
+ // Route-selected toolCallIdStyle must win over captured context to prevent cross-provider leakage
384
+ // (e.g. LM Studio "preserve" contaminating OpenAI "fc").
385
+ const toolCallIdStyle = routeToolCallIdStyle ?? envelopeToolCallIdStyle ?? contextToolCallIdStyle;
360
386
  const fallbackHistory = ctx?.input && Array.isArray(ctx.input)
361
387
  ? {
362
388
  input: ctx.input,
@@ -392,7 +418,7 @@ export function buildResponsesRequestFromChat(payload, ctx, extras) {
392
418
  }
393
419
  }
394
420
  // 不追加 metadata,以便 roundtrip 与原始 payload 对齐;系统提示直接写入 instructions。
395
- const upstreamInput = filterBridgeInputForUpstream(input);
421
+ const upstreamInput = filterBridgeInputForUpstream(input, { allowToolCallId: toolCallIdStyle === 'preserve' });
396
422
  if (upstreamInput.length) {
397
423
  out.input = upstreamInput;
398
424
  }
@@ -15,6 +15,21 @@ function safeJson(v) {
15
15
  return '{}';
16
16
  }
17
17
  }
18
+ function sanitizeToolUseId(raw) {
19
+ const trimmed = typeof raw === 'string' ? raw.trim() : '';
20
+ if (!trimmed) {
21
+ return `call_${Math.random().toString(36).slice(2, 10)}`;
22
+ }
23
+ if (/^[A-Za-z0-9_-]+$/.test(trimmed)) {
24
+ return trimmed;
25
+ }
26
+ const sanitized = trimmed
27
+ .replace(/[^A-Za-z0-9_-]/g, '_')
28
+ .replace(/_{2,}/g, '_')
29
+ .replace(/^_+/, '')
30
+ .replace(/_+$/, '');
31
+ return sanitized || `call_${Math.random().toString(36).slice(2, 10)}`;
32
+ }
18
33
  function flattenAnthropicText(content) {
19
34
  if (content == null)
20
35
  return '';
@@ -477,7 +492,7 @@ export function buildAnthropicFromOpenAIChat(oa, options) {
477
492
  const toolNameResolver = createAnthropicToolNameResolver(options?.toolNameMap ?? extractToolNameMapFromPayload(oa));
478
493
  for (const tc of toolCalls) {
479
494
  try {
480
- const id = requireTrimmedString(tc?.id, 'chat.tool_call.id');
495
+ const id = sanitizeToolUseId(requireTrimmedString(tc?.id, 'chat.tool_call.id'));
481
496
  const fn = isObject(tc?.function) ? tc.function : {};
482
497
  const canonicalName = requireTrimmedString(fn.name, 'chat.tool_call.function.name');
483
498
  const name = toolNameResolver ? toolNameResolver(canonicalName) : canonicalName;
@@ -752,7 +767,7 @@ export function buildAnthropicRequestFromOpenAIChat(chatReq) {
752
767
  for (const tc of toolCalls) {
753
768
  if (!tc || typeof tc !== 'object')
754
769
  continue;
755
- const id = requireTrimmedString(tc.id, 'chat.tool_call.id');
770
+ const id = sanitizeToolUseId(requireTrimmedString(tc.id, 'chat.tool_call.id'));
756
771
  knownToolCallIds.add(id);
757
772
  }
758
773
  }
@@ -816,7 +831,7 @@ export function buildAnthropicRequestFromOpenAIChat(chatReq) {
816
831
  continue;
817
832
  }
818
833
  if (role === 'tool') {
819
- const toolCallId = requireTrimmedString(m.tool_call_id ?? m.call_id ?? m.tool_use_id ?? m.id, 'tool_result.tool_call_id');
834
+ const toolCallId = sanitizeToolUseId(requireTrimmedString(m.tool_call_id ?? m.call_id ?? m.tool_use_id ?? m.id, 'tool_result.tool_call_id'));
820
835
  if (!knownToolCallIds.has(toolCallId)) {
821
836
  throw new ProviderProtocolError(`Anthropic bridge constraint violated: tool result ${toolCallId} has no matching tool call`, {
822
837
  code: 'TOOL_PROTOCOL_ERROR',
@@ -896,7 +911,7 @@ export function buildAnthropicRequestFromOpenAIChat(chatReq) {
896
911
  for (const tc of toolCalls) {
897
912
  if (!tc || typeof tc !== 'object')
898
913
  continue;
899
- const id = requireTrimmedString(tc.id, 'chat.tool_call.id');
914
+ const id = sanitizeToolUseId(requireTrimmedString(tc.id, 'chat.tool_call.id'));
900
915
  const fn = tc.function || {};
901
916
  const name = requireTrimmedString(fn.name, 'chat.tool_call.function.name');
902
917
  const argsRaw = fn.arguments;
@@ -917,7 +932,7 @@ export function buildAnthropicRequestFromOpenAIChat(chatReq) {
917
932
  if (blocks.length > 0) {
918
933
  const hasStructuredBlocks = blocks.some((block) => block && typeof block === 'object' && block.type !== 'text');
919
934
  let contentNode = blocks;
920
- if (targetShape === 'string' || (!targetShape && !hasStructuredBlocks)) {
935
+ if (!hasStructuredBlocks && (targetShape === 'string' || !targetShape)) {
921
936
  contentNode = text;
922
937
  }
923
938
  messages.push({ role, content: contentNode });
@@ -4,4 +4,6 @@ type NormalizeOptions = {
4
4
  };
5
5
  export declare function normalizeFunctionCallId(options: NormalizeOptions): string;
6
6
  export declare function normalizeFunctionCallOutputId(options: NormalizeOptions): string;
7
+ export declare function normalizeResponsesCallId(options: NormalizeOptions): string;
8
+ export declare function clampResponsesInputItemId(raw: unknown): string | undefined;
7
9
  export {};