@jsonstudio/rcc 0.89.1136 → 0.89.1205

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 (145) hide show
  1. package/dist/build-info.js +2 -2
  2. package/dist/cli/commands/clean.d.ts +16 -0
  3. package/dist/cli/commands/clean.js +58 -0
  4. package/dist/cli/commands/clean.js.map +1 -0
  5. package/dist/cli/commands/code.d.ts +55 -0
  6. package/dist/cli/commands/code.js +376 -0
  7. package/dist/cli/commands/code.js.map +1 -0
  8. package/dist/cli/commands/config.d.ts +31 -0
  9. package/dist/cli/commands/config.js +168 -0
  10. package/dist/cli/commands/config.js.map +1 -0
  11. package/dist/cli/commands/env.d.ts +20 -0
  12. package/dist/cli/commands/env.js +73 -0
  13. package/dist/cli/commands/env.js.map +1 -0
  14. package/dist/cli/commands/examples.d.ts +5 -0
  15. package/dist/cli/commands/examples.js +66 -0
  16. package/dist/cli/commands/examples.js.map +1 -0
  17. package/dist/cli/commands/port.d.ts +24 -0
  18. package/dist/cli/commands/port.js +85 -0
  19. package/dist/cli/commands/port.js.map +1 -0
  20. package/dist/cli/commands/restart.d.ts +50 -0
  21. package/dist/cli/commands/restart.js +176 -0
  22. package/dist/cli/commands/restart.js.map +1 -0
  23. package/dist/cli/commands/start.d.ts +68 -0
  24. package/dist/cli/commands/start.js +295 -0
  25. package/dist/cli/commands/start.js.map +1 -0
  26. package/dist/cli/commands/status.d.ts +16 -0
  27. package/dist/cli/commands/status.js +104 -0
  28. package/dist/cli/commands/status.js.map +1 -0
  29. package/dist/cli/commands/stop.d.ts +35 -0
  30. package/dist/cli/commands/stop.js +95 -0
  31. package/dist/cli/commands/stop.js.map +1 -0
  32. package/dist/cli/logger.d.ts +8 -0
  33. package/dist/cli/logger.js +9 -0
  34. package/dist/cli/logger.js.map +1 -0
  35. package/dist/cli/main.d.ts +6 -0
  36. package/dist/cli/main.js +16 -0
  37. package/dist/cli/main.js.map +1 -0
  38. package/dist/cli/program.d.ts +8 -0
  39. package/dist/cli/program.js +16 -0
  40. package/dist/cli/program.js.map +1 -0
  41. package/dist/cli/register/basic-commands.d.ts +30 -0
  42. package/dist/cli/register/basic-commands.js +11 -0
  43. package/dist/cli/register/basic-commands.js.map +1 -0
  44. package/dist/cli/register/code-command.d.ts +3 -0
  45. package/dist/cli/register/code-command.js +5 -0
  46. package/dist/cli/register/code-command.js.map +1 -0
  47. package/dist/cli/register/restart-command.d.ts +3 -0
  48. package/dist/cli/register/restart-command.js +5 -0
  49. package/dist/cli/register/restart-command.js.map +1 -0
  50. package/dist/cli/register/start-command.d.ts +3 -0
  51. package/dist/cli/register/start-command.js +5 -0
  52. package/dist/cli/register/start-command.js.map +1 -0
  53. package/dist/cli/register/status-config-commands.d.ts +16 -0
  54. package/dist/cli/register/status-config-commands.js +7 -0
  55. package/dist/cli/register/status-config-commands.js.map +1 -0
  56. package/dist/cli/register/stop-command.d.ts +3 -0
  57. package/dist/cli/register/stop-command.js +5 -0
  58. package/dist/cli/register/stop-command.js.map +1 -0
  59. package/dist/cli/runtime.d.ts +5 -0
  60. package/dist/cli/runtime.js +11 -0
  61. package/dist/cli/runtime.js.map +1 -0
  62. package/dist/cli/server/port-utils.d.ts +52 -0
  63. package/dist/cli/server/port-utils.js +193 -0
  64. package/dist/cli/server/port-utils.js.map +1 -0
  65. package/dist/cli/spinner.d.ts +10 -0
  66. package/dist/cli/spinner.js +59 -0
  67. package/dist/cli/spinner.js.map +1 -0
  68. package/dist/cli/utils/normalize.d.ts +2 -0
  69. package/dist/cli/utils/normalize.js +22 -0
  70. package/dist/cli/utils/normalize.js.map +1 -0
  71. package/dist/cli/utils/safe-read-json.d.ts +1 -0
  72. package/dist/cli/utils/safe-read-json.js +11 -0
  73. package/dist/cli/utils/safe-read-json.js.map +1 -0
  74. package/dist/cli.js +148 -1775
  75. package/dist/cli.js.map +1 -1
  76. package/dist/client/anthropic/anthropic-protocol-client.js +4 -3
  77. package/dist/client/anthropic/anthropic-protocol-client.js.map +1 -1
  78. package/dist/client/gemini-cli/gemini-cli-protocol-client.d.ts +1 -1
  79. package/dist/client/gemini-cli/gemini-cli-protocol-client.js +10 -3
  80. package/dist/client/gemini-cli/gemini-cli-protocol-client.js.map +1 -1
  81. package/dist/commands/quota-daemon.js +2 -2
  82. package/dist/commands/quota-daemon.js.map +1 -1
  83. package/dist/config/provider-v2-loader.js +4 -2
  84. package/dist/config/provider-v2-loader.js.map +1 -1
  85. package/dist/manager/modules/quota/index.js +21 -4
  86. package/dist/manager/modules/quota/index.js.map +1 -1
  87. package/dist/manager/modules/routing/index.js.map +1 -1
  88. package/dist/manager/storage/file-store.js +1 -1
  89. package/dist/manager/storage/file-store.js.map +1 -1
  90. package/dist/modules/llmswitch/bridge.js +45 -1
  91. package/dist/modules/llmswitch/bridge.js.map +1 -1
  92. package/dist/providers/auth/oauth-lifecycle.js +2 -2
  93. package/dist/providers/auth/oauth-lifecycle.js.map +1 -1
  94. package/dist/providers/core/api/provider-config.d.ts +2 -0
  95. package/dist/providers/core/api/provider-types.d.ts +2 -0
  96. package/dist/providers/core/runtime/base-provider.js +21 -27
  97. package/dist/providers/core/runtime/base-provider.js.map +1 -1
  98. package/dist/providers/core/runtime/gemini-cli-http-provider.d.ts +1 -0
  99. package/dist/providers/core/runtime/gemini-cli-http-provider.js +37 -5
  100. package/dist/providers/core/runtime/gemini-cli-http-provider.js.map +1 -1
  101. package/dist/providers/core/runtime/http-request-executor.js +23 -29
  102. package/dist/providers/core/runtime/http-request-executor.js.map +1 -1
  103. package/dist/providers/core/runtime/http-transport-provider.js +20 -0
  104. package/dist/providers/core/runtime/http-transport-provider.js.map +1 -1
  105. package/dist/providers/core/utils/http-client.d.ts +9 -0
  106. package/dist/providers/core/utils/http-client.js +9 -11
  107. package/dist/providers/core/utils/http-client.js.map +1 -1
  108. package/dist/providers/core/utils/provider-error-reporter.js +2 -6
  109. package/dist/providers/core/utils/provider-error-reporter.js.map +1 -1
  110. package/dist/providers/mock/mock-provider-runtime.js +19 -5
  111. package/dist/providers/mock/mock-provider-runtime.js.map +1 -1
  112. package/dist/server/runtime/http-server/hub-shadow-compare.d.ts +18 -0
  113. package/dist/server/runtime/http-server/hub-shadow-compare.js +180 -0
  114. package/dist/server/runtime/http-server/hub-shadow-compare.js.map +1 -0
  115. package/dist/server/runtime/http-server/index.d.ts +4 -0
  116. package/dist/server/runtime/http-server/index.js +202 -11
  117. package/dist/server/runtime/http-server/index.js.map +1 -1
  118. package/dist/server/runtime/http-server/request-executor.js +9 -1
  119. package/dist/server/runtime/http-server/request-executor.js.map +1 -1
  120. package/dist/server/runtime/http-server/routes.js +8 -4
  121. package/dist/server/runtime/http-server/routes.js.map +1 -1
  122. package/dist/server/runtime/http-server/stats-manager.js +9 -3
  123. package/dist/server/runtime/http-server/stats-manager.js.map +1 -1
  124. package/dist/utils/errorsamples.d.ts +5 -0
  125. package/dist/utils/errorsamples.js +27 -0
  126. package/dist/utils/errorsamples.js.map +1 -0
  127. package/dist/utils/runtime-versions.d.ts +1 -0
  128. package/dist/utils/runtime-versions.js +38 -0
  129. package/dist/utils/runtime-versions.js.map +1 -0
  130. package/package.json +10 -4
  131. package/scripts/anthropic-compare-modes.mjs +40 -3
  132. package/scripts/antigravity-smoke.mjs +180 -0
  133. package/scripts/backfill-apply-patch-exec-errorsamples.mjs +225 -0
  134. package/scripts/compare-codex-rccx.mjs +59 -1
  135. package/scripts/compare-responses-request.mjs +50 -4
  136. package/scripts/lib/errorsamples.mjs +23 -0
  137. package/scripts/mock-provider/run-regressions.mjs +12 -2
  138. package/scripts/policy-violations-report.mjs +257 -0
  139. package/scripts/publish-rcc.mjs +16 -2
  140. package/scripts/tests/unified-hub-responses-enforce-safe.mjs +37 -0
  141. package/scripts/tests/unified-hub-shadow-regression.mjs +55 -0
  142. package/scripts/unified-hub-shadow-compare.mjs +359 -0
  143. package/scripts/verify-e2e-gemini-followup-sample.mjs +269 -0
  144. package/scripts/virtual-router-shadow-v2-real.mjs +71 -1
  145. package/scripts/virtual-router-shadow-v2.mjs +41 -0
