@jsonstudio/llms 0.6.0 → 0.6.2

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.
@@ -29,7 +29,7 @@ async function applyRequestToolGovernance(request, context) {
29
29
  const providerProtocol = readString(metadata.providerProtocol) ?? readString(metadata.provider) ?? 'openai-chat';
30
30
  const metadataToolHints = metadata.toolFilterHints;
31
31
  const metadataStreamFlag = metadata.stream;
32
- const metadataStream = typeof metadataStreamFlag === 'boolean' ? metadataStreamFlag : request.parameters?.stream === true;
32
+ const inboundStreamIntent = typeof metadataStreamFlag === 'boolean' ? metadataStreamFlag : request.parameters?.stream === true;
33
33
  const shaped = {
34
34
  model: request.model,
35
35
  messages: deepClone(request.messages),
@@ -43,10 +43,11 @@ async function applyRequestToolGovernance(request, context) {
43
43
  requestId: context.requestId,
44
44
  model: request.model,
45
45
  profile: providerProtocol,
46
- stream: metadataStream,
46
+ stream: inboundStreamIntent,
47
47
  toolFilterHints: metadataToolHints
48
48
  });
49
49
  const governed = normalizeRecord(governedPayload);
50
+ const providerStreamIntent = typeof governed.stream === 'boolean' ? governed.stream : undefined;
50
51
  const merged = {
51
52
  ...request,
52
53
  messages: Array.isArray(governed.messages)
@@ -60,14 +61,19 @@ async function applyRequestToolGovernance(request, context) {
60
61
  metadata: {
61
62
  ...request.metadata,
62
63
  toolChoice: readToolChoice(governed.tool_choice),
63
- originalStream: request.parameters?.stream === true,
64
- stream: typeof governed.stream === 'boolean'
65
- ? governed.stream
66
- : request.parameters?.stream,
64
+ originalStream: inboundStreamIntent,
65
+ stream: inboundStreamIntent,
66
+ providerStream: providerStreamIntent,
67
67
  governedTools: governed.tools !== undefined,
68
68
  governanceTimestamp: Date.now()
69
69
  }
70
70
  };
71
+ if (typeof inboundStreamIntent === 'boolean') {
72
+ merged.metadata = {
73
+ ...merged.metadata,
74
+ inboundStream: inboundStreamIntent
75
+ };
76
+ }
71
77
  if (typeof governed.stream === 'boolean') {
72
78
  merged.parameters = {
73
79
  ...merged.parameters,
@@ -5,6 +5,7 @@ import { resolveBridgePolicy, resolvePolicyActions } from '../../shared/bridge-p
5
5
  import { encodeMetadataPassthrough, extractMetadataPassthrough } from '../../shared/metadata-passthrough.js';
6
6
  import { buildAnthropicToolAliasMap } from '../../shared/anthropic-message-utils.js';
7
7
  import { ChatSemanticMapper } from './chat-mapper.js';
8
+ import { ensureProtocolState, getProtocolState } from '../../shared/protocol-state.js';
8
9
  const ANTHROPIC_PARAMETER_KEYS = [
9
10
  'model',
10
11
  'temperature',
@@ -53,6 +54,16 @@ function collectParameters(payload) {
53
54
  }
54
55
  return Object.keys(params).length ? params : undefined;
55
56
  }
57
+ function cloneAnthropicSystemBlocks(value) {
58
+ if (value === undefined || value === null) {
59
+ return undefined;
60
+ }
61
+ const blocks = Array.isArray(value) ? value : [value];
62
+ if (!blocks.length) {
63
+ return undefined;
64
+ }
65
+ return blocks.map((entry) => jsonClone(entry));
66
+ }
56
67
  export class AnthropicSemanticMapper {
57
68
  chatMapper = new ChatSemanticMapper();
58
69
  async toChat(format, ctx) {
@@ -86,6 +97,11 @@ export class AnthropicSemanticMapper {
86
97
  }
87
98
  return metadata.extraFields;
88
99
  };
100
+ const protocolState = ensureProtocolState(metadata, 'anthropic');
101
+ const systemBlocks = cloneAnthropicSystemBlocks(payload.system);
102
+ if (systemBlocks) {
103
+ protocolState.systemBlocks = systemBlocks;
104
+ }
89
105
  if (payload.tools && Array.isArray(payload.tools) && payload.tools.length === 0) {
90
106
  metadata.toolsFieldPresent = true;
91
107
  resolveExtraFields().toolsFieldPresent = true;
@@ -227,6 +243,10 @@ export class AnthropicSemanticMapper {
227
243
  if (chat.metadata?.toolsFieldPresent && (!Array.isArray(chat.tools) || chat.tools.length === 0)) {
228
244
  baseRequest.tools = [];
229
245
  }
246
+ const protocolState = getProtocolState(chat.metadata, 'anthropic');
247
+ if (protocolState?.systemBlocks !== undefined) {
248
+ baseRequest.system = jsonClone(protocolState.systemBlocks);
249
+ }
230
250
  if (chat.metadata &&
231
251
  typeof chat.metadata === 'object' &&
232
252
  chat.metadata.extraFields &&
@@ -2,6 +2,7 @@ import { isJsonObject, jsonClone } from '../types/json.js';
2
2
  import { createBridgeActionState, runBridgeActionPipeline } from '../../shared/bridge-actions.js';
3
3
  import { resolveBridgePolicy, resolvePolicyActions } from '../../shared/bridge-policies.js';
4
4
  import { normalizeChatMessageContent } from '../../shared/chat-output-normalizer.js';
5
+ import { ensureProtocolState } from '../../shared/protocol-state.js';
5
6
  const CHAT_PARAMETER_KEYS = [
6
7
  'model',
7
8
  'temperature',
@@ -93,35 +94,12 @@ function collectSystemRawBlocks(raw) {
93
94
  if (!Array.isArray(raw))
94
95
  return undefined;
95
96
  const blocks = [];
96
- const pushText = (text) => {
97
- if (typeof text === 'string' && text.trim().length) {
98
- blocks.push({ type: 'text', text });
99
- }
100
- };
101
97
  raw.forEach((entry) => {
102
98
  if (!isJsonObject(entry))
103
99
  return;
104
- if (entry.role !== 'system')
105
- return;
106
- const content = entry.content;
107
- if (typeof content === 'string') {
108
- pushText(content);
109
- return;
110
- }
111
- if (Array.isArray(content)) {
112
- content.forEach((part) => {
113
- if (typeof part === 'string') {
114
- pushText(part);
115
- }
116
- else if (isJsonObject(part) && typeof part.text === 'string') {
117
- pushText(part.text);
118
- }
119
- });
100
+ if (String(entry.role ?? '').toLowerCase() !== 'system')
120
101
  return;
121
- }
122
- if (isJsonObject(content) && typeof content.text === 'string') {
123
- pushText(content.text);
124
- }
102
+ blocks.push(jsonClone(entry));
125
103
  });
126
104
  return blocks.length ? blocks : undefined;
127
105
  }
@@ -274,8 +252,9 @@ export class ChatSemanticMapper {
274
252
  metadata.systemInstructions = normalized.systemSegments;
275
253
  }
276
254
  const rawSystemBlocks = collectSystemRawBlocks(payload.messages);
277
- if (!metadata.rawSystem && rawSystemBlocks) {
278
- metadata.rawSystem = rawSystemBlocks;
255
+ if (rawSystemBlocks) {
256
+ const protocolState = ensureProtocolState(metadata, 'openai');
257
+ protocolState.systemMessages = jsonClone(rawSystemBlocks);
279
258
  }
280
259
  if (normalized.missingFields.length) {
281
260
  metadata.missingFields = normalized.missingFields;
@@ -5,6 +5,7 @@ import { resolveBridgePolicy, resolvePolicyActions } from '../../shared/bridge-p
5
5
  import { encodeMetadataPassthrough, extractMetadataPassthrough } from '../../shared/metadata-passthrough.js';
6
6
  import { mapBridgeToolsToChat, mapChatToolsToBridge } from '../../shared/tool-mapping.js';
7
7
  import { prepareGeminiToolsForBridge, buildGeminiToolsFromBridge } from '../../shared/gemini-tool-utils.js';
8
+ import { ensureProtocolState, getProtocolState } from '../../shared/protocol-state.js';
8
9
  const GENERATION_CONFIG_KEYS = [
9
10
  { source: 'temperature', target: 'temperature' },
10
11
  { source: 'topP', target: 'top_p' },
@@ -153,11 +154,9 @@ function buildGeminiRequestFromChat(chat, metadata) {
153
154
  model: chat.parameters?.model || 'models/gemini-pro',
154
155
  contents
155
156
  };
156
- if (metadata?.rawSystem && isJsonObject(metadata.rawSystem) && String(metadata.rawSystem.role || '').toLowerCase() === 'system') {
157
- request.systemInstruction = metadata.rawSystem;
158
- }
159
- else if (metadata?.rawSystemInstruction !== undefined) {
160
- request.systemInstruction = metadata.rawSystemInstruction;
157
+ const geminiState = getProtocolState(metadata, 'gemini');
158
+ if (geminiState?.systemInstruction !== undefined) {
159
+ request.systemInstruction = jsonClone(geminiState.systemInstruction);
161
160
  }
162
161
  else if (metadata?.systemInstructions && Array.isArray(metadata.systemInstructions)) {
163
162
  const sysBlocks = metadata.systemInstructions
@@ -276,8 +275,7 @@ export class GeminiSemanticMapper {
276
275
  }
277
276
  if (payload.systemInstruction !== undefined) {
278
277
  const rawSystem = jsonClone(payload.systemInstruction);
279
- metadata.rawSystemInstruction = rawSystem;
280
- metadata.rawSystem = rawSystem;
278
+ ensureProtocolState(metadata, 'gemini').systemInstruction = rawSystem;
281
279
  }
282
280
  if (payload.safetySettings) {
283
281
  metadata.safetySettings = jsonClone(payload.safetySettings);
@@ -320,7 +318,6 @@ export class GeminiSemanticMapper {
320
318
  if (providerMetadataSource) {
321
319
  const providerMetadata = jsonClone(providerMetadataSource);
322
320
  let toolsFieldPresent = false;
323
- let rawSystemValue;
324
321
  if (isJsonObject(providerMetadata)) {
325
322
  delete providerMetadata.__rcc_stream;
326
323
  if (Object.prototype.hasOwnProperty.call(providerMetadata, '__rcc_tools_field_present')) {
@@ -329,24 +326,12 @@ export class GeminiSemanticMapper {
329
326
  delete providerMetadata.__rcc_tools_field_present;
330
327
  }
331
328
  if (Object.prototype.hasOwnProperty.call(providerMetadata, '__rcc_raw_system')) {
332
- const rawSystemSentinel = providerMetadata.__rcc_raw_system;
333
329
  delete providerMetadata.__rcc_raw_system;
334
- if (typeof rawSystemSentinel === 'string') {
335
- try {
336
- rawSystemValue = JSON.parse(rawSystemSentinel);
337
- }
338
- catch {
339
- rawSystemValue = undefined;
340
- }
341
- }
342
330
  }
343
331
  }
344
332
  if (toolsFieldPresent) {
345
333
  metadata.toolsFieldPresent = true;
346
334
  }
347
- if (rawSystemValue !== undefined) {
348
- metadata.rawSystem = rawSystemValue;
349
- }
350
335
  metadata.providerMetadata = providerMetadata;
351
336
  }
352
337
  return {
@@ -1,4 +1,4 @@
1
- import { jsonClone } from './types/json.js';
1
+ import { isJsonObject, jsonClone } from './types/json.js';
2
2
  const HUB_CAPTURE_KEY = '__hub_capture';
3
3
  export function chatEnvelopeToStandardized(chat, options) {
4
4
  const parameters = { ...(chat.parameters ?? {}) };
@@ -16,8 +16,8 @@ export function chatEnvelopeToStandardized(chat, options) {
16
16
  if (isJsonObject(chat.metadata?.extraFields)) {
17
17
  hubState.extraFields = chat.metadata?.extraFields;
18
18
  }
19
- if (chat.metadata?.rawSystem !== undefined) {
20
- hubState.rawSystem = chat.metadata.rawSystem;
19
+ if (isJsonObject(chat.metadata?.protocolState)) {
20
+ hubState.protocolState = chat.metadata?.protocolState;
21
21
  }
22
22
  hubState.context = options.adapterContext;
23
23
  if (hubStateContextPopulated(hubState)) {
@@ -72,8 +72,8 @@ export function standardizedToChatEnvelope(request, options) {
72
72
  if (hubState?.extraFields) {
73
73
  metadata.extraFields = hubState.extraFields;
74
74
  }
75
- if (hubState?.rawSystem !== undefined) {
76
- metadata.rawSystem = hubState.rawSystem;
75
+ if (hubState?.protocolState) {
76
+ metadata.protocolState = hubState.protocolState;
77
77
  }
78
78
  return {
79
79
  messages,
@@ -232,9 +232,6 @@ function normalizeTools(tools) {
232
232
  }
233
233
  return normalized;
234
234
  }
235
- function isJsonObject(value) {
236
- return Boolean(value && typeof value === 'object' && !Array.isArray(value));
237
- }
238
235
  function extractHubCapture(request) {
239
236
  const captured = request.metadata?.capturedContext;
240
237
  if (!captured || typeof captured !== 'object') {
@@ -261,5 +258,5 @@ function hubStateContextPopulated(state) {
261
258
  (state.missingFields && state.missingFields.length) ||
262
259
  state.providerMetadata ||
263
260
  state.extraFields ||
264
- state.rawSystem !== undefined);
261
+ (state.protocolState && Object.keys(state.protocolState).length));
265
262
  }
@@ -161,7 +161,6 @@ export function buildResponsesRequestFromChat(payload, ctx, extras) {
161
161
  'response_format',
162
162
  'user',
163
163
  'top_p',
164
- 'max_output_tokens',
165
164
  'logit_bias',
166
165
  'seed'
167
166
  ];
@@ -169,9 +168,6 @@ export function buildResponsesRequestFromChat(payload, ctx, extras) {
169
168
  if (chat[key] !== undefined)
170
169
  out[key] = chat[key];
171
170
  }
172
- if (typeof chat.max_tokens === 'number' && chat.max_output_tokens === undefined) {
173
- out.max_output_tokens = chat.max_tokens;
174
- }
175
171
  let messages = Array.isArray(chat.messages) ? chat.messages : [];
176
172
  let bridgeMetadata;
177
173
  try {
@@ -32,6 +32,85 @@ function flattenAnthropicText(content) {
32
32
  }
33
33
  return '';
34
34
  }
35
+ function normalizeToolResultContent(block) {
36
+ if (!block || typeof block !== 'object') {
37
+ return '';
38
+ }
39
+ const content = block.content;
40
+ if (typeof content === 'string') {
41
+ return content;
42
+ }
43
+ if (Array.isArray(content)) {
44
+ const segments = [];
45
+ for (const entry of content) {
46
+ const segment = extractToolResultSegment(entry);
47
+ if (segment) {
48
+ segments.push(segment);
49
+ }
50
+ }
51
+ if (segments.length) {
52
+ return segments.join('\n');
53
+ }
54
+ }
55
+ else if (content != null) {
56
+ try {
57
+ return JSON.stringify(content);
58
+ }
59
+ catch {
60
+ return String(content);
61
+ }
62
+ }
63
+ return '';
64
+ }
65
+ function extractToolResultSegment(entry) {
66
+ if (entry == null) {
67
+ return '';
68
+ }
69
+ if (typeof entry === 'string') {
70
+ return entry;
71
+ }
72
+ if (Array.isArray(entry)) {
73
+ return entry.map(extractToolResultSegment).filter(Boolean).join('');
74
+ }
75
+ if (typeof entry === 'object') {
76
+ const node = entry;
77
+ const type = typeof node.type === 'string' ? node.type.toLowerCase() : '';
78
+ if (type === 'input_text' || type === 'input_json' || type === 'tool_result_status' || type === 'status' || type === 'metadata') {
79
+ return '';
80
+ }
81
+ if (type === 'output_text' || type === 'text' || type === 'reasoning' || type === 'log') {
82
+ return flattenAnthropicText(entry);
83
+ }
84
+ if (type === 'output_json' || type === 'json') {
85
+ const payload = node.content ?? node.text ?? node.data ?? node.output;
86
+ if (payload === undefined) {
87
+ return '';
88
+ }
89
+ try {
90
+ return JSON.stringify(payload);
91
+ }
92
+ catch {
93
+ return String(payload ?? '');
94
+ }
95
+ }
96
+ if (typeof node.text === 'string') {
97
+ return node.text;
98
+ }
99
+ if ('content' in node) {
100
+ const nested = extractToolResultSegment(node.content);
101
+ if (nested) {
102
+ return nested;
103
+ }
104
+ }
105
+ try {
106
+ return JSON.stringify(entry);
107
+ }
108
+ catch {
109
+ return '';
110
+ }
111
+ }
112
+ return String(entry);
113
+ }
35
114
  function requireTrimmedString(value, context) {
36
115
  if (typeof value !== 'string') {
37
116
  throw new Error(`Anthropic bridge constraint violated: ${context} must be a string`);
@@ -218,18 +297,7 @@ export function buildOpenAIChatFromAnthropic(payload) {
218
297
  block.call_id ??
219
298
  block.tool_use_id ??
220
299
  block.id, 'tool_result.tool_use_id');
221
- let contentStr = '';
222
- const c = block.content;
223
- if (typeof c === 'string')
224
- contentStr = c;
225
- else if (c != null) {
226
- try {
227
- contentStr = JSON.stringify(c);
228
- }
229
- catch {
230
- contentStr = String(c);
231
- }
232
- }
300
+ const contentStr = normalizeToolResultContent(block);
233
301
  toolResults.push({ role: 'tool', tool_call_id: callId, content: contentStr });
234
302
  }
235
303
  }
@@ -879,58 +879,3 @@ const metadataProviderSentinelAction = (ctx) => {
879
879
  };
880
880
  registerBridgeAction('metadata.provider-field', metadataProviderFieldAction);
881
881
  registerBridgeAction('metadata.provider-sentinel', metadataProviderSentinelAction);
882
- const metadataSystemSentinelAction = (ctx) => {
883
- const sentinel = typeof ctx.descriptor.options?.sentinel === 'string'
884
- ? ctx.descriptor.options.sentinel
885
- : undefined;
886
- const targetKey = typeof ctx.descriptor.options?.target === 'string'
887
- ? ctx.descriptor.options.target
888
- : 'rawSystem';
889
- const stringifyOutbound = ctx.descriptor.options?.stringify !== false;
890
- if (!sentinel)
891
- return;
892
- const payload = ctx.stage.startsWith('request') ? ctx.state.rawRequest :
893
- ctx.stage.startsWith('response') ? ctx.state.rawResponse :
894
- undefined;
895
- if (!payload || typeof payload !== 'object')
896
- return;
897
- if (ctx.stage.endsWith('inbound')) {
898
- const rawValue = payload[sentinel];
899
- if (rawValue === undefined)
900
- return;
901
- let parsed = rawValue;
902
- if (typeof rawValue === 'string') {
903
- try {
904
- parsed = JSON.parse(rawValue);
905
- }
906
- catch {
907
- parsed = rawValue;
908
- }
909
- }
910
- delete payload[sentinel];
911
- const metadata = ensureMetadataRecord(ctx.state);
912
- metadata[targetKey] = parsed;
913
- return;
914
- }
915
- const metadata = ctx.state.metadata;
916
- if (!metadata || typeof metadata !== 'object') {
917
- delete payload[sentinel];
918
- return;
919
- }
920
- const value = metadata[targetKey];
921
- if (value === undefined) {
922
- delete payload[sentinel];
923
- return;
924
- }
925
- if (stringifyOutbound) {
926
- try {
927
- payload[sentinel] = JSON.stringify(value);
928
- }
929
- catch {
930
- payload[sentinel] = value;
931
- }
932
- return;
933
- }
934
- payload[sentinel] = value;
935
- };
936
- registerBridgeAction('metadata.system-sentinel', metadataSystemSentinelAction);
@@ -136,8 +136,7 @@ const RESPONSES_POLICY = {
136
136
  { name: 'tools.ensure-placeholders' },
137
137
  { name: 'metadata.extra-fields', options: { allowedKeys: RESPONSES_ALLOWED_FIELDS } },
138
138
  { name: 'metadata.provider-field', options: { field: 'metadata', target: 'providerMetadata' } },
139
- { name: 'metadata.provider-sentinel', options: { sentinel: '__rcc_provider_metadata', target: 'providerMetadata' } },
140
- { name: 'metadata.system-sentinel', options: { sentinel: '__rcc_raw_system', target: 'rawSystem' } }
139
+ { name: 'metadata.provider-sentinel', options: { sentinel: '__rcc_provider_metadata', target: 'providerMetadata' } }
141
140
  ],
142
141
  outbound: [
143
142
  { name: 'messages.normalize-history' },
@@ -149,8 +148,7 @@ const RESPONSES_POLICY = {
149
148
  reasoningAction('responses_reasoning'),
150
149
  { name: 'metadata.extra-fields', options: { allowedKeys: RESPONSES_ALLOWED_FIELDS } },
151
150
  { name: 'metadata.provider-field', options: { field: 'metadata', target: 'providerMetadata' } },
152
- { name: 'metadata.provider-sentinel', options: { sentinel: '__rcc_provider_metadata', target: 'providerMetadata' } },
153
- { name: 'metadata.system-sentinel', options: { sentinel: '__rcc_raw_system', target: 'rawSystem' } }
151
+ { name: 'metadata.provider-sentinel', options: { sentinel: '__rcc_provider_metadata', target: 'providerMetadata' } }
154
152
  ]
155
153
  },
156
154
  response: {
@@ -158,14 +156,12 @@ const RESPONSES_POLICY = {
158
156
  { name: 'reasoning.attach-output' },
159
157
  reasoningAction('responses_reasoning'),
160
158
  toolCallNormalizationAction('responses_tool_call'),
161
- { name: 'metadata.extra-fields', options: { allowedKeys: RESPONSES_ALLOWED_FIELDS } },
162
- { name: 'metadata.system-sentinel', options: { sentinel: '__rcc_raw_system', target: 'rawSystem' } }
159
+ { name: 'metadata.extra-fields', options: { allowedKeys: RESPONSES_ALLOWED_FIELDS } }
163
160
  ],
164
161
  outbound: [
165
162
  reasoningAction('responses_reasoning'),
166
163
  toolCallNormalizationAction('responses_tool_call'),
167
- { name: 'metadata.extra-fields', options: { allowedKeys: RESPONSES_ALLOWED_FIELDS } },
168
- { name: 'metadata.system-sentinel', options: { sentinel: '__rcc_raw_system', target: 'rawSystem' } }
164
+ { name: 'metadata.extra-fields', options: { allowedKeys: RESPONSES_ALLOWED_FIELDS } }
169
165
  ]
170
166
  }
171
167
  };
@@ -177,7 +173,6 @@ const OPENAI_CHAT_POLICY = {
177
173
  reasoningAction('openai_chat_reasoning'),
178
174
  toolCallNormalizationAction('openai_chat_tool_call'),
179
175
  { name: 'messages.ensure-system-instruction' },
180
- { name: 'metadata.system-sentinel', options: { sentinel: '__rcc_raw_system', target: 'rawSystem' } },
181
176
  { name: 'metadata.extra-fields', options: { allowedKeys: OPENAI_CHAT_ALLOWED_FIELDS } },
182
177
  { name: 'metadata.provider-field', options: { field: 'metadata', target: 'providerMetadata' } },
183
178
  { name: 'metadata.provider-sentinel', options: { sentinel: '__rcc_provider_metadata', target: 'providerMetadata' } }
@@ -190,7 +185,6 @@ const OPENAI_CHAT_POLICY = {
190
185
  { name: 'messages.ensure-output-fields', options: { toolFallback: 'Tool call completed (no output).' } },
191
186
  { name: 'messages.ensure-system-instruction' },
192
187
  reasoningAction('openai_chat_reasoning'),
193
- { name: 'metadata.system-sentinel', options: { sentinel: '__rcc_raw_system', target: 'rawSystem' } },
194
188
  { name: 'metadata.extra-fields', options: { allowedKeys: OPENAI_CHAT_ALLOWED_FIELDS } },
195
189
  { name: 'metadata.provider-field', options: { field: 'metadata', target: 'providerMetadata' } },
196
190
  { name: 'metadata.provider-sentinel', options: { sentinel: '__rcc_provider_metadata', target: 'providerMetadata' } }
@@ -1,4 +1,4 @@
1
- import { FilterEngine, RequestToolCallsStringifyFilter, RequestToolChoicePolicyFilter, RequestStreamingToNonStreamingFilter } from '../../filters/index.js';
1
+ import { FilterEngine, RequestToolCallsStringifyFilter, RequestToolChoicePolicyFilter } from '../../filters/index.js';
2
2
  import { normalizeChatRequest } from '../index.js';
3
3
  import { loadFieldMapConfig } from '../../filters/utils/fieldmap-loader.js';
4
4
  import { createSnapshotWriter } from './snapshot-utils.js';
@@ -66,8 +66,6 @@ export async function runStandardChatRequestFilters(chatRequest, profile, contex
66
66
  }
67
67
  engine.registerFilter(new RequestToolCallsStringifyFilter());
68
68
  engine.registerFilter(new RequestToolChoicePolicyFilter());
69
- // 统一将 Provider 请求转为非流式 Chat;流式语义由上层 SSE 合成处理
70
- engine.registerFilter(new RequestStreamingToNonStreamingFilter());
71
69
  // FieldMap:保持与 Chat 入口一致,使用 openai-openai.fieldmap.json
72
70
  try {
73
71
  const cfg = await loadFieldMapConfig('openai-openai.fieldmap.json');
@@ -0,0 +1,4 @@
1
+ import type { ChatEnvelope } from '../hub/types/chat-envelope.js';
2
+ import { type JsonObject } from '../hub/types/json.js';
3
+ export declare function ensureProtocolState(metadata: ChatEnvelope['metadata'], protocol: string): JsonObject;
4
+ export declare function getProtocolState(metadata: ChatEnvelope['metadata'] | undefined, protocol: string): JsonObject | undefined;
@@ -0,0 +1,23 @@
1
+ import { isJsonObject } from '../hub/types/json.js';
2
+ const PROTOCOL_STATE_KEY = 'protocolState';
3
+ export function ensureProtocolState(metadata, protocol) {
4
+ if (!metadata[PROTOCOL_STATE_KEY] || !isJsonObject(metadata[PROTOCOL_STATE_KEY])) {
5
+ metadata[PROTOCOL_STATE_KEY] = {};
6
+ }
7
+ const container = metadata[PROTOCOL_STATE_KEY];
8
+ if (!isJsonObject(container[protocol])) {
9
+ container[protocol] = {};
10
+ }
11
+ return container[protocol];
12
+ }
13
+ export function getProtocolState(metadata, protocol) {
14
+ if (!metadata) {
15
+ return undefined;
16
+ }
17
+ const store = metadata[PROTOCOL_STATE_KEY];
18
+ if (!isJsonObject(store)) {
19
+ return undefined;
20
+ }
21
+ const node = store[protocol];
22
+ return isJsonObject(node) ? node : undefined;
23
+ }
@@ -35,12 +35,11 @@ export async function runChatRequestToolFilters(chatRequest, options = {}) {
35
35
  /* optional */
36
36
  }
37
37
  }
38
- const { RequestToolCallsStringifyFilter, RequestToolChoicePolicyFilter, RequestStreamingToNonStreamingFilter } = await import('../../filters/index.js');
38
+ const { RequestToolCallsStringifyFilter, RequestToolChoicePolicyFilter } = await import('../../filters/index.js');
39
39
  engine.registerFilter(new RequestToolCallsStringifyFilter());
40
40
  if (!isAnthropic) {
41
41
  engine.registerFilter(new RequestToolChoicePolicyFilter());
42
42
  }
43
- engine.registerFilter(new RequestStreamingToNonStreamingFilter());
44
43
  try {
45
44
  const cfg = await loadFieldMapConfig('openai-openai.fieldmap.json');
46
45
  if (cfg)
@@ -0,0 +1,18 @@
1
+ export * from './types.js';
2
+ export * from './engine.js';
3
+ export * from './builtin/whitelist-filter.js';
4
+ export * from './builtin/blacklist-filter.js';
5
+ export * from './builtin/add-fields-filter.js';
6
+ export * from './special/request-toolcalls-stringify.js';
7
+ export * from './special/request-tool-choice-policy.js';
8
+ export * from './special/request-tool-list-filter.js';
9
+ export * from './special/tool-filter-hooks.js';
10
+ export * from './special/response-tool-text-canonicalize.js';
11
+ export * from './special/response-tool-arguments-stringify.js';
12
+ export * from './special/response-finish-invariants.js';
13
+ export * from './special/request-tools-normalize.js';
14
+ export * from './special/response-tool-arguments-toon-decode.js';
15
+ export * from './special/response-tool-arguments-blacklist.js';
16
+ export * from './special/response-tool-arguments-schema-converge.js';
17
+ export * from './special/response-tool-arguments-whitelist.js';
18
+ export * from './special/tool-post-constraints.js';
@@ -9,7 +9,6 @@ export * from './special/request-toolcalls-stringify.js';
9
9
  export * from './special/request-tool-choice-policy.js';
10
10
  export * from './special/request-tool-list-filter.js';
11
11
  export * from './special/tool-filter-hooks.js';
12
- export * from './special/request-streaming-to-nonstreaming.js';
13
12
  export * from './special/response-tool-text-canonicalize.js';
14
13
  export * from './special/response-tool-arguments-stringify.js';
15
14
  export * from './special/response-finish-invariants.js';
@@ -0,0 +1,13 @@
1
+ import type { Filter, FilterContext, FilterResult, JsonObject } from '../types.js';
2
+ /**
3
+ * RequestStreamingToNonStreamingFilter (request_post)
4
+ * - Unifies provider requests to non-streaming regardless of inbound stream flag.
5
+ * - Preserves original intent via `originalStream` and `_originalStreamOptions`,
6
+ * but ensures `stream: false` is sent to providers.
7
+ */
8
+ export declare class RequestStreamingToNonStreamingFilter implements Filter<JsonObject> {
9
+ readonly name = "request_streaming_to_nonstreaming";
10
+ readonly stage: FilterContext['stage'];
11
+ apply(input: JsonObject, ctx: FilterContext): FilterResult<JsonObject>;
12
+ private shouldPreserveStream;
13
+ }
@@ -7,9 +7,12 @@
7
7
  export class RequestStreamingToNonStreamingFilter {
8
8
  name = 'request_streaming_to_nonstreaming';
9
9
  stage = 'request_post';
10
- apply(input) {
10
+ apply(input, ctx) {
11
11
  try {
12
12
  const out = JSON.parse(JSON.stringify(input || {}));
13
+ if (this.shouldPreserveStream(out, ctx)) {
14
+ return { ok: true, data: out };
15
+ }
13
16
  if (out.stream === true) {
14
17
  out.originalStream = true;
15
18
  out.stream = false;
@@ -24,4 +27,13 @@ export class RequestStreamingToNonStreamingFilter {
24
27
  return { ok: true, data: input };
25
28
  }
26
29
  }
30
+ shouldPreserveStream(payload, ctx) {
31
+ if (ctx.stream === true) {
32
+ return true;
33
+ }
34
+ if (payload.stream === true) {
35
+ return true;
36
+ }
37
+ return false;
38
+ }
27
39
  }
@@ -574,7 +574,30 @@ export class ResponsesResponseBuilder {
574
574
  * 处理required_action事件
575
575
  */
576
576
  handleRequiredAction(event) {
577
- // TODO: 实现required_action处理
577
+ const payload = event.data ?? {};
578
+ const responsePayload = payload.response ?? payload;
579
+ const requiredAction = responsePayload.required_action ??
580
+ payload.required_action ??
581
+ undefined;
582
+ const usage = responsePayload.usage ??
583
+ payload.usage ??
584
+ this.response.usage;
585
+ const nextResponse = {
586
+ ...this.response,
587
+ object: 'response',
588
+ id: responsePayload.id ?? this.response.id,
589
+ status: responsePayload.status ?? 'requires_action',
590
+ output: Array.isArray(responsePayload.output) && responsePayload.output.length
591
+ ? responsePayload.output
592
+ : this.buildOutputItems(),
593
+ required_action: requiredAction ?? this.response.required_action,
594
+ usage
595
+ };
596
+ if (responsePayload.metadata) {
597
+ nextResponse.metadata = responsePayload.metadata;
598
+ }
599
+ this.response = nextResponse;
600
+ this.state = 'completed';
578
601
  }
579
602
  /**
580
603
  * 处理response.done事件
@@ -3,6 +3,7 @@
3
3
  * 支持OpenAI Responses API的JSON↔SSE双向转换
4
4
  */
5
5
  import { BaseSseEvent, StreamDirection, type JsonObject, type JsonValue } from './core-interfaces.js';
6
+ import type { RequiredAction } from './sse-events.js';
6
7
  export * from './sse-events.js';
7
8
  export type ResponsesSseEventType = 'response.created' | 'response.in_progress' | 'response.completed' | 'response.required_action' | 'response.done' | 'response.output_item.added' | 'response.output_item.done' | 'response.content_part.added' | 'response.content_part.done' | 'response.output_text.delta' | 'response.output_text.done' | 'response.reasoning_text.delta' | 'response.reasoning_text.done' | 'response.reasoning_signature.delta' | 'response.reasoning_image.delta' | 'response.reasoning_summary_part.added' | 'response.reasoning_summary_part.done' | 'response.reasoning_summary_text.delta' | 'response.reasoning_summary_text.done' | 'response.function_call_arguments.delta' | 'response.function_call_arguments.done' | 'response.error' | 'response.cancelled' | 'response.start' | 'content_part.delta' | 'reasoning.delta' | 'function_call.start' | 'function_call.delta' | 'function_call.done' | 'output_item.start' | 'content_part.start' | 'content_part.done' | 'output_item.done' | 'reasoning.start' | 'reasoning.done' | 'required_action' | 'error';
8
9
  export interface ResponsesSseEvent extends BaseSseEvent {
@@ -118,6 +119,7 @@ export interface ResponsesResponse {
118
119
  output: ResponsesOutputItem[];
119
120
  previous_response_id?: string;
120
121
  usage?: ResponsesUsage;
122
+ required_action?: RequiredAction;
121
123
  temperature?: number;
122
124
  top_p?: number;
123
125
  max_output_tokens?: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsonstudio/llms",
3
- "version": "0.6.0",
3
+ "version": "0.6.002",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",