@jsonstudio/llms 0.6.954 → 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 (130) 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 +489 -19
  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_outbound/req_outbound_stage1_semantic_map/index.js +11 -0
  17. package/dist/conversion/hub/policy/policy-engine.js +41 -9
  18. package/dist/conversion/hub/policy/protocol-spec.d.ts +25 -0
  19. package/dist/conversion/hub/policy/protocol-spec.js +73 -23
  20. package/dist/conversion/hub/process/chat-process.js +252 -41
  21. package/dist/conversion/hub/response/provider-response.js +175 -2
  22. package/dist/conversion/hub/response/response-runtime.js +1 -1
  23. package/dist/conversion/hub/semantic-mappers/anthropic-mapper.d.ts +1 -8
  24. package/dist/conversion/hub/semantic-mappers/anthropic-mapper.js +1 -365
  25. package/dist/conversion/hub/semantic-mappers/chat-mapper.d.ts +1 -8
  26. package/dist/conversion/hub/semantic-mappers/chat-mapper.js +1 -467
  27. package/dist/conversion/hub/semantic-mappers/gemini-mapper.d.ts +1 -7
  28. package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +1 -903
  29. package/dist/conversion/hub/semantic-mappers/responses-mapper.d.ts +1 -21
  30. package/dist/conversion/hub/semantic-mappers/responses-mapper.js +1 -593
  31. package/dist/conversion/hub/tool-surface/tool-surface-engine.d.ts +18 -0
  32. package/dist/conversion/hub/tool-surface/tool-surface-engine.js +571 -0
  33. package/dist/conversion/responses/responses-openai-bridge.js +14 -2
  34. package/dist/conversion/shared/bridge-message-utils.js +2 -8
  35. package/dist/conversion/shared/bridge-policies.js +5 -105
  36. package/dist/conversion/shared/gemini-tool-utils.js +89 -15
  37. package/dist/conversion/shared/protocol-field-allowlists.d.ts +7 -0
  38. package/dist/conversion/shared/protocol-field-allowlists.js +145 -0
  39. package/dist/conversion/shared/reasoning-tool-normalizer.js +4 -2
  40. package/dist/conversion/shared/snapshot-hooks.js +166 -3
  41. package/dist/conversion/shared/text-markup-normalizer.d.ts +2 -0
  42. package/dist/conversion/shared/text-markup-normalizer.js +345 -9
  43. package/dist/conversion/shared/thought-signature-validator.d.ts +52 -0
  44. package/dist/conversion/shared/thought-signature-validator.js +170 -0
  45. package/dist/conversion/shared/tool-argument-repairer.d.ts +39 -0
  46. package/dist/conversion/shared/tool-argument-repairer.js +56 -0
  47. package/dist/conversion/shared/tool-call-id-manager.d.ts +113 -0
  48. package/dist/conversion/shared/tool-call-id-manager.js +231 -0
  49. package/dist/conversion/shared/tool-canonicalizer.js +2 -11
  50. package/dist/router/virtual-router/bootstrap.js +54 -5
  51. package/dist/router/virtual-router/engine-selection.js +132 -42
  52. package/dist/router/virtual-router/engine.d.ts +3 -0
  53. package/dist/router/virtual-router/engine.js +142 -33
  54. package/dist/router/virtual-router/health-weighted.d.ts +25 -0
  55. package/dist/router/virtual-router/health-weighted.js +63 -0
  56. package/dist/router/virtual-router/load-balancer.d.ts +2 -0
  57. package/dist/router/virtual-router/load-balancer.js +45 -16
  58. package/dist/router/virtual-router/routing-instructions.js +17 -1
  59. package/dist/router/virtual-router/sticky-session-store.js +136 -24
  60. package/dist/router/virtual-router/stop-message-file-resolver.d.ts +1 -0
  61. package/dist/router/virtual-router/stop-message-file-resolver.js +74 -0
  62. package/dist/router/virtual-router/stop-message-state-sync.d.ts +15 -0
  63. package/dist/router/virtual-router/stop-message-state-sync.js +57 -0
  64. package/dist/router/virtual-router/types.d.ts +70 -0
  65. package/dist/servertool/clock/config.d.ts +7 -0
  66. package/dist/servertool/clock/config.js +27 -0
  67. package/dist/servertool/clock/daemon.d.ts +3 -0
  68. package/dist/servertool/clock/daemon.js +79 -0
  69. package/dist/servertool/clock/io.d.ts +2 -0
  70. package/dist/servertool/clock/io.js +13 -0
  71. package/dist/servertool/clock/paths.d.ts +4 -0
  72. package/dist/servertool/clock/paths.js +25 -0
  73. package/dist/servertool/clock/session-store.d.ts +3 -0
  74. package/dist/servertool/clock/session-store.js +56 -0
  75. package/dist/servertool/clock/state.d.ts +5 -0
  76. package/dist/servertool/clock/state.js +62 -0
  77. package/dist/servertool/clock/task-store.d.ts +5 -0
  78. package/dist/servertool/clock/task-store.js +4 -0
  79. package/dist/servertool/clock/tasks.d.ts +17 -0
  80. package/dist/servertool/clock/tasks.js +221 -0
  81. package/dist/servertool/clock/types.d.ts +36 -0
  82. package/dist/servertool/clock/types.js +1 -0
  83. package/dist/servertool/engine.d.ts +2 -0
  84. package/dist/servertool/engine.js +161 -7
  85. package/dist/servertool/followup-shadow.d.ts +16 -0
  86. package/dist/servertool/followup-shadow.js +145 -0
  87. package/dist/servertool/handlers/apply-patch-guard.js +1 -265
  88. package/dist/servertool/handlers/clock-auto.d.ts +1 -0
  89. package/dist/servertool/handlers/clock-auto.js +160 -0
  90. package/dist/servertool/handlers/clock.d.ts +1 -0
  91. package/dist/servertool/handlers/clock.js +197 -0
  92. package/dist/servertool/handlers/exec-command-guard.js +7 -555
  93. package/dist/servertool/handlers/followup-request-builder.d.ts +15 -7
  94. package/dist/servertool/handlers/followup-request-builder.js +248 -28
  95. package/dist/servertool/handlers/gemini-empty-reply-continue.js +62 -169
  96. package/dist/servertool/handlers/iflow-model-error-retry.js +18 -28
  97. package/dist/servertool/handlers/recursive-detection-guard.d.ts +1 -0
  98. package/dist/servertool/handlers/recursive-detection-guard.js +333 -0
  99. package/dist/servertool/handlers/stop-message-auto.js +47 -175
  100. package/dist/servertool/handlers/vision.d.ts +7 -1
  101. package/dist/servertool/handlers/vision.js +61 -117
  102. package/dist/servertool/handlers/web-search.d.ts +7 -1
  103. package/dist/servertool/handlers/web-search.js +122 -105
  104. package/dist/servertool/reenter-backend.d.ts +23 -0
  105. package/dist/servertool/reenter-backend.js +18 -0
  106. package/dist/servertool/server-side-tools.d.ts +3 -2
  107. package/dist/servertool/server-side-tools.js +64 -10
  108. package/dist/servertool/types.d.ts +92 -3
  109. package/dist/sse/json-to-sse/event-generators/responses.js +3 -21
  110. package/dist/sse/shared/serializers/responses-event-serializer.d.ts +8 -0
  111. package/dist/sse/shared/serializers/responses-event-serializer.js +19 -0
  112. package/dist/sse/shared/writer.js +24 -7
  113. package/dist/tools/apply-patch/execution-capturer.js +3 -1
  114. package/dist/tools/apply-patch/json/parse-loose.d.ts +3 -0
  115. package/dist/tools/apply-patch/json/parse-loose.js +139 -0
  116. package/dist/tools/apply-patch/patch-text/context-diff.d.ts +1 -0
  117. package/dist/tools/apply-patch/patch-text/context-diff.js +173 -0
  118. package/dist/tools/apply-patch/patch-text/git-diff.d.ts +1 -0
  119. package/dist/tools/apply-patch/patch-text/git-diff.js +138 -0
  120. package/dist/tools/apply-patch/patch-text/looks-like-patch.d.ts +1 -0
  121. package/dist/tools/apply-patch/patch-text/looks-like-patch.js +13 -0
  122. package/dist/tools/apply-patch/patch-text/normalize.d.ts +3 -0
  123. package/dist/tools/apply-patch/patch-text/normalize.js +262 -0
  124. package/dist/tools/apply-patch/structured/coercion.d.ts +3 -0
  125. package/dist/tools/apply-patch/structured/coercion.js +82 -0
  126. package/dist/tools/apply-patch/validation/shared.d.ts +3 -0
  127. package/dist/tools/apply-patch/validation/shared.js +6 -0
  128. package/dist/tools/apply-patch/validator.d.ts +2 -2
  129. package/dist/tools/apply-patch/validator.js +6 -556
  130. 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
  };
