@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,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'
@@ -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 observeProviderOutboundPayload(providerProtocol, payload) {
103
+ function observeProviderPayload(options) {
87
104
  const violations = [];
88
- // V0 (observe-only): detect known "layout anti-patterns" and reserved keys.
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: 'provider_outbound',
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 = observeProviderOutboundPayload(options.providerProtocol, options.payload);
140
- observation.phase = phase;
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: false,
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: false,
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: false,
96
- forbidWrappers: [],
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.metadata = {
94
- ...merged.metadata,
95
- inboundStream: inboundStreamIntent
96
- };
95
+ merged = applyHubOperations(merged, buildInboundStreamingOperations(inboundStreamIntent));
97
96
  }
98
97
  if (typeof governed.stream === 'boolean') {
99
- merged.parameters = {
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.parameters = {
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 = maybeInjectWebSearchTool(merged, metadata);
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 request;
430
+ return [];
404
431
  }
405
432
  const rawConfig = metadata.webSearch;
406
433
  if (!rawConfig || !Array.isArray(rawConfig.engines) || rawConfig.engines.length === 0) {
407
- return request;
434
+ return [];
408
435
  }
409
436
  const semanticsWebSearch = extractWebSearchSemantics(request.semantics);
410
437
  if (semanticsWebSearch?.disable === true) {
411
- return request;
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 request;
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 request;
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 nextMetadata = {
515
- ...(request.metadata ?? {}),
516
- webSearchEnabled: true
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
- return {
519
- ...request,
520
- metadata: nextMetadata,
521
- tools: [...existingTools, webSearchTool]
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') {