waypoi 0.0.0

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 (260) hide show
  1. package/.github/instructions/ui.instructions.md +42 -0
  2. package/.github/workflows/ci.yml +35 -0
  3. package/.github/workflows/publish.yml +71 -0
  4. package/.github/workflows/release.yml +48 -0
  5. package/.playwright-mcp/console-2026-04-04T01-41-10-746Z.log +2 -0
  6. package/.playwright-mcp/console-2026-04-04T01-41-28-799Z.log +3 -0
  7. package/.playwright-mcp/console-2026-04-05T02-26-51-909Z.log +76 -0
  8. package/.playwright-mcp/page-2026-04-04T01-41-10-816Z.yml +1 -0
  9. package/.playwright-mcp/page-2026-04-04T01-41-29-141Z.yml +77 -0
  10. package/.playwright-mcp/page-2026-04-04T01-41-42-633Z.yml +190 -0
  11. package/.playwright-mcp/page-2026-04-04T01-42-03-929Z.yml +262 -0
  12. package/.playwright-mcp/page-2026-04-04T02-12-54-813Z.yml +6 -0
  13. package/.playwright-mcp/page-2026-04-04T02-14-58-600Z.yml +190 -0
  14. package/.playwright-mcp/page-2026-04-04T02-15-03-923Z.yml +190 -0
  15. package/.playwright-mcp/page-2026-04-04T02-15-07-426Z.yml +190 -0
  16. package/.playwright-mcp/page-2026-04-04T02-15-25-729Z.yml +262 -0
  17. package/.playwright-mcp/page-2026-04-04T02-16-22-984Z.yml +262 -0
  18. package/.playwright-mcp/page-2026-04-04T02-17-00-599Z.yml +190 -0
  19. package/.playwright-mcp/page-2026-04-04T02-17-50-874Z.yml +190 -0
  20. package/.playwright-mcp/page-2026-04-05T02-26-55-570Z.yml +6 -0
  21. package/AGENTS.md +48 -0
  22. package/CHANGELOG.md +131 -0
  23. package/README.md +552 -0
  24. package/assets/agent-mode.png +0 -0
  25. package/assets/categorize.png +0 -0
  26. package/assets/dashboard.png +0 -0
  27. package/assets/endpoint-proxy.png +0 -0
  28. package/assets/icon.png +0 -0
  29. package/assets/mcp-generate-image.png +0 -0
  30. package/assets/mcp-understand-image.png +0 -0
  31. package/assets/peek-token-flow.png +0 -0
  32. package/assets/playground.png +0 -0
  33. package/assets/sankey.png +0 -0
  34. package/cli/index.ts +2805 -0
  35. package/cli/legacyRewrite.ts +108 -0
  36. package/cli/modelRef.ts +24 -0
  37. package/dist/cli/index.js +2536 -0
  38. package/dist/cli/legacyRewrite.js +92 -0
  39. package/dist/cli/modelRef.js +20 -0
  40. package/dist/src/benchmark/artifacts.js +131 -0
  41. package/dist/src/benchmark/capabilityClassifier.js +81 -0
  42. package/dist/src/benchmark/capabilityStore.js +144 -0
  43. package/dist/src/benchmark/config.js +238 -0
  44. package/dist/src/benchmark/gates.js +118 -0
  45. package/dist/src/benchmark/jobs.js +252 -0
  46. package/dist/src/benchmark/runner.js +1847 -0
  47. package/dist/src/benchmark/schema.js +353 -0
  48. package/dist/src/benchmark/suites.js +314 -0
  49. package/dist/src/benchmark/tinyQaDataset.js +422 -0
  50. package/dist/src/benchmark/types.js +25 -0
  51. package/dist/src/config.js +47 -0
  52. package/dist/src/index.js +178 -0
  53. package/dist/src/mcp/client.js +215 -0
  54. package/dist/src/mcp/discovery.js +226 -0
  55. package/dist/src/mcp/policy.js +65 -0
  56. package/dist/src/mcp/registry.js +129 -0
  57. package/dist/src/mcp/service.js +460 -0
  58. package/dist/src/middleware/auth.js +179 -0
  59. package/dist/src/middleware/requestCapture.js +192 -0
  60. package/dist/src/middleware/requestStats.js +118 -0
  61. package/dist/src/pools/builder.js +132 -0
  62. package/dist/src/pools/repository.js +69 -0
  63. package/dist/src/pools/scheduler.js +360 -0
  64. package/dist/src/pools/types.js +2 -0
  65. package/dist/src/protocols/adapters/dashscope.js +267 -0
  66. package/dist/src/protocols/adapters/inferenceV2.js +346 -0
  67. package/dist/src/protocols/adapters/openai.js +27 -0
  68. package/dist/src/protocols/registry.js +99 -0
  69. package/dist/src/protocols/types.js +2 -0
  70. package/dist/src/providers/health.js +153 -0
  71. package/dist/src/providers/importer.js +289 -0
  72. package/dist/src/providers/modelRegistry.js +313 -0
  73. package/dist/src/providers/repository.js +361 -0
  74. package/dist/src/providers/types.js +2 -0
  75. package/dist/src/routes/admin.js +531 -0
  76. package/dist/src/routes/audio.js +295 -0
  77. package/dist/src/routes/chat.js +240 -0
  78. package/dist/src/routes/embeddings.js +157 -0
  79. package/dist/src/routes/images.js +288 -0
  80. package/dist/src/routes/mcp.js +256 -0
  81. package/dist/src/routes/mcpService.js +100 -0
  82. package/dist/src/routes/models.js +48 -0
  83. package/dist/src/routes/responses.js +711 -0
  84. package/dist/src/routes/sessions.js +450 -0
  85. package/dist/src/routes/stats.js +270 -0
  86. package/dist/src/routes/ui.js +97 -0
  87. package/dist/src/routes/videos.js +107 -0
  88. package/dist/src/routing/router.js +338 -0
  89. package/dist/src/services/imageGeneration.js +280 -0
  90. package/dist/src/services/imageUnderstanding.js +352 -0
  91. package/dist/src/services/videoGeneration.js +79 -0
  92. package/dist/src/storage/captureRepository.js +1591 -0
  93. package/dist/src/storage/files.js +157 -0
  94. package/dist/src/storage/imageCache.js +346 -0
  95. package/dist/src/storage/repositories.js +388 -0
  96. package/dist/src/storage/sessionRepository.js +370 -0
  97. package/dist/src/storage/statsRepository.js +204 -0
  98. package/dist/src/transport/httpClient.js +126 -0
  99. package/dist/src/types.js +2 -0
  100. package/dist/src/utils/messageMedia.js +285 -0
  101. package/dist/src/utils/modelCapabilities.js +108 -0
  102. package/dist/src/utils/modelDiscovery.js +170 -0
  103. package/dist/src/version.js +5 -0
  104. package/dist/src/workers/captureRetention.js +25 -0
  105. package/dist/src/workers/configWatcher.js +91 -0
  106. package/dist/src/workers/healthChecker.js +21 -0
  107. package/dist/src/workers/statsRotation.js +41 -0
  108. package/docs/LLM/output_schema.md +312 -0
  109. package/docs/benchmark.md +208 -0
  110. package/docs/mcp-guidelines.md +125 -0
  111. package/docs/mcp-service.md +178 -0
  112. package/docs/opencode.md +86 -0
  113. package/docs/providers.md +79 -0
  114. package/examples/benchmark.config.yaml +28 -0
  115. package/examples/providers/alibaba-dashscope.yaml +88 -0
  116. package/examples/providers/alibaba-llm.yaml +64 -0
  117. package/examples/providers/alibaba-registry.yaml +7 -0
  118. package/examples/providers/inference-v2-ray.yaml +29 -0
  119. package/examples/scenarios/assets/omni-call-sample.wav +0 -0
  120. package/examples/scenarios/custom.jsonl +5 -0
  121. package/examples/scenarios/custom.yaml +40 -0
  122. package/model-form-v2.png +0 -0
  123. package/package.json +66 -0
  124. package/provider-form-v2.png +0 -0
  125. package/provider-form.png +0 -0
  126. package/scripts/manual-test.sh +11 -0
  127. package/scripts/version-from-git.js +23 -0
  128. package/src/benchmark/artifacts.ts +149 -0
  129. package/src/benchmark/capabilityClassifier.ts +99 -0
  130. package/src/benchmark/capabilityStore.ts +174 -0
  131. package/src/benchmark/config.ts +337 -0
  132. package/src/benchmark/gates.ts +164 -0
  133. package/src/benchmark/jobs.ts +312 -0
  134. package/src/benchmark/runner.ts +2519 -0
  135. package/src/benchmark/schema.ts +443 -0
  136. package/src/benchmark/suites.ts +323 -0
  137. package/src/benchmark/tinyQaDataset.ts +428 -0
  138. package/src/benchmark/types.ts +442 -0
  139. package/src/config.ts +44 -0
  140. package/src/index.ts +195 -0
  141. package/src/mcp/client.ts +305 -0
  142. package/src/mcp/discovery.ts +266 -0
  143. package/src/mcp/policy.ts +105 -0
  144. package/src/mcp/registry.ts +164 -0
  145. package/src/mcp/service.ts +611 -0
  146. package/src/middleware/auth.ts +251 -0
  147. package/src/middleware/requestCapture.ts +245 -0
  148. package/src/middleware/requestStats.ts +163 -0
  149. package/src/pools/builder.ts +159 -0
  150. package/src/pools/repository.ts +71 -0
  151. package/src/pools/scheduler.ts +425 -0
  152. package/src/pools/types.ts +117 -0
  153. package/src/protocols/adapters/dashscope.ts +335 -0
  154. package/src/protocols/adapters/inferenceV2.ts +428 -0
  155. package/src/protocols/adapters/openai.ts +32 -0
  156. package/src/protocols/registry.ts +117 -0
  157. package/src/protocols/types.ts +81 -0
  158. package/src/providers/health.ts +207 -0
  159. package/src/providers/importer.ts +402 -0
  160. package/src/providers/modelRegistry.ts +415 -0
  161. package/src/providers/repository.ts +439 -0
  162. package/src/providers/types.ts +113 -0
  163. package/src/routes/admin.ts +666 -0
  164. package/src/routes/audio.ts +372 -0
  165. package/src/routes/chat.ts +301 -0
  166. package/src/routes/embeddings.ts +197 -0
  167. package/src/routes/images.ts +356 -0
  168. package/src/routes/mcp.ts +320 -0
  169. package/src/routes/mcpService.ts +114 -0
  170. package/src/routes/models.ts +50 -0
  171. package/src/routes/responses.ts +872 -0
  172. package/src/routes/sessions.ts +558 -0
  173. package/src/routes/stats.ts +312 -0
  174. package/src/routes/ui.ts +96 -0
  175. package/src/routes/videos.ts +132 -0
  176. package/src/routing/router.ts +501 -0
  177. package/src/services/imageGeneration.ts +396 -0
  178. package/src/services/imageUnderstanding.ts +449 -0
  179. package/src/services/videoGeneration.ts +127 -0
  180. package/src/storage/captureRepository.ts +1835 -0
  181. package/src/storage/files.ts +178 -0
  182. package/src/storage/imageCache.ts +405 -0
  183. package/src/storage/repositories.ts +494 -0
  184. package/src/storage/sessionRepository.ts +419 -0
  185. package/src/storage/statsRepository.ts +238 -0
  186. package/src/transport/httpClient.ts +145 -0
  187. package/src/types.ts +322 -0
  188. package/src/utils/messageMedia.ts +293 -0
  189. package/src/utils/modelCapabilities.ts +161 -0
  190. package/src/utils/modelDiscovery.ts +203 -0
  191. package/src/workers/captureRetention.ts +25 -0
  192. package/src/workers/configWatcher.ts +115 -0
  193. package/src/workers/healthChecker.ts +22 -0
  194. package/src/workers/statsRotation.ts +49 -0
  195. package/tests/benchmarkAdminRoutes.test.ts +82 -0
  196. package/tests/benchmarkBasics.test.ts +116 -0
  197. package/tests/captureAdminRoutes.test.ts +420 -0
  198. package/tests/captureRepository.test.ts +797 -0
  199. package/tests/cliLegacyRewrite.test.ts +45 -0
  200. package/tests/imageGeneration.service.test.ts +107 -0
  201. package/tests/imageUnderstanding.service.test.ts +123 -0
  202. package/tests/mcpPolicy.test.ts +105 -0
  203. package/tests/mcpService.test.ts +1245 -0
  204. package/tests/modelRef.test.ts +23 -0
  205. package/tests/modelsRoutes.test.ts +154 -0
  206. package/tests/sessionMediaCache.test.ts +167 -0
  207. package/tests/statsRoutes.test.ts +323 -0
  208. package/tsconfig.json +15 -0
  209. package/ui/index.html +16 -0
  210. package/ui/package-lock.json +8521 -0
  211. package/ui/package.json +52 -0
  212. package/ui/postcss.config.js +6 -0
  213. package/ui/public/assets/apple-touch-icon.png +0 -0
  214. package/ui/public/assets/favicon-16.png +0 -0
  215. package/ui/public/assets/favicon-32.png +0 -0
  216. package/ui/public/assets/icon-192.png +0 -0
  217. package/ui/public/assets/icon-512.png +0 -0
  218. package/ui/src/App.tsx +27 -0
  219. package/ui/src/api/client.ts +1503 -0
  220. package/ui/src/components/EndpointUsageGuide.tsx +361 -0
  221. package/ui/src/components/Layout.tsx +124 -0
  222. package/ui/src/components/MessageContent.tsx +365 -0
  223. package/ui/src/components/ToolCallMessage.tsx +179 -0
  224. package/ui/src/components/ToolPicker.tsx +442 -0
  225. package/ui/src/components/messageContentParser.test.ts +41 -0
  226. package/ui/src/components/messageContentParser.ts +73 -0
  227. package/ui/src/components/thinkingPreview.test.ts +27 -0
  228. package/ui/src/components/thinkingPreview.ts +15 -0
  229. package/ui/src/components/toMermaidSankey.test.ts +78 -0
  230. package/ui/src/components/toMermaidSankey.ts +56 -0
  231. package/ui/src/components/ui/button.tsx +58 -0
  232. package/ui/src/components/ui/input.tsx +21 -0
  233. package/ui/src/components/ui/textarea.tsx +21 -0
  234. package/ui/src/lib/utils.ts +6 -0
  235. package/ui/src/main.tsx +9 -0
  236. package/ui/src/pages/AgentPlayground.tsx +2010 -0
  237. package/ui/src/pages/Benchmark.tsx +988 -0
  238. package/ui/src/pages/Dashboard.tsx +581 -0
  239. package/ui/src/pages/Peek.tsx +962 -0
  240. package/ui/src/pages/Settings.tsx +2013 -0
  241. package/ui/src/pages/agentPlaygroundPayload.test.ts +109 -0
  242. package/ui/src/pages/agentPlaygroundPayload.ts +97 -0
  243. package/ui/src/pages/agentThinkingContent.test.ts +50 -0
  244. package/ui/src/pages/agentThinkingContent.ts +57 -0
  245. package/ui/src/pages/dashboardTokenUsage.test.ts +66 -0
  246. package/ui/src/pages/dashboardTokenUsage.ts +36 -0
  247. package/ui/src/pages/imageUpload.test.ts +39 -0
  248. package/ui/src/pages/imageUpload.ts +71 -0
  249. package/ui/src/pages/peekFilters.test.ts +29 -0
  250. package/ui/src/pages/peekFilters.ts +13 -0
  251. package/ui/src/pages/peekMedia.test.ts +58 -0
  252. package/ui/src/pages/peekMedia.ts +148 -0
  253. package/ui/src/pages/sessionAutoTitle.test.ts +128 -0
  254. package/ui/src/pages/sessionAutoTitle.ts +106 -0
  255. package/ui/src/stores/settings.ts +58 -0
  256. package/ui/src/styles/globals.css +223 -0
  257. package/ui/src/vite-env.d.ts +8 -0
  258. package/ui/tailwind.config.js +106 -0
  259. package/ui/tsconfig.json +32 -0
  260. package/ui/vite.config.ts +37 -0
