@jsonstudio/llms 0.6.568 → 0.6.626

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 (42) hide show
  1. package/dist/conversion/compat/profiles/chat-gemini.json +15 -15
  2. package/dist/conversion/compat/profiles/chat-glm.json +194 -194
  3. package/dist/conversion/compat/profiles/chat-iflow.json +199 -199
  4. package/dist/conversion/compat/profiles/chat-lmstudio.json +43 -43
  5. package/dist/conversion/compat/profiles/chat-qwen.json +20 -20
  6. package/dist/conversion/compat/profiles/responses-c4m.json +42 -42
  7. package/dist/conversion/compat/profiles/responses-output2choices-test.json +9 -10
  8. package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +0 -1
  9. package/dist/conversion/hub/pipeline/hub-pipeline.js +68 -69
  10. package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.js +0 -34
  11. package/dist/conversion/hub/process/chat-process.js +37 -16
  12. package/dist/conversion/hub/response/provider-response.js +0 -8
  13. package/dist/conversion/hub/response/response-runtime.js +47 -1
  14. package/dist/conversion/hub/semantic-mappers/anthropic-mapper.js +59 -4
  15. package/dist/conversion/hub/semantic-mappers/chat-mapper.d.ts +8 -0
  16. package/dist/conversion/hub/semantic-mappers/chat-mapper.js +93 -12
  17. package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +208 -31
  18. package/dist/conversion/hub/semantic-mappers/responses-mapper.js +280 -14
  19. package/dist/conversion/hub/standardized-bridge.js +11 -2
  20. package/dist/conversion/hub/types/chat-envelope.d.ts +10 -0
  21. package/dist/conversion/hub/types/standardized.d.ts +2 -1
  22. package/dist/conversion/responses/responses-openai-bridge.d.ts +3 -2
  23. package/dist/conversion/responses/responses-openai-bridge.js +1 -13
  24. package/dist/conversion/shared/text-markup-normalizer.d.ts +20 -0
  25. package/dist/conversion/shared/text-markup-normalizer.js +84 -5
  26. package/dist/conversion/shared/tool-filter-pipeline.d.ts +1 -1
  27. package/dist/conversion/shared/tool-filter-pipeline.js +54 -29
  28. package/dist/filters/index.d.ts +1 -0
  29. package/dist/filters/special/response-apply-patch-toon-decode.js +15 -7
  30. package/dist/filters/special/response-tool-arguments-toon-decode.js +108 -22
  31. package/dist/guidance/index.js +2 -0
  32. package/dist/router/virtual-router/classifier.js +16 -12
  33. package/dist/router/virtual-router/engine.js +45 -4
  34. package/dist/router/virtual-router/tool-signals.d.ts +2 -1
  35. package/dist/router/virtual-router/tool-signals.js +293 -134
  36. package/dist/router/virtual-router/types.d.ts +1 -1
  37. package/dist/router/virtual-router/types.js +1 -1
  38. package/dist/servertool/handlers/gemini-empty-reply-continue.js +28 -4
  39. package/dist/sse/json-to-sse/event-generators/responses.js +9 -2
  40. package/dist/sse/sse-to-json/builders/anthropic-response-builder.js +7 -3
  41. package/dist/tools/apply-patch-structured.js +4 -3
  42. package/package.json +2 -2
@@ -1,12 +1,11 @@
1
1
  {
2
- "id": "responses:output2choices-test",
3
- "protocol": "openai-responses",
4
- "response": {
5
- "mappings": [
6
- {
7
- "action": "convert_responses_output_to_choices"
8
- }
9
- ]
10
- }
2
+ "id": "responses:output2choices-test",
3
+ "protocol": "openai-responses",
4
+ "response": {
5
+ "mappings": [
6
+ {
7
+ "action": "convert_responses_output_to_choices"
8
+ }
9
+ ]
10
+ }
11
11
  }