@@ -0,0 +1,359 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Unified Hub Framework V1 – Black-box shadow compare
4
+ *
5
+ * Runs the llmswitch-core HubPipeline twice (baseline vs candidate) on the same
6
+ * input payload and diffs providerPayload + key metadata.
7
+ *
8
+ * This is the gating tool for gradual rollout:
9
+ * off → shadow(observe) → enforce → widen
10
+ */
11
+
12
+ import fs from 'node:fs';
13
+ import path from 'node:path';
14
+ import url from 'node:url';
15
+
16
+ function resolveErrorSamplesRoot() {
17
+ const envOverride =
18
+ process.env.ROUTECODEX_ERRORSAMPLES_DIR ||
19
+ process.env.ROUTECODEX_ERROR_SAMPLES_DIR;
20
+ if (envOverride && String(envOverride).trim()) {
21
+ return path.resolve(String(envOverride).trim());
22
+ }
23
+ const home = process.env.HOME || process.env.USERPROFILE || '';
24
+ return path.join(home, '.routecodex', 'errorsamples');
25
+ }
26
+
27
+ function ensureDirSync(dir) {
28
+ try {
29
+ fs.mkdirSync(dir, { recursive: true });
30
+ } catch {
31
+ // ignore
32
+ }
33
+ }
34
+
35
+ function usage() {
36
+ console.log(`Usage:
37
+ node scripts/unified-hub-shadow-compare.mjs --request <file.json> [options]
38
+
39
+ Options:
40
+ --request <file.json> can be a raw request body JSON OR a codex-samples client-request.json
41
+ --entry-endpoint <path> default: /v1/chat/completions
42
+ --route-hint <name> optional: select route (default/openai/responses/anthropic/gemini)
43
+ --baseline-mode <off|observe|enforce> default: off
44
+ --candidate-mode <off|observe|enforce> default: observe
45
+ --help show help
46
+ `);
47
+ }
48
+
49
+ function parseArgs() {
50
+ const args = process.argv.slice(2);
51
+ const opts = {
52
+ entryEndpoint: '/v1/chat/completions',
53
+ baselineMode: 'off',
54
+ candidateMode: 'observe',
55
+ routeHint: undefined
56
+ };
57
+ for (let i = 0; i < args.length; i += 1) {
58
+ const a = args[i];
59
+ if (a === '--request') opts.request = args[++i];
60
+ else if (a === '--entry-endpoint') opts.entryEndpoint = args[++i];
61
+ else if (a === '--route-hint') opts.routeHint = args[++i];
62
+ else if (a === '--baseline-mode') opts.baselineMode = args[++i];
63
+ else if (a === '--candidate-mode') opts.candidateMode = args[++i];
64
+ else if (a === '--help' || a === '-h') {
65
+ usage();
66
+ process.exit(0);
67
+ } else {
68
+ console.error(`Unknown arg: ${a}`);
69
+ usage();
70
+ process.exit(2);
71
+ }
72
+ }
73
+ if (!opts.request) {
74
+ usage();
75
+ process.exit(2);
76
+ }
77
+ return opts;
78
+ }
79
+
80
+ function normalizeEntryProviderProtocol(entryEndpoint) {
81
+ const lowered = String(entryEndpoint || '').toLowerCase();
82
+ if (lowered.includes('/v1/responses')) return 'openai-responses';
83
+ if (lowered.includes('/v1/messages')) return 'anthropic-messages';
84
+ return 'openai-chat';
85
+ }
86
+
87
+ function readJson(file) {
88
+ return JSON.parse(fs.readFileSync(path.resolve(file), 'utf8'));
89
+ }
90
+
91
+ function extractEndpointFromSample(doc) {
92
+ return doc?.data?.url || doc?.url || doc?.endpoint || undefined;
93
+ }
94
+
95
+ function extractRouteHintFromSample(doc) {
96
+ const headers = doc?.headers;
97
+ if (!headers || typeof headers !== 'object') return undefined;
98
+ const hint = headers['X-Route-Hint'] || headers['x-route-hint'];
99
+ return typeof hint === 'string' && hint.trim() ? hint.trim() : undefined;
100
+ }
101
+
102
+ function extractBodyFromSample(doc) {
103
+ // Keep in sync with scripts/replay-codex-sample.mjs
104
+ const bodyNode = doc?.data?.body || doc?.body;
105
+ if (!bodyNode) {
106
+ if (typeof doc?.data?.data === 'object') return doc.data.data;
107
+ if (typeof doc?.body?.data === 'object') return doc.body.data;
108
+ return undefined;
109
+ }
110
+ if (typeof bodyNode?.body === 'object') return bodyNode.body;
111
+ if (typeof bodyNode === 'object') return bodyNode;
112
+ if (typeof doc?.data?.data === 'object') return doc.data.data;
113
+ if (typeof doc?.body?.data === 'object') return doc.body.data;
114
+ return undefined;
115
+ }
116
+
117
+ function readRequestPayloadAndHints(file) {
118
+ const doc = readJson(file);
119
+ const extracted = extractBodyFromSample(doc);
120
+ return {
121
+ payload: extracted && typeof extracted === 'object' ? extracted : doc,
122
+ entryEndpoint: extractEndpointFromSample(doc),
123
+ routeHint: extractRouteHintFromSample(doc)
124
+ };
125
+ }
126
+
127
+ function stableStringify(value) {
128
+ return JSON.stringify(
129
+ value,
130
+ (key, val) => {
131
+ if (val && typeof val === 'object' && !Array.isArray(val)) {
132
+ const out = {};
133
+ for (const k of Object.keys(val).sort()) {
134
+ out[k] = val[k];
135
+ }
136
+ return out;
137
+ }
138
+ return val;
139
+ },
140
+ 2
141
+ );
142
+ }
143
+
144
+ function diffPayloads(expected, actual, p = '<root>') {
145
+ if (Object.is(expected, actual)) return [];
146
+ if (typeof expected !== typeof actual) {
147
+ return [{ path: p, expected, actual }];
148
+ }
149
+ if (Array.isArray(expected) && Array.isArray(actual)) {
150
+ const max = Math.max(expected.length, actual.length);
151
+ const diffs = [];
152
+ for (let i = 0; i < max; i += 1) {
153
+ diffs.push(...diffPayloads(expected[i], actual[i], `${p}[${i}]`));
154
+ }
155
+ return diffs;
156
+ }
157
+ if (expected && typeof expected === 'object' && actual && typeof actual === 'object') {
158
+ const keys = new Set([...Object.keys(expected), ...Object.keys(actual)]);
159
+ const diffs = [];
160
+ for (const key of keys) {
161
+ const next = p === '<root>' ? key : `${p}.${key}`;
162
+ if (!(key in actual)) diffs.push({ path: next, expected: expected[key], actual: undefined });
163
+ else if (!(key in expected)) diffs.push({ path: next, expected: undefined, actual: actual[key] });
164
+ else diffs.push(...diffPayloads(expected[key], actual[key], next));
165
+ }
166
+ return diffs;
167
+ }
168
+ return [{ path: p, expected, actual }];
169
+ }
170
+
171
+ function buildVirtualRouterConfig() {
172
+ // Minimal deterministic routes. We only need routing to select an outbound providerType/protocol.
173
+ return {
174
+ providers: {
175
+ mockOpenai: {
176
+ id: 'mockOpenai',
177
+ enabled: true,
178
+ type: 'openai',
179
+ baseURL: 'mock://openai',
180
+ auth: { type: 'apikey', apiKey: 'mock' },
181
+ models: { 'gpt-test': {} }
182
+ },
183
+ mockResponses: {
184
+ id: 'mockResponses',
185
+ enabled: true,
186
+ type: 'responses',
187
+ baseURL: 'mock://responses',
188
+ auth: { type: 'apikey', apiKey: 'mock' },
189
+ models: { 'gpt-test': {} }
190
+ },
191
+ mockAnthropic: {
192
+ id: 'mockAnthropic',
193
+ enabled: true,
194
+ type: 'anthropic',
195
+ baseURL: 'mock://anthropic',
196
+ auth: { type: 'apikey', apiKey: 'mock' },
197
+ models: { 'gpt-test': {} }
198
+ },
199
+ mockGemini: {
200
+ id: 'mockGemini',
201
+ enabled: true,
202
+ type: 'gemini',
203
+ baseURL: 'mock://gemini',
204
+ auth: { type: 'apikey', apiKey: 'mock' },
205
+ models: { 'gpt-test': {} }
206
+ }
207
+ },
208
+ routing: {
209
+ default: [
210
+ { id: 'default-primary', targets: ['mockOpenai.gpt-test'] }
211
+ ],
212
+ openai: [
213
+ { id: 'openai-primary', targets: ['mockOpenai.gpt-test'] }
214
+ ],
215
+ responses: [
216
+ { id: 'responses-primary', targets: ['mockResponses.gpt-test'] }
217
+ ],
218
+ anthropic: [
219
+ { id: 'anthropic-primary', targets: ['mockAnthropic.gpt-test'] }
220
+ ],
221
+ gemini: [
222
+ { id: 'gemini-primary', targets: ['mockGemini.gpt-test'] }
223
+ ]
224
+ }
225
+ };
226
+ }
227
+
228
+ async function importHubPipelineCtor() {
229
+ // Use the locally linked @jsonstudio/llms (symlinked to sharedmodule/llmswitch-core).
230
+ const repoRoot = path.resolve(path.dirname(url.fileURLToPath(import.meta.url)), '..');
231
+ const hubPath = path.join(repoRoot, 'sharedmodule', 'llmswitch-core', 'dist', 'conversion', 'hub', 'pipeline', 'hub-pipeline.js');
232
+ const mod = await import(url.pathToFileURL(hubPath).href);
233
+ if (typeof mod.HubPipeline !== 'function') {
234
+ throw new Error('HubPipeline ctor not found in built llmswitch-core dist. Run `npm run llmswitch:build` or `cd sharedmodule/llmswitch-core && npm run build`.');
235
+ }
236
+ return mod.HubPipeline;
237
+ }
238
+
239
+ async function bootstrapVirtualRouterConfig(rawConfig) {
240
+ const repoRoot = path.resolve(path.dirname(url.fileURLToPath(import.meta.url)), '..');
241
+ const bootstrapPath = path.join(
242
+ repoRoot,
243
+ 'sharedmodule',
244
+ 'llmswitch-core',
245
+ 'dist',
246
+ 'router',
247
+ 'virtual-router',
248
+ 'bootstrap.js'
249
+ );
250
+ const mod = await import(url.pathToFileURL(bootstrapPath).href);
251
+ if (typeof mod.bootstrapVirtualRouterConfig !== 'function') {
252
+ throw new Error('bootstrapVirtualRouterConfig not found in built llmswitch-core dist.');
253
+ }
254
+ return mod.bootstrapVirtualRouterConfig(rawConfig);
255
+ }
256
+
257
+ async function runOnce({ requestId, mode, entryEndpoint, routeHint, payload }) {
258
+ const HubPipeline = await importHubPipelineCtor();
259
+ const artifacts = await bootstrapVirtualRouterConfig(buildVirtualRouterConfig());
260
+ const pipeline = new HubPipeline({
261
+ virtualRouter: artifacts.config,
262
+ policy: { mode }
263
+ });
264
+ const providerProtocol = normalizeEntryProviderProtocol(entryEndpoint);
265
+ const result = await pipeline.execute({
266
+ id: requestId,
267
+ endpoint: entryEndpoint,
268
+ payload,
269
+ metadata: {
270
+ entryEndpoint,
271
+ providerProtocol,
272
+ routeHint
273
+ }
274
+ });
275
+ return result;
276
+ }
277
+
278
+ function writeCompareErrorSample(opts) {
279
+ const root = resolveErrorSamplesRoot();
280
+ if (!root || !String(root).trim()) return;
281
+ const dir = path.join(root, 'unified-hub-shadow');
282
+ ensureDirSync(dir);
283
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
284
+ const file = path.join(dir, `unified-hub-shadow-diff-${stamp}.json`);
285
+ try {
286
+ fs.writeFileSync(file, JSON.stringify(opts, null, 2), 'utf8');
287
+ console.error(`[unified-hub-shadow-compare] wrote errorsample: ${file}`);
288
+ } catch {
289
+ // ignore
290
+ }
291
+ }
292
+
293
+ async function main() {
294
+ const opts = parseArgs();
295
+ const loaded = readRequestPayloadAndHints(opts.request);
296
+ const payload = loaded.payload;
297
+ const entryEndpoint = String(opts.entryEndpoint || loaded.entryEndpoint || '/v1/chat/completions');
298
+ const routeHint = opts.routeHint ? String(opts.routeHint) : (loaded.routeHint || undefined);
299
+ const baselineMode = String(opts.baselineMode);
300
+ const candidateMode = String(opts.candidateMode);
301
+ const requestId = `shadow_unified_hub_${Date.now()}`;
302
+
303
+ const baseline = await runOnce({ requestId, mode: baselineMode, entryEndpoint, routeHint, payload });
304
+ const candidate = await runOnce({ requestId, mode: candidateMode, entryEndpoint, routeHint, payload });
305
+
306
+ const baselineOut = {
307
+ providerPayload: baseline.providerPayload,
308
+ target: baseline.target,
309
+ metadata: {
310
+ entryEndpoint: baseline.metadata?.entryEndpoint,
311
+ providerProtocol: baseline.metadata?.providerProtocol,
312
+ processMode: baseline.metadata?.processMode,
313
+ stream: baseline.metadata?.stream,
314
+ routeHint: baseline.metadata?.routeHint
315
+ }
316
+ };
317
+ const candidateOut = {
318
+ providerPayload: candidate.providerPayload,
319
+ target: candidate.target,
320
+ metadata: {
321
+ entryEndpoint: candidate.metadata?.entryEndpoint,
322
+ providerProtocol: candidate.metadata?.providerProtocol,
323
+ processMode: candidate.metadata?.processMode,
324
+ stream: candidate.metadata?.stream,
325
+ routeHint: candidate.metadata?.routeHint
326
+ }
327
+ };
328
+
329
+ const diffs = diffPayloads(baselineOut, candidateOut);
330
+ if (!diffs.length) {
331
+ console.log('[unified-hub-shadow-compare] OK diff=0');
332
+ return;
333
+ }
334
+ console.error(`[unified-hub-shadow-compare] DIFF count=${diffs.length} (showing first 80)`);
335
+ diffs.slice(0, 80).forEach((d) => {
336
+ console.error(`- ${d.path}`);
337
+ });
338
+ writeCompareErrorSample({
339
+ kind: 'unified-hub-shadow-diff',
340
+ timestamp: new Date().toISOString(),
341
+ requestFile: path.resolve(opts.request),
342
+ entryEndpoint,
343
+ routeHint,
344
+ baselineMode,
345
+ candidateMode,
346
+ diffCount: diffs.length,
347
+ diffPaths: diffs.slice(0, 200).map((d) => d.path),
348
+ baseline: baselineOut,
349
+ candidate: candidateOut
350
+ });
351
+ console.error('\n--- baseline (stable) ---\n' + stableStringify(baselineOut));
352
+ console.error('\n--- candidate (stable) ---\n' + stableStringify(candidateOut));
353
+ process.exitCode = 1;
354
+ }
355
+
356
+ main().catch((err) => {
357
+ console.error('[unified-hub-shadow-compare] failed:', err);
358
+ process.exit(1);
359
+ });
@@ -0,0 +1,269 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * E2E (real upstream) regression:
4
+ * - Replays a known-large OpenAI Responses payload that previously triggered
5
+ * Gemini `MALFORMED_FUNCTION_CALL` (history tool args not aligned to schema),
6
+ * causing empty reply + SERVERTOOL_EMPTY_FOLLOWUP (502).
7
+ *
8
+ * Safety: this hits real Antigravity/Gemini upstream and requires local auth.
9
+ * Default: skipped unless ROUTECODEX_VERIFY_ANTIGRAVITY=1.
10
+ */
11
+
12
+ import { spawn } from 'node:child_process';
13
+ import fs from 'node:fs';
14
+ import os from 'node:os';
15
+ import path from 'node:path';
16
+
17
+ if (String(process.env.ROUTECODEX_VERIFY_ANTIGRAVITY || '').trim() !== '1') {
18
+ console.log('[verify:e2e-gemini-followup] skip (set ROUTECODEX_VERIFY_ANTIGRAVITY=1 to enable)');
19
+ process.exit(0);
20
+ }
21
+
22
+ const VERIFY_PORT = process.env.ROUTECODEX_VERIFY_PORT || '5582';
23
+ const VERIFY_BASE = process.env.ROUTECODEX_VERIFY_BASE_URL || `http://127.0.0.1:${VERIFY_PORT}`;
24
+ const VERIFY_CONFIG =
25
+ process.env.ROUTECODEX_VERIFY_CONFIG ||
26
+ process.env.ROUTECODEX_CONFIG_PATH ||
27
+ `${process.env.HOME || ''}/.routecodex/config.json`;
28
+
29
+ const DEFAULT_SAMPLE = path.join(
30
+ os.homedir(),
31
+ '.routecodex',
32
+ 'codex-samples',
33
+ 'openai-responses',
34
+ 'antigravity.geetasamodgeetasamoda.gemini-3-pro-high',
35
+ 'req_1768707507351_30f4e503',
36
+ 'client-request.json'
37
+ );
38
+
39
+ function readJson(file) {
40
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
41
+ }
42
+
43
+ function readServerApiKeyFromConfig(configPath) {
44
+ try {
45
+ const raw = fs.readFileSync(configPath, 'utf8');
46
+ const json = raw && raw.trim() ? JSON.parse(raw) : {};
47
+ const apikey = json?.httpserver?.apikey;
48
+ return typeof apikey === 'string' && apikey.trim() ? apikey.trim() : '';
49
+ } catch {
50
+ return '';
51
+ }
52
+ }
53
+
54
+ function buildAuthHeaders(serverApiKey) {
55
+ if (!serverApiKey) return {};
56
+ // middleware accepts x-api-key and many aliases; keep it simple.
57
+ return { 'x-api-key': serverApiKey };
58
+ }
59
+
60
+ function extractResponsesBody(sampleDoc) {
61
+ // common snapshot shapes:
62
+ // - { body: { body: <responses payload>, metadata: ... }, headers: ..., meta: ... }
63
+ // - { data: { body: <responses payload> } }
64
+ // - <responses payload>
65
+ const bodyNode = sampleDoc?.data?.body ?? sampleDoc?.body ?? sampleDoc;
66
+ if (bodyNode && typeof bodyNode === 'object' && typeof bodyNode.body === 'object' && bodyNode.body) {
67
+ return bodyNode.body;
68
+ }
69
+ if (bodyNode && typeof bodyNode === 'object') {
70
+ return bodyNode;
71
+ }
72
+ return undefined;
73
+ }
74
+
75
+ async function waitForServer(timeoutMs = 45_000) {
76
+ const start = Date.now();
77
+ while (Date.now() - start < timeoutMs) {
78
+ try {
79
+ const res = await fetch(`${VERIFY_BASE}/health`);
80
+ if (res.ok) return;
81
+ } catch {
82
+ // ignore
83
+ }
84
+ await new Promise((resolve) => setTimeout(resolve, 500));
85
+ }
86
+ throw new Error(`[verify:e2e-gemini-followup] server health timeout: ${VERIFY_BASE}/health`);
87
+ }
88
+
89
+ async function readSse(response) {
90
+ const reader = response.body?.getReader();
91
+ if (!reader) throw new Error('Response is not streamable');
92
+ const decoder = new TextDecoder();
93
+ let buffer = '';
94
+ const frames = [];
95
+ while (true) {
96
+ const { done, value } = await reader.read();
97
+ if (done) break;
98
+ buffer += decoder.decode(value, { stream: true });
99
+ let idx;
100
+ while ((idx = buffer.indexOf('\n\n')) !== -1) {
101
+ const chunk = buffer.slice(0, idx).trim();
102
+ buffer = buffer.slice(idx + 2);
103
+ if (chunk) frames.push(chunk);
104
+ }
105
+ }
106
+ buffer += decoder.decode(new Uint8Array(), { stream: false });
107
+ if (buffer.trim()) frames.push(buffer.trim());
108
+ return frames;
109
+ }
110
+
111
+ async function waitForFile(filePath, timeoutMs = 60_000) {
112
+ const start = Date.now();
113
+ while (Date.now() - start < timeoutMs) {
114
+ if (fs.existsSync(filePath)) return;
115
+ await new Promise((resolve) => setTimeout(resolve, 500));
116
+ }
117
+ throw new Error(`[verify:e2e-gemini-followup] timeout waiting for ${filePath}`);
118
+ }
119
+
120
+ async function main() {
121
+ if (!VERIFY_CONFIG) {
122
+ throw new Error('Missing ROUTECODEX_VERIFY_CONFIG/ROUTECODEX_CONFIG_PATH');
123
+ }
124
+
125
+ const serverApiKey = readServerApiKeyFromConfig(VERIFY_CONFIG);
126
+ if (!serverApiKey) {
127
+ throw new Error(`Missing httpserver.apikey in config: ${VERIFY_CONFIG}`);
128
+ }
129
+ const authHeaders = buildAuthHeaders(serverApiKey);
130
+
131
+ const samplePath = process.env.ROUTECODEX_VERIFY_SAMPLE || DEFAULT_SAMPLE;
132
+ if (!fs.existsSync(samplePath)) {
133
+ throw new Error(`Sample file not found: ${samplePath}`);
134
+ }
135
+ const sampleDoc = readJson(samplePath);
136
+ const payload = extractResponsesBody(sampleDoc);
137
+ if (!payload || typeof payload !== 'object') {
138
+ throw new Error('Sample did not contain a JSON request body');
139
+ }
140
+
141
+ const requestId =
142
+ (typeof process.env.ROUTECODEX_VERIFY_REQUEST_ID === 'string' && process.env.ROUTECODEX_VERIFY_REQUEST_ID.trim()) ||
143
+ `req_e2e_gemini_followup_${Date.now()}`;
144
+
145
+ const serverEnv = {
146
+ ...process.env,
147
+ ROUTECODEX_CONFIG_PATH: VERIFY_CONFIG,
148
+ ROUTECODEX_PORT: VERIFY_PORT,
149
+ ROUTECODEX_V2_HOOKS: '0',
150
+ RCC_V2_HOOKS: '0'
151
+ };
152
+
153
+ const debugLogs = String(process.env.ROUTECODEX_VERIFY_DEBUG || '').trim() === '1';
154
+ const serverLogPath = path.join(
155
+ os.tmpdir(),
156
+ `routecodex-verify-e2e-gemini-followup-${Date.now()}.log`
157
+ );
158
+ const serverLogStream = fs.createWriteStream(serverLogPath, { flags: 'a' });
159
+
160
+ const server = spawn('node', ['dist/index.js'], {
161
+ env: serverEnv,
162
+ stdio: ['ignore', 'pipe', 'pipe']
163
+ });
164
+
165
+ if (server.stdout) {
166
+ server.stdout.on('data', (chunk) => {
167
+ serverLogStream.write(chunk);
168
+ if (debugLogs) process.stdout.write(chunk);
169
+ });
170
+ }
171
+ if (server.stderr) {
172
+ server.stderr.on('data', (chunk) => {
173
+ serverLogStream.write(chunk);
174
+ if (debugLogs) process.stderr.write(chunk);
175
+ });
176
+ }
177
+
178
+ const shutdown = () => {
179
+ if (!server.killed) server.kill('SIGTERM');
180
+ };
181
+ process.on('SIGINT', shutdown);
182
+ process.on('SIGTERM', shutdown);
183
+
184
+ try {
185
+ await waitForServer();
186
+
187
+ // Keep headers minimal (we observed certain extra “sample headers” can trigger malformed capture in replay tooling).
188
+ const wantsSse = payload.stream === true;
189
+ const res = await fetch(`${VERIFY_BASE}/v1/responses`, {
190
+ method: 'POST',
191
+ headers: {
192
+ 'Content-Type': 'application/json',
193
+ Accept: wantsSse ? 'text/event-stream' : 'application/json',
194
+ 'OpenAI-Beta': 'responses-2024-12-17',
195
+ 'x-request-id': requestId,
196
+ ...(authHeaders || {})
197
+ },
198
+ body: JSON.stringify(payload)
199
+ });
200
+
201
+ if (!res.ok) {
202
+ const text = await res.text();
203
+ throw new Error(`HTTP ${res.status}: ${text}`);
204
+ }
205
+
206
+ if (wantsSse) {
207
+ const frames = await readSse(res);
208
+ const hasErrorFrame = frames.some((frame) => /^event:\\s*error\\b/m.test(frame));
209
+ if (hasErrorFrame) {
210
+ throw new Error(`SSE error frame returned (frames=${frames.length})`);
211
+ }
212
+ } else {
213
+ // ensure it is parseable JSON
214
+ await res.json();
215
+ }
216
+
217
+ // Snapshot assertions (ensures we really hit Gemini upstream and validated the mapped payload).
218
+ const snapDir = path.join(
219
+ os.homedir(),
220
+ '.routecodex',
221
+ 'codex-samples',
222
+ 'openai-responses',
223
+ 'antigravity.geetasamodgeetasamoda.gemini-3-pro-high',
224
+ requestId
225
+ );
226
+ const providerReq = path.join(snapDir, 'provider-request.json');
227
+ const providerResp = path.join(snapDir, 'provider-response.json');
228
+
229
+ await waitForFile(providerReq);
230
+ await waitForFile(providerResp);
231
+
232
+ const providerReqText = fs.readFileSync(providerReq, 'utf8');
233
+ if (/\"cmd\"\s*:/.test(providerReqText)) {
234
+ throw new Error('provider-request still contains legacy tool arg key "cmd"');
235
+ }
236
+ if (!/\"command\"\s*:/.test(providerReqText)) {
237
+ throw new Error('provider-request missing expected tool arg key "command"');
238
+ }
239
+ if (!/\"instructions\"\s*:/.test(providerReqText)) {
240
+ throw new Error('provider-request missing expected tool arg key "instructions"');
241
+ }
242
+ if (!/\"chars\"\s*:/.test(providerReqText)) {
243
+ throw new Error('provider-request missing expected tool arg key "chars"');
244
+ }
245
+
246
+ const providerRespJson = readJson(providerResp);
247
+ const raw = providerRespJson?.body?.raw;
248
+ if (typeof raw === 'string' && raw.includes('MALFORMED_FUNCTION_CALL')) {
249
+ throw new Error('provider-response still contains MALFORMED_FUNCTION_CALL');
250
+ }
251
+
252
+ console.log(`✅ [verify:e2e-gemini-followup] OK requestId=${requestId}`);
253
+ if (!debugLogs) {
254
+ console.log(`[verify:e2e-gemini-followup] server log: ${serverLogPath}`);
255
+ }
256
+ } finally {
257
+ shutdown();
258
+ try {
259
+ serverLogStream.end();
260
+ } catch {
261
+ // ignore
262
+ }
263
+ }
264
+ }
265
+
266
+ main().catch((err) => {
267
+ console.error('[verify:e2e-gemini-followup] failed:', err instanceof Error ? err.stack || err.message : String(err));
268
+ process.exit(1);
269
+ });