@@ -0,0 +1,2536 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ const commander_1 = require("commander");
8
+ const repositories_1 = require("../src/storage/repositories");
9
+ const undici_1 = require("undici");
10
+ const files_1 = require("../src/storage/files");
11
+ const child_process_1 = require("child_process");
12
+ const fs_1 = __importDefault(require("fs"));
13
+ const path_1 = __importDefault(require("path"));
14
+ const router_1 = require("../src/routing/router");
15
+ const statsRepository_1 = require("../src/storage/statsRepository");
16
+ const registry_1 = require("../src/mcp/registry");
17
+ const runner_1 = require("../src/benchmark/runner");
18
+ const importer_1 = require("../src/providers/importer");
19
+ const modelRegistry_1 = require("../src/providers/modelRegistry");
20
+ const health_1 = require("../src/providers/health");
21
+ const repository_1 = require("../src/providers/repository");
22
+ const builder_1 = require("../src/pools/builder");
23
+ const repository_2 = require("../src/pools/repository");
24
+ const registry_2 = require("../src/protocols/registry");
25
+ const legacyRewrite_1 = require("./legacyRewrite");
26
+ const modelRef_1 = require("./modelRef");
27
+ const config_1 = require("../src/config");
28
+ if (!process.env.WAYPOI_DIR && config_1.appConfig.storageDirOverride) {
29
+ process.env.WAYPOI_DIR = config_1.appConfig.storageDirOverride;
30
+ }
31
+ const program = new commander_1.Command();
32
+ const paths = (0, files_1.resolveStoragePaths)();
33
+ const pidFile = path_1.default.join(paths.baseDir, config_1.appConfig.pidFileName);
34
+ const DEFAULT_PORT = String(config_1.appConfig.port);
35
+ const DISPLAY_NAME = config_1.appConfig.appName.charAt(0).toUpperCase() + config_1.appConfig.appName.slice(1);
36
+ function resolveDefaultRegistryPath() {
37
+ const candidates = [
38
+ path_1.default.resolve(process.cwd(), "providers/free-llm-api/registry.yaml"),
39
+ path_1.default.resolve(__dirname, "../providers/free-llm-api/registry.yaml"),
40
+ path_1.default.resolve(__dirname, "../../providers/free-llm-api/registry.yaml"),
41
+ ];
42
+ for (const candidate of candidates) {
43
+ if (fs_1.default.existsSync(candidate)) {
44
+ return candidate;
45
+ }
46
+ }
47
+ return candidates[0];
48
+ }
49
+ /**
50
+ * Perform an on-demand health check for all endpoints.
51
+ * Updates health.json with fresh status before returning.
52
+ */
53
+ async function refreshHealthStatus() {
54
+ const endpoints = (await (0, repositories_1.listEndpoints)(paths)).filter((endpoint) => !endpoint.disabled);
55
+ await Promise.all(endpoints.map(async (endpoint) => {
56
+ const start = Date.now();
57
+ try {
58
+ const dispatcher = endpoint.insecureTls
59
+ ? new undici_1.Agent({ connect: { rejectUnauthorized: false } })
60
+ : undefined;
61
+ const url = new URL("/v1/models", endpoint.baseUrl).toString();
62
+ const headers = {};
63
+ if (endpoint.apiKey) {
64
+ headers.authorization = `Bearer ${endpoint.apiKey}`;
65
+ }
66
+ const response = await (0, undici_1.request)(url, {
67
+ method: "GET",
68
+ headers,
69
+ headersTimeout: 3000,
70
+ bodyTimeout: 3000,
71
+ dispatcher
72
+ });
73
+ const latency = Date.now() - start;
74
+ response.body.resume();
75
+ if (response.statusCode >= 200 && response.statusCode < 300) {
76
+ await (0, repositories_1.updateHealthCheck)(paths, endpoint.id, "up", latency);
77
+ console.log(`✓ ${endpoint.name}: UP (${response.statusCode}, ${latency}ms)`);
78
+ }
79
+ else {
80
+ await (0, repositories_1.updateHealthCheck)(paths, endpoint.id, "down", null);
81
+ console.log(`✗ ${endpoint.name}: DOWN (status ${response.statusCode})`);
82
+ }
83
+ }
84
+ catch (error) {
85
+ await (0, repositories_1.updateHealthCheck)(paths, endpoint.id, "down", null);
86
+ const errorMsg = error.message || "unknown error";
87
+ console.log(`✗ ${endpoint.name}: DOWN (${errorMsg})`);
88
+ }
89
+ }));
90
+ }
91
+ function printJson(payload) {
92
+ console.log(JSON.stringify(payload, null, 2));
93
+ }
94
+ function printWarning(message) {
95
+ console.error(message);
96
+ }
97
+ function printErrorWithSuggestion(message, suggestions = []) {
98
+ console.error(message);
99
+ for (const suggestion of suggestions) {
100
+ console.error(suggestion);
101
+ }
102
+ }
103
+ function warnLegacyRewrite(result) {
104
+ if (!result.legacyUsed || process.env.WAYPOI_NO_WARN === "1") {
105
+ return;
106
+ }
107
+ const oldCmd = result.oldCmd ?? "";
108
+ const newCmd = result.newCmd ? `waypoi ${result.newCmd}` : "waypoi --help";
109
+ printWarning(`Deprecated command: ${oldCmd}`);
110
+ printWarning(`Use instead: ${newCmd}`);
111
+ }
112
+ program
113
+ .name(config_1.appConfig.appName)
114
+ .description("Waypoi proxy and operations CLI")
115
+ .version("0.7.1-beta.3")
116
+ .option("--json", "Machine-readable JSON output where supported")
117
+ .option("--quiet", "Suppress non-essential output")
118
+ .option("--no-color", "Disable ANSI color output");
119
+ program.addHelpText("after", `
120
+ Examples:
121
+ ${config_1.appConfig.appName} providers
122
+ ${config_1.appConfig.appName} models
123
+ ${config_1.appConfig.appName} models show provider-id/model-id
124
+ ${config_1.appConfig.appName} service
125
+ ${config_1.appConfig.appName} logs -f
126
+ ${config_1.appConfig.appName} bench --suite smoke
127
+ `);
128
+ program
129
+ .command("add")
130
+ .description("Add a new endpoint")
131
+ .requiredOption("--name <name>", "Endpoint display name")
132
+ .requiredOption("--url <url>", "Base URL of the endpoint")
133
+ .requiredOption("--priority <priority>", "Routing priority (lower = preferred)")
134
+ .option("--type <type>", "Endpoint type: llm (chat/completions), diffusion (/images/generations), audio, embedding", "llm")
135
+ .option("--insecureTls", "Allow self-signed TLS certificates")
136
+ .option("--apiKey <apiKey>", "Bearer token for Authorization header")
137
+ .option("--model <mapping...>", "Model mapping as 'public' or 'public=upstream'. If endpoint has 1 model, upstream is auto-detected.")
138
+ .action(async () => {
139
+ console.error("Endpoint writes are deprecated in v0.5.0. Use `waypoi models add ...` and migration commands.");
140
+ process.exitCode = 1;
141
+ });
142
+ program
143
+ .command("ls")
144
+ .option("--no-check", "Skip health check for faster listing")
145
+ .option("--verbose", "Show full endpoint fields")
146
+ .action(async (options) => {
147
+ await (0, files_1.ensureStorageDir)(paths);
148
+ // Refresh health status unless --no-check is specified
149
+ if (options.check !== false) {
150
+ await refreshHealthStatus();
151
+ }
152
+ const endpoints = await (0, repositories_1.listEndpoints)(paths);
153
+ if (endpoints.length === 0) {
154
+ console.log("No endpoints found.");
155
+ return;
156
+ }
157
+ const rows = options.verbose
158
+ ? endpoints.map((endpoint) => ({
159
+ id: endpoint.id,
160
+ name: endpoint.name,
161
+ baseUrl: endpoint.baseUrl,
162
+ type: endpoint.type,
163
+ disabled: endpoint.disabled ? "yes" : "no",
164
+ status: endpoint.health.status,
165
+ priority: endpoint.priority,
166
+ }))
167
+ : endpoints.map((endpoint) => ({
168
+ name: endpoint.name,
169
+ host: compactEndpointUrl(endpoint.baseUrl),
170
+ type: endpoint.type,
171
+ disabled: endpoint.disabled ? "yes" : "no",
172
+ status: endpoint.health.status,
173
+ prio: endpoint.priority,
174
+ }));
175
+ console.table(rows);
176
+ });
177
+ program
178
+ .command("test")
179
+ .argument("<model>")
180
+ .action(async (model) => {
181
+ await (0, files_1.ensureStorageDir)(paths);
182
+ const start = Date.now();
183
+ const controller = new AbortController();
184
+ try {
185
+ const type = await resolveModelType(model);
186
+ const isImage = type === "diffusion";
187
+ const requestPath = isImage ? "/v1/images/generations" : "/v1/chat/completions";
188
+ const payload = isImage
189
+ ? { model, prompt: "A small blue square on a white background." }
190
+ : { model, messages: [{ role: "user", content: "Say hello in one short sentence." }], max_tokens: 32 };
191
+ const outcome = await (0, router_1.routeRequest)(paths, model, requestPath, payload, {}, controller.signal);
192
+ const responseBody = await readResponsePayload(outcome.attempt.response);
193
+ const latency = Date.now() - start;
194
+ console.log(JSON.stringify({ status: outcome.attempt.response.statusCode, latencyMs: latency, response: responseBody }, null, 2));
195
+ }
196
+ catch (error) {
197
+ console.error(error.message);
198
+ process.exitCode = 1;
199
+ }
200
+ });
201
+ program
202
+ .command("rm")
203
+ .argument("<idOrName>")
204
+ .action(async () => {
205
+ console.error("Endpoint writes are deprecated in v0.5.0. Disable or migrate endpoints instead of deleting them.");
206
+ process.exitCode = 1;
207
+ });
208
+ program
209
+ .command("edit")
210
+ .description("Open the config file in your editor")
211
+ .action(async () => {
212
+ console.error("Endpoint config edit is blocked in v0.5.0. Use provider/model management commands instead.");
213
+ process.exitCode = 1;
214
+ });
215
+ program
216
+ .command("stat")
217
+ .description("Run a health check against each endpoint")
218
+ .action(async () => {
219
+ await (0, files_1.ensureStorageDir)(paths);
220
+ const endpoints = await (0, repositories_1.listEndpoints)(paths);
221
+ const activeEndpoints = endpoints.filter((endpoint) => !endpoint.disabled);
222
+ if (activeEndpoints.length === 0) {
223
+ console.log("No endpoints found.");
224
+ return;
225
+ }
226
+ const results = await Promise.all(activeEndpoints.map(async (endpoint) => {
227
+ const start = Date.now();
228
+ try {
229
+ const dispatcher = endpoint.insecureTls
230
+ ? new undici_1.Agent({ connect: { rejectUnauthorized: false } })
231
+ : undefined;
232
+ const headers = {};
233
+ if (endpoint.apiKey) {
234
+ headers.authorization = `Bearer ${endpoint.apiKey}`;
235
+ }
236
+ const response = await (0, undici_1.request)(new URL("/v1/models", endpoint.baseUrl).toString(), {
237
+ method: "GET",
238
+ headers,
239
+ headersTimeout: 3000,
240
+ bodyTimeout: 3000,
241
+ dispatcher
242
+ });
243
+ response.body.resume();
244
+ const latency = Date.now() - start;
245
+ const status = response.statusCode >= 200 && response.statusCode < 300 ? "up" : "down";
246
+ await (0, repositories_1.updateHealthCheck)(paths, endpoint.id, status, status === "up" ? latency : null);
247
+ return { name: endpoint.name, status, statusCode: response.statusCode, latencyMs: latency };
248
+ }
249
+ catch (error) {
250
+ await (0, repositories_1.updateHealthCheck)(paths, endpoint.id, "down", null);
251
+ return { name: endpoint.name, status: "down", error: error.message };
252
+ }
253
+ }));
254
+ const disabledRows = endpoints
255
+ .filter((endpoint) => endpoint.disabled)
256
+ .map((endpoint) => ({
257
+ name: endpoint.name,
258
+ status: "disabled",
259
+ error: "skipped (disabled)",
260
+ }));
261
+ if (disabledRows.length > 0) {
262
+ results.push(...disabledRows);
263
+ }
264
+ console.table(results);
265
+ });
266
+ // Alias: waypoi status -> waypoi stat
267
+ program
268
+ .command("status")
269
+ .description("Alias for 'stat' - Run a health check against each endpoint")
270
+ .action(async () => {
271
+ await (0, files_1.ensureStorageDir)(paths);
272
+ const endpoints = await (0, repositories_1.listEndpoints)(paths);
273
+ const activeEndpoints = endpoints.filter((endpoint) => !endpoint.disabled);
274
+ if (activeEndpoints.length === 0) {
275
+ console.log("No endpoints found.");
276
+ return;
277
+ }
278
+ const results = await Promise.all(activeEndpoints.map(async (endpoint) => {
279
+ const start = Date.now();
280
+ try {
281
+ const dispatcher = endpoint.insecureTls
282
+ ? new undici_1.Agent({ connect: { rejectUnauthorized: false } })
283
+ : undefined;
284
+ const headers = {};
285
+ if (endpoint.apiKey) {
286
+ headers.authorization = `Bearer ${endpoint.apiKey}`;
287
+ }
288
+ const response = await (0, undici_1.request)(new URL("/v1/models", endpoint.baseUrl).toString(), {
289
+ method: "GET",
290
+ headers,
291
+ headersTimeout: 3000,
292
+ bodyTimeout: 3000,
293
+ dispatcher
294
+ });
295
+ response.body.resume();
296
+ const latency = Date.now() - start;
297
+ const status = response.statusCode >= 200 && response.statusCode < 300 ? "up" : "down";
298
+ await (0, repositories_1.updateHealthCheck)(paths, endpoint.id, status, status === "up" ? latency : null);
299
+ return { name: endpoint.name, status, statusCode: response.statusCode, latencyMs: latency };
300
+ }
301
+ catch (error) {
302
+ await (0, repositories_1.updateHealthCheck)(paths, endpoint.id, "down", null);
303
+ return { name: endpoint.name, status: "down", error: error.message };
304
+ }
305
+ }));
306
+ const disabledRows = endpoints
307
+ .filter((endpoint) => endpoint.disabled)
308
+ .map((endpoint) => ({
309
+ name: endpoint.name,
310
+ status: "disabled",
311
+ error: "skipped (disabled)",
312
+ }));
313
+ if (disabledRows.length > 0) {
314
+ results.push(...disabledRows);
315
+ }
316
+ console.table(results);
317
+ });
318
+ program
319
+ .command("acct")
320
+ .description("Aggregate token usage per endpoint from logs")
321
+ .action(async () => {
322
+ await (0, files_1.ensureStorageDir)(paths);
323
+ const endpoints = await (0, repositories_1.listEndpoints)(paths);
324
+ const usage = await (0, repositories_1.getUsageByEndpoint)(paths);
325
+ if (usage.length === 0) {
326
+ console.log("No usage records found.");
327
+ return;
328
+ }
329
+ const byId = new Map(usage.map((entry) => [entry.endpointId, entry]));
330
+ const rows = endpoints.map((endpoint) => {
331
+ const entry = byId.get(endpoint.id);
332
+ return {
333
+ id: endpoint.id,
334
+ name: endpoint.name,
335
+ totalTokens: entry?.totalTokens ?? 0,
336
+ requests: entry?.count ?? 0
337
+ };
338
+ });
339
+ console.table(rows);
340
+ });
341
+ const service = program
342
+ .command("service")
343
+ .alias("srv")
344
+ .description("Manage the Waypoi service process")
345
+ .action(async () => {
346
+ await (0, files_1.ensureStorageDir)(paths);
347
+ const pid = readPid(pidFile);
348
+ if (pid && isRunning(pidFile)) {
349
+ console.log(`${DISPLAY_NAME} is running (pid ${pid}).`);
350
+ return;
351
+ }
352
+ console.log(`${DISPLAY_NAME} is not running.`);
353
+ });
354
+ service
355
+ .command("start")
356
+ .description("Start Waypoi in the background (PID file)")
357
+ .action(async () => {
358
+ await startService();
359
+ });
360
+ service
361
+ .command("stop")
362
+ .description("Stop Waypoi")
363
+ .action(async () => {
364
+ await stopService();
365
+ });
366
+ service
367
+ .command("restart")
368
+ .description("Restart Waypoi")
369
+ .action(async () => {
370
+ await stopService();
371
+ await startService();
372
+ });
373
+ service
374
+ .command("status")
375
+ .description("Show service status")
376
+ .action(async () => {
377
+ await (0, files_1.ensureStorageDir)(paths);
378
+ const pid = readPid(pidFile);
379
+ if (pid && isRunning(pidFile)) {
380
+ console.log(`${DISPLAY_NAME} is running (pid ${pid}).`);
381
+ return;
382
+ }
383
+ console.log(`${DISPLAY_NAME} is not running.`);
384
+ });
385
+ // ─────────────────────────────────────────────────────────────────────────────
386
+ // Logs Command
387
+ // ─────────────────────────────────────────────────────────────────────────────
388
+ program
389
+ .command("logs")
390
+ .description("Tail the waypoi log file")
391
+ .option("-f, --follow", "Follow log output (like tail -f)")
392
+ .option("-n, --lines <n>", "Number of lines to show", "50")
393
+ .action(async (options) => {
394
+ await (0, files_1.ensureStorageDir)(paths);
395
+ const logFile = path_1.default.join(paths.baseDir, "waypoi.log");
396
+ if (!fs_1.default.existsSync(logFile)) {
397
+ console.log("No log file found. Start the service first.");
398
+ return;
399
+ }
400
+ const lines = Number(options.lines) || 50;
401
+ if (options.follow) {
402
+ // Tail with follow using spawn
403
+ const tail = (0, child_process_1.spawn)("tail", ["-n", String(lines), "-f", logFile], {
404
+ stdio: "inherit"
405
+ });
406
+ process.on("SIGINT", () => {
407
+ tail.kill();
408
+ process.exit(0);
409
+ });
410
+ await new Promise((resolve) => {
411
+ tail.on("exit", resolve);
412
+ });
413
+ }
414
+ else {
415
+ // Just show last N lines
416
+ try {
417
+ const content = fs_1.default.readFileSync(logFile, "utf8");
418
+ const allLines = content.split("\n");
419
+ const lastLines = allLines.slice(-lines).join("\n");
420
+ console.log(lastLines);
421
+ }
422
+ catch (error) {
423
+ console.error(`Failed to read log file: ${error.message}`);
424
+ process.exitCode = 1;
425
+ }
426
+ }
427
+ });
428
+ // ─────────────────────────────────────────────────────────────────────────────
429
+ // Stats Command
430
+ // ─────────────────────────────────────────────────────────────────────────────
431
+ program
432
+ .command("stats")
433
+ .description("Show request statistics")
434
+ .option("--window <window>", "Time window (e.g., 24h, 7d)", "7d")
435
+ .option("--json", "Output as JSON")
436
+ .action(async (options) => {
437
+ await (0, files_1.ensureStorageDir)(paths);
438
+ // Parse window
439
+ const windowStr = options.window;
440
+ let windowMs;
441
+ if (windowStr.endsWith("h")) {
442
+ windowMs = parseInt(windowStr) * 60 * 60 * 1000;
443
+ }
444
+ else if (windowStr.endsWith("d")) {
445
+ windowMs = parseInt(windowStr) * 24 * 60 * 60 * 1000;
446
+ }
447
+ else {
448
+ windowMs = parseInt(windowStr) || 7 * 24 * 60 * 60 * 1000;
449
+ }
450
+ try {
451
+ const stats = await (0, statsRepository_1.aggregateStats)(paths, windowMs);
452
+ if (options.json) {
453
+ console.log(JSON.stringify(stats, null, 2));
454
+ return;
455
+ }
456
+ // Pretty print
457
+ console.log("\n📊 Waypoi Statistics");
458
+ console.log(` Window: ${stats.window}\n`);
459
+ console.log("── Request Summary ──");
460
+ console.log(` Total: ${stats.total}`);
461
+ console.log(` Success: ${stats.success}`);
462
+ console.log(` Errors: ${stats.errors}`);
463
+ console.log(` Rate: ${stats.total > 0 ? ((stats.success / stats.total) * 100).toFixed(1) : 0}% success\n`);
464
+ console.log("── Latency (ms) ──");
465
+ console.log(` Avg: ${stats.avgLatencyMs?.toFixed(0) ?? "N/A"}`);
466
+ console.log(` P50: ${stats.p50LatencyMs?.toFixed(0) ?? "N/A"}`);
467
+ console.log(` P95: ${stats.p95LatencyMs?.toFixed(0) ?? "N/A"}`);
468
+ console.log(` P99: ${stats.p99LatencyMs?.toFixed(0) ?? "N/A"}\n`);
469
+ console.log("── Token Usage ──");
470
+ console.log(` Total: ${stats.totalTokens.toLocaleString()}`);
471
+ console.log(` Per Hour: ${stats.tokensPerHour?.toFixed(0) ?? "N/A"}\n`);
472
+ if (Object.keys(stats.byModel).length > 0) {
473
+ console.log("── By Model ──");
474
+ console.table(Object.entries(stats.byModel).map(([model, data]) => ({
475
+ model,
476
+ requests: data.count,
477
+ avgLatencyMs: data.avgLatencyMs.toFixed(0),
478
+ tokens: data.tokens.toLocaleString()
479
+ })));
480
+ }
481
+ if (Object.keys(stats.byEndpoint).length > 0) {
482
+ console.log("── By Endpoint ──");
483
+ console.table(Object.entries(stats.byEndpoint).map(([id, data]) => ({
484
+ id: id.slice(0, 8),
485
+ requests: data.count,
486
+ avgLatencyMs: data.avgLatencyMs.toFixed(0),
487
+ tokens: data.tokens.toLocaleString(),
488
+ errors: data.errors
489
+ })));
490
+ }
491
+ }
492
+ catch (error) {
493
+ console.error(`Failed to load stats: ${error.message}`);
494
+ process.exitCode = 1;
495
+ }
496
+ });
497
+ // ─────────────────────────────────────────────────────────────────────────────
498
+ // MCP Commands
499
+ // ─────────────────────────────────────────────────────────────────────────────
500
+ async function listMcpServersAction(options = {}) {
501
+ await (0, files_1.ensureStorageDir)(paths);
502
+ try {
503
+ const servers = await (0, registry_1.listMcpServers)(paths);
504
+ if (servers.length === 0) {
505
+ console.log("No MCP servers configured.");
506
+ return;
507
+ }
508
+ if (options.json) {
509
+ printJson(servers);
510
+ return;
511
+ }
512
+ console.table(servers.map((s) => ({
513
+ id: s.id.slice(0, 8),
514
+ name: s.name,
515
+ url: s.url,
516
+ status: s.status,
517
+ enabled: s.enabled ? "✓" : "✗",
518
+ tools: s.toolCount ?? 0
519
+ })));
520
+ }
521
+ catch (error) {
522
+ console.error(`Failed to list servers: ${error.message}`);
523
+ process.exitCode = 1;
524
+ }
525
+ }
526
+ const mcp = program
527
+ .command("mcp")
528
+ .description("Manage MCP servers for agentic workflows")
529
+ .action(async () => {
530
+ await listMcpServersAction();
531
+ });
532
+ mcp
533
+ .command("add")
534
+ .description("Add a new MCP server")
535
+ .requiredOption("--name <name>", "Server name")
536
+ .requiredOption("--url <url>", "Server URL (streamable HTTP)")
537
+ .option("--disabled", "Add as disabled")
538
+ .action(async (options) => {
539
+ await (0, files_1.ensureStorageDir)(paths);
540
+ try {
541
+ const server = await (0, registry_1.addMcpServer)(paths, {
542
+ name: options.name,
543
+ url: options.url,
544
+ enabled: !options.disabled
545
+ });
546
+ console.log(`Added MCP server: ${server.name}`);
547
+ console.log(JSON.stringify(server, null, 2));
548
+ }
549
+ catch (error) {
550
+ console.error(`Failed to add server: ${error.message}`);
551
+ process.exitCode = 1;
552
+ }
553
+ });
554
+ mcp
555
+ .command("list")
556
+ .alias("ls")
557
+ .description("List all MCP servers")
558
+ .option("--json", "Output as JSON")
559
+ .action(async (options) => {
560
+ await listMcpServersAction(options);
561
+ });
562
+ mcp
563
+ .command("rm")
564
+ .alias("remove")
565
+ .description("Remove an MCP server")
566
+ .argument("<idOrName>", "Server ID (prefix) or name")
567
+ .action(async (idOrName) => {
568
+ await (0, files_1.ensureStorageDir)(paths);
569
+ try {
570
+ const servers = await (0, registry_1.listMcpServers)(paths);
571
+ const server = servers.find((s) => s.id.startsWith(idOrName) || s.name.toLowerCase() === idOrName.toLowerCase());
572
+ if (!server) {
573
+ console.error("Server not found");
574
+ process.exitCode = 1;
575
+ return;
576
+ }
577
+ await (0, registry_1.removeMcpServer)(paths, server.id);
578
+ console.log(`Removed MCP server: ${server.name}`);
579
+ }
580
+ catch (error) {
581
+ console.error(`Failed to remove server: ${error.message}`);
582
+ process.exitCode = 1;
583
+ }
584
+ });
585
+ mcp
586
+ .command("enable")
587
+ .description("Enable an MCP server")
588
+ .argument("<idOrName>", "Server ID (prefix) or name")
589
+ .action(async (idOrName) => {
590
+ await (0, files_1.ensureStorageDir)(paths);
591
+ try {
592
+ const servers = await (0, registry_1.listMcpServers)(paths);
593
+ const server = servers.find((s) => s.id.startsWith(idOrName) || s.name.toLowerCase() === idOrName.toLowerCase());
594
+ if (!server) {
595
+ console.error("Server not found");
596
+ process.exitCode = 1;
597
+ return;
598
+ }
599
+ await (0, registry_1.updateMcpServer)(paths, server.id, { enabled: true });
600
+ console.log(`Enabled MCP server: ${server.name}`);
601
+ }
602
+ catch (error) {
603
+ console.error(`Failed to enable server: ${error.message}`);
604
+ process.exitCode = 1;
605
+ }
606
+ });
607
+ mcp
608
+ .command("disable")
609
+ .description("Disable an MCP server")
610
+ .argument("<idOrName>", "Server ID (prefix) or name")
611
+ .action(async (idOrName) => {
612
+ await (0, files_1.ensureStorageDir)(paths);
613
+ try {
614
+ const servers = await (0, registry_1.listMcpServers)(paths);
615
+ const server = servers.find((s) => s.id.startsWith(idOrName) || s.name.toLowerCase() === idOrName.toLowerCase());
616
+ if (!server) {
617
+ console.error("Server not found");
618
+ process.exitCode = 1;
619
+ return;
620
+ }
621
+ await (0, registry_1.updateMcpServer)(paths, server.id, { enabled: false });
622
+ console.log(`Disabled MCP server: ${server.name}`);
623
+ }
624
+ catch (error) {
625
+ console.error(`Failed to disable server: ${error.message}`);
626
+ process.exitCode = 1;
627
+ }
628
+ });
629
+ // ─────────────────────────────────────────────────────────────────────────────
630
+ // Provider Commands
631
+ // ─────────────────────────────────────────────────────────────────────────────
632
+ async function listProvidersAction(options = {}) {
633
+ await (0, files_1.ensureStorageDir)(paths);
634
+ if (options.check !== false) {
635
+ await (0, health_1.probeProviderModels)(paths);
636
+ }
637
+ const providers = await (0, repository_1.listProviders)(paths);
638
+ if (options.json) {
639
+ printJson(providers);
640
+ return;
641
+ }
642
+ if (providers.length === 0) {
643
+ console.log("No providers imported.");
644
+ return;
645
+ }
646
+ const healthMap = await (0, health_1.getProviderModelHealthMap)(paths);
647
+ const rows = options.verbose
648
+ ? providers.map((provider) => ({
649
+ protocol: provider.protocolRaw ?? provider.protocol,
650
+ operations: (0, registry_2.listAdapterOperations)(provider.protocol)?.operations.join(",") ?? "-",
651
+ streamOps: (0, registry_2.listAdapterOperations)(provider.protocol)?.streamOperations.join(",") ?? "-",
652
+ id: provider.id,
653
+ name: provider.name,
654
+ enabled: provider.enabled ? "yes" : "no",
655
+ tls: provider.insecureTls ? "insecure" : "strict",
656
+ autoInsecureDomains: provider.autoInsecureTlsDomains?.length ?? 0,
657
+ routable: provider.supportsRouting ? "yes" : "no",
658
+ models: provider.models.length,
659
+ scored: provider.models.filter((model) => typeof model.benchmark?.livebench === "number")
660
+ .length,
661
+ hasKey: provider.apiKey || provider.models.some((model) => Boolean(model.apiKey)) ? "yes" : "no",
662
+ health: summarizeProviderHealth(provider.models, healthMap),
663
+ }))
664
+ : providers.map((provider) => ({
665
+ id: provider.id,
666
+ protocol: provider.protocolRaw ?? provider.protocol,
667
+ enabled: provider.enabled ? "yes" : "no",
668
+ tls: provider.insecureTls ? "insecure" : "strict",
669
+ autoInsecureDomains: provider.autoInsecureTlsDomains?.length ?? 0,
670
+ models: provider.models.length,
671
+ scored: provider.models.filter((model) => typeof model.benchmark?.livebench === "number")
672
+ .length,
673
+ hasKey: provider.apiKey || provider.models.some((model) => Boolean(model.apiKey)) ? "yes" : "no",
674
+ health: summarizeProviderHealth(provider.models, healthMap),
675
+ }));
676
+ console.table(rows);
677
+ }
678
+ const provider = program
679
+ .command("providers")
680
+ .alias("provider")
681
+ .alias("prov")
682
+ .description("Manage provider catalog and smart pools")
683
+ .action(async () => {
684
+ await listProvidersAction();
685
+ });
686
+ provider.addHelpText("after", `
687
+ Default: \`waypoi providers\` runs \`providers list\`.
688
+ Examples:
689
+ waypoi providers
690
+ waypoi providers show provider-id
691
+ waypoi providers import --registry ./providers/registry.yaml -f .env
692
+ `);
693
+ provider
694
+ .command("import")
695
+ .description("Import providers from a registry and load credentials")
696
+ .option("--registry <path>", "Path to providers registry yaml", resolveDefaultRegistryPath())
697
+ .option("-f, --env-file <path>", "Path to .env file", ".env")
698
+ .option("--overwrite-auth", "Overwrite stored provider keys with env values")
699
+ .option("--no-rebuild-pools", "Skip automatic smart pool rebuild")
700
+ .action(async (options) => {
701
+ await (0, files_1.ensureStorageDir)(paths);
702
+ try {
703
+ const result = await (0, importer_1.importProviders)(paths, {
704
+ registryPath: options.registry,
705
+ envFilePath: options.envFile,
706
+ overwriteAuth: Boolean(options.overwriteAuth),
707
+ });
708
+ let rebuilt = 0;
709
+ if (options.rebuildPools !== false) {
710
+ const pools = await (0, builder_1.rebuildDefaultPools)(paths);
711
+ rebuilt = pools.length;
712
+ }
713
+ console.log(`Imported providers: ${result.importedProviders}`);
714
+ console.log(`Imported models: ${result.importedModels}`);
715
+ if (rebuilt > 0) {
716
+ console.log(`Rebuilt pools: ${rebuilt}`);
717
+ }
718
+ if (result.warnings.length > 0) {
719
+ console.log("Warnings:");
720
+ for (const warning of result.warnings) {
721
+ console.log(` - ${warning}`);
722
+ }
723
+ }
724
+ }
725
+ catch (error) {
726
+ console.error(`Provider import failed: ${error.message}`);
727
+ process.exitCode = 1;
728
+ }
729
+ });
730
+ provider
731
+ .command("ls")
732
+ .alias("list")
733
+ .description("List providers")
734
+ .option("--json", "Output as JSON")
735
+ .option("--verbose", "Show protocol operation details")
736
+ .option("--no-check", "Skip health check for faster listing")
737
+ .action(async (options) => {
738
+ await listProvidersAction(options);
739
+ });
740
+ provider
741
+ .command("show")
742
+ .description("Show one provider")
743
+ .argument("<providerId>")
744
+ .action(async (providerId) => {
745
+ await (0, files_1.ensureStorageDir)(paths);
746
+ const providerRecord = await (0, repository_1.getProviderById)(paths, providerId);
747
+ if (!providerRecord) {
748
+ console.error("Provider not found");
749
+ process.exitCode = 1;
750
+ return;
751
+ }
752
+ const adapterOps = (0, registry_2.listAdapterOperations)(providerRecord.protocol);
753
+ console.log(JSON.stringify({
754
+ ...providerRecord,
755
+ supportedOperations: adapterOps?.operations ?? [],
756
+ streamSupportedOperations: adapterOps?.streamOperations ?? [],
757
+ }, null, 2));
758
+ });
759
+ provider
760
+ .command("update")
761
+ .description("Update provider TLS policy and allowlist")
762
+ .argument("<providerId>")
763
+ .option("--insecure-tls", "Set provider default TLS mode to insecure")
764
+ .option("--strict-tls", "Set provider default TLS mode to strict")
765
+ .option("--auto-insecure-domain <suffix...>", "Set auto-insecure TLS allowlist domains")
766
+ .option("--clear-auto-insecure-domains", "Clear auto-insecure TLS allowlist")
767
+ .option("--no-rebuild", "Skip automatic smart pool rebuild")
768
+ .action(async (providerId, options) => {
769
+ await (0, files_1.ensureStorageDir)(paths);
770
+ if (options.insecureTls && options.strictTls) {
771
+ console.error("Choose either --insecure-tls or --strict-tls, not both.");
772
+ process.exitCode = 1;
773
+ return;
774
+ }
775
+ const patch = {};
776
+ if (options.insecureTls) {
777
+ patch.insecureTls = true;
778
+ }
779
+ if (options.strictTls) {
780
+ patch.insecureTls = false;
781
+ }
782
+ if (options.clearAutoInsecureDomains) {
783
+ patch.autoInsecureTlsDomains = [];
784
+ }
785
+ else if (options.autoInsecureDomain) {
786
+ patch.autoInsecureTlsDomains = (0, repository_1.normalizeDomainSuffixes)(options.autoInsecureDomain);
787
+ }
788
+ if (Object.keys(patch).length === 0) {
789
+ console.error("No provider changes requested.");
790
+ process.exitCode = 1;
791
+ return;
792
+ }
793
+ const updated = await (0, repository_1.updateProvider)(paths, providerId, patch);
794
+ if (!updated) {
795
+ console.error("Provider not found");
796
+ process.exitCode = 1;
797
+ return;
798
+ }
799
+ if (options.rebuild !== false) {
800
+ await (0, builder_1.rebuildDefaultPools)(paths);
801
+ }
802
+ console.log(`Updated provider: ${updated.id}`);
803
+ });
804
+ provider
805
+ .command("models")
806
+ .description("List models for a provider")
807
+ .argument("<providerId>")
808
+ .option("--free", "Only free models")
809
+ .option("--modality <modality>", "Filter by modality (e.g., text-to-text,image-to-text)")
810
+ .option("--json", "Output as JSON")
811
+ .action(async (providerId, options) => {
812
+ await (0, files_1.ensureStorageDir)(paths);
813
+ const providerRecord = await (0, repository_1.getProviderById)(paths, providerId);
814
+ if (!providerRecord) {
815
+ console.error("Provider not found");
816
+ process.exitCode = 1;
817
+ return;
818
+ }
819
+ const modality = typeof options.modality === "string" ? options.modality.trim() : undefined;
820
+ const filtered = providerRecord.models.filter((model) => {
821
+ if (options.free && !model.free) {
822
+ return false;
823
+ }
824
+ if (modality && !model.modalities.includes(modality)) {
825
+ return false;
826
+ }
827
+ return true;
828
+ });
829
+ if (options.json) {
830
+ console.log(JSON.stringify(filtered, null, 2));
831
+ return;
832
+ }
833
+ console.table(filtered.map((model) => ({
834
+ id: model.modelId,
835
+ upstream: model.upstreamModel,
836
+ baseUrl: model.baseUrl ?? providerRecord.baseUrl,
837
+ enabled: model.enabled === false ? "no" : "yes",
838
+ free: model.free ? "yes" : "no",
839
+ modalities: model.modalities.join(","),
840
+ livebench: model.benchmark?.livebench ?? "-",
841
+ })));
842
+ });
843
+ const providerModel = provider
844
+ .command("model")
845
+ .description("Manage provider-owned models");
846
+ providerModel
847
+ .command("ls")
848
+ .description("List models for a provider")
849
+ .argument("<providerId>")
850
+ .option("--json", "Output as JSON")
851
+ .option("--enabled", "Only show enabled models")
852
+ .option("--modality <modality>", "Filter by modality (e.g. text-to-text,image-to-text)")
853
+ .option("--verbose", "Show full model metadata")
854
+ .option("--no-check", "Skip health check for faster listing")
855
+ .action(async (providerId, options) => {
856
+ await (0, files_1.ensureStorageDir)(paths);
857
+ if (options.check !== false) {
858
+ await (0, health_1.probeProviderModels)(paths);
859
+ }
860
+ const healthMap = await (0, health_1.getProviderModelHealthMap)(paths);
861
+ const models = await (0, repository_1.listProviderModels)(paths, providerId);
862
+ if (!models) {
863
+ console.error("Provider not found");
864
+ process.exitCode = 1;
865
+ return;
866
+ }
867
+ const modality = typeof options.modality === "string" ? options.modality.trim() : undefined;
868
+ const filtered = models.filter((model) => {
869
+ if (options.enabled && model.enabled === false) {
870
+ return false;
871
+ }
872
+ if (modality && !model.modalities.includes(modality)) {
873
+ return false;
874
+ }
875
+ return true;
876
+ });
877
+ if (options.json) {
878
+ console.log(JSON.stringify(filtered, null, 2));
879
+ return;
880
+ }
881
+ const rows = options.verbose
882
+ ? filtered.map((model) => ({
883
+ providerModelId: model.providerModelId,
884
+ modelId: model.modelId,
885
+ upstreamModel: model.upstreamModel,
886
+ enabled: model.enabled === false ? "no" : "yes",
887
+ tls: model.insecureTls === undefined ? "inherit" : model.insecureTls ? "insecure" : "strict",
888
+ endpointType: model.endpointType,
889
+ baseUrl: model.baseUrl ?? "-",
890
+ aliases: (model.aliases ?? []).join(","),
891
+ free: model.free ? "yes" : "no",
892
+ livebench: model.benchmark?.livebench ?? "-",
893
+ status: healthMap[model.providerModelId]?.status ?? "-",
894
+ latency: formatLatency(healthMap[model.providerModelId]?.latencyMsEwma),
895
+ lastStatus: healthMap[model.providerModelId]?.lastStatusCode ?? "-",
896
+ lastError: healthMap[model.providerModelId]?.lastError ?? "-",
897
+ }))
898
+ : filtered.map((model) => ({
899
+ id: model.modelId,
900
+ enabled: model.enabled === false ? "no" : "yes",
901
+ tls: model.insecureTls === undefined ? "inherit" : model.insecureTls ? "insecure" : "strict",
902
+ type: model.endpointType,
903
+ aliases: (model.aliases ?? []).length,
904
+ livebench: model.benchmark?.livebench ?? "-",
905
+ status: healthMap[model.providerModelId]?.status ?? "-",
906
+ latency: formatLatency(healthMap[model.providerModelId]?.latencyMsEwma),
907
+ lastStatus: healthMap[model.providerModelId]?.lastStatusCode ?? "-",
908
+ lastError: healthMap[model.providerModelId]?.lastError ?? "-",
909
+ }));
910
+ console.table(rows);
911
+ });
912
+ providerModel
913
+ .command("show")
914
+ .description("Show one model from a provider")
915
+ .argument("<providerId>")
916
+ .argument("<modelRef>")
917
+ .action(async (providerId, modelRef) => {
918
+ await (0, files_1.ensureStorageDir)(paths);
919
+ const model = await (0, repository_1.getProviderModel)(paths, providerId, modelRef);
920
+ if (!model) {
921
+ console.error("Model not found");
922
+ process.exitCode = 1;
923
+ return;
924
+ }
925
+ console.log(JSON.stringify(model, null, 2));
926
+ });
927
+ providerModel
928
+ .command("add")
929
+ .description("Add a model under a provider")
930
+ .argument("<providerId>")
931
+ .requiredOption("--model-id <id>", "Provider model ID suffix")
932
+ .requiredOption("--upstream <name>", "Upstream model name")
933
+ .requiredOption("--base-url <url>", "Base URL for this model")
934
+ .option("--api-key <key>", "API key for this model")
935
+ .option("--insecure-tls", "Allow self-signed TLS certificates for this model")
936
+ .option("--endpoint-type <type>", "Endpoint type (llm|diffusion|audio|embedding)", "llm")
937
+ .option("--capability <spec...>", "Capability spec, e.g. text->text or text+image->text")
938
+ .option("--alias <alias...>", "Legacy/public aliases")
939
+ .option("--free", "Mark model as free")
940
+ .option("--no-free", "Mark model as not free")
941
+ .option("--disabled", "Add model in disabled state")
942
+ .option("--no-rebuild", "Skip automatic pool rebuild")
943
+ .action(async (providerId, options) => {
944
+ await (0, files_1.ensureStorageDir)(paths);
945
+ const providerRecord = await (0, repository_1.getProviderById)(paths, providerId);
946
+ if (!providerRecord) {
947
+ console.error("Provider not found");
948
+ process.exitCode = 1;
949
+ return;
950
+ }
951
+ const endpointType = normalizeType(options.endpointType);
952
+ const capabilities = options.capability
953
+ ? parseCapabilitySpecs(options.capability)
954
+ : defaultCapabilitiesForEndpointType(endpointType);
955
+ const modelId = String(options.modelId).trim();
956
+ const providerModelId = (0, repository_1.canonicalProviderModelId)(providerId, modelId);
957
+ const modelRecord = {
958
+ providerModelId,
959
+ providerId,
960
+ modelId,
961
+ upstreamModel: String(options.upstream).trim(),
962
+ baseUrl: String(options.baseUrl).trim(),
963
+ apiKey: options.apiKey,
964
+ insecureTls: options.insecureTls ? true : undefined,
965
+ enabled: options.disabled ? false : true,
966
+ aliases: normalizeAliasList(options.alias ?? []),
967
+ free: options.free !== false,
968
+ modalities: capabilitiesToModalities(capabilities),
969
+ capabilities,
970
+ endpointType,
971
+ };
972
+ const result = await (0, repository_1.upsertProviderModel)(paths, providerId, modelRecord);
973
+ if (!result) {
974
+ console.error("Failed to add model");
975
+ process.exitCode = 1;
976
+ return;
977
+ }
978
+ if (options.rebuild !== false) {
979
+ await (0, builder_1.rebuildDefaultPools)(paths);
980
+ }
981
+ console.log(`Model ${result.created ? "added" : "updated"}: ${providerModelId}`);
982
+ });
983
+ providerModel
984
+ .command("update")
985
+ .description("Update a provider model")
986
+ .argument("<providerId>")
987
+ .argument("<modelRef>")
988
+ .option("--upstream <name>", "Set upstream model")
989
+ .option("--base-url <url>", "Set base URL")
990
+ .option("--clear-base-url", "Clear model-specific base URL override")
991
+ .option("--api-key <key>", "Set API key")
992
+ .option("--clear-api-key", "Clear model-specific API key")
993
+ .option("--insecure-tls", "Enable insecure TLS for this model")
994
+ .option("--clear-insecure-tls", "Clear model TLS override and inherit provider setting")
995
+ .option("--endpoint-type <type>", "Endpoint type")
996
+ .option("--capability <spec...>", "Replace capabilities")
997
+ .option("--alias <alias...>", "Set aliases")
998
+ .option("--free", "Set free=true")
999
+ .option("--not-free", "Set free=false")
1000
+ .option("--enabled", "Set enabled=true")
1001
+ .option("--disabled", "Set enabled=false")
1002
+ .option("--no-rebuild", "Skip automatic pool rebuild")
1003
+ .action(async (providerId, modelRef, options) => {
1004
+ await (0, files_1.ensureStorageDir)(paths);
1005
+ const patch = {};
1006
+ if (typeof options.upstream === "string") {
1007
+ patch.upstreamModel = options.upstream.trim();
1008
+ }
1009
+ if (typeof options.baseUrl === "string") {
1010
+ patch.baseUrl = options.baseUrl.trim();
1011
+ }
1012
+ if (options.clearBaseUrl) {
1013
+ patch.baseUrl = undefined;
1014
+ }
1015
+ if (typeof options.apiKey === "string") {
1016
+ patch.apiKey = options.apiKey;
1017
+ }
1018
+ if (options.clearApiKey) {
1019
+ patch.apiKey = undefined;
1020
+ }
1021
+ if (options.insecureTls) {
1022
+ patch.insecureTls = true;
1023
+ }
1024
+ if (options.clearInsecureTls) {
1025
+ patch.insecureTls = undefined;
1026
+ }
1027
+ if (typeof options.endpointType === "string") {
1028
+ patch.endpointType = normalizeType(options.endpointType);
1029
+ }
1030
+ if (options.capability) {
1031
+ const capabilities = parseCapabilitySpecs(options.capability);
1032
+ patch.capabilities = capabilities;
1033
+ patch.modalities = capabilitiesToModalities(capabilities);
1034
+ }
1035
+ if (options.alias) {
1036
+ patch.aliases = normalizeAliasList(options.alias);
1037
+ }
1038
+ if (options.free) {
1039
+ patch.free = true;
1040
+ }
1041
+ if (options.notFree) {
1042
+ patch.free = false;
1043
+ }
1044
+ if (options.enabled) {
1045
+ patch.enabled = true;
1046
+ }
1047
+ if (options.disabled) {
1048
+ patch.enabled = false;
1049
+ }
1050
+ if (Object.keys(patch).length === 0) {
1051
+ console.error("No changes requested.");
1052
+ process.exitCode = 1;
1053
+ return;
1054
+ }
1055
+ const updated = await (0, repository_1.updateProviderModel)(paths, providerId, modelRef, patch);
1056
+ if (!updated) {
1057
+ console.error("Model not found");
1058
+ process.exitCode = 1;
1059
+ return;
1060
+ }
1061
+ if (options.rebuild !== false) {
1062
+ await (0, builder_1.rebuildDefaultPools)(paths);
1063
+ }
1064
+ console.log(`Updated model: ${updated.providerModelId}`);
1065
+ });
1066
+ providerModel
1067
+ .command("rm")
1068
+ .description("Remove a provider model")
1069
+ .argument("<providerId>")
1070
+ .argument("<modelRef>")
1071
+ .option("--no-rebuild", "Skip automatic pool rebuild")
1072
+ .action(async (providerId, modelRef, options) => {
1073
+ await (0, files_1.ensureStorageDir)(paths);
1074
+ const removed = await (0, repository_1.deleteProviderModel)(paths, providerId, modelRef);
1075
+ if (!removed) {
1076
+ console.error("Model not found");
1077
+ process.exitCode = 1;
1078
+ return;
1079
+ }
1080
+ if (options.rebuild !== false) {
1081
+ await (0, builder_1.rebuildDefaultPools)(paths);
1082
+ }
1083
+ console.log(`Removed model: ${removed.providerModelId}`);
1084
+ });
1085
+ providerModel
1086
+ .command("enable")
1087
+ .description("Enable a provider model")
1088
+ .argument("<providerId>")
1089
+ .argument("<modelRef>")
1090
+ .option("--no-rebuild", "Skip automatic pool rebuild")
1091
+ .action(async (providerId, modelRef, options) => {
1092
+ await (0, files_1.ensureStorageDir)(paths);
1093
+ const model = await (0, repository_1.setProviderModelEnabled)(paths, providerId, modelRef, true);
1094
+ if (!model) {
1095
+ console.error("Model not found");
1096
+ process.exitCode = 1;
1097
+ return;
1098
+ }
1099
+ if (options.rebuild !== false) {
1100
+ await (0, builder_1.rebuildDefaultPools)(paths);
1101
+ }
1102
+ console.log(`Enabled model: ${model.providerModelId}`);
1103
+ });
1104
+ providerModel
1105
+ .command("disable")
1106
+ .description("Disable a provider model")
1107
+ .argument("<providerId>")
1108
+ .argument("<modelRef>")
1109
+ .option("--no-rebuild", "Skip automatic pool rebuild")
1110
+ .action(async (providerId, modelRef, options) => {
1111
+ await (0, files_1.ensureStorageDir)(paths);
1112
+ const model = await (0, repository_1.setProviderModelEnabled)(paths, providerId, modelRef, false);
1113
+ if (!model) {
1114
+ console.error("Model not found");
1115
+ process.exitCode = 1;
1116
+ return;
1117
+ }
1118
+ if (options.rebuild !== false) {
1119
+ await (0, builder_1.rebuildDefaultPools)(paths);
1120
+ }
1121
+ console.log(`Disabled model: ${model.providerModelId}`);
1122
+ });
1123
+ providerModel
1124
+ .command("set-key")
1125
+ .description("Set plaintext API key for a provider model")
1126
+ .argument("<providerId>")
1127
+ .argument("<modelRef>")
1128
+ .option("--api-key <key>", "API key value")
1129
+ .option("--env-var <name>", "Read API key from environment variable")
1130
+ .option("--no-rebuild", "Skip automatic pool rebuild")
1131
+ .action(async (providerId, modelRef, options) => {
1132
+ await (0, files_1.ensureStorageDir)(paths);
1133
+ let apiKey = options.apiKey;
1134
+ if (!apiKey && options.envVar) {
1135
+ apiKey = process.env[String(options.envVar)] ?? undefined;
1136
+ if (!apiKey) {
1137
+ console.error(`Environment variable '${options.envVar}' is not set.`);
1138
+ process.exitCode = 1;
1139
+ return;
1140
+ }
1141
+ }
1142
+ if (!apiKey) {
1143
+ console.error("Provide --api-key or --env-var.");
1144
+ process.exitCode = 1;
1145
+ return;
1146
+ }
1147
+ const model = await (0, repository_1.setProviderModelApiKey)(paths, providerId, modelRef, apiKey);
1148
+ if (!model) {
1149
+ console.error("Model not found");
1150
+ process.exitCode = 1;
1151
+ return;
1152
+ }
1153
+ if (options.rebuild !== false) {
1154
+ await (0, builder_1.rebuildDefaultPools)(paths);
1155
+ }
1156
+ console.log(`Updated key for model: ${model.providerModelId}`);
1157
+ });
1158
+ provider
1159
+ .command("enable")
1160
+ .description("Enable a provider")
1161
+ .argument("<providerId>")
1162
+ .action(async (providerId) => {
1163
+ await (0, files_1.ensureStorageDir)(paths);
1164
+ const updated = await (0, repository_1.setProviderEnabled)(paths, providerId, true);
1165
+ if (!updated) {
1166
+ console.error("Provider not found");
1167
+ process.exitCode = 1;
1168
+ return;
1169
+ }
1170
+ await (0, builder_1.rebuildDefaultPools)(paths);
1171
+ console.log(`Enabled provider: ${updated.id}`);
1172
+ });
1173
+ provider
1174
+ .command("disable")
1175
+ .description("Disable a provider")
1176
+ .argument("<providerId>")
1177
+ .action(async (providerId) => {
1178
+ await (0, files_1.ensureStorageDir)(paths);
1179
+ const updated = await (0, repository_1.setProviderEnabled)(paths, providerId, false);
1180
+ if (!updated) {
1181
+ console.error("Provider not found");
1182
+ process.exitCode = 1;
1183
+ return;
1184
+ }
1185
+ await (0, builder_1.rebuildDefaultPools)(paths);
1186
+ console.log(`Disabled provider: ${updated.id}`);
1187
+ });
1188
+ provider
1189
+ .command("migrate-endpoints")
1190
+ .description("Copy matching endpoints into a provider and disable source endpoints")
1191
+ .requiredOption("--provider <id>", "Destination provider ID (e.g. pcai)")
1192
+ .option("--match-domain <domain>", "Hostname suffix to migrate (e.g. ai-application.stjude.org)")
1193
+ .option("--all", "Migrate all endpoints (ignore domain filter)")
1194
+ .option("--protocol <protocol>", "Protocol for destination provider", "openai")
1195
+ .action(async (options) => {
1196
+ await (0, files_1.ensureStorageDir)(paths);
1197
+ const providerId = String(options.provider).trim();
1198
+ const domain = typeof options.matchDomain === "string" ? options.matchDomain.trim().toLowerCase() : "";
1199
+ const includeAll = options.all === true;
1200
+ const protocol = normalizeProviderProtocol(String(options.protocol));
1201
+ const now = new Date().toISOString();
1202
+ const warnings = [];
1203
+ let skippedEndpoints = 0;
1204
+ let migratedModels = 0;
1205
+ let createdModels = 0;
1206
+ let updatedModels = 0;
1207
+ let disabledEndpoints = 0;
1208
+ const endpointIdsToDisable = new Set();
1209
+ if (!providerId) {
1210
+ console.error("--provider is required");
1211
+ process.exitCode = 1;
1212
+ return;
1213
+ }
1214
+ if (!includeAll && !domain) {
1215
+ console.error("Provide --match-domain <domain> or use --all.");
1216
+ process.exitCode = 1;
1217
+ return;
1218
+ }
1219
+ const allEndpoints = await (0, repositories_1.listEndpoints)(paths);
1220
+ const matchedEndpoints = allEndpoints.filter((endpoint) => {
1221
+ if (includeAll) {
1222
+ return true;
1223
+ }
1224
+ try {
1225
+ const host = new URL(endpoint.baseUrl).hostname.toLowerCase();
1226
+ return hostMatchesDomain(host, domain);
1227
+ }
1228
+ catch (error) {
1229
+ warnings.push(`Skipped endpoint '${endpoint.name}': invalid baseUrl (${error.message})`);
1230
+ skippedEndpoints += 1;
1231
+ return false;
1232
+ }
1233
+ });
1234
+ if (matchedEndpoints.length === 0) {
1235
+ console.log(includeAll
1236
+ ? "No endpoints found to migrate."
1237
+ : `No endpoints matched domain suffix '${domain}'.`);
1238
+ return;
1239
+ }
1240
+ const existingProvider = await (0, repository_1.getProviderById)(paths, providerId);
1241
+ const providerSeed = {
1242
+ id: providerId,
1243
+ name: existingProvider?.name ?? providerId.toUpperCase(),
1244
+ description: existingProvider?.description ?? `Migrated endpoints for ${includeAll ? "all legacy endpoints" : domain}`,
1245
+ docs: existingProvider?.docs,
1246
+ protocol,
1247
+ protocolRaw: options.protocol,
1248
+ protocolConfig: existingProvider?.protocolConfig,
1249
+ baseUrl: existingProvider?.baseUrl ?? matchedEndpoints[0].baseUrl,
1250
+ enabled: existingProvider?.enabled ?? true,
1251
+ supportsRouting: (0, registry_2.hasProtocolAdapter)(protocol),
1252
+ auth: existingProvider?.auth ?? { type: "bearer" },
1253
+ envVar: existingProvider?.envVar,
1254
+ apiKey: existingProvider?.apiKey,
1255
+ limits: existingProvider?.limits,
1256
+ models: existingProvider?.models ?? [],
1257
+ warnings: existingProvider?.warnings,
1258
+ importedAt: existingProvider?.importedAt ?? now,
1259
+ };
1260
+ await (0, repository_1.upsertProvider)(paths, providerSeed);
1261
+ for (const endpoint of matchedEndpoints) {
1262
+ if (endpoint.models.length === 0) {
1263
+ warnings.push(`Endpoint '${endpoint.name}' has no models; skipped.`);
1264
+ skippedEndpoints += 1;
1265
+ continue;
1266
+ }
1267
+ let migratedFromEndpoint = 0;
1268
+ for (const mapping of endpoint.models) {
1269
+ const capabilities = (0, repositories_1.getModelCapabilitiesForEndpoint)(endpoint.type, mapping);
1270
+ const canonicalId = (0, repository_1.canonicalProviderModelId)(providerId, mapping.publicName);
1271
+ const aliases = normalizeAliasList([mapping.publicName, ...(existingProvider?.models ?? [])
1272
+ .filter((m) => m.modelId === mapping.publicName)
1273
+ .flatMap((m) => m.aliases ?? [])]);
1274
+ const modelRecord = {
1275
+ providerModelId: canonicalId,
1276
+ providerId,
1277
+ modelId: mapping.publicName,
1278
+ upstreamModel: mapping.upstreamModel,
1279
+ baseUrl: endpoint.baseUrl,
1280
+ apiKey: endpoint.apiKey,
1281
+ insecureTls: endpoint.insecureTls,
1282
+ enabled: true,
1283
+ aliases,
1284
+ free: true,
1285
+ modalities: capabilitiesToModalities(capabilities),
1286
+ capabilities,
1287
+ endpointType: endpoint.type,
1288
+ };
1289
+ const result = await (0, repository_1.upsertProviderModel)(paths, providerId, modelRecord);
1290
+ if (!result) {
1291
+ warnings.push(`Failed to write model '${mapping.publicName}' into provider '${providerId}'.`);
1292
+ continue;
1293
+ }
1294
+ migratedModels += 1;
1295
+ migratedFromEndpoint += 1;
1296
+ if (result.created) {
1297
+ createdModels += 1;
1298
+ }
1299
+ else {
1300
+ updatedModels += 1;
1301
+ }
1302
+ }
1303
+ if (migratedFromEndpoint > 0) {
1304
+ endpointIdsToDisable.add(endpoint.id);
1305
+ }
1306
+ else {
1307
+ warnings.push(`Endpoint '${endpoint.name}' had no models migrated; left enabled.`);
1308
+ }
1309
+ }
1310
+ for (const endpoint of matchedEndpoints) {
1311
+ if (!endpointIdsToDisable.has(endpoint.id)) {
1312
+ continue;
1313
+ }
1314
+ if (endpoint.disabled) {
1315
+ continue;
1316
+ }
1317
+ const updatedEndpoint = await (0, repositories_1.setEndpointDisabled)(paths, endpoint.id, true);
1318
+ if (updatedEndpoint) {
1319
+ disabledEndpoints += 1;
1320
+ }
1321
+ }
1322
+ const pools = await (0, builder_1.rebuildDefaultPools)(paths);
1323
+ const reportPath = writeMigrationReport(paths.baseDir, {
1324
+ timestamp: now,
1325
+ providerId,
1326
+ includeAll,
1327
+ domain: domain || undefined,
1328
+ matchedEndpoints: matchedEndpoints.length,
1329
+ migratedModels,
1330
+ createdModels,
1331
+ updatedModels,
1332
+ disabledEndpoints,
1333
+ skippedEndpoints,
1334
+ warnings,
1335
+ });
1336
+ console.log(`Migrated provider: ${providerId}`);
1337
+ console.log(`Matched endpoints: ${matchedEndpoints.length}`);
1338
+ console.log(`Migrated models: ${migratedModels} (created ${createdModels}, updated ${updatedModels})`);
1339
+ console.log(`Disabled source endpoints: ${disabledEndpoints}`);
1340
+ console.log(`Rebuilt pools: ${pools.length}`);
1341
+ console.log(`Migration report: ${reportPath}`);
1342
+ if (warnings.length > 0) {
1343
+ console.log("Warnings:");
1344
+ for (const warning of warnings) {
1345
+ console.log(` - ${warning}`);
1346
+ }
1347
+ }
1348
+ if (skippedEndpoints > 0) {
1349
+ console.log(`Skipped endpoints: ${skippedEndpoints}`);
1350
+ }
1351
+ });
1352
+ provider
1353
+ .command("pools")
1354
+ .description("List smart pools")
1355
+ .option("--json", "Output as JSON")
1356
+ .action(async (options) => {
1357
+ await (0, files_1.ensureStorageDir)(paths);
1358
+ const pools = await (0, repository_2.listPools)(paths);
1359
+ if (options.json) {
1360
+ console.log(JSON.stringify(pools, null, 2));
1361
+ return;
1362
+ }
1363
+ if (pools.length === 0) {
1364
+ console.log("No pools found.");
1365
+ return;
1366
+ }
1367
+ console.table(pools.map((pool) => ({
1368
+ id: pool.id,
1369
+ aliases: pool.aliases.join(","),
1370
+ candidates: pool.candidates.length,
1371
+ strategy: pool.strategy,
1372
+ })));
1373
+ });
1374
+ async function resolveModelTarget(modelRef) {
1375
+ const parsed = (0, modelRef_1.parseModelRef)(modelRef);
1376
+ if (parsed.providerId) {
1377
+ const model = await (0, repository_1.getProviderModel)(paths, parsed.providerId, parsed.modelId);
1378
+ if (!model) {
1379
+ printErrorWithSuggestion(`Model not found: ${parsed.providerId}/${parsed.modelId}`, [
1380
+ `Try: waypoi models ${parsed.providerId}`,
1381
+ "List providers: waypoi providers",
1382
+ ]);
1383
+ process.exitCode = 1;
1384
+ return null;
1385
+ }
1386
+ return {
1387
+ providerId: parsed.providerId,
1388
+ modelId: parsed.modelId,
1389
+ model,
1390
+ };
1391
+ }
1392
+ const providers = await (0, repository_1.listProviders)(paths);
1393
+ const matches = [];
1394
+ for (const providerEntry of providers) {
1395
+ for (const model of providerEntry.models) {
1396
+ if (model.modelId === parsed.modelId ||
1397
+ model.providerModelId === parsed.modelId ||
1398
+ (model.aliases ?? []).includes(parsed.modelId)) {
1399
+ matches.push({
1400
+ providerId: providerEntry.id,
1401
+ modelId: model.modelId,
1402
+ model,
1403
+ });
1404
+ }
1405
+ }
1406
+ }
1407
+ if (matches.length === 0) {
1408
+ printErrorWithSuggestion(`Unknown model '${parsed.modelId}'`, [
1409
+ "Try: waypoi models",
1410
+ "List providers: waypoi providers",
1411
+ ]);
1412
+ process.exitCode = 1;
1413
+ return null;
1414
+ }
1415
+ if (matches.length > 1) {
1416
+ const candidates = matches
1417
+ .map((entry) => ` - waypoi models show ${entry.providerId}/${entry.modelId}`)
1418
+ .slice(0, 10);
1419
+ printErrorWithSuggestion(`Model '${parsed.modelId}' is ambiguous across providers.`, [
1420
+ "Use a provider-qualified model reference:",
1421
+ ...candidates,
1422
+ ]);
1423
+ process.exitCode = 1;
1424
+ return null;
1425
+ }
1426
+ return matches[0];
1427
+ }
1428
+ async function listModelsAction(providerId, options) {
1429
+ await (0, files_1.ensureStorageDir)(paths);
1430
+ if (options.check !== false) {
1431
+ await (0, health_1.probeProviderModels)(paths);
1432
+ }
1433
+ const healthMap = await (0, health_1.getProviderModelHealthMap)(paths);
1434
+ const modality = typeof options.modality === "string" ? options.modality.trim() : undefined;
1435
+ if (providerId) {
1436
+ const models = await (0, repository_1.listProviderModels)(paths, providerId);
1437
+ if (!models) {
1438
+ printErrorWithSuggestion(`Provider not found: ${providerId}`, ["List providers: waypoi providers"]);
1439
+ process.exitCode = 1;
1440
+ return;
1441
+ }
1442
+ const filtered = models.filter((model) => {
1443
+ if (options.enabled && model.enabled === false) {
1444
+ return false;
1445
+ }
1446
+ if (modality && !model.modalities.includes(modality)) {
1447
+ return false;
1448
+ }
1449
+ return true;
1450
+ });
1451
+ if (options.json) {
1452
+ printJson(filtered);
1453
+ return;
1454
+ }
1455
+ const rows = options.verbose
1456
+ ? filtered.map((model) => ({
1457
+ provider: providerId,
1458
+ providerModelId: model.providerModelId,
1459
+ modelId: model.modelId,
1460
+ upstreamModel: model.upstreamModel,
1461
+ enabled: model.enabled === false ? "no" : "yes",
1462
+ tls: model.insecureTls === undefined ? "inherit" : model.insecureTls ? "insecure" : "strict",
1463
+ endpointType: model.endpointType,
1464
+ baseUrl: model.baseUrl ?? "-",
1465
+ aliases: (model.aliases ?? []).join(","),
1466
+ free: model.free ? "yes" : "no",
1467
+ livebench: model.benchmark?.livebench ?? "-",
1468
+ status: healthMap[model.providerModelId]?.status ?? "-",
1469
+ latency: formatLatency(healthMap[model.providerModelId]?.latencyMsEwma),
1470
+ lastStatus: healthMap[model.providerModelId]?.lastStatusCode ?? "-",
1471
+ lastError: healthMap[model.providerModelId]?.lastError ?? "-",
1472
+ }))
1473
+ : filtered.map((model) => ({
1474
+ provider: providerId,
1475
+ id: model.modelId,
1476
+ enabled: model.enabled === false ? "no" : "yes",
1477
+ tls: model.insecureTls === undefined ? "inherit" : model.insecureTls ? "insecure" : "strict",
1478
+ type: model.endpointType,
1479
+ aliases: (model.aliases ?? []).length,
1480
+ livebench: model.benchmark?.livebench ?? "-",
1481
+ status: healthMap[model.providerModelId]?.status ?? "-",
1482
+ latency: formatLatency(healthMap[model.providerModelId]?.latencyMsEwma),
1483
+ }));
1484
+ console.table(rows);
1485
+ return;
1486
+ }
1487
+ const providers = await (0, repository_1.listProviders)(paths);
1488
+ const flattened = providers.flatMap((providerEntry) => providerEntry.models.map((model) => ({ providerId: providerEntry.id, model })));
1489
+ const filtered = flattened.filter(({ model }) => {
1490
+ if (options.enabled && model.enabled === false) {
1491
+ return false;
1492
+ }
1493
+ if (modality && !model.modalities.includes(modality)) {
1494
+ return false;
1495
+ }
1496
+ return true;
1497
+ });
1498
+ if (options.json) {
1499
+ printJson(filtered.map(({ providerId, model }) => ({
1500
+ ...model,
1501
+ providerId,
1502
+ })));
1503
+ return;
1504
+ }
1505
+ const rows = filtered.map(({ providerId, model }) => ({
1506
+ provider: providerId,
1507
+ model: model.modelId,
1508
+ enabled: model.enabled === false ? "no" : "yes",
1509
+ type: model.endpointType,
1510
+ aliases: (model.aliases ?? []).length,
1511
+ livebench: model.benchmark?.livebench ?? "-",
1512
+ status: healthMap[model.providerModelId]?.status ?? "-",
1513
+ latency: formatLatency(healthMap[model.providerModelId]?.latencyMsEwma),
1514
+ }));
1515
+ console.table(rows);
1516
+ }
1517
+ const models = program
1518
+ .command("models")
1519
+ .alias("model")
1520
+ .description("List and manage provider-owned models")
1521
+ .argument("[providerId]")
1522
+ .option("--json", "Output as JSON")
1523
+ .option("--enabled", "Only show enabled models")
1524
+ .option("--modality <modality>", "Filter by modality (e.g. text-to-text,image-to-text)")
1525
+ .option("--verbose", "Show full model metadata")
1526
+ .option("--no-check", "Skip health check for faster listing")
1527
+ .action(async (providerId, options) => {
1528
+ await listModelsAction(providerId, options);
1529
+ });
1530
+ models.addHelpText("after", `
1531
+ Default: \`waypoi models [providerId]\` runs \`models list [providerId]\`.
1532
+ Examples:
1533
+ waypoi models
1534
+ waypoi models provider-id
1535
+ waypoi models show provider-id/model-id
1536
+ `);
1537
+ models
1538
+ .command("list")
1539
+ .alias("ls")
1540
+ .description("List models, optionally filtered by provider")
1541
+ .argument("[providerId]")
1542
+ .option("--json", "Output as JSON")
1543
+ .option("--enabled", "Only show enabled models")
1544
+ .option("--modality <modality>", "Filter by modality (e.g. text-to-text,image-to-text)")
1545
+ .option("--verbose", "Show full model metadata")
1546
+ .option("--no-check", "Skip health check for faster listing")
1547
+ .action(async (providerId, options) => {
1548
+ await listModelsAction(providerId, options);
1549
+ });
1550
+ models
1551
+ .command("show")
1552
+ .description("Show one model (provider/model preferred)")
1553
+ .argument("<modelRef>")
1554
+ .action(async (modelRef) => {
1555
+ await (0, files_1.ensureStorageDir)(paths);
1556
+ const resolved = await resolveModelTarget(modelRef);
1557
+ if (!resolved) {
1558
+ return;
1559
+ }
1560
+ printJson({
1561
+ ...resolved.model,
1562
+ providerId: resolved.providerId,
1563
+ modelId: resolved.modelId,
1564
+ });
1565
+ });
1566
+ models
1567
+ .command("enable")
1568
+ .description("Enable a provider model")
1569
+ .argument("<modelRef>")
1570
+ .option("--no-rebuild", "Skip automatic pool rebuild")
1571
+ .action(async (modelRef, options) => {
1572
+ await (0, files_1.ensureStorageDir)(paths);
1573
+ const resolved = await resolveModelTarget(modelRef);
1574
+ if (!resolved) {
1575
+ return;
1576
+ }
1577
+ const model = await (0, repository_1.setProviderModelEnabled)(paths, resolved.providerId, resolved.modelId, true);
1578
+ if (!model) {
1579
+ printErrorWithSuggestion(`Model not found: ${modelRef}`, ["Try: waypoi models"]);
1580
+ process.exitCode = 1;
1581
+ return;
1582
+ }
1583
+ if (options.rebuild !== false) {
1584
+ await (0, builder_1.rebuildDefaultPools)(paths);
1585
+ }
1586
+ console.log(`Enabled model: ${model.providerModelId}`);
1587
+ });
1588
+ models
1589
+ .command("disable")
1590
+ .description("Disable a provider model")
1591
+ .argument("<modelRef>")
1592
+ .option("--no-rebuild", "Skip automatic pool rebuild")
1593
+ .action(async (modelRef, options) => {
1594
+ await (0, files_1.ensureStorageDir)(paths);
1595
+ const resolved = await resolveModelTarget(modelRef);
1596
+ if (!resolved) {
1597
+ return;
1598
+ }
1599
+ const model = await (0, repository_1.setProviderModelEnabled)(paths, resolved.providerId, resolved.modelId, false);
1600
+ if (!model) {
1601
+ printErrorWithSuggestion(`Model not found: ${modelRef}`, ["Try: waypoi models"]);
1602
+ process.exitCode = 1;
1603
+ return;
1604
+ }
1605
+ if (options.rebuild !== false) {
1606
+ await (0, builder_1.rebuildDefaultPools)(paths);
1607
+ }
1608
+ console.log(`Disabled model: ${model.providerModelId}`);
1609
+ });
1610
+ models
1611
+ .command("set-key")
1612
+ .description("Set plaintext API key for a provider model")
1613
+ .argument("<modelRef>")
1614
+ .option("--api-key <key>", "API key value")
1615
+ .option("--env-var <name>", "Read API key from environment variable")
1616
+ .option("--no-rebuild", "Skip automatic pool rebuild")
1617
+ .action(async (modelRef, options) => {
1618
+ await (0, files_1.ensureStorageDir)(paths);
1619
+ const resolved = await resolveModelTarget(modelRef);
1620
+ if (!resolved) {
1621
+ return;
1622
+ }
1623
+ let apiKey = options.apiKey;
1624
+ if (!apiKey && options.envVar) {
1625
+ apiKey = process.env[String(options.envVar)] ?? undefined;
1626
+ if (!apiKey) {
1627
+ printErrorWithSuggestion(`Environment variable '${options.envVar}' is not set.`, [
1628
+ "Provide --api-key <key> or set the environment variable.",
1629
+ ]);
1630
+ process.exitCode = 1;
1631
+ return;
1632
+ }
1633
+ }
1634
+ if (!apiKey) {
1635
+ printErrorWithSuggestion("Provide --api-key or --env-var.", [
1636
+ "Try: waypoi models set-key provider/model --env-var API_KEY",
1637
+ ]);
1638
+ process.exitCode = 1;
1639
+ return;
1640
+ }
1641
+ const model = await (0, repository_1.setProviderModelApiKey)(paths, resolved.providerId, resolved.modelId, apiKey);
1642
+ if (!model) {
1643
+ printErrorWithSuggestion(`Model not found: ${modelRef}`, ["Try: waypoi models"]);
1644
+ process.exitCode = 1;
1645
+ return;
1646
+ }
1647
+ if (options.rebuild !== false) {
1648
+ await (0, builder_1.rebuildDefaultPools)(paths);
1649
+ }
1650
+ console.log(`Updated key for model: ${model.providerModelId}`);
1651
+ });
1652
+ models
1653
+ .command("add")
1654
+ .description("Add a model under a provider")
1655
+ .argument("<providerId>")
1656
+ .requiredOption("--model-id <id>", "Provider model ID suffix")
1657
+ .requiredOption("--upstream <name>", "Upstream model name")
1658
+ .requiredOption("--base-url <url>", "Base URL for this model")
1659
+ .option("--api-key <key>", "API key for this model")
1660
+ .option("--insecure-tls", "Allow self-signed TLS certificates for this model")
1661
+ .option("--endpoint-type <type>", "Endpoint type (llm|diffusion|audio|embedding)", "llm")
1662
+ .option("--capability <spec...>", "Capability spec, e.g. text->text or text+image->text")
1663
+ .option("--alias <alias...>", "Legacy/public aliases")
1664
+ .option("--free", "Mark model as free")
1665
+ .option("--no-free", "Mark model as not free")
1666
+ .option("--disabled", "Add model in disabled state")
1667
+ .option("--no-rebuild", "Skip automatic pool rebuild")
1668
+ .action(async (providerId, options) => {
1669
+ await (0, files_1.ensureStorageDir)(paths);
1670
+ const providerRecord = await (0, repository_1.getProviderById)(paths, providerId);
1671
+ if (!providerRecord) {
1672
+ printErrorWithSuggestion(`Provider not found: ${providerId}`, ["List providers: waypoi providers"]);
1673
+ process.exitCode = 1;
1674
+ return;
1675
+ }
1676
+ const endpointType = normalizeType(options.endpointType);
1677
+ const capabilities = options.capability
1678
+ ? parseCapabilitySpecs(options.capability)
1679
+ : defaultCapabilitiesForEndpointType(endpointType);
1680
+ const modelId = String(options.modelId).trim();
1681
+ const providerModelId = (0, repository_1.canonicalProviderModelId)(providerId, modelId);
1682
+ const modelRecord = {
1683
+ providerModelId,
1684
+ providerId,
1685
+ modelId,
1686
+ upstreamModel: String(options.upstream).trim(),
1687
+ baseUrl: String(options.baseUrl).trim(),
1688
+ apiKey: options.apiKey,
1689
+ insecureTls: options.insecureTls ? true : undefined,
1690
+ enabled: options.disabled ? false : true,
1691
+ aliases: normalizeAliasList(options.alias ?? []),
1692
+ free: options.free !== false,
1693
+ modalities: capabilitiesToModalities(capabilities),
1694
+ capabilities,
1695
+ endpointType,
1696
+ };
1697
+ const result = await (0, repository_1.upsertProviderModel)(paths, providerId, modelRecord);
1698
+ if (!result) {
1699
+ console.error("Failed to add model");
1700
+ process.exitCode = 1;
1701
+ return;
1702
+ }
1703
+ if (options.rebuild !== false) {
1704
+ await (0, builder_1.rebuildDefaultPools)(paths);
1705
+ }
1706
+ console.log(`Model ${result.created ? "added" : "updated"}: ${providerModelId}`);
1707
+ });
1708
+ models
1709
+ .command("update")
1710
+ .description("Update a provider model")
1711
+ .argument("<providerId>")
1712
+ .argument("<modelRef>")
1713
+ .option("--upstream <name>", "Set upstream model")
1714
+ .option("--base-url <url>", "Set base URL")
1715
+ .option("--clear-base-url", "Clear model-specific base URL override")
1716
+ .option("--api-key <key>", "Set API key")
1717
+ .option("--clear-api-key", "Clear model-specific API key")
1718
+ .option("--insecure-tls", "Enable insecure TLS for this model")
1719
+ .option("--clear-insecure-tls", "Clear model TLS override and inherit provider setting")
1720
+ .option("--endpoint-type <type>", "Endpoint type")
1721
+ .option("--capability <spec...>", "Replace capabilities")
1722
+ .option("--alias <alias...>", "Set aliases")
1723
+ .option("--free", "Set free=true")
1724
+ .option("--not-free", "Set free=false")
1725
+ .option("--enabled", "Set enabled=true")
1726
+ .option("--disabled", "Set enabled=false")
1727
+ .option("--no-rebuild", "Skip automatic pool rebuild")
1728
+ .action(async (providerId, modelRef, options) => {
1729
+ await (0, files_1.ensureStorageDir)(paths);
1730
+ const patch = {};
1731
+ if (typeof options.upstream === "string") {
1732
+ patch.upstreamModel = options.upstream.trim();
1733
+ }
1734
+ if (typeof options.baseUrl === "string") {
1735
+ patch.baseUrl = options.baseUrl.trim();
1736
+ }
1737
+ if (options.clearBaseUrl) {
1738
+ patch.baseUrl = undefined;
1739
+ }
1740
+ if (typeof options.apiKey === "string") {
1741
+ patch.apiKey = options.apiKey;
1742
+ }
1743
+ if (options.clearApiKey) {
1744
+ patch.apiKey = undefined;
1745
+ }
1746
+ if (options.insecureTls) {
1747
+ patch.insecureTls = true;
1748
+ }
1749
+ if (options.clearInsecureTls) {
1750
+ patch.insecureTls = undefined;
1751
+ }
1752
+ if (typeof options.endpointType === "string") {
1753
+ patch.endpointType = normalizeType(options.endpointType);
1754
+ }
1755
+ if (options.capability) {
1756
+ const capabilities = parseCapabilitySpecs(options.capability);
1757
+ patch.capabilities = capabilities;
1758
+ patch.modalities = capabilitiesToModalities(capabilities);
1759
+ }
1760
+ if (options.alias) {
1761
+ patch.aliases = normalizeAliasList(options.alias);
1762
+ }
1763
+ if (options.free) {
1764
+ patch.free = true;
1765
+ }
1766
+ if (options.notFree) {
1767
+ patch.free = false;
1768
+ }
1769
+ if (options.enabled) {
1770
+ patch.enabled = true;
1771
+ }
1772
+ if (options.disabled) {
1773
+ patch.enabled = false;
1774
+ }
1775
+ if (Object.keys(patch).length === 0) {
1776
+ printErrorWithSuggestion("No changes requested.", [
1777
+ "Try: waypoi models update <providerId> <modelRef> --upstream <name>",
1778
+ ]);
1779
+ process.exitCode = 1;
1780
+ return;
1781
+ }
1782
+ const updated = await (0, repository_1.updateProviderModel)(paths, providerId, modelRef, patch);
1783
+ if (!updated) {
1784
+ printErrorWithSuggestion(`Model not found: ${providerId}/${modelRef}`, [
1785
+ `Try: waypoi models ${providerId}`,
1786
+ ]);
1787
+ process.exitCode = 1;
1788
+ return;
1789
+ }
1790
+ if (options.rebuild !== false) {
1791
+ await (0, builder_1.rebuildDefaultPools)(paths);
1792
+ }
1793
+ console.log(`Updated model: ${updated.providerModelId}`);
1794
+ });
1795
+ models
1796
+ .command("rm")
1797
+ .description("Remove a provider model")
1798
+ .argument("<providerId>")
1799
+ .argument("<modelRef>")
1800
+ .option("--no-rebuild", "Skip automatic pool rebuild")
1801
+ .action(async (providerId, modelRef, options) => {
1802
+ await (0, files_1.ensureStorageDir)(paths);
1803
+ const removed = await (0, repository_1.deleteProviderModel)(paths, providerId, modelRef);
1804
+ if (!removed) {
1805
+ printErrorWithSuggestion(`Model not found: ${providerId}/${modelRef}`, [
1806
+ `Try: waypoi models ${providerId}`,
1807
+ ]);
1808
+ process.exitCode = 1;
1809
+ return;
1810
+ }
1811
+ if (options.rebuild !== false) {
1812
+ await (0, builder_1.rebuildDefaultPools)(paths);
1813
+ }
1814
+ console.log(`Removed model: ${removed.providerModelId}`);
1815
+ });
1816
+ // ─────────────────────────────────────────────────────────────────────────────
1817
+ // Benchmark Command
1818
+ // ─────────────────────────────────────────────────────────────────────────────
1819
+ program
1820
+ .command("bench")
1821
+ .alias("benchmark")
1822
+ .description("Run showcase benchmark examples or internal diagnostic suites")
1823
+ .option("--suite <name>", "Built-in suite to run (default: showcase)")
1824
+ .option("--example <id>", "Run one built-in example from the selected suite")
1825
+ .option("--list-examples", "List showcase examples and exit")
1826
+ .option("--scenario <path>", "Scenario file (.json, .jsonl, .yaml)")
1827
+ .option("--model <name>", "Override model for all scenarios")
1828
+ .option("--out <path>", "Output file path or directory for benchmark artifact")
1829
+ .option("--config <path>", "Benchmark config file (YAML or JSON)")
1830
+ .option("--profile <name>", "Benchmark profile (local|ci)")
1831
+ .option("--mode <name>", "Execution mode (showcase|diagnostic)")
1832
+ .option("--baseline <path>", "Baseline benchmark JSON for regression comparison")
1833
+ .option("--update-cap-cache", "Persist capability findings to capability cache")
1834
+ .option("--cap-ttl-days <n>", "Capability cache TTL days for freshness/output", parseInt)
1835
+ .option("--temperature <n>", "Run-level temperature override", parseFloat)
1836
+ .option("--top-p <n>", "Run-level top_p override", parseFloat)
1837
+ .option("--max-tokens <n>", "Run-level max_tokens override", parseInt)
1838
+ .option("--presence-penalty <n>", "Run-level presence penalty override", parseFloat)
1839
+ .option("--frequency-penalty <n>", "Run-level frequency penalty override", parseFloat)
1840
+ .option("--seed <n>", "Run-level seed override", parseInt)
1841
+ .option("--stop <value>", "Run-level stop sequence override (comma-separated for multiple)")
1842
+ .action(async (options) => {
1843
+ await (0, files_1.ensureStorageDir)(paths);
1844
+ try {
1845
+ if (options.listExamples) {
1846
+ const suiteName = options.suite ?? "showcase";
1847
+ const examples = (0, runner_1.listBenchmarkExamples)(suiteName);
1848
+ console.log(`\nExamples in suite '${suiteName}':\n`);
1849
+ console.table(examples.map((example) => ({
1850
+ id: example.id,
1851
+ mode: example.mode,
1852
+ title: example.title,
1853
+ source: example.exampleSource,
1854
+ tools: example.requiresAvailableTools ? "required" : "optional",
1855
+ })));
1856
+ return;
1857
+ }
1858
+ const { report, artifactPath, textArtifactPath } = await (0, runner_1.runBenchmark)(paths, {
1859
+ temperature: options.temperature,
1860
+ top_p: options.topP,
1861
+ max_tokens: options.maxTokens,
1862
+ presence_penalty: options.presencePenalty,
1863
+ frequency_penalty: options.frequencyPenalty,
1864
+ seed: options.seed,
1865
+ stop: typeof options.stop === "string"
1866
+ ? options.stop.split(",").map((item) => item.trim()).filter(Boolean)
1867
+ : undefined,
1868
+ suite: options.suite,
1869
+ exampleId: options.example,
1870
+ scenarioPath: options.scenario,
1871
+ modelOverride: options.model,
1872
+ outPath: options.out,
1873
+ configPath: options.config,
1874
+ profile: options.profile,
1875
+ baselinePath: options.baseline,
1876
+ executionMode: options.mode,
1877
+ updateCapCache: options.updateCapCache,
1878
+ capTtlDays: options.capTtlDays,
1879
+ });
1880
+ console.log("\n🏁 Benchmark complete");
1881
+ console.log(` Profile: ${report.profile}`);
1882
+ console.log(` Mode: ${report.executionMode}`);
1883
+ if (report.suite) {
1884
+ console.log(` Suite: ${report.suite}`);
1885
+ }
1886
+ if (report.exampleId) {
1887
+ console.log(` Example: ${report.exampleId}`);
1888
+ }
1889
+ if (report.capabilityMatrix) {
1890
+ console.log(` Cap TTL: ${report.capabilityMatrix.ttlDays}d`);
1891
+ }
1892
+ console.log(` Scenarios: ${report.total}`);
1893
+ console.log(` Executed: ${report.executed}`);
1894
+ console.log(` Skipped: ${report.skipped}`);
1895
+ console.log(` Success: ${report.succeeded}`);
1896
+ console.log(` Failed: ${report.failed}`);
1897
+ console.log(` SuccessRate: ${(report.successRate * 100).toFixed(1)}%`);
1898
+ console.log(` AvgLatency: ${report.avgLatencyMs}ms`);
1899
+ console.log(` P95Latency: ${report.p95LatencyMs}ms`);
1900
+ console.log(` Tokens: ${report.totalTokens}`);
1901
+ console.log(` ToolCalls: ${report.totalToolCalls}`);
1902
+ console.log(` Throughput: ${report.avgThroughputTokensPerSec.toFixed(2)} t/s`);
1903
+ console.log(` Artifact: ${artifactPath}\n`);
1904
+ console.log(` Summary: ${textArtifactPath}\n`);
1905
+ if (report.executionMode === "showcase" && report.scenarioDetails.length > 0) {
1906
+ console.log("Showcase details:");
1907
+ for (const detail of report.scenarioDetails) {
1908
+ console.log(`- ${detail.example?.title ?? detail.id}`);
1909
+ console.log(` Goal: ${detail.example?.userVisibleGoal ?? "n/a"}`);
1910
+ console.log(` Model: ${detail.model}`);
1911
+ console.log(` Verdict: ${detail.verdict}`);
1912
+ if (detail.usedToolNames.length > 0) {
1913
+ console.log(` Tools: ${detail.usedToolNames.join(", ")}`);
1914
+ }
1915
+ if (detail.finalResponsePreview) {
1916
+ console.log(` Final: ${detail.finalResponsePreview}`);
1917
+ }
1918
+ if (detail.exchanges.length > 0) {
1919
+ const finalExchange = detail.exchanges[detail.exchanges.length - 1];
1920
+ console.log(` Request: ${finalExchange.requestPath}`);
1921
+ console.log(` Response: ${finalExchange.responsePreview}`);
1922
+ }
1923
+ }
1924
+ console.log();
1925
+ }
1926
+ if (report.warnings.length > 0) {
1927
+ console.log("Warnings:");
1928
+ for (const warning of report.warnings) {
1929
+ console.log(` - ${warning}`);
1930
+ }
1931
+ console.log();
1932
+ }
1933
+ const skipped = report.results.filter((item) => item.status === "skipped");
1934
+ if (skipped.length > 0) {
1935
+ console.log("Skipped scenarios:");
1936
+ console.table(skipped.map((item) => ({
1937
+ id: item.id,
1938
+ mode: item.mode,
1939
+ reason: item.skippedReason ?? "no compatible model",
1940
+ })));
1941
+ }
1942
+ if (report.gateResults.soft.messages.length > 0) {
1943
+ console.log("Soft gate warnings:");
1944
+ for (const warning of report.gateResults.soft.messages) {
1945
+ console.log(` - ${warning}`);
1946
+ }
1947
+ console.log();
1948
+ }
1949
+ if (!report.gateResults.hard.passed) {
1950
+ console.log("Hard gate failures:");
1951
+ for (const failure of report.gateResults.hard.messages) {
1952
+ console.log(` - ${failure}`);
1953
+ }
1954
+ console.log();
1955
+ const failed = report.results.filter((item) => !item.success);
1956
+ if (failed.length > 0) {
1957
+ console.log("Failed scenarios:");
1958
+ console.table(failed.map((item) => ({
1959
+ id: item.id,
1960
+ mode: item.mode,
1961
+ model: item.model,
1962
+ passRate: `${(item.passRate * 100).toFixed(1)}%`,
1963
+ error: item.errorReasons[0] ?? "failed",
1964
+ })));
1965
+ }
1966
+ process.exitCode = 1;
1967
+ }
1968
+ else {
1969
+ const failed = report.results.filter((item) => !item.success);
1970
+ if (failed.length > 0) {
1971
+ console.log("Scenarios below pass-rate threshold:");
1972
+ console.table(failed.map((item) => ({
1973
+ id: item.id,
1974
+ mode: item.mode,
1975
+ model: item.model,
1976
+ passRate: `${(item.passRate * 100).toFixed(1)}%`,
1977
+ error: item.errorReasons[0] ?? "failed",
1978
+ })));
1979
+ }
1980
+ }
1981
+ if (report.gateResults.soft.messages.length > 0 && report.gateResults.hard.passed) {
1982
+ console.log("Benchmark finished with soft warnings (exit code 0).");
1983
+ }
1984
+ if (report.capabilityMatrix && report.capabilityMatrix.models.length > 0) {
1985
+ console.log("\nCapability Matrix:");
1986
+ const rows = report.capabilityMatrix.models.map((model) => ({
1987
+ model: model.model,
1988
+ freshness: model.freshness,
1989
+ verified: model.lastVerifiedAt,
1990
+ chat: model.findings.chat_basic.status,
1991
+ tools: model.findings.chat_tool_calls.status,
1992
+ embed: model.findings.embeddings.status,
1993
+ image: model.findings.images_generation.status,
1994
+ audioIn: model.findings.audio_transcription.status,
1995
+ audioOut: model.findings.audio_speech.status,
1996
+ }));
1997
+ console.table(rows);
1998
+ }
1999
+ }
2000
+ catch (error) {
2001
+ console.error(`Benchmark failed: ${error.message}`);
2002
+ process.exitCode = 1;
2003
+ }
2004
+ });
2005
+ // ─────────────────────────────────────────────────────────────────────────────
2006
+ // Chat Command (requires the waypoi server to be running)
2007
+ // ─────────────────────────────────────────────────────────────────────────────
2008
+ program
2009
+ .command("chat")
2010
+ .description("Send a message and stream the response (server must be running)")
2011
+ .argument("[message]", "Message to send (reads from stdin if omitted)")
2012
+ .option("--model <model>", "Model to use")
2013
+ .option("--session <id>", "Continue an existing session")
2014
+ .option("--no-stream", "Return full response instead of streaming")
2015
+ .option("--port <port>", "Waypoi server port", DEFAULT_PORT)
2016
+ .option("--json", "Output raw JSON response (implies --no-stream)")
2017
+ .action(async (message, options) => {
2018
+ const baseUrl = `http://localhost:${options.port}`;
2019
+ let content = message;
2020
+ // Read from stdin if no argument given and stdin is piped
2021
+ if (!content && !process.stdin.isTTY) {
2022
+ const chunks = [];
2023
+ for await (const chunk of process.stdin) {
2024
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
2025
+ }
2026
+ content = Buffer.concat(chunks).toString("utf8").trim();
2027
+ }
2028
+ if (!content) {
2029
+ console.error("Provide a message as argument or pipe it via stdin.");
2030
+ process.exitCode = 1;
2031
+ return;
2032
+ }
2033
+ // Resolve or create session
2034
+ let sessionId = options.session;
2035
+ if (!sessionId) {
2036
+ try {
2037
+ const resp = await (0, undici_1.request)(`${baseUrl}/admin/sessions`, {
2038
+ method: "POST",
2039
+ headers: { "content-type": "application/json" },
2040
+ body: JSON.stringify({ model: options.model }),
2041
+ });
2042
+ const body = await resp.body.json();
2043
+ sessionId = body.id;
2044
+ }
2045
+ catch (err) {
2046
+ console.error(`Cannot reach server at ${baseUrl} — is it running? (${config_1.appConfig.appName} service start)`);
2047
+ console.error(err.message);
2048
+ process.exitCode = 1;
2049
+ return;
2050
+ }
2051
+ }
2052
+ const payload = {
2053
+ model: options.model ?? "smart",
2054
+ messages: [{ role: "user", content }],
2055
+ stream: !options.json && options.stream !== false,
2056
+ };
2057
+ const useStream = !options.json && options.stream !== false;
2058
+ try {
2059
+ const resp = await (0, undici_1.request)(`${baseUrl}/v1/chat/completions`, {
2060
+ method: "POST",
2061
+ headers: { "content-type": "application/json" },
2062
+ body: JSON.stringify(payload),
2063
+ });
2064
+ if (!useStream || options.json) {
2065
+ const body = await resp.body.json();
2066
+ if (options.json) {
2067
+ printJson(body);
2068
+ }
2069
+ else {
2070
+ const text = body.choices?.[0]?.message?.content ?? "";
2071
+ process.stdout.write(text + "\n");
2072
+ }
2073
+ return;
2074
+ }
2075
+ // Streaming SSE
2076
+ let fullContent = "";
2077
+ const decoder = new TextDecoder();
2078
+ for await (const chunk of resp.body) {
2079
+ const text = decoder.decode(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
2080
+ for (const line of text.split("\n")) {
2081
+ if (!line.startsWith("data: "))
2082
+ continue;
2083
+ const data = line.slice(6).trim();
2084
+ if (data === "[DONE]")
2085
+ break;
2086
+ try {
2087
+ const parsed = JSON.parse(data);
2088
+ const delta = parsed.choices?.[0]?.delta?.content;
2089
+ if (delta) {
2090
+ process.stdout.write(delta);
2091
+ fullContent += delta;
2092
+ }
2093
+ }
2094
+ catch {
2095
+ // Skip malformed SSE chunk
2096
+ }
2097
+ }
2098
+ }
2099
+ process.stdout.write("\n");
2100
+ // Save to session
2101
+ if (sessionId) {
2102
+ const model = options.model ?? "smart";
2103
+ await (0, undici_1.request)(`${baseUrl}/admin/sessions/${sessionId}/messages`, {
2104
+ method: "POST",
2105
+ headers: { "content-type": "application/json" },
2106
+ body: JSON.stringify({ role: "user", content }),
2107
+ });
2108
+ await (0, undici_1.request)(`${baseUrl}/admin/sessions/${sessionId}/messages`, {
2109
+ method: "POST",
2110
+ headers: { "content-type": "application/json" },
2111
+ body: JSON.stringify({ role: "assistant", content: fullContent, model }),
2112
+ });
2113
+ console.error(`\n[session: ${sessionId}]`);
2114
+ }
2115
+ }
2116
+ catch (err) {
2117
+ console.error(`Chat request failed: ${err.message}`);
2118
+ process.exitCode = 1;
2119
+ }
2120
+ });
2121
+ // ─────────────────────────────────────────────────────────────────────────────
2122
+ // Sessions Command
2123
+ // ─────────────────────────────────────────────────────────────────────────────
2124
+ const sessions = program
2125
+ .command("sessions")
2126
+ .alias("session")
2127
+ .description("Manage chat sessions")
2128
+ .option("--port <port>", "Waypoi server port", DEFAULT_PORT)
2129
+ .option("--json", "Output as JSON")
2130
+ .action(async (options) => {
2131
+ await listSessionsAction(options);
2132
+ });
2133
+ async function listSessionsAction(options) {
2134
+ const baseUrl = `http://localhost:${options.port}`;
2135
+ try {
2136
+ const resp = await (0, undici_1.request)(`${baseUrl}/admin/sessions`, { method: "GET" });
2137
+ const body = await resp.body.json();
2138
+ if (options.json) {
2139
+ printJson(body);
2140
+ return;
2141
+ }
2142
+ if (!body.length) {
2143
+ console.log("No sessions found.");
2144
+ return;
2145
+ }
2146
+ console.table(body.map((s) => ({
2147
+ id: s.id.slice(0, 8),
2148
+ name: s.name,
2149
+ model: s.model ?? "-",
2150
+ messages: s.messageCount ?? "-",
2151
+ updated: s.updatedAt ? new Date(s.updatedAt).toLocaleString() : "-",
2152
+ })));
2153
+ }
2154
+ catch (err) {
2155
+ console.error(`Cannot reach server — is it running? (${config_1.appConfig.appName} service start)\n${err.message}`);
2156
+ process.exitCode = 1;
2157
+ }
2158
+ }
2159
+ sessions
2160
+ .command("list")
2161
+ .alias("ls")
2162
+ .description("List sessions")
2163
+ .option("--port <port>", "Waypoi server port", DEFAULT_PORT)
2164
+ .option("--json", "Output as JSON")
2165
+ .action(async (options) => {
2166
+ await listSessionsAction(options);
2167
+ });
2168
+ sessions
2169
+ .command("show <id>")
2170
+ .description("Print full message history of a session")
2171
+ .option("--port <port>", "Waypoi server port", DEFAULT_PORT)
2172
+ .option("--json", "Output as JSON")
2173
+ .action(async (id, options) => {
2174
+ const baseUrl = `http://localhost:${options.port}`;
2175
+ try {
2176
+ const resp = await (0, undici_1.request)(`${baseUrl}/admin/sessions/${id}`, { method: "GET" });
2177
+ const body = await resp.body.json();
2178
+ if (options.json) {
2179
+ printJson(body);
2180
+ return;
2181
+ }
2182
+ for (const msg of body.messages ?? []) {
2183
+ const ts = msg.createdAt ? new Date(msg.createdAt).toLocaleTimeString() : "";
2184
+ const label = msg.model ? `${msg.role} (${msg.model})` : msg.role;
2185
+ console.log(`\n[${ts}] ${label}:`);
2186
+ if (typeof msg.content === "string") {
2187
+ console.log(msg.content);
2188
+ }
2189
+ else {
2190
+ console.log(JSON.stringify(msg.content, null, 2));
2191
+ }
2192
+ }
2193
+ }
2194
+ catch (err) {
2195
+ console.error(`${err.message}`);
2196
+ process.exitCode = 1;
2197
+ }
2198
+ });
2199
+ sessions
2200
+ .command("rm <id>")
2201
+ .alias("delete")
2202
+ .description("Delete a session")
2203
+ .option("--port <port>", "Waypoi server port", DEFAULT_PORT)
2204
+ .action(async (id, options) => {
2205
+ const baseUrl = `http://localhost:${options.port}`;
2206
+ try {
2207
+ await (0, undici_1.request)(`${baseUrl}/admin/sessions/${id}`, { method: "DELETE" });
2208
+ console.log(`Deleted session: ${id}`);
2209
+ }
2210
+ catch (err) {
2211
+ console.error(`${err.message}`);
2212
+ process.exitCode = 1;
2213
+ }
2214
+ });
2215
+ sessions
2216
+ .command("export <id>")
2217
+ .description("Export session messages as JSONL to stdout")
2218
+ .option("--port <port>", "Waypoi server port", DEFAULT_PORT)
2219
+ .action(async (id, options) => {
2220
+ const baseUrl = `http://localhost:${options.port}`;
2221
+ try {
2222
+ const resp = await (0, undici_1.request)(`${baseUrl}/admin/sessions/${id}`, { method: "GET" });
2223
+ const body = await resp.body.json();
2224
+ for (const msg of body.messages ?? []) {
2225
+ process.stdout.write(JSON.stringify(msg) + "\n");
2226
+ }
2227
+ }
2228
+ catch (err) {
2229
+ console.error(`${err.message}`);
2230
+ process.exitCode = 1;
2231
+ }
2232
+ });
2233
+ // ─────────────────────────────────────────────────────────────────────────────
2234
+ const rawArgv = process.argv.slice(2);
2235
+ const rewriteResult = (0, legacyRewrite_1.rewriteLegacyArgv)(rawArgv);
2236
+ warnLegacyRewrite(rewriteResult);
2237
+ program.parseAsync(["node", "waypoi", ...rewriteResult.argv]).catch((error) => {
2238
+ console.error(error);
2239
+ process.exit(1);
2240
+ });
2241
+ function compactEndpointUrl(value, maxLength = 36) {
2242
+ try {
2243
+ const parsed = new URL(value);
2244
+ return truncateText(`${parsed.protocol}//${parsed.host}`, maxLength);
2245
+ }
2246
+ catch {
2247
+ return truncateText(value, maxLength);
2248
+ }
2249
+ }
2250
+ function truncateText(value, maxLength) {
2251
+ if (value.length <= maxLength) {
2252
+ return value;
2253
+ }
2254
+ if (maxLength <= 1) {
2255
+ return value.slice(0, maxLength);
2256
+ }
2257
+ return `${value.slice(0, maxLength - 1)}…`;
2258
+ }
2259
+ function normalizeProviderProtocol(value) {
2260
+ const normalized = (0, registry_2.canonicalizeProtocol)(value);
2261
+ if (normalized === "openai" || normalized === "inference_v2") {
2262
+ return normalized;
2263
+ }
2264
+ return "unknown";
2265
+ }
2266
+ function hostMatchesDomain(hostname, domain) {
2267
+ const normalizedHost = hostname.toLowerCase();
2268
+ const normalizedDomain = domain.replace(/^\*\./, "").toLowerCase();
2269
+ return normalizedHost === normalizedDomain || normalizedHost.endsWith(`.${normalizedDomain}`);
2270
+ }
2271
+ function capabilitiesToModalities(capabilities) {
2272
+ const modalities = new Set();
2273
+ const hasTextInput = capabilities.input.includes("text");
2274
+ const hasImageInput = capabilities.input.includes("image");
2275
+ const hasAudioInput = capabilities.input.includes("audio");
2276
+ const hasTextOutput = capabilities.output.includes("text");
2277
+ const hasImageOutput = capabilities.output.includes("image");
2278
+ const hasAudioOutput = capabilities.output.includes("audio");
2279
+ const hasEmbeddingOutput = capabilities.output.includes("embedding");
2280
+ if (hasTextInput && hasTextOutput) {
2281
+ modalities.add("text-to-text");
2282
+ }
2283
+ if (hasImageInput && hasTextOutput) {
2284
+ modalities.add("image-to-text");
2285
+ }
2286
+ if (hasTextInput && hasImageOutput) {
2287
+ modalities.add("text-to-image");
2288
+ }
2289
+ if (hasAudioInput && hasTextOutput) {
2290
+ modalities.add("audio-to-text");
2291
+ }
2292
+ if (hasTextInput && hasAudioOutput) {
2293
+ modalities.add("text-to-audio");
2294
+ }
2295
+ if (hasTextInput && hasEmbeddingOutput) {
2296
+ modalities.add("text-to-embedding");
2297
+ }
2298
+ return Array.from(modalities);
2299
+ }
2300
+ function defaultCapabilitiesForEndpointType(endpointType) {
2301
+ if (endpointType === "embedding") {
2302
+ return { input: ["text"], output: ["embedding"], source: "configured" };
2303
+ }
2304
+ if (endpointType === "diffusion") {
2305
+ return { input: ["text"], output: ["image"], source: "configured" };
2306
+ }
2307
+ if (endpointType === "audio") {
2308
+ return { input: ["audio"], output: ["text"], source: "configured" };
2309
+ }
2310
+ if (endpointType === "video") {
2311
+ return { input: ["text"], output: ["video"], source: "configured" };
2312
+ }
2313
+ return {
2314
+ input: ["text"],
2315
+ output: ["text"],
2316
+ supportsTools: true,
2317
+ supportsStreaming: true,
2318
+ source: "configured",
2319
+ };
2320
+ }
2321
+ function parseCapabilitySpecs(values) {
2322
+ const input = new Set();
2323
+ const output = new Set();
2324
+ for (const value of values) {
2325
+ const [inputSpec, outputSpec] = value.split("->").map((part) => part.trim());
2326
+ if (!inputSpec || !outputSpec) {
2327
+ throw new Error(`Invalid capability spec '${value}'. Use format input->output, e.g. text+image->text`);
2328
+ }
2329
+ for (const modality of inputSpec.split("+").map((item) => item.trim())) {
2330
+ input.add(parseModality(modality));
2331
+ }
2332
+ for (const modality of outputSpec.split("+").map((item) => item.trim())) {
2333
+ output.add(parseModality(modality));
2334
+ }
2335
+ }
2336
+ if (input.size === 0 || output.size === 0) {
2337
+ throw new Error("Capability spec must include at least one input and one output modality.");
2338
+ }
2339
+ return {
2340
+ input: Array.from(input),
2341
+ output: Array.from(output),
2342
+ source: "configured",
2343
+ };
2344
+ }
2345
+ function parseModality(value) {
2346
+ if (value === "text" || value === "image" || value === "audio" || value === "embedding" || value === "video") {
2347
+ return value;
2348
+ }
2349
+ throw new Error(`Unsupported modality '${value}'. Use one of: text,image,audio,embedding,video.`);
2350
+ }
2351
+ function normalizeAliasList(values) {
2352
+ const seen = new Set();
2353
+ for (const value of values) {
2354
+ const alias = value.trim();
2355
+ if (alias.length > 0) {
2356
+ seen.add(alias);
2357
+ }
2358
+ }
2359
+ return Array.from(seen);
2360
+ }
2361
+ function writeMigrationReport(baseDir, payload) {
2362
+ const migrationsDir = path_1.default.join(baseDir, "migrations");
2363
+ fs_1.default.mkdirSync(migrationsDir, { recursive: true });
2364
+ const filePath = path_1.default.join(migrationsDir, `migrate-${new Date().toISOString().replace(/[:.]/g, "-")}.json`);
2365
+ fs_1.default.writeFileSync(filePath, JSON.stringify(payload, null, 2), "utf8");
2366
+ return filePath;
2367
+ }
2368
+ function isImageModel(model) {
2369
+ const name = model.toLowerCase();
2370
+ return name.includes("diffusion") || name.includes("stable") || name.includes("sd") || name.includes("flux");
2371
+ }
2372
+ function isAudioModel(model) {
2373
+ const name = model.toLowerCase();
2374
+ return name.includes("whisper") || name.includes("tts") || name.includes("speech");
2375
+ }
2376
+ function isVideoModel(model) {
2377
+ const name = model.toLowerCase();
2378
+ return name.includes("wan") || name.includes("video") || name.includes("i2v") || name.includes("t2v");
2379
+ }
2380
+ async function resolveModelType(model) {
2381
+ const providerModels = await (0, modelRegistry_1.listModelsForApi)(paths);
2382
+ const providerMatch = providerModels.find((entry) => entry.id === model || entry.aliases.includes(model));
2383
+ if (providerMatch) {
2384
+ return providerMatch.endpoint_type;
2385
+ }
2386
+ const endpoints = await (0, repositories_1.listEndpoints)(paths);
2387
+ const match = endpoints.find((endpoint) => endpoint.models.some((entry) => entry.publicName === model));
2388
+ if (match) {
2389
+ return match.type;
2390
+ }
2391
+ if (isImageModel(model))
2392
+ return "diffusion";
2393
+ if (isAudioModel(model))
2394
+ return "audio";
2395
+ if (isVideoModel(model))
2396
+ return "video";
2397
+ return "llm";
2398
+ }
2399
+ function normalizeType(value) {
2400
+ if (value === "diffusion")
2401
+ return "diffusion";
2402
+ if (value === "audio")
2403
+ return "audio";
2404
+ if (value === "embedding")
2405
+ return "embedding";
2406
+ if (value === "video")
2407
+ return "video";
2408
+ return "llm";
2409
+ }
2410
+ async function readResponsePayload(response) {
2411
+ const chunks = [];
2412
+ for await (const chunk of response.body) {
2413
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
2414
+ }
2415
+ const buffer = Buffer.concat(chunks);
2416
+ const contentType = normalizeHeaders(response.headers)["content-type"] ?? "";
2417
+ if (contentType.includes("application/json")) {
2418
+ try {
2419
+ return JSON.parse(buffer.toString("utf8"));
2420
+ }
2421
+ catch {
2422
+ return buffer.toString("utf8");
2423
+ }
2424
+ }
2425
+ return buffer.toString("utf8");
2426
+ }
2427
+ function normalizeHeaders(headers) {
2428
+ const normalized = {};
2429
+ for (const [key, value] of Object.entries(headers)) {
2430
+ normalized[key.toLowerCase()] = Array.isArray(value) ? value.join(", ") : value;
2431
+ }
2432
+ return normalized;
2433
+ }
2434
+ function readPid(filePath) {
2435
+ try {
2436
+ const raw = fs_1.default.readFileSync(filePath, "utf8").trim();
2437
+ const pid = Number(raw);
2438
+ return Number.isFinite(pid) ? pid : null;
2439
+ }
2440
+ catch {
2441
+ return null;
2442
+ }
2443
+ }
2444
+ function isRunning(filePath) {
2445
+ const pid = readPid(filePath);
2446
+ if (!pid) {
2447
+ return false;
2448
+ }
2449
+ try {
2450
+ process.kill(pid, 0);
2451
+ return true;
2452
+ }
2453
+ catch {
2454
+ return false;
2455
+ }
2456
+ }
2457
+ function summarizeProviderHealth(models, healthMap) {
2458
+ const enabled = models.filter((model) => model.enabled !== false);
2459
+ let up = 0;
2460
+ let down = 0;
2461
+ for (const model of enabled) {
2462
+ const health = healthMap[model.providerModelId];
2463
+ if (health?.status === "up") {
2464
+ up += 1;
2465
+ }
2466
+ else if (health?.status === "down") {
2467
+ down += 1;
2468
+ }
2469
+ }
2470
+ return `${up}/${down}/${enabled.length}`;
2471
+ }
2472
+ function formatLatency(latency) {
2473
+ if (!latency || !Number.isFinite(latency)) {
2474
+ return "-";
2475
+ }
2476
+ return `${Math.round(latency)}ms`;
2477
+ }
2478
+ async function startService() {
2479
+ await (0, files_1.ensureStorageDir)(paths);
2480
+ const existingPid = readPid(pidFile);
2481
+ if (existingPid) {
2482
+ if (isRunning(pidFile)) {
2483
+ console.log(`${DISPLAY_NAME} is already running.`);
2484
+ return;
2485
+ }
2486
+ fs_1.default.unlinkSync(pidFile);
2487
+ }
2488
+ const rootDir = getPackageRoot();
2489
+ const entry = path_1.default.join(rootDir, "dist", "src", "index.js");
2490
+ if (!fs_1.default.existsSync(entry)) {
2491
+ console.error(`Missing ${entry}. Run npm run build first.`);
2492
+ process.exitCode = 1;
2493
+ return;
2494
+ }
2495
+ const child = (0, child_process_1.spawn)(process.execPath, [entry], {
2496
+ detached: true,
2497
+ stdio: "ignore",
2498
+ env: {
2499
+ ...process.env
2500
+ }
2501
+ });
2502
+ if (!child.pid) {
2503
+ console.error(`Failed to start ${DISPLAY_NAME}.`);
2504
+ process.exitCode = 1;
2505
+ return;
2506
+ }
2507
+ child.unref();
2508
+ fs_1.default.writeFileSync(pidFile, String(child.pid), "utf8");
2509
+ console.log(`${DISPLAY_NAME} started (pid ${child.pid}).`);
2510
+ }
2511
+ async function stopService() {
2512
+ await (0, files_1.ensureStorageDir)(paths);
2513
+ const pid = readPid(pidFile);
2514
+ if (!pid) {
2515
+ console.log(`${DISPLAY_NAME} is not running.`);
2516
+ return;
2517
+ }
2518
+ try {
2519
+ process.kill(pid);
2520
+ fs_1.default.unlinkSync(pidFile);
2521
+ console.log(`${DISPLAY_NAME} stopped.`);
2522
+ }
2523
+ catch (error) {
2524
+ console.error(`Failed to stop: ${error.message}`);
2525
+ process.exitCode = 1;
2526
+ }
2527
+ }
2528
+ function getPackageRoot() {
2529
+ const dir = __dirname;
2530
+ const base = path_1.default.basename(dir);
2531
+ const parent = path_1.default.basename(path_1.default.dirname(dir));
2532
+ if (base === "cli" && parent === "dist") {
2533
+ return path_1.default.resolve(dir, "..", "..");
2534
+ }
2535
+ return path_1.default.resolve(dir, "..");
2536
+ }