@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.
- package/dist/conversion/hub/operation-table/operation-table-runner.d.ts +18 -0
- package/dist/conversion/hub/operation-table/operation-table-runner.js +158 -0
- package/dist/conversion/hub/operation-table/semantic-mappers/anthropic-mapper.d.ts +8 -0
- package/dist/conversion/hub/operation-table/semantic-mappers/anthropic-mapper.js +303 -0
- package/dist/conversion/hub/operation-table/semantic-mappers/chat-mapper.d.ts +8 -0
- package/dist/conversion/hub/operation-table/semantic-mappers/chat-mapper.js +413 -0
- package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.d.ts +7 -0
- package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.js +841 -0
- package/dist/conversion/hub/operation-table/semantic-mappers/responses-mapper.d.ts +21 -0
- package/dist/conversion/hub/operation-table/semantic-mappers/responses-mapper.js +535 -0
- package/dist/conversion/hub/ops/operations.d.ts +19 -0
- package/dist/conversion/hub/ops/operations.js +126 -0
- package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +9 -0
- package/dist/conversion/hub/pipeline/hub-pipeline.js +489 -19
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.js +6 -0
- package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage1_semantic_map/index.js +11 -0
- package/dist/conversion/hub/policy/policy-engine.js +41 -9
- package/dist/conversion/hub/policy/protocol-spec.d.ts +25 -0
- package/dist/conversion/hub/policy/protocol-spec.js +73 -23
- package/dist/conversion/hub/process/chat-process.js +252 -41
- package/dist/conversion/hub/response/provider-response.js +175 -2
- package/dist/conversion/hub/response/response-runtime.js +1 -1
- package/dist/conversion/hub/semantic-mappers/anthropic-mapper.d.ts +1 -8
- package/dist/conversion/hub/semantic-mappers/anthropic-mapper.js +1 -365
- package/dist/conversion/hub/semantic-mappers/chat-mapper.d.ts +1 -8
- package/dist/conversion/hub/semantic-mappers/chat-mapper.js +1 -467
- package/dist/conversion/hub/semantic-mappers/gemini-mapper.d.ts +1 -7
- package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +1 -903
- package/dist/conversion/hub/semantic-mappers/responses-mapper.d.ts +1 -21
- package/dist/conversion/hub/semantic-mappers/responses-mapper.js +1 -593
- package/dist/conversion/hub/tool-surface/tool-surface-engine.d.ts +18 -0
- package/dist/conversion/hub/tool-surface/tool-surface-engine.js +571 -0
- package/dist/conversion/responses/responses-openai-bridge.js +14 -2
- package/dist/conversion/shared/bridge-message-utils.js +2 -8
- package/dist/conversion/shared/bridge-policies.js +5 -105
- package/dist/conversion/shared/gemini-tool-utils.js +89 -15
- package/dist/conversion/shared/protocol-field-allowlists.d.ts +7 -0
- package/dist/conversion/shared/protocol-field-allowlists.js +145 -0
- package/dist/conversion/shared/reasoning-tool-normalizer.js +4 -2
- package/dist/conversion/shared/snapshot-hooks.js +166 -3
- package/dist/conversion/shared/text-markup-normalizer.d.ts +2 -0
- package/dist/conversion/shared/text-markup-normalizer.js +345 -9
- package/dist/conversion/shared/thought-signature-validator.d.ts +52 -0
- package/dist/conversion/shared/thought-signature-validator.js +170 -0
- package/dist/conversion/shared/tool-argument-repairer.d.ts +39 -0
- package/dist/conversion/shared/tool-argument-repairer.js +56 -0
- package/dist/conversion/shared/tool-call-id-manager.d.ts +113 -0
- package/dist/conversion/shared/tool-call-id-manager.js +231 -0
- package/dist/conversion/shared/tool-canonicalizer.js +2 -11
- package/dist/router/virtual-router/bootstrap.js +54 -5
- package/dist/router/virtual-router/engine-selection.js +132 -42
- package/dist/router/virtual-router/engine.d.ts +3 -0
- package/dist/router/virtual-router/engine.js +142 -33
- package/dist/router/virtual-router/health-weighted.d.ts +25 -0
- package/dist/router/virtual-router/health-weighted.js +63 -0
- package/dist/router/virtual-router/load-balancer.d.ts +2 -0
- package/dist/router/virtual-router/load-balancer.js +45 -16
- package/dist/router/virtual-router/routing-instructions.js +17 -1
- package/dist/router/virtual-router/sticky-session-store.js +136 -24
- package/dist/router/virtual-router/stop-message-file-resolver.d.ts +1 -0
- package/dist/router/virtual-router/stop-message-file-resolver.js +74 -0
- package/dist/router/virtual-router/stop-message-state-sync.d.ts +15 -0
- package/dist/router/virtual-router/stop-message-state-sync.js +57 -0
- package/dist/router/virtual-router/types.d.ts +70 -0
- package/dist/servertool/clock/config.d.ts +7 -0
- package/dist/servertool/clock/config.js +27 -0
- package/dist/servertool/clock/daemon.d.ts +3 -0
- package/dist/servertool/clock/daemon.js +79 -0
- package/dist/servertool/clock/io.d.ts +2 -0
- package/dist/servertool/clock/io.js +13 -0
- package/dist/servertool/clock/paths.d.ts +4 -0
- package/dist/servertool/clock/paths.js +25 -0
- package/dist/servertool/clock/session-store.d.ts +3 -0
- package/dist/servertool/clock/session-store.js +56 -0
- package/dist/servertool/clock/state.d.ts +5 -0
- package/dist/servertool/clock/state.js +62 -0
- package/dist/servertool/clock/task-store.d.ts +5 -0
- package/dist/servertool/clock/task-store.js +4 -0
- package/dist/servertool/clock/tasks.d.ts +17 -0
- package/dist/servertool/clock/tasks.js +221 -0
- package/dist/servertool/clock/types.d.ts +36 -0
- package/dist/servertool/clock/types.js +1 -0
- package/dist/servertool/engine.d.ts +2 -0
- package/dist/servertool/engine.js +161 -7
- package/dist/servertool/followup-shadow.d.ts +16 -0
- package/dist/servertool/followup-shadow.js +145 -0
- package/dist/servertool/handlers/apply-patch-guard.js +1 -265
- package/dist/servertool/handlers/clock-auto.d.ts +1 -0
- package/dist/servertool/handlers/clock-auto.js +160 -0
- package/dist/servertool/handlers/clock.d.ts +1 -0
- package/dist/servertool/handlers/clock.js +197 -0
- package/dist/servertool/handlers/exec-command-guard.js +7 -555
- package/dist/servertool/handlers/followup-request-builder.d.ts +15 -7
- package/dist/servertool/handlers/followup-request-builder.js +248 -28
- package/dist/servertool/handlers/gemini-empty-reply-continue.js +62 -169
- package/dist/servertool/handlers/iflow-model-error-retry.js +18 -28
- package/dist/servertool/handlers/recursive-detection-guard.d.ts +1 -0
- package/dist/servertool/handlers/recursive-detection-guard.js +333 -0
- package/dist/servertool/handlers/stop-message-auto.js +47 -175
- package/dist/servertool/handlers/vision.d.ts +7 -1
- package/dist/servertool/handlers/vision.js +61 -117
- package/dist/servertool/handlers/web-search.d.ts +7 -1
- package/dist/servertool/handlers/web-search.js +122 -105
- package/dist/servertool/reenter-backend.d.ts +23 -0
- package/dist/servertool/reenter-backend.js +18 -0
- package/dist/servertool/server-side-tools.d.ts +3 -2
- package/dist/servertool/server-side-tools.js +64 -10
- package/dist/servertool/types.d.ts +92 -3
- package/dist/sse/json-to-sse/event-generators/responses.js +3 -21
- package/dist/sse/shared/serializers/responses-event-serializer.d.ts +8 -0
- package/dist/sse/shared/serializers/responses-event-serializer.js +19 -0
- package/dist/sse/shared/writer.js +24 -7
- package/dist/tools/apply-patch/execution-capturer.js +3 -1
- package/dist/tools/apply-patch/json/parse-loose.d.ts +3 -0
- package/dist/tools/apply-patch/json/parse-loose.js +139 -0
- package/dist/tools/apply-patch/patch-text/context-diff.d.ts +1 -0
- package/dist/tools/apply-patch/patch-text/context-diff.js +173 -0
- package/dist/tools/apply-patch/patch-text/git-diff.d.ts +1 -0
- package/dist/tools/apply-patch/patch-text/git-diff.js +138 -0
- package/dist/tools/apply-patch/patch-text/looks-like-patch.d.ts +1 -0
- package/dist/tools/apply-patch/patch-text/looks-like-patch.js +13 -0
- package/dist/tools/apply-patch/patch-text/normalize.d.ts +3 -0
- package/dist/tools/apply-patch/patch-text/normalize.js +262 -0
- package/dist/tools/apply-patch/structured/coercion.d.ts +3 -0
- package/dist/tools/apply-patch/structured/coercion.js +82 -0
- package/dist/tools/apply-patch/validation/shared.d.ts +3 -0
- package/dist/tools/apply-patch/validation/shared.js +6 -0
- package/dist/tools/apply-patch/validator.d.ts +2 -2
- package/dist/tools/apply-patch/validator.js +6 -556
- package/package.json +1 -1
package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.js
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { chatEnvelopeToStandardized } from '../../../../standardized-bridge.js';
|
|
2
2
|
import { validateChatEnvelope } from '../../../../../shared/chat-envelope-validator.js';
|
|
3
|
+
import { applyHubOperationTableInbound } from '../../../../operation-table/operation-table-runner.js';
|
|
3
4
|
import { recordStage } from '../../../stages/utils.js';
|
|
4
5
|
export async function runReqInboundStage2SemanticMap(options) {
|
|
5
6
|
const chatEnvelope = await options.semanticMapper.toChat(options.formatEnvelope, options.adapterContext);
|
|
7
|
+
applyHubOperationTableInbound({
|
|
8
|
+
formatEnvelope: options.formatEnvelope,
|
|
9
|
+
chatEnvelope,
|
|
10
|
+
adapterContext: options.adapterContext
|
|
11
|
+
});
|
|
6
12
|
validateChatEnvelope(chatEnvelope, {
|
|
7
13
|
stage: 'req_inbound',
|
|
8
14
|
direction: 'request'
|
package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage1_semantic_map/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { standardizedToChatEnvelope } from '../../../../standardized-bridge.js';
|
|
2
2
|
import { validateChatEnvelope } from '../../../../../shared/chat-envelope-validator.js';
|
|
3
|
+
import { applyHubOperationTableOutboundPostMap, applyHubOperationTableOutboundPreMap } from '../../../../operation-table/operation-table-runner.js';
|
|
3
4
|
import { recordStage } from '../../../stages/utils.js';
|
|
4
5
|
export async function runReqOutboundStage1SemanticMap(options) {
|
|
5
6
|
const chatEnvelope = standardizedToChatEnvelope(options.request, {
|
|
@@ -16,7 +17,17 @@ export async function runReqOutboundStage1SemanticMap(options) {
|
|
|
16
17
|
stage: 'req_outbound',
|
|
17
18
|
direction: 'request'
|
|
18
19
|
});
|
|
20
|
+
await applyHubOperationTableOutboundPreMap({
|
|
21
|
+
protocol: options.adapterContext.providerProtocol,
|
|
22
|
+
chatEnvelope,
|
|
23
|
+
adapterContext: options.adapterContext
|
|
24
|
+
});
|
|
19
25
|
const formatEnvelope = (await options.semanticMapper.fromChat(chatEnvelope, options.adapterContext));
|
|
26
|
+
applyHubOperationTableOutboundPostMap({
|
|
27
|
+
chatEnvelope,
|
|
28
|
+
formatEnvelope,
|
|
29
|
+
adapterContext: options.adapterContext
|
|
30
|
+
});
|
|
20
31
|
recordStage(options.stageRecorder, 'req_outbound_stage1_semantic_map', chatEnvelope);
|
|
21
32
|
return { chatEnvelope, formatEnvelope };
|
|
22
33
|
}
|
|
@@ -38,6 +38,11 @@ function applyProviderOutboundPolicy(providerProtocol, payload) {
|
|
|
38
38
|
out = { ...payload };
|
|
39
39
|
}
|
|
40
40
|
};
|
|
41
|
+
const allowedTopLevelKeys = Array.isArray(spec.providerOutbound.allowedTopLevelKeys) &&
|
|
42
|
+
spec.providerOutbound.allowedTopLevelKeys.length > 0 &&
|
|
43
|
+
spec.providerOutbound.enforceAllowedTopLevelKeys === true
|
|
44
|
+
? new Set(spec.providerOutbound.allowedTopLevelKeys)
|
|
45
|
+
: undefined;
|
|
41
46
|
// Reserved/private keys must never be sent upstream.
|
|
42
47
|
for (const key of Object.keys(payload)) {
|
|
43
48
|
if (spec.providerOutbound.reservedKeyPrefixes.some((prefix) => key.startsWith(prefix))) {
|
|
@@ -76,6 +81,18 @@ function applyProviderOutboundPolicy(providerProtocol, payload) {
|
|
|
76
81
|
delete out[wrapperKey];
|
|
77
82
|
flattenedWrappers.push(wrapperKey);
|
|
78
83
|
}
|
|
84
|
+
// Enforce protocol allowlist (top-level). Only runs when explicitly enabled
|
|
85
|
+
// for this protocol, and only after wrapper flatten so allowed fields are
|
|
86
|
+
// present at the correct level.
|
|
87
|
+
if (allowedTopLevelKeys) {
|
|
88
|
+
for (const key of Object.keys(out)) {
|
|
89
|
+
if (allowedTopLevelKeys.has(key))
|
|
90
|
+
continue;
|
|
91
|
+
ensureOutClone();
|
|
92
|
+
delete out[key];
|
|
93
|
+
removedTopLevelKeys.push(key);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
79
96
|
return {
|
|
80
97
|
payload: out,
|
|
81
98
|
changed: out !== payload || removedTopLevelKeys.length > 0 || flattenedWrappers.length > 0,
|
|
@@ -83,16 +100,20 @@ function applyProviderOutboundPolicy(providerProtocol, payload) {
|
|
|
83
100
|
flattenedWrappers
|
|
84
101
|
};
|
|
85
102
|
}
|
|
86
|
-
function
|
|
103
|
+
function observeProviderPayload(options) {
|
|
87
104
|
const violations = [];
|
|
88
|
-
//
|
|
105
|
+
// Observe-only: detect known layout anti-patterns and reserved keys.
|
|
89
106
|
// Do NOT modify payload here.
|
|
90
|
-
const spec = resolveHubProtocolSpec(providerProtocol);
|
|
107
|
+
const spec = resolveHubProtocolSpec(options.providerProtocol);
|
|
108
|
+
const allowlistEnabled = options.phase === 'provider_outbound';
|
|
109
|
+
const allowedTopLevelKeys = allowlistEnabled && Array.isArray(spec.providerOutbound.allowedTopLevelKeys)
|
|
110
|
+
? new Set(spec.providerOutbound.allowedTopLevelKeys)
|
|
111
|
+
: undefined;
|
|
91
112
|
for (const rule of spec.providerOutbound.forbidWrappers) {
|
|
92
113
|
if (rule.code !== 'forbid_wrapper') {
|
|
93
114
|
continue;
|
|
94
115
|
}
|
|
95
|
-
if (rule.path in payload && isJsonRecord(payload[rule.path])) {
|
|
116
|
+
if (rule.path in options.payload && isJsonRecord(options.payload[rule.path])) {
|
|
96
117
|
violations.push({
|
|
97
118
|
code: 'unexpected_wrapper',
|
|
98
119
|
path: rule.path,
|
|
@@ -101,18 +122,26 @@ function observeProviderOutboundPayload(providerProtocol, payload) {
|
|
|
101
122
|
}
|
|
102
123
|
}
|
|
103
124
|
// Always record unknown private wrapper keys (best-effort, conservative).
|
|
104
|
-
for (const key of Object.keys(payload)) {
|
|
125
|
+
for (const key of Object.keys(options.payload)) {
|
|
105
126
|
if (spec.providerOutbound.reservedKeyPrefixes.some((prefix) => key.startsWith(prefix))) {
|
|
106
127
|
violations.push({
|
|
107
128
|
code: 'unexpected_field',
|
|
108
129
|
path: key
|
|
109
130
|
});
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (allowedTopLevelKeys && !allowedTopLevelKeys.has(key)) {
|
|
134
|
+
violations.push({
|
|
135
|
+
code: 'unexpected_field',
|
|
136
|
+
path: key,
|
|
137
|
+
detail: `Top-level key is not in protocol allowlist: ${options.providerProtocol}`
|
|
138
|
+
});
|
|
110
139
|
}
|
|
111
140
|
}
|
|
112
141
|
const unexpectedFieldCount = violations.filter((v) => v.code === 'unexpected_field').length;
|
|
113
142
|
return {
|
|
114
|
-
phase:
|
|
115
|
-
providerProtocol,
|
|
143
|
+
phase: options.phase,
|
|
144
|
+
providerProtocol: options.providerProtocol,
|
|
116
145
|
violations,
|
|
117
146
|
summary: {
|
|
118
147
|
totalViolations: violations.length,
|
|
@@ -136,8 +165,11 @@ export function recordHubPolicyObservation(options) {
|
|
|
136
165
|
}
|
|
137
166
|
try {
|
|
138
167
|
const phase = options.phase ?? 'provider_outbound';
|
|
139
|
-
const observation =
|
|
140
|
-
|
|
168
|
+
const observation = observeProviderPayload({
|
|
169
|
+
phase,
|
|
170
|
+
providerProtocol: options.providerProtocol,
|
|
171
|
+
payload: options.payload
|
|
172
|
+
});
|
|
141
173
|
if (observation.summary.totalViolations <= 0) {
|
|
142
174
|
return;
|
|
143
175
|
}
|
|
@@ -26,6 +26,19 @@ export interface ProviderOutboundPolicySpec {
|
|
|
26
26
|
* Keep this false for protocols not yet migrated, to avoid behavior changes.
|
|
27
27
|
*/
|
|
28
28
|
enforceEnabled: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Provider outbound payload allowlist (top-level keys), used for observation
|
|
31
|
+
* to detect drift.
|
|
32
|
+
*/
|
|
33
|
+
allowedTopLevelKeys?: readonly string[];
|
|
34
|
+
/**
|
|
35
|
+
* When enabled, provider outbound payload will drop any top-level keys not
|
|
36
|
+
* present in allowedTopLevelKeys (after wrapper flatten).
|
|
37
|
+
*
|
|
38
|
+
* Keep this configurable for progressive rollout, but Phase 1 completion
|
|
39
|
+
* requires enabling it for all protocols.
|
|
40
|
+
*/
|
|
41
|
+
enforceAllowedTopLevelKeys?: boolean;
|
|
29
42
|
/**
|
|
30
43
|
* Reserved/private key prefixes that must not be sent upstream.
|
|
31
44
|
* (Enforced only when enforceEnabled=true.)
|
|
@@ -42,9 +55,21 @@ export interface ProviderOutboundPolicySpec {
|
|
|
42
55
|
*/
|
|
43
56
|
flattenWrappers: ProviderOutboundWrapperFlattenRule[];
|
|
44
57
|
}
|
|
58
|
+
export type ToolDefinitionFormat = 'openai' | 'anthropic' | 'gemini';
|
|
59
|
+
export type ProviderOutboundHistoryCarrier = 'messages' | 'input';
|
|
60
|
+
export interface ToolSurfaceSpec {
|
|
61
|
+
expectedToolFormat: ToolDefinitionFormat;
|
|
62
|
+
/**
|
|
63
|
+
* For OpenAI protocols, tool call/result history may be carried in either
|
|
64
|
+
* chat `messages[]` or responses `input[]`. This spec describes the expected
|
|
65
|
+
* carrier so toolSurface can normalize or at least record diffs.
|
|
66
|
+
*/
|
|
67
|
+
expectedHistoryCarrier?: ProviderOutboundHistoryCarrier;
|
|
68
|
+
}
|
|
45
69
|
export interface ProtocolSpec {
|
|
46
70
|
id: HubProviderProtocol;
|
|
47
71
|
providerOutbound: ProviderOutboundPolicySpec;
|
|
72
|
+
toolSurface: ToolSurfaceSpec;
|
|
48
73
|
}
|
|
49
74
|
export declare const HUB_PROTOCOL_SPECS: Record<HubProviderProtocol, ProtocolSpec>;
|
|
50
75
|
export declare function resolveHubProtocolSpec(protocol: string): ProtocolSpec;
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { ANTHROPIC_ALLOWED_FIELDS, ANTHROPIC_PARAMETERS_WRAPPER_ALLOW_KEYS, GEMINI_ALLOWED_FIELDS, OPENAI_CHAT_ALLOWED_FIELDS, OPENAI_CHAT_PARAMETERS_WRAPPER_ALLOW_KEYS, OPENAI_RESPONSES_ALLOWED_FIELDS, OPENAI_RESPONSES_PARAMETERS_WRAPPER_ALLOW_KEYS } from '../../shared/protocol-field-allowlists.js';
|
|
1
2
|
const RESPONSES_SPEC = {
|
|
2
3
|
id: 'openai-responses',
|
|
3
4
|
providerOutbound: {
|
|
4
5
|
enforceEnabled: true,
|
|
6
|
+
allowedTopLevelKeys: OPENAI_RESPONSES_ALLOWED_FIELDS,
|
|
7
|
+
enforceAllowedTopLevelKeys: true,
|
|
5
8
|
forbidWrappers: [
|
|
6
9
|
{
|
|
7
10
|
code: 'forbid_wrapper',
|
|
@@ -26,30 +29,21 @@ const RESPONSES_SPEC = {
|
|
|
26
29
|
aliasKeys: {
|
|
27
30
|
max_tokens: 'max_output_tokens'
|
|
28
31
|
},
|
|
29
|
-
allowKeys: [
|
|
30
|
-
'temperature',
|
|
31
|
-
'top_p',
|
|
32
|
-
'max_output_tokens',
|
|
33
|
-
'seed',
|
|
34
|
-
'logit_bias',
|
|
35
|
-
'user',
|
|
36
|
-
'parallel_tool_calls',
|
|
37
|
-
'tool_choice',
|
|
38
|
-
'response_format',
|
|
39
|
-
'stream',
|
|
40
|
-
'stop',
|
|
41
|
-
'stop_sequences',
|
|
42
|
-
'modalities',
|
|
43
|
-
'top_k'
|
|
44
|
-
]
|
|
32
|
+
allowKeys: [...OPENAI_RESPONSES_PARAMETERS_WRAPPER_ALLOW_KEYS]
|
|
45
33
|
}
|
|
46
34
|
]
|
|
35
|
+
},
|
|
36
|
+
toolSurface: {
|
|
37
|
+
expectedToolFormat: 'openai',
|
|
38
|
+
expectedHistoryCarrier: 'input'
|
|
47
39
|
}
|
|
48
40
|
};
|
|
49
41
|
const DEFAULT_SPEC = {
|
|
50
42
|
id: 'openai-chat',
|
|
51
43
|
providerOutbound: {
|
|
52
|
-
enforceEnabled:
|
|
44
|
+
enforceEnabled: true,
|
|
45
|
+
allowedTopLevelKeys: OPENAI_CHAT_ALLOWED_FIELDS,
|
|
46
|
+
enforceAllowedTopLevelKeys: true,
|
|
53
47
|
forbidWrappers: [
|
|
54
48
|
{
|
|
55
49
|
code: 'forbid_wrapper',
|
|
@@ -63,7 +57,24 @@ const DEFAULT_SPEC = {
|
|
|
63
57
|
}
|
|
64
58
|
],
|
|
65
59
|
reservedKeyPrefixes: ['__', '_'],
|
|
66
|
-
flattenWrappers: [
|
|
60
|
+
flattenWrappers: [
|
|
61
|
+
{
|
|
62
|
+
wrapperKey: 'request',
|
|
63
|
+
onlyIfTargetMissing: true
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
wrapperKey: 'parameters',
|
|
67
|
+
onlyIfTargetMissing: true,
|
|
68
|
+
aliasKeys: {
|
|
69
|
+
max_output_tokens: 'max_tokens'
|
|
70
|
+
},
|
|
71
|
+
allowKeys: [...OPENAI_CHAT_PARAMETERS_WRAPPER_ALLOW_KEYS]
|
|
72
|
+
}
|
|
73
|
+
]
|
|
74
|
+
},
|
|
75
|
+
toolSurface: {
|
|
76
|
+
expectedToolFormat: 'openai',
|
|
77
|
+
expectedHistoryCarrier: 'messages'
|
|
67
78
|
}
|
|
68
79
|
};
|
|
69
80
|
export const HUB_PROTOCOL_SPECS = {
|
|
@@ -72,7 +83,9 @@ export const HUB_PROTOCOL_SPECS = {
|
|
|
72
83
|
'anthropic-messages': {
|
|
73
84
|
id: 'anthropic-messages',
|
|
74
85
|
providerOutbound: {
|
|
75
|
-
enforceEnabled:
|
|
86
|
+
enforceEnabled: true,
|
|
87
|
+
allowedTopLevelKeys: ANTHROPIC_ALLOWED_FIELDS,
|
|
88
|
+
enforceAllowedTopLevelKeys: true,
|
|
76
89
|
forbidWrappers: [
|
|
77
90
|
{
|
|
78
91
|
code: 'forbid_wrapper',
|
|
@@ -86,16 +99,53 @@ export const HUB_PROTOCOL_SPECS = {
|
|
|
86
99
|
}
|
|
87
100
|
],
|
|
88
101
|
reservedKeyPrefixes: ['__', '_'],
|
|
89
|
-
flattenWrappers: [
|
|
102
|
+
flattenWrappers: [
|
|
103
|
+
{
|
|
104
|
+
wrapperKey: 'request',
|
|
105
|
+
onlyIfTargetMissing: true
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
wrapperKey: 'parameters',
|
|
109
|
+
onlyIfTargetMissing: true,
|
|
110
|
+
aliasKeys: {
|
|
111
|
+
max_output_tokens: 'max_tokens'
|
|
112
|
+
},
|
|
113
|
+
allowKeys: [...ANTHROPIC_PARAMETERS_WRAPPER_ALLOW_KEYS]
|
|
114
|
+
}
|
|
115
|
+
]
|
|
116
|
+
},
|
|
117
|
+
toolSurface: {
|
|
118
|
+
expectedToolFormat: 'anthropic'
|
|
90
119
|
}
|
|
91
120
|
},
|
|
92
121
|
'gemini-chat': {
|
|
93
122
|
id: 'gemini-chat',
|
|
94
123
|
providerOutbound: {
|
|
95
|
-
enforceEnabled:
|
|
96
|
-
|
|
124
|
+
enforceEnabled: true,
|
|
125
|
+
allowedTopLevelKeys: GEMINI_ALLOWED_FIELDS,
|
|
126
|
+
enforceAllowedTopLevelKeys: true,
|
|
127
|
+
forbidWrappers: [
|
|
128
|
+
{
|
|
129
|
+
code: 'forbid_wrapper',
|
|
130
|
+
path: 'parameters',
|
|
131
|
+
detail: 'Gemini provider payload must not contain a top-level parameters wrapper.'
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
code: 'forbid_wrapper',
|
|
135
|
+
path: 'request',
|
|
136
|
+
detail: 'Gemini provider payload must not contain a nested request wrapper.'
|
|
137
|
+
}
|
|
138
|
+
],
|
|
97
139
|
reservedKeyPrefixes: ['__', '_'],
|
|
98
|
-
flattenWrappers: [
|
|
140
|
+
flattenWrappers: [
|
|
141
|
+
{
|
|
142
|
+
wrapperKey: 'request',
|
|
143
|
+
onlyIfTargetMissing: true
|
|
144
|
+
}
|
|
145
|
+
]
|
|
146
|
+
},
|
|
147
|
+
toolSurface: {
|
|
148
|
+
expectedToolFormat: 'gemini'
|
|
99
149
|
}
|
|
100
150
|
}
|
|
101
151
|
};
|
|
@@ -2,7 +2,9 @@ import { runChatRequestToolFilters } from '../../shared/tool-filter-pipeline.js'
|
|
|
2
2
|
import { ToolGovernanceEngine } from '../tool-governance/index.js';
|
|
3
3
|
import { ensureApplyPatchSchema } from '../../shared/tool-mapping.js';
|
|
4
4
|
import { normalizeApplyPatchToolCallsOnRequest } from '../../shared/tool-governor.js';
|
|
5
|
+
import { clearClockSession, normalizeClockConfig, reserveDueTasksForRequest, startClockDaemonIfNeeded } from '../../../servertool/clock/task-store.js';
|
|
5
6
|
import { isJsonObject } from '../types/json.js';
|
|
7
|
+
import { applyHubOperations } from '../ops/operations.js';
|
|
6
8
|
const toolGovernanceEngine = new ToolGovernanceEngine();
|
|
7
9
|
export async function runHubChatProcess(options) {
|
|
8
10
|
const startTime = Date.now();
|
|
@@ -90,28 +92,22 @@ async function applyRequestToolGovernance(request, context) {
|
|
|
90
92
|
merged.metadata.hasImageAttachment = true;
|
|
91
93
|
}
|
|
92
94
|
if (typeof inboundStreamIntent === 'boolean') {
|
|
93
|
-
merged
|
|
94
|
-
...merged.metadata,
|
|
95
|
-
inboundStream: inboundStreamIntent
|
|
96
|
-
};
|
|
95
|
+
merged = applyHubOperations(merged, buildInboundStreamingOperations(inboundStreamIntent));
|
|
97
96
|
}
|
|
98
97
|
if (typeof governed.stream === 'boolean') {
|
|
99
|
-
merged
|
|
100
|
-
...merged.parameters,
|
|
101
|
-
stream: governed.stream
|
|
102
|
-
};
|
|
98
|
+
merged = applyHubOperations(merged, buildOutboundStreamingOperations(governed.stream));
|
|
103
99
|
}
|
|
104
100
|
if (governed.tool_choice !== undefined) {
|
|
105
|
-
merged
|
|
106
|
-
...merged.parameters,
|
|
107
|
-
tool_choice: governed.tool_choice
|
|
108
|
-
};
|
|
101
|
+
merged = applyHubOperations(merged, buildToolChoiceOperations(governed.tool_choice));
|
|
109
102
|
}
|
|
110
103
|
if (typeof governed.model === 'string' && governed.model.trim()) {
|
|
111
104
|
merged.model = governed.model.trim();
|
|
112
105
|
}
|
|
113
106
|
// Server-side web_search tool injection (config-driven, best-effort).
|
|
114
|
-
merged =
|
|
107
|
+
merged = applyHubOperations(merged, buildWebSearchOperations(merged, metadata));
|
|
108
|
+
// Server-side clock tool + scheduled reminders injection (config-driven, best-effort).
|
|
109
|
+
merged = applyHubOperations(merged, buildClockOperations(metadata));
|
|
110
|
+
merged = await maybeInjectClockRemindersAndApplyDirectives(merged, metadata, context.requestId);
|
|
115
111
|
const { request: sanitized, summary } = toolGovernanceEngine.governRequest(merged, providerProtocol);
|
|
116
112
|
if (summary.applied) {
|
|
117
113
|
sanitized.metadata = {
|
|
@@ -397,18 +393,49 @@ function isRecord(value) {
|
|
|
397
393
|
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
398
394
|
}
|
|
399
395
|
function maybeInjectWebSearchTool(request, metadata) {
|
|
396
|
+
const ops = buildWebSearchOperations(request, metadata);
|
|
397
|
+
if (!ops.length) {
|
|
398
|
+
return request;
|
|
399
|
+
}
|
|
400
|
+
return applyHubOperations(request, ops);
|
|
401
|
+
}
|
|
402
|
+
function buildInboundStreamingOperations(intent) {
|
|
403
|
+
return [
|
|
404
|
+
{
|
|
405
|
+
op: 'set_request_metadata_fields',
|
|
406
|
+
fields: { inboundStream: intent }
|
|
407
|
+
}
|
|
408
|
+
];
|
|
409
|
+
}
|
|
410
|
+
function buildOutboundStreamingOperations(stream) {
|
|
411
|
+
return [
|
|
412
|
+
{
|
|
413
|
+
op: 'set_request_parameter_fields',
|
|
414
|
+
fields: { stream }
|
|
415
|
+
}
|
|
416
|
+
];
|
|
417
|
+
}
|
|
418
|
+
function buildToolChoiceOperations(toolChoice) {
|
|
419
|
+
return [
|
|
420
|
+
{
|
|
421
|
+
op: 'set_request_parameter_fields',
|
|
422
|
+
fields: { tool_choice: toolChoice }
|
|
423
|
+
}
|
|
424
|
+
];
|
|
425
|
+
}
|
|
426
|
+
function buildWebSearchOperations(request, metadata) {
|
|
400
427
|
// ServerTool 二/三跳(serverToolFollowup=true)不再注入 web_search 工具,
|
|
401
428
|
// 以避免在 web_search 流程内部形成循环命中。
|
|
402
429
|
if (metadata.serverToolFollowup === true) {
|
|
403
|
-
return
|
|
430
|
+
return [];
|
|
404
431
|
}
|
|
405
432
|
const rawConfig = metadata.webSearch;
|
|
406
433
|
if (!rawConfig || !Array.isArray(rawConfig.engines) || rawConfig.engines.length === 0) {
|
|
407
|
-
return
|
|
434
|
+
return [];
|
|
408
435
|
}
|
|
409
436
|
const semanticsWebSearch = extractWebSearchSemantics(request.semantics);
|
|
410
437
|
if (semanticsWebSearch?.disable === true) {
|
|
411
|
-
return
|
|
438
|
+
return [];
|
|
412
439
|
}
|
|
413
440
|
const injectPolicy = semanticsWebSearch?.force === true
|
|
414
441
|
? 'always'
|
|
@@ -420,26 +447,10 @@ function maybeInjectWebSearchTool(request, metadata) {
|
|
|
420
447
|
// 仅当当前这一轮用户输入明确表达“联网搜索”意图时才注入 web_search。
|
|
421
448
|
// 不再依赖上一轮工具分类(read/search/websearch),避免形成隐式续写语义。
|
|
422
449
|
if (!intent.hasIntent) {
|
|
423
|
-
return
|
|
450
|
+
return [];
|
|
424
451
|
}
|
|
425
452
|
}
|
|
426
453
|
const existingTools = Array.isArray(request.tools) ? request.tools : [];
|
|
427
|
-
const hasWebSearch = existingTools.some((tool) => {
|
|
428
|
-
if (!tool || typeof tool !== 'object')
|
|
429
|
-
return false;
|
|
430
|
-
const fn = tool.function;
|
|
431
|
-
return typeof fn?.name === 'string' && fn.name.trim() === 'web_search';
|
|
432
|
-
});
|
|
433
|
-
if (hasWebSearch) {
|
|
434
|
-
const nextMetadata = {
|
|
435
|
-
...(request.metadata ?? {}),
|
|
436
|
-
webSearchEnabled: true
|
|
437
|
-
};
|
|
438
|
-
return {
|
|
439
|
-
...request,
|
|
440
|
-
metadata: nextMetadata
|
|
441
|
-
};
|
|
442
|
-
}
|
|
443
454
|
let engines = rawConfig.engines.filter((engine) => typeof engine?.id === 'string' && !!engine.id.trim() && !engine.serverToolsDisabled);
|
|
444
455
|
// 当用户明确要求「谷歌搜索」时,只暴露 Gemini / Antigravity 类搜索后端:
|
|
445
456
|
// - providerKey 以 gemini-cli. 或 antigravity. 开头;
|
|
@@ -461,7 +472,7 @@ function maybeInjectWebSearchTool(request, metadata) {
|
|
|
461
472
|
}
|
|
462
473
|
}
|
|
463
474
|
if (!engines.length) {
|
|
464
|
-
return
|
|
475
|
+
return [];
|
|
465
476
|
}
|
|
466
477
|
const engineIds = engines.map((engine) => engine.id.trim());
|
|
467
478
|
const engineDescriptions = engines
|
|
@@ -511,15 +522,215 @@ function maybeInjectWebSearchTool(request, metadata) {
|
|
|
511
522
|
strict: true
|
|
512
523
|
}
|
|
513
524
|
};
|
|
514
|
-
const
|
|
515
|
-
|
|
516
|
-
|
|
525
|
+
const ops = [
|
|
526
|
+
{
|
|
527
|
+
op: 'set_request_metadata_fields',
|
|
528
|
+
fields: { webSearchEnabled: true }
|
|
529
|
+
}
|
|
530
|
+
];
|
|
531
|
+
ops.push({
|
|
532
|
+
op: 'append_tool_if_missing',
|
|
533
|
+
toolName: 'web_search',
|
|
534
|
+
tool: webSearchTool
|
|
535
|
+
});
|
|
536
|
+
return ops;
|
|
537
|
+
}
|
|
538
|
+
function buildClockOperations(metadata) {
|
|
539
|
+
const rawConfig = metadata.clock;
|
|
540
|
+
const clockConfig = normalizeClockConfig(rawConfig);
|
|
541
|
+
if (!clockConfig) {
|
|
542
|
+
return [];
|
|
543
|
+
}
|
|
544
|
+
const parameters = {
|
|
545
|
+
type: 'object',
|
|
546
|
+
properties: {
|
|
547
|
+
action: {
|
|
548
|
+
type: 'string',
|
|
549
|
+
enum: ['schedule', 'list', 'cancel', 'clear'],
|
|
550
|
+
description: 'Schedule, list, cancel, or clear session-scoped reminders.'
|
|
551
|
+
},
|
|
552
|
+
items: {
|
|
553
|
+
type: 'array',
|
|
554
|
+
description: 'For schedule: list of reminders to add.',
|
|
555
|
+
items: {
|
|
556
|
+
type: 'object',
|
|
557
|
+
properties: {
|
|
558
|
+
dueAt: {
|
|
559
|
+
type: 'string',
|
|
560
|
+
description: 'ISO8601 datetime with timezone (e.g. 2026-01-21T20:30:00-08:00).'
|
|
561
|
+
},
|
|
562
|
+
task: {
|
|
563
|
+
type: 'string',
|
|
564
|
+
description: 'Reminder text (should include which tool to use and what to do).'
|
|
565
|
+
},
|
|
566
|
+
tool: {
|
|
567
|
+
type: 'string',
|
|
568
|
+
description: 'Optional suggested tool name (hint only).'
|
|
569
|
+
},
|
|
570
|
+
arguments: {
|
|
571
|
+
type: 'object',
|
|
572
|
+
description: 'Optional suggested tool arguments (hint only).',
|
|
573
|
+
additionalProperties: true
|
|
574
|
+
}
|
|
575
|
+
},
|
|
576
|
+
required: ['dueAt', 'task'],
|
|
577
|
+
additionalProperties: false
|
|
578
|
+
}
|
|
579
|
+
},
|
|
580
|
+
taskId: {
|
|
581
|
+
type: 'string',
|
|
582
|
+
description: 'For cancel: taskId to remove.'
|
|
583
|
+
}
|
|
584
|
+
},
|
|
585
|
+
required: ['action'],
|
|
586
|
+
additionalProperties: false
|
|
517
587
|
};
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
588
|
+
const clockTool = {
|
|
589
|
+
type: 'function',
|
|
590
|
+
function: {
|
|
591
|
+
name: 'clock',
|
|
592
|
+
description: 'Schedule session-scoped reminders. Use schedule/list/cancel/clear. Scheduled reminders will be injected into future requests as [scheduled task:"..."].',
|
|
593
|
+
parameters,
|
|
594
|
+
strict: true
|
|
595
|
+
}
|
|
522
596
|
};
|
|
597
|
+
return [
|
|
598
|
+
{ op: 'set_request_metadata_fields', fields: { clockEnabled: true, serverToolRequired: true } },
|
|
599
|
+
{ op: 'append_tool_if_missing', toolName: 'clock', tool: clockTool }
|
|
600
|
+
];
|
|
601
|
+
}
|
|
602
|
+
function resolveSessionIdForClock(metadata, request) {
|
|
603
|
+
const candidate = readString(metadata.sessionId) ?? readString(request.metadata?.sessionId);
|
|
604
|
+
return candidate && candidate.trim() ? candidate.trim() : null;
|
|
605
|
+
}
|
|
606
|
+
function stripClockClearDirectiveFromText(text) {
|
|
607
|
+
const pattern = /<\*\*\s*clock\s*:\s*clear\s*\*\*>/gi;
|
|
608
|
+
const hadClear = pattern.test(text);
|
|
609
|
+
if (!hadClear) {
|
|
610
|
+
return { hadClear: false, next: text };
|
|
611
|
+
}
|
|
612
|
+
const replaced = text.replace(pattern, '');
|
|
613
|
+
// Clean up leftover excessive blank lines to keep prompts tidy.
|
|
614
|
+
const next = replaced.replace(/\n{3,}/g, '\n\n').trim();
|
|
615
|
+
return { hadClear: true, next };
|
|
616
|
+
}
|
|
617
|
+
function stripClockClearDirectiveFromContent(content) {
|
|
618
|
+
if (typeof content === 'string') {
|
|
619
|
+
const { hadClear, next } = stripClockClearDirectiveFromText(content);
|
|
620
|
+
return { hadClear, next };
|
|
621
|
+
}
|
|
622
|
+
if (Array.isArray(content)) {
|
|
623
|
+
let hadClear = false;
|
|
624
|
+
const next = content.map((part) => {
|
|
625
|
+
if (typeof part === 'string') {
|
|
626
|
+
const stripped = stripClockClearDirectiveFromText(part);
|
|
627
|
+
if (stripped.hadClear)
|
|
628
|
+
hadClear = true;
|
|
629
|
+
return stripped.next;
|
|
630
|
+
}
|
|
631
|
+
if (part && typeof part === 'object' && !Array.isArray(part)) {
|
|
632
|
+
const block = part;
|
|
633
|
+
const text = typeof block.text === 'string' ? block.text : undefined;
|
|
634
|
+
if (!text)
|
|
635
|
+
return part;
|
|
636
|
+
const stripped = stripClockClearDirectiveFromText(text);
|
|
637
|
+
if (stripped.hadClear)
|
|
638
|
+
hadClear = true;
|
|
639
|
+
return { ...block, text: stripped.next };
|
|
640
|
+
}
|
|
641
|
+
return part;
|
|
642
|
+
});
|
|
643
|
+
return { hadClear, next };
|
|
644
|
+
}
|
|
645
|
+
return { hadClear: false, next: content };
|
|
646
|
+
}
|
|
647
|
+
function findLastUserMessageIndex(messages) {
|
|
648
|
+
if (!Array.isArray(messages) || messages.length === 0) {
|
|
649
|
+
return -1;
|
|
650
|
+
}
|
|
651
|
+
for (let idx = messages.length - 1; idx >= 0; idx -= 1) {
|
|
652
|
+
const candidate = messages[idx];
|
|
653
|
+
if (candidate && candidate.role === 'user') {
|
|
654
|
+
return idx;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
return -1;
|
|
658
|
+
}
|
|
659
|
+
async function maybeInjectClockRemindersAndApplyDirectives(request, metadata, requestId) {
|
|
660
|
+
const rawConfig = metadata.clock;
|
|
661
|
+
const clockConfig = normalizeClockConfig(rawConfig);
|
|
662
|
+
if (!clockConfig) {
|
|
663
|
+
return request;
|
|
664
|
+
}
|
|
665
|
+
try {
|
|
666
|
+
await startClockDaemonIfNeeded(clockConfig);
|
|
667
|
+
}
|
|
668
|
+
catch {
|
|
669
|
+
// best-effort
|
|
670
|
+
}
|
|
671
|
+
const sessionId = resolveSessionIdForClock(metadata, request);
|
|
672
|
+
const messages = Array.isArray(request.messages) ? request.messages : [];
|
|
673
|
+
const lastUserIdx = findLastUserMessageIndex(messages);
|
|
674
|
+
// 1) Apply <**clock:clear**> directive (latest user message only).
|
|
675
|
+
let hadClear = false;
|
|
676
|
+
let nextMessages = messages;
|
|
677
|
+
if (lastUserIdx >= 0) {
|
|
678
|
+
const lastUser = messages[lastUserIdx];
|
|
679
|
+
const stripped = stripClockClearDirectiveFromContent(lastUser.content);
|
|
680
|
+
hadClear = stripped.hadClear;
|
|
681
|
+
if (hadClear) {
|
|
682
|
+
nextMessages = messages.slice();
|
|
683
|
+
nextMessages[lastUserIdx] = { ...lastUser, content: stripped.next };
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
if (hadClear) {
|
|
687
|
+
if (sessionId) {
|
|
688
|
+
try {
|
|
689
|
+
await clearClockSession(sessionId);
|
|
690
|
+
}
|
|
691
|
+
catch {
|
|
692
|
+
// best-effort: user directive should not crash request
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
return { ...request, messages: nextMessages };
|
|
696
|
+
}
|
|
697
|
+
// 2) Inject due reminders as a system message + attach reservation for response-side commit.
|
|
698
|
+
if (!sessionId) {
|
|
699
|
+
return request;
|
|
700
|
+
}
|
|
701
|
+
try {
|
|
702
|
+
const { reservation, injectText } = await reserveDueTasksForRequest({
|
|
703
|
+
reservationId: `${requestId}:clock`,
|
|
704
|
+
sessionId,
|
|
705
|
+
config: clockConfig
|
|
706
|
+
});
|
|
707
|
+
if (!reservation || typeof injectText !== 'string' || !injectText.trim()) {
|
|
708
|
+
return request;
|
|
709
|
+
}
|
|
710
|
+
const baseMetadata = request.metadata && typeof request.metadata === 'object'
|
|
711
|
+
? request.metadata
|
|
712
|
+
: {
|
|
713
|
+
originalEndpoint: readString(metadata.originalEndpoint) ?? '/v1/chat/completions'
|
|
714
|
+
};
|
|
715
|
+
return {
|
|
716
|
+
...request,
|
|
717
|
+
messages: [
|
|
718
|
+
...messages,
|
|
719
|
+
{
|
|
720
|
+
role: 'system',
|
|
721
|
+
content: injectText.trim()
|
|
722
|
+
}
|
|
723
|
+
],
|
|
724
|
+
metadata: {
|
|
725
|
+
...baseMetadata,
|
|
726
|
+
__clockReservation: reservation
|
|
727
|
+
}
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
catch {
|
|
731
|
+
// best-effort: never break request due to reminder injection failures
|
|
732
|
+
return request;
|
|
733
|
+
}
|
|
523
734
|
}
|
|
524
735
|
function extractWebSearchSemantics(semantics) {
|
|
525
736
|
if (!semantics || typeof semantics !== 'object') {
|