@jsonstudio/rcc 0.89.3 → 0.89.164

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 (270) hide show
  1. package/README.md +240 -179
  2. package/config/modules.json +1 -11
  3. package/dist/build-info.js +2 -2
  4. package/dist/build-info.js.map +1 -1
  5. package/dist/client/gemini-cli/gemini-cli-protocol-client.d.ts +16 -0
  6. package/dist/client/gemini-cli/gemini-cli-protocol-client.js +56 -0
  7. package/dist/client/gemini-cli/gemini-cli-protocol-client.js.map +1 -0
  8. package/dist/client/openai/chat-protocol-client.js.map +1 -1
  9. package/dist/config/modules.json +1 -11
  10. package/dist/core/provider-health-manager.d.ts +17 -0
  11. package/dist/core/provider-health-manager.js +66 -0
  12. package/dist/core/provider-health-manager.js.map +1 -0
  13. package/dist/error-handling/route-error-hub.d.ts +48 -0
  14. package/dist/error-handling/route-error-hub.js +131 -0
  15. package/dist/error-handling/route-error-hub.js.map +1 -0
  16. package/dist/index.js +26 -1
  17. package/dist/index.js.map +1 -1
  18. package/dist/modules/llmswitch/bridge.d.ts +2 -0
  19. package/dist/modules/llmswitch/bridge.js +17 -0
  20. package/dist/modules/llmswitch/bridge.js.map +1 -1
  21. package/dist/modules/pipeline/utils/colored-logger.d.ts +14 -0
  22. package/dist/modules/pipeline/utils/colored-logger.js +48 -0
  23. package/dist/modules/pipeline/utils/colored-logger.js.map +1 -0
  24. package/dist/modules/pipeline/utils/debug-logger.d.ts +2 -0
  25. package/dist/modules/pipeline/utils/debug-logger.js +36 -0
  26. package/dist/modules/pipeline/utils/debug-logger.js.map +1 -1
  27. package/dist/providers/auth/gemini-cli-userinfo-helper.d.ts +53 -0
  28. package/dist/providers/auth/gemini-cli-userinfo-helper.js +152 -0
  29. package/dist/providers/auth/gemini-cli-userinfo-helper.js.map +1 -0
  30. package/dist/providers/auth/oauth-auth.js +3 -2
  31. package/dist/providers/auth/oauth-auth.js.map +1 -1
  32. package/dist/providers/auth/oauth-lifecycle.js +21 -20
  33. package/dist/providers/auth/oauth-lifecycle.js.map +1 -1
  34. package/dist/providers/auth/oauth-logger.d.ts +1 -0
  35. package/dist/providers/auth/oauth-logger.js +21 -0
  36. package/dist/providers/auth/oauth-logger.js.map +1 -0
  37. package/dist/providers/compat/compat-directory-loader.js +2 -55
  38. package/dist/providers/compat/compat-directory-loader.js.map +1 -1
  39. package/dist/providers/compat/compatibility-factory.d.ts +4 -4
  40. package/dist/providers/compat/compatibility-factory.js +108 -0
  41. package/dist/providers/compat/compatibility-factory.js.map +1 -1
  42. package/dist/providers/compat/glm/glm-compatibility.d.ts +2 -2
  43. package/dist/providers/compat/glm/glm-compatibility.js +7 -7
  44. package/dist/providers/compat/glm/glm-compatibility.js.map +1 -1
  45. package/dist/providers/compat/glm/index.js +0 -6
  46. package/dist/providers/compat/glm/index.js.map +1 -1
  47. package/dist/providers/compat/iflow/iflow-compatibility.d.ts +1 -1
  48. package/dist/providers/compat/iflow/iflow-compatibility.js +6 -6
  49. package/dist/providers/compat/iflow/iflow-compatibility.js.map +1 -1
  50. package/dist/providers/compat/index.d.ts +0 -6
  51. package/dist/providers/compat/index.js +0 -7
  52. package/dist/providers/compat/index.js.map +1 -1
  53. package/dist/providers/compat/lmstudio-compatibility.d.ts +2 -2
  54. package/dist/providers/compat/lmstudio-compatibility.js +4 -4
  55. package/dist/providers/compat/lmstudio-compatibility.js.map +1 -1
  56. package/dist/providers/compat/passthrough-compatibility.d.ts +1 -1
  57. package/dist/providers/compat/passthrough-compatibility.js +3 -3
  58. package/dist/providers/compat/passthrough-compatibility.js.map +1 -1
  59. package/dist/providers/compat/profiles/chat/glm/index.d.ts +6 -0
  60. package/dist/providers/compat/profiles/chat/glm/index.js +6 -0
  61. package/dist/providers/compat/profiles/chat/glm/index.js.map +1 -0
  62. package/dist/providers/compat/profiles/chat/iflow/index.d.ts +6 -0
  63. package/dist/providers/compat/profiles/chat/iflow/index.js +6 -0
  64. package/dist/providers/compat/profiles/chat/iflow/index.js.map +1 -0
  65. package/dist/providers/compat/profiles/chat/lmstudio/index.d.ts +6 -0
  66. package/dist/providers/compat/profiles/chat/lmstudio/index.js +6 -0
  67. package/dist/providers/compat/profiles/chat/lmstudio/index.js.map +1 -0
  68. package/dist/providers/compat/profiles/chat/qwen/index.d.ts +6 -0
  69. package/dist/providers/compat/profiles/chat/qwen/index.js +6 -0
  70. package/dist/providers/compat/profiles/chat/qwen/index.js.map +1 -0
  71. package/dist/providers/compat/profiles/compat/passthrough/index.d.ts +6 -0
  72. package/dist/providers/compat/profiles/compat/passthrough/index.js +6 -0
  73. package/dist/providers/compat/profiles/compat/passthrough/index.js.map +1 -0
  74. package/dist/providers/compat/profiles/responses/c4m/index.d.ts +6 -0
  75. package/dist/providers/compat/profiles/responses/c4m/index.js +6 -0
  76. package/dist/providers/compat/profiles/responses/c4m/index.js.map +1 -0
  77. package/dist/providers/compat/profiles/responses/default/index.d.ts +6 -0
  78. package/dist/providers/compat/profiles/responses/default/index.js +6 -0
  79. package/dist/providers/compat/profiles/responses/default/index.js.map +1 -0
  80. package/dist/providers/compat/profiles/responses/fai/index.d.ts +6 -0
  81. package/dist/providers/compat/profiles/responses/fai/index.js +6 -0
  82. package/dist/providers/compat/profiles/responses/fai/index.js.map +1 -0
  83. package/dist/providers/compat/profiles/responses/fc/index.d.ts +6 -0
  84. package/dist/providers/compat/profiles/responses/fc/index.js +6 -0
  85. package/dist/providers/compat/profiles/responses/fc/index.js.map +1 -0
  86. package/dist/providers/compat/qwen/index.js +0 -6
  87. package/dist/providers/compat/qwen/index.js.map +1 -1
  88. package/dist/providers/compat/qwen-compatibility.d.ts +2 -2
  89. package/dist/providers/compat/qwen-compatibility.js +4 -4
  90. package/dist/providers/compat/qwen-compatibility.js.map +1 -1
  91. package/dist/providers/compat/register-compat-module.d.ts +8 -0
  92. package/dist/providers/compat/register-compat-module.js +53 -0
  93. package/dist/providers/compat/register-compat-module.js.map +1 -0
  94. package/dist/providers/compat/responses/c4m-responses-compatibility.d.ts +6 -2
  95. package/dist/providers/compat/responses/c4m-responses-compatibility.js +85 -3
  96. package/dist/providers/compat/responses/c4m-responses-compatibility.js.map +1 -1
  97. package/dist/providers/compat/standard-compatibility-utils.js +45 -15
  98. package/dist/providers/compat/standard-compatibility-utils.js.map +1 -1
  99. package/dist/providers/core/api/provider-config.d.ts +1 -1
  100. package/dist/providers/core/api/provider-types.d.ts +3 -1
  101. package/dist/providers/core/api/provider-types.js +1 -0
  102. package/dist/providers/core/api/provider-types.js.map +1 -1
  103. package/dist/providers/core/config/service-profiles.js +5 -2
  104. package/dist/providers/core/config/service-profiles.js.map +1 -1
  105. package/dist/providers/core/runtime/base-provider.d.ts +3 -0
  106. package/dist/providers/core/runtime/base-provider.js +101 -6
  107. package/dist/providers/core/runtime/base-provider.js.map +1 -1
  108. package/dist/providers/core/runtime/gemini-cli-http-provider.d.ts +34 -0
  109. package/dist/providers/core/runtime/gemini-cli-http-provider.js +152 -0
  110. package/dist/providers/core/runtime/gemini-cli-http-provider.js.map +1 -0
  111. package/dist/providers/core/runtime/http-transport-provider.d.ts +1 -0
  112. package/dist/providers/core/runtime/http-transport-provider.js +178 -123
  113. package/dist/providers/core/runtime/http-transport-provider.js.map +1 -1
  114. package/dist/providers/core/runtime/provider-factory.d.ts +1 -1
  115. package/dist/providers/core/runtime/provider-factory.js +8 -0
  116. package/dist/providers/core/runtime/provider-factory.js.map +1 -1
  117. package/dist/providers/core/runtime/provider-runtime-metadata.d.ts +1 -0
  118. package/dist/providers/core/runtime/provider-runtime-metadata.js.map +1 -1
  119. package/dist/providers/core/runtime/responses-provider.d.ts +7 -0
  120. package/dist/providers/core/runtime/responses-provider.js +228 -12
  121. package/dist/providers/core/runtime/responses-provider.js.map +1 -1
  122. package/dist/providers/core/strategies/oauth-auth-code-flow.js +10 -9
  123. package/dist/providers/core/strategies/oauth-auth-code-flow.js.map +1 -1
  124. package/dist/providers/core/strategies/oauth-device-flow.js +10 -9
  125. package/dist/providers/core/strategies/oauth-device-flow.js.map +1 -1
  126. package/dist/providers/core/utils/provider-error-reporter.js +63 -15
  127. package/dist/providers/core/utils/provider-error-reporter.js.map +1 -1
  128. package/dist/providers/core/utils/provider-type-utils.d.ts +1 -1
  129. package/dist/providers/core/utils/provider-type-utils.js +6 -1
  130. package/dist/providers/core/utils/provider-type-utils.js.map +1 -1
  131. package/dist/providers/core/utils/snapshot-writer.d.ts +10 -0
  132. package/dist/providers/core/utils/snapshot-writer.js +85 -0
  133. package/dist/providers/core/utils/snapshot-writer.js.map +1 -1
  134. package/dist/providers/mock/mock-provider-runtime.js +44 -0
  135. package/dist/providers/mock/mock-provider-runtime.js.map +1 -1
  136. package/dist/providers/profile/provider-profile-loader.js +26 -19
  137. package/dist/providers/profile/provider-profile-loader.js.map +1 -1
  138. package/dist/providers/profile/provider-profile.d.ts +2 -2
  139. package/dist/server/handlers/chat-handler.js +9 -3
  140. package/dist/server/handlers/chat-handler.js.map +1 -1
  141. package/dist/server/handlers/handler-utils.d.ts +7 -1
  142. package/dist/server/handlers/handler-utils.js +64 -52
  143. package/dist/server/handlers/handler-utils.js.map +1 -1
  144. package/dist/server/handlers/messages-handler.js +9 -3
  145. package/dist/server/handlers/messages-handler.js.map +1 -1
  146. package/dist/server/handlers/responses-handler.js +21 -13
  147. package/dist/server/handlers/responses-handler.js.map +1 -1
  148. package/dist/server/runtime/http-server/colored-logger.d.ts +1 -0
  149. package/dist/server/runtime/http-server/colored-logger.js +33 -0
  150. package/dist/server/runtime/http-server/colored-logger.js.map +1 -0
  151. package/dist/server/runtime/http-server/index.d.ts +3 -0
  152. package/dist/server/runtime/http-server/index.js +76 -19
  153. package/dist/server/runtime/http-server/index.js.map +1 -1
  154. package/dist/server/runtime/http-server/provider-utils.d.ts +3 -1
  155. package/dist/server/runtime/http-server/provider-utils.js +12 -2
  156. package/dist/server/runtime/http-server/provider-utils.js.map +1 -1
  157. package/dist/server/runtime/http-server/request-executor.js +6 -2
  158. package/dist/server/runtime/http-server/request-executor.js.map +1 -1
  159. package/dist/server/runtime/http-server/routes.js +31 -11
  160. package/dist/server/runtime/http-server/routes.js.map +1 -1
  161. package/dist/server/runtime/http-server/types.d.ts +2 -1
  162. package/dist/utils/error-center-payload.d.ts +7 -0
  163. package/dist/utils/error-center-payload.js +67 -0
  164. package/dist/utils/error-center-payload.js.map +1 -0
  165. package/dist/utils/error-handler-registry.d.ts +7 -0
  166. package/dist/utils/error-handler-registry.js +44 -12
  167. package/dist/utils/error-handler-registry.js.map +1 -1
  168. package/node_modules/@jsonstudio/llms/dist/conversion/codecs/responses-openai-codec.js +16 -1
  169. package/node_modules/@jsonstudio/llms/dist/conversion/compat/profiles/chat-glm.json +17 -0
  170. package/node_modules/@jsonstudio/llms/dist/conversion/compat/profiles/chat-iflow.json +36 -0
  171. package/node_modules/@jsonstudio/llms/dist/conversion/compat/profiles/chat-lmstudio.json +37 -0
  172. package/node_modules/@jsonstudio/llms/dist/conversion/compat/profiles/chat-qwen.json +18 -0
  173. package/node_modules/@jsonstudio/llms/dist/conversion/compat/profiles/responses-c4m.json +45 -0
  174. package/node_modules/@jsonstudio/llms/dist/conversion/config/compat-profiles.json +38 -0
  175. package/node_modules/@jsonstudio/llms/dist/conversion/config/sample-config.json +314 -0
  176. package/node_modules/@jsonstudio/llms/dist/conversion/config/version-switch.json +150 -0
  177. package/node_modules/@jsonstudio/llms/dist/conversion/hub/pipeline/compat/compat-engine.d.ts +4 -0
  178. package/node_modules/@jsonstudio/llms/dist/conversion/hub/pipeline/compat/compat-engine.js +667 -0
  179. package/node_modules/@jsonstudio/llms/dist/conversion/hub/pipeline/compat/compat-profile-store.d.ts +2 -0
  180. package/node_modules/@jsonstudio/llms/dist/conversion/hub/pipeline/compat/compat-profile-store.js +76 -0
  181. package/node_modules/@jsonstudio/llms/dist/conversion/hub/pipeline/compat/compat-types.d.ts +62 -0
  182. package/node_modules/@jsonstudio/llms/dist/conversion/hub/pipeline/compat/compat-types.js +1 -0
  183. package/node_modules/@jsonstudio/llms/dist/conversion/hub/pipeline/hub-pipeline.d.ts +2 -0
  184. package/node_modules/@jsonstudio/llms/dist/conversion/hub/pipeline/hub-pipeline.js +110 -29
  185. package/node_modules/@jsonstudio/llms/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage3_compat/index.d.ts +14 -0
  186. package/node_modules/@jsonstudio/llms/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage3_compat/index.js +23 -0
  187. package/node_modules/@jsonstudio/llms/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.js +34 -0
  188. package/node_modules/@jsonstudio/llms/dist/conversion/hub/process/chat-process.js +4 -1
  189. package/node_modules/@jsonstudio/llms/dist/conversion/hub/response/provider-response.js +26 -0
  190. package/node_modules/@jsonstudio/llms/dist/conversion/responses/responses-openai-bridge.d.ts +1 -0
  191. package/node_modules/@jsonstudio/llms/dist/conversion/responses/responses-openai-bridge.js +71 -0
  192. package/node_modules/@jsonstudio/llms/dist/conversion/shared/responses-conversation-store.d.ts +35 -0
  193. package/node_modules/@jsonstudio/llms/dist/conversion/shared/responses-conversation-store.js +64 -19
  194. package/node_modules/@jsonstudio/llms/dist/conversion/shared/tool-filter-pipeline.d.ts +21 -0
  195. package/node_modules/@jsonstudio/llms/dist/conversion/shared/tool-filter-pipeline.js +138 -22
  196. package/node_modules/@jsonstudio/llms/dist/conversion/shared/tool-governor.d.ts +21 -0
  197. package/node_modules/@jsonstudio/llms/dist/conversion/shared/tool-governor.js +116 -1
  198. package/node_modules/@jsonstudio/llms/dist/conversion/shared/tool-mapping.js +52 -2
  199. package/node_modules/@jsonstudio/llms/dist/filters/config/openai-openai.fieldmap.json +18 -0
  200. package/node_modules/@jsonstudio/llms/dist/filters/special/request-tools-normalize.js +20 -1
  201. package/node_modules/@jsonstudio/llms/dist/guidance/index.js +6 -2
  202. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/bootstrap.js +16 -7
  203. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/classifier.js +40 -37
  204. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/default-thinking-keywords.d.ts +1 -0
  205. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/default-thinking-keywords.js +13 -0
  206. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/engine.d.ts +39 -0
  207. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/engine.js +52 -11
  208. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/features.js +340 -11
  209. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/token-counter.d.ts +2 -0
  210. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/token-counter.js +105 -0
  211. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/types.d.ts +8 -0
  212. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/types.js +2 -2
  213. package/node_modules/@jsonstudio/llms/dist/sse/sse-to-json/builders/response-builder.d.ts +2 -0
  214. package/node_modules/@jsonstudio/llms/dist/sse/sse-to-json/builders/response-builder.js +53 -11
  215. package/node_modules/@jsonstudio/llms/dist/test-output/virtual-router/results.json +1 -0
  216. package/node_modules/@jsonstudio/llms/dist/test-output/virtual-router/summary.json +12 -0
  217. package/node_modules/@jsonstudio/llms/dist/tools/tool-registry.js +4 -3
  218. package/node_modules/@jsonstudio/llms/package.json +3 -3
  219. package/package.json +11 -9
  220. package/scripts/analyze-routing-classifier.mjs +166 -0
  221. package/scripts/analyze-routing-samples.mjs +216 -0
  222. package/scripts/analyze-thinking-keywords.mjs +17 -2
  223. package/scripts/build-core.mjs +8 -0
  224. package/scripts/classify-sample-tools.mjs +252 -0
  225. package/scripts/ensure-llmswitch-mode.mjs +95 -0
  226. package/scripts/gen-build-info.mjs +58 -4
  227. package/scripts/install-global.sh +1 -1
  228. package/scripts/install-release.sh +7 -0
  229. package/scripts/mock-provider/run-regressions.mjs +60 -14
  230. package/scripts/tests/apply-patch-loop.mjs +100 -9
  231. package/scripts/tests/golden-provider-cycle.mjs +12 -1
  232. package/scripts/tests/responses-provider-dry-run.mjs +15 -1
  233. package/scripts/tests/virtual-router-health.mjs +12 -5
  234. package/scripts/tools/capture-provider-goldens.mjs +75 -25
  235. package/scripts/tools/responses-golden-dry-run.mjs +17 -1
  236. package/scripts/tools/responses-provider-replay.mjs +17 -1
  237. package/scripts/tools/sync-ci-goldens.mjs +131 -0
  238. package/scripts/verification/samples/openai-chat-list-local-files.json +19 -796
  239. package/scripts/verify-e2e-toolcall.mjs +52 -0
  240. package/dist/providers/compat/config/index.d.ts +0 -1
  241. package/dist/providers/compat/config/index.js +0 -5
  242. package/dist/providers/compat/config/index.js.map +0 -1
  243. package/dist/providers/compat/iflow/index.d.ts +0 -27
  244. package/dist/providers/compat/iflow/index.js +0 -32
  245. package/dist/providers/compat/iflow/index.js.map +0 -1
  246. package/dist/providers/compat/lmstudio/index.d.ts +0 -4
  247. package/dist/providers/compat/lmstudio/index.js +0 -10
  248. package/dist/providers/compat/lmstudio/index.js.map +0 -1
  249. package/dist/providers/compat/passthrough/index.d.ts +0 -4
  250. package/dist/providers/compat/passthrough/index.js +0 -9
  251. package/dist/providers/compat/passthrough/index.js.map +0 -1
  252. package/dist/providers/compat/responses/index.d.ts +0 -1
  253. package/dist/providers/compat/responses/index.js +0 -8
  254. package/dist/providers/compat/responses/index.js.map +0 -1
  255. package/dist/providers/core/composite/compat/anthropic.d.ts +0 -3
  256. package/dist/providers/core/composite/compat/anthropic.js +0 -7
  257. package/dist/providers/core/composite/compat/anthropic.js.map +0 -1
  258. package/dist/providers/core/composite/compat/gemini.d.ts +0 -3
  259. package/dist/providers/core/composite/compat/gemini.js +0 -7
  260. package/dist/providers/core/composite/compat/gemini.js.map +0 -1
  261. package/dist/providers/core/composite/compat/openai-compat-aggregator.d.ts +0 -9
  262. package/dist/providers/core/composite/compat/openai-compat-aggregator.js +0 -135
  263. package/dist/providers/core/composite/compat/openai-compat-aggregator.js.map +0 -1
  264. package/dist/providers/core/composite/compat/responses.d.ts +0 -3
  265. package/dist/providers/core/composite/compat/responses.js +0 -91
  266. package/dist/providers/core/composite/compat/responses.js.map +0 -1
  267. package/dist/providers/core/composite/provider-composite.d.ts +0 -50
  268. package/dist/providers/core/composite/provider-composite.js +0 -235
  269. package/dist/providers/core/composite/provider-composite.js.map +0 -1
  270. package/scripts/tests/apply-patch-loop.mjs.bak +0 -363
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import fs from 'node:fs/promises';
3
3
  import path from 'node:path';
