waypoi 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (260) hide show
  1. package/.github/instructions/ui.instructions.md +42 -0
  2. package/.github/workflows/ci.yml +35 -0
  3. package/.github/workflows/publish.yml +71 -0
  4. package/.github/workflows/release.yml +48 -0
  5. package/.playwright-mcp/console-2026-04-04T01-41-10-746Z.log +2 -0
  6. package/.playwright-mcp/console-2026-04-04T01-41-28-799Z.log +3 -0
  7. package/.playwright-mcp/console-2026-04-05T02-26-51-909Z.log +76 -0
  8. package/.playwright-mcp/page-2026-04-04T01-41-10-816Z.yml +1 -0
  9. package/.playwright-mcp/page-2026-04-04T01-41-29-141Z.yml +77 -0
  10. package/.playwright-mcp/page-2026-04-04T01-41-42-633Z.yml +190 -0
  11. package/.playwright-mcp/page-2026-04-04T01-42-03-929Z.yml +262 -0
  12. package/.playwright-mcp/page-2026-04-04T02-12-54-813Z.yml +6 -0
  13. package/.playwright-mcp/page-2026-04-04T02-14-58-600Z.yml +190 -0
  14. package/.playwright-mcp/page-2026-04-04T02-15-03-923Z.yml +190 -0
  15. package/.playwright-mcp/page-2026-04-04T02-15-07-426Z.yml +190 -0
  16. package/.playwright-mcp/page-2026-04-04T02-15-25-729Z.yml +262 -0
  17. package/.playwright-mcp/page-2026-04-04T02-16-22-984Z.yml +262 -0
  18. package/.playwright-mcp/page-2026-04-04T02-17-00-599Z.yml +190 -0
  19. package/.playwright-mcp/page-2026-04-04T02-17-50-874Z.yml +190 -0
  20. package/.playwright-mcp/page-2026-04-05T02-26-55-570Z.yml +6 -0
  21. package/AGENTS.md +48 -0
  22. package/CHANGELOG.md +131 -0
  23. package/README.md +552 -0
  24. package/assets/agent-mode.png +0 -0
  25. package/assets/categorize.png +0 -0
  26. package/assets/dashboard.png +0 -0
  27. package/assets/endpoint-proxy.png +0 -0
  28. package/assets/icon.png +0 -0
  29. package/assets/mcp-generate-image.png +0 -0
  30. package/assets/mcp-understand-image.png +0 -0
  31. package/assets/peek-token-flow.png +0 -0
  32. package/assets/playground.png +0 -0
  33. package/assets/sankey.png +0 -0
  34. package/cli/index.ts +2805 -0
  35. package/cli/legacyRewrite.ts +108 -0
  36. package/cli/modelRef.ts +24 -0
  37. package/dist/cli/index.js +2536 -0
  38. package/dist/cli/legacyRewrite.js +92 -0
  39. package/dist/cli/modelRef.js +20 -0
  40. package/dist/src/benchmark/artifacts.js +131 -0
  41. package/dist/src/benchmark/capabilityClassifier.js +81 -0
  42. package/dist/src/benchmark/capabilityStore.js +144 -0
  43. package/dist/src/benchmark/config.js +238 -0
  44. package/dist/src/benchmark/gates.js +118 -0
  45. package/dist/src/benchmark/jobs.js +252 -0
  46. package/dist/src/benchmark/runner.js +1847 -0
  47. package/dist/src/benchmark/schema.js +353 -0
  48. package/dist/src/benchmark/suites.js +314 -0
  49. package/dist/src/benchmark/tinyQaDataset.js +422 -0
  50. package/dist/src/benchmark/types.js +25 -0
  51. package/dist/src/config.js +47 -0
  52. package/dist/src/index.js +178 -0
  53. package/dist/src/mcp/client.js +215 -0
  54. package/dist/src/mcp/discovery.js +226 -0
  55. package/dist/src/mcp/policy.js +65 -0
  56. package/dist/src/mcp/registry.js +129 -0
  57. package/dist/src/mcp/service.js +460 -0
  58. package/dist/src/middleware/auth.js +179 -0
  59. package/dist/src/middleware/requestCapture.js +192 -0
  60. package/dist/src/middleware/requestStats.js +118 -0
  61. package/dist/src/pools/builder.js +132 -0
  62. package/dist/src/pools/repository.js +69 -0
  63. package/dist/src/pools/scheduler.js +360 -0
  64. package/dist/src/pools/types.js +2 -0
  65. package/dist/src/protocols/adapters/dashscope.js +267 -0
  66. package/dist/src/protocols/adapters/inferenceV2.js +346 -0
  67. package/dist/src/protocols/adapters/openai.js +27 -0
  68. package/dist/src/protocols/registry.js +99 -0
  69. package/dist/src/protocols/types.js +2 -0
  70. package/dist/src/providers/health.js +153 -0
  71. package/dist/src/providers/importer.js +289 -0
  72. package/dist/src/providers/modelRegistry.js +313 -0
  73. package/dist/src/providers/repository.js +361 -0
  74. package/dist/src/providers/types.js +2 -0
  75. package/dist/src/routes/admin.js +531 -0
  76. package/dist/src/routes/audio.js +295 -0
  77. package/dist/src/routes/chat.js +240 -0
  78. package/dist/src/routes/embeddings.js +157 -0
  79. package/dist/src/routes/images.js +288 -0
  80. package/dist/src/routes/mcp.js +256 -0
  81. package/dist/src/routes/mcpService.js +100 -0
  82. package/dist/src/routes/models.js +48 -0
  83. package/dist/src/routes/responses.js +711 -0
  84. package/dist/src/routes/sessions.js +450 -0
  85. package/dist/src/routes/stats.js +270 -0
  86. package/dist/src/routes/ui.js +97 -0
  87. package/dist/src/routes/videos.js +107 -0
  88. package/dist/src/routing/router.js +338 -0
  89. package/dist/src/services/imageGeneration.js +280 -0
  90. package/dist/src/services/imageUnderstanding.js +352 -0
  91. package/dist/src/services/videoGeneration.js +79 -0
  92. package/dist/src/storage/captureRepository.js +1591 -0
  93. package/dist/src/storage/files.js +157 -0
  94. package/dist/src/storage/imageCache.js +346 -0
  95. package/dist/src/storage/repositories.js +388 -0
  96. package/dist/src/storage/sessionRepository.js +370 -0
  97. package/dist/src/storage/statsRepository.js +204 -0
  98. package/dist/src/transport/httpClient.js +126 -0
  99. package/dist/src/types.js +2 -0
  100. package/dist/src/utils/messageMedia.js +285 -0
  101. package/dist/src/utils/modelCapabilities.js +108 -0
  102. package/dist/src/utils/modelDiscovery.js +170 -0
  103. package/dist/src/version.js +5 -0
  104. package/dist/src/workers/captureRetention.js +25 -0
  105. package/dist/src/workers/configWatcher.js +91 -0
  106. package/dist/src/workers/healthChecker.js +21 -0
  107. package/dist/src/workers/statsRotation.js +41 -0
  108. package/docs/LLM/output_schema.md +312 -0
  109. package/docs/benchmark.md +208 -0
  110. package/docs/mcp-guidelines.md +125 -0
  111. package/docs/mcp-service.md +178 -0
  112. package/docs/opencode.md +86 -0
  113. package/docs/providers.md +79 -0
  114. package/examples/benchmark.config.yaml +28 -0
  115. package/examples/providers/alibaba-dashscope.yaml +88 -0
  116. package/examples/providers/alibaba-llm.yaml +64 -0
  117. package/examples/providers/alibaba-registry.yaml +7 -0
  118. package/examples/providers/inference-v2-ray.yaml +29 -0
  119. package/examples/scenarios/assets/omni-call-sample.wav +0 -0
  120. package/examples/scenarios/custom.jsonl +5 -0
  121. package/examples/scenarios/custom.yaml +40 -0
  122. package/model-form-v2.png +0 -0
  123. package/package.json +66 -0
  124. package/provider-form-v2.png +0 -0
  125. package/provider-form.png +0 -0
  126. package/scripts/manual-test.sh +11 -0
  127. package/scripts/version-from-git.js +23 -0
  128. package/src/benchmark/artifacts.ts +149 -0
  129. package/src/benchmark/capabilityClassifier.ts +99 -0
  130. package/src/benchmark/capabilityStore.ts +174 -0
  131. package/src/benchmark/config.ts +337 -0
  132. package/src/benchmark/gates.ts +164 -0
  133. package/src/benchmark/jobs.ts +312 -0
  134. package/src/benchmark/runner.ts +2519 -0
  135. package/src/benchmark/schema.ts +443 -0
  136. package/src/benchmark/suites.ts +323 -0
  137. package/src/benchmark/tinyQaDataset.ts +428 -0
  138. package/src/benchmark/types.ts +442 -0
  139. package/src/config.ts +44 -0
  140. package/src/index.ts +195 -0
  141. package/src/mcp/client.ts +305 -0
  142. package/src/mcp/discovery.ts +266 -0
  143. package/src/mcp/policy.ts +105 -0
  144. package/src/mcp/registry.ts +164 -0
  145. package/src/mcp/service.ts +611 -0
  146. package/src/middleware/auth.ts +251 -0
  147. package/src/middleware/requestCapture.ts +245 -0
  148. package/src/middleware/requestStats.ts +163 -0
  149. package/src/pools/builder.ts +159 -0
  150. package/src/pools/repository.ts +71 -0
  151. package/src/pools/scheduler.ts +425 -0
  152. package/src/pools/types.ts +117 -0
  153. package/src/protocols/adapters/dashscope.ts +335 -0
  154. package/src/protocols/adapters/inferenceV2.ts +428 -0
  155. package/src/protocols/adapters/openai.ts +32 -0
  156. package/src/protocols/registry.ts +117 -0
  157. package/src/protocols/types.ts +81 -0
  158. package/src/providers/health.ts +207 -0
  159. package/src/providers/importer.ts +402 -0
  160. package/src/providers/modelRegistry.ts +415 -0
  161. package/src/providers/repository.ts +439 -0
  162. package/src/providers/types.ts +113 -0
  163. package/src/routes/admin.ts +666 -0
  164. package/src/routes/audio.ts +372 -0
  165. package/src/routes/chat.ts +301 -0
  166. package/src/routes/embeddings.ts +197 -0
  167. package/src/routes/images.ts +356 -0
  168. package/src/routes/mcp.ts +320 -0
  169. package/src/routes/mcpService.ts +114 -0
  170. package/src/routes/models.ts +50 -0
  171. package/src/routes/responses.ts +872 -0
  172. package/src/routes/sessions.ts +558 -0
  173. package/src/routes/stats.ts +312 -0
  174. package/src/routes/ui.ts +96 -0
  175. package/src/routes/videos.ts +132 -0
  176. package/src/routing/router.ts +501 -0
  177. package/src/services/imageGeneration.ts +396 -0
  178. package/src/services/imageUnderstanding.ts +449 -0
  179. package/src/services/videoGeneration.ts +127 -0
  180. package/src/storage/captureRepository.ts +1835 -0
  181. package/src/storage/files.ts +178 -0
  182. package/src/storage/imageCache.ts +405 -0
  183. package/src/storage/repositories.ts +494 -0
  184. package/src/storage/sessionRepository.ts +419 -0
  185. package/src/storage/statsRepository.ts +238 -0
  186. package/src/transport/httpClient.ts +145 -0
  187. package/src/types.ts +322 -0
  188. package/src/utils/messageMedia.ts +293 -0
  189. package/src/utils/modelCapabilities.ts +161 -0
  190. package/src/utils/modelDiscovery.ts +203 -0
  191. package/src/workers/captureRetention.ts +25 -0
  192. package/src/workers/configWatcher.ts +115 -0
  193. package/src/workers/healthChecker.ts +22 -0
  194. package/src/workers/statsRotation.ts +49 -0
  195. package/tests/benchmarkAdminRoutes.test.ts +82 -0
  196. package/tests/benchmarkBasics.test.ts +116 -0
  197. package/tests/captureAdminRoutes.test.ts +420 -0
  198. package/tests/captureRepository.test.ts +797 -0
  199. package/tests/cliLegacyRewrite.test.ts +45 -0
  200. package/tests/imageGeneration.service.test.ts +107 -0
  201. package/tests/imageUnderstanding.service.test.ts +123 -0
  202. package/tests/mcpPolicy.test.ts +105 -0
  203. package/tests/mcpService.test.ts +1245 -0
  204. package/tests/modelRef.test.ts +23 -0
  205. package/tests/modelsRoutes.test.ts +154 -0
  206. package/tests/sessionMediaCache.test.ts +167 -0
  207. package/tests/statsRoutes.test.ts +323 -0
  208. package/tsconfig.json +15 -0
  209. package/ui/index.html +16 -0
  210. package/ui/package-lock.json +8521 -0
  211. package/ui/package.json +52 -0
  212. package/ui/postcss.config.js +6 -0
  213. package/ui/public/assets/apple-touch-icon.png +0 -0
  214. package/ui/public/assets/favicon-16.png +0 -0
  215. package/ui/public/assets/favicon-32.png +0 -0
  216. package/ui/public/assets/icon-192.png +0 -0
  217. package/ui/public/assets/icon-512.png +0 -0
  218. package/ui/src/App.tsx +27 -0
  219. package/ui/src/api/client.ts +1503 -0
  220. package/ui/src/components/EndpointUsageGuide.tsx +361 -0
  221. package/ui/src/components/Layout.tsx +124 -0
  222. package/ui/src/components/MessageContent.tsx +365 -0
  223. package/ui/src/components/ToolCallMessage.tsx +179 -0
  224. package/ui/src/components/ToolPicker.tsx +442 -0
  225. package/ui/src/components/messageContentParser.test.ts +41 -0
  226. package/ui/src/components/messageContentParser.ts +73 -0
  227. package/ui/src/components/thinkingPreview.test.ts +27 -0
  228. package/ui/src/components/thinkingPreview.ts +15 -0
  229. package/ui/src/components/toMermaidSankey.test.ts +78 -0
  230. package/ui/src/components/toMermaidSankey.ts +56 -0
  231. package/ui/src/components/ui/button.tsx +58 -0
  232. package/ui/src/components/ui/input.tsx +21 -0
  233. package/ui/src/components/ui/textarea.tsx +21 -0
  234. package/ui/src/lib/utils.ts +6 -0
  235. package/ui/src/main.tsx +9 -0
  236. package/ui/src/pages/AgentPlayground.tsx +2010 -0
  237. package/ui/src/pages/Benchmark.tsx +988 -0
  238. package/ui/src/pages/Dashboard.tsx +581 -0
  239. package/ui/src/pages/Peek.tsx +962 -0
  240. package/ui/src/pages/Settings.tsx +2013 -0
  241. package/ui/src/pages/agentPlaygroundPayload.test.ts +109 -0
  242. package/ui/src/pages/agentPlaygroundPayload.ts +97 -0
  243. package/ui/src/pages/agentThinkingContent.test.ts +50 -0
  244. package/ui/src/pages/agentThinkingContent.ts +57 -0
  245. package/ui/src/pages/dashboardTokenUsage.test.ts +66 -0
  246. package/ui/src/pages/dashboardTokenUsage.ts +36 -0
  247. package/ui/src/pages/imageUpload.test.ts +39 -0
  248. package/ui/src/pages/imageUpload.ts +71 -0
  249. package/ui/src/pages/peekFilters.test.ts +29 -0
  250. package/ui/src/pages/peekFilters.ts +13 -0
  251. package/ui/src/pages/peekMedia.test.ts +58 -0
  252. package/ui/src/pages/peekMedia.ts +148 -0
  253. package/ui/src/pages/sessionAutoTitle.test.ts +128 -0
  254. package/ui/src/pages/sessionAutoTitle.ts +106 -0
  255. package/ui/src/stores/settings.ts +58 -0
  256. package/ui/src/styles/globals.css +223 -0
  257. package/ui/src/vite-env.d.ts +8 -0
  258. package/ui/tailwind.config.js +106 -0
  259. package/ui/tsconfig.json +32 -0
  260. package/ui/vite.config.ts +37 -0
