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.
- package/.github/instructions/ui.instructions.md +42 -0
- package/.github/workflows/ci.yml +35 -0
- package/.github/workflows/publish.yml +71 -0
- package/.github/workflows/release.yml +48 -0
- package/.playwright-mcp/console-2026-04-04T01-41-10-746Z.log +2 -0
- package/.playwright-mcp/console-2026-04-04T01-41-28-799Z.log +3 -0
- package/.playwright-mcp/console-2026-04-05T02-26-51-909Z.log +76 -0
- package/.playwright-mcp/page-2026-04-04T01-41-10-816Z.yml +1 -0
- package/.playwright-mcp/page-2026-04-04T01-41-29-141Z.yml +77 -0
- package/.playwright-mcp/page-2026-04-04T01-41-42-633Z.yml +190 -0
- package/.playwright-mcp/page-2026-04-04T01-42-03-929Z.yml +262 -0
- package/.playwright-mcp/page-2026-04-04T02-12-54-813Z.yml +6 -0
- package/.playwright-mcp/page-2026-04-04T02-14-58-600Z.yml +190 -0
- package/.playwright-mcp/page-2026-04-04T02-15-03-923Z.yml +190 -0
- package/.playwright-mcp/page-2026-04-04T02-15-07-426Z.yml +190 -0
- package/.playwright-mcp/page-2026-04-04T02-15-25-729Z.yml +262 -0
- package/.playwright-mcp/page-2026-04-04T02-16-22-984Z.yml +262 -0
- package/.playwright-mcp/page-2026-04-04T02-17-00-599Z.yml +190 -0
- package/.playwright-mcp/page-2026-04-04T02-17-50-874Z.yml +190 -0
- package/.playwright-mcp/page-2026-04-05T02-26-55-570Z.yml +6 -0
- package/AGENTS.md +48 -0
- package/CHANGELOG.md +131 -0
- package/README.md +552 -0
- package/assets/agent-mode.png +0 -0
- package/assets/categorize.png +0 -0
- package/assets/dashboard.png +0 -0
- package/assets/endpoint-proxy.png +0 -0
- package/assets/icon.png +0 -0
- package/assets/mcp-generate-image.png +0 -0
- package/assets/mcp-understand-image.png +0 -0
- package/assets/peek-token-flow.png +0 -0
- package/assets/playground.png +0 -0
- package/assets/sankey.png +0 -0
- package/cli/index.ts +2805 -0
- package/cli/legacyRewrite.ts +108 -0
- package/cli/modelRef.ts +24 -0
- package/dist/cli/index.js +2536 -0
- package/dist/cli/legacyRewrite.js +92 -0
- package/dist/cli/modelRef.js +20 -0
- package/dist/src/benchmark/artifacts.js +131 -0
- package/dist/src/benchmark/capabilityClassifier.js +81 -0
- package/dist/src/benchmark/capabilityStore.js +144 -0
- package/dist/src/benchmark/config.js +238 -0
- package/dist/src/benchmark/gates.js +118 -0
- package/dist/src/benchmark/jobs.js +252 -0
- package/dist/src/benchmark/runner.js +1847 -0
- package/dist/src/benchmark/schema.js +353 -0
- package/dist/src/benchmark/suites.js +314 -0
- package/dist/src/benchmark/tinyQaDataset.js +422 -0
- package/dist/src/benchmark/types.js +25 -0
- package/dist/src/config.js +47 -0
- package/dist/src/index.js +178 -0
- package/dist/src/mcp/client.js +215 -0
- package/dist/src/mcp/discovery.js +226 -0
- package/dist/src/mcp/policy.js +65 -0
- package/dist/src/mcp/registry.js +129 -0
- package/dist/src/mcp/service.js +460 -0
- package/dist/src/middleware/auth.js +179 -0
- package/dist/src/middleware/requestCapture.js +192 -0
- package/dist/src/middleware/requestStats.js +118 -0
- package/dist/src/pools/builder.js +132 -0
- package/dist/src/pools/repository.js +69 -0
- package/dist/src/pools/scheduler.js +360 -0
- package/dist/src/pools/types.js +2 -0
- package/dist/src/protocols/adapters/dashscope.js +267 -0
- package/dist/src/protocols/adapters/inferenceV2.js +346 -0
- package/dist/src/protocols/adapters/openai.js +27 -0
- package/dist/src/protocols/registry.js +99 -0
- package/dist/src/protocols/types.js +2 -0
- package/dist/src/providers/health.js +153 -0
- package/dist/src/providers/importer.js +289 -0
- package/dist/src/providers/modelRegistry.js +313 -0
- package/dist/src/providers/repository.js +361 -0
- package/dist/src/providers/types.js +2 -0
- package/dist/src/routes/admin.js +531 -0
- package/dist/src/routes/audio.js +295 -0
- package/dist/src/routes/chat.js +240 -0
- package/dist/src/routes/embeddings.js +157 -0
- package/dist/src/routes/images.js +288 -0
- package/dist/src/routes/mcp.js +256 -0
- package/dist/src/routes/mcpService.js +100 -0
- package/dist/src/routes/models.js +48 -0
- package/dist/src/routes/responses.js +711 -0
- package/dist/src/routes/sessions.js +450 -0
- package/dist/src/routes/stats.js +270 -0
- package/dist/src/routes/ui.js +97 -0
- package/dist/src/routes/videos.js +107 -0
- package/dist/src/routing/router.js +338 -0
- package/dist/src/services/imageGeneration.js +280 -0
- package/dist/src/services/imageUnderstanding.js +352 -0
- package/dist/src/services/videoGeneration.js +79 -0
- package/dist/src/storage/captureRepository.js +1591 -0
- package/dist/src/storage/files.js +157 -0
- package/dist/src/storage/imageCache.js +346 -0
- package/dist/src/storage/repositories.js +388 -0
- package/dist/src/storage/sessionRepository.js +370 -0
- package/dist/src/storage/statsRepository.js +204 -0
- package/dist/src/transport/httpClient.js +126 -0
- package/dist/src/types.js +2 -0
- package/dist/src/utils/messageMedia.js +285 -0
- package/dist/src/utils/modelCapabilities.js +108 -0
- package/dist/src/utils/modelDiscovery.js +170 -0
- package/dist/src/version.js +5 -0
- package/dist/src/workers/captureRetention.js +25 -0
- package/dist/src/workers/configWatcher.js +91 -0
- package/dist/src/workers/healthChecker.js +21 -0
- package/dist/src/workers/statsRotation.js +41 -0
- package/docs/LLM/output_schema.md +312 -0
- package/docs/benchmark.md +208 -0
- package/docs/mcp-guidelines.md +125 -0
- package/docs/mcp-service.md +178 -0
- package/docs/opencode.md +86 -0
- package/docs/providers.md +79 -0
- package/examples/benchmark.config.yaml +28 -0
- package/examples/providers/alibaba-dashscope.yaml +88 -0
- package/examples/providers/alibaba-llm.yaml +64 -0
- package/examples/providers/alibaba-registry.yaml +7 -0
- package/examples/providers/inference-v2-ray.yaml +29 -0
- package/examples/scenarios/assets/omni-call-sample.wav +0 -0
- package/examples/scenarios/custom.jsonl +5 -0
- package/examples/scenarios/custom.yaml +40 -0
- package/model-form-v2.png +0 -0
- package/package.json +66 -0
- package/provider-form-v2.png +0 -0
- package/provider-form.png +0 -0
- package/scripts/manual-test.sh +11 -0
- package/scripts/version-from-git.js +23 -0
- package/src/benchmark/artifacts.ts +149 -0
- package/src/benchmark/capabilityClassifier.ts +99 -0
- package/src/benchmark/capabilityStore.ts +174 -0
- package/src/benchmark/config.ts +337 -0
- package/src/benchmark/gates.ts +164 -0
- package/src/benchmark/jobs.ts +312 -0
- package/src/benchmark/runner.ts +2519 -0
- package/src/benchmark/schema.ts +443 -0
- package/src/benchmark/suites.ts +323 -0
- package/src/benchmark/tinyQaDataset.ts +428 -0
- package/src/benchmark/types.ts +442 -0
- package/src/config.ts +44 -0
- package/src/index.ts +195 -0
- package/src/mcp/client.ts +305 -0
- package/src/mcp/discovery.ts +266 -0
- package/src/mcp/policy.ts +105 -0
- package/src/mcp/registry.ts +164 -0
- package/src/mcp/service.ts +611 -0
- package/src/middleware/auth.ts +251 -0
- package/src/middleware/requestCapture.ts +245 -0
- package/src/middleware/requestStats.ts +163 -0
- package/src/pools/builder.ts +159 -0
- package/src/pools/repository.ts +71 -0
- package/src/pools/scheduler.ts +425 -0
- package/src/pools/types.ts +117 -0
- package/src/protocols/adapters/dashscope.ts +335 -0
- package/src/protocols/adapters/inferenceV2.ts +428 -0
- package/src/protocols/adapters/openai.ts +32 -0
- package/src/protocols/registry.ts +117 -0
- package/src/protocols/types.ts +81 -0
- package/src/providers/health.ts +207 -0
- package/src/providers/importer.ts +402 -0
- package/src/providers/modelRegistry.ts +415 -0
- package/src/providers/repository.ts +439 -0
- package/src/providers/types.ts +113 -0
- package/src/routes/admin.ts +666 -0
- package/src/routes/audio.ts +372 -0
- package/src/routes/chat.ts +301 -0
- package/src/routes/embeddings.ts +197 -0
- package/src/routes/images.ts +356 -0
- package/src/routes/mcp.ts +320 -0
- package/src/routes/mcpService.ts +114 -0
- package/src/routes/models.ts +50 -0
- package/src/routes/responses.ts +872 -0
- package/src/routes/sessions.ts +558 -0
- package/src/routes/stats.ts +312 -0
- package/src/routes/ui.ts +96 -0
- package/src/routes/videos.ts +132 -0
- package/src/routing/router.ts +501 -0
- package/src/services/imageGeneration.ts +396 -0
- package/src/services/imageUnderstanding.ts +449 -0
- package/src/services/videoGeneration.ts +127 -0
- package/src/storage/captureRepository.ts +1835 -0
- package/src/storage/files.ts +178 -0
- package/src/storage/imageCache.ts +405 -0
- package/src/storage/repositories.ts +494 -0
- package/src/storage/sessionRepository.ts +419 -0
- package/src/storage/statsRepository.ts +238 -0
- package/src/transport/httpClient.ts +145 -0
- package/src/types.ts +322 -0
- package/src/utils/messageMedia.ts +293 -0
- package/src/utils/modelCapabilities.ts +161 -0
- package/src/utils/modelDiscovery.ts +203 -0
- package/src/workers/captureRetention.ts +25 -0
- package/src/workers/configWatcher.ts +115 -0
- package/src/workers/healthChecker.ts +22 -0
- package/src/workers/statsRotation.ts +49 -0
- package/tests/benchmarkAdminRoutes.test.ts +82 -0
- package/tests/benchmarkBasics.test.ts +116 -0
- package/tests/captureAdminRoutes.test.ts +420 -0
- package/tests/captureRepository.test.ts +797 -0
- package/tests/cliLegacyRewrite.test.ts +45 -0
- package/tests/imageGeneration.service.test.ts +107 -0
- package/tests/imageUnderstanding.service.test.ts +123 -0
- package/tests/mcpPolicy.test.ts +105 -0
- package/tests/mcpService.test.ts +1245 -0
- package/tests/modelRef.test.ts +23 -0
- package/tests/modelsRoutes.test.ts +154 -0
- package/tests/sessionMediaCache.test.ts +167 -0
- package/tests/statsRoutes.test.ts +323 -0
- package/tsconfig.json +15 -0
- package/ui/index.html +16 -0
- package/ui/package-lock.json +8521 -0
- package/ui/package.json +52 -0
- package/ui/postcss.config.js +6 -0
- package/ui/public/assets/apple-touch-icon.png +0 -0
- package/ui/public/assets/favicon-16.png +0 -0
- package/ui/public/assets/favicon-32.png +0 -0
- package/ui/public/assets/icon-192.png +0 -0
- package/ui/public/assets/icon-512.png +0 -0
- package/ui/src/App.tsx +27 -0
- package/ui/src/api/client.ts +1503 -0
- package/ui/src/components/EndpointUsageGuide.tsx +361 -0
- package/ui/src/components/Layout.tsx +124 -0
- package/ui/src/components/MessageContent.tsx +365 -0
- package/ui/src/components/ToolCallMessage.tsx +179 -0
- package/ui/src/components/ToolPicker.tsx +442 -0
- package/ui/src/components/messageContentParser.test.ts +41 -0
- package/ui/src/components/messageContentParser.ts +73 -0
- package/ui/src/components/thinkingPreview.test.ts +27 -0
- package/ui/src/components/thinkingPreview.ts +15 -0
- package/ui/src/components/toMermaidSankey.test.ts +78 -0
- package/ui/src/components/toMermaidSankey.ts +56 -0
- package/ui/src/components/ui/button.tsx +58 -0
- package/ui/src/components/ui/input.tsx +21 -0
- package/ui/src/components/ui/textarea.tsx +21 -0
- package/ui/src/lib/utils.ts +6 -0
- package/ui/src/main.tsx +9 -0
- package/ui/src/pages/AgentPlayground.tsx +2010 -0
- package/ui/src/pages/Benchmark.tsx +988 -0
- package/ui/src/pages/Dashboard.tsx +581 -0
- package/ui/src/pages/Peek.tsx +962 -0
- package/ui/src/pages/Settings.tsx +2013 -0
- package/ui/src/pages/agentPlaygroundPayload.test.ts +109 -0
- package/ui/src/pages/agentPlaygroundPayload.ts +97 -0
- package/ui/src/pages/agentThinkingContent.test.ts +50 -0
- package/ui/src/pages/agentThinkingContent.ts +57 -0
- package/ui/src/pages/dashboardTokenUsage.test.ts +66 -0
- package/ui/src/pages/dashboardTokenUsage.ts +36 -0
- package/ui/src/pages/imageUpload.test.ts +39 -0
- package/ui/src/pages/imageUpload.ts +71 -0
- package/ui/src/pages/peekFilters.test.ts +29 -0
- package/ui/src/pages/peekFilters.ts +13 -0
- package/ui/src/pages/peekMedia.test.ts +58 -0
- package/ui/src/pages/peekMedia.ts +148 -0
- package/ui/src/pages/sessionAutoTitle.test.ts +128 -0
- package/ui/src/pages/sessionAutoTitle.ts +106 -0
- package/ui/src/stores/settings.ts +58 -0
- package/ui/src/styles/globals.css +223 -0
- package/ui/src/vite-env.d.ts +8 -0
- package/ui/tailwind.config.js +106 -0
- package/ui/tsconfig.json +32 -0
- 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
|
+
}
|