12
-
@@ -61,7 +61,6 @@ export declare class HubPipeline {
61
61
  private buildAdapterContext;
62
62
  private maybeCreateStageRecorder;
63
63
  private asJsonObject;
64
- private pickRawRequestBody;
65
64
  private normalizeRequest;
66
65
  private convertProcessNodeResult;
67
66
  private materializePayload;
@@ -1,4 +1,5 @@
1
1
  import { Readable } from 'node:stream';
2
+ import { isJsonObject } from '../types/json.js';
2
3
  import { VirtualRouterEngine } from '../../../router/virtual-router/engine.js';
3
4
  import { providerErrorCenter } from '../../../router/virtual-router/error-center.js';
4
5
  import { defaultSseCodecRegistry } from '../../../sse/index.js';
@@ -22,6 +23,7 @@ import { runReqOutboundStage1SemanticMap } from './stages/req_outbound/req_outbo
22
23
  import { runReqOutboundStage2FormatBuild } from './stages/req_outbound/req_outbound_stage2_format_build/index.js';
23
24
  import { runReqOutboundStage3Compat } from './stages/req_outbound/req_outbound_stage3_compat/index.js';
24
25
  import { extractSessionIdentifiersFromMetadata } from './session-identifiers.js';
26
+ import { computeRequestTokens } from '../../../router/virtual-router/token-estimator.js';
25
27
  export class HubPipeline {
26
28
  routerEngine;
27
29
  config;
@@ -119,6 +121,18 @@ export class HubPipeline {
119
121
  }
120
122
  }
121
123
  const workingRequest = processedRequest ?? standardizedRequest;
124
+ // 使用与 VirtualRouter 一致的 tiktoken 计数逻辑,对标准化请求进行一次
125
+ // 上下文 token 估算,供后续 usage 归一化与统计使用。
126
+ try {
127
+ const estimatedTokens = computeRequestTokens(workingRequest, '');
128
+ if (typeof estimatedTokens === 'number' && Number.isFinite(estimatedTokens) && estimatedTokens > 0) {
129
+ normalized.metadata = normalized.metadata || {};
130
+ normalized.metadata.estimatedInputTokens = estimatedTokens;
131
+ }
132
+ }
133
+ catch {
134
+ // 估算失败不应影响主流程
135
+ }
122
136
  const normalizedMeta = normalized.metadata;
123
137
  const responsesResume = normalizedMeta && typeof normalizedMeta.responsesResume === 'object'
124
138
  ? normalizedMeta.responsesResume
@@ -182,66 +196,42 @@ export class HubPipeline {
182
196
  const outboundEndpoint = resolveEndpointForProviderProtocol(outboundAdapterContext.providerProtocol);
183
197
  const outboundRecorder = this.maybeCreateStageRecorder(outboundAdapterContext, outboundEndpoint);
184
198
  const outboundStart = Date.now();
185
- const isResponsesSubmit = normalized.entryEndpoint === '/v1/responses.submit_tool_outputs' &&
186
- outboundProtocol === 'openai-responses';
187
199
  let providerPayload;
188
- if (isResponsesSubmit) {
189
- providerPayload =
190
- this.pickRawRequestBody(normalized.metadata) ?? normalized.payload;
191
- const outboundEnd = Date.now();
192
- nodeResults.push({
193
- id: 'req_outbound',
194
- success: true,
195
- metadata: {
196
- node: 'req_outbound',
197
- executionTime: outboundEnd - outboundStart,
198
- startTime: outboundStart,
199
- endTime: outboundEnd,
200
- dataProcessed: {
201
- messages: 0,
202
- tools: 0
203
- },
204
- bypass: 'responses-submit-passthrough'
205
- }
206
- });
207
- }
208
- else {
209
- const outboundStage1 = await runReqOutboundStage1SemanticMap({
210
- request: workingRequest,
211
- adapterContext: outboundAdapterContext,
212
- semanticMapper: outboundSemanticMapper,
213
- contextSnapshot: outboundContextSnapshot,
214
- contextMetadataKey: outboundContextMetadataKey,
215
- stageRecorder: outboundRecorder
216
- });
217
- let formattedPayload = await runReqOutboundStage2FormatBuild({
218
- formatEnvelope: outboundStage1.formatEnvelope,
219
- adapterContext: outboundAdapterContext,
220
- formatAdapter: outboundFormatAdapter,
221
- stageRecorder: outboundRecorder
222
- });
223
- formattedPayload = await runReqOutboundStage3Compat({
224
- payload: formattedPayload,
225
- adapterContext: outboundAdapterContext,
226
- stageRecorder: outboundRecorder
227
- });
228
- providerPayload = formattedPayload;
229
- const outboundEnd = Date.now();
230
- nodeResults.push({
231
- id: 'req_outbound',
232
- success: true,
233
- metadata: {
234
- node: 'req_outbound',
235
- executionTime: outboundEnd - outboundStart,
236
- startTime: outboundStart,
237
- endTime: outboundEnd,
238
- dataProcessed: {
239
- messages: workingRequest.messages.length,
240
- tools: workingRequest.tools?.length ?? 0
241
- }
200
+ const outboundStage1 = await runReqOutboundStage1SemanticMap({
201
+ request: workingRequest,
202
+ adapterContext: outboundAdapterContext,
203
+ semanticMapper: outboundSemanticMapper,
204
+ contextSnapshot: outboundContextSnapshot,
205
+ contextMetadataKey: outboundContextMetadataKey,
206
+ stageRecorder: outboundRecorder
207
+ });
208
+ let formattedPayload = await runReqOutboundStage2FormatBuild({
209
+ formatEnvelope: outboundStage1.formatEnvelope,
210
+ adapterContext: outboundAdapterContext,
211
+ formatAdapter: outboundFormatAdapter,
212
+ stageRecorder: outboundRecorder
213
+ });
214
+ formattedPayload = await runReqOutboundStage3Compat({
215
+ payload: formattedPayload,
216
+ adapterContext: outboundAdapterContext,
217
+ stageRecorder: outboundRecorder
218
+ });
219
+ providerPayload = formattedPayload;
220
+ const outboundEnd = Date.now();
221
+ nodeResults.push({
222
+ id: 'req_outbound',
223
+ success: true,
224
+ metadata: {
225
+ node: 'req_outbound',
226
+ executionTime: outboundEnd - outboundStart,
227
+ startTime: outboundStart,
228
+ endTime: outboundEnd,
229
+ dataProcessed: {
230
+ messages: workingRequest.messages.length,
231
+ tools: workingRequest.tools?.length ?? 0
242
232
  }
243
- });
244
- }
233
+ }
234
+ });
245
235
  // 为响应侧 servertool/web_search 提供一次性 Chat 请求快照,便于在 Hub 内部实现
246
236
  // 第三跳(将工具结果注入消息历史后重新调用主模型)。
247
237
  //
@@ -322,7 +312,11 @@ export class HubPipeline {
322
312
  const contextNode = metadataNode && metadataNode.context && typeof metadataNode.context === 'object'
323
313
  ? metadataNode.context
324
314
  : undefined;
325
- return coerceAliasMap(contextNode?.anthropicToolNameMap);
315
+ const fromContextNode = coerceAliasMap(contextNode?.anthropicToolNameMap);
316
+ if (fromContextNode) {
317
+ return fromContextNode;
318
+ }
319
+ return readAliasMapFromSemantics(chatEnvelope);
326
320
  }
327
321
  resolveProtocolHooks(protocol) {
328
322
  switch (protocol) {
@@ -409,6 +403,11 @@ export class HubPipeline {
409
403
  if (responsesResume) {
410
404
  adapterContext.responsesResume = responsesResume;
411
405
  }
406
+ // 透传 gemini_empty_reply_continue 的重试计数,便于在多次空回复后终止自动续写。
407
+ const emptyReplyCount = metadata.geminiEmptyReplyCount;
408
+ if (typeof emptyReplyCount === 'number' && Number.isFinite(emptyReplyCount)) {
409
+ adapterContext.geminiEmptyReplyCount = emptyReplyCount;
410
+ }
412
411
  if (target?.compatibilityProfile && typeof target.compatibilityProfile === 'string') {
413
412
  adapterContext.compatibilityProfile = target.compatibilityProfile;
414
413
  }
@@ -432,16 +431,6 @@ export class HubPipeline {
432
431
  }
433
432
  return value;
434
433
  }
435
- pickRawRequestBody(metadata) {
436
- if (!metadata || typeof metadata !== 'object') {
437
- return undefined;
438
- }
439
- const raw = metadata.__raw_request_body;
440
- if (!raw || typeof raw !== 'object') {
441
- return undefined;
442
- }
443
- return raw;
444
- }
445
434
  async normalizeRequest(request) {
446
435
  if (!request || typeof request !== 'object') {
447
436
  throw new Error('HubPipeline requires request payload');
@@ -695,3 +684,13 @@ function coerceAliasMap(candidate) {
695
684
  }
696
685
  return Object.keys(normalized).length ? normalized : undefined;
697
686
  }
687
+ function readAliasMapFromSemantics(chatEnvelope) {
688
+ if (!chatEnvelope?.semantics || typeof chatEnvelope.semantics !== 'object') {
689
+ return undefined;
690
+ }
691
+ const node = chatEnvelope.semantics.anthropic;
692
+ if (!node || !isJsonObject(node)) {
693
+ return undefined;
694
+ }
695
+ return coerceAliasMap(node.toolAliasMap);
696
+ }
@@ -15,7 +15,6 @@ export function runRespOutboundStage1ClientRemap(options) {
15
15
  clientPayload = buildResponsesPayloadFromChat(options.payload, {
16
16
  requestId: options.requestId
17
17
  });
18
- mergeOriginalResponsesPayload(clientPayload, options.adapterContext);
19
18
  }
20
19
  recordStage(options.stageRecorder, 'resp_outbound_stage1_client_remap', clientPayload);
21
20
  return clientPayload;
@@ -42,36 +41,3 @@ function resolveAliasMapFromContext(adapterContext) {
42
41
  }
43
42
  return Object.keys(map).length ? map : undefined;
44
43
  }
45
- function mergeOriginalResponsesPayload(payload, adapterContext) {
46
- if (!adapterContext) {
47
- return;
48
- }
49
- const raw = adapterContext.__raw_responses_payload;
50
- if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
51
- return;
52
- }
53
- try {
54
- if (payload.required_action == null && raw.required_action != null) {
55
- payload.required_action = JSON.parse(JSON.stringify(raw.required_action));
56
- }
57
- }
58
- catch {
59
- /* ignore clone errors */
60
- }
61
- const rawStatus = typeof raw.status === 'string' ? raw.status : undefined;
62
- if (rawStatus === 'requires_action') {
63
- payload.status = 'requires_action';
64
- }
65
- // 如果桥接后的 payload 没有 usage,而原始 Responses 载荷带有 usage,则回填原始 usage,
66
- // 确保 token usage 不在工具/桥接路径中丢失。
67
- const payloadUsage = payload.usage;
68
- const rawUsage = raw.usage;
69
- if ((payloadUsage == null || typeof payloadUsage !== 'object') && rawUsage && typeof rawUsage === 'object') {
70
- try {
71
- payload.usage = JSON.parse(JSON.stringify(rawUsage));
72
- }
73
- catch {
74
- payload.usage = rawUsage;
75
- }
76
- }
77
- }
@@ -1,7 +1,7 @@
1
1
  import { runChatRequestToolFilters } from '../../shared/tool-filter-pipeline.js';
2
2
  import { ToolGovernanceEngine } from '../tool-governance/index.js';
3
- import { detectLastAssistantToolCategory } from '../../../router/virtual-router/tool-signals.js';
4
3
  import { ensureApplyPatchSchema } from '../../shared/tool-mapping.js';
4
+ import { isJsonObject } from '../types/json.js';
5
5
  const toolGovernanceEngine = new ToolGovernanceEngine();
6
6
  export async function runHubChatProcess(options) {
7
7
  const startTime = Date.now();
@@ -404,24 +404,21 @@ function maybeInjectWebSearchTool(request, metadata) {
404
404
  if (!rawConfig || !Array.isArray(rawConfig.engines) || rawConfig.engines.length === 0) {
405
405
  return request;
406
406
  }
407
- const injectPolicy = rawConfig.injectPolicy === 'always' || rawConfig.injectPolicy === 'selective'
408
- ? rawConfig.injectPolicy
409
- : 'selective';
407
+ const semanticsWebSearch = extractWebSearchSemantics(request.semantics);
408
+ if (semanticsWebSearch?.disable === true) {
409
+ return request;
410
+ }
411
+ const injectPolicy = semanticsWebSearch?.force === true
412
+ ? 'always'
413
+ : rawConfig.injectPolicy === 'always' || rawConfig.injectPolicy === 'selective'
414
+ ? rawConfig.injectPolicy
415
+ : 'selective';
410
416
  const intent = detectWebSearchIntent(request);
411
417
  if (injectPolicy === 'selective') {
418
+ // 仅当当前这一轮用户输入明确表达“联网搜索”意图时才注入 web_search。
419
+ // 不再依赖上一轮工具分类(read/search/websearch),避免形成隐式续写语义。
412
420
  if (!intent.hasIntent) {
413
- // 当最近一条用户消息没有明显的“联网搜索”关键词时,
414
- // 如果上一轮 assistant 的工具调用已经属于搜索类(如 web_search),
415
- // 则仍然视为 web_search 续写场景,强制注入 web_search 工具,
416
- // 以便在后续路由中按 servertool 逻辑跳过不适配的 Provider(例如 serverToolsDisabled 的 crs)。
417
- const assistantMessages = Array.isArray(request.messages)
418
- ? request.messages.filter((msg) => msg && msg.role === 'assistant')
419
- : [];
420
- const lastTool = detectLastAssistantToolCategory(assistantMessages);
421
- const hasSearchToolContext = lastTool?.category === 'search';
422
- if (!hasSearchToolContext) {
423
- return request;
424
- }
421
+ return request;
425
422
  }
426
423
  }
427
424
  const existingTools = Array.isArray(request.tools) ? request.tools : [];
@@ -522,6 +519,30 @@ function maybeInjectWebSearchTool(request, metadata) {
522
519
  tools: [...existingTools, webSearchTool]
523
520
  };
524
521
  }
522
+ function extractWebSearchSemantics(semantics) {
523
+ if (!semantics || typeof semantics !== 'object') {
524
+ return undefined;
525
+ }
526
+ const extras = semantics.providerExtras;
527
+ if (!extras || !isJsonObject(extras)) {
528
+ return undefined;
529
+ }
530
+ const hint = extras.webSearch;
531
+ if (typeof hint === 'boolean') {
532
+ return hint ? { force: true } : { disable: true };
533
+ }
534
+ if (isJsonObject(hint)) {
535
+ const normalized = {};
536
+ if (hint.force === true) {
537
+ normalized.force = true;
538
+ }
539
+ if (hint.disable === true) {
540
+ normalized.disable = true;
541
+ }
542
+ return Object.keys(normalized).length ? normalized : undefined;
543
+ }
544
+ return undefined;
545
+ }
525
546
  function detectWebSearchIntent(request) {
526
547
  const messages = Array.isArray(request.messages) ? request.messages : [];
527
548
  if (!messages.length) {
@@ -130,14 +130,6 @@ export async function convertProviderResponse(options) {
130
130
  catch {
131
131
  // ignore conversation capture errors
132
132
  }
133
- if (formatEnvelope.payload && typeof formatEnvelope.payload === 'object') {
134
- try {
135
- options.context.__raw_responses_payload = JSON.parse(JSON.stringify(formatEnvelope.payload));
136
- }
137
- catch {
138
- /* best-effort clone */
139
- }
140
- }
141
133
  }
142
134
  formatEnvelope.payload = runRespInboundStageCompatResponse({
143
135
  payload: formatEnvelope.payload,
@@ -21,6 +21,15 @@ function flattenAnthropicContent(content) {
21
21
  }
22
22
  return '';
23
23
  }
24
+ function sanitizeAnthropicToolUseId(raw) {
25
+ if (typeof raw === 'string') {
26
+ const trimmed = raw.trim();
27
+ if (trimmed && /^[A-Za-z0-9_-]+$/.test(trimmed)) {
28
+ return trimmed;
29
+ }
30
+ }
31
+ return `call_${Math.random().toString(36).slice(2, 10)}`;
32
+ }
24
33
  function createToolNameResolver(options) {
25
34
  const reverse = new Map();
26
35
  const aliasMap = options?.aliasMap;
@@ -281,6 +290,39 @@ export function buildOpenAIChatFromAnthropicMessage(payload, options) {
281
290
  }
282
291
  return chatResponse;
283
292
  }
293
+ function mapShellCommandArgsForAnthropic(raw) {
294
+ const result = {};
295
+ const source = (raw && typeof raw === 'object' && !Array.isArray(raw)) ? raw : {};
296
+ const commandRaw = typeof source.command === 'string' && source.command.trim().length
297
+ ? source.command
298
+ : typeof source.cmd === 'string' && source.cmd.trim().length
299
+ ? source.cmd
300
+ : '';
301
+ const command = commandRaw.trim();
302
+ if (command) {
303
+ result.command = command;
304
+ }
305
+ const timeoutRaw = source.timeout_ms ?? source.timeout;
306
+ if (typeof timeoutRaw === 'number' && Number.isFinite(timeoutRaw)) {
307
+ result.timeout = timeoutRaw;
308
+ }
309
+ else if (typeof timeoutRaw === 'string' && timeoutRaw.trim().length) {
310
+ const parsed = Number(timeoutRaw.trim());
311
+ if (Number.isFinite(parsed)) {
312
+ result.timeout = parsed;
313
+ }
314
+ }
315
+ if (typeof source.description === 'string' && source.description.trim().length) {
316
+ result.description = source.description;
317
+ }
318
+ if (typeof source.run_in_background === 'boolean') {
319
+ result.run_in_background = source.run_in_background;
320
+ }
321
+ if (typeof source.dangerouslyDisableSandbox === 'boolean') {
322
+ result.dangerouslyDisableSandbox = source.dangerouslyDisableSandbox;
323
+ }
324
+ return result;
325
+ }
284
326
  export function buildAnthropicResponseFromChat(chatResponse, options) {
285
327
  const choice = Array.isArray(chatResponse?.choices) ? chatResponse.choices[0] : undefined;
286
328
  const message = choice && typeof choice === 'object' ? choice.message : undefined;
@@ -327,6 +369,7 @@ export function buildAnthropicResponseFromChat(chatResponse, options) {
327
369
  const fn = call.function || {};
328
370
  if (typeof fn?.name !== 'string')
329
371
  continue;
372
+ const canonicalName = normalizeAnthropicToolName(fn.name) ?? fn.name;
330
373
  const serializedName = outboundAliasSerializer(fn.name);
331
374
  let parsedArgs = {};
332
375
  const args = fn.arguments;
@@ -341,9 +384,12 @@ export function buildAnthropicResponseFromChat(chatResponse, options) {
341
384
  else {
342
385
  parsedArgs = args ?? {};
343
386
  }
387
+ if ((canonicalName || '').trim() === 'shell_command') {
388
+ parsedArgs = mapShellCommandArgsForAnthropic(parsedArgs);
389
+ }
344
390
  contentBlocks.push({
345
391
  type: 'tool_use',
346
- id: typeof call.id === 'string' ? call.id : `call_${Math.random().toString(36).slice(2, 8)}`,
392
+ id: sanitizeAnthropicToolUseId(call.id),
347
393
  name: serializedName,
348
394
  input: parsedArgs
349
395
  });
@@ -34,6 +34,43 @@ const ANTHROPIC_TOP_LEVEL_FIELDS = new Set([
34
34
  ]);
35
35
  const PASSTHROUGH_METADATA_PREFIX = 'rcc_passthrough_';
36
36
  const PASSTHROUGH_PARAMETERS = ['tool_choice'];
37
+ function ensureSemantics(chat) {
38
+ if (!chat.semantics || typeof chat.semantics !== 'object') {
39
+ chat.semantics = {};
40
+ }
41
+ return chat.semantics;
42
+ }
43
+ function ensureAnthropicSemanticsNode(chat) {
44
+ const semantics = ensureSemantics(chat);
45
+ if (!semantics.anthropic || !isJsonObject(semantics.anthropic)) {
46
+ semantics.anthropic = {};
47
+ }
48
+ return semantics.anthropic;
49
+ }
50
+ function markExplicitEmptyTools(chat) {
51
+ const semantics = ensureSemantics(chat);
52
+ if (!semantics.tools || !isJsonObject(semantics.tools)) {
53
+ semantics.tools = {};
54
+ }
55
+ semantics.tools.explicitEmpty = true;
56
+ }
57
+ function readAnthropicSemantics(chat) {
58
+ if (!chat.semantics || typeof chat.semantics !== 'object') {
59
+ return undefined;
60
+ }
61
+ const node = chat.semantics.anthropic;
62
+ return node && isJsonObject(node) ? node : undefined;
63
+ }
64
+ function hasExplicitEmptyToolsSemantics(chat) {
65
+ if (!chat.semantics || typeof chat.semantics !== 'object') {
66
+ return false;
67
+ }
68
+ const toolsNode = chat.semantics.tools;
69
+ if (!toolsNode || !isJsonObject(toolsNode)) {
70
+ return false;
71
+ }
72
+ return Boolean(toolsNode.explicitEmpty);
73
+ }
37
74
  function sanitizeAnthropicPayload(payload) {
38
75
  for (const key of Object.keys(payload)) {
39
76
  if (!ANTHROPIC_TOP_LEVEL_FIELDS.has(key)) {
@@ -91,6 +128,7 @@ export class AnthropicSemanticMapper {
91
128
  const metadata = chatEnvelope.metadata ?? { context: canonicalContext };
92
129
  chatEnvelope.metadata = metadata;
93
130
  metadata.context = canonicalContext;
131
+ let semanticsNode;
94
132
  const resolveExtraFields = () => {
95
133
  if (!isJsonObject(metadata.extraFields)) {
96
134
  metadata.extraFields = {};
@@ -101,10 +139,13 @@ export class AnthropicSemanticMapper {
101
139
  const systemBlocks = cloneAnthropicSystemBlocks(payload.system);
102
140
  if (systemBlocks) {
103
141
  protocolState.systemBlocks = systemBlocks;
142
+ semanticsNode = semanticsNode ?? ensureAnthropicSemanticsNode(chatEnvelope);
143
+ semanticsNode.systemBlocks = jsonClone(systemBlocks);
104
144
  }
105
145
  if (payload.tools && Array.isArray(payload.tools) && payload.tools.length === 0) {
106
146
  metadata.toolsFieldPresent = true;
107
147
  resolveExtraFields().toolsFieldPresent = true;
148
+ markExplicitEmptyTools(chatEnvelope);
108
149
  }
109
150
  const aliasMap = buildAnthropicToolAliasMap(payload.tools);
110
151
  if (aliasMap) {
@@ -113,6 +154,8 @@ export class AnthropicSemanticMapper {
113
154
  canonicalContext.anthropicToolNameMap = aliasMap;
114
155
  metadata.anthropicToolNameMap = aliasMap;
115
156
  extraFields.anthropicToolNameMap = aliasMap;
157
+ semanticsNode = semanticsNode ?? ensureAnthropicSemanticsNode(chatEnvelope);
158
+ semanticsNode.toolAliasMap = jsonClone(aliasMap);
116
159
  }
117
160
  if (Array.isArray(payload.messages) && payload.messages.length) {
118
161
  const shapes = payload.messages.map((entry) => {
@@ -137,6 +180,8 @@ export class AnthropicSemanticMapper {
137
180
  : {};
138
181
  mirrorNode.messageContentShape = shapes;
139
182
  extraFields.anthropicMirror = mirrorNode;
183
+ semanticsNode = semanticsNode ?? ensureAnthropicSemanticsNode(chatEnvelope);
184
+ semanticsNode.mirror = jsonClone(mirrorNode);
140
185
  }
141
186
  if (missing.length) {
142
187
  metadata.missingFields = Array.isArray(metadata.missingFields)
@@ -147,6 +192,8 @@ export class AnthropicSemanticMapper {
147
192
  (payload.metadata && isJsonObject(payload.metadata) ? jsonClone(payload.metadata) : undefined);
148
193
  if (providerMetadata) {
149
194
  metadata.providerMetadata = providerMetadata;
195
+ semanticsNode = semanticsNode ?? ensureAnthropicSemanticsNode(chatEnvelope);
196
+ semanticsNode.providerMetadata = jsonClone(providerMetadata);
150
197
  }
151
198
  const mergedParameters = { ...(chatEnvelope.parameters ?? {}) };
152
199
  const mergeParameters = (source) => {
@@ -217,6 +264,8 @@ export class AnthropicSemanticMapper {
217
264
  messages: chat.messages,
218
265
  tools: chat.tools
219
266
  };
267
+ const semanticsNode = readAnthropicSemantics(chat);
268
+ const explicitEmptyTools = (chat.metadata?.toolsFieldPresent === true) || hasExplicitEmptyToolsSemantics(chat);
220
269
  const trimmedParameters = chat.parameters && typeof chat.parameters === 'object' ? chat.parameters : undefined;
221
270
  if (trimmedParameters) {
222
271
  for (const [key, value] of Object.entries(trimmedParameters)) {
@@ -245,16 +294,19 @@ export class AnthropicSemanticMapper {
245
294
  if (baseRequest.max_output_tokens && !baseRequest.max_tokens) {
246
295
  baseRequest.max_tokens = baseRequest.max_output_tokens;
247
296
  }
248
- if (isJsonObject(chat.metadata?.providerMetadata)) {
249
- baseRequest.metadata = jsonClone(chat.metadata?.providerMetadata);
250
- }
251
- if (chat.metadata?.toolsFieldPresent && (!Array.isArray(chat.tools) || chat.tools.length === 0)) {
297
+ // 出站阶段不再直接透传其它协议的 providerMetadata,避免跨协议打洞;
298
+ // Anthropic 自身入口的 metadata 已在入站阶段通过 collectParameters/encodeMetadataPassthrough
299
+ // 按白名单收集,这里仅依赖这些显式映射结果。
300
+ if (explicitEmptyTools && (!Array.isArray(chat.tools) || chat.tools.length === 0)) {
252
301
  baseRequest.tools = [];
253
302
  }
254
303
  const protocolState = getProtocolState(chat.metadata, 'anthropic');
255
304
  if (protocolState?.systemBlocks !== undefined) {
256
305
  baseRequest.system = jsonClone(protocolState.systemBlocks);
257
306
  }
307
+ else if (semanticsNode?.systemBlocks !== undefined) {
308
+ baseRequest.system = jsonClone(semanticsNode.systemBlocks);
309
+ }
258
310
  if (chat.metadata &&
259
311
  typeof chat.metadata === 'object' &&
260
312
  chat.metadata.extraFields &&
@@ -262,6 +314,9 @@ export class AnthropicSemanticMapper {
262
314
  chat.metadata.extraFields.anthropicMirror) {
263
315
  baseRequest.__anthropicMirror = jsonClone(chat.metadata.extraFields.anthropicMirror ?? {});
264
316
  }
317
+ else if (semanticsNode?.mirror && isJsonObject(semanticsNode.mirror)) {
318
+ baseRequest.__anthropicMirror = jsonClone(semanticsNode.mirror);
319
+ }
265
320
  const payloadSource = buildAnthropicRequestFromOpenAIChat(baseRequest);
266
321
  const payload = sanitizeAnthropicPayload(JSON.parse(JSON.stringify(payloadSource)));
267
322
  if (chat.metadata?.toolsFieldPresent && (!Array.isArray(chat.tools) || chat.tools.length === 0)) {
@@ -0,0 +1,8 @@
1
+ import type { SemanticMapper } from '../format-adapters/index.js';
2
+ import type { AdapterContext, ChatEnvelope } from '../types/chat-envelope.js';
3
+ import type { FormatEnvelope } from '../types/format-envelope.js';
4
+ export declare function maybeAugmentApplyPatchErrorContent(content: string, toolName?: string): string;
5
+ export declare class ChatSemanticMapper implements SemanticMapper {
6
+ toChat(format: FormatEnvelope, ctx: AdapterContext): Promise<ChatEnvelope>;
7
+ fromChat(chat: ChatEnvelope, ctx: AdapterContext): Promise<FormatEnvelope>;
8
+ }