@jsonstudio/llms 0.6.938 → 0.6.1164

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 (131) hide show
  1. package/dist/conversion/hub/operation-table/operation-table-runner.d.ts +18 -0
  2. package/dist/conversion/hub/operation-table/operation-table-runner.js +158 -0
  3. package/dist/conversion/hub/operation-table/semantic-mappers/anthropic-mapper.d.ts +8 -0
  4. package/dist/conversion/hub/operation-table/semantic-mappers/anthropic-mapper.js +303 -0
  5. package/dist/conversion/hub/operation-table/semantic-mappers/chat-mapper.d.ts +8 -0
  6. package/dist/conversion/hub/operation-table/semantic-mappers/chat-mapper.js +413 -0
  7. package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.d.ts +7 -0
  8. package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.js +841 -0
  9. package/dist/conversion/hub/operation-table/semantic-mappers/responses-mapper.d.ts +21 -0
  10. package/dist/conversion/hub/operation-table/semantic-mappers/responses-mapper.js +535 -0
  11. package/dist/conversion/hub/ops/operations.d.ts +19 -0
  12. package/dist/conversion/hub/ops/operations.js +126 -0
  13. package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +9 -0
  14. package/dist/conversion/hub/pipeline/hub-pipeline.js +533 -24
  15. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.js +6 -0
  16. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js +6 -3
  17. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage1_semantic_map/index.js +11 -0
  18. package/dist/conversion/hub/policy/policy-engine.js +41 -9
  19. package/dist/conversion/hub/policy/protocol-spec.d.ts +25 -0
  20. package/dist/conversion/hub/policy/protocol-spec.js +73 -23
  21. package/dist/conversion/hub/process/chat-process.js +252 -41
  22. package/dist/conversion/hub/response/provider-response.js +175 -2
  23. package/dist/conversion/hub/response/response-runtime.js +1 -1
  24. package/dist/conversion/hub/semantic-mappers/anthropic-mapper.d.ts +1 -8
  25. package/dist/conversion/hub/semantic-mappers/anthropic-mapper.js +1 -365
  26. package/dist/conversion/hub/semantic-mappers/chat-mapper.d.ts +1 -8
  27. package/dist/conversion/hub/semantic-mappers/chat-mapper.js +1 -436
  28. package/dist/conversion/hub/semantic-mappers/gemini-mapper.d.ts +1 -7
  29. package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +1 -894
  30. package/dist/conversion/hub/semantic-mappers/responses-mapper.d.ts +1 -21
  31. package/dist/conversion/hub/semantic-mappers/responses-mapper.js +1 -593
  32. package/dist/conversion/hub/tool-surface/tool-surface-engine.d.ts +18 -0
  33. package/dist/conversion/hub/tool-surface/tool-surface-engine.js +571 -0
  34. package/dist/conversion/responses/responses-openai-bridge.js +14 -2
  35. package/dist/conversion/shared/bridge-message-utils.js +2 -8
  36. package/dist/conversion/shared/bridge-policies.js +5 -105
  37. package/dist/conversion/shared/gemini-tool-utils.js +121 -4
  38. package/dist/conversion/shared/protocol-field-allowlists.d.ts +7 -0
  39. package/dist/conversion/shared/protocol-field-allowlists.js +145 -0
  40. package/dist/conversion/shared/reasoning-tool-normalizer.js +4 -2
  41. package/dist/conversion/shared/snapshot-hooks.js +166 -3
  42. package/dist/conversion/shared/text-markup-normalizer.d.ts +2 -0
  43. package/dist/conversion/shared/text-markup-normalizer.js +345 -9
  44. package/dist/conversion/shared/thought-signature-validator.d.ts +52 -0
  45. package/dist/conversion/shared/thought-signature-validator.js +170 -0
  46. package/dist/conversion/shared/tool-argument-repairer.d.ts +39 -0
  47. package/dist/conversion/shared/tool-argument-repairer.js +56 -0
  48. package/dist/conversion/shared/tool-call-id-manager.d.ts +113 -0
  49. package/dist/conversion/shared/tool-call-id-manager.js +231 -0
  50. package/dist/conversion/shared/tool-canonicalizer.js +2 -11
  51. package/dist/router/virtual-router/bootstrap.js +54 -5
  52. package/dist/router/virtual-router/engine-selection.js +132 -42
  53. package/dist/router/virtual-router/engine.d.ts +3 -0
  54. package/dist/router/virtual-router/engine.js +142 -33
  55. package/dist/router/virtual-router/health-weighted.d.ts +25 -0
  56. package/dist/router/virtual-router/health-weighted.js +63 -0
  57. package/dist/router/virtual-router/load-balancer.d.ts +2 -0
  58. package/dist/router/virtual-router/load-balancer.js +45 -16
  59. package/dist/router/virtual-router/routing-instructions.js +17 -1
  60. package/dist/router/virtual-router/sticky-session-store.js +136 -24
  61. package/dist/router/virtual-router/stop-message-file-resolver.d.ts +1 -0
  62. package/dist/router/virtual-router/stop-message-file-resolver.js +74 -0
  63. package/dist/router/virtual-router/stop-message-state-sync.d.ts +15 -0
  64. package/dist/router/virtual-router/stop-message-state-sync.js +57 -0
  65. package/dist/router/virtual-router/types.d.ts +70 -0
  66. package/dist/servertool/clock/config.d.ts +7 -0
  67. package/dist/servertool/clock/config.js +27 -0
  68. package/dist/servertool/clock/daemon.d.ts +3 -0
  69. package/dist/servertool/clock/daemon.js +79 -0
  70. package/dist/servertool/clock/io.d.ts +2 -0
  71. package/dist/servertool/clock/io.js +13 -0
  72. package/dist/servertool/clock/paths.d.ts +4 -0
  73. package/dist/servertool/clock/paths.js +25 -0
  74. package/dist/servertool/clock/session-store.d.ts +3 -0
  75. package/dist/servertool/clock/session-store.js +56 -0
  76. package/dist/servertool/clock/state.d.ts +5 -0
  77. package/dist/servertool/clock/state.js +62 -0
  78. package/dist/servertool/clock/task-store.d.ts +5 -0
  79. package/dist/servertool/clock/task-store.js +4 -0
  80. package/dist/servertool/clock/tasks.d.ts +17 -0
  81. package/dist/servertool/clock/tasks.js +221 -0
  82. package/dist/servertool/clock/types.d.ts +36 -0
  83. package/dist/servertool/clock/types.js +1 -0
  84. package/dist/servertool/engine.d.ts +2 -0
  85. package/dist/servertool/engine.js +164 -8
  86. package/dist/servertool/followup-shadow.d.ts +16 -0
  87. package/dist/servertool/followup-shadow.js +145 -0
  88. package/dist/servertool/handlers/apply-patch-guard.js +1 -265
  89. package/dist/servertool/handlers/clock-auto.d.ts +1 -0
  90. package/dist/servertool/handlers/clock-auto.js +160 -0
  91. package/dist/servertool/handlers/clock.d.ts +1 -0
  92. package/dist/servertool/handlers/clock.js +197 -0
  93. package/dist/servertool/handlers/exec-command-guard.js +7 -555
  94. package/dist/servertool/handlers/followup-request-builder.d.ts +15 -7
  95. package/dist/servertool/handlers/followup-request-builder.js +248 -28
  96. package/dist/servertool/handlers/gemini-empty-reply-continue.js +62 -169
  97. package/dist/servertool/handlers/iflow-model-error-retry.js +18 -28
  98. package/dist/servertool/handlers/recursive-detection-guard.d.ts +1 -0
  99. package/dist/servertool/handlers/recursive-detection-guard.js +333 -0
  100. package/dist/servertool/handlers/stop-message-auto.js +47 -175
  101. package/dist/servertool/handlers/vision.d.ts +7 -1
  102. package/dist/servertool/handlers/vision.js +61 -117
  103. package/dist/servertool/handlers/web-search.d.ts +7 -1
  104. package/dist/servertool/handlers/web-search.js +122 -105
  105. package/dist/servertool/reenter-backend.d.ts +23 -0
  106. package/dist/servertool/reenter-backend.js +18 -0
  107. package/dist/servertool/server-side-tools.d.ts +3 -2
  108. package/dist/servertool/server-side-tools.js +64 -10
  109. package/dist/servertool/types.d.ts +92 -3
  110. package/dist/sse/json-to-sse/event-generators/responses.js +3 -21
  111. package/dist/sse/shared/serializers/responses-event-serializer.d.ts +8 -0
  112. package/dist/sse/shared/serializers/responses-event-serializer.js +19 -0
  113. package/dist/sse/shared/writer.js +24 -7
  114. package/dist/tools/apply-patch/execution-capturer.js +3 -1
  115. package/dist/tools/apply-patch/json/parse-loose.d.ts +3 -0
  116. package/dist/tools/apply-patch/json/parse-loose.js +139 -0
  117. package/dist/tools/apply-patch/patch-text/context-diff.d.ts +1 -0
  118. package/dist/tools/apply-patch/patch-text/context-diff.js +173 -0
  119. package/dist/tools/apply-patch/patch-text/git-diff.d.ts +1 -0
  120. package/dist/tools/apply-patch/patch-text/git-diff.js +138 -0
  121. package/dist/tools/apply-patch/patch-text/looks-like-patch.d.ts +1 -0
  122. package/dist/tools/apply-patch/patch-text/looks-like-patch.js +13 -0
  123. package/dist/tools/apply-patch/patch-text/normalize.d.ts +3 -0
  124. package/dist/tools/apply-patch/patch-text/normalize.js +262 -0
  125. package/dist/tools/apply-patch/structured/coercion.d.ts +3 -0
  126. package/dist/tools/apply-patch/structured/coercion.js +82 -0
  127. package/dist/tools/apply-patch/validation/shared.d.ts +3 -0
  128. package/dist/tools/apply-patch/validation/shared.js +6 -0
  129. package/dist/tools/apply-patch/validator.d.ts +2 -2
  130. package/dist/tools/apply-patch/validator.js +6 -556
  131. package/package.json +1 -1