@@ -0,0 +1,207 @@
1
+ import { Agent, request } from "undici";
2
+ import { StoragePaths, loadProviderHealth, saveProviderHealth } from "../storage/files";
3
+ import { ProviderModelHealth } from "../types";
4
+ import { listProviders } from "./repository";
5
+ import { getEffectiveModelInsecureTls } from "./repository";
6
+
7
+ const DEFAULT_TIMEOUT_MS = 5000;
8
+
9
+ export interface ProviderModelProbeResult {
10
+ providerModelId: string;
11
+ providerId: string;
12
+ modelId: string;
13
+ baseUrl: string;
14
+ status: "up" | "down";
15
+ latencyMs?: number | null;
16
+ statusCode?: number;
17
+ error?: string;
18
+ }
19
+
20
+ export function defaultProviderModelHealth(): ProviderModelHealth {
21
+ return {
22
+ status: "up",
23
+ consecutiveFailures: 0,
24
+ };
25
+ }
26
+
27
+ export async function getProviderModelHealthMap(
28
+ paths: StoragePaths
29
+ ): Promise<Record<string, ProviderModelHealth>> {
30
+ const health = await loadProviderHealth(paths);
31
+ return health.models;
32
+ }
33
+
34
+ export async function updateProviderModelHealthCheck(
35
+ paths: StoragePaths,
36
+ providerModelId: string,
37
+ status: "up" | "down",
38
+ latencyMs: number | null,
39
+ lastStatusCode?: number,
40
+ lastError?: string
41
+ ): Promise<void> {
42
+ const health = await loadProviderHealth(paths);
43
+ const now = new Date();
44
+ const current = health.models[providerModelId] ?? defaultProviderModelHealth();
45
+
46
+ let consecutiveFailures = current.consecutiveFailures;
47
+ let nextStatus = current.status;
48
+
49
+ if (status === "up") {
50
+ consecutiveFailures = 0;
51
+ nextStatus = "up";
52
+ } else {
53
+ consecutiveFailures = current.consecutiveFailures + 1;
54
+ if (consecutiveFailures >= 3) {
55
+ nextStatus = "down";
56
+ }
57
+ }
58
+
59
+ const next: ProviderModelHealth = {
60
+ ...current,
61
+ status: nextStatus,
62
+ consecutiveFailures,
63
+ lastCheckedAt: now,
64
+ lastStatusCode,
65
+ lastError,
66
+ };
67
+
68
+ if (status === "up" && latencyMs !== null) {
69
+ next.latencyMsEwma = ewma(current.latencyMsEwma, latencyMs);
70
+ next.lastSuccessAt = now;
71
+ next.lastFailureAt = undefined;
72
+ next.lastError = undefined;
73
+ } else if (status === "down") {
74
+ next.lastFailureAt = now;
75
+ }
76
+
77
+ health.models[providerModelId] = next;
78
+ await saveProviderHealth(paths, health);
79
+ }
80
+
81
+ export async function probeProviderModels(
82
+ paths: StoragePaths,
83
+ options?: { timeoutMs?: number }
84
+ ): Promise<ProviderModelProbeResult[]> {
85
+ const providers = await listProviders(paths);
86
+ const targets = collectTargets(providers);
87
+ const results: ProviderModelProbeResult[] = [];
88
+ const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
89
+
90
+ await Promise.all(
91
+ Array.from(targets.values()).map(async (target) => {
92
+ const start = Date.now();
93
+ try {
94
+ const dispatcher = target.insecureTls
95
+ ? new Agent({ connect: { rejectUnauthorized: false } })
96
+ : undefined;
97
+ const headers: Record<string, string> = {};
98
+ if (target.apiKey) {
99
+ headers.authorization = `Bearer ${target.apiKey}`;
100
+ }
101
+ const response = await request(new URL("/v1/models", target.baseUrl).toString(), {
102
+ method: "GET",
103
+ headers,
104
+ headersTimeout: timeoutMs,
105
+ bodyTimeout: timeoutMs,
106
+ dispatcher,
107
+ });
108
+ const latency = Date.now() - start;
109
+ response.body.resume();
110
+ const ok = response.statusCode >= 200 && response.statusCode < 300;
111
+ for (const model of target.models) {
112
+ await updateProviderModelHealthCheck(
113
+ paths,
114
+ model.providerModelId,
115
+ ok ? "up" : "down",
116
+ ok ? latency : null,
117
+ response.statusCode,
118
+ ok ? undefined : `status ${response.statusCode}`
119
+ );
120
+ results.push({
121
+ providerModelId: model.providerModelId,
122
+ providerId: model.providerId,
123
+ modelId: model.modelId,
124
+ baseUrl: target.baseUrl,
125
+ status: ok ? "up" : "down",
126
+ latencyMs: ok ? latency : null,
127
+ statusCode: response.statusCode,
128
+ error: ok ? undefined : `status ${response.statusCode}`,
129
+ });
130
+ }
131
+ } catch (error) {
132
+ const errorMsg = (error as Error).message || "unknown error";
133
+ for (const model of target.models) {
134
+ await updateProviderModelHealthCheck(
135
+ paths,
136
+ model.providerModelId,
137
+ "down",
138
+ null,
139
+ undefined,
140
+ errorMsg
141
+ );
142
+ results.push({
143
+ providerModelId: model.providerModelId,
144
+ providerId: model.providerId,
145
+ modelId: model.modelId,
146
+ baseUrl: target.baseUrl,
147
+ status: "down",
148
+ latencyMs: null,
149
+ error: errorMsg,
150
+ });
151
+ }
152
+ }
153
+ })
154
+ );
155
+
156
+ return results;
157
+ }
158
+
159
+ type HealthProbeTarget = {
160
+ baseUrl: string;
161
+ apiKey?: string;
162
+ insecureTls?: boolean;
163
+ models: Array<{ providerModelId: string; providerId: string; modelId: string }>;
164
+ };
165
+
166
+ function collectTargets(
167
+ providers: Awaited<ReturnType<typeof listProviders>>
168
+ ): Map<string, HealthProbeTarget> {
169
+ const targets = new Map<string, HealthProbeTarget>();
170
+ for (const provider of providers) {
171
+ if (!provider.enabled) {
172
+ continue;
173
+ }
174
+ for (const model of provider.models) {
175
+ if (model.enabled === false) {
176
+ continue;
177
+ }
178
+ const baseUrl = model.baseUrl ?? provider.baseUrl;
179
+ if (!baseUrl) {
180
+ continue;
181
+ }
182
+ const apiKey = model.apiKey ?? provider.apiKey;
183
+ const insecureTls = getEffectiveModelInsecureTls(provider, model);
184
+ const key = `${baseUrl}@@${apiKey ? "key" : "nokey"}@@${insecureTls ? "insecure" : "secure"}`;
185
+ const entry = targets.get(key) ?? {
186
+ baseUrl,
187
+ apiKey,
188
+ insecureTls,
189
+ models: [],
190
+ };
191
+ entry.models.push({
192
+ providerModelId: model.providerModelId,
193
+ providerId: provider.id,
194
+ modelId: model.modelId,
195
+ });
196
+ targets.set(key, entry);
197
+ }
198
+ }
199
+ return targets;
200
+ }
201
+
202
+ function ewma(previous: number | undefined, next: number, alpha = 0.2): number {
203
+ if (previous === undefined) {
204
+ return next;
205
+ }
206
+ return alpha * next + (1 - alpha) * previous;
207
+ }
@@ -0,0 +1,402 @@
1
+ import { promises as fs } from "fs";
2
+ import path from "path";
3
+ import YAML from "yaml";
4
+ import { ModelCapabilities, ModelModality } from "../types";
5
+ import { StoragePaths } from "../storage/files";
6
+ import { canonicalizeProtocol, hasProtocolAdapter } from "../protocols/registry";
7
+ import { loadProviderStore, normalizeDomainSuffixes, saveProviderStore } from "./repository";
8
+ import {
9
+ EnvMap,
10
+ ProviderAuthConfig,
11
+ ProviderImportOptions,
12
+ ProviderImportResult,
13
+ ProviderLimits,
14
+ ProviderProtocol,
15
+ ProviderProtocolConfig,
16
+ ProviderRecord,
17
+ ProviderRegistryEntry,
18
+ } from "./types";
19
+
20
+ interface RegistryFile {
21
+ providers?: ProviderRegistryEntry[];
22
+ }
23
+
24
+ interface ProviderConfigFile {
25
+ $id?: string;
26
+ name?: string;
27
+ description?: string;
28
+ docs?: string;
29
+ endpoint?: {
30
+ baseUrl?: string;
31
+ protocol?: string;
32
+ insecureTls?: boolean;
33
+ router?: string;
34
+ responseTextPaths?: string[];
35
+ };
36
+ auth?: {
37
+ type?: string;
38
+ keyParam?: string;
39
+ headerName?: string;
40
+ keyPrefix?: string;
41
+ };
42
+ env?: string;
43
+ autoInsecureTlsDomains?: string[];
44
+ responseTextPaths?: string[];
45
+ limits?: ProviderLimits;
46
+ models?: Array<{
47
+ id?: string;
48
+ upstream?: string;
49
+ baseUrl?: string;
50
+ apiKey?: string;
51
+ insecureTls?: boolean;
52
+ enabled?: boolean;
53
+ free?: boolean;
54
+ modalities?: string[];
55
+ capabilities?: {
56
+ tools?: boolean;
57
+ streaming?: boolean;
58
+ vision?: boolean;
59
+ };
60
+ benchmark?: {
61
+ livebench?: number;
62
+ };
63
+ limits?: ProviderLimits;
64
+ }>;
65
+ }
66
+
67
+ export async function importProviders(
68
+ paths: StoragePaths,
69
+ options: ProviderImportOptions
70
+ ): Promise<ProviderImportResult> {
71
+ const registryPath = path.resolve(options.registryPath);
72
+ const registryDir = path.dirname(registryPath);
73
+ const registryRaw = await fs.readFile(registryPath, "utf8");
74
+ const registry = (YAML.parse(registryRaw) ?? {}) as RegistryFile;
75
+ const envMap = await loadEnvMap(options.envFilePath);
76
+
77
+ const warnings: string[] = [];
78
+ const providers: ProviderRecord[] = [];
79
+ const existing = await loadProviderStore(paths);
80
+ const existingById = new Map(existing.providers.map((provider) => [provider.id, provider]));
81
+
82
+ for (const entry of registry.providers ?? []) {
83
+ if (!entry?.id || !entry.file) {
84
+ warnings.push(`Skipping malformed registry entry: ${JSON.stringify(entry)}`);
85
+ continue;
86
+ }
87
+
88
+ const providerConfigPath = path.resolve(registryDir, entry.file);
89
+ let configRaw: string;
90
+ try {
91
+ configRaw = await fs.readFile(providerConfigPath, "utf8");
92
+ } catch (error) {
93
+ warnings.push(`Failed to load provider config ${providerConfigPath}: ${(error as Error).message}`);
94
+ continue;
95
+ }
96
+
97
+ const config = (YAML.parse(configRaw) ?? {}) as ProviderConfigFile;
98
+
99
+ const protocolRaw = (config.endpoint?.protocol ?? entry.protocol ?? "unknown").toLowerCase();
100
+ const normalizedProtocol = canonicalizeProtocol(protocolRaw);
101
+ const protocol: ProviderProtocol =
102
+ normalizedProtocol === "openai" || normalizedProtocol === "inference_v2" || normalizedProtocol === "dashscope"
103
+ ? normalizedProtocol
104
+ : "unknown";
105
+ const auth = parseAuthConfig(config.auth);
106
+ const protocolConfig = parseProtocolConfig(config, protocol);
107
+ let supportsRouting = hasProtocolAdapter(protocol);
108
+
109
+ const providerId = config.$id ?? entry.id;
110
+ const previous = existingById.get(providerId);
111
+ const envVar = typeof config.env === "string" ? config.env : undefined;
112
+ const apiKey = resolveApiKey(
113
+ envVar,
114
+ envMap,
115
+ previous?.apiKey,
116
+ options.overwriteAuth ?? false
117
+ );
118
+ const baseUrl = config.endpoint?.baseUrl;
119
+
120
+ if (!baseUrl) {
121
+ warnings.push(`Provider '${providerId}' has no endpoint.baseUrl; skipped.`);
122
+ continue;
123
+ }
124
+
125
+ if (protocol === "inference_v2" && !protocolConfig?.router) {
126
+ warnings.push(`Provider '${providerId}' protocol '${protocolRaw}' requires endpoint.router; imported non-routable.`);
127
+ supportsRouting = false;
128
+ }
129
+
130
+ if (!supportsRouting) {
131
+ warnings.push(
132
+ `Provider '${providerId}' protocol '${protocolRaw}' imported as non-routable in v1.`
133
+ );
134
+ }
135
+
136
+ const models = (config.models ?? [])
137
+ .map((model) => {
138
+ if (!model.id) {
139
+ warnings.push(`Provider '${providerId}' has model without id; skipped.`);
140
+ return null;
141
+ }
142
+ const providerModelId = `${providerId}/${model.id}`;
143
+ const capabilities = toCapabilities(model.modalities ?? [], model.capabilities ?? {});
144
+ return {
145
+ providerModelId,
146
+ providerId,
147
+ modelId: model.id,
148
+ upstreamModel: model.upstream ?? model.id,
149
+ baseUrl: typeof model.baseUrl === "string" ? model.baseUrl : undefined,
150
+ apiKey: typeof model.apiKey === "string" ? model.apiKey : undefined,
151
+ insecureTls: model.insecureTls === true,
152
+ enabled: model.enabled !== false,
153
+ aliases: [],
154
+ free: model.free !== false,
155
+ modalities: model.modalities ?? [],
156
+ capabilities,
157
+ endpointType: inferEndpointType(capabilities),
158
+ benchmark: normalizeBenchmark(model.benchmark),
159
+ limits: model.limits,
160
+ };
161
+ })
162
+ .filter((model): model is NonNullable<typeof model> => model !== null);
163
+
164
+ providers.push({
165
+ id: providerId,
166
+ name: config.name ?? entry.name ?? providerId,
167
+ description: config.description,
168
+ docs: config.docs,
169
+ protocol,
170
+ protocolRaw,
171
+ protocolConfig,
172
+ baseUrl,
173
+ insecureTls: previous?.insecureTls ?? (config.endpoint?.insecureTls === true),
174
+ autoInsecureTlsDomains:
175
+ previous?.autoInsecureTlsDomains ?? normalizeDomainSuffixes(config.autoInsecureTlsDomains),
176
+ enabled: previous?.enabled ?? true,
177
+ supportsRouting,
178
+ auth,
179
+ envVar,
180
+ apiKey,
181
+ limits: config.limits,
182
+ models,
183
+ warnings: supportsRouting ? undefined : [`Unsupported protocol: ${protocolRaw}`],
184
+ importedAt: new Date().toISOString(),
185
+ });
186
+ }
187
+
188
+ await saveProviderStore(paths, providers);
189
+
190
+ return {
191
+ importedProviders: providers.length,
192
+ importedModels: providers.reduce((sum, provider) => sum + provider.models.length, 0),
193
+ warnings,
194
+ providers,
195
+ };
196
+ }
197
+
198
+ function parseAuthConfig(
199
+ auth?: ProviderConfigFile["auth"]
200
+ ): ProviderAuthConfig | undefined {
201
+ if (!auth || typeof auth !== "object") {
202
+ return undefined;
203
+ }
204
+ const type = typeof auth.type === "string" ? auth.type.trim().toLowerCase() : "";
205
+ if (!type || !["bearer", "query", "header", "none"].includes(type)) {
206
+ return undefined;
207
+ }
208
+ return {
209
+ type: type as ProviderAuthConfig["type"],
210
+ keyParam: typeof auth.keyParam === "string" ? auth.keyParam : undefined,
211
+ headerName: typeof auth.headerName === "string" ? auth.headerName : undefined,
212
+ keyPrefix: typeof auth.keyPrefix === "string" ? auth.keyPrefix : undefined,
213
+ };
214
+ }
215
+
216
+ function parseProtocolConfig(
217
+ config: ProviderConfigFile,
218
+ protocol: ProviderProtocol
219
+ ): ProviderProtocolConfig | undefined {
220
+ if (protocol !== "inference_v2") {
221
+ return undefined;
222
+ }
223
+ const router =
224
+ typeof config.endpoint?.router === "string"
225
+ ? config.endpoint.router.trim()
226
+ : undefined;
227
+ const responseTextPaths = extractStringArray(
228
+ config.endpoint?.responseTextPaths ?? config.responseTextPaths
229
+ );
230
+ return {
231
+ router: router && router.length > 0 ? router : undefined,
232
+ responseTextPaths: responseTextPaths.length > 0 ? responseTextPaths : undefined,
233
+ };
234
+ }
235
+
236
+ function extractStringArray(value: unknown): string[] {
237
+ if (!Array.isArray(value)) {
238
+ return [];
239
+ }
240
+ return value
241
+ .map((entry) => (typeof entry === "string" ? entry.trim() : ""))
242
+ .filter((entry) => entry.length > 0);
243
+ }
244
+
245
+ function resolveApiKey(
246
+ envVar: string | undefined,
247
+ envMap: EnvMap,
248
+ previous: string | undefined,
249
+ overwriteAuth: boolean
250
+ ): string | undefined {
251
+ if (!envVar) {
252
+ return previous;
253
+ }
254
+ const fromEnvFile = envMap[envVar];
255
+ const fromProcess = process.env[envVar];
256
+ const value = fromEnvFile ?? fromProcess;
257
+ if (!value && !previous) {
258
+ return undefined;
259
+ }
260
+ if (!overwriteAuth && previous) {
261
+ return previous;
262
+ }
263
+ return value ?? previous;
264
+ }
265
+
266
+ async function loadEnvMap(envFilePath?: string): Promise<EnvMap> {
267
+ const loaded: EnvMap = {};
268
+ const candidates = envFilePath ? [envFilePath] : [];
269
+
270
+ for (const candidate of candidates) {
271
+ const fullPath = path.resolve(candidate);
272
+ try {
273
+ const raw = await fs.readFile(fullPath, "utf8");
274
+ parseEnvInto(raw, loaded);
275
+ break;
276
+ } catch (error) {
277
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
278
+ throw error;
279
+ }
280
+ }
281
+ }
282
+
283
+ return loaded;
284
+ }
285
+
286
+ function parseEnvInto(raw: string, out: EnvMap): void {
287
+ const lines = raw.split("\n");
288
+ for (const line of lines) {
289
+ const trimmed = line.trim();
290
+ if (!trimmed || trimmed.startsWith("#")) {
291
+ continue;
292
+ }
293
+ const equals = trimmed.indexOf("=");
294
+ if (equals <= 0) {
295
+ continue;
296
+ }
297
+ const key = trimmed.slice(0, equals).trim();
298
+ let value = trimmed.slice(equals + 1).trim();
299
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
300
+ value = value.slice(1, -1);
301
+ }
302
+ if (key) {
303
+ out[key] = value;
304
+ }
305
+ }
306
+ }
307
+
308
+ function normalizeBenchmark(
309
+ benchmark: { livebench?: number } | undefined
310
+ ): { livebench?: number } | undefined {
311
+ if (!benchmark || typeof benchmark.livebench !== "number" || !Number.isFinite(benchmark.livebench)) {
312
+ return undefined;
313
+ }
314
+ return { livebench: benchmark.livebench };
315
+ }
316
+
317
+ function toCapabilities(
318
+ modalities: string[],
319
+ caps: { tools?: boolean; streaming?: boolean; vision?: boolean }
320
+ ): ModelCapabilities {
321
+ const input = new Set<ModelModality>();
322
+ const output = new Set<ModelModality>();
323
+
324
+ for (const modality of modalities) {
325
+ switch (modality) {
326
+ case "text-to-text":
327
+ input.add("text");
328
+ output.add("text");
329
+ break;
330
+ case "image-to-text":
331
+ input.add("image");
332
+ input.add("text");
333
+ output.add("text");
334
+ break;
335
+ case "text-to-image":
336
+ input.add("text");
337
+ output.add("image");
338
+ break;
339
+ case "audio-to-text":
340
+ input.add("audio");
341
+ output.add("text");
342
+ break;
343
+ case "text-to-audio":
344
+ input.add("text");
345
+ output.add("audio");
346
+ break;
347
+ case "text-to-embedding":
348
+ input.add("text");
349
+ output.add("embedding");
350
+ break;
351
+ case "text-to-video":
352
+ input.add("text");
353
+ output.add("video");
354
+ break;
355
+ case "image-to-video":
356
+ input.add("text");
357
+ input.add("image");
358
+ output.add("video");
359
+ break;
360
+ case "text-image-to-video":
361
+ input.add("text");
362
+ input.add("image");
363
+ output.add("video");
364
+ break;
365
+ }
366
+ }
367
+
368
+ if (input.size === 0 || output.size === 0) {
369
+ input.add("text");
370
+ output.add(caps.vision ? "text" : "text");
371
+ }
372
+
373
+ if (caps.vision && !input.has("image")) {
374
+ input.add("image");
375
+ }
376
+
377
+ return {
378
+ input: Array.from(input),
379
+ output: Array.from(output),
380
+ supportsTools: caps.tools,
381
+ supportsStreaming: caps.streaming,
382
+ source: "configured",
383
+ };
384
+ }
385
+
386
+ function inferEndpointType(
387
+ capabilities: ModelCapabilities
388
+ ): "llm" | "diffusion" | "audio" | "embedding" | "video" {
389
+ if (capabilities.output.includes("embedding")) {
390
+ return "embedding";
391
+ }
392
+ if (capabilities.output.includes("video")) {
393
+ return "video";
394
+ }
395
+ if (capabilities.output.includes("image")) {
396
+ return "diffusion";
397
+ }
398
+ if (capabilities.output.includes("audio")) {
399
+ return "audio";
400
+ }
401
+ return "llm";
402
+ }