@jsonstudio/llms 0.6.938 → 0.6.954

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.
@@ -26,6 +26,22 @@ import { extractSessionIdentifiersFromMetadata } from './session-identifiers.js'
26
26
  import { computeRequestTokens } from '../../../router/virtual-router/token-estimator.js';
27
27
  import { isCompactionRequest } from '../../shared/compaction-detect.js';
28
28
  import { applyHubProviderOutboundPolicy, recordHubPolicyObservation, setHubPolicyRuntimePolicy } from '../policy/policy-engine.js';
29
+ function extractHubPolicyOverride(metadata) {
30
+ const raw = metadata ? metadata.__hubPolicyOverride : undefined;
31
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
32
+ return undefined;
33
+ }
34
+ const obj = raw;
35
+ const mode = typeof obj.mode === 'string' ? obj.mode.trim().toLowerCase() : '';
36
+ const sampleRate = typeof obj.sampleRate === 'number' && Number.isFinite(obj.sampleRate) ? obj.sampleRate : undefined;
37
+ if (mode !== 'off' && mode !== 'observe' && mode !== 'enforce') {
38
+ return undefined;
39
+ }
40
+ return {
41
+ mode: mode,
42
+ ...(sampleRate !== undefined ? { sampleRate } : {})
43
+ };
44
+ }
29
45
  export class HubPipeline {
30
46
  routerEngine;
31
47
  config;
@@ -116,11 +132,15 @@ export class HubPipeline {
116
132
  normalized.metadata = normalized.metadata || {};
117
133
  normalized.metadata.compactionRequest = true;
118
134
  }
135
+ const effectivePolicy = normalized.policyOverride ?? this.config.policy;
119
136
  const inboundAdapterContext = this.buildAdapterContext(normalized);
120
- const inboundRecorder = this.maybeCreateStageRecorder(inboundAdapterContext, normalized.entryEndpoint);
137
+ const inboundRecorder = this.maybeCreateStageRecorder(inboundAdapterContext, normalized.entryEndpoint, {
138
+ disableSnapshots: normalized.disableSnapshots === true
139
+ });
121
140
  const inboundStart = Date.now();
122
141
  // Phase 0: observe client inbound payload violations (best-effort; no rewrites).
123
142
  recordHubPolicyObservation({
143
+ policy: effectivePolicy,
124
144
  providerProtocol: this.resolveClientProtocol(normalized.entryEndpoint),
125
145
  payload: rawRequest,
126
146
  phase: 'client_inbound',
@@ -280,7 +300,9 @@ export class HubPipeline {
280
300
  // Snapshots must be grouped by entry endpoint (client-facing protocol), not by provider protocol.
281
301
  // Otherwise one request would be split across multiple folders (e.g. openai-responses + anthropic-messages),
282
302
  // which breaks codex-samples correlation.
283
- const outboundRecorder = this.maybeCreateStageRecorder(outboundAdapterContext, normalized.entryEndpoint);
303
+ const outboundRecorder = this.maybeCreateStageRecorder(outboundAdapterContext, normalized.entryEndpoint, {
304
+ disableSnapshots: normalized.disableSnapshots === true
305
+ });
284
306
  const outboundStart = Date.now();
285
307
  let providerPayload;
286
308
  const outboundStage1 = await runReqOutboundStage1SemanticMap({
@@ -303,14 +325,14 @@ export class HubPipeline {
303
325
  stageRecorder: outboundRecorder
304
326
  });
305
327
  providerPayload = applyHubProviderOutboundPolicy({
306
- policy: this.config.policy,
328
+ policy: effectivePolicy,
307
329
  providerProtocol: outboundProtocol,
308
330
  payload: formattedPayload,
309
331
  stageRecorder: outboundRecorder,
310
332
  requestId: normalized.id
311
333
  });
312
334
  recordHubPolicyObservation({
313
- policy: this.config.policy,
335
+ policy: effectivePolicy,
314
336
  providerProtocol: outboundProtocol,
315
337
  payload: providerPayload,
316
338
  stageRecorder: outboundRecorder,
@@ -481,6 +503,10 @@ export class HubPipeline {
481
503
  streamingHint,
482
504
  toolCallIdStyle
483
505
  };
506
+ const runtime = metadata.runtime;
507
+ if (runtime && typeof runtime === 'object' && !Array.isArray(runtime)) {
508
+ adapterContext.runtime = jsonClone(runtime);
509
+ }
484
510
  const clientRequestId = typeof metadata.clientRequestId === 'string'
485
511
  ? metadata.clientRequestId.trim()
486
512
  : '';
@@ -569,7 +595,10 @@ export class HubPipeline {
569
595
  }
570
596
  return adapterContext;
571
597
  }
572
- maybeCreateStageRecorder(context, endpoint) {
598
+ maybeCreateStageRecorder(context, endpoint, options) {
599
+ if (options?.disableSnapshots === true) {
600
+ return undefined;
601
+ }
573
602
  if (!shouldRecordSnapshots()) {
574
603
  return undefined;
575
604
  }
@@ -596,6 +625,14 @@ export class HubPipeline {
596
625
  const metadataRecord = {
597
626
  ...(request.metadata ?? {})
598
627
  };
628
+ const policyOverride = extractHubPolicyOverride(metadataRecord);
629
+ if (Object.prototype.hasOwnProperty.call(metadataRecord, '__hubPolicyOverride')) {
630
+ delete metadataRecord.__hubPolicyOverride;
631
+ }
632
+ const disableSnapshots = metadataRecord.__disableHubSnapshots === true;
633
+ if (Object.prototype.hasOwnProperty.call(metadataRecord, '__disableHubSnapshots')) {
634
+ delete metadataRecord.__disableHubSnapshots;
635
+ }
599
636
  const entryEndpoint = typeof metadataRecord.entryEndpoint === 'string'
600
637
  ? normalizeEndpoint(metadataRecord.entryEndpoint)
601
638
  : endpoint;
@@ -633,6 +670,8 @@ export class HubPipeline {
633
670
  providerProtocol,
634
671
  payload,
635
672
  metadata: normalizedMetadata,
673
+ policyOverride: policyOverride ?? undefined,
674
+ disableSnapshots,
636
675
  processMode,
637
676
  direction,
638
677
  stage,
@@ -312,9 +312,6 @@ function buildApplyPatchDiagnostics(output) {
312
312
  }
313
313
  function appendDiagnosticsToRecord(record) {
314
314
  const name = typeof record.name === 'string' ? record.name.trim() : undefined;
315
- if (name !== 'apply_patch') {
316
- return;
317
- }
318
315
  let text;
319
316
  if (typeof record.output === 'string') {
320
317
  text = record.output;
@@ -329,6 +326,12 @@ function appendDiagnosticsToRecord(record) {
329
326
  if (!diag) {
330
327
  return;
331
328
  }
329
+ // Some providers / compatibility layers omit `name` on tool outputs.
330
+ // When the output text matches apply_patch argument-parse failures, still inject the diagnostics
331
+ // to keep user-visible behavior stable across modes.
332
+ if (name && name !== 'apply_patch') {
333
+ return;
334
+ }
332
335
  const merged = `${text}${diag}`;
333
336
  if (typeof record.output === 'string') {
334
337
  record.output = merged;
@@ -59,6 +59,25 @@ function normalizeToolContent(content) {
59
59
  return String(content ?? '');
60
60
  }
61
61
  }
62
+ function maybeAugmentRouteCodexApplyPatchPrecheck(content) {
63
+ if (!content || typeof content !== 'string') {
64
+ return content;
65
+ }
66
+ if (content.includes('[RouteCodex precheck]')) {
67
+ return content;
68
+ }
69
+ const lower = content.toLowerCase();
70
+ if (!lower.includes('failed to parse function arguments')) {
71
+ return content;
72
+ }
73
+ if (content.includes('missing field `input`')) {
74
+ return `${content}\n\n[RouteCodex precheck] apply_patch 参数解析失败:缺少字段 "input"。当前 RouteCodex 期望 { input, patch } 形态,并且两个字段都应包含完整统一 diff 文本。`;
75
+ }
76
+ if (content.includes('invalid type: map, expected a string')) {
77
+ return `${content}\n\n[RouteCodex precheck] apply_patch 参数类型错误:检测到 JSON 对象(map),但客户端期望字符串。请先对参数做 JSON.stringify 再写入 arguments,或直接提供 { patch: "<统一 diff>" } 形式。`;
78
+ }
79
+ return content;
80
+ }
62
81
  export function maybeAugmentApplyPatchErrorContent(content, toolName) {
63
82
  if (!content)
64
83
  return content;
@@ -175,9 +194,21 @@ function normalizeChatMessages(raw) {
175
194
  return;
176
195
  }
177
196
  const nameValue = typeof value.name === 'string' && value.name.trim().length ? value.name : undefined;
197
+ const normalizedToolOutput = normalizeToolContent(value.content ?? value.output);
198
+ const routeCodexPrechecked = maybeAugmentRouteCodexApplyPatchPrecheck(normalizedToolOutput);
199
+ if (routeCodexPrechecked !== normalizedToolOutput) {
200
+ // Keep tool role message content aligned with outbound provider requests (e.g. Chat→Responses),
201
+ // while avoiding double-injection.
202
+ if (typeof chatMessage.content === 'string' || chatMessage.content === undefined || chatMessage.content === null) {
203
+ chatMessage.content = routeCodexPrechecked;
204
+ }
205
+ else if (typeof chatMessage.output === 'string') {
206
+ chatMessage.output = routeCodexPrechecked;
207
+ }
208
+ }
178
209
  const outputEntry = {
179
210
  tool_call_id: toolCallId,
180
- content: normalizeToolContent(value.content ?? value.output),
211
+ content: routeCodexPrechecked,
181
212
  name: nameValue
182
213
  };
183
214
  outputEntry.content = maybeAugmentApplyPatchErrorContent(outputEntry.content, outputEntry.name);
@@ -624,20 +624,25 @@ function alignToolCallArgsToSchema(options) {
624
624
  const next = { ...options.args };
625
625
  // Align historical Codex tool args to the *declared schema* for Gemini.
626
626
  // Gemini validates historical functionCall.args against tool declarations, so mismatches like:
627
- // - exec_command: { cmd } vs schema { command }
628
- // - apply_patch: { patch/input } vs schema { instructions/changes }
627
+ // - exec_command: { cmd } vs schema { command } (or vice-versa)
628
+ // - apply_patch: { patch/input } vs schema { instructions } (or vice-versa)
629
629
  // can cause MALFORMED_FUNCTION_CALL and empty responses.
630
630
  if (lowered === 'exec_command') {
631
+ // Prefer the declared schema key; do not delete keys blindly.
632
+ if (schema.has('cmd') && !Object.prototype.hasOwnProperty.call(next, 'cmd') && Object.prototype.hasOwnProperty.call(next, 'command')) {
633
+ next.cmd = next.command;
634
+ }
631
635
  if (schema.has('command') && !Object.prototype.hasOwnProperty.call(next, 'command') && Object.prototype.hasOwnProperty.call(next, 'cmd')) {
632
636
  next.command = next.cmd;
633
637
  }
634
- delete next.cmd;
635
638
  }
636
639
  else if (lowered === 'write_stdin') {
637
640
  if (schema.has('chars') && !Object.prototype.hasOwnProperty.call(next, 'chars') && Object.prototype.hasOwnProperty.call(next, 'text')) {
638
641
  next.chars = next.text;
639
642
  }
640
- delete next.text;
643
+ if (schema.has('text') && !Object.prototype.hasOwnProperty.call(next, 'text') && Object.prototype.hasOwnProperty.call(next, 'chars')) {
644
+ next.text = next.chars;
645
+ }
641
646
  }
642
647
  else if (lowered === 'apply_patch') {
643
648
  if (schema.has('instructions') && !Object.prototype.hasOwnProperty.call(next, 'instructions')) {
@@ -648,8 +653,12 @@ function alignToolCallArgsToSchema(options) {
648
653
  next.instructions = candidate;
649
654
  }
650
655
  }
651
- delete next.patch;
652
- delete next.input;
656
+ if (schema.has('patch') && !Object.prototype.hasOwnProperty.call(next, 'patch')) {
657
+ const input = typeof next.input === 'string' ? next.input : undefined;
658
+ if (input && input.trim().length) {
659
+ next.patch = input;
660
+ }
661
+ }
653
662
  }
654
663
  // Prune to schema keys for known Codex tools to reduce strict upstream validation failures.
655
664
  if (lowered === 'exec_command' || lowered === 'write_stdin' || lowered === 'apply_patch') {
@@ -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,49 @@ 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 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.
181
+ if (!Object.prototype.hasOwnProperty.call(props, 'command') && Object.prototype.hasOwnProperty.call(props, 'cmd')) {
182
+ props.command = props.cmd;
183
+ try {
184
+ delete props.cmd;
185
+ }
186
+ catch {
187
+ // ignore
188
+ }
189
+ }
190
+ if (!Object.prototype.hasOwnProperty.call(props, 'command')) {
191
+ props.command = { type: 'string' };
192
+ }
193
+ params.properties = props;
194
+ params.required = ['command'];
195
+ return params;
196
+ }
197
+ if (lowered === 'write_stdin') {
198
+ params.required = ['session_id'];
199
+ return params;
200
+ }
201
+ if (lowered === 'apply_patch') {
202
+ // Force a non-empty patch payload; leaving the schema fully permissive results in repeated `{}` tool calls.
203
+ if (!Object.prototype.hasOwnProperty.call(props, 'patch')) {
204
+ props.patch = { type: 'string' };
205
+ }
206
+ params.properties = props;
207
+ params.required = ['patch'];
208
+ return params;
209
+ }
210
+ return params;
211
+ };
169
212
  defs.forEach((def) => {
170
213
  if (!def || typeof def !== 'object') {
171
214
  return;
@@ -184,7 +227,7 @@ export function buildGeminiToolsFromBridge(defs) {
184
227
  : typeof def.description === 'string'
185
228
  ? def.description
186
229
  : undefined;
187
- const parameters = cloneParameters(fnNode?.parameters ?? def.parameters ?? { type: 'object', properties: {} });
230
+ const parameters = applyFixups(name, cloneParameters(fnNode?.parameters ?? def.parameters ?? { type: 'object', properties: {} }));
188
231
  tools.push({
189
232
  functionDeclarations: [
190
233
  {
@@ -185,10 +185,12 @@ export async function runServerToolOrchestration(options) {
185
185
  }
186
186
  const isStopMessageFlow = engineResult.execution.flowId === 'stop_message_flow';
187
187
  const isGeminiEmptyReplyContinue = engineResult.execution.flowId === 'gemini_empty_reply_continue';
188
+ const isApplyPatchGuard = engineResult.execution.flowId === 'apply_patch_guard';
189
+ const isExecCommandGuard = engineResult.execution.flowId === 'exec_command_guard';
188
190
  const stopMessageSource = isStopMessageFlow ? getStopMessageSource(options.adapterContext) : undefined;
189
191
  const isAutoStopMessage = isStopMessageFlow && stopMessageSource !== 'explicit';
190
192
  const isErrorAutoFlow = engineResult.execution.flowId === 'iflow_model_error_retry';
191
- const applyAutoLimit = isAutoStopMessage || isErrorAutoFlow;
193
+ const applyAutoLimit = isAutoStopMessage || isErrorAutoFlow || isGeminiEmptyReplyContinue || isApplyPatchGuard || isExecCommandGuard;
192
194
  // ServerTool followups must not inherit or inject any routeHint; always route fresh.
193
195
  const preserveRouteHint = false;
194
196
  const loopState = buildServerToolLoopState(options.adapterContext, engineResult.execution.flowId, engineResult.execution.followup.payload);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsonstudio/llms",
3
- "version": "0.6.938",
3
+ "version": "0.6.954",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",