@@ -1,105 +1,5 @@
1
1
  import { RESPONSES_INSTRUCTIONS_REASONING_FIELD } from './reasoning-normalizer.js';
2
- const OPENAI_CHAT_ALLOWED_FIELDS = [
3
- 'messages',
4
- 'tools',
5
- 'tool_outputs',
6
- 'model',
7
- 'temperature',
8
- 'top_p',
9
- 'top_k',
10
- 'max_tokens',
11
- 'frequency_penalty',
12
- 'presence_penalty',
13
- 'logit_bias',
14
- 'response_format',
15
- 'parallel_tool_calls',
16
- 'tool_choice',
17
- 'seed',
18
- 'user',
19
- 'metadata',
20
- 'stop',
21
- 'stop_sequences',
22
- 'stream'
23
- ];
24
- const ANTHROPIC_ALLOWED_FIELDS = [
25
- 'model',
26
- 'messages',
27
- 'tools',
28
- 'system',
29
- 'stop_sequences',
30
- 'temperature',
31
- 'top_p',
32
- 'top_k',
33
- 'max_tokens',
34
- 'max_output_tokens',
35
- 'metadata',
36
- 'stream',
37
- 'tool_choice'
38
- ];
39
- const RESPONSES_ALLOWED_FIELDS = [
40
- 'id',
41
- 'object',
42
- 'created_at',
43
- 'model',
44
- 'status',
45
- 'input',
46
- 'instructions',
47
- 'output',
48
- 'output_text',
49
- 'required_action',
50
- 'response_id',
51
- 'previous_response_id',
52
- 'tool_outputs',
53
- 'tools',
54
- 'metadata',
55
- 'include',
56
- 'store',
57
- 'user',
58
- 'response_format',
59
- 'temperature',
60
- 'top_p',
61
- 'top_k',
62
- 'max_tokens',
63
- 'max_output_tokens',
64
- 'logit_bias',
65
- 'seed',
66
- 'parallel_tool_calls',
67
- 'tool_choice',
68
- 'stream',
69
- 'instructions_is_raw'
70
- ];
71
- const GEMINI_ALLOWED_FIELDS = [
72
- 'model',
73
- 'contents',
74
- 'systemInstruction',
75
- 'system_instruction',
76
- 'generationConfig',
77
- 'generation_config',
78
- 'safetySettings',
79
- 'safety_settings',
80
- 'metadata',
81
- 'toolConfig',
82
- 'tool_config',
83
- 'tools',
84
- 'tool_choice',
85
- 'parallelToolCalls',
86
- 'parallel_tool_calls',
87
- 'responseMimeType',
88
- 'response_mime_type',
89
- 'stopSequences',
90
- 'stop_sequences',
91
- 'cachedContent',
92
- 'prompt',
93
- 'response',
94
- 'candidates',
95
- 'usageMetadata',
96
- 'responseMetadata',
97
- 'promptFeedback',
98
- 'modelVersion',
99
- 'client',
100
- 'user',
101
- 'stream'
102
- ];
2
+ import { ANTHROPIC_ALLOWED_FIELDS, GEMINI_ALLOWED_FIELDS, OPENAI_CHAT_ALLOWED_FIELDS, OPENAI_RESPONSES_ALLOWED_FIELDS } from './protocol-field-allowlists.js';
103
3
  function reasoningAction(idPrefix) {
104
4
  return {
105
5
  name: 'reasoning.extract',
@@ -134,7 +34,7 @@ const RESPONSES_POLICY = {
134
34
  reasoningAction('responses_reasoning'),
135
35
  toolCallNormalizationAction('responses_tool_call'),
136
36
  { name: 'tools.ensure-placeholders' },
137
- { name: 'metadata.extra-fields', options: { allowedKeys: RESPONSES_ALLOWED_FIELDS } },
37
+ { name: 'metadata.extra-fields', options: { allowedKeys: OPENAI_RESPONSES_ALLOWED_FIELDS } },
138
38
  { name: 'metadata.provider-field', options: { field: 'metadata', target: 'providerMetadata' } },
139
39
  { name: 'metadata.provider-sentinel', options: { sentinel: '__rcc_provider_metadata', target: 'providerMetadata' } }
140
40
  ],
@@ -146,7 +46,7 @@ const RESPONSES_POLICY = {
146
46
  { name: 'messages.ensure-output-fields', options: { toolFallback: 'Tool call completed (no output).' } },
147
47
  { name: 'messages.ensure-system-instruction' },
148
48
  reasoningAction('responses_reasoning'),
149
- { name: 'metadata.extra-fields', options: { allowedKeys: RESPONSES_ALLOWED_FIELDS } },
49
+ { name: 'metadata.extra-fields', options: { allowedKeys: OPENAI_RESPONSES_ALLOWED_FIELDS } },
150
50
  { name: 'metadata.provider-field', options: { field: 'metadata', target: 'providerMetadata' } },
151
51
  { name: 'metadata.provider-sentinel', options: { sentinel: '__rcc_provider_metadata', target: 'providerMetadata' } }
152
52
  ]
@@ -156,12 +56,12 @@ const RESPONSES_POLICY = {
156
56
  { name: 'reasoning.attach-output' },
157
57
  reasoningAction('responses_reasoning'),
158
58
  toolCallNormalizationAction('responses_tool_call'),
159
- { name: 'metadata.extra-fields', options: { allowedKeys: RESPONSES_ALLOWED_FIELDS } }
59
+ { name: 'metadata.extra-fields', options: { allowedKeys: OPENAI_RESPONSES_ALLOWED_FIELDS } }
160
60
  ],
161
61
  outbound: [
162
62
  reasoningAction('responses_reasoning'),
163
63
  toolCallNormalizationAction('responses_tool_call'),
164
- { name: 'metadata.extra-fields', options: { allowedKeys: RESPONSES_ALLOWED_FIELDS } }
64
+ { name: 'metadata.extra-fields', options: { allowedKeys: OPENAI_RESPONSES_ALLOWED_FIELDS } }
165
65
  ]
166
66
  }
167
67
  };