4
+ import os from 'node:os';
4
5
  import { fileURLToPath } from 'node:url';
5
6
  import { TextDecoder } from 'node:util';
6
7
  import { Readable } from 'node:stream';
@@ -14,6 +15,11 @@ const PROJECT_ROOT = path.resolve(__dirname, '../..');
14
15
  const MOCK_SAMPLES_DIR = path.join(PROJECT_ROOT, 'samples/mock-provider');
15
16
  const PORT = Number(process.env.RCC_TOOL_LOOP_PORT || 5555);
16
17
  const BASE_URL = `http://127.0.0.1:${PORT}`;
18
+ const HOME = os.homedir();
19
+ const STAGE_DIR = path.join(HOME, '.routecodex', 'golden_samples', 'openai-responses');
20
+ const STAGE_SUFFIX = '_req_outbound_stage2_format_build.json';
21
+ const STAGE1_SUFFIX = '_req_outbound_stage1_semantic_map.json';
22
+ const MOCK_PROVIDER_ID = 'mock.apply_patch.toolloop';
17
23
 
18
24
  function listProcessesOnPort(port) {
19
25
  try {
@@ -59,6 +65,84 @@ async function ensurePortFree(port) {
59
65
  await delay(200);
60
66
  }
61
67
 
68
+ async function snapshotStageFiles() {
69
+ try {
70
+ const entries = await fs.readdir(STAGE_DIR);
71
+ return new Set(entries.filter((name) => name.endsWith(STAGE_SUFFIX)));
72
+ } catch {
73
+ return new Set();
74
+ }
75
+ }
76
+
77
+ async function diffStageFiles(beforeSet) {
78
+ try {
79
+ const entries = await fs.readdir(STAGE_DIR);
80
+ return entries
81
+ .filter((name) => name.endsWith(STAGE_SUFFIX))
82
+ .filter((name) => !beforeSet.has(name));
83
+ } catch {
84
+ return [];
85
+ }
86
+ }
87
+
88
+ async function waitForMockStage(beforeSet, timeoutMs = 10000) {
89
+ const deadline = Date.now() + timeoutMs;
90
+ while (Date.now() < deadline) {
91
+ const candidates = await diffStageFiles(beforeSet);
92
+ for (const name of candidates) {
93
+ const stage1Name = name.replace(STAGE_SUFFIX, STAGE1_SUFFIX);
94
+ const stage1Path = path.join(STAGE_DIR, stage1Name);
95
+ try {
96
+ await fs.access(stage1Path);
97
+ } catch {
98
+ continue;
99
+ }
100
+ let providerId = '';
101
+ try {
102
+ const stage1Doc = JSON.parse(await fs.readFile(stage1Path, 'utf-8'));
103
+ providerId = stage1Doc?.body?.meta?.context?.providerId ?? '';
104
+ } catch {
105
+ providerId = '';
106
+ }
107
+ if (typeof providerId === 'string' && providerId === MOCK_PROVIDER_ID) {
108
+ return path.join(STAGE_DIR, name);
109
+ }
110
+ }
111
+ await delay(250);
112
+ }
113
+ throw new Error('mock apply_patch stage snapshot not found (enable ROUTECODEX_STAGE_LOG)');
114
+ }
115
+
116
+ async function verifyApplyPatchTool(stagePath) {
117
+ const raw = await fs.readFile(stagePath, 'utf-8');
118
+ const doc = JSON.parse(raw);
119
+ const payload = doc?.body ?? doc;
120
+ const tools = Array.isArray(payload?.tools) ? payload.tools : [];
121
+ if (!tools.length) {
122
+ throw new Error('provider payload missing tools array');
123
+ }
124
+ const match = tools.find((tool) => {
125
+ const name = tool?.name || tool?.function?.name;
126
+ return typeof name === 'string' && name.trim() === 'apply_patch';
127
+ });
128
+ if (!match) {
129
+ throw new Error('apply_patch tool declaration missing in provider payload');
130
+ }
131
+ const params = match.parameters || match.function?.parameters;
132
+ const props = params?.properties;
133
+ const inputField = props?.input;
134
+ if (!inputField || typeof inputField !== 'object') {
135
+ throw new Error('apply_patch.parameters.input missing');
136
+ }
137
+ if (String(inputField.type).toLowerCase() !== 'string') {
138
+ throw new Error('apply_patch.parameters.input must be a string');
139
+ }
140
+ const required = Array.isArray(params?.required) ? params.required.map((v) => String(v)) : [];
141
+ if (!required.includes('input')) {
142
+ throw new Error('apply_patch.parameters.required must include \"input\"');
143
+ }
144
+ }
145
+
62
146
  function buildMockConfig(port) {
63
147
  return {
64
148
  version: '1.0.0',
@@ -210,11 +294,12 @@ async function requestApplyPatchLoop() {
210
294
  responseId = String(data?.response?.id || '');
211
295
  console.log(`[tool-loop] response.created id=${responseId}`);
212
296
  } else if (ev.event === 'response.required_action') {
213
- toolCalls = Array.isArray(data?.required_action?.submit_tool_outputs?.tool_calls)
214
- ? data.required_action.submit_tool_outputs.tool_calls
215
- : [];
216
- console.log(`[tool-loop] required_action tool_calls=${toolCalls.length}`);
217
- break;
297
+ if (!toolCalls.length) {
298
+ toolCalls = Array.isArray(data?.required_action?.submit_tool_outputs?.tool_calls)
299
+ ? data.required_action.submit_tool_outputs.tool_calls
300
+ : [];
301
+ console.log(`[tool-loop] required_action tool_calls=${toolCalls.length}`);
302
+ }
218
303
  }
219
304
  }
220
305
 
@@ -231,7 +316,8 @@ async function requestApplyPatchLoop() {
231
316
  let patchText = '';
232
317
  try {
233
318
  const parsed = JSON.parse(firstCall.function.arguments || '{}');
234
- patchText = String(parsed?.patch || '');
319
+ const diffText = parsed?.input ?? parsed?.patch;
320
+ patchText = typeof diffText === 'string' ? diffText : '';
235
321
  } catch {
236
322
  throw new Error('apply_patch.arguments JSON parse failed');
237
323
  }
@@ -264,12 +350,12 @@ function buildResponsesPayload() {
264
350
  parameters: {
265
351
  type: 'object',
266
352
  properties: {
267
- patch: {
353
+ input: {
268
354
  type: 'string',
269
355
  description: 'Unified diff patch content (*** Begin Patch ... *** End Patch)'
270
356
  }
271
357
  },
272
- required: ['patch'],
358
+ required: ['input'],
273
359
  additionalProperties: false
274
360
  },
275
361
  strict: true
@@ -336,13 +422,18 @@ async function main() {
336
422
  ROUTECODEX_MOCK_CONFIG_PATH: file,
337
423
  ROUTECODEX_MOCK_SAMPLES_DIR: MOCK_SAMPLES_DIR,
338
424
  ROUTECODEX_MOCK_VALIDATE_NAMES: '1',
425
+ ROUTECODEX_CONFIG_PATH: file,
339
426
  ROUTECODEX_PORT: String(PORT),
340
- ROUTECODEX_STAGE_LOG: process.env.ROUTECODEX_STAGE_LOG ?? '0'
427
+ ROUTECODEX_STAGE_LOG: '1'
341
428
  }
342
429
  });
343
430
  try {
344
431
  await waitForHealth(server);
432
+ const stageBefore = await snapshotStageFiles();
345
433
  const { responseId, toolCalls, patchText } = await requestApplyPatchLoop();
434
+ const stagePath = await waitForMockStage(stageBefore);
435
+ await verifyApplyPatchTool(stagePath);
436
+ console.log(`[tool-loop] verified provider payload stage → ${stagePath}`);
346
437
  await submitToolOutputs(responseId, toolCalls, patchText);
347
438
  console.log('[tool-loop] apply_patch loop PASSED');
348
439
  } finally {
@@ -1,11 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
3
5
  import { spawn } from 'node:child_process';
4
6
  import { fileURLToPath } from 'node:url';
5
7
  import path from 'node:path';
6
8
 
7
9
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
10
  const PROJECT_ROOT = path.resolve(__dirname, '../..');
11
+ const HOME = os.homedir();
12
+ const USER_SAMPLES_HINT = path.join(HOME, '.routecodex', 'codex-samples');
9
13
 
10
14
  function run(command, args, env = {}) {
11
15
  return new Promise((resolve, reject) => {
@@ -27,7 +31,14 @@ async function main() {
27
31
  '--custom-only',
28
32
  '--update-golden'
29
33
  ]);
30
- await run('node', ['scripts/mock-provider/run-regressions.mjs']);
34
+ await run('node', ['scripts/mock-provider/run-regressions.mjs'], {
35
+ ROUTECODEX_MOCK_ENTRY_FILTER: 'all'
36
+ });
37
+ if (fs.existsSync(USER_SAMPLES_HINT)) {
38
+ console.log('[golden-cycle] detected ~/.routecodex/codex-samples; run "node scripts/mock-provider/capture-from-configs.mjs" to ingest latest provider recordings for deep regression.');
39
+ } else {
40
+ console.log('[golden-cycle] ~/.routecodex/codex-samples missing; skipping deep regression (optional).');
41
+ }
31
42
  }
32
43
 
33
44
  main().catch((error) => {
@@ -32,6 +32,20 @@ if (!providerDef) usage(`Provider "${providerId}" not found in config`);
32
32
  const auth = providerDef.auth || {};
33
33
  const apiKey = auth.apiKey || auth.value || process.env.DRY_RUN_API_KEY || 'dry-run-key';
34
34
 
35
+ const legacyCompatFields = [];
36
+ if (typeof providerDef.compatibility_profile === 'string') legacyCompatFields.push('compatibility_profile');
37
+ if (typeof providerDef.compat === 'string') legacyCompatFields.push('compat');
38
+ if (providerDef.compatibility && typeof providerDef.compatibility === 'object') {
39
+ if (typeof providerDef.compatibility.profile === 'string') legacyCompatFields.push('compatibility.profile');
40
+ if (typeof providerDef.compatibility.id === 'string') legacyCompatFields.push('compatibility.id');
41
+ }
42
+ if (legacyCompatFields.length > 0) {
43
+ usage(`Provider "${providerId}" uses legacy compatibility field(s): ${legacyCompatFields.join(', ')}. Rename to "compatibilityProfile".`);
44
+ }
45
+ const compatProfile =
46
+ (typeof providerDef.compatibilityProfile === 'string' && providerDef.compatibilityProfile.trim()) ||
47
+ undefined;
48
+
35
49
  const runtime = {
36
50
  runtimeKey: `${providerId}.dry`,
37
51
  providerId,
@@ -42,7 +56,7 @@ const runtime = {
42
56
  type: 'apikey',
43
57
  value: apiKey
44
58
  },
45
- compatibilityProfile: providerDef.compat || 'default',
59
+ compatibilityProfile: compatProfile,
46
60
  outboundProfile: providerDef.type === 'responses' ? 'openai-responses' : 'openai-chat',
47
61
  defaultModel: modelId
48
62
  };
@@ -45,7 +45,7 @@ function buildProviderProfile(providerKey, endpoint) {
45
45
  endpoint,
46
46
  auth: { type: 'apiKey', secretRef: providerKey },
47
47
  outboundProfile: 'openai-chat',
48
- compatibilityProfile: 'default',
48
+ compatibilityProfile: 'compat:passthrough',
49
49
  defaultModel: 'sim-model'
50
50
  };
51
51
  }
@@ -65,7 +65,7 @@ function createRouterConfig() {
65
65
  routing,
66
66
  providers,
67
67
  classifier: {
68
- longContextThresholdTokens: 60000,
68
+ longContextThresholdTokens: 180000,
69
69
  thinkingKeywords: ['think', '考', 'reason'],
70
70
  backgroundKeywords: []
71
71
  },
@@ -128,9 +128,9 @@ class VirtualRouterSimulator {
128
128
  return event;
129
129
  }
130
130
 
131
- runRoute(label = 'default') {
131
+ runRoute(label = 'default', text) {
132
132
  const requestId = `req_${++this.sequence}`;
133
- const request = cloneRequest(`scenario:${label}:${Date.now()}`);
133
+ const request = cloneRequest(text ?? `scenario:${label}:${Date.now()}`);
134
134
  const metadata = {
135
135
  requestId,
136
136
  entryEndpoint: '/v1/chat/completions',
@@ -227,6 +227,12 @@ async function scenarioScheduler(sim) {
227
227
  sim.runRoute('thinking');
228
228
  }
229
229
 
230
+ async function scenarioRoutingDirectives(sim) {
231
+ sim.runRoute('baseline', '普通请求');
232
+ sim.runRoute('forced-thinking', '请仔细分析这个问题 <**thinking**>');
233
+ sim.runRoute('forced-provider', '请强制使用这个provider <**charlie.sim-model**> 来回答');
234
+ }
235
+
230
236
  async function main() {
231
237
  const args = parseArgs(process.argv.slice(2));
232
238
  if (args.help) {
@@ -240,7 +246,8 @@ async function main() {
240
246
  ['client-error', scenarioClientError],
241
247
  ['upstream', scenarioUpstream],
242
248
  ['timeout', scenarioTimeout],
243
- ['scheduler', scenarioScheduler]
249
+ ['scheduler', scenarioScheduler],
250
+ ['routing-directives', scenarioRoutingDirectives]
244
251
  ];
245
252
 
246
253
  const summary = [];
@@ -15,7 +15,11 @@ const HOME = os.homedir();
15
15
  const PROVIDER_ROOT = path.join(HOME, '.routecodex', 'provider');
16
16
  const SNAPSHOT_ROOT = path.join(HOME, '.routecodex', 'golden_samples');
17
17
  const PROVIDER_GOLDEN_ROOT = path.join(SNAPSHOT_ROOT, 'provider_golden_samples');
18
- const CUSTOM_SAMPLE_ROOT = path.join(SNAPSHOT_ROOT, 'new');
18
+ const CI_GOLDENS_ROOT = path.join(ROOT, 'samples', 'ci-goldens');
19
+ const CUSTOM_SAMPLE_ROOTS = [
20
+ path.join(SNAPSHOT_ROOT, 'new'),
21
+ CI_GOLDENS_ROOT
22
+ ];
19
23
  const TEMP_ROOT = path.join(process.cwd(), 'tmp', 'provider-captures');
20
24
  const STAGE_DIRS = {
21
25
  'openai-chat': path.join(SNAPSHOT_ROOT, 'openai-chat'),
@@ -138,7 +142,7 @@ function ensureDir(p) {
138
142
  function listProviderConfigs() {
139
143
  const entries = [];
140
144
  if (!fs.existsSync(PROVIDER_ROOT)) {
141
- throw new Error(`Provider directory missing: ${PROVIDER_ROOT}`);
145
+ return entries;
142
146
  }
143
147
  for (const dir of fs.readdirSync(PROVIDER_ROOT)) {
144
148
  const absDir = path.join(PROVIDER_ROOT, dir);
@@ -169,6 +173,39 @@ function listProviderConfigs() {
169
173
  return entries;
170
174
  }
171
175
 
176
+ function mapEntryTypeToProviderType(entryType) {
177
+ if (entryType === 'anthropic-messages') return 'anthropic-http-provider';
178
+ if (entryType === 'openai-responses') return 'responses-http-provider';
179
+ return 'openai-http-provider';
180
+ }
181
+
182
+ function listCiGoldenProviders() {
183
+ const entries = [];
184
+ if (!fs.existsSync(CI_GOLDENS_ROOT)) {
185
+ return entries;
186
+ }
187
+ for (const entryType of fs.readdirSync(CI_GOLDENS_ROOT)) {
188
+ const entryDir = path.join(CI_GOLDENS_ROOT, entryType);
189
+ if (!fs.statSync(entryDir).isDirectory()) continue;
190
+ for (const providerId of fs.readdirSync(entryDir)) {
191
+ const providerDir = path.join(entryDir, providerId);
192
+ if (!fs.statSync(providerDir).isDirectory()) continue;
193
+ entries.push({
194
+ dir: 'ci-goldens',
195
+ configFile: path.join(providerDir, 'request.sample.json'),
196
+ providerId,
197
+ providerConfig: {
198
+ id: providerId,
199
+ type: mapEntryTypeToProviderType(entryType)
200
+ },
201
+ doc: null,
202
+ entryTypeOverride: entryType
203
+ });
204
+ }
205
+ }
206
+ return entries;
207
+ }
208
+
172
209
  function detectEntryType(providerConfig) {
173
210
  const type = String(providerConfig?.type || '').toLowerCase();
174
211
  if (type.includes('anthropic')) return 'anthropic-messages';
@@ -309,24 +346,30 @@ function diffJson(expected, actual, prefix = '<root>') {
309
346
  }
310
347
 
311
348
  function loadCustomSample(providerId, entryType) {
312
- const dir = path.join(CUSTOM_SAMPLE_ROOT, entryType, providerId);
313
- const samplePath = path.join(dir, 'request.sample.json');
314
- if (!fs.existsSync(samplePath)) return null;
315
- let stageFile;
316
- const metaPath = path.join(dir, 'meta.json');
317
- if (fs.existsSync(metaPath)) {
318
- try {
319
- const meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
320
- stageFile = meta?.stageFile;
321
- } catch (error) {
322
- console.warn(`[capture] custom meta parse failed for ${providerId}/${entryType}: ${error.message}`);
349
+ for (const root of CUSTOM_SAMPLE_ROOTS) {
350
+ if (!root) continue;
351
+ const dir = path.join(root, entryType, providerId);
352
+ const samplePath = path.join(dir, 'request.sample.json');
353
+ if (!fs.existsSync(samplePath)) {
354
+ continue;
323
355
  }
356
+ let stageFile;
357
+ const metaPath = path.join(dir, 'meta.json');
358
+ if (fs.existsSync(metaPath)) {
359
+ try {
360
+ const meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
361
+ stageFile = meta?.stageFile || meta?.originStage || null;
362
+ } catch (error) {
363
+ console.warn(`[capture] custom meta parse failed for ${providerId}/${entryType}: ${error.message}`);
364
+ }
365
+ }
366
+ return {
367
+ body: JSON.parse(fs.readFileSync(samplePath, 'utf-8')),
368
+ stageFile,
369
+ dir
370
+ };
324
371
  }
325
- return {
326
- body: JSON.parse(fs.readFileSync(samplePath, 'utf-8')),
327
- stageFile,
328
- dir
329
- };
372
+ return null;
330
373
  }
331
374
 
332
375
  function saveProviderSample(providerId, entryType, sourceStagePath, providedBody, updateGolden) {
@@ -427,24 +470,26 @@ async function captureProvider(providerEntry, entryType, port, options, sanitize
427
470
  async function main() {
428
471
  const options = parseArgs();
429
472
  ensureDir(TEMP_ROOT);
430
- const providers = listProviderConfigs();
473
+ const providers = [
474
+ ...listProviderConfigs(),
475
+ ...listCiGoldenProviders()
476
+ ];
477
+ if (!providers.length) {
478
+ console.warn('[capture] no provider configs or ci goldens detected; exiting');
479
+ return;
480
+ }
431
481
  const captured = new Set();
432
482
  const results = [];
433
483
  let portBase = 5800;
434
484
 
435
485
  for (const entry of providers) {
436
- const entryType = detectEntryType(entry.providerConfig);
486
+ const entryType = entry.entryTypeOverride || detectEntryType(entry.providerConfig);
437
487
  const entryDef = ENTRY_DEFS[entryType];
438
488
  if (!entryDef) continue;
439
489
  const captureKey = `${entry.providerId}:${entryType}`;
440
490
  if (captured.has(captureKey)) {
441
491
  continue;
442
492
  }
443
- const sampleExists = fs.existsSync(entryDef.samplePath);
444
- if (!sampleExists) {
445
- results.push({ provider: entry.providerId, entryType, status: 'skipped', reason: 'sample missing' });
446
- continue;
447
- }
448
493
  const custom = loadCustomSample(entry.providerId, entryType);
449
494
  if (custom) {
450
495
  console.log(`[capture] using custom sample for ${entry.providerId} (${entryType}) from ${custom.dir}`);
@@ -459,6 +504,11 @@ async function main() {
459
504
  });
460
505
  continue;
461
506
  }
507
+ const sampleExists = fs.existsSync(entryDef.samplePath);
508
+ if (!sampleExists) {
509
+ results.push({ provider: entry.providerId, entryType, status: 'skipped', reason: 'sample missing' });
510
+ continue;
511
+ }
462
512
  if (options.customOnly) {
463
513
  results.push({
464
514
  provider: entry.providerId,
@@ -64,6 +64,22 @@ function resolveProviderRuntime(configPath, target) {
64
64
  const auth = providerDef.auth || {};
65
65
  const apiKey = auth.apiKey || auth.value || process.env.C4M_API_KEY || process.env.RCC_PROVIDER_KEY;
66
66
  if (!apiKey) throw new Error(`Missing API key for ${providerId} (set in config or env)`);
67
+ const legacyCompatFields = [];
68
+ if (typeof providerDef.compatibility_profile === 'string') legacyCompatFields.push('compatibility_profile');
69
+ if (typeof providerDef.compat === 'string') legacyCompatFields.push('compat');
70
+ if (providerDef.compatibility && typeof providerDef.compatibility === 'object') {
71
+ if (typeof providerDef.compatibility.profile === 'string') legacyCompatFields.push('compatibility.profile');
72
+ if (typeof providerDef.compatibility.id === 'string') legacyCompatFields.push('compatibility.id');
73
+ }
74
+ if (legacyCompatFields.length > 0) {
75
+ throw new Error(
76
+ `Provider ${providerId} uses legacy compatibility field(s): ${legacyCompatFields.join(
77
+ ', '
78
+ )}. Rename to "compatibilityProfile".`
79
+ );
80
+ }
81
+ const compatProfile =
82
+ (typeof providerDef.compatibilityProfile === 'string' && providerDef.compatibilityProfile.trim()) || undefined;
67
83
  return {
68
84
  runtimeKey: `${providerId}.golden.${Date.now()}`,
69
85
  providerId,
@@ -72,7 +88,7 @@ function resolveProviderRuntime(configPath, target) {
72
88
  providerType: (providerDef.type || 'responses').toLowerCase(),
73
89
  endpoint: providerDef.baseURL || providerDef.baseUrl || providerDef.endpoint || 'https://api.example.net/v1',
74
90
  auth: { type: 'apikey', value: apiKey },
75
- compatibilityProfile: providerDef.compat || 'default',
91
+ compatibilityProfile: compatProfile,
76
92
  outboundProfile: 'openai-responses',
77
93
  defaultModel: modelId
78
94
  };
@@ -209,6 +209,22 @@ function resolveProviderRuntime(configPath, target) {
209
209
  const auth = providerDef.auth || {};
210
210
  const apiKey = auth.apiKey || auth.value || process.env.C4M_API_KEY;
211
211
  if (!apiKey) throw new Error(`Missing API key for ${providerId}. set in config or C4M_API_KEY env`);
212
+ const legacyCompatFields = [];
213
+ if (typeof providerDef.compatibility_profile === 'string') legacyCompatFields.push('compatibility_profile');
214
+ if (typeof providerDef.compat === 'string') legacyCompatFields.push('compat');
215
+ if (providerDef.compatibility && typeof providerDef.compatibility === 'object') {
216
+ if (typeof providerDef.compatibility.profile === 'string') legacyCompatFields.push('compatibility.profile');
217
+ if (typeof providerDef.compatibility.id === 'string') legacyCompatFields.push('compatibility.id');
218
+ }
219
+ if (legacyCompatFields.length > 0) {
220
+ throw new Error(
221
+ `Provider ${providerId} uses legacy compatibility field(s): ${legacyCompatFields.join(
222
+ ', '
223
+ )}. Rename to "compatibilityProfile".`
224
+ );
225
+ }
226
+ const compatProfile =
227
+ (typeof providerDef.compatibilityProfile === 'string' && providerDef.compatibilityProfile.trim()) || undefined;
212
228
  return {
213
229
  runtime: {
214
230
  runtimeKey: `${providerId}.replay.${Date.now()}`,
@@ -218,7 +234,7 @@ function resolveProviderRuntime(configPath, target) {
218
234
  providerType: (providerDef.type || 'responses').toLowerCase(),
219
235
  endpoint: providerDef.baseURL || providerDef.baseUrl || providerDef.endpoint || 'https://api.example.net/v1',
220
236
  auth: { type: 'apikey', value: apiKey },
221
- compatibilityProfile: providerDef.compat || 'default',
237
+ compatibilityProfile: compatProfile,
222
238
  outboundProfile: 'openai-responses',
223
239
  defaultModel: modelId
224
240
  },
@@ -0,0 +1,131 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import os from 'node:os';
6
+ import { fileURLToPath } from 'node:url';
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = path.dirname(__filename);
10
+ const PROJECT_ROOT = path.resolve(__dirname, '../..');
11
+ const TARGET_ROOT = path.join(PROJECT_ROOT, 'samples', 'ci-goldens');
12
+ const SOURCE_ROOT = path.join(os.homedir(), '.routecodex', 'golden_samples', 'new');
13
+
14
+ function ensureDir(dir) {
15
+ fs.mkdirSync(dir, { recursive: true });
16
+ }
17
+
18
+ function readJson(file) {
19
+ return JSON.parse(fs.readFileSync(file, 'utf-8'));
20
+ }
21
+
22
+ function writeIfChanged(target, content) {
23
+ const next = typeof content === 'string' ? content : JSON.stringify(content, null, 2);
24
+ if (fs.existsSync(target)) {
25
+ const prev = fs.readFileSync(target, 'utf-8');
26
+ if (prev === next) {
27
+ return false;
28
+ }
29
+ }
30
+ fs.writeFileSync(target, next);
31
+ return true;
32
+ }
33
+
34
+ function syncProvider(entryType, providerId) {
35
+ const sourceDir = path.join(SOURCE_ROOT, entryType, providerId);
36
+ const requestPath = path.join(sourceDir, 'request.sample.json');
37
+ if (!fs.existsSync(requestPath)) {
38
+ console.warn(`[sync-ci-goldens] skip ${entryType}/${providerId}: request.sample.json missing`);
39
+ return { status: 'skipped' };
40
+ }
41
+ const targetDir = path.join(TARGET_ROOT, entryType, providerId);
42
+ ensureDir(targetDir);
43
+ const targetRequest = path.join(targetDir, 'request.sample.json');
44
+ const changedRequest = writeIfChanged(targetRequest, fs.readFileSync(requestPath, 'utf-8'));
45
+ const sourceMetaPath = path.join(sourceDir, 'meta.json');
46
+ const targetMeta = path.join(targetDir, 'meta.json');
47
+ if (fs.existsSync(sourceMetaPath)) {
48
+ const meta = readJson(sourceMetaPath);
49
+ meta.source = 'ci-goldens';
50
+ writeIfChanged(targetMeta, meta);
51
+ } else if (!fs.existsSync(targetMeta)) {
52
+ const meta = {
53
+ providerId,
54
+ entryType,
55
+ capturedAt: new Date().toISOString(),
56
+ source: 'ci-goldens',
57
+ stageFile: null
58
+ };
59
+ writeIfChanged(targetMeta, meta);
60
+ }
61
+ return { status: changedRequest ? 'updated' : 'unchanged', targetDir };
62
+ }
63
+
64
+ function listEntries() {
65
+ if (!fs.existsSync(SOURCE_ROOT)) {
66
+ throw new Error(`Source golden samples missing: ${SOURCE_ROOT}`);
67
+ }
68
+ const entries = [];
69
+ for (const entryType of fs.readdirSync(SOURCE_ROOT)) {
70
+ const entryDir = path.join(SOURCE_ROOT, entryType);
71
+ if (!fs.statSync(entryDir).isDirectory()) continue;
72
+ for (const providerId of fs.readdirSync(entryDir)) {
73
+ const providerDir = path.join(entryDir, providerId);
74
+ if (!fs.statSync(providerDir).isDirectory()) continue;
75
+ entries.push({ entryType, providerId });
76
+ }
77
+ }
78
+ return entries;
79
+ }
80
+
81
+ function usage() {
82
+ console.log('Usage: node scripts/tools/sync-ci-goldens.mjs [--entry <type>] [--provider <id>]');
83
+ process.exit(0);
84
+ }
85
+
86
+ function parseArgs() {
87
+ const args = process.argv.slice(2);
88
+ const filters = { entry: null, provider: null };
89
+ for (let i = 0; i < args.length; i += 1) {
90
+ const arg = args[i];
91
+ if (arg === '--entry') {
92
+ filters.entry = args[++i] || null;
93
+ } else if (arg === '--provider') {
94
+ filters.provider = args[++i] || null;
95
+ } else if (arg === '--help' || arg === '-h') {
96
+ usage();
97
+ } else {
98
+ console.error(`Unknown argument: ${arg}`);
99
+ usage();
100
+ }
101
+ }
102
+ return filters;
103
+ }
104
+
105
+ function main() {
106
+ const filters = parseArgs();
107
+ const entries = listEntries().filter((item) => {
108
+ if (filters.entry && filters.entry !== item.entryType) return false;
109
+ if (filters.provider && filters.provider !== item.providerId) return false;
110
+ return true;
111
+ });
112
+ if (!entries.length) {
113
+ console.warn('[sync-ci-goldens] no matching samples found');
114
+ return;
115
+ }
116
+ let updated = 0;
117
+ let unchanged = 0;
118
+ for (const item of entries) {
119
+ const result = syncProvider(item.entryType, item.providerId);
120
+ if (result.status === 'updated') updated += 1;
121
+ else if (result.status === 'unchanged') unchanged += 1;
122
+ }
123
+ console.log(`[sync-ci-goldens] synced ${entries.length} provider samples (${updated} updated, ${unchanged} unchanged).`);
124
+ }
125
+
126
+ try {
127
+ main();
128
+ } catch (error) {
129
+ console.error('[sync-ci-goldens] failed:', error.message || error);
130
+ process.exit(1);
131
+ }