@@ -175,40 +175,114 @@ export function buildGeminiToolsFromBridge(defs) {
175
175
  const props = isPlainRecord(propsRaw) ? propsRaw : {};
176
176
  const lowered = String(name || '').trim().toLowerCase();
177
177
  if (lowered === 'exec_command') {
178
- // Keep Gemini tool schema aligned with historical functionCall args. We observed large
179
- // conversations containing exec_command args as { command, workdir } and strict Gemini
180
- // validation will emit MALFORMED_FUNCTION_CALL when schema/args mismatch.
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
+ }
181
185
  if (!Object.prototype.hasOwnProperty.call(props, 'command') && Object.prototype.hasOwnProperty.call(props, 'cmd')) {
182
186
  props.command = props.cmd;
183
- try {
184
- delete props.cmd;
185
- }
186
- catch {
187
- // ignore
188
- }
187
+ }
188
+ if (!Object.prototype.hasOwnProperty.call(props, 'cmd')) {
189
+ props.cmd = { type: 'string' };
189
190
  }
190
191
  if (!Object.prototype.hasOwnProperty.call(props, 'command')) {
191
192
  props.command = { type: 'string' };
192
193
  }
194
+ if (!Object.prototype.hasOwnProperty.call(props, 'workdir')) {
195
+ props.workdir = { type: 'string' };
196
+ }
193
197
  params.properties = props;
194
- params.required = ['command'];
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
+ }
195
206
  return params;
196
207
  }
197
208
  if (lowered === 'write_stdin') {
198
- params.required = ['session_id'];
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
+ }
199
229
  return params;
200
230
  }
201
231
  if (lowered === 'apply_patch') {
202
- // Force a non-empty patch payload; leaving the schema fully permissive results in repeated `{}` tool calls.
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
203
236
  if (!Object.prototype.hasOwnProperty.call(props, 'patch')) {
204
- props.patch = { type: 'string' };
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
+ };
205
259
  }
206
260
  params.properties = props;
207
- params.required = ['patch'];
261
+ try {
262
+ delete params.required;
263
+ }
264
+ catch {
265
+ params.required = undefined;
266
+ }
208
267
  return params;
209
268
  }
210
269
  return params;
211
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
+ };
212
286
  defs.forEach((def) => {
213
287
  if (!def || typeof def !== 'object') {
214
288
  return;
@@ -232,7 +306,7 @@ export function buildGeminiToolsFromBridge(defs) {
232
306
  functionDeclarations: [
233
307
  {
234
308
  name,
235
- description,
309
+ description: rewriteDescription(name, description),
236
310
  parameters
237
311
  }
238
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
  }