coding-tool-x 3.8.4 → 3.8.6

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 (434) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +86 -15
  3. package/dist/web/assets/AgentsDrawer-Db9wAyaZ.js +1 -0
  4. package/dist/web/assets/AgentsDrawer-Pw2k-ZAa.css +1 -0
  5. package/dist/web/assets/Analytics-C4gMzv7g.js +1 -0
  6. package/dist/web/assets/Analytics-CyHhR126.css +1 -0
  7. package/dist/web/assets/AssetPathField-Bd_nMFu3.js +1 -0
  8. package/dist/web/assets/ChatHistoryDrawer-B2qBQs9L.css +1 -0
  9. package/dist/web/assets/ChatHistoryDrawer-CwzJHyNP.js +1 -0
  10. package/dist/web/assets/CollapseItem-CTCfSiue.js +1 -0
  11. package/dist/web/assets/CommandsDrawer-BZJxlPvV.css +1 -0
  12. package/dist/web/assets/CommandsDrawer-vDghB1U9.js +1 -0
  13. package/dist/web/assets/ConfigExportDrawer-C-XDvlHc.js +1 -0
  14. package/dist/web/assets/ConfigExportDrawer-dwruRKWr.css +1 -0
  15. package/dist/web/assets/ConfigTemplates-CYY8XRED.js +1 -0
  16. package/dist/web/assets/ConfigTemplatesDrawer-BfDi8yO3.css +1 -0
  17. package/dist/web/assets/ConfigTemplatesDrawer-CbsJp76X.js +1 -0
  18. package/dist/web/assets/ConfigTemplatesPanel-CWyPtk67.css +1 -0
  19. package/dist/web/assets/ConfigTemplatesPanel-liMSTh7s.js +1 -0
  20. package/dist/web/assets/Divider-D28g1gwj.js +1 -0
  21. package/dist/web/assets/DrawerContent-ChXgZ1Xm.js +1 -0
  22. package/dist/web/assets/FavoritesDrawer-CG9eI9Ay.css +1 -0
  23. package/dist/web/assets/FavoritesDrawer-D6QSiYaL.js +1 -0
  24. package/dist/web/assets/GatewayConvertDrawer-CuNlf7AC.css +1 -0
  25. package/dist/web/assets/GatewayConvertDrawer-RvET6iNG.js +1 -0
  26. package/dist/web/assets/Home-CSuFuJ9Y.css +1 -0
  27. package/dist/web/assets/Home-P8eseZcf.js +1 -0
  28. package/dist/web/assets/MarkdownEditor-CYmLGYju.js +1 -0
  29. package/dist/web/assets/MarkdownEditor-qfVgSre_.css +1 -0
  30. package/dist/web/assets/McpDrawer-C0dnsZhQ.css +1 -0
  31. package/dist/web/assets/McpDrawer-Ch3bkKIq.js +1 -0
  32. package/dist/web/assets/McpFormDrawer-BRbDcawn.css +1 -0
  33. package/dist/web/assets/McpFormDrawer-BcTlgSpN.js +1 -0
  34. package/dist/web/assets/PluginManager-Dh2o0xkZ.js +1 -0
  35. package/dist/web/assets/PluginsDrawer-DM3QjprE.css +1 -0
  36. package/dist/web/assets/PluginsDrawer-deKQ5u3I.js +1 -0
  37. package/dist/web/assets/PluginsPanel-BofsgQgj.js +1 -0
  38. package/dist/web/assets/PluginsPanel-D6ZqITcR.css +1 -0
  39. package/dist/web/assets/ProjectList-677RLZ8O.css +1 -0
  40. package/dist/web/assets/ProjectList-DhSWJHTA.js +1 -0
  41. package/dist/web/assets/PromptsDrawer-BIgiYCEQ.css +1 -0
  42. package/dist/web/assets/PromptsDrawer-Cfb2FeeU.js +1 -0
  43. package/dist/web/assets/RecentSessionsDrawer-3eXamJhr.js +1 -0
  44. package/dist/web/assets/RecentSessionsDrawer-CKFsOYDM.css +1 -0
  45. package/dist/web/assets/SessionCard-CW0eHURI.js +1 -0
  46. package/dist/web/assets/SessionCard-aM2NHu6i.css +1 -0
  47. package/dist/web/assets/SessionList-BpYRz1cX.css +1 -0
  48. package/dist/web/assets/SessionList-DQfyDGWU.js +1 -0
  49. package/dist/web/assets/SettingsDrawer-Dw5NUgHC.css +1 -0
  50. package/dist/web/assets/SettingsDrawer-u_vwUuPS.js +1 -0
  51. package/dist/web/assets/SkillManager-C0GCT2Kb.css +1 -0
  52. package/dist/web/assets/SkillManager-DHMprJLA.js +1 -0
  53. package/dist/web/assets/SkillsDrawer-BCNme-aJ.css +1 -0
  54. package/dist/web/assets/SkillsDrawer-CkF3BpyC.js +1 -0
  55. package/dist/web/assets/SkillsPanel-B3Nm5j32.css +1 -0
  56. package/dist/web/assets/SkillsPanel-DWE4pFGx.js +1 -0
  57. package/dist/web/assets/SpeedTestDrawer-BgfG1Ked.js +1 -0
  58. package/dist/web/assets/SpeedTestDrawer-Dc0WToJ_.css +1 -0
  59. package/dist/web/assets/Upload-CNwaGOws.js +1 -0
  60. package/dist/web/assets/WorkspaceDrawer-Br5CeCzL.js +1 -0
  61. package/dist/web/assets/WorkspaceDrawer-CGnMu9p1.css +1 -0
  62. package/dist/web/assets/WorkspaceManager-B2O8-qIw.js +1 -0
  63. package/dist/web/assets/WorkspacePanel-5oDKit3c.js +1 -0
  64. package/dist/web/assets/WorkspacePanel-D0-zHTHP.css +1 -0
  65. package/dist/web/assets/_plugin-vue_export-helper-BeLZJZij.js +1 -0
  66. package/dist/web/assets/client-UDwXBP9i.js +1 -0
  67. package/dist/web/assets/clipboard-B46yyoz_.js +1 -0
  68. package/dist/web/assets/headers-cf-QapUq.js +1 -0
  69. package/dist/web/assets/icons-B2Vg-IOC.js +1 -0
  70. package/dist/web/assets/index-BMAUPq5K.js +2 -0
  71. package/dist/web/assets/index-Bi1MQHYX.css +1 -0
  72. package/dist/web/assets/light-Cp0t7v32.js +1 -0
  73. package/dist/web/assets/markdown-u082bbtj.js +1 -0
  74. package/dist/web/assets/platforms-CBkB45JP.js +1 -0
  75. package/dist/web/assets/platforms-FTpjxAT6.js +1 -0
  76. package/dist/web/assets/project-config-B2baPzYL.js +1 -0
  77. package/dist/web/assets/sessions-Cmy31slD.js +1 -0
  78. package/dist/web/assets/sessions-yC_oVVh8.js +1 -0
  79. package/dist/web/assets/skill-repo-input-B_fkGlUK.js +1 -0
  80. package/dist/web/assets/use-lock-html-scroll-DXIqmUW4.js +1 -0
  81. package/dist/web/assets/use-message-DoLM8C1C.js +1 -0
  82. package/dist/web/assets/useResponsiveDrawer-jbmqZuJs.js +1 -0
  83. package/dist/web/assets/vendors-D0c_0KLP.js +1 -0
  84. package/dist/web/assets/vue-vendor-BqZbbXHA.js +1 -0
  85. package/dist/web/assets/workspaces-mjyPVGS8.js +1 -0
  86. package/dist/web/assets/xml-CgY3RMlX.js +1 -0
  87. package/dist/web/index.html +12 -8
  88. package/docs/native-log-memory-2026-09-16.md +43 -0
  89. package/docs/superpowers/plans/2026-08-26-configuration-driven-cli-platform.md +1578 -0
  90. package/docs/superpowers/plans/2026-08-30-project-scoped-config-control.md +1260 -0
  91. package/docs/superpowers/plans/2026-09-01-omp-skill-refresh-boundary.md +213 -0
  92. package/docs/superpowers/plans/2026-09-01-realtime-logs-panel.md +125 -0
  93. package/docs/superpowers/plans/2026-09-02-config-driven-platform-boundary.md +86 -0
  94. package/docs/superpowers/plans/2026-09-02-platform-implementation-drivers.md +508 -0
  95. package/docs/superpowers/plans/2026-09-03-cli-platform-correctness-boundary.md +760 -0
  96. package/docs/superpowers/plans/2026-09-03-dependency-security-remediation.md +133 -0
  97. package/docs/superpowers/plans/2026-09-04-model-catalog-pricing-sync.md +1078 -0
  98. package/docs/superpowers/plans/2026-09-05-codex-project-scan-500.md +128 -0
  99. package/docs/superpowers/plans/2026-09-06-project-skill-path-fix.md +97 -0
  100. package/docs/superpowers/specs/2026-08-26-configuration-driven-cli-platform-design.md +408 -0
  101. package/docs/superpowers/specs/2026-08-30-project-scoped-config-control-design.md +467 -0
  102. package/docs/superpowers/specs/2026-09-01-claude-session-content-repair-design.md +111 -0
  103. package/docs/superpowers/specs/2026-09-01-omp-skill-refresh-boundary-design.md +83 -0
  104. package/docs/superpowers/specs/2026-09-01-realtime-logs-panel-design.md +27 -0
  105. package/docs/superpowers/specs/2026-09-02-config-driven-platform-boundary-design.md +64 -0
  106. package/docs/superpowers/specs/2026-09-02-platform-implementation-driver-boundary-design.md +123 -0
  107. package/docs/superpowers/specs/2026-09-03-cli-platform-correctness-boundary-design.md +249 -0
  108. package/docs/superpowers/specs/2026-09-03-dependency-security-remediation-design.md +87 -0
  109. package/docs/superpowers/specs/2026-09-04-model-catalog-pricing-sync-design.md +301 -0
  110. package/docs/superpowers/specs/2026-09-05-codex-project-scan-500-design.md +24 -0
  111. package/package.json +8 -4
  112. package/src/commands/channels.js +88 -191
  113. package/src/commands/cli-type.js +19 -11
  114. package/src/commands/daemon.js +104 -32
  115. package/src/commands/doctor.js +16 -9
  116. package/src/commands/export-config.js +23 -5
  117. package/src/commands/list.js +130 -52
  118. package/src/commands/logs.js +44 -27
  119. package/src/commands/platform-command-registry.js +78 -0
  120. package/src/commands/plugin.js +11 -5
  121. package/src/commands/port-config.js +34 -52
  122. package/src/commands/proxy-control.js +207 -145
  123. package/src/commands/proxy.js +2 -2
  124. package/src/commands/resume.js +87 -91
  125. package/src/commands/search.js +137 -67
  126. package/src/commands/stats.js +30 -22
  127. package/src/commands/switch.js +1 -1
  128. package/src/commands/toggle-proxy.js +94 -111
  129. package/src/commands/ui.js +67 -6
  130. package/src/commands/workspace.js +90 -8
  131. package/src/config/default.js +6 -53
  132. package/src/config/loader.js +61 -24
  133. package/src/config/model-metadata.js +49 -59
  134. package/src/config/model-metadata.json +4095 -99
  135. package/src/config/paths.js +181 -8
  136. package/src/index.js +134 -71
  137. package/src/platforms/access.js +196 -0
  138. package/src/{server/services → platforms}/agents-service.js +354 -197
  139. package/src/platforms/channel-auth-registry.js +165 -0
  140. package/src/platforms/channel-auth-service.js +107 -0
  141. package/src/{server/services → platforms}/commands-service.js +195 -115
  142. package/src/platforms/driver-registry.js +59 -0
  143. package/src/platforms/drivers/claude/api-operations.js +62 -0
  144. package/src/{server/services/channels.js → platforms/drivers/claude/channels-implementation.js} +131 -33
  145. package/src/platforms/drivers/claude/channels.js +39 -0
  146. package/src/{server/services/config-sync-service.js → platforms/drivers/claude/config-sync.js} +29 -5
  147. package/src/{server/services → platforms/drivers/claude}/health-check.js +4 -4
  148. package/src/platforms/drivers/claude/health.js +22 -0
  149. package/src/platforms/drivers/claude/mcp.js +9 -0
  150. package/src/{server/services/settings-manager.js → platforms/drivers/claude/native-config-implementation.js} +13 -4
  151. package/src/platforms/drivers/claude/native-config.js +46 -0
  152. package/src/platforms/drivers/claude/native-logs.js +72 -0
  153. package/src/{server/services/claude-openai-gateway.js → platforms/drivers/claude/openai-gateway.js} +8 -65
  154. package/src/platforms/drivers/claude/project-config.js +21 -0
  155. package/src/platforms/drivers/claude/projects.js +62 -0
  156. package/src/platforms/drivers/claude/prompts.js +9 -0
  157. package/src/{server/proxy-server.js → platforms/drivers/claude/proxy-implementation.js} +41 -278
  158. package/src/platforms/drivers/claude/proxy.js +16 -0
  159. package/src/platforms/drivers/claude/resource-sync.js +9 -0
  160. package/src/platforms/drivers/claude/session-history-adapter.js +313 -0
  161. package/src/{server/services/sessions.js → platforms/drivers/claude/sessions-implementation.js} +145 -35
  162. package/src/platforms/drivers/claude/sessions.js +76 -0
  163. package/src/{server/services/claude-statistics-service.js → platforms/drivers/claude/statistics-implementation.js} +2 -2
  164. package/src/platforms/drivers/claude/statistics.js +16 -0
  165. package/src/{server/services/claude-wire.js → platforms/drivers/claude/wire.js} +1 -1
  166. package/src/platforms/drivers/codex/api-operations.js +9 -0
  167. package/src/{server/services/codex-channels.js → platforms/drivers/codex/channels-implementation.js} +149 -14
  168. package/src/platforms/drivers/codex/channels.js +45 -0
  169. package/src/{server/services/codex-config.js → platforms/drivers/codex/config.js} +12 -2
  170. package/src/{server/services/codex-env-manager.js → platforms/drivers/codex/env-manager.js} +1 -1
  171. package/src/platforms/drivers/codex/mcp.js +9 -0
  172. package/src/{server/services/codex-settings-manager.js → platforms/drivers/codex/native-config-implementation.js} +15 -6
  173. package/src/platforms/drivers/codex/native-config.js +47 -0
  174. package/src/platforms/drivers/codex/native-logs.js +166 -0
  175. package/src/platforms/drivers/codex/project-config.js +21 -0
  176. package/src/platforms/drivers/codex/projects.js +26 -0
  177. package/src/platforms/drivers/codex/prompts.js +9 -0
  178. package/src/{server/codex-proxy-server.js → platforms/drivers/codex/proxy-implementation.js} +30 -233
  179. package/src/platforms/drivers/codex/proxy.js +16 -0
  180. package/src/platforms/drivers/codex/resource-sync.js +9 -0
  181. package/src/{server/services/session-history-adapters/codex.js → platforms/drivers/codex/session-history-adapter.js} +37 -23
  182. package/src/{server/services/codex-sessions.js → platforms/drivers/codex/sessions-implementation.js} +81 -75
  183. package/src/platforms/drivers/codex/sessions.js +36 -0
  184. package/src/{server/services/codex-statistics-service.js → platforms/drivers/codex/statistics-implementation.js} +2 -2
  185. package/src/platforms/drivers/codex/statistics.js +16 -0
  186. package/src/platforms/drivers/gemini/api-operations.js +9 -0
  187. package/src/{server/services/gemini-channels.js → platforms/drivers/gemini/channels-implementation.js} +202 -37
  188. package/src/platforms/drivers/gemini/channels.js +39 -0
  189. package/src/{server/services/gemini-config.js → platforms/drivers/gemini/config.js} +12 -2
  190. package/src/platforms/drivers/gemini/mcp.js +9 -0
  191. package/src/{server/services/gemini-settings-manager.js → platforms/drivers/gemini/native-config-implementation.js} +14 -5
  192. package/src/platforms/drivers/gemini/native-config.js +39 -0
  193. package/src/platforms/drivers/gemini/native-logs.js +180 -0
  194. package/src/platforms/drivers/gemini/project-config.js +21 -0
  195. package/src/platforms/drivers/gemini/projects.js +26 -0
  196. package/src/platforms/drivers/gemini/prompts.js +9 -0
  197. package/src/{server/gemini-proxy-server.js → platforms/drivers/gemini/proxy-implementation.js} +36 -267
  198. package/src/platforms/drivers/gemini/proxy.js +16 -0
  199. package/src/platforms/drivers/gemini/resource-sync.js +9 -0
  200. package/src/{server/services/session-history-adapters/gemini.js → platforms/drivers/gemini/session-history-adapter.js} +70 -18
  201. package/src/{server/services/gemini-sessions.js → platforms/drivers/gemini/sessions-implementation.js} +53 -37
  202. package/src/platforms/drivers/gemini/sessions.js +47 -0
  203. package/src/{server/services/gemini-statistics-service.js → platforms/drivers/gemini/statistics-implementation.js} +2 -2
  204. package/src/platforms/drivers/gemini/statistics.js +16 -0
  205. package/src/{server/services/gemini-wire.js → platforms/drivers/gemini/wire.js} +1 -1
  206. package/src/platforms/drivers/generic-filesystem.js +166 -0
  207. package/src/platforms/drivers/generic-jsonl.js +141 -0
  208. package/src/platforms/drivers/generic-mcp.js +44 -0
  209. package/src/platforms/drivers/generic-openai-compatible.js +45 -0
  210. package/src/platforms/drivers/generic-prompt.js +41 -0
  211. package/src/platforms/drivers/legacy.js +677 -0
  212. package/src/platforms/drivers/native-config-snapshot.js +125 -0
  213. package/src/platforms/drivers/native-log-utils.js +1292 -0
  214. package/src/platforms/drivers/notification-hooks.js +578 -0
  215. package/src/{server/api/omp-config.js → platforms/drivers/omp/api-operations.js} +52 -64
  216. package/src/{server/services/omp-auth-provider-worker.js → platforms/drivers/omp/auth-provider-worker.js} +1 -1
  217. package/src/{server/services/omp-auth-providers.js → platforms/drivers/omp/auth-providers.js} +1 -1
  218. package/src/{server/services/omp-channels.js → platforms/drivers/omp/channels-implementation.js} +166 -39
  219. package/src/platforms/drivers/omp/channels.js +58 -0
  220. package/src/{server/services/omp-config.js → platforms/drivers/omp/config.js} +53 -13
  221. package/src/{server/services/omp-gateway-routing.js → platforms/drivers/omp/gateway-routing.js} +14 -7
  222. package/src/{server/services/omp-gateway.js → platforms/drivers/omp/gateway.js} +4 -262
  223. package/src/platforms/drivers/omp/mcp.js +9 -0
  224. package/src/{server/services/omp-settings-manager.js → platforms/drivers/omp/native-config-implementation.js} +75 -10
  225. package/src/platforms/drivers/omp/native-config.js +46 -0
  226. package/src/platforms/drivers/omp/native-logs.js +52 -0
  227. package/src/{server/services/omp-native-plugin-adapter.js → platforms/drivers/omp/native-plugin-adapter.js} +91 -4
  228. package/src/platforms/drivers/omp/project-config.js +29 -0
  229. package/src/platforms/drivers/omp/projects.js +20 -0
  230. package/src/platforms/drivers/omp/prompts.js +9 -0
  231. package/src/{server/omp-proxy-server.js → platforms/drivers/omp/proxy-implementation.js} +6 -15
  232. package/src/platforms/drivers/omp/proxy.js +16 -0
  233. package/src/platforms/drivers/omp/resource-sync.js +9 -0
  234. package/src/{server/services/session-history-adapters/omp.js → platforms/drivers/omp/session-history-adapter.js} +42 -18
  235. package/src/{server/services/omp-sessions.js → platforms/drivers/omp/sessions-implementation.js} +130 -94
  236. package/src/platforms/drivers/omp/sessions.js +36 -0
  237. package/src/{server/services/omp-skill-discovery.js → platforms/drivers/omp/skill-discovery.js} +159 -32
  238. package/src/{server/services/omp-skill-settings-service.js → platforms/drivers/omp/skill-settings.js} +1 -1
  239. package/src/{server/services/omp-statistics-service.js → platforms/drivers/omp/statistics-implementation.js} +3 -3
  240. package/src/platforms/drivers/omp/statistics.js +16 -0
  241. package/src/platforms/drivers/opencode/api-operations.js +9 -0
  242. package/src/{server/services/opencode-channels.js → platforms/drivers/opencode/channels-implementation.js} +28 -9
  243. package/src/platforms/drivers/opencode/channels.js +40 -0
  244. package/src/platforms/drivers/opencode/conversion.js +41 -0
  245. package/src/{server/services/opencode-gateway-converter.js → platforms/drivers/opencode/gateway-converter.js} +1 -1
  246. package/src/platforms/drivers/opencode/mcp.js +9 -0
  247. package/src/{server/services/opencode-settings-manager.js → platforms/drivers/opencode/native-config-implementation.js} +17 -6
  248. package/src/platforms/drivers/opencode/native-config.js +26 -0
  249. package/src/platforms/drivers/opencode/native-logs.js +133 -0
  250. package/src/platforms/drivers/opencode/project-config.js +98 -0
  251. package/src/platforms/drivers/opencode/projects.js +20 -0
  252. package/src/platforms/drivers/opencode/prompts.js +9 -0
  253. package/src/{server/opencode-proxy-server.js → platforms/drivers/opencode/proxy-implementation.js} +27 -362
  254. package/src/platforms/drivers/opencode/proxy.js +16 -0
  255. package/src/platforms/drivers/opencode/resource-sync.js +9 -0
  256. package/src/{server/services/opencode-sessions.js → platforms/drivers/opencode/sessions-implementation.js} +211 -64
  257. package/src/platforms/drivers/opencode/sessions.js +48 -0
  258. package/src/{server/services/opencode-statistics-service.js → platforms/drivers/opencode/statistics-implementation.js} +2 -2
  259. package/src/platforms/drivers/opencode/statistics.js +16 -0
  260. package/src/platforms/drivers/secure-file-driver.js +272 -0
  261. package/src/platforms/drivers/unsupported.js +7 -0
  262. package/src/platforms/manifest-schema.js +398 -0
  263. package/src/platforms/manifests/claude.json +489 -0
  264. package/src/platforms/manifests/codex.json +448 -0
  265. package/src/platforms/manifests/gemini.json +447 -0
  266. package/src/platforms/manifests/omp.json +496 -0
  267. package/src/platforms/manifests/opencode.json +422 -0
  268. package/src/{server/services → platforms}/native-oauth-adapters.js +98 -418
  269. package/src/{server/services → platforms}/notification-hooks.js +190 -1023
  270. package/src/platforms/oauth-credentials-service.js +635 -0
  271. package/src/platforms/path-resolver.js +179 -0
  272. package/src/platforms/platform-path-context.js +305 -0
  273. package/src/platforms/registry.js +248 -0
  274. package/src/platforms/remote-notification-providers.js +678 -0
  275. package/src/platforms/runtime.js +259 -0
  276. package/src/{server/services/session-history-adapters/index.js → platforms/session-history-adapters.js} +8 -1
  277. package/src/plugins/plugin-loader.js +2 -2
  278. package/src/plugins/plugin-manager.js +2 -2
  279. package/src/plugins/registry.js +27 -10
  280. package/src/reset-config.js +21 -5
  281. package/src/server/api/agents.js +46 -72
  282. package/src/server/api/commands.js +16 -6
  283. package/src/server/api/config-registry.js +206 -59
  284. package/src/server/api/config-sync.js +3 -2
  285. package/src/server/api/config-templates.js +20 -4
  286. package/src/server/api/config.js +111 -102
  287. package/src/server/api/convert.js +54 -178
  288. package/src/server/api/dashboard.js +168 -102
  289. package/src/server/api/hooks.js +1 -1
  290. package/src/server/api/mcp.js +224 -84
  291. package/src/server/api/platform-api-config.js +102 -0
  292. package/src/server/api/platform-route-factory.js +215 -0
  293. package/src/server/api/platform-routes.js +26 -0
  294. package/src/server/api/platforms.js +110 -0
  295. package/src/server/api/plugins.js +57 -57
  296. package/src/server/api/project-config.js +168 -0
  297. package/src/server/api/prompts.js +58 -45
  298. package/src/server/api/settings.js +55 -8
  299. package/src/server/api/skills.js +262 -133
  300. package/src/server/api/ui-config.js +2 -2
  301. package/src/server/api/validation-errors.js +18 -4
  302. package/src/server/api/workspaces.js +47 -12
  303. package/src/server/index.js +164 -213
  304. package/src/server/platform-context.js +81 -0
  305. package/src/server/services/channel-balance.js +133 -24
  306. package/src/server/services/channel-health.js +21 -6
  307. package/src/server/services/channel-scheduler.js +29 -39
  308. package/src/server/services/channel-sync-utils.js +23 -13
  309. package/src/server/services/config-export-service.js +350 -319
  310. package/src/server/services/config-registry-service.js +280 -54
  311. package/src/server/services/config-sync-manager.js +172 -944
  312. package/src/server/services/config-templates-service.js +419 -353
  313. package/src/server/services/control-manifest-store.js +206 -0
  314. package/src/server/services/dashboard-snapshot-worker.js +656 -100
  315. package/src/server/services/effective-control-service.js +557 -0
  316. package/src/server/services/local-resource-index.js +314 -0
  317. package/src/server/services/mcp-service.js +607 -977
  318. package/src/server/services/model-detector.js +15 -10
  319. package/src/server/services/native-log-channel-resolver.js +214 -0
  320. package/src/server/services/native-log-observer.js +291 -0
  321. package/src/server/services/network-access.js +12 -2
  322. package/src/server/services/platform-catalog.js +82 -0
  323. package/src/server/services/platform-resolution.js +32 -29
  324. package/src/server/services/plugins-service.js +376 -122
  325. package/src/server/services/project-config-service.js +550 -0
  326. package/src/server/services/project-path-validation.js +31 -0
  327. package/src/server/services/prompts-service.js +267 -233
  328. package/src/server/services/proxy-runtime.js +7 -4
  329. package/src/server/services/remote-credential-cache.js +43 -0
  330. package/src/server/services/repo-scanner-base.js +182 -75
  331. package/src/server/services/request-logger.js +7 -4
  332. package/src/server/services/session-history-index.js +876 -214
  333. package/src/server/services/session-history-worker.js +228 -33
  334. package/src/server/services/session-snapshot-worker.js +124 -34
  335. package/src/server/services/session-snapshots.js +4 -2
  336. package/src/server/services/skill-artifact-store.js +355 -0
  337. package/src/server/services/skill-format-adapters.js +65 -0
  338. package/src/server/services/skill-projection-service.js +323 -0
  339. package/src/server/services/skill-refresh-task-service.js +275 -0
  340. package/src/server/services/skill-service.js +2020 -677
  341. package/src/server/services/snapshot-cache.js +30 -8
  342. package/src/server/services/speed-test.js +8 -8
  343. package/src/server/services/sqlite-connection.js +19 -3
  344. package/src/server/services/statistics-service.js +240 -57
  345. package/src/server/services/ui-config.js +107 -76
  346. package/src/server/services/usage-log-utils.js +98 -0
  347. package/src/server/services/web-build.js +1 -1
  348. package/src/server/services/workspace-service.js +59 -29
  349. package/src/server/utils/pricing.js +10 -19
  350. package/src/server/websocket-server.js +153 -28
  351. package/src/{server/services/base → shared}/base-channel-service.js +156 -28
  352. package/src/shared/capability-driver.js +104 -0
  353. package/src/shared/channel-auth-policy.js +27 -0
  354. package/src/shared/channel-driver.js +161 -0
  355. package/src/shared/driver-factories/api.js +180 -0
  356. package/src/shared/driver-factories/mcp.js +244 -0
  357. package/src/shared/driver-factories/projects.js +121 -0
  358. package/src/shared/driver-factories/prompts.js +58 -0
  359. package/src/shared/driver-factories/resource-sync.js +375 -0
  360. package/src/shared/driver-result.js +78 -0
  361. package/src/shared/mcp-format.js +189 -0
  362. package/src/shared/platforms.js +104 -167
  363. package/src/shared/project-config.js +512 -0
  364. package/src/shared/proxy-driver.js +57 -0
  365. package/src/{server/services/base → shared}/response-usage-parser.js +8 -4
  366. package/src/ui/menu.js +91 -67
  367. package/src/utils/port-helper.js +35 -19
  368. package/src/utils/session.js +1 -1
  369. package/dist/web/assets/Analytics-COEUXmd4.js +0 -25
  370. package/dist/web/assets/Analytics-RNn1BUbG.css +0 -1
  371. package/dist/web/assets/ConfigTemplates-DLLkS53H.js +0 -1
  372. package/dist/web/assets/Home-DCEyLzwX.js +0 -1
  373. package/dist/web/assets/Home-DYVy4cGZ.css +0 -1
  374. package/dist/web/assets/PluginManager-Oe1ZHoMO.js +0 -1
  375. package/dist/web/assets/ProjectList-CWjuAFof.js +0 -1
  376. package/dist/web/assets/ProjectList-VeMNXfgO.css +0 -1
  377. package/dist/web/assets/SessionList-C4chCPxK.css +0 -1
  378. package/dist/web/assets/SessionList-v3ehGg9U.js +0 -1
  379. package/dist/web/assets/SkillManager-BsX8Qpj0.css +0 -1
  380. package/dist/web/assets/SkillManager-DUF_qP5S.js +0 -1
  381. package/dist/web/assets/WorkspaceManager-CjAWBosO.js +0 -1
  382. package/dist/web/assets/icons-nGdSKQn2.js +0 -1
  383. package/dist/web/assets/index-D5hyHPj7.js +0 -2
  384. package/dist/web/assets/index-bJRG3RXH.css +0 -1
  385. package/dist/web/assets/markdown-BNJfEgCA.js +0 -1
  386. package/dist/web/assets/markdown-BfC0goYb.css +0 -10
  387. package/dist/web/assets/naive-ui-BALVpMgd.js +0 -1
  388. package/dist/web/assets/vendors-DQA4Hw9v.js +0 -7
  389. package/dist/web/assets/vue-vendor-B5RqjEjQ.js +0 -45
  390. package/src/config/model-pricing.js +0 -35
  391. package/src/server/api/channels.js +0 -439
  392. package/src/server/api/claude-hooks.js +0 -56
  393. package/src/server/api/claude-statistics.js +0 -57
  394. package/src/server/api/codex-channels.js +0 -491
  395. package/src/server/api/codex-projects.js +0 -117
  396. package/src/server/api/codex-proxy.js +0 -293
  397. package/src/server/api/codex-sessions.js +0 -652
  398. package/src/server/api/codex-statistics.js +0 -57
  399. package/src/server/api/gemini-channels.js +0 -392
  400. package/src/server/api/gemini-projects.js +0 -103
  401. package/src/server/api/gemini-proxy.js +0 -231
  402. package/src/server/api/gemini-sessions.js +0 -550
  403. package/src/server/api/gemini-statistics.js +0 -57
  404. package/src/server/api/health-check.js +0 -31
  405. package/src/server/api/oauth-credentials.js +0 -198
  406. package/src/server/api/omp-channels.js +0 -575
  407. package/src/server/api/omp-projects.js +0 -88
  408. package/src/server/api/omp-proxy.js +0 -125
  409. package/src/server/api/omp-sessions.js +0 -386
  410. package/src/server/api/omp-statistics.js +0 -40
  411. package/src/server/api/opencode-channels.js +0 -471
  412. package/src/server/api/opencode-projects.js +0 -110
  413. package/src/server/api/opencode-proxy.js +0 -354
  414. package/src/server/api/opencode-sessions.js +0 -466
  415. package/src/server/api/opencode-statistics.js +0 -57
  416. package/src/server/api/projects.js +0 -120
  417. package/src/server/api/proxy.js +0 -330
  418. package/src/server/api/sessions.js +0 -810
  419. package/src/server/services/oauth-credentials-service.js +0 -945
  420. package/src/server/services/omp-session-log-observer.js +0 -138
  421. package/src/server/services/opencode-gateway-adapters.js +0 -43
  422. package/src/server/services/project-snapshots.js +0 -59
  423. package/src/server/services/proxy-log-helper.js +0 -298
  424. package/src/server/services/session-cache.js +0 -127
  425. package/src/server/services/session-converter.js +0 -582
  426. package/src/server/services/session-history-adapters/claude.js +0 -235
  427. /package/src/{server/services/claude-token-recovery.js → platforms/drivers/claude/token-recovery.js} +0 -0
  428. /package/src/{server/services/codex-parser.js → platforms/drivers/codex/parser.js} +0 -0
  429. /package/src/{server/services/codex-speed-test-template.json → platforms/drivers/codex/speed-test-template.json} +0 -0
  430. /package/src/{server/services/codex-wire.js → platforms/drivers/codex/wire.js} +0 -0
  431. /package/src/{server/services/omp-auth-gateway-client.js → platforms/drivers/omp/auth-gateway-client.js} +0 -0
  432. /package/src/{server/services/opencode-normalization.js → platforms/drivers/opencode/normalization.js} +0 -0
  433. /package/src/{server/services → shared}/config-artifact-paths.js +0 -0
  434. /package/src/{server/services/base → shared}/proxy-utils.js +0 -0
