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,494 @@
|
|
|
1
|
+
import {
|
|
2
|
+
appendRequestLog,
|
|
3
|
+
defaultHealth,
|
|
4
|
+
loadConfig,
|
|
5
|
+
loadHealth,
|
|
6
|
+
newEndpointId,
|
|
7
|
+
readRequestLogs,
|
|
8
|
+
saveConfig,
|
|
9
|
+
saveHealth,
|
|
10
|
+
StoragePaths,
|
|
11
|
+
ConfigFile
|
|
12
|
+
} from "./files";
|
|
13
|
+
import {
|
|
14
|
+
EndpointDoc,
|
|
15
|
+
EndpointHealth,
|
|
16
|
+
EndpointType,
|
|
17
|
+
ModelCapabilities,
|
|
18
|
+
ModelMapping,
|
|
19
|
+
ModelModality,
|
|
20
|
+
RequestLog,
|
|
21
|
+
} from "../types";
|
|
22
|
+
import {
|
|
23
|
+
CapabilitiesRequirements,
|
|
24
|
+
resolveCapabilities,
|
|
25
|
+
supportsRequirements,
|
|
26
|
+
} from "../utils/modelCapabilities";
|
|
27
|
+
import { pickBestProviderModelByCapabilities } from "../providers/modelRegistry";
|
|
28
|
+
|
|
29
|
+
// ========================================
|
|
30
|
+
// Config Cache for Hot-Reload Support
|
|
31
|
+
// ========================================
|
|
32
|
+
|
|
33
|
+
interface ConfigCache {
|
|
34
|
+
config: ConfigFile | null;
|
|
35
|
+
health: { endpoints: Record<string, EndpointHealth> } | null;
|
|
36
|
+
lastLoadedAt: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const cache: ConfigCache = {
|
|
40
|
+
config: null,
|
|
41
|
+
health: null,
|
|
42
|
+
lastLoadedAt: 0
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const CACHE_TTL_MS = 1000; // 1 second TTL for cache
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Invalidate the config cache. Call this when config changes externally.
|
|
49
|
+
*/
|
|
50
|
+
export function invalidateConfigCache(): void {
|
|
51
|
+
cache.config = null;
|
|
52
|
+
cache.health = null;
|
|
53
|
+
cache.lastLoadedAt = 0;
|
|
54
|
+
console.log("[repositories] Config cache invalidated");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Check if cache is valid
|
|
59
|
+
*/
|
|
60
|
+
function isCacheValid(): boolean {
|
|
61
|
+
return cache.config !== null && (Date.now() - cache.lastLoadedAt) < CACHE_TTL_MS;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function getCachedConfig(paths: StoragePaths): Promise<ConfigFile> {
|
|
65
|
+
if (!isCacheValid()) {
|
|
66
|
+
cache.config = await loadConfig(paths);
|
|
67
|
+
cache.lastLoadedAt = Date.now();
|
|
68
|
+
}
|
|
69
|
+
return cache.config!;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function getCachedHealth(paths: StoragePaths): Promise<{ endpoints: Record<string, EndpointHealth> }> {
|
|
73
|
+
// Health is always fresh-loaded since it changes frequently
|
|
74
|
+
return loadHealth(paths);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function listEndpoints(paths: StoragePaths): Promise<EndpointDoc[]> {
|
|
78
|
+
const config = await getCachedConfig(paths);
|
|
79
|
+
const normalized = normalizeConfig(config);
|
|
80
|
+
if (normalized.changed) {
|
|
81
|
+
await saveConfig(paths, normalized.config);
|
|
82
|
+
cache.config = normalized.config;
|
|
83
|
+
}
|
|
84
|
+
const health = await getCachedHealth(paths);
|
|
85
|
+
return normalized.config.endpoints.map((endpoint) => ({
|
|
86
|
+
...endpoint,
|
|
87
|
+
health: health.endpoints[endpoint.id] ?? defaultHealth()
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function createEndpoint(
|
|
92
|
+
paths: StoragePaths,
|
|
93
|
+
input: Omit<EndpointDoc, "id" | "health" | "createdAt" | "updatedAt">
|
|
94
|
+
): Promise<EndpointDoc> {
|
|
95
|
+
const normalized = normalizeConfig(await loadConfig(paths));
|
|
96
|
+
if (normalized.changed) {
|
|
97
|
+
await saveConfig(paths, normalized.config);
|
|
98
|
+
}
|
|
99
|
+
const config = normalized.config;
|
|
100
|
+
const now = new Date();
|
|
101
|
+
const endpoint: EndpointDoc = {
|
|
102
|
+
...input,
|
|
103
|
+
id: newEndpointId(),
|
|
104
|
+
health: defaultHealth(),
|
|
105
|
+
disabled: input.disabled ?? false,
|
|
106
|
+
createdAt: now,
|
|
107
|
+
updatedAt: now
|
|
108
|
+
};
|
|
109
|
+
config.endpoints.push(stripHealth(endpoint));
|
|
110
|
+
await saveConfig(paths, config);
|
|
111
|
+
|
|
112
|
+
const health = await loadHealth(paths);
|
|
113
|
+
health.endpoints[endpoint.id] = endpoint.health;
|
|
114
|
+
await saveHealth(paths, health);
|
|
115
|
+
|
|
116
|
+
return endpoint;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function updateEndpoint(
|
|
120
|
+
paths: StoragePaths,
|
|
121
|
+
id: string,
|
|
122
|
+
patch: Partial<EndpointDoc>
|
|
123
|
+
): Promise<EndpointDoc | null> {
|
|
124
|
+
const config = normalizeConfig(await loadConfig(paths)).config;
|
|
125
|
+
const idx = config.endpoints.findIndex((endpoint) => endpoint.id === id);
|
|
126
|
+
if (idx === -1) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
const existing = config.endpoints[idx];
|
|
130
|
+
const updated = {
|
|
131
|
+
...existing,
|
|
132
|
+
...stripHealth(patch as EndpointDoc),
|
|
133
|
+
updatedAt: new Date()
|
|
134
|
+
};
|
|
135
|
+
config.endpoints[idx] = updated;
|
|
136
|
+
await saveConfig(paths, config);
|
|
137
|
+
|
|
138
|
+
const health = await loadHealth(paths);
|
|
139
|
+
const healthState = health.endpoints[id] ?? defaultHealth();
|
|
140
|
+
return { ...updated, health: healthState };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export async function setEndpointDisabled(
|
|
144
|
+
paths: StoragePaths,
|
|
145
|
+
id: string,
|
|
146
|
+
disabled: boolean
|
|
147
|
+
): Promise<EndpointDoc | null> {
|
|
148
|
+
const endpoint = await updateEndpoint(paths, id, { disabled });
|
|
149
|
+
return endpoint;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function deleteEndpointByIdOrName(
|
|
153
|
+
paths: StoragePaths,
|
|
154
|
+
value: string
|
|
155
|
+
): Promise<EndpointDoc | null> {
|
|
156
|
+
const config = normalizeConfig(await loadConfig(paths)).config;
|
|
157
|
+
const target = config.endpoints.find((endpoint) => endpoint.id === value || endpoint.name === value);
|
|
158
|
+
if (!target) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
config.endpoints = config.endpoints.filter((endpoint) => endpoint.id !== target.id);
|
|
162
|
+
await saveConfig(paths, config);
|
|
163
|
+
|
|
164
|
+
const health = await loadHealth(paths);
|
|
165
|
+
const healthState = health.endpoints[target.id] ?? defaultHealth();
|
|
166
|
+
delete health.endpoints[target.id];
|
|
167
|
+
await saveHealth(paths, health);
|
|
168
|
+
|
|
169
|
+
return { ...target, health: healthState };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export async function getEndpointByIdOrName(paths: StoragePaths, value: string): Promise<EndpointDoc | null> {
|
|
173
|
+
const config = normalizeConfig(await loadConfig(paths)).config;
|
|
174
|
+
const endpoint = config.endpoints.find((item) => item.id === value || item.name === value);
|
|
175
|
+
if (!endpoint) {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
const health = await loadHealth(paths);
|
|
179
|
+
return { ...endpoint, health: health.endpoints[endpoint.id] ?? defaultHealth() };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export async function getEligibleEndpointsForModel(
|
|
183
|
+
paths: StoragePaths,
|
|
184
|
+
publicModel: string,
|
|
185
|
+
requirements: {
|
|
186
|
+
endpointType?: EndpointType;
|
|
187
|
+
requiredInput?: ModelModality[];
|
|
188
|
+
requiredOutput?: ModelModality[];
|
|
189
|
+
} = {}
|
|
190
|
+
): Promise<EndpointDoc[]> {
|
|
191
|
+
const endpoints = await listEndpoints(paths);
|
|
192
|
+
const now = new Date();
|
|
193
|
+
return endpoints.filter((endpoint) => {
|
|
194
|
+
if (endpoint.disabled) {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
// Filter by endpoint type if specified
|
|
198
|
+
if (requirements.endpointType && endpoint.type !== requirements.endpointType) {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
const model = endpoint.models.find((mapping) => mapping.publicName === publicModel);
|
|
202
|
+
if (!model) {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
const capabilities = resolveCapabilities(model, endpoint.type);
|
|
206
|
+
if (!supportsRequirements(capabilities, requirements)) {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
if (endpoint.health.status === "down") {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
if (endpoint.health.downUntil && endpoint.health.downUntil > now) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
return true;
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export async function listEligibleEndpoints(paths: StoragePaths): Promise<EndpointDoc[]> {
|
|
220
|
+
const endpoints = await listEndpoints(paths);
|
|
221
|
+
const now = new Date();
|
|
222
|
+
return endpoints.filter((endpoint) => {
|
|
223
|
+
if (endpoint.disabled) {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
if (endpoint.health.status === "down") {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
if (endpoint.health.downUntil && endpoint.health.downUntil > now) {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
return true;
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function sortEndpointsForRouting(endpoints: EndpointDoc[]): EndpointDoc[] {
|
|
237
|
+
return endpoints.sort((a, b) => {
|
|
238
|
+
if (a.priority !== b.priority) {
|
|
239
|
+
return a.priority - b.priority;
|
|
240
|
+
}
|
|
241
|
+
const aLatency = a.health.latencyMsEwma ?? Number.POSITIVE_INFINITY;
|
|
242
|
+
const bLatency = b.health.latencyMsEwma ?? Number.POSITIVE_INFINITY;
|
|
243
|
+
return aLatency - bLatency;
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export async function updateHealthSuccess(
|
|
248
|
+
paths: StoragePaths,
|
|
249
|
+
endpointId: string,
|
|
250
|
+
latencyMs: number
|
|
251
|
+
): Promise<void> {
|
|
252
|
+
const health = await loadHealth(paths);
|
|
253
|
+
const now = new Date();
|
|
254
|
+
const current = health.endpoints[endpointId] ?? defaultHealth();
|
|
255
|
+
const nextLatency = ewma(current.latencyMsEwma, latencyMs);
|
|
256
|
+
health.endpoints[endpointId] = {
|
|
257
|
+
...current,
|
|
258
|
+
status: "up",
|
|
259
|
+
lastSuccessAt: now,
|
|
260
|
+
consecutiveFailures: 0,
|
|
261
|
+
latencyMsEwma: nextLatency,
|
|
262
|
+
downUntil: undefined
|
|
263
|
+
};
|
|
264
|
+
await saveHealth(paths, health);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export async function updateHealthFailure(
|
|
268
|
+
paths: StoragePaths,
|
|
269
|
+
endpointId: string
|
|
270
|
+
): Promise<{ consecutiveFailures: number } | null> {
|
|
271
|
+
const health = await loadHealth(paths);
|
|
272
|
+
const now = new Date();
|
|
273
|
+
const current = health.endpoints[endpointId] ?? defaultHealth();
|
|
274
|
+
const next = {
|
|
275
|
+
...current,
|
|
276
|
+
lastFailureAt: now,
|
|
277
|
+
consecutiveFailures: current.consecutiveFailures + 1
|
|
278
|
+
};
|
|
279
|
+
health.endpoints[endpointId] = next;
|
|
280
|
+
await saveHealth(paths, health);
|
|
281
|
+
return { consecutiveFailures: next.consecutiveFailures };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export async function markEndpointDown(
|
|
285
|
+
paths: StoragePaths,
|
|
286
|
+
endpointId: string,
|
|
287
|
+
downUntil: Date
|
|
288
|
+
): Promise<void> {
|
|
289
|
+
const health = await loadHealth(paths);
|
|
290
|
+
const current = health.endpoints[endpointId] ?? defaultHealth();
|
|
291
|
+
health.endpoints[endpointId] = {
|
|
292
|
+
...current,
|
|
293
|
+
status: "down",
|
|
294
|
+
downUntil
|
|
295
|
+
};
|
|
296
|
+
await saveHealth(paths, health);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export async function updateHealthCheck(
|
|
300
|
+
paths: StoragePaths,
|
|
301
|
+
endpointId: string,
|
|
302
|
+
status: "up" | "down",
|
|
303
|
+
latencyMs: number | null
|
|
304
|
+
): Promise<void> {
|
|
305
|
+
const health = await loadHealth(paths);
|
|
306
|
+
const now = new Date();
|
|
307
|
+
const current = health.endpoints[endpointId] ?? defaultHealth();
|
|
308
|
+
const next: EndpointHealth = {
|
|
309
|
+
...current,
|
|
310
|
+
status,
|
|
311
|
+
lastCheckedAt: now
|
|
312
|
+
};
|
|
313
|
+
if (latencyMs !== null) {
|
|
314
|
+
next.latencyMsEwma = ewma(current.latencyMsEwma, latencyMs);
|
|
315
|
+
next.lastSuccessAt = now;
|
|
316
|
+
} else {
|
|
317
|
+
next.lastFailureAt = now;
|
|
318
|
+
}
|
|
319
|
+
health.endpoints[endpointId] = next;
|
|
320
|
+
await saveHealth(paths, health);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export async function listPublicModels(paths: StoragePaths): Promise<string[]> {
|
|
324
|
+
const config = normalizeConfig(await loadConfig(paths)).config;
|
|
325
|
+
const names = new Set<string>();
|
|
326
|
+
for (const endpoint of config.endpoints) {
|
|
327
|
+
for (const model of endpoint.models) {
|
|
328
|
+
names.add(model.publicName);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return Array.from(names).sort();
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export interface ModelWithType {
|
|
335
|
+
id: string;
|
|
336
|
+
type: 'llm' | 'diffusion' | 'audio' | 'embedding' | 'video';
|
|
337
|
+
endpointName: string;
|
|
338
|
+
capabilities: ModelCapabilities;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export async function listModelsWithTypes(paths: StoragePaths): Promise<ModelWithType[]> {
|
|
342
|
+
// Get endpoints with health status to filter out unhealthy ones
|
|
343
|
+
const endpoints = await listEligibleEndpoints(paths);
|
|
344
|
+
const models = new Map<string, ModelWithType>();
|
|
345
|
+
for (const endpoint of endpoints) {
|
|
346
|
+
for (const model of endpoint.models) {
|
|
347
|
+
// First endpoint wins (in case model is on multiple endpoints)
|
|
348
|
+
if (!models.has(model.publicName)) {
|
|
349
|
+
models.set(model.publicName, {
|
|
350
|
+
id: model.publicName,
|
|
351
|
+
type: endpoint.type,
|
|
352
|
+
endpointName: endpoint.name,
|
|
353
|
+
capabilities: resolveCapabilities(model, endpoint.type),
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return Array.from(models.values()).sort((a, b) => a.id.localeCompare(b.id));
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Pick the best available LLM model based on endpoint priority and health.
|
|
363
|
+
* Returns the publicName of the first LLM model from the highest-priority healthy endpoint.
|
|
364
|
+
*/
|
|
365
|
+
export async function pickBestLlmModel(paths: StoragePaths): Promise<string | null> {
|
|
366
|
+
return pickBestProviderModelByCapabilities(
|
|
367
|
+
paths,
|
|
368
|
+
{ requiredInput: ["text"], requiredOutput: ["text"] },
|
|
369
|
+
"llm"
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export async function pickBestModelByCapabilities(
|
|
374
|
+
paths: StoragePaths,
|
|
375
|
+
requirements: CapabilitiesRequirements,
|
|
376
|
+
preferredEndpointType?: EndpointType
|
|
377
|
+
): Promise<string | null> {
|
|
378
|
+
return pickBestProviderModelByCapabilities(paths, requirements, preferredEndpointType);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export function getModelCapabilitiesForEndpoint(
|
|
382
|
+
endpointType: EndpointType,
|
|
383
|
+
mapping: ModelMapping
|
|
384
|
+
): ModelCapabilities {
|
|
385
|
+
return resolveCapabilities(mapping, endpointType);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export async function logRequest(paths: StoragePaths, log: RequestLog): Promise<void> {
|
|
389
|
+
await appendRequestLog(paths, log);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export async function getStats(
|
|
393
|
+
paths: StoragePaths,
|
|
394
|
+
windowMs: number
|
|
395
|
+
): Promise<{ total: number; success: number; errors: number; avgLatencyMs: number | null }> {
|
|
396
|
+
const since = Date.now() - windowMs;
|
|
397
|
+
const logs = await readRequestLogs(paths);
|
|
398
|
+
const filtered = logs.filter((log) => new Date(log.ts).getTime() >= since);
|
|
399
|
+
if (filtered.length === 0) {
|
|
400
|
+
return { total: 0, success: 0, errors: 0, avgLatencyMs: null };
|
|
401
|
+
}
|
|
402
|
+
let sumLatency = 0;
|
|
403
|
+
let latencyCount = 0;
|
|
404
|
+
let errors = 0;
|
|
405
|
+
for (const log of filtered) {
|
|
406
|
+
if (log.result.errorType) {
|
|
407
|
+
errors += 1;
|
|
408
|
+
}
|
|
409
|
+
if (typeof log.result.latencyMs === "number") {
|
|
410
|
+
sumLatency += log.result.latencyMs;
|
|
411
|
+
latencyCount += 1;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
const avgLatencyMs = latencyCount > 0 ? sumLatency / latencyCount : null;
|
|
415
|
+
return {
|
|
416
|
+
total: filtered.length,
|
|
417
|
+
success: filtered.length - errors,
|
|
418
|
+
errors,
|
|
419
|
+
avgLatencyMs
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export async function getUsageByEndpoint(paths: StoragePaths): Promise<
|
|
424
|
+
Array<{ endpointId: string; endpointName: string; totalTokens: number; count: number }>
|
|
425
|
+
> {
|
|
426
|
+
const logs = await readRequestLogs(paths);
|
|
427
|
+
const totals = new Map<string, { endpointName: string; totalTokens: number; count: number }>();
|
|
428
|
+
for (const log of logs) {
|
|
429
|
+
const endpointId = log.route.endpointId;
|
|
430
|
+
if (!endpointId) {
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
const entry = totals.get(endpointId) ?? { endpointName: log.route.endpointName ?? "unknown", totalTokens: 0, count: 0 };
|
|
434
|
+
entry.totalTokens += log.result.totalTokens ?? 0;
|
|
435
|
+
entry.count += 1;
|
|
436
|
+
totals.set(endpointId, entry);
|
|
437
|
+
}
|
|
438
|
+
return Array.from(totals.entries()).map(([endpointId, entry]) => ({
|
|
439
|
+
endpointId,
|
|
440
|
+
endpointName: entry.endpointName,
|
|
441
|
+
totalTokens: entry.totalTokens,
|
|
442
|
+
count: entry.count
|
|
443
|
+
}));
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function ewma(prev: number | undefined, next: number, alpha = 0.2): number {
|
|
447
|
+
if (prev === undefined) {
|
|
448
|
+
return next;
|
|
449
|
+
}
|
|
450
|
+
return alpha * next + (1 - alpha) * prev;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function stripHealth(endpoint: EndpointDoc): Omit<EndpointDoc, "health"> {
|
|
454
|
+
const { health: _health, ...rest } = endpoint;
|
|
455
|
+
return rest;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function normalizeConfig(config: ConfigFile): {
|
|
459
|
+
config: ConfigFile;
|
|
460
|
+
changed: boolean;
|
|
461
|
+
} {
|
|
462
|
+
let changed = false;
|
|
463
|
+
const endpoints = config.endpoints.map((endpoint) => {
|
|
464
|
+
let next = endpoint;
|
|
465
|
+
if (!endpoint.id) {
|
|
466
|
+
next = { ...next, id: newEndpointId() };
|
|
467
|
+
changed = true;
|
|
468
|
+
}
|
|
469
|
+
if (!next.type) {
|
|
470
|
+
next = { ...next, type: "llm" };
|
|
471
|
+
changed = true;
|
|
472
|
+
}
|
|
473
|
+
if (typeof next.disabled !== "boolean") {
|
|
474
|
+
next = { ...next, disabled: false };
|
|
475
|
+
changed = true;
|
|
476
|
+
}
|
|
477
|
+
if (!next.createdAt) {
|
|
478
|
+
next = { ...next, createdAt: new Date() };
|
|
479
|
+
changed = true;
|
|
480
|
+
} else if (!(next.createdAt instanceof Date)) {
|
|
481
|
+
next = { ...next, createdAt: new Date(next.createdAt) };
|
|
482
|
+
changed = true;
|
|
483
|
+
}
|
|
484
|
+
if (!next.updatedAt) {
|
|
485
|
+
next = { ...next, updatedAt: new Date() };
|
|
486
|
+
changed = true;
|
|
487
|
+
} else if (!(next.updatedAt instanceof Date)) {
|
|
488
|
+
next = { ...next, updatedAt: new Date(next.updatedAt) };
|
|
489
|
+
changed = true;
|
|
490
|
+
}
|
|
491
|
+
return next;
|
|
492
|
+
});
|
|
493
|
+
return { config: { ...config, endpoints }, changed };
|
|
494
|
+
}
|