@jsonstudio/llms 0.4.4 → 0.4.5

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 (159) hide show
  1. package/dist/conversion/codec-registry.js +11 -1
  2. package/dist/conversion/codecs/anthropic-openai-codec.d.ts +13 -0
  3. package/dist/conversion/codecs/anthropic-openai-codec.js +18 -473
  4. package/dist/conversion/codecs/gemini-openai-codec.js +91 -48
  5. package/dist/conversion/codecs/responses-openai-codec.js +9 -2
  6. package/dist/conversion/hub/format-adapters/anthropic-format-adapter.js +3 -0
  7. package/dist/conversion/hub/format-adapters/chat-format-adapter.js +3 -0
  8. package/dist/conversion/hub/format-adapters/gemini-format-adapter.js +3 -0
  9. package/dist/conversion/hub/format-adapters/responses-format-adapter.d.ts +19 -0
  10. package/dist/conversion/hub/format-adapters/responses-format-adapter.js +9 -0
  11. package/dist/conversion/hub/node-support.js +3 -1
  12. package/dist/conversion/hub/pipeline/hub-pipeline.js +37 -32
  13. package/dist/conversion/hub/response/provider-response.js +1 -1
  14. package/dist/conversion/hub/response/response-mappers.js +1 -1
  15. package/dist/conversion/hub/response/response-runtime.js +109 -10
  16. package/dist/conversion/hub/semantic-mappers/anthropic-mapper.js +70 -156
  17. package/dist/conversion/hub/semantic-mappers/chat-mapper.js +63 -52
  18. package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +76 -143
  19. package/dist/conversion/hub/semantic-mappers/responses-mapper.js +40 -160
  20. package/dist/conversion/hub/standardized-bridge.js +3 -0
  21. package/dist/conversion/hub/tool-governance/rules.js +2 -2
  22. package/dist/conversion/index.d.ts +5 -0
  23. package/dist/conversion/index.js +5 -0
  24. package/dist/conversion/pipeline/codecs/v2/anthropic-openai-pipeline.d.ts +12 -0
  25. package/dist/conversion/pipeline/codecs/v2/anthropic-openai-pipeline.js +100 -0
  26. package/dist/conversion/pipeline/codecs/v2/openai-openai-pipeline.d.ts +15 -0
  27. package/dist/conversion/pipeline/codecs/v2/openai-openai-pipeline.js +174 -0
  28. package/dist/conversion/pipeline/codecs/v2/responses-openai-pipeline.d.ts +14 -0
  29. package/dist/conversion/pipeline/codecs/v2/responses-openai-pipeline.js +166 -0
  30. package/dist/conversion/pipeline/codecs/v2/shared/openai-chat-helpers.d.ts +13 -0
  31. package/dist/conversion/pipeline/codecs/v2/shared/openai-chat-helpers.js +66 -0
  32. package/dist/conversion/pipeline/hooks/adapter-context.d.ts +7 -0
  33. package/dist/conversion/pipeline/hooks/adapter-context.js +18 -0
  34. package/dist/conversion/pipeline/hooks/protocol-hooks.d.ts +67 -0
  35. package/dist/conversion/pipeline/hooks/protocol-hooks.js +1 -0
  36. package/dist/conversion/pipeline/index.d.ts +35 -0
  37. package/dist/conversion/pipeline/index.js +103 -0
  38. package/dist/conversion/pipeline/meta/meta-bag.d.ts +20 -0
  39. package/dist/conversion/pipeline/meta/meta-bag.js +81 -0
  40. package/dist/conversion/pipeline/schema/canonical-chat.d.ts +18 -0
  41. package/dist/conversion/pipeline/schema/canonical-chat.js +1 -0
  42. package/dist/conversion/pipeline/schema/index.d.ts +1 -0
  43. package/dist/conversion/pipeline/schema/index.js +1 -0
  44. package/dist/conversion/responses/responses-openai-bridge.d.ts +48 -0
  45. package/dist/conversion/responses/responses-openai-bridge.js +157 -1146
  46. package/dist/conversion/shared/anthropic-message-utils.d.ts +12 -0
  47. package/dist/conversion/shared/anthropic-message-utils.js +587 -0
  48. package/dist/conversion/shared/bridge-actions.d.ts +39 -0
  49. package/dist/conversion/shared/bridge-actions.js +709 -0
  50. package/dist/conversion/shared/bridge-conversation-store.d.ts +41 -0
  51. package/dist/conversion/shared/bridge-conversation-store.js +279 -0
  52. package/dist/conversion/shared/bridge-id-utils.d.ts +7 -0
  53. package/dist/conversion/shared/bridge-id-utils.js +42 -0
  54. package/dist/conversion/shared/bridge-instructions.d.ts +1 -0
  55. package/dist/conversion/shared/bridge-instructions.js +113 -0
  56. package/dist/conversion/shared/bridge-message-types.d.ts +39 -0
  57. package/dist/conversion/shared/bridge-message-types.js +1 -0
  58. package/dist/conversion/shared/bridge-message-utils.d.ts +22 -0
  59. package/dist/conversion/shared/bridge-message-utils.js +473 -0
  60. package/dist/conversion/shared/bridge-metadata.d.ts +1 -0
  61. package/dist/conversion/shared/bridge-metadata.js +1 -0
  62. package/dist/conversion/shared/bridge-policies.d.ts +18 -0
  63. package/dist/conversion/shared/bridge-policies.js +276 -0
  64. package/dist/conversion/shared/bridge-request-adapter.d.ts +28 -0
  65. package/dist/conversion/shared/bridge-request-adapter.js +430 -0
  66. package/dist/conversion/shared/chat-output-normalizer.d.ts +4 -0
  67. package/dist/conversion/shared/chat-output-normalizer.js +56 -0
  68. package/dist/conversion/shared/chat-request-filters.js +24 -1
  69. package/dist/conversion/shared/gemini-tool-utils.d.ts +5 -0
  70. package/dist/conversion/shared/gemini-tool-utils.js +130 -0
  71. package/dist/conversion/shared/metadata-passthrough.d.ts +11 -0
  72. package/dist/conversion/shared/metadata-passthrough.js +57 -0
  73. package/dist/conversion/shared/output-content-normalizer.d.ts +12 -0
  74. package/dist/conversion/shared/output-content-normalizer.js +119 -0
  75. package/dist/conversion/shared/reasoning-normalizer.d.ts +21 -0
  76. package/dist/conversion/shared/reasoning-normalizer.js +368 -0
  77. package/dist/conversion/shared/reasoning-tool-normalizer.d.ts +12 -0
  78. package/dist/conversion/shared/reasoning-tool-normalizer.js +132 -0
  79. package/dist/conversion/shared/reasoning-tool-parser.d.ts +10 -0
  80. package/dist/conversion/shared/reasoning-tool-parser.js +95 -0
  81. package/dist/conversion/shared/reasoning-utils.d.ts +2 -0
  82. package/dist/conversion/shared/reasoning-utils.js +42 -0
  83. package/dist/conversion/shared/responses-conversation-store.js +5 -11
  84. package/dist/conversion/shared/responses-message-utils.d.ts +15 -0
  85. package/dist/conversion/shared/responses-message-utils.js +206 -0
  86. package/dist/conversion/shared/responses-output-builder.d.ts +15 -0
  87. package/dist/conversion/shared/responses-output-builder.js +179 -0
  88. package/dist/conversion/shared/responses-output-utils.d.ts +7 -0
  89. package/dist/conversion/shared/responses-output-utils.js +108 -0
  90. package/dist/conversion/shared/responses-request-adapter.d.ts +28 -0
  91. package/dist/conversion/shared/responses-request-adapter.js +9 -40
  92. package/dist/conversion/shared/responses-response-utils.d.ts +3 -0
  93. package/dist/conversion/shared/responses-response-utils.js +209 -0
  94. package/dist/conversion/shared/responses-tool-utils.d.ts +12 -0
  95. package/dist/conversion/shared/responses-tool-utils.js +90 -0
  96. package/dist/conversion/shared/responses-types.d.ts +33 -0
  97. package/dist/conversion/shared/responses-types.js +1 -0
  98. package/dist/conversion/shared/tool-call-utils.d.ts +11 -0
  99. package/dist/conversion/shared/tool-call-utils.js +56 -0
  100. package/dist/conversion/shared/tool-mapping.d.ts +19 -0
  101. package/dist/conversion/shared/tool-mapping.js +124 -0
  102. package/dist/conversion/shared/tool-normalizers.d.ts +4 -0
  103. package/dist/conversion/shared/tool-normalizers.js +84 -0
  104. package/dist/router/virtual-router/bootstrap.js +18 -3
  105. package/dist/router/virtual-router/provider-registry.js +4 -2
  106. package/dist/router/virtual-router/types.d.ts +212 -0
  107. package/dist/sse/index.d.ts +38 -2
  108. package/dist/sse/index.js +27 -0
  109. package/dist/sse/json-to-sse/anthropic-json-to-sse-converter.d.ts +14 -0
  110. package/dist/sse/json-to-sse/anthropic-json-to-sse-converter.js +106 -73
  111. package/dist/sse/json-to-sse/chat-json-to-sse-converter.js +6 -2
  112. package/dist/sse/json-to-sse/gemini-json-to-sse-converter.d.ts +14 -0
  113. package/dist/sse/json-to-sse/gemini-json-to-sse-converter.js +99 -0
  114. package/dist/sse/json-to-sse/index.d.ts +7 -0
  115. package/dist/sse/json-to-sse/index.js +2 -0
  116. package/dist/sse/json-to-sse/sequencers/anthropic-sequencer.d.ts +13 -0
  117. package/dist/sse/json-to-sse/sequencers/anthropic-sequencer.js +150 -0
  118. package/dist/sse/json-to-sse/sequencers/chat-sequencer.d.ts +39 -0
  119. package/dist/sse/json-to-sse/sequencers/chat-sequencer.js +49 -3
  120. package/dist/sse/json-to-sse/sequencers/gemini-sequencer.d.ts +10 -0
  121. package/dist/sse/json-to-sse/sequencers/gemini-sequencer.js +95 -0
  122. package/dist/sse/json-to-sse/sequencers/responses-sequencer.js +31 -5
  123. package/dist/sse/registry/sse-codec-registry.d.ts +32 -0
  124. package/dist/sse/registry/sse-codec-registry.js +30 -1
  125. package/dist/sse/shared/reasoning-dispatcher.d.ts +10 -0
  126. package/dist/sse/shared/reasoning-dispatcher.js +25 -0
  127. package/dist/sse/shared/responses-output-normalizer.d.ts +12 -0
  128. package/dist/sse/shared/responses-output-normalizer.js +45 -0
  129. package/dist/sse/shared/serializers/anthropic-event-serializer.d.ts +2 -0
  130. package/dist/sse/shared/serializers/anthropic-event-serializer.js +9 -0
  131. package/dist/sse/shared/serializers/gemini-event-serializer.d.ts +2 -0
  132. package/dist/sse/shared/serializers/gemini-event-serializer.js +5 -0
  133. package/dist/sse/shared/serializers/index.d.ts +41 -0
  134. package/dist/sse/shared/serializers/index.js +2 -0
  135. package/dist/sse/shared/writer.d.ts +127 -0
  136. package/dist/sse/shared/writer.js +37 -1
  137. package/dist/sse/sse-to-json/anthropic-sse-to-json-converter.d.ts +11 -0
  138. package/dist/sse/sse-to-json/anthropic-sse-to-json-converter.js +92 -127
  139. package/dist/sse/sse-to-json/builders/anthropic-response-builder.d.ts +16 -0
  140. package/dist/sse/sse-to-json/builders/anthropic-response-builder.js +151 -0
  141. package/dist/sse/sse-to-json/builders/response-builder.d.ts +165 -0
  142. package/dist/sse/sse-to-json/builders/response-builder.js +27 -6
  143. package/dist/sse/sse-to-json/chat-sse-to-json-converter.d.ts +114 -0
  144. package/dist/sse/sse-to-json/chat-sse-to-json-converter.js +79 -3
  145. package/dist/sse/sse-to-json/gemini-sse-to-json-converter.d.ts +13 -0
  146. package/dist/sse/sse-to-json/gemini-sse-to-json-converter.js +160 -0
  147. package/dist/sse/sse-to-json/index.d.ts +7 -0
  148. package/dist/sse/sse-to-json/index.js +2 -0
  149. package/dist/sse/sse-to-json/parsers/sse-parser.js +53 -1
  150. package/dist/sse/types/anthropic-types.d.ts +170 -0
  151. package/dist/sse/types/anthropic-types.js +8 -5
  152. package/dist/sse/types/chat-types.d.ts +10 -0
  153. package/dist/sse/types/chat-types.js +2 -1
  154. package/dist/sse/types/core-interfaces.d.ts +1 -1
  155. package/dist/sse/types/gemini-types.d.ts +116 -0
  156. package/dist/sse/types/gemini-types.js +5 -0
  157. package/dist/sse/types/index.d.ts +5 -2
  158. package/dist/sse/types/index.js +2 -0
  159. package/package.json +1 -1