@@ -73,8 +73,8 @@ function cloneParameters(value) {
73
73
  continue;
74
74
  if (key === 'exclusiveMinimum' || key === 'exclusiveMaximum' || key === 'propertyNames')
75
75
  continue;
76
- // Keep Gemini tool schemas permissive to avoid upstream MALFORMED_FUNCTION_CALL on strict validation.
77
- // Validation is enforced by llmswitch-core / servertool layers instead.
76
+ // Keep Gemini tool schemas mostly permissive to avoid upstream MALFORMED_FUNCTION_CALL on strict validation.
77
+ // We selectively re-introduce safe required fields for critical tools in `buildGeminiToolsFromBridge`.
78
78
  if (key === 'required' || key === 'additionalProperties')
79
79
  continue;
80
80
  // Combinators are handled at the node level above.
@@ -166,6 +166,123 @@ export function buildGeminiToolsFromBridge(defs) {
166
166
  return undefined;
167
167
  }
168
168
  const tools = [];
169
+ const applyFixups = (name, parameters) => {
170
+ if (!parameters || typeof parameters !== 'object' || Array.isArray(parameters)) {
171
+ return parameters;
172
+ }
173
+ const params = parameters;
174
+ const propsRaw = params.properties;
175
+ const props = isPlainRecord(propsRaw) ? propsRaw : {};
176
+ const lowered = String(name || '').trim().toLowerCase();
177
+ if (lowered === 'exec_command') {
178
+ // Keep Gemini tool schema aligned with both historical Codex and our runtime tool signature:
179
+ // - Codex CLI: { cmd, workdir }
180
+ // - Some history: { command, workdir }
181
+ // Strict Gemini validation (and/or tool selection) is sensitive to schema/arg drift.
182
+ if (!Object.prototype.hasOwnProperty.call(props, 'cmd') && Object.prototype.hasOwnProperty.call(props, 'command')) {
183
+ props.cmd = props.command;
184
+ }
185
+ if (!Object.prototype.hasOwnProperty.call(props, 'command') && Object.prototype.hasOwnProperty.call(props, 'cmd')) {
186
+ props.command = props.cmd;
187
+ }
188
+ if (!Object.prototype.hasOwnProperty.call(props, 'cmd')) {
189
+ props.cmd = { type: 'string' };
190
+ }
191
+ if (!Object.prototype.hasOwnProperty.call(props, 'command')) {
192
+ props.command = { type: 'string' };
193
+ }
194
+ if (!Object.prototype.hasOwnProperty.call(props, 'workdir')) {
195
+ props.workdir = { type: 'string' };
196
+ }
197
+ params.properties = props;
198
+ // Avoid hard required keys for Gemini: the model may emit either alias (cmd/command),
199
+ // and "required" mismatch surfaces as MALFORMED_FUNCTION_CALL (empty reply) upstream.
200
+ try {
201
+ delete params.required;
202
+ }
203
+ catch {
204
+ params.required = undefined;
205
+ }
206
+ return params;
207
+ }
208
+ if (lowered === 'write_stdin') {
209
+ // Keep both aliases to avoid strict validation failures when the model echoes history.
210
+ if (!Object.prototype.hasOwnProperty.call(props, 'chars') && Object.prototype.hasOwnProperty.call(props, 'text')) {
211
+ props.chars = props.text;
212
+ }
213
+ if (!Object.prototype.hasOwnProperty.call(props, 'text') && Object.prototype.hasOwnProperty.call(props, 'chars')) {
214
+ props.text = props.chars;
215
+ }
216
+ if (!Object.prototype.hasOwnProperty.call(props, 'session_id')) {
217
+ props.session_id = { type: 'number' };
218
+ }
219
+ if (!Object.prototype.hasOwnProperty.call(props, 'chars')) {
220
+ props.chars = { type: 'string' };
221
+ }
222
+ params.properties = props;
223
+ try {
224
+ delete params.required;
225
+ }
226
+ catch {
227
+ params.required = undefined;
228
+ }
229
+ return params;
230
+ }
231
+ if (lowered === 'apply_patch') {
232
+ // Gemini function calling needs JSON args, but Codex tool docs often describe apply_patch as "freeform".
233
+ // To prevent MALFORMED_FUNCTION_CALL, accept common aliases used by various bridge layers:
234
+ // - patch: canonical patch text
235
+ // - input/instructions/text: historical aliases containing patch text
236
+ if (!Object.prototype.hasOwnProperty.call(props, 'patch')) {
237
+ props.patch = {
238
+ type: 'string',
239
+ description: 'Patch text (*** Begin Patch / *** End Patch or GNU unified diff).'
240
+ };
241
+ }
242
+ if (!Object.prototype.hasOwnProperty.call(props, 'input')) {
243
+ props.input = {
244
+ type: 'string',
245
+ description: 'Alias of patch (patch text). Prefer patch.'
246
+ };
247
+ }
248
+ if (!Object.prototype.hasOwnProperty.call(props, 'instructions')) {
249
+ props.instructions = {
250
+ type: 'string',
251
+ description: 'Alias of patch (patch text). Prefer patch.'
252
+ };
253
+ }
254
+ if (!Object.prototype.hasOwnProperty.call(props, 'text')) {
255
+ props.text = {
256
+ type: 'string',
257
+ description: 'Alias of patch (patch text). Prefer patch.'
258
+ };
259
+ }
260
+ params.properties = props;
261
+ try {
262
+ delete params.required;
263
+ }
264
+ catch {
265
+ params.required = undefined;
266
+ }
267
+ return params;
268
+ }
269
+ return params;
270
+ };
271
+ const rewriteDescription = (name, description) => {
272
+ const lowered = String(name || '').trim().toLowerCase();
273
+ if (lowered === 'apply_patch') {
274
+ return ('Edit files by providing patch text in `patch` (string). ' +
275
+ 'Supports "*** Begin Patch" / "*** End Patch" or GNU unified diff. ' +
276
+ '`input`/`instructions`/`text` are accepted as aliases.');
277
+ }
278
+ if (lowered === 'exec_command') {
279
+ return ('Run a shell command. Provide `cmd` (string) (alias: `command`) and optional `workdir` (string).');
280
+ }
281
+ if (lowered === 'write_stdin') {
282
+ return 'Write to an existing exec session. Provide `session_id` (number) and optional `chars` (string).';
283
+ }
284
+ return description;
285
+ };
169
286
  defs.forEach((def) => {
170
287
  if (!def || typeof def !== 'object') {
171
288
  return;
@@ -184,12 +301,12 @@ export function buildGeminiToolsFromBridge(defs) {
184
301
  : typeof def.description === 'string'
185
302
  ? def.description
186
303
  : undefined;
187
- const parameters = cloneParameters(fnNode?.parameters ?? def.parameters ?? { type: 'object', properties: {} });
304
+ const parameters = applyFixups(name, cloneParameters(fnNode?.parameters ?? def.parameters ?? { type: 'object', properties: {} }));
188
305
  tools.push({
189
306
  functionDeclarations: [
190
307
  {
191
308
  name,
192
- description,
309
+ description: rewriteDescription(name, description),
193
310
  parameters
194
311
  }
195
312
  ]
@@ -0,0 +1,7 @@
1
+ export declare const OPENAI_CHAT_ALLOWED_FIELDS: readonly ["messages", "tools", "tool_outputs", "model", "temperature", "top_p", "top_k", "max_tokens", "frequency_penalty", "presence_penalty", "logit_bias", "response_format", "parallel_tool_calls", "tool_choice", "seed", "user", "metadata", "stop", "stop_sequences", "stream"];
2
+ export declare const ANTHROPIC_ALLOWED_FIELDS: readonly ["model", "messages", "tools", "system", "stop_sequences", "temperature", "top_p", "top_k", "max_tokens", "max_output_tokens", "metadata", "stream", "tool_choice"];
3
+ export declare const OPENAI_RESPONSES_ALLOWED_FIELDS: readonly ["id", "object", "created_at", "model", "status", "input", "instructions", "output", "output_text", "required_action", "response_id", "previous_response_id", "tool_outputs", "tools", "metadata", "include", "store", "user", "response_format", "temperature", "top_p", "top_k", "max_tokens", "max_output_tokens", "logit_bias", "seed", "parallel_tool_calls", "tool_choice", "stream", "instructions_is_raw"];
4
+ export declare const GEMINI_ALLOWED_FIELDS: readonly ["model", "contents", "systemInstruction", "system_instruction", "generationConfig", "generation_config", "safetySettings", "safety_settings", "metadata", "toolConfig", "tool_config", "tools", "tool_choice", "parallelToolCalls", "parallel_tool_calls", "responseMimeType", "response_mime_type", "stopSequences", "stop_sequences", "cachedContent", "prompt", "response", "candidates", "usageMetadata", "responseMetadata", "promptFeedback", "modelVersion", "client", "user", "stream"];
5
+ export declare const OPENAI_RESPONSES_PARAMETERS_WRAPPER_ALLOW_KEYS: readonly ["temperature", "top_p", "max_output_tokens", "seed", "logit_bias", "user", "parallel_tool_calls", "tool_choice", "response_format", "stream", "stop", "stop_sequences", "modalities", "top_k"];
6
+ export declare const OPENAI_CHAT_PARAMETERS_WRAPPER_ALLOW_KEYS: readonly ["temperature", "top_p", "top_k", "max_tokens", "frequency_penalty", "presence_penalty", "logit_bias", "seed", "user", "parallel_tool_calls", "tool_choice", "response_format", "stream", "stop", "stop_sequences"];
7
+ export declare const ANTHROPIC_PARAMETERS_WRAPPER_ALLOW_KEYS: readonly ["stop_sequences", "temperature", "top_p", "top_k", "max_tokens", "max_output_tokens", "metadata", "stream", "tool_choice"];
@@ -0,0 +1,145 @@
1
+ export const OPENAI_CHAT_ALLOWED_FIELDS = [
2
+ 'messages',
3
+ 'tools',
4
+ 'tool_outputs',
5
+ 'model',
6
+ 'temperature',
7
+ 'top_p',
8
+ 'top_k',
9
+ 'max_tokens',
10
+ 'frequency_penalty',
11
+ 'presence_penalty',
12
+ 'logit_bias',
13
+ 'response_format',
14
+ 'parallel_tool_calls',
15
+ 'tool_choice',
16
+ 'seed',
17
+ 'user',
18
+ 'metadata',
19
+ 'stop',
20
+ 'stop_sequences',
21
+ 'stream'
22
+ ];
23
+ export const ANTHROPIC_ALLOWED_FIELDS = [
24
+ 'model',
25
+ 'messages',
26
+ 'tools',
27
+ 'system',
28
+ 'stop_sequences',
29
+ 'temperature',
30
+ 'top_p',
31
+ 'top_k',
32
+ 'max_tokens',
33
+ 'max_output_tokens',
34
+ 'metadata',
35
+ 'stream',
36
+ 'tool_choice'
37
+ ];
38
+ export const OPENAI_RESPONSES_ALLOWED_FIELDS = [
39
+ 'id',
40
+ 'object',
41
+ 'created_at',
42
+ 'model',
43
+ 'status',
44
+ 'input',
45
+ 'instructions',
46
+ 'output',
47
+ 'output_text',
48
+ 'required_action',
49
+ 'response_id',
50
+ 'previous_response_id',
51
+ 'tool_outputs',
52
+ 'tools',
53
+ 'metadata',
54
+ 'include',
55
+ 'store',
56
+ 'user',
57
+ 'response_format',
58
+ 'temperature',
59
+ 'top_p',
60
+ 'top_k',
61
+ 'max_tokens',
62
+ 'max_output_tokens',
63
+ 'logit_bias',
64
+ 'seed',
65
+ 'parallel_tool_calls',
66
+ 'tool_choice',
67
+ 'stream',
68
+ 'instructions_is_raw'
69
+ ];
70
+ export const GEMINI_ALLOWED_FIELDS = [
71
+ 'model',
72
+ 'contents',
73
+ 'systemInstruction',
74
+ 'system_instruction',
75
+ 'generationConfig',
76
+ 'generation_config',
77
+ 'safetySettings',
78
+ 'safety_settings',
79
+ 'metadata',
80
+ 'toolConfig',
81
+ 'tool_config',
82
+ 'tools',
83
+ 'tool_choice',
84
+ 'parallelToolCalls',
85
+ 'parallel_tool_calls',
86
+ 'responseMimeType',
87
+ 'response_mime_type',
88
+ 'stopSequences',
89
+ 'stop_sequences',
90
+ 'cachedContent',
91
+ 'prompt',
92
+ 'response',
93
+ 'candidates',
94
+ 'usageMetadata',
95
+ 'responseMetadata',
96
+ 'promptFeedback',
97
+ 'modelVersion',
98
+ 'client',
99
+ 'user',
100
+ 'stream'
101
+ ];
102
+ export const OPENAI_RESPONSES_PARAMETERS_WRAPPER_ALLOW_KEYS = [
103
+ 'temperature',
104
+ 'top_p',
105
+ 'max_output_tokens',
106
+ 'seed',
107
+ 'logit_bias',
108
+ 'user',
109
+ 'parallel_tool_calls',
110
+ 'tool_choice',
111
+ 'response_format',
112
+ 'stream',
113
+ 'stop',
114
+ 'stop_sequences',
115
+ 'modalities',
116
+ 'top_k'
117
+ ];
118
+ export const OPENAI_CHAT_PARAMETERS_WRAPPER_ALLOW_KEYS = [
119
+ 'temperature',
120
+ 'top_p',
121
+ 'top_k',
122
+ 'max_tokens',
123
+ 'frequency_penalty',
124
+ 'presence_penalty',
125
+ 'logit_bias',
126
+ 'seed',
127
+ 'user',
128
+ 'parallel_tool_calls',
129
+ 'tool_choice',
130
+ 'response_format',
131
+ 'stream',
132
+ 'stop',
133
+ 'stop_sequences'
134
+ ];
135
+ export const ANTHROPIC_PARAMETERS_WRAPPER_ALLOW_KEYS = [
136
+ 'stop_sequences',
137
+ 'temperature',
138
+ 'top_p',
139
+ 'top_k',
140
+ 'max_tokens',
141
+ 'max_output_tokens',
142
+ 'metadata',
143
+ 'stream',
144
+ 'tool_choice'
145
+ ];
@@ -97,11 +97,13 @@ export function normalizeMessageReasoningTools(message, options) {
97
97
  const trimmed = (cleanedText || '').trim();
98
98
  writeReasoningContent(message, trimmed);
99
99
  // Chat 统一行为:如果 message 本身没有正文内容,但存在非空 reasoning_content,
100
- // 则将思考内容提升到正文,前后包裹 [思考] 标记,避免客户端只看到“空回答”。
100
+ // 则将思考内容提升到正文,前后包裹 [思考] 标记,避免客户端只看到"空回答"。
101
101
  const rawContent = message.content;
102
102
  if ((typeof rawContent !== 'string' || rawContent.trim().length === 0) &&
103
103
  trimmed.length > 0) {
104
- message.content = `[思考]\n${trimmed}\n[/思考]`;
104
+ // 检查是否已经包含 [思考] 标签,避免重复嵌套
105
+ const hasThinkingTags = trimmed.includes('[思考]') && trimmed.includes('[/思考]');
106
+ message.content = hasThinkingTags ? trimmed : `[思考]\n${trimmed}\n[/思考]`;
105
107
  }
106
108
  if (!Array.isArray(toolCalls) || toolCalls.length === 0) {
107
109
  return { toolCallsAdded: 0, cleanedReasoning: trimmed };
@@ -2,6 +2,7 @@ import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import os from 'node:os';
4
4
  const DEFAULT_SNAPSHOT_ROOT = path.join(os.homedir(), '.routecodex', 'codex-samples');
5
+ const DEFAULT_ERRORSAMPLES_ROOT = path.join(os.homedir(), '.routecodex', 'errorsamples');
5
6
  const PENDING_PROVIDER_DIR = '__pending__';
6
7
  const POLICY_VIOLATIONS_DIR = '__policy_violations__';
7
8
  function resolveSnapshotRoot() {
@@ -12,6 +13,75 @@ function resolveSnapshotRoot() {
12
13
  }
13
14
  return DEFAULT_SNAPSHOT_ROOT;
14
15
  }
16
+ function resolveErrorsamplesRoot() {
17
+ const envOverride = process.env.ROUTECODEX_ERRORSAMPLES_DIR ||
18
+ process.env.ROUTECODEX_ERROR_SAMPLES_DIR;
19
+ if (envOverride && String(envOverride).trim()) {
20
+ return path.resolve(String(envOverride).trim());
21
+ }
22
+ return DEFAULT_ERRORSAMPLES_ROOT;
23
+ }
24
+ function safeErrorsampleName(name) {
25
+ return String(name || 'sample').replace(/[^\w.-]/g, '_');
26
+ }
27
+ async function cleanupZeroByteJsonFiles(dir) {
28
+ let entries = [];
29
+ try {
30
+ entries = await fs.readdir(dir);
31
+ }
32
+ catch {
33
+ return;
34
+ }
35
+ const candidates = entries.filter((name) => name.endsWith('.json'));
36
+ if (candidates.length === 0) {
37
+ return;
38
+ }
39
+ await Promise.allSettled(candidates.map(async (name) => {
40
+ const full = path.join(dir, name);
41
+ try {
42
+ const st = await fs.stat(full);
43
+ if (!st.isFile() || st.size > 0) {
44
+ return;
45
+ }
46
+ await fs.unlink(full);
47
+ }
48
+ catch {
49
+ // ignore cleanup failures
50
+ }
51
+ }));
52
+ }
53
+ async function writeUniqueErrorsampleFile(dir, baseName, contents) {
54
+ const parsed = path.parse(baseName);
55
+ const ext = parsed.ext || '.json';
56
+ const stem = parsed.name || 'sample';
57
+ const tmpDir = path.join(dir, '_tmp');
58
+ try {
59
+ await fs.mkdir(tmpDir, { recursive: true });
60
+ }
61
+ catch {
62
+ // ignore; fallback to direct writes
63
+ }
64
+ for (let i = 0; i < 32; i += 1) {
65
+ const suffix = `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
66
+ const fileName = `${stem}_${suffix}${ext}`;
67
+ try {
68
+ const dest = path.join(dir, fileName);
69
+ const tmp = path.join(tmpDir, `${fileName}.tmp`);
70
+ await fs.writeFile(tmp, contents, { encoding: 'utf-8', flag: 'wx' });
71
+ await fs.rename(tmp, dest);
72
+ return;
73
+ }
74
+ catch (error) {
75
+ if (toErrorCode(error) === 'EEXIST') {
76
+ continue;
77
+ }
78
+ throw error;
79
+ }
80
+ }
81
+ // last resort (best-effort overwrite into a timestamped file)
82
+ const fallback = `${stem}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}${ext}`;
83
+ await fs.writeFile(path.join(dir, fallback), contents, { encoding: 'utf-8' });
84
+ }
15
85
  function resolveSnapshotFolder(endpoint) {
16
86
  const lowered = (endpoint || '').toLowerCase();
17
87
  if (lowered.includes('/responses')) {
@@ -177,6 +247,9 @@ function readNumberField(value) {
177
247
  function isHubPolicyStage(stage) {
178
248
  return typeof stage === 'string' && stage.startsWith('hub_policy.');
179
249
  }
250
+ function isHubToolSurfaceStage(stage) {
251
+ return typeof stage === 'string' && stage.startsWith('hub_toolsurface.');
252
+ }
180
253
  function hasPolicyViolations(value) {
181
254
  if (!value || typeof value !== 'object') {
182
255
  return false;
@@ -195,6 +268,24 @@ function hasPolicyViolations(value) {
195
268
  }
196
269
  return false;
197
270
  }
271
+ function hasToolSurfaceDiff(value) {
272
+ if (!value || typeof value !== 'object') {
273
+ return false;
274
+ }
275
+ const obj = value;
276
+ const diffCount = readNumberField(obj.diffCount);
277
+ if (typeof diffCount === 'number' && diffCount > 0) {
278
+ return true;
279
+ }
280
+ // Response-side tool surface mismatch detection records expected/detected protocol
281
+ // without a numeric diffCount; treat any protocol mismatch as a diff.
282
+ const expected = readStringField(obj.expectedProtocol);
283
+ const detected = readStringField(obj.detectedProtocol);
284
+ if (expected && detected && expected !== detected) {
285
+ return true;
286
+ }
287
+ return false;
288
+ }
198
289
  function hasPolicyEnforcementChanges(value) {
199
290
  if (!value || typeof value !== 'object') {
200
291
  return false;
@@ -214,11 +305,25 @@ async function writeUniqueFile(dir, baseName, contents) {
214
305
  const parsed = path.parse(baseName);
215
306
  const ext = parsed.ext || '.json';
216
307
  const stem = parsed.name || 'snapshot';
308
+ const tmpPrefix = `.__tmp_${stem}_${process.pid}_${Date.now()}`;
217
309
  for (let i = 0; i < 64; i += 1) {
218
310
  const name = i === 0 ? `${stem}${ext}` : `${stem}_${i}${ext}`;
219
311
  try {
220
- await fs.writeFile(path.join(dir, name), contents, { encoding: 'utf-8', flag: 'wx' });
221
- return;
312
+ const dest = path.join(dir, name);
313
+ const tmp = path.join(dir, `${tmpPrefix}_${Math.random().toString(36).slice(2, 8)}${ext}`);
314
+ await fs.writeFile(tmp, contents, { encoding: 'utf-8', flag: 'wx' });
315
+ try {
316
+ await fs.link(tmp, dest);
317
+ await fs.unlink(tmp).catch(() => { });
318
+ return;
319
+ }
320
+ catch (error) {
321
+ await fs.unlink(tmp).catch(() => { });
322
+ if (toErrorCode(error) === 'EEXIST') {
323
+ continue;
324
+ }
325
+ throw error;
326
+ }
222
327
  }
223
328
  catch (error) {
224
329
  if (toErrorCode(error) === 'EEXIST') {
@@ -228,7 +333,15 @@ async function writeUniqueFile(dir, baseName, contents) {
228
333
  }
229
334
  }
230
335
  const fallback = `${stem}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}${ext}`;
231
- await fs.writeFile(path.join(dir, fallback), contents, 'utf-8');
336
+ const dest = path.join(dir, fallback);
337
+ const tmp = path.join(dir, `${tmpPrefix}_${Math.random().toString(36).slice(2, 8)}${ext}`);
338
+ await fs.writeFile(tmp, contents, { encoding: 'utf-8', flag: 'wx' });
339
+ try {
340
+ await fs.link(tmp, dest);
341
+ }
342
+ finally {
343
+ await fs.unlink(tmp).catch(() => { });
344
+ }
232
345
  }
233
346
  const requestProviderIndex = globalThis
234
347
  .__routecodexSnapshotProviderIndex ||
@@ -313,6 +426,23 @@ async function writeSnapshotFile(options, rootOverride) {
313
426
  }
314
427
  const dir = path.join(root, folder, providerToken, groupRequestToken);
315
428
  await fs.mkdir(dir, { recursive: true });
429
+ // Write a stable runtime marker for this request folder (best-effort).
430
+ try {
431
+ const metaPath = path.join(dir, '__runtime.json');
432
+ const payload = JSON.stringify({
433
+ timestamp: new Date().toISOString(),
434
+ versions: {
435
+ routecodex: process.env.ROUTECODEX_VERSION,
436
+ routecodexBuildTime: process.env.ROUTECODEX_BUILD_TIME,
437
+ llmswitchCore: process.env.ROUTECODEX_LLMSWITCH_CORE_VERSION,
438
+ node: process.version
439
+ }
440
+ }, null, 2);
441
+ await fs.writeFile(metaPath, payload, { encoding: 'utf-8', flag: 'wx' }).catch(() => { });
442
+ }
443
+ catch {
444
+ // ignore
445
+ }
316
446
  const spacing = options.verbosity === 'minimal' ? undefined : 2;
317
447
  const payload = spacing !== undefined
318
448
  ? JSON.stringify(options.data, null, spacing)
@@ -342,4 +472,37 @@ export async function writeSnapshotViaHooks(options) {
342
472
  catch {
343
473
  // never block callers
344
474
  }
475
+ // 3) Tool surface diffs (copy-on-diff only) → ~/.routecodex/errorsamples/tool-surface/
476
+ try {
477
+ if (isHubToolSurfaceStage(stage) && hasToolSurfaceDiff(options.data)) {
478
+ const root = resolveErrorsamplesRoot();
479
+ const dir = path.join(root, safeErrorsampleName('tool-surface'));
480
+ await fs.mkdir(dir, { recursive: true });
481
+ // Best-effort cleanup: when the process exits abruptly, an in-flight async
482
+ // write can leave behind 0-byte placeholder files. Keep the observable
483
+ // directory clean so operators can tail it.
484
+ await cleanupZeroByteJsonFiles(dir);
485
+ const stamp = new Date().toISOString();
486
+ const payload = {
487
+ kind: 'hub_toolsurface_diff',
488
+ timestamp: stamp,
489
+ endpoint: options.endpoint,
490
+ stage,
491
+ requestId: options.requestId,
492
+ providerKey: options.providerKey,
493
+ groupRequestId: options.groupRequestId,
494
+ runtime: {
495
+ routecodexVersion: process.env.ROUTECODEX_VERSION,
496
+ routecodexBuildTime: process.env.ROUTECODEX_BUILD_TIME,
497
+ llmswitchCore: process.env.ROUTECODEX_LLMSWITCH_CORE_VERSION,
498
+ node: process.version
499
+ },
500
+ observation: options.data
501
+ };
502
+ await writeUniqueErrorsampleFile(dir, `${safeErrorsampleName(stage)}.json`, JSON.stringify(payload, null, 2));
503
+ }
504
+ }
505
+ catch {
506
+ // never block callers
507
+ }
345
508
  }