@@ -0,0 +1,1078 @@
1
+ # Model Catalog Pricing Sync Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Use Models.dev as the single curated source for a daily, reviewable model metadata snapshot that ships offline, filters models per tool, and preserves user-edited metadata.
6
+
7
+ **Architecture:** A dependency-free Node synchronizer fetches `https://models.dev/api.json`, applies a checked-in popular-model policy, normalizes provider-scoped entries into the existing `src/config/model-metadata.json` shape, and leaves the previous snapshot untouched on failure. GitHub Actions runs it daily and opens a PR. Runtime code reads the packaged snapshot only; `modelMetadataOverrides` and `modelDefinitions` remain the higher-priority local layer, and the OMP catalog-metadata operation reads the snapshot without invoking the network or OMP's external model command.
8
+
9
+ **Tech Stack:** Node.js >=22.13.0 with built-in `fetch`, JSON, Express, Vue 3, Vitest, GitHub Actions, and the existing npm scripts.
10
+
11
+ ---
12
+
13
+ ## Files and ownership
14
+
15
+ ### New files
16
+
17
+ - `scripts/model-catalog-policy.json`: explicit provider/model-family allowlist, runtime ID mode, tool visibility, and minimum accepted result size.
18
+ - `scripts/sync-model-catalog.js`: pure source validation/normalization functions plus the CLI entry point that writes the generated snapshot and a temporary PR report.
19
+ - `tests/unit/scripts/model-catalog-sync.test.js`: deterministic fixture tests for filtering, ID mapping, prices, preservation, and invalid upstream data.
20
+ - `src/web/src/composables/__tests__/useDefaultModels.test.js`: front-end tool-specific model grouping tests.
21
+ - `.github/workflows/sync-model-catalog.yml`: daily and manually dispatched synchronizer that opens a PR only for a validated diff.
22
+
23
+ ### Modified files
24
+
25
+ - `package.json`: add `sync:model-catalog` without adding a runtime dependency.
26
+ - `src/config/model-metadata.json`: generated snapshot; never hand-edit generated model entries.
27
+ - `src/config/model-metadata.js`: expose snapshot provenance and tool-aware lookup helpers while preserving old aliases and fallback behavior.
28
+ - `src/server/api/settings.js`: return source information and tool-grouped catalog data without changing existing response fields.
29
+ - `src/platforms/drivers/codex/proxy-implementation.js`: remove the duplicate OpenAI model price table and family fallback.
30
+ - `src/platforms/drivers/gemini/proxy-implementation.js`: remove the duplicate Gemini model price table and family fallback.
31
+ - `src/platforms/drivers/gemini/sessions-implementation.js`: use the shared pricing resolver/default object instead of literal rates.
32
+ - `src/platforms/drivers/opencode/proxy-implementation.js`: remove the duplicate OpenAI model price table and family fallback.
33
+ - `src/platforms/drivers/omp/channels-implementation.js`: add local snapshot-backed `getCatalogMetadata`.
34
+ - `src/platforms/drivers/omp/channels.js`: expose `catalogMetadata` to the descriptor route.
35
+ - `src/web/src/composables/useDefaultModels.js`: group catalog entries using `toolTypes`, with legacy prefix fallback.
36
+ - `src/web/src/components/SettingsDrawer.vue`: display the Models.dev reference-price label and use metadata/provider fields for filters.
37
+ - `src/web/src/components/channel/BaseChannelPanel.vue`: explain the offline metadata action versus live model probing.
38
+ - `src/web/src/components/channel/channelPanelFactories.js`: allow offline OMP metadata retrieval without requiring a Provider Key.
39
+ - `tests/unit/config/model-metadata.test.js`: test provenance and tool-aware lookup.
40
+ - `tests/unit/api/settings.test.js`: test source and tool-group response fields.
41
+ - `tests/unit/services/proxy-cost.test.js`: prove shared metadata/default pricing is used instead of stale family tables.
42
+ - `tests/unit/platforms/drivers/omp-channels.test.js`: test the descriptor driver operation contract.
43
+ - `tests/unit/services/omp-catalog-metadata.test.js`: test local OMP metadata selection and no external lookup.
44
+
45
+ ---
46
+
47
+ ## Task 1: Freeze the synchronizer contract with policy and failing tests
48
+
49
+ **Files:**
50
+
51
+ - Create: `scripts/model-catalog-policy.json`
52
+ - Create: `tests/unit/scripts/model-catalog-sync.test.js`
53
+
54
+ - [ ] **Step 1: Add the explicit policy file.**
55
+
56
+ Create `scripts/model-catalog-policy.json` with this exact initial policy. Provider entries refer to the outer provider keys in Models.dev `api.json`; compatible providers retain a provider prefix in their runtime model ID.
57
+
58
+ ```json
59
+ {
60
+ "sourceUrl": "https://models.dev/api.json",
61
+ "minimumModelCount": 12,
62
+ "providers": {
63
+ "openai": {
64
+ "runtimeMode": "direct",
65
+ "toolTypes": ["codex", "opencode", "omp"],
66
+ "modelPatterns": ["^(gpt-|o[134](?:-|$))"]
67
+ },
68
+ "anthropic": {
69
+ "runtimeMode": "direct",
70
+ "toolTypes": ["claude", "opencode", "omp"],
71
+ "modelPatterns": ["^claude-"]
72
+ },
73
+ "google": {
74
+ "runtimeMode": "direct",
75
+ "toolTypes": ["gemini", "opencode", "omp"],
76
+ "modelPatterns": ["^gemini-"]
77
+ },
78
+ "deepseek": {
79
+ "runtimeMode": "compatible",
80
+ "toolTypes": ["opencode", "omp"],
81
+ "modelPatterns": ["^deepseek-"]
82
+ },
83
+ "alibaba": {
84
+ "runtimeMode": "compatible",
85
+ "toolTypes": ["opencode", "omp"],
86
+ "modelPatterns": ["^(qwen|deepseek-)"]
87
+ },
88
+ "moonshotai": {
89
+ "runtimeMode": "compatible",
90
+ "toolTypes": ["opencode", "omp"],
91
+ "modelPatterns": ["^kimi-"]
92
+ },
93
+ "zai": {
94
+ "runtimeMode": "compatible",
95
+ "toolTypes": ["opencode", "omp"],
96
+ "modelPatterns": ["^glm-"]
97
+ },
98
+ "mistral": {
99
+ "runtimeMode": "compatible",
100
+ "toolTypes": ["opencode", "omp"],
101
+ "modelPatterns": ["^(mistral|codestral|devstral)"]
102
+ },
103
+ "meta": {
104
+ "runtimeMode": "compatible",
105
+ "toolTypes": ["opencode", "omp"],
106
+ "modelPatterns": ["^(llama|meta-llama)"]
107
+ },
108
+ "xai": {
109
+ "runtimeMode": "compatible",
110
+ "toolTypes": ["opencode", "omp"],
111
+ "modelPatterns": ["^grok-"]
112
+ }
113
+ }
114
+ }
115
+ ```
116
+
117
+ - [ ] **Step 2: Write the normalizer tests before the implementation.**
118
+
119
+ Create `tests/unit/scripts/model-catalog-sync.test.js` using a source fixture shaped like the real Models.dev response: outer provider key, then `models`, then a model whose `id` is provider-local.
120
+
121
+ ```js
122
+ 'use strict';
123
+
124
+ const {
125
+ buildSnapshot,
126
+ validateSourcePayload,
127
+ validateSnapshot
128
+ } = require('../../../scripts/sync-model-catalog');
129
+
130
+ const policy = {
131
+ sourceUrl: 'https://models.dev/api.json',
132
+ minimumModelCount: 1,
133
+ providers: {
134
+ openai: {
135
+ runtimeMode: 'direct',
136
+ toolTypes: ['codex', 'opencode', 'omp'],
137
+ modelPatterns: ['^(gpt-|o[134](?:-|$))']
138
+ },
139
+ deepseek: {
140
+ runtimeMode: 'compatible',
141
+ toolTypes: ['opencode', 'omp'],
142
+ modelPatterns: ['^deepseek-']
143
+ }
144
+ }
145
+ };
146
+
147
+ const source = {
148
+ openai: {
149
+ id: 'openai',
150
+ models: {
151
+ 'gpt-5.5': {
152
+ id: 'gpt-5.5',
153
+ name: 'GPT-5.5',
154
+ family: 'gpt',
155
+ reasoning: true,
156
+ tool_call: true,
157
+ modalities: { input: ['text', 'image'], output: ['text'] },
158
+ limit: { context: 1050000, output: 128000 },
159
+ cost: { input: 5, output: 30, cache_read: 0.5 }
160
+ },
161
+ 'chatgpt-image-latest': {
162
+ id: 'chatgpt-image-latest',
163
+ name: 'ChatGPT Image',
164
+ family: 'image',
165
+ tool_call: false,
166
+ modalities: { input: ['text'], output: ['image'] },
167
+ limit: { context: 128000, output: 1 },
168
+ cost: { input: 1, output: 1 }
169
+ }
170
+ }
171
+ },
172
+ deepseek: {
173
+ id: 'deepseek',
174
+ models: {
175
+ 'deepseek-v4-pro': {
176
+ id: 'deepseek-v4-pro',
177
+ name: 'DeepSeek V4 Pro',
178
+ family: 'deepseek-thinking',
179
+ reasoning: true,
180
+ tool_call: true,
181
+ modalities: { input: ['text'], output: ['text'] },
182
+ limit: { context: 1000000, output: 384000 },
183
+ cost: { input: 0.435, output: 0.87, cache_read: 0.003625 }
184
+ },
185
+ 'deepseek-no-price': {
186
+ id: 'deepseek-no-price',
187
+ family: 'deepseek',
188
+ tool_call: true,
189
+ modalities: { input: ['text'], output: ['text'] },
190
+ limit: { context: 128000, output: 8192 }
191
+ }
192
+ }
193
+ }
194
+ };
195
+
196
+ describe('model catalog synchronizer contract', () => {
197
+ test('accepts a provider-scoped Models.dev payload', () => {
198
+ expect(() => validateSourcePayload(source)).not.toThrow();
199
+ });
200
+
201
+ test('normalizes direct and compatible IDs and maps cost fields', () => {
202
+ const result = buildSnapshot(source, {
203
+ policy,
204
+ previousSnapshot: { aliases: {}, models: {} },
205
+ updatedAt: '2026-09-04'
206
+ });
207
+
208
+ expect(result.snapshot.models['gpt-5.5']).toMatchObject({
209
+ sourceId: 'openai/gpt-5.5',
210
+ provider: 'openai',
211
+ toolTypes: ['codex', 'opencode', 'omp'],
212
+ pricing: { input: 5, output: 30, cacheRead: 0.5 }
213
+ });
214
+ expect(result.snapshot.models['deepseek/deepseek-v4-pro']).toMatchObject({
215
+ sourceId: 'deepseek/deepseek-v4-pro',
216
+ provider: 'deepseek',
217
+ toolTypes: ['opencode', 'omp']
218
+ });
219
+ expect(result.snapshot.aliases['deepseek-v4-pro']).toBe('deepseek/deepseek-v4-pro');
220
+ });
221
+
222
+ test('excludes non-text and unpriced entries and reports them', () => {
223
+ const result = buildSnapshot(source, {
224
+ policy,
225
+ previousSnapshot: { aliases: {}, models: {} },
226
+ updatedAt: '2026-09-04'
227
+ });
228
+
229
+ expect(result.snapshot.models).not.toHaveProperty('chatgpt-image-latest');
230
+ expect(result.snapshot.models).not.toHaveProperty('deepseek/deepseek-no-price');
231
+ expect(result.warnings.join('\n')).toMatch(/chatgpt-image-latest|deepseek-no-price/);
232
+ });
233
+
234
+ test('preserves an existing model omitted by the source', () => {
235
+ const previousSnapshot = {
236
+ aliases: {},
237
+ defaultModels: { claude: [], codex: [], gemini: [] },
238
+ models: {
239
+ 'legacy-model': {
240
+ limit: { context: 8192, output: 1024 },
241
+ pricing: { input: 1, output: 2 }
242
+ }
243
+ }
244
+ };
245
+ const result = buildSnapshot(source, {
246
+ policy,
247
+ previousSnapshot,
248
+ updatedAt: '2026-09-04'
249
+ });
250
+
251
+ expect(result.snapshot.models['legacy-model']).toEqual(previousSnapshot.models['legacy-model']);
252
+ expect(result.changes.removed).toContain('legacy-model');
253
+ });
254
+
255
+ test('reports price and limit changes deterministically', () => {
256
+ const previousSnapshot = {
257
+ aliases: {},
258
+ models: {
259
+ 'gpt-5.5': {
260
+ sourceId: 'openai/gpt-5.5',
261
+ limit: { context: 500000, output: 64000 },
262
+ pricing: { input: 4, output: 20 }
263
+ }
264
+ }
265
+ };
266
+ const result = buildSnapshot(source, {
267
+ policy,
268
+ previousSnapshot,
269
+ updatedAt: '2026-09-04'
270
+ });
271
+ expect(result.changes.updated).toContain('gpt-5.5');
272
+ expect(result.changes.priceChanged).toContain('gpt-5.5');
273
+ expect(result.changes.limitChanged).toContain('gpt-5.5');
274
+ expect(result.snapshot.models['gpt-5.5'].limit).toEqual({ context: 1050000, output: 128000 });
275
+ expect(result.snapshot.models['gpt-5.5'].pricing.input).toBe(5);
276
+ });
277
+
278
+ test('keeps the previous snapshot date when normalized content is unchanged', () => {
279
+ const first = buildSnapshot(source, {
280
+ policy,
281
+ previousSnapshot: { aliases: {}, models: {} },
282
+ updatedAt: '2026-09-04'
283
+ }).snapshot;
284
+ const second = buildSnapshot(source, {
285
+ policy,
286
+ previousSnapshot: first,
287
+ updatedAt: '2026-09-05'
288
+ }).snapshot;
289
+
290
+ expect(second.lastUpdated).toBe('2026-09-04');
291
+ });
292
+
293
+ test('rejects duplicate runtime IDs and abnormal result sizes', () => {
294
+ const duplicatePolicy = {
295
+ ...policy,
296
+ providers: {
297
+ ...policy.providers,
298
+ mirror: {
299
+ runtimeMode: 'direct',
300
+ toolTypes: ['codex'],
301
+ modelPatterns: ['^gpt-']
302
+ }
303
+ }
304
+ };
305
+ const duplicateSource = {
306
+ ...source,
307
+ mirror: {
308
+ models: {
309
+ 'gpt-5.5': { ...source.openai.models['gpt-5.5'], id: 'gpt-5.5' }
310
+ }
311
+ }
312
+ };
313
+ expect(() => buildSnapshot(duplicateSource, {
314
+ policy: duplicatePolicy,
315
+ previousSnapshot: { aliases: {}, models: {} },
316
+ updatedAt: '2026-09-04'
317
+ })).toThrow(/duplicate runtime model id/i);
318
+
319
+ expect(() => validateSnapshot({ models: {} }, {
320
+ selectedCount: 0,
321
+ previousCount: 4,
322
+ minimumModelCount: 1
323
+ })).toThrow(/empty|abnormal/i);
324
+ });
325
+
326
+ test('filters negative prices and invalid limits with a warning', () => {
327
+ const invalidSource = {
328
+ ...source,
329
+ openai: {
330
+ ...source.openai,
331
+ models: {
332
+ ...source.openai.models,
333
+ 'gpt-invalid': {
334
+ id: 'gpt-invalid',
335
+ modalities: { input: ['text'], output: ['text'] },
336
+ limit: { context: -1, output: 1 },
337
+ cost: { input: -1, output: 1 }
338
+ }
339
+ }
340
+ }
341
+ };
342
+ const result = buildSnapshot(invalidSource, {
343
+ policy,
344
+ previousSnapshot: { aliases: {}, models: {} },
345
+ updatedAt: '2026-09-04'
346
+ });
347
+ expect(result.snapshot.models).not.toHaveProperty('gpt-invalid');
348
+ expect(result.warnings.join('\n')).toMatch(/gpt-invalid|limit|cost/i);
349
+ });
350
+
351
+ test('rejects an invalid or empty upstream payload', () => {
352
+ expect(() => validateSourcePayload({})).toThrow(/provider models/i);
353
+ expect(() => validateSourcePayload({ openai: { models: [] } })).toThrow(/models must be an object/i);
354
+ expect(() => validateSourcePayload({
355
+ openai: { models: { invalid: { id: '' } } }
356
+ })).toThrow(/model id/i);
357
+ });
358
+ });
359
+ ```
360
+
361
+ - [ ] **Step 3: Run only the new test to verify the contract is red.**
362
+
363
+ ```bash
364
+ npx vitest run tests/unit/scripts/model-catalog-sync.test.js
365
+ ```
366
+
367
+ Expected result: the test process fails because `scripts/sync-model-catalog.js` does not exist yet. Do not change the assertions to make a missing implementation pass.
368
+
369
+ - [ ] **Step 4: Commit the policy and red tests.**
370
+
371
+ ```bash
372
+ git add scripts/model-catalog-policy.json tests/unit/scripts/model-catalog-sync.test.js
373
+ git commit -m "test: define model catalog sync contract"
374
+ ```
375
+
376
+ Expected result: one commit containing only the synchronizer policy and its failing contract tests.
377
+
378
+ ## Task 2: Implement the Models.dev normalizer and generator
379
+
380
+ **Files:**
381
+
382
+ - Create: `scripts/sync-model-catalog.js`
383
+ - Modify: `package.json`
384
+ - Modify: `src/config/model-metadata.json` through the generator only
385
+
386
+ - [ ] **Step 1: Implement the pure normalizer interface used by Task 1.**
387
+
388
+ Create `scripts/sync-model-catalog.js` with these exported functions and invariants:
389
+
390
+ ```js
391
+ const SOURCE_URL = 'https://models.dev/api.json';
392
+
393
+ module.exports = {
394
+ SOURCE_URL,
395
+ validateSourcePayload,
396
+ validateSnapshot,
397
+ buildSnapshot,
398
+ fetchSource,
399
+ writeSnapshot,
400
+ formatReport
401
+ };
402
+ ```
403
+
404
+ Implement the functions as follows:
405
+
406
+ 1. `validateSourcePayload(payload)` requires a non-array object with at least one outer provider whose `models` is a non-array object. Each model entry must be an object with a non-empty string `id`; throw messages containing `provider models`, `models must be an object`, or `model id` for those failures.
407
+ 2. `buildSnapshot(payload, { policy, previousSnapshot, updatedAt })` iterates the policy provider keys, reads `payload[provider].models`, and applies the configured regular expressions to the provider-local model ID.
408
+ 3. Reject entries unless `modalities.input` contains `text`, `modalities.output` contains `text`, `limit.context` and `limit.output` are finite positive numbers, and `cost.input` and `cost.output` are finite non-negative numbers. `tool_call === false` is rejected; missing `tool_call` is allowed so popular text models with incomplete capability metadata are not silently lost.
409
+ 4. Omit entries whose `status` is explicitly `deprecated` or `expired`, unless the previous snapshot references their runtime ID or source ID; append a warning for every such omission.
410
+ 5. Use `runtimeMode: "direct"` to set `id` to the raw model ID. Use `runtimeMode: "compatible"` to set `id` to `${provider}/${raw.id}`. Always set `sourceId` to `${provider}/${raw.id}` and `provider` to the outer provider key.
411
+ 6. Map only fields understood by the current project: `name`, `reasoning`, `input` from text/image modalities, `supportsTools` from `tool_call`, `limit.context`, `limit.output`, and `pricing.input`, `pricing.output`, `pricing.cacheRead` from `cost.cache_read`, and `pricing.cacheCreation` from `cost.cache_write`. Copy only recognized reasoning effort values `minimal`, `low`, `medium`, `high`, and `xhigh` into the existing `thinking: { mode: 'effort', efforts }` shape.
412
+ 7. If a selected entry lacks a usable cost or text capability, omit it from the generated source set and append a warning with its source ID. If it has `cost.tiers`, `context_over_200k`, or other unrepresented pricing dimensions, use the base `cost` values and append a warning naming the model and ignored fields.
413
+ 8. Preserve every previous `aliases` entry and every previous `models` entry not selected in the new source. Record those retained IDs in `changes.removed`; never delete them automatically.
414
+ 9. Build each native `defaultModels` list by retaining previous entries that still exist, then appending newly selected direct-provider entries with the matching tool type in `last_updated` descending/runtime ID ascending order. Preserve the previous order of retained defaults. Preserve `defaultSpeedTestModels` when its model still exists; otherwise choose the first generated entry for that tool.
415
+ 10. Set `source: 'models.dev'` and `sourceUrl: SOURCE_URL`. Set `lastUpdated` to `updatedAt` only when normalized model/alias/default content differs from the previous snapshot; otherwise preserve `previousSnapshot.lastUpdated` so a no-change daily run produces no diff. Sort all model keys, aliases, and only the newly appended default entries deterministically before serialization. For a compatible model, add `aliases[raw.id] = runtimeId` only when that short ID is not already a model ID or alias; never overwrite an existing alias.
416
+ 11. Return `{ snapshot, warnings, changes: { added, updated, removed, priceChanged, limitChanged } }`.
417
+
418
+ `validateSnapshot(snapshot, { selectedCount, previousCount, minimumModelCount })` rejects zero selected entries, selected counts below the policy minimum, selected counts below half of the previous model count when the previous count is non-zero, duplicate IDs, non-positive limits, and negative prices. `buildSnapshot` calls it only after source filtering and before returning.
419
+
420
+ The implementation must keep the CLI behind `if (require.main === module)` so the fixture tests import the pure functions without performing network I/O. `fetchSource(fetchImpl = fetch)` uses a 30-second abort timeout and throws on non-2xx responses. `writeSnapshot` compares the current file text before replacing it and writes `${JSON.stringify(snapshot, null, 2)}\n` only when it differs. `main` must read the previous snapshot before fetching, validate the transformed snapshot before writing, preserve `previousSnapshot.lastUpdated` when `changes` has no model/alias/default changes, and write `process.env.MODEL_CATALOG_REPORT_PATH || path.join(os.tmpdir(), 'model-catalog-sync-report.md')`. `formatReport` emits Markdown sections for added IDs, price changes, limit changes, retained/removed source IDs, unsupported fields, and warnings; it never includes credentials.
421
+
422
+ - [ ] **Step 2: Add the root npm script without changing dependencies.**
423
+
424
+ Modify `package.json` scripts by adding:
425
+
426
+ ```json
427
+ "sync:model-catalog": "node scripts/sync-model-catalog.js"
428
+ ```
429
+
430
+ Do not add a package or modify `package-lock.json`; Node 22 already provides the required fetch implementation.
431
+
432
+ - [ ] **Step 3: Run the synchronizer fixture tests.**
433
+
434
+ ```bash
435
+ npx vitest run tests/unit/scripts/model-catalog-sync.test.js
436
+ ```
437
+
438
+ Expected result: all synchronizer tests pass, including direct/compatible IDs, provider metadata, cost mapping, source filtering, retained models, and invalid payload rejection.
439
+
440
+ - [ ] **Step 4: Generate the first repository snapshot from the live source.**
441
+
442
+ ```bash
443
+ npm run sync:model-catalog
444
+ ```
445
+
446
+ Expected result: exit code 0; `src/config/model-metadata.json` is valid JSON, has `source: "models.dev"`, has a current `lastUpdated`, and the command reports either a generated diff or no changes. Any warning about unsupported upstream fields is printed in the report rather than silently discarded.
447
+
448
+ - [ ] **Step 5: Commit the generator, policy wiring, and generated snapshot.**
449
+
450
+ ```bash
451
+ git add scripts/sync-model-catalog.js package.json src/config/model-metadata.json
452
+ git commit -m "feat: sync model catalog from models.dev"
453
+ ```
454
+
455
+ Expected result: one commit containing the generator, npm entry point, and generated snapshot.
456
+
457
+ ## Task 3: Expose provenance and tool-aware metadata through the runtime/API
458
+
459
+ **Files:**
460
+
461
+ - Modify: `src/config/model-metadata.js`
462
+ - Modify: `src/server/api/settings.js`
463
+ - Modify: `tests/unit/config/model-metadata.test.js`
464
+ - Modify: `tests/unit/api/settings.test.js`
465
+ - Verify without modification: `tests/unit/services/model-definition-schema.test.js` (existing explicit-over-builtin precedence tests)
466
+
467
+ - [ ] **Step 1: Add failing runtime/API assertions.**
468
+
469
+ Extend `tests/unit/config/model-metadata.test.js` to import `METADATA_SOURCE` and `getModelIdsByToolType`, then add assertions with this behavior:
470
+
471
+ ```js
472
+ expect(METADATA_SOURCE).toMatchObject({
473
+ name: 'models.dev',
474
+ url: 'https://models.dev/api.json'
475
+ });
476
+ expect(getModelIdsByToolType('claude').every((id) => {
477
+ const meta = MODEL_METADATA[id];
478
+ return meta.toolTypes?.includes('claude') || id.startsWith('claude-');
479
+ })).toBe(true);
480
+ expect(getModelIdsByToolType('omp').some((id) => MODEL_METADATA[id].provider === 'deepseek')).toBe(true);
481
+ ```
482
+
483
+ Update the `modelMetaStub` in `tests/unit/api/settings.test.js` with `METADATA_SOURCE` and `getModelIdsByToolType`, then assert that `GET /model-settings` returns `metadataSource` and `toolModels` while retaining `lastUpdated` and `builtinModelIds`.
484
+
485
+ - [ ] **Step 2: Run the focused tests and verify they fail for the missing exports/fields.**
486
+
487
+ ```bash
488
+ npx vitest run tests/unit/config/model-metadata.test.js tests/unit/api/settings.test.js
489
+ ```
490
+
491
+ Expected result: failures identify the missing provenance export, tool-aware lookup, or response fields. Preserve the existing assertions while adding the new contract.
492
+
493
+ - [ ] **Step 3: Implement provenance and dynamic lookup in `model-metadata.js`.**
494
+
495
+ Add:
496
+
497
+ ```js
498
+ const METADATA_SOURCE = Object.freeze({
499
+ name: metadataConfig.source || 'models.dev',
500
+ url: metadataConfig.sourceUrl || 'https://models.dev/api.json',
501
+ lastUpdated: metadataConfig.lastUpdated || 'unknown'
502
+ });
503
+
504
+ function getModelIdsByToolType(toolType) {
505
+ const key = String(toolType || '').trim().toLowerCase();
506
+ return Object.entries(MODEL_METADATA)
507
+ .filter(([modelId, meta]) => {
508
+ if (Array.isArray(meta?.toolTypes)) return meta.toolTypes.includes(key);
509
+ const normalizedId = modelId.toLowerCase();
510
+ return key === 'claude' ? normalizedId.startsWith('claude-')
511
+ : key === 'codex' ? /^(gpt-|o[134](?:-|$))/.test(normalizedId)
512
+ : key === 'gemini' ? normalizedId.startsWith('gemini-')
513
+ : false;
514
+ })
515
+ .map(([modelId]) => modelId);
516
+ }
517
+ ```
518
+
519
+ Export `METADATA_SOURCE` and `getModelIdsByToolType`. Keep `getDefaultModelsByToolType` backward compatible for existing `claude`, `codex`, `gemini`, and `openai_compatible` callers. Do not remove existing aliases or generic Claude fallback.
520
+
521
+ - [ ] **Step 4: Return the metadata source and tool groups from `settings.js`.**
522
+
523
+ Import the new exports and add these fields to the existing `GET /model-settings` response:
524
+
525
+ ```js
526
+ metadataSource: METADATA_SOURCE,
527
+ toolModels: {
528
+ claude: getModelIdsByToolType('claude'),
529
+ codex: getModelIdsByToolType('codex'),
530
+ gemini: getModelIdsByToolType('gemini'),
531
+ opencode: getModelIdsByToolType('opencode'),
532
+ omp: getModelIdsByToolType('omp')
533
+ }
534
+ ```
535
+
536
+ Leave `models`, `overrides`, `definitions`, `resolvedModels`, `provenance`, `warnings`, `builtinModelIds`, `lastUpdated`, and `defaultSpeedTestModels` unchanged.
537
+
538
+ - [ ] **Step 5: Run the focused runtime/API tests.**
539
+
540
+ ```bash
541
+ npx vitest run tests/unit/config/model-metadata.test.js tests/unit/api/settings.test.js tests/unit/services/model-definition-schema.test.js
542
+ ```
543
+
544
+ Expected result: all existing and new tests pass, including user override merging, explicit model-definition precedence, and the new source/tool-group fields.
545
+
546
+ - [ ] **Step 6: Commit the runtime/API contract.**
547
+
548
+ ```bash
549
+ git add src/config/model-metadata.js src/server/api/settings.js tests/unit/config/model-metadata.test.js tests/unit/api/settings.test.js
550
+ git commit -m "feat: expose model catalog provenance"
551
+ ```
552
+
553
+ Expected result: one commit containing only the runtime/API metadata contract and its tests.
554
+
555
+ ## Task 4: Remove duplicate proxy pricing tables
556
+
557
+ **Files:**
558
+
559
+ - Modify: `src/platforms/drivers/codex/proxy-implementation.js`
560
+ - Modify: `src/platforms/drivers/gemini/proxy-implementation.js`
561
+ - Modify: `src/platforms/drivers/gemini/sessions-implementation.js`
562
+ - Modify: `src/platforms/drivers/opencode/proxy-implementation.js`
563
+ - Modify: `tests/unit/services/proxy-cost.test.js`
564
+
565
+ - [ ] **Step 1: Add regression assertions for unknown model fallback behavior.**
566
+
567
+ Extend `tests/unit/services/proxy-cost.test.js` with these tests before removing the tables:
568
+
569
+ ```js
570
+ test('Codex unknown family uses configured base pricing instead of a stale o1 table', () => {
571
+ expect(calculateCodexCost('o1-custom', {
572
+ input: 1000000,
573
+ output: 1000000
574
+ })).toBeCloseTo(17.5, 8);
575
+ });
576
+
577
+ test('OpenCode unknown family uses configured base pricing instead of a stale gpt-4 table', () => {
578
+ expect(calculateOpenCodeCost('gpt-4-custom', {
579
+ input: 1000000,
580
+ output: 1000000
581
+ })).toBeCloseTo(17.5, 8);
582
+ });
583
+
584
+ test('Gemini unknown family uses configured base pricing instead of a stale family table', () => {
585
+ expect(calculateGeminiCost('gemini-new-custom', {
586
+ input: 1000000,
587
+ output: 1000000
588
+ })).toBeCloseTo(11.25, 8);
589
+ });
590
+ ```
591
+
592
+ - [ ] **Step 2: Run the pricing tests and verify the new assertions are red.**
593
+
594
+ ```bash
595
+ npx vitest run tests/unit/services/proxy-cost.test.js
596
+ ```
597
+
598
+ Expected result: the existing tests pass, while the new unknown-family assertions fail because the current proxy tables still intercept `o1`, `gpt-4`, or Gemini family names.
599
+
600
+ - [ ] **Step 3: Delete the three duplicate model tables and family branches.**
601
+
602
+ In the Codex proxy, remove the `PRICING` object and replace the `calculateCost` body with:
603
+
604
+ ```js
605
+ function calculateCost(model, tokens) {
606
+ const pricing = resolveModelPricing('codex', model, {}, CODEX_BASE_PRICING);
607
+ return calculateTokenCost(pricing, tokens, CODEX_BASE_PRICING);
608
+ }
609
+ ```
610
+
611
+ In the OpenCode proxy, remove its `PRICING` object and use the corresponding tool key and base constant:
612
+
613
+ ```js
614
+ function calculateCost(model, tokens) {
615
+ const pricing = resolveModelPricing('opencode', model, {}, OPENCODE_BASE_PRICING);
616
+ return calculateTokenCost(pricing, tokens, OPENCODE_BASE_PRICING);
617
+ }
618
+ ```
619
+
620
+ In Gemini, remove the `PRICING` object and replace its fallback lookup with:
621
+
622
+ ```js
623
+ function calculateCost(model, tokens) {
624
+ const pricing = resolveModelPricing('gemini', model, {}, GEMINI_BASE_PRICING);
625
+ return calculateTokenCost(pricing, tokens, GEMINI_BASE_PRICING);
626
+ }
627
+ ```
628
+
629
+ - [ ] **Step 4: Remove literal Gemini session rates while preserving the existing aggregation.**
630
+
631
+ In `src/platforms/drivers/gemini/sessions-implementation.js`, import `DEFAULT_CONFIG` and `calculateTokenCost`, then replace the manual two-rate calculation with:
632
+
633
+ ```js
634
+ const pricing = resolveModelPricing('gemini', msg.model, {}, DEFAULT_CONFIG.pricing.gemini);
635
+ totalCost += calculateTokenCost(pricing, msg.tokens, DEFAULT_CONFIG.pricing.gemini);
636
+ ```
637
+
638
+ This preserves cache-aware metadata pricing and the configured Gemini base fallback without duplicating numeric rates.
639
+
640
+ - [ ] **Step 5: Run pricing and existing proxy tests.**
641
+
642
+ ```bash
643
+ npx vitest run tests/unit/services/proxy-cost.test.js tests/unit/utils/pricing.test.js tests/unit/platforms/drivers/codex-proxy.test.js tests/unit/platforms/drivers/gemini-proxy.test.js tests/unit/platforms/drivers/opencode-proxy.test.js
644
+ ```
645
+
646
+ Expected result: all tests pass; known metadata models still use snapshot prices, and unknown families use provider base pricing rather than removed family tables.
647
+
648
+ - [ ] **Step 6: Commit the pricing cutover.**
649
+
650
+ ```bash
651
+ git add src/platforms/drivers/codex/proxy-implementation.js src/platforms/drivers/gemini/proxy-implementation.js src/platforms/drivers/gemini/sessions-implementation.js src/platforms/drivers/opencode/proxy-implementation.js tests/unit/services/proxy-cost.test.js
652
+ git commit -m "refactor: centralize proxy model pricing"
653
+ ```
654
+
655
+ Expected result: one commit with no duplicate model-specific pricing tables in those proxy implementations.
656
+
657
+ ## Task 5: Add offline OMP catalog-metadata operation
658
+
659
+ **Files:**
660
+
661
+ - Create: `tests/unit/services/omp-catalog-metadata.test.js`
662
+ - Modify: `src/platforms/drivers/omp/channels-implementation.js`
663
+ - Modify: `src/platforms/drivers/omp/channels.js`
664
+ - Modify: `tests/unit/platforms/drivers/omp-channels.test.js`
665
+
666
+ - [ ] **Step 1: Write the driver/service tests first.**
667
+
668
+ Add a driver contract test to `tests/unit/platforms/drivers/omp-channels.test.js`:
669
+
670
+ ```js
671
+ test('exposes local catalog metadata without a provider key', async () => {
672
+ const catalogMetadata = vi.fn().mockReturnValue({
673
+ models: [{ id: 'deepseek/deepseek-v4-pro' }],
674
+ warnings: []
675
+ });
676
+ const { createDriver } = require('../../../../src/platforms/drivers/omp/channels');
677
+ const driver = createDriver({ requireImpl: () => ({ catalogMetadata }) });
678
+ const result = await driver.catalogMetadata({ body: { providerKey: '' } });
679
+
680
+ expect(catalogMetadata).toHaveBeenCalledWith({ providerKey: '' });
681
+ expect(result).toEqual({ models: [{ id: 'deepseek/deepseek-v4-pro' }], warnings: [] });
682
+ });
683
+ ```
684
+
685
+ Create `tests/unit/services/omp-catalog-metadata.test.js` with the real service module:
686
+
687
+ 'use strict';
688
+
689
+ const { execFileSync } = vi.hoisted(() => ({ execFileSync: vi.fn() }));
690
+ vi.mock('child_process', async () => ({
691
+ ...(await vi.importActual('child_process')),
692
+ execFileSync
693
+ }));
694
+
695
+ const channels = require('../../../src/platforms/drivers/omp/channels-implementation');
696
+
697
+ describe('offline OMP catalog metadata', () => {
698
+ beforeEach(() => {
699
+ vi.stubGlobal('fetch', vi.fn(() => Promise.reject(new Error('network must not be used'))));
700
+ process.env.OMP_COMMAND = '__missing_omp_command__';
701
+ });
702
+
703
+ afterEach(() => {
704
+ vi.unstubAllGlobals();
705
+ vi.clearAllMocks();
706
+ delete process.env.OMP_COMMAND;
707
+ });
708
+
709
+ test('returns the packaged OMP catalog with no provider key', () => {
710
+ const result = channels.getCatalogMetadata({ providerKey: '' });
711
+ expect(Array.isArray(result.models)).toBe(true);
712
+ expect(result.models.length).toBeGreaterThan(0);
713
+ expect(global.fetch).not.toHaveBeenCalled();
714
+ expect(execFileSync).not.toHaveBeenCalled();
715
+ });
716
+
717
+ test('filters a known provider from the packaged catalog', () => {
718
+ const result = channels.getCatalogMetadata({ providerKey: 'deepseek' });
719
+ expect(result.models.every(model => (
720
+ model.provider === 'deepseek'
721
+ || model.sourceId?.startsWith('deepseek/')
722
+ ))).toBe(true);
723
+ expect(global.fetch).not.toHaveBeenCalled();
724
+ expect(execFileSync).not.toHaveBeenCalled();
725
+ });
726
+ });
727
+ ```
728
+
729
+ - [ ] **Step 2: Run the focused OMP tests and verify the operation is missing.**
730
+
731
+ ```bash
732
+ npx vitest run tests/unit/platforms/drivers/omp-channels.test.js tests/unit/services/omp-catalog-metadata.test.js
733
+ ```
734
+
735
+ Expected result: the new tests fail because the OMP driver/service does not yet expose `catalogMetadata`/`getCatalogMetadata`.
736
+
737
+ - [ ] **Step 3: Implement local selection in `channels-implementation.js`.**
738
+
739
+ Import `MODEL_METADATA` and `METADATA_SOURCE` from `src/config/model-metadata.js`. Do not import the model-definition schema: this operation returns the same metadata shape already used by the settings API and must not normalize or persist user definitions. Add:
740
+
741
+ ```js
742
+ function getCatalogMetadata({ providerKey = '', model = '', speedTestModel = '', allowedModels = [], models = [] } = {}) {
743
+ const requestedIds = new Set([
744
+ model,
745
+ speedTestModel,
746
+ ...(Array.isArray(allowedModels) ? allowedModels : []),
747
+ ...(Array.isArray(models) ? models.map(item => item?.id || item?.name) : [])
748
+ ].map(value => String(value || '').trim().toLowerCase()).filter(Boolean));
749
+ const provider = String(providerKey || '').trim().toLowerCase();
750
+ const allEntries = Object.entries(MODEL_METADATA)
751
+ .filter(([, meta]) => Array.isArray(meta?.toolTypes) && meta.toolTypes.includes('omp'))
752
+ .map(([id, meta]) => ({ ...structuredClone(meta), id }));
753
+ const providerEntries = provider
754
+ ? allEntries.filter(({ id, provider: entryProvider, sourceId }) => (
755
+ requestedIds.has(id.toLowerCase())
756
+ || String(entryProvider || '').toLowerCase() === provider
757
+ || String(sourceId || '').toLowerCase().startsWith(`${provider}/`)
758
+ ))
759
+ : allEntries;
760
+ const entries = provider && providerEntries.length > 0 ? providerEntries : allEntries;
761
+
762
+ return {
763
+ models: entries,
764
+ warnings: [],
765
+ source: METADATA_SOURCE
766
+ };
767
+ }
768
+
769
+ ```
770
+ Do not alter `getOmpCatalogModels` or `buildCatalogIndex` in `src/platforms/drivers/omp/native-config-implementation.js`; `catalogFromCli: true` remains the separate explicit `omp models --json` discovery path.
771
+
772
+ Return cloned model objects so callers cannot mutate the imported JSON. If a non-empty provider has no local match, return the full eligible OMP list rather than an empty offline directory; requested IDs are always retained. Never call `fetch`, `omp`, `spawnSync`, or a channel probe from this method. Export `getCatalogMetadata` alongside the existing channel methods.
773
+
774
+ - [ ] **Step 4: Expose the operation through the OMP descriptor driver.**
775
+
776
+ In `src/platforms/drivers/omp/channels.js`, add:
777
+
778
+ ```js
779
+ driver.catalogMetadata = ({ body = {} } = {}) => (
780
+ driver._service().getCatalogMetadata(body)
781
+ );
782
+ ```
783
+
784
+ Keep the existing `createChannelDriver` methods and the manifest route unchanged. The existing descriptor route already maps `POST /omp/channels/catalog-metadata` to operation `catalogMetadata`.
785
+
786
+ - [ ] **Step 5: Run the OMP tests.**
787
+
788
+ ```bash
789
+ npx vitest run tests/unit/platforms/drivers/omp-channels.test.js tests/unit/services/omp-catalog-metadata.test.js tests/unit/services/omp-settings-manager.test.js
790
+ ```
791
+
792
+ Expected result: the driver forwards the request body, the real method returns packaged OMP models with no network/command lookup, and existing native OMP synchronization tests remain green.
793
+
794
+ - [ ] **Step 6: Commit the offline OMP operation.**
795
+
796
+ ```bash
797
+ git add src/platforms/drivers/omp/channels-implementation.js src/platforms/drivers/omp/channels.js tests/unit/platforms/drivers/omp-channels.test.js tests/unit/services/omp-catalog-metadata.test.js
798
+ git commit -m "feat: provide offline OMP model metadata"
799
+ ```
800
+
801
+ Expected result: one commit containing the local OMP metadata operation and focused tests.
802
+
803
+ - Create: `src/web/src/composables/__tests__/useDefaultModels.test.js`
804
+ - Modify: `src/web/src/composables/useDefaultModels.js`
805
+ - Modify: `src/web/src/components/SettingsDrawer.vue`
806
+ - Modify: `src/web/src/components/channel/BaseChannelPanel.vue`
807
+ - Modify: `src/web/src/components/channel/channelPanelFactories.js`
808
+
809
+ - [ ] **Step 1: Add the front-end grouping test.**
810
+
811
+ Create `src/web/src/composables/__tests__/useDefaultModels.test.js`:
812
+
813
+ ```js
814
+ import { describe, expect, it, vi } from 'vitest'
815
+ import { buildAllModelsFromMetadata, useDefaultModels } from '../useDefaultModels'
816
+
817
+ describe('buildAllModelsFromMetadata', () => {
818
+ it('uses toolTypes instead of model-name prefixes', () => {
819
+ const grouped = buildAllModelsFromMetadata({
820
+ 'claude-sonnet-4-6': { limit: {}, pricing: {}, toolTypes: ['claude', 'opencode', 'omp'] },
821
+ 'gpt-5.5': { limit: {}, pricing: {}, toolTypes: ['codex', 'opencode', 'omp'] },
822
+ 'deepseek/deepseek-v4-pro': { limit: {}, pricing: {}, toolTypes: ['opencode', 'omp'] }
823
+ })
824
+
825
+ expect(grouped.claude).toEqual(['claude-sonnet-4-6'])
826
+ expect(grouped.codex).toEqual(['gpt-5.5'])
827
+ expect(grouped.gemini).toEqual([])
828
+ expect(grouped.omp).toEqual(['claude-sonnet-4-6', 'deepseek/deepseek-v4-pro', 'gpt-5.5'])
829
+ expect(grouped.opencode).toEqual(['claude-sonnet-4-6', 'deepseek/deepseek-v4-pro', 'gpt-5.5'])
830
+ })
831
+
832
+ it('loads compatible models into the offline selector groups', async () => {
833
+ vi.stubGlobal('fetch', vi.fn((url) => Promise.resolve({
834
+ ok: true,
835
+ json: async () => String(url).includes('/api/config/')
836
+ ? { defaultModels: { claude: [], codex: [], gemini: [] } }
837
+ : {
838
+ models: {
839
+ 'deepseek/deepseek-v4-pro': {
840
+ limit: { context: 1000000, output: 384000 },
841
+ pricing: { input: 0.435, output: 0.87 },
842
+ toolTypes: ['opencode', 'omp']
843
+ }
844
+ }
845
+ }
846
+ })))
847
+
848
+ const { loadDefaultModels, getAllModelsByToolType, getDefaultModels } = useDefaultModels()
849
+ try {
850
+ await loadDefaultModels({ forceRefresh: true })
851
+
852
+ expect(getAllModelsByToolType('omp')).toContain('deepseek/deepseek-v4-pro')
853
+ expect(getDefaultModels('omp')).toContain('deepseek/deepseek-v4-pro')
854
+ } finally {
855
+ vi.unstubAllGlobals()
856
+ }
857
+ })
858
+ })
859
+ ```
860
+
861
+ - [ ] **Step 2: Run the web test before implementation.**
862
+
863
+ ```bash
864
+ cd src/web
865
+ npm run test:unit -- src/composables/__tests__/useDefaultModels.test.js
866
+ ```
867
+
868
+ Expected result: the test fails because `buildAllModelsFromMetadata` is private and does not yet create dynamic `omp`/`opencode` groups.
869
+
870
+ - [ ] **Step 3: Update `useDefaultModels.js` to use `toolTypes`.**
871
+
872
+ Export the pure `buildAllModelsFromMetadata` helper. Initialize groups dynamically from each metadata entry's `toolTypes`; require `limit` and `pricing` as the current selectable-model contract does. If an entry lacks `toolTypes`, use the existing Claude/Codex/Gemini prefix fallback only for backward-compatible snapshots. Keep `FALLBACK_MODELS` for the three native tools. Make `getAllModelsByToolType(toolType)` return any dynamic group before falling back, and make `getDefaultModels(toolType)` return an explicit server default first, then the matching dynamic group, then the three native fallback arrays.
873
+
874
+ The returned group shape must be:
875
+
876
+ ```js
877
+ {
878
+ claude: [...],
879
+ codex: [...],
880
+ gemini: [...],
881
+ opencode: [...],
882
+ omp: [...]
883
+ }
884
+ ```
885
+
886
+ Sort each group in the source order emitted by the synchronizer; do not alphabetically reorder the preferred default arrays.
887
+
888
+ - [ ] **Step 4: Update the model settings page to use metadata fields and show provenance.**
889
+
890
+ In `SettingsDrawer.vue`:
891
+
892
+ 1. Add `const modelMetadataSource = ref({ name: 'Models.dev', url: '', lastUpdated: '' })`.
893
+ 2. In the template under the existing model settings subtitle, render `价格来源:Models.dev 参考价格 · {{ modelMetadataSource.lastUpdated }}`.
894
+ 3. Add an `OMP/OpenCode` filter radio with value `compatible`.
895
+ 4. Make `filteredModelMeta` inspect `meta.toolTypes` first; for `compatible`, require `omp` or `opencode` and exclude models already classified as native-only; retain the current prefix checks only when the field is absent.
896
+ 5. Make `speedTestModelOptions` group only entries whose `toolTypes` include `claude`, `codex`, or `gemini`, with the old prefix helper as fallback.
897
+ 6. Store `data.metadataSource` in `loadModelMetadata()` and leave the current override edit/save/reset flow untouched.
898
+
899
+ - [ ] **Step 5: Add offline catalog fallbacks to the OpenCode and OMP model selectors and keep metadata retrieval separate.**
900
+
901
+ In the OpenCode factory, compute offline options from `getAllModelsByToolType('opencode')` for an `openai_compatible` preset and from the existing `claude`/`codex`/`gemini` group when the gateway source explicitly selects one of those native adapters. Merge offline options after live probe results, so live provider IDs remain first while popular curated IDs remain selectable when the provider omits a list endpoint.
902
+
903
+ In the OMP factory, compute offline options from `getAllModelsByToolType('omp')`. For a new channel with no `baseUrl`, return those options instead of an empty list. When a live probe returns no models or fails, retain the offline options and set the existing error hint to explain that the packaged `Models.dev` reference catalog is being used. Do not remove the live `probeOmpChannelModels`/`fetchOmpChannelModels` calls.
904
+
905
+ For the existing `fetchModelMetadataForChannel`, remove the early `if (!providerKey) throw` so it can call `fetchOmpCatalogMetadata('', ...)`. Continue sending `providerKey` when present, together with `model`, `speedTestModel`, `allowedModels`, and existing definitions. Set the status message from the returned `source` and model count, for example:
906
+
907
+ ```js
908
+ const sourceName = result.source?.name === 'models.dev'
909
+ ? 'Models.dev'
910
+ : (result.source?.name || 'Models.dev')
911
+ form.modelMetadataStatus = `已读取 ${result.models?.length || 0} 个模型(${sourceName} 离线快照)${warningCount ? `,${warningCount} 条兼容提示` : ''}`
912
+ ```
913
+
914
+ Update the OMP Metadata status fallback in `BaseChannelPanel.vue` from `仅在点击时调用 OMP;普通保存不会启动 OMP 命令。` to `Metadata 读取 Models.dev 离线快照;模型列表按钮才探测渠道,普通保存不会启动网络或 OMP 命令。` Keep the save path free of both network calls and OMP command execution.
915
+
916
+ - [ ] **Step 6: Run the web unit suite and build the web bundle.**
917
+ ```bash
918
+ npm run test:unit
919
+ cd ../..
920
+ npm run build:web
921
+ ```
922
+
923
+ Expected result: all existing web tests, the new grouping test, and the production Vite build pass. The settings page still saves and resets user overrides, while OMP's Metadata button can be used before a Provider Key is entered.
924
+
925
+ - [ ] **Step 7: Commit the front-end contract.**
926
+
927
+ ```bash
928
+ git add src/web/src/composables/useDefaultModels.js src/web/src/composables/__tests__/useDefaultModels.test.js src/web/src/components/SettingsDrawer.vue src/web/src/components/channel/BaseChannelPanel.vue src/web/src/components/channel/channelPanelFactories.js
929
+ ```
930
+
931
+ Expected result: one commit containing dynamic tool filtering, source labeling, and offline OMP UI integration.
932
+
933
+ ## Task 7: Add the daily GitHub Actions PR workflow
934
+
935
+ **Files:**
936
+
937
+ - Create: `.github/workflows/sync-model-catalog.yml`
938
+
939
+ - [ ] **Step 1: Add the workflow with explicit permissions and a temporary report path.**
940
+
941
+ Create `.github/workflows/sync-model-catalog.yml`:
942
+
943
+ ```yaml
944
+ name: Sync model catalog
945
+
946
+ on:
947
+ schedule:
948
+ - cron: '17 3 * * *'
949
+ workflow_dispatch:
950
+
951
+ permissions:
952
+ contents: write
953
+ pull-requests: write
954
+
955
+ jobs:
956
+ sync:
957
+ runs-on: ubuntu-latest
958
+ steps:
959
+ - name: Check out repository
960
+ uses: actions/checkout@v4
961
+
962
+ - name: Set up Node.js
963
+ uses: actions/setup-node@v4
964
+ with:
965
+ node-version: '22.13.0'
966
+ cache: npm
967
+
968
+ - name: Install root dependencies
969
+ run: npm ci --ignore-scripts
970
+
971
+ - name: Generate model catalog
972
+ env:
973
+ MODEL_CATALOG_REPORT_PATH: ${{ runner.temp }}/model-catalog-sync-report.md
974
+ run: npm run sync:model-catalog
975
+
976
+ - name: Open catalog update PR
977
+ uses: peter-evans/create-pull-request@v7
978
+ with:
979
+ token: ${{ secrets.GITHUB_TOKEN }}
980
+ add-paths: src/config/model-metadata.json
981
+ branch: automation/model-catalog-sync
982
+ delete-branch: true
983
+ commit-message: 'chore: refresh model catalog'
984
+ title: 'chore: refresh model catalog'
985
+ body-path: ${{ runner.temp }}/model-catalog-sync-report.md
986
+ ```
987
+
988
+ The synchronizer must return success when there is no diff; `create-pull-request` then has no changed path to commit and creates no PR. The workflow must never add the temporary report to the repository.
989
+
990
+ - [ ] **Step 2: Parse the workflow locally.**
991
+
992
+ ```bash
993
+ node -e "const fs=require('fs'); const yaml=require('js-yaml'); const value=yaml.load(fs.readFileSync('.github/workflows/sync-model-catalog.yml','utf8')); if (!value.jobs?.sync || !value.on || !value.permissions?.contents || !value.permissions?.['pull-requests']) process.exit(1); console.log('workflow valid')"
994
+ ```
995
+
996
+ Expected result: `workflow valid`.
997
+
998
+ - [ ] **Step 3: Commit the automation workflow.**
999
+
1000
+ ```bash
1001
+ git add .github/workflows/sync-model-catalog.yml
1002
+ git commit -m "ci: refresh model catalog daily"
1003
+ ```
1004
+
1005
+ Expected result: one commit containing only the scheduled/manual catalog workflow.
1006
+
1007
+ ## Task 8: Run end-to-end verification and finish the implementation
1008
+
1009
+ **Files:**
1010
+
1011
+ - Modify only files required by failing verification; do not broaden the feature or restore duplicate pricing tables.
1012
+
1013
+ - [ ] **Step 1: Regenerate once from the live Models.dev source.**
1014
+
1015
+ ```bash
1016
+ npm run sync:model-catalog
1017
+ ```
1018
+
1019
+ Expected result: exit code 0, deterministic JSON output, no secret values, and a report listing source changes or stating that the snapshot is unchanged.
1020
+
1021
+ - [ ] **Step 2: Run all focused root tests.**
1022
+
1023
+ ```bash
1024
+ npx vitest run \
1025
+ tests/unit/scripts/model-catalog-sync.test.js \
1026
+ tests/unit/config/model-metadata.test.js \
1027
+ tests/unit/api/settings.test.js \
1028
+ tests/unit/utils/pricing.test.js \
1029
+ tests/unit/services/proxy-cost.test.js \
1030
+ tests/unit/platforms/drivers/omp-channels.test.js \
1031
+ tests/unit/services/omp-catalog-metadata.test.js
1032
+ ```
1033
+
1034
+ Expected result: all listed tests pass.
1035
+
1036
+ - [ ] **Step 3: Run the complete web tests and production build.**
1037
+
1038
+ ```bash
1039
+ cd src/web
1040
+ npm run test:unit
1041
+ cd ../..
1042
+ npm run build:web
1043
+ ```
1044
+
1045
+ Expected result: all web tests pass and the production bundle builds successfully.
1046
+
1047
+ - [ ] **Step 4: Run the complete root regression suite.**
1048
+
1049
+ ```bash
1050
+ npm test
1051
+ ```
1052
+
1053
+ Expected result: the existing basic, API, Codex-agent, skill, plugin-market, and unit suites pass with the new catalog behavior.
1054
+
1055
+ - [ ] **Step 5: Execute an offline OMP smoke test against the packaged snapshot.**
1056
+
1057
+ ```bash
1058
+ node -e "const service=require('./src/platforms/drivers/omp/channels-implementation'); const result=service.getCatalogMetadata({providerKey:''}); if(!Array.isArray(result.models)||result.models.length===0) throw new Error('offline OMP catalog is empty'); if(result.source?.name!=='models.dev') throw new Error('unexpected catalog source'); console.log('offline OMP catalog:', result.models.length)"
1059
+ ```
1060
+
1061
+ Expected result: the command prints a positive model count without contacting Models.dev or starting `omp`.
1062
+
1063
+ - [ ] **Step 6: Check the generated snapshot for stale duplicate price declarations.**
1064
+
1065
+ ```bash
1066
+ node -e "const fs=require('fs'); const text=['src/platforms/drivers/codex/proxy-implementation.js','src/platforms/drivers/gemini/proxy-implementation.js','src/platforms/drivers/opencode/proxy-implementation.js'].map(file=>fs.readFileSync(file,'utf8')).join('\\n'); if(/const PRICING\\s*=/.test(text)) process.exit(1); console.log('duplicate proxy price tables absent')"
1067
+ ```
1068
+
1069
+ Expected result: `duplicate proxy price tables absent`.
1070
+
1071
+ - [ ] **Step 7: Commit any final verification-only fixes.**
1072
+
1073
+ ```bash
1074
+ git add scripts src/config/model-metadata.json src/server src/platforms/drivers src/web package.json .github tests
1075
+ git commit -m "feat: complete offline model catalog sync"
1076
+ ```
1077
+
1078
+ Expected result: the final implementation commit contains only the approved model catalog synchronization, pricing consolidation, OMP offline metadata, UI filtering, workflow, and tests. No LiteLLM/OpenRouter integration, runtime network refresh, or unrelated refactor is present.