@@ -1,3 +1,8 @@
1
+ import { createBridgeActionState, runBridgeActionPipeline } from '../shared/bridge-actions.js';
2
+ import { resolveBridgePolicy, resolvePolicyActions } from '../shared/bridge-policies.js';
3
+ import { normalizeChatMessageContent } from '../shared/chat-output-normalizer.js';
4
+ import { mapBridgeToolsToChat } from '../shared/tool-mapping.js';
5
+ import { prepareGeminiToolsForBridge } from '../shared/gemini-tool-utils.js';
1
6
  function isObject(v) {
2
7
  return !!v && typeof v === 'object' && !Array.isArray(v);
3
8
  }
@@ -48,49 +53,6 @@ function mapChatRoleToGemini(role) {
48
53
  return 'tool';
49
54
  return 'user';
50
55
  }
51
- function mapGeminiToolsToOpenAI(tools) {
52
- const out = [];
53
- if (!tools)
54
- return out;
55
- const arr = Array.isArray(tools) ? tools : [tools];
56
- for (const t of arr) {
57
- if (!t || typeof t !== 'object')
58
- continue;
59
- const tObj = t;
60
- const fds = Array.isArray(tObj.functionDeclarations) ? tObj.functionDeclarations : [];
61
- if (fds.length) {
62
- for (const fd of fds) {
63
- if (!fd || typeof fd !== 'object')
64
- continue;
65
- const name = typeof fd.name === 'string' ? String(fd.name) : undefined;
66
- if (!name)
67
- continue;
68
- const description = typeof fd.description === 'string' ? String(fd.description) : undefined;
69
- const parameters = isObject(fd.parameters)
70
- ? JSON.parse(JSON.stringify(fd.parameters))
71
- : { type: 'object', properties: {} };
72
- const fn = { name, parameters };
73
- if (description)
74
- fn.description = description;
75
- out.push({ type: 'function', function: fn });
76
- }
77
- continue;
78
- }
79
- // 单个函数声明形状:直接把对象视为 FunctionDeclaration
80
- const name = typeof tObj.name === 'string' ? String(tObj.name) : undefined;
81
- if (name) {
82
- const description = typeof tObj.description === 'string' ? String(tObj.description) : undefined;
83
- const parameters = isObject(tObj.parameters)
84
- ? JSON.parse(JSON.stringify(tObj.parameters))
85
- : { type: 'object', properties: {} };
86
- const fn = { name, parameters };
87
- if (description)
88
- fn.description = description;
89
- out.push({ type: 'function', function: fn });
90
- }
91
- }
92
- return out;
93
- }
94
56
  export function buildOpenAIChatFromGeminiRequest(payload) {
95
57
  const messages = [];
96
58
  // systemInstruction → Chat system 消息
@@ -160,8 +122,21 @@ export function buildOpenAIChatFromGeminiRequest(payload) {
160
122
  continue;
161
123
  }
162
124
  }
163
- if (textParts.length > 0 || toolCalls.length > 0) {
164
- const msg = { role, content: textParts.join('\n') };
125
+ const combinedText = textParts.join('\n');
126
+ const normalized = combinedText.length ? normalizeChatMessageContent(combinedText) : { contentText: undefined, reasoningText: undefined };
127
+ const hasText = typeof normalized.contentText === 'string' && normalized.contentText.length > 0;
128
+ const reasoningChunks = [];
129
+ if (typeof normalized.reasoningText === 'string' && normalized.reasoningText.trim().length) {
130
+ reasoningChunks.push(normalized.reasoningText.trim());
131
+ }
132
+ if (hasText || toolCalls.length > 0 || reasoningChunks.length > 0) {
133
+ const msg = {
134
+ role,
135
+ content: normalized.contentText ?? combinedText ?? ''
136
+ };
137
+ if (reasoningChunks.length) {
138
+ msg.reasoning_content = reasoningChunks.join('\n');
139
+ }
165
140
  if (toolCalls.length)
166
141
  msg.tool_calls = toolCalls;
167
142
  messages.push(msg);
@@ -178,6 +153,7 @@ export function buildOpenAIChatFromGeminiResponse(payload) {
178
153
  const role = mapGeminiRoleToChat(content.role);
179
154
  const parts = Array.isArray(content.parts) ? content.parts : [];
180
155
  const textParts = [];
156
+ const reasoningParts = [];
181
157
  const toolCalls = [];
182
158
  for (const part of parts) {
183
159
  if (!part || typeof part !== 'object')
@@ -189,6 +165,21 @@ export function buildOpenAIChatFromGeminiResponse(payload) {
189
165
  textParts.push(t);
190
166
  continue;
191
167
  }
168
+ if (Array.isArray(pObj.content)) {
169
+ for (const inner of pObj.content) {
170
+ if (typeof inner === 'string') {
171
+ textParts.push(inner);
172
+ }
173
+ else if (inner && typeof inner === 'object' && typeof inner.text === 'string') {
174
+ textParts.push(inner.text);
175
+ }
176
+ }
177
+ continue;
178
+ }
179
+ if (typeof pObj.reasoning === 'string') {
180
+ reasoningParts.push(pObj.reasoning);
181
+ continue;
182
+ }
192
183
  if (pObj.functionCall && typeof pObj.functionCall === 'object') {
193
184
  const fc = pObj.functionCall;
194
185
  const name = typeof fc.name === 'string' ? String(fc.name) : undefined;
@@ -234,13 +225,42 @@ export function buildOpenAIChatFromGeminiResponse(payload) {
234
225
  usage.completion_tokens = completionTokens;
235
226
  if (Number.isFinite(totalTokens))
236
227
  usage.total_tokens = totalTokens;
228
+ const combinedText = textParts.join('\n');
229
+ const normalized = combinedText.length ? normalizeChatMessageContent(combinedText) : { contentText: undefined, reasoningText: undefined };
237
230
  const chatMsg = {
238
231
  role,
239
- content: textParts.join('\n')
232
+ content: normalized.contentText ?? combinedText ?? ''
240
233
  };
234
+ if (typeof normalized.reasoningText === 'string' && normalized.reasoningText.trim().length) {
235
+ reasoningParts.push(normalized.reasoningText.trim());
236
+ }
237
+ if (reasoningParts.length) {
238
+ chatMsg.reasoning_content = reasoningParts.join('\n');
239
+ }
241
240
  if (toolCalls.length) {
242
241
  chatMsg.tool_calls = toolCalls;
243
242
  }
243
+ try {
244
+ const bridgePolicy = resolveBridgePolicy({ protocol: 'gemini-chat' });
245
+ const actions = resolvePolicyActions(bridgePolicy, 'response_inbound');
246
+ if (actions?.length) {
247
+ const actionState = createBridgeActionState({
248
+ messages: [chatMsg],
249
+ rawResponse: isObject(payload) ? payload : undefined
250
+ });
251
+ runBridgeActionPipeline({
252
+ stage: 'response_inbound',
253
+ actions,
254
+ protocol: bridgePolicy?.protocol ?? 'gemini-chat',
255
+ moduleType: bridgePolicy?.moduleType ?? 'gemini-chat',
256
+ requestId: typeof payload?.id === 'string' ? String(payload.id) : undefined,
257
+ state: actionState
258
+ });
259
+ }
260
+ }
261
+ catch {
262
+ // best-effort policy execution
263
+ }
244
264
  const chatResp = {
245
265
  id: payload?.id || `chatcmpl_${Date.now()}`,
246
266
  object: 'chat.completion',
@@ -276,6 +296,28 @@ export function buildGeminiFromOpenAIChat(chatResp) {
276
296
  return 'STOP';
277
297
  })();
278
298
  const baseRole = mapChatRoleToGemini(msg.role || 'assistant');
299
+ if (msg) {
300
+ try {
301
+ const bridgePolicy = resolveBridgePolicy({ protocol: 'gemini-chat' });
302
+ const actions = resolvePolicyActions(bridgePolicy, 'response_outbound');
303
+ if (actions?.length) {
304
+ const actionState = createBridgeActionState({
305
+ messages: [msg]
306
+ });
307
+ runBridgeActionPipeline({
308
+ stage: 'response_outbound',
309
+ actions,
310
+ protocol: bridgePolicy?.protocol ?? 'gemini-chat',
311
+ moduleType: bridgePolicy?.moduleType ?? 'gemini-chat',
312
+ requestId: typeof chatResp?.id === 'string' ? String(chatResp.id) : undefined,
313
+ state: actionState
314
+ });
315
+ }
316
+ }
317
+ catch {
318
+ // ignore policy failures
319
+ }
320
+ }
279
321
  const parts = [];
280
322
  const contentText = (() => {
281
323
  const raw = msg.content;
@@ -377,8 +419,9 @@ export class GeminiOpenAIConversionCodec {
377
419
  const { messages } = buildOpenAIChatFromGeminiRequest(payload);
378
420
  const out = { model, messages };
379
421
  try {
380
- const tools = mapGeminiToolsToOpenAI(payload?.tools);
381
- if (tools.length > 0)
422
+ const bridgeTools = prepareGeminiToolsForBridge(payload?.tools);
423
+ const tools = bridgeTools ? mapBridgeToolsToChat(bridgeTools) : undefined;
424
+ if (tools && tools.length > 0)
382
425
  out.tools = tools;
383
426
  }
384
427
  catch {
@@ -1,5 +1,5 @@
1
1
  import { buildResponsesPayloadFromChat, runStandardChatRequestFilters } from '../index.js';
2
- import { captureResponsesContext, buildChatRequestFromResponses } from '../shared/responses-request-adapter.js';
2
+ import { captureResponsesContext, buildChatRequestFromResponses } from '../responses/responses-openai-bridge.js';
3
3
  import { FilterEngine, ResponseToolTextCanonicalizeFilter, ResponseToolArgumentsStringifyFilter, ResponseFinishInvariantsFilter } from '../../filters/index.js';
4
4
  // Ported from root package (no behavior change). Types relaxed.
5
5
  export class ResponsesOpenAIConversionCodec {
@@ -82,7 +82,14 @@ export class ResponsesOpenAIConversionCodec {
82
82
  entryEndpoint: context.entryEndpoint ?? dto.metadata?.entryEndpoint ?? context.endpoint,
83
83
  endpoint: context.endpoint ?? dto.metadata?.endpoint
84
84
  };
85
- return runStandardChatRequestFilters(chatRequest, profile, ctxForFilters);
85
+ const filtered = await runStandardChatRequestFilters(chatRequest, profile, ctxForFilters);
86
+ if (filtered && typeof filtered === 'object') {
87
+ const maybe = filtered;
88
+ if (maybe.max_tokens === undefined && typeof maybe.max_output_tokens === 'number') {
89
+ maybe.max_tokens = maybe.max_output_tokens;
90
+ }
91
+ }
92
+ return filtered;
86
93
  }
87
94
  async convertResponse(payload, _profile, context) {
88
95
  await this.ensureInit();
@@ -1,6 +1,8 @@
1
+ import { normalizeReasoningInAnthropicPayload } from '../../shared/reasoning-normalizer.js';
1
2
  export class AnthropicFormatAdapter {
2
3
  protocol = 'anthropic-messages';
3
4
  async parseRequest(original, _ctx) {
5
+ normalizeReasoningInAnthropicPayload(original);
4
6
  return {
5
7
  protocol: this.protocol,
6
8
  direction: 'request',
@@ -11,6 +13,7 @@ export class AnthropicFormatAdapter {
11
13
  return format.payload;
12
14
  }
13
15
  async parseResponse(original, _ctx) {
16
+ normalizeReasoningInAnthropicPayload(original);
14
17
  return {
15
18
  protocol: this.protocol,
16
19
  direction: 'response',
@@ -1,6 +1,8 @@
1
+ import { normalizeReasoningInChatPayload } from '../../shared/reasoning-normalizer.js';
1
2
  export class ChatFormatAdapter {
2
3
  protocol = 'openai-chat';
3
4
  async parseRequest(original, _ctx) {
5
+ normalizeReasoningInChatPayload(original);
4
6
  return {
5
7
  protocol: this.protocol,
6
8
  direction: 'request',
@@ -11,6 +13,7 @@ export class ChatFormatAdapter {
11
13
  return format.payload;
12
14
  }
13
15
  async parseResponse(original, _ctx) {
16
+ normalizeReasoningInChatPayload(original);
14
17
  return {
15
18
  protocol: this.protocol,
16
19
  direction: 'response',
@@ -1,6 +1,8 @@
1
+ import { normalizeReasoningInGeminiPayload } from '../../shared/reasoning-normalizer.js';
1
2
  export class GeminiFormatAdapter {
2
3
  protocol = 'gemini-chat';
3
4
  async parseRequest(original, _ctx) {
5
+ normalizeReasoningInGeminiPayload(original);
4
6
  return {
5
7
  protocol: this.protocol,
6
8
  direction: 'request',
@@ -11,6 +13,7 @@ export class GeminiFormatAdapter {
11
13
  return format.payload;
12
14
  }
13
15
  async parseResponse(original, _ctx) {
16
+ normalizeReasoningInGeminiPayload(original);
14
17
  return {
15
18
  protocol: this.protocol,
16
19
  direction: 'response',
@@ -0,0 +1,19 @@
1
+ import type { FormatAdapter } from './index.js';
2
+ import type { AdapterContext } from '../types/chat-envelope.js';
3
+ import type { FormatEnvelope } from '../types/format-envelope.js';
4
+ import type { JsonObject, JsonValue } from '../types/json.js';
5
+ interface ResponsesFormatPayload extends JsonObject {
6
+ input?: JsonValue[];
7
+ tools?: JsonValue[];
8
+ tool_outputs?: JsonValue[];
9
+ output?: JsonValue[];
10
+ id?: string;
11
+ }
12
+ export declare class ResponsesFormatAdapter implements FormatAdapter {
13
+ readonly protocol = "openai-responses";
14
+ parseRequest(original: JsonObject, _ctx: AdapterContext): Promise<FormatEnvelope<ResponsesFormatPayload>>;
15
+ buildRequest(format: FormatEnvelope<ResponsesFormatPayload>, _ctx: AdapterContext): Promise<ResponsesFormatPayload>;
16
+ parseResponse(original: JsonObject, _ctx: AdapterContext): Promise<FormatEnvelope<ResponsesFormatPayload>>;
17
+ buildResponse(format: FormatEnvelope<ResponsesFormatPayload>, _ctx: AdapterContext): Promise<ResponsesFormatPayload>;
18
+ }
19
+ export {};
@@ -1,6 +1,11 @@
1
+ import { normalizeReasoningInResponsesPayload } from '../../shared/reasoning-normalizer.js';
1
2
  export class ResponsesFormatAdapter {
2
3
  protocol = 'openai-responses';
3
4
  async parseRequest(original, _ctx) {
5
+ normalizeReasoningInResponsesPayload(original, {
6
+ includeInput: true,
7
+ includeInstructions: true
8
+ });
4
9
  return {
5
10
  protocol: this.protocol,
6
11
  direction: 'request',
@@ -11,6 +16,10 @@ export class ResponsesFormatAdapter {
11
16
  return format.payload;
12
17
  }
13
18
  async parseResponse(original, _ctx) {
19
+ normalizeReasoningInResponsesPayload(original, {
20
+ includeOutput: true,
21
+ includeRequiredAction: true
22
+ });
14
23
  return {
15
24
  protocol: this.protocol,
16
25
  direction: 'response',
@@ -102,6 +102,7 @@ function deriveAdapterContext(context, fallbackProtocol) {
102
102
  (typeof metadata.providerProtocol === 'string' ? metadata.providerProtocol : undefined) ||
103
103
  fallbackProtocol;
104
104
  const streamingHint = metadata.stream === true ? 'force' : metadata.stream === false ? 'disable' : 'auto';
105
+ const toolCallIdStyle = typeof metadata.toolCallIdStyle === 'string' ? metadata.toolCallIdStyle : undefined;
105
106
  return {
106
107
  requestId: context.request.id,
107
108
  entryEndpoint: (typeof requestContext.entryEndpoint === 'string' ? requestContext.entryEndpoint : context.request.endpoint) ||
@@ -110,6 +111,7 @@ function deriveAdapterContext(context, fallbackProtocol) {
110
111
  providerId: (target?.providerKey || metadata.providerKey),
111
112
  routeId: metadata.routeName,
112
113
  profileId: metadata.pipelineId,
113
- streamingHint
114
+ streamingHint,
115
+ toolCallIdStyle
114
116
  };
115
117
  }
@@ -121,6 +121,9 @@ export class HubPipeline {
121
121
  metadata.providerType = target.providerType;
122
122
  metadata.modelId = target.modelId;
123
123
  metadata.processMode = target.processMode || 'chat';
124
+ if (target.responsesConfig?.toolCallIdStyle) {
125
+ metadata.toolCallIdStyle = target.responsesConfig.toolCallIdStyle;
126
+ }
124
127
  if (originalModel && typeof originalModel === 'string' && originalModel.trim()) {
125
128
  const trimmed = originalModel.trim();
126
129
  if (typeof metadata.originalModelId !== 'string' || !metadata.originalModelId) {
@@ -237,9 +240,7 @@ export class HubPipeline {
237
240
  const entryEndpoint = typeof metadataRecord.entryEndpoint === 'string'
238
241
  ? normalizeEndpoint(metadataRecord.entryEndpoint)
239
242
  : endpoint;
240
- const providerProtocol = typeof metadataRecord.providerProtocol === 'string'
241
- ? metadataRecord.providerProtocol
242
- : inferProviderProtocol(entryEndpoint);
243
+ const providerProtocol = resolveProviderProtocol(metadataRecord.providerProtocol);
243
244
  const processMode = metadataRecord.processMode === 'passthrough' ? 'passthrough' : 'chat';
244
245
  const direction = metadataRecord.direction === 'response' ? 'response' : 'request';
245
246
  const stage = metadataRecord.stage === 'outbound' ? 'outbound' : 'inbound';
@@ -358,15 +359,11 @@ export class HubPipeline {
358
359
  }
359
360
  }
360
361
  resolveSseProtocol(context) {
361
- const providerMeta = context.metadata.provider ?? undefined;
362
- const profile = typeof providerMeta?.profile === 'string' ? providerMeta.profile : undefined;
363
- const providerProtocol = typeof context.metadata.providerProtocol === 'string'
364
- ? context.metadata.providerProtocol
365
- : context.providerProtocol;
366
- const routeHint = typeof context.metadata.routeHint === 'string'
367
- ? context.metadata.routeHint
368
- : undefined;
369
- return inferSseProtocol(routeHint, providerProtocol, context.entryEndpoint);
362
+ const explicitProtocol = resolveSseProtocolFromMetadata(context.metadata);
363
+ if (explicitProtocol) {
364
+ return explicitProtocol;
365
+ }
366
+ return context.providerProtocol;
370
367
  }
371
368
  extractModelHint(metadata) {
372
369
  if (typeof metadata.model === 'string' && metadata.model.trim()) {
@@ -395,26 +392,34 @@ function normalizeEndpoint(endpoint) {
395
392
  const normalized = trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
396
393
  return normalized.replace(/\/{2,}/g, '/');
397
394
  }
398
- function inferProviderProtocol(endpoint) {
399
- const lowered = endpoint.toLowerCase();
400
- if (lowered.includes('/v1/messages'))
401
- return 'anthropic-messages';
402
- if (lowered.includes('/v1/responses'))
403
- return 'openai-responses';
404
- if (lowered.includes('/v1beta'))
405
- return 'gemini-chat';
406
- return 'openai-chat';
395
+ function resolveProviderProtocol(value) {
396
+ if (typeof value !== 'string' || !value.trim()) {
397
+ return 'openai-chat';
398
+ }
399
+ const normalized = value.trim().toLowerCase();
400
+ const mapped = PROVIDER_PROTOCOL_ALIASES[normalized];
401
+ if (mapped) {
402
+ return mapped;
403
+ }
404
+ throw new Error(`[HubPipeline] Unsupported providerProtocol "${value}". Configure a valid protocol (openai-chat|openai-responses|anthropic-messages|gemini-chat).`);
407
405
  }
408
- function inferSseProtocol(routeHint, providerProtocol, entryEndpoint) {
409
- const candidates = [routeHint, providerProtocol, entryEndpoint].filter((value) => typeof value === 'string' && value.length > 0);
410
- for (const candidate of candidates) {
411
- const lowered = candidate.toLowerCase();
412
- if (lowered.includes('responses')) {
413
- return 'openai-responses';
414
- }
415
- if (lowered.includes('messages') || lowered.includes('anthropic')) {
416
- return 'anthropic-messages';
417
- }
406
+ const PROVIDER_PROTOCOL_ALIASES = {
407
+ 'openai-chat': 'openai-chat',
408
+ openai: 'openai-chat',
409
+ chat: 'openai-chat',
410
+ 'responses': 'openai-responses',
411
+ 'openai-responses': 'openai-responses',
412
+ 'anthropic': 'anthropic-messages',
413
+ 'anthropic-messages': 'anthropic-messages',
414
+ 'messages': 'anthropic-messages',
415
+ 'gemini': 'gemini-chat',
416
+ 'google-gemini': 'gemini-chat',
417
+ 'gemini-chat': 'gemini-chat'
418
+ };
419
+ function resolveSseProtocolFromMetadata(metadata) {
420
+ const candidate = metadata.sseProtocol ?? metadata.clientSseProtocol ?? metadata.routeSseProtocol;
421
+ if (typeof candidate !== 'string' || !candidate.trim()) {
422
+ return undefined;
418
423
  }
419
- return 'openai-chat';
424
+ return resolveProviderProtocol(candidate);
420
425
  }
@@ -89,7 +89,7 @@ async function maybeCreateSseStream(protocol, payload, requestId) {
89
89
  return codec.convertJsonToSse(payload, { requestId });
90
90
  }
91
91
  function supportsSseProtocol(protocol) {
92
- return protocol === 'openai-chat' || protocol === 'openai-responses' || protocol === 'anthropic-messages';
92
+ return protocol === 'openai-chat' || protocol === 'openai-responses' || protocol === 'anthropic-messages' || protocol === 'gemini-chat';
93
93
  }
94
94
  function extractDisplayModel(context) {
95
95
  const candidates = [
@@ -1,5 +1,5 @@
1
1
  import { buildOpenAIChatFromGeminiResponse } from '../../codecs/gemini-openai-codec.js';
2
- import { buildChatResponseFromResponses } from '../../responses/responses-openai-bridge.js';
2
+ import { buildChatResponseFromResponses } from '../../shared/responses-response-utils.js';
3
3
  import { buildOpenAIChatFromAnthropicMessage } from './response-runtime.js';
4
4
  export class OpenAIChatResponseMapper {
5
5
  toChatCompletion(format, _ctx) {
@@ -1,3 +1,7 @@
1
+ import { extractToolCallsFromReasoningText } from '../../shared/reasoning-tool-parser.js';
2
+ import { deriveToolCallKey } from '../../shared/tool-call-utils.js';
3
+ import { createBridgeActionState, runBridgeActionPipeline } from '../../shared/bridge-actions.js';
4
+ import { resolveBridgePolicy, resolvePolicyActions } from '../../shared/bridge-policies.js';
1
5
  function flattenAnthropicContent(content) {
2
6
  if (typeof content === 'string')
3
7
  return content;
@@ -19,6 +23,11 @@ export function buildOpenAIChatFromAnthropicMessage(payload) {
19
23
  const content = Array.isArray(payload?.content) ? payload.content : [];
20
24
  const textParts = [];
21
25
  const toolCalls = [];
26
+ const inferredToolCalls = [];
27
+ const reasoningParts = [];
28
+ if (typeof payload?.reasoning_content === 'string' && payload.reasoning_content.trim().length) {
29
+ reasoningParts.push(String(payload.reasoning_content).trim());
30
+ }
22
31
  for (const part of content) {
23
32
  if (!part || typeof part !== 'object')
24
33
  continue;
@@ -50,6 +59,21 @@ export function buildOpenAIChatFromAnthropicMessage(payload) {
50
59
  toolCalls.push({ id, name, args });
51
60
  }
52
61
  }
62
+ else if (kind === 'thinking' || kind === 'reasoning') {
63
+ const text = typeof part.text === 'string'
64
+ ? part.text
65
+ : flattenAnthropicContent(part);
66
+ if (text) {
67
+ const { cleanedText, toolCalls: inferred } = extractToolCallsFromReasoningText(text, { idPrefix: 'anthropic_reasoning' });
68
+ const trimmed = cleanedText.trim();
69
+ if (trimmed.length) {
70
+ reasoningParts.push(trimmed);
71
+ }
72
+ if (Array.isArray(inferred) && inferred.length) {
73
+ inferredToolCalls.push(...inferred);
74
+ }
75
+ }
76
+ }
53
77
  }
54
78
  const mapFinishReason = (reason) => {
55
79
  switch (reason) {
@@ -59,6 +83,60 @@ export function buildOpenAIChatFromAnthropicMessage(payload) {
59
83
  default: return 'stop';
60
84
  }
61
85
  };
86
+ const canonicalToolCalls = toolCalls.map((tc) => ({
87
+ id: tc.id,
88
+ type: 'function',
89
+ function: { name: tc.name, arguments: tc.args }
90
+ }));
91
+ if (inferredToolCalls.length) {
92
+ const seen = new Set();
93
+ for (const existing of canonicalToolCalls) {
94
+ const key = deriveToolCallKey(existing);
95
+ if (key)
96
+ seen.add(key);
97
+ }
98
+ for (const inferred of inferredToolCalls) {
99
+ const key = deriveToolCallKey(inferred);
100
+ if (key && seen.has(key))
101
+ continue;
102
+ canonicalToolCalls.push(inferred);
103
+ if (key)
104
+ seen.add(key);
105
+ }
106
+ }
107
+ const message = {
108
+ role: typeof payload.role === 'string' ? payload.role : 'assistant',
109
+ content: textParts.join('\n')
110
+ };
111
+ if (canonicalToolCalls.length) {
112
+ message.tool_calls = canonicalToolCalls;
113
+ }
114
+ if (reasoningParts.length) {
115
+ message.reasoning_content = reasoningParts.join('\n');
116
+ }
117
+ try {
118
+ const bridgePolicy = resolveBridgePolicy({ protocol: 'anthropic-messages' });
119
+ const actions = resolvePolicyActions(bridgePolicy, 'response_inbound');
120
+ if (actions?.length) {
121
+ const actionState = createBridgeActionState({
122
+ messages: [message],
123
+ rawResponse: payload
124
+ });
125
+ runBridgeActionPipeline({
126
+ stage: 'response_inbound',
127
+ actions,
128
+ protocol: bridgePolicy?.protocol ?? 'anthropic-messages',
129
+ moduleType: bridgePolicy?.moduleType ?? 'anthropic-messages',
130
+ requestId: typeof payload.id === 'string' ? payload.id : undefined,
131
+ state: actionState
132
+ });
133
+ }
134
+ }
135
+ catch {
136
+ // ignore policy failures
137
+ }
138
+ const stopReason = typeof payload['stop_reason'] === 'string' ? payload['stop_reason'] : undefined;
139
+ const finishReason = canonicalToolCalls.length ? 'tool_calls' : mapFinishReason(stopReason);
62
140
  return {
63
141
  id: typeof payload.id === 'string' ? payload.id : `chatcmpl_${Date.now()}`,
64
142
  object: 'chat.completion',
@@ -67,16 +145,8 @@ export function buildOpenAIChatFromAnthropicMessage(payload) {
67
145
  choices: [
68
146
  {
69
147
  index: 0,
70
- finish_reason: mapFinishReason(typeof payload['stop_reason'] === 'string' ? payload['stop_reason'] : undefined),
71
- message: {
72
- role: typeof payload.role === 'string' ? payload.role : 'assistant',
73
- content: textParts.join('\n'),
74
- tool_calls: toolCalls.map((tc) => ({
75
- id: tc.id,
76
- type: 'function',
77
- function: { name: tc.name, arguments: tc.args }
78
- }))
79
- }
148
+ finish_reason: finishReason,
149
+ message
80
150
  }
81
151
  ],
82
152
  usage: payload['usage'] && typeof payload['usage'] === 'object'
@@ -87,9 +157,38 @@ export function buildOpenAIChatFromAnthropicMessage(payload) {
87
157
  export function buildAnthropicResponseFromChat(chatResponse) {
88
158
  const choice = Array.isArray(chatResponse?.choices) ? chatResponse.choices[0] : undefined;
89
159
  const message = choice && typeof choice === 'object' ? choice.message : undefined;
160
+ if (message) {
161
+ try {
162
+ const bridgePolicy = resolveBridgePolicy({ protocol: 'anthropic-messages' });
163
+ const actions = resolvePolicyActions(bridgePolicy, 'response_outbound');
164
+ if (actions?.length) {
165
+ const actionState = createBridgeActionState({
166
+ messages: [message]
167
+ });
168
+ runBridgeActionPipeline({
169
+ stage: 'response_outbound',
170
+ actions,
171
+ protocol: bridgePolicy?.protocol ?? 'anthropic-messages',
172
+ moduleType: bridgePolicy?.moduleType ?? 'anthropic-messages',
173
+ requestId: typeof chatResponse.id === 'string' ? chatResponse.id : undefined,
174
+ state: actionState
175
+ });
176
+ }
177
+ }
178
+ catch {
179
+ // ignore policy failures
180
+ }
181
+ }
90
182
  const text = flattenAnthropicContent(message?.content);
91
183
  const toolCalls = Array.isArray(message?.tool_calls) ? message.tool_calls : [];
92
184
  const contentBlocks = [];
185
+ const reasoningRaw = message?.reasoning_content ?? message?.reasoning;
186
+ const reasoningText = typeof reasoningRaw === 'string'
187
+ ? reasoningRaw.trim()
188
+ : flattenAnthropicContent(reasoningRaw).trim();
189
+ if (reasoningText) {
190
+ contentBlocks.push({ type: 'thinking', text: reasoningText });
191
+ }
93
192
  if (text && text.trim().length) {
94
193
  contentBlocks.push({ type: 'text', text });
95
194
  }