@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.
- package/dist/conversion/hub/pipeline/hub-pipeline.js +44 -5
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js +6 -3
- package/dist/conversion/hub/semantic-mappers/chat-mapper.js +32 -1
- package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +15 -6
- package/dist/conversion/shared/gemini-tool-utils.js +46 -3
- package/dist/servertool/engine.js +3 -1
- package/package.json +1 -1
|
@@ -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:
|
|
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:
|
|
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,
|
package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js
CHANGED
|
@@ -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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
652
|
-
|
|
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
|
-
//
|
|
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);
|