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,428 @@
1
+ import { promises as fs } from "fs";
2
+ import { Readable } from "stream";
3
+ import { getMediaPath } from "../../storage/imageCache";
4
+ import {
5
+ PreparedUpstreamRequest,
6
+ ProtocolAdapter,
7
+ ProtocolBuildRequestContext,
8
+ ProtocolNormalizeResponseContext,
9
+ ProtocolSupportContext,
10
+ } from "../types";
11
+ import { UpstreamError, UpstreamResult } from "../../types";
12
+
13
+ const DEFAULT_RESPONSE_TEXT_PATHS = [
14
+ "outputs.0.outputs.text",
15
+ "outputs.0.text",
16
+ "outputs.0.generated_text",
17
+ ];
18
+
19
+ export const inferenceV2ProtocolAdapter: ProtocolAdapter = {
20
+ id: "inference_v2",
21
+ supportedOperations: ["chat_completions"],
22
+ streamSupportedOperations: [],
23
+ supports(context: ProtocolSupportContext) {
24
+ if (context.operation !== "chat_completions") {
25
+ return { supported: false, reason: "unsupported_operation" };
26
+ }
27
+ if (context.stream) {
28
+ return { supported: false, reason: "stream_unsupported" };
29
+ }
30
+ return { supported: true };
31
+ },
32
+ async buildRequest(context: ProtocolBuildRequestContext): Promise<PreparedUpstreamRequest> {
33
+ const router = typeof context.config?.router === "string" ? context.config.router.trim() : "";
34
+ if (!router) {
35
+ throw protocolError(
36
+ "invalid_protocol_config",
37
+ "inference_v2 protocol requires provider protocolConfig.router.",
38
+ false
39
+ );
40
+ }
41
+
42
+ const { text, image } = await extractPromptAndImage(
43
+ context.paths,
44
+ context.payload
45
+ );
46
+
47
+ const inferInputs: Record<string, unknown> = {
48
+ text,
49
+ max_new_tokens: numberOrUndefined(context.payload.max_tokens),
50
+ temperature: numberOrUndefined(context.payload.temperature),
51
+ top_p: numberOrUndefined(context.payload.top_p),
52
+ };
53
+ if (image) {
54
+ inferInputs.image = image;
55
+ }
56
+
57
+ const payload: Record<string, unknown> = {
58
+ inputs: [
59
+ {
60
+ model_name: context.upstreamModel,
61
+ inputs: compactObject(inferInputs),
62
+ },
63
+ ],
64
+ };
65
+
66
+ const requestPath = `/v2/models/${encodeURIComponent(router)}/infer`;
67
+ const auth = buildAuth(context, requestPath);
68
+ return {
69
+ path: auth.path,
70
+ payload,
71
+ headers: auth.headers,
72
+ skipDefaultAuth: auth.skipDefaultAuth,
73
+ };
74
+ },
75
+ async normalizeResponse(
76
+ context: ProtocolNormalizeResponseContext
77
+ ): Promise<UpstreamResult> {
78
+ const responseJson = await readJsonBody(context.upstreamResult);
79
+ const textPaths = normalizeTextPaths(context.config?.responseTextPaths);
80
+ const content = firstStringByPaths(responseJson, textPaths);
81
+ if (!content) {
82
+ throw protocolError(
83
+ "invalid_upstream_response",
84
+ "Inference v2 response does not contain assistant text at configured paths.",
85
+ true
86
+ );
87
+ }
88
+
89
+ const promptTokens = numberFromPath(responseJson, "usage.prompt_tokens");
90
+ const completionTokens = numberFromPath(responseJson, "usage.completion_tokens");
91
+ const totalTokensCandidate = numberFromPath(responseJson, "usage.total_tokens");
92
+ const totalTokens =
93
+ totalTokensCandidate ??
94
+ (promptTokens !== undefined && completionTokens !== undefined
95
+ ? promptTokens + completionTokens
96
+ : undefined);
97
+
98
+ const normalized = {
99
+ id: `chatcmpl-infer-${Date.now().toString(36)}`,
100
+ object: "chat.completion",
101
+ created: Math.floor(Date.now() / 1000),
102
+ model: context.publicModel,
103
+ choices: [
104
+ {
105
+ index: 0,
106
+ message: {
107
+ role: "assistant",
108
+ content,
109
+ },
110
+ finish_reason: "stop",
111
+ },
112
+ ],
113
+ usage:
114
+ promptTokens !== undefined || completionTokens !== undefined || totalTokens !== undefined
115
+ ? {
116
+ prompt_tokens: promptTokens ?? 0,
117
+ completion_tokens: completionTokens ?? 0,
118
+ total_tokens: totalTokens ?? 0,
119
+ }
120
+ : undefined,
121
+ waypoi_adapter: {
122
+ protocol: "inference_v2",
123
+ },
124
+ };
125
+
126
+ const buffer = Buffer.from(JSON.stringify(normalized), "utf8");
127
+ const headers = {
128
+ ...context.upstreamResult.headers,
129
+ "content-type": "application/json",
130
+ };
131
+ return {
132
+ statusCode: context.upstreamResult.statusCode,
133
+ headers,
134
+ body: Readable.from([buffer]),
135
+ rawBody: buffer,
136
+ };
137
+ },
138
+ };
139
+
140
+ async function extractPromptAndImage(
141
+ paths: ProtocolBuildRequestContext["paths"],
142
+ payload: Record<string, unknown>
143
+ ): Promise<{ text: string; image?: string }> {
144
+ const messages = Array.isArray(payload.messages)
145
+ ? payload.messages
146
+ : [];
147
+ const message =
148
+ [...messages]
149
+ .reverse()
150
+ .find(
151
+ (item) =>
152
+ item &&
153
+ typeof item === "object" &&
154
+ (item as { role?: unknown }).role === "user"
155
+ ) ?? messages[messages.length - 1];
156
+
157
+ if (!message || typeof message !== "object") {
158
+ return { text: "" };
159
+ }
160
+
161
+ const content = (message as { content?: unknown }).content;
162
+ if (typeof content === "string") {
163
+ return { text: content };
164
+ }
165
+ if (!Array.isArray(content)) {
166
+ return { text: "" };
167
+ }
168
+
169
+ const textParts: string[] = [];
170
+ let image: string | undefined;
171
+
172
+ for (const part of content) {
173
+ if (!part || typeof part !== "object") {
174
+ continue;
175
+ }
176
+ const typed = part as Record<string, unknown>;
177
+ const type = typeof typed.type === "string" ? typed.type : "";
178
+ if (type === "text" || type === "input_text" || type === "output_text") {
179
+ const text = typed.text;
180
+ if (typeof text === "string" && text.length > 0) {
181
+ textParts.push(text);
182
+ }
183
+ continue;
184
+ }
185
+ if (!image && type === "image_url") {
186
+ const imageUrl = typed.image_url as { url?: unknown } | undefined;
187
+ if (typeof imageUrl?.url === "string") {
188
+ image = await resolveImageToBase64(paths, imageUrl.url);
189
+ }
190
+ continue;
191
+ }
192
+ if (!image && type === "image") {
193
+ const value = typed.image;
194
+ if (typeof value === "string") {
195
+ image = await resolveImageToBase64(paths, value);
196
+ }
197
+ continue;
198
+ }
199
+ if (!image && type === "input_image") {
200
+ const imageValue = typed.image ?? (typed.image_url as { url?: unknown } | undefined)?.url;
201
+ if (typeof imageValue === "string") {
202
+ image = await resolveImageToBase64(paths, imageValue);
203
+ }
204
+ }
205
+ }
206
+
207
+ return {
208
+ text: textParts.join("\n").trim(),
209
+ image,
210
+ };
211
+ }
212
+
213
+ async function resolveImageToBase64(
214
+ paths: ProtocolBuildRequestContext["paths"],
215
+ value: string
216
+ ): Promise<string> {
217
+ if (value.startsWith("data:")) {
218
+ const match = value.match(/^data:[^;]+;base64,(.+)$/i);
219
+ if (!match) {
220
+ throw protocolError("invalid_request", "Invalid data URL for image input.", false);
221
+ }
222
+ return match[1].replace(/\s+/g, "");
223
+ }
224
+ if (looksLikeBase64(value)) {
225
+ return value.replace(/\s+/g, "");
226
+ }
227
+
228
+ const hash = extractLocalMediaHash(value);
229
+ if (!hash) {
230
+ throw protocolError(
231
+ "invalid_request",
232
+ "Only local /admin/media or /admin/images URLs are allowed for image input.",
233
+ false
234
+ );
235
+ }
236
+ const mediaPath = await getMediaPath(paths, hash);
237
+ if (!mediaPath) {
238
+ throw protocolError("invalid_request", "Referenced image not found in cache.", false);
239
+ }
240
+ const buffer = await fs.readFile(mediaPath);
241
+ return buffer.toString("base64");
242
+ }
243
+
244
+ function normalizeTextPaths(raw: unknown): string[] {
245
+ if (Array.isArray(raw)) {
246
+ const values = raw.filter((value): value is string => typeof value === "string" && value.length > 0);
247
+ if (values.length > 0) {
248
+ return values;
249
+ }
250
+ }
251
+ return DEFAULT_RESPONSE_TEXT_PATHS;
252
+ }
253
+
254
+ function firstStringByPaths(source: unknown, paths: string[]): string | undefined {
255
+ for (const path of paths) {
256
+ const value = getByPath(source, path);
257
+ if (typeof value === "string" && value.trim().length > 0) {
258
+ return value.trim();
259
+ }
260
+ }
261
+ return undefined;
262
+ }
263
+
264
+ function numberFromPath(source: unknown, path: string): number | undefined {
265
+ const value = getByPath(source, path);
266
+ if (typeof value === "number" && Number.isFinite(value)) {
267
+ return value;
268
+ }
269
+ return undefined;
270
+ }
271
+
272
+ function getByPath(source: unknown, path: string): unknown {
273
+ const segments = path
274
+ .replace(/\[(\d+)\]/g, ".$1")
275
+ .split(".")
276
+ .map((segment) => segment.trim())
277
+ .filter((segment) => segment.length > 0);
278
+ let cursor: unknown = source;
279
+ for (const segment of segments) {
280
+ if (cursor === null || cursor === undefined) {
281
+ return undefined;
282
+ }
283
+ if (Array.isArray(cursor)) {
284
+ const index = Number(segment);
285
+ if (!Number.isInteger(index) || index < 0 || index >= cursor.length) {
286
+ return undefined;
287
+ }
288
+ cursor = cursor[index];
289
+ continue;
290
+ }
291
+ if (typeof cursor !== "object") {
292
+ return undefined;
293
+ }
294
+ cursor = (cursor as Record<string, unknown>)[segment];
295
+ }
296
+ return cursor;
297
+ }
298
+
299
+ function compactObject(input: Record<string, unknown>): Record<string, unknown> {
300
+ const output: Record<string, unknown> = {};
301
+ for (const [key, value] of Object.entries(input)) {
302
+ if (value !== undefined) {
303
+ output[key] = value;
304
+ }
305
+ }
306
+ return output;
307
+ }
308
+
309
+ function numberOrUndefined(value: unknown): number | undefined {
310
+ if (typeof value === "number" && Number.isFinite(value)) {
311
+ return value;
312
+ }
313
+ return undefined;
314
+ }
315
+
316
+ function looksLikeBase64(value: string): boolean {
317
+ if (value.length < 32) {
318
+ return false;
319
+ }
320
+ return /^[A-Za-z0-9+/=\s]+$/.test(value);
321
+ }
322
+
323
+ function extractLocalMediaHash(url: string): string | null {
324
+ const normalized = normalizeLocalUrl(url);
325
+ if (!normalized) {
326
+ return null;
327
+ }
328
+ const match = normalized.match(/^\/admin\/(media|images)\/([a-f0-9]{16})$/i);
329
+ return match ? match[2] : null;
330
+ }
331
+
332
+ function normalizeLocalUrl(url: string): string | null {
333
+ if (url.startsWith("/")) {
334
+ return url;
335
+ }
336
+ try {
337
+ const parsed = new URL(url);
338
+ if (!["http:", "https:"].includes(parsed.protocol)) {
339
+ return null;
340
+ }
341
+ if (!["localhost", "127.0.0.1", "::1"].includes(parsed.hostname)) {
342
+ return null;
343
+ }
344
+ return parsed.pathname;
345
+ } catch {
346
+ return null;
347
+ }
348
+ }
349
+
350
+ function buildAuth(
351
+ context: ProtocolBuildRequestContext,
352
+ defaultPath: string
353
+ ): {
354
+ path: string;
355
+ headers?: Record<string, string>;
356
+ skipDefaultAuth?: boolean;
357
+ } {
358
+ const authType = context.auth?.type ?? "bearer";
359
+ if (authType === "none") {
360
+ return { path: defaultPath, skipDefaultAuth: true };
361
+ }
362
+
363
+ if (authType === "query") {
364
+ const keyParam = context.auth?.keyParam ?? "api_key";
365
+ const apiKey = context.endpoint.apiKey;
366
+ if (!apiKey) {
367
+ return { path: defaultPath, skipDefaultAuth: true };
368
+ }
369
+ const parsed = new URL(defaultPath, "http://placeholder.local");
370
+ parsed.searchParams.set(keyParam, apiKey);
371
+ return { path: `${parsed.pathname}${parsed.search}`, skipDefaultAuth: true };
372
+ }
373
+
374
+ if (authType === "header") {
375
+ const headerName =
376
+ context.auth?.headerName ?? context.auth?.keyParam ?? "x-api-key";
377
+ const apiKey = context.endpoint.apiKey;
378
+ if (!apiKey) {
379
+ return { path: defaultPath, skipDefaultAuth: true };
380
+ }
381
+ const prefix = context.auth?.keyPrefix ? `${context.auth.keyPrefix} ` : "";
382
+ return {
383
+ path: defaultPath,
384
+ headers: {
385
+ [headerName]: `${prefix}${apiKey}`,
386
+ },
387
+ skipDefaultAuth: true,
388
+ };
389
+ }
390
+
391
+ return { path: defaultPath, skipDefaultAuth: false };
392
+ }
393
+
394
+ async function readJsonBody(result: UpstreamResult): Promise<unknown> {
395
+ if (result.rawBody) {
396
+ return safeJsonParse(result.rawBody.toString("utf8"));
397
+ }
398
+ const chunks: Buffer[] = [];
399
+ for await (const chunk of result.body) {
400
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
401
+ }
402
+ const buffer = Buffer.concat(chunks);
403
+ return safeJsonParse(buffer.toString("utf8"));
404
+ }
405
+
406
+ function safeJsonParse(text: string): unknown {
407
+ try {
408
+ return JSON.parse(text);
409
+ } catch (error) {
410
+ throw protocolError(
411
+ "invalid_upstream_response",
412
+ `Inference v2 response is not valid JSON: ${(error as Error).message}`,
413
+ true
414
+ );
415
+ }
416
+ }
417
+
418
+ function protocolError(
419
+ type: string,
420
+ message: string,
421
+ retryable: boolean
422
+ ): UpstreamError {
423
+ const error = new Error(message) as UpstreamError;
424
+ error.type = type;
425
+ error.retryable = retryable;
426
+ return error;
427
+ }
428
+
@@ -0,0 +1,32 @@
1
+ import {
2
+ PreparedUpstreamRequest,
3
+ ProtocolAdapter,
4
+ ProtocolBuildRequestContext,
5
+ ProtocolSupportContext,
6
+ } from "../types";
7
+
8
+ const ALL_OPERATIONS = [
9
+ "chat_completions",
10
+ "embeddings",
11
+ "images_generation",
12
+ "images_edits",
13
+ "images_variations",
14
+ "audio_transcriptions",
15
+ "audio_translations",
16
+ "audio_speech",
17
+ ] as const;
18
+
19
+ export const openAiProtocolAdapter: ProtocolAdapter = {
20
+ id: "openai",
21
+ supportedOperations: [...ALL_OPERATIONS],
22
+ streamSupportedOperations: [...ALL_OPERATIONS],
23
+ supports(_context: ProtocolSupportContext) {
24
+ return { supported: true };
25
+ },
26
+ async buildRequest(context: ProtocolBuildRequestContext): Promise<PreparedUpstreamRequest> {
27
+ return {
28
+ path: context.path,
29
+ payload: context.payload,
30
+ };
31
+ },
32
+ };
@@ -0,0 +1,117 @@
1
+ import { inferenceV2ProtocolAdapter } from "./adapters/inferenceV2";
2
+ import { openAiProtocolAdapter } from "./adapters/openai";
3
+ import { dashscopeProtocolAdapter } from "./adapters/dashscope";
4
+ import { ProtocolAdapter, ProtocolOperation } from "./types";
5
+
6
+ const PROTOCOL_ALIASES: Record<string, string> = {
7
+ openai: "openai",
8
+ inference_v2: "inference_v2",
9
+ "kserve-v2": "inference_v2",
10
+ kserve_v2: "inference_v2",
11
+ ray_infer_v2: "inference_v2",
12
+ "ray-infer-v2": "inference_v2",
13
+ v2_infer: "inference_v2",
14
+ "v2-infer": "inference_v2",
15
+ dashscope: "dashscope",
16
+ };
17
+
18
+ const ADAPTERS = new Map<string, ProtocolAdapter>([
19
+ [openAiProtocolAdapter.id, openAiProtocolAdapter],
20
+ [inferenceV2ProtocolAdapter.id, inferenceV2ProtocolAdapter],
21
+ [dashscopeProtocolAdapter.id, dashscopeProtocolAdapter],
22
+ ]);
23
+
24
+ const PROTOCOL_METADATA: Record<string, { label: string; description: string }> = {
25
+ openai: {
26
+ label: "OpenAI Compatible",
27
+ description: "Standard OpenAI API format. Supports chat, embeddings, images, and audio.",
28
+ },
29
+ inference_v2: {
30
+ label: "Inference V2 (KServe/Ray)",
31
+ description: "KServe v2 / Ray Serve inference format. Chat only, no streaming.",
32
+ },
33
+ dashscope: {
34
+ label: "DashScope (Alibaba ModelStudio)",
35
+ description: "Alibaba Cloud ModelStudio native API. Supports image generation, video generation, and async task-based operations.",
36
+ },
37
+ };
38
+
39
+ export interface ProtocolInfo {
40
+ id: string;
41
+ label: string;
42
+ description: string;
43
+ operations: ProtocolOperation[];
44
+ streamOperations: ProtocolOperation[];
45
+ supportsRouting: boolean;
46
+ }
47
+
48
+ export function canonicalizeProtocol(raw: string | undefined): string {
49
+ const normalized = (raw ?? "unknown").trim().toLowerCase();
50
+ return (PROTOCOL_ALIASES[normalized] ?? normalized) || "unknown";
51
+ }
52
+
53
+ export function getProtocolAdapter(
54
+ protocol: string | undefined
55
+ ): ProtocolAdapter | null {
56
+ const canonical = canonicalizeProtocol(protocol);
57
+ return ADAPTERS.get(canonical) ?? null;
58
+ }
59
+
60
+ export function hasProtocolAdapter(protocol: string | undefined): boolean {
61
+ return getProtocolAdapter(protocol) !== null;
62
+ }
63
+
64
+ export function listAdapterOperations(
65
+ protocol: string | undefined
66
+ ): {
67
+ operations: ProtocolOperation[];
68
+ streamOperations: ProtocolOperation[];
69
+ } | null {
70
+ const adapter = getProtocolAdapter(protocol);
71
+ if (!adapter) {
72
+ return null;
73
+ }
74
+ return {
75
+ operations: [...adapter.supportedOperations],
76
+ streamOperations: [...adapter.streamSupportedOperations],
77
+ };
78
+ }
79
+
80
+ export function listAllProtocolAdapters(): ProtocolInfo[] {
81
+ return Array.from(ADAPTERS.entries()).map(([id, adapter]) => {
82
+ const meta = PROTOCOL_METADATA[id] ?? { label: id, description: "" };
83
+ return {
84
+ id,
85
+ label: meta.label,
86
+ description: meta.description,
87
+ operations: [...adapter.supportedOperations],
88
+ streamOperations: [...adapter.streamSupportedOperations],
89
+ supportsRouting: true,
90
+ };
91
+ });
92
+ }
93
+
94
+ export function routePathToOperation(path: string): ProtocolOperation | null {
95
+ switch (path) {
96
+ case "/v1/chat/completions":
97
+ return "chat_completions";
98
+ case "/v1/embeddings":
99
+ return "embeddings";
100
+ case "/v1/images/generations":
101
+ return "images_generation";
102
+ case "/v1/images/edits":
103
+ return "images_edits";
104
+ case "/v1/images/variations":
105
+ return "images_variations";
106
+ case "/v1/audio/transcriptions":
107
+ return "audio_transcriptions";
108
+ case "/v1/audio/translations":
109
+ return "audio_translations";
110
+ case "/v1/audio/speech":
111
+ return "audio_speech";
112
+ case "/v1/videos/generations":
113
+ return "video_generations";
114
+ default:
115
+ return null;
116
+ }
117
+ }
@@ -0,0 +1,81 @@
1
+ import { StoragePaths } from "../storage/files";
2
+ import { EndpointDoc, ModelCapabilities, ModelModality, UpstreamResult } from "../types";
3
+
4
+ export type ProtocolOperation =
5
+ | "chat_completions"
6
+ | "embeddings"
7
+ | "images_generation"
8
+ | "images_edits"
9
+ | "images_variations"
10
+ | "audio_transcriptions"
11
+ | "audio_translations"
12
+ | "audio_speech"
13
+ | "video_generations";
14
+
15
+ export interface ProtocolAuthConfig {
16
+ type: "bearer" | "query" | "header" | "none";
17
+ keyParam?: string;
18
+ headerName?: string;
19
+ keyPrefix?: string;
20
+ }
21
+
22
+ export interface ProtocolAdapterConfig {
23
+ router?: string;
24
+ responseTextPaths?: string[];
25
+ [key: string]: unknown;
26
+ }
27
+
28
+ export interface ProtocolSupportContext {
29
+ operation: ProtocolOperation;
30
+ stream: boolean;
31
+ capabilities?: ModelCapabilities;
32
+ requiredInput?: ModelModality[];
33
+ requiredOutput?: ModelModality[];
34
+ }
35
+
36
+ export interface ProtocolSupportResult {
37
+ supported: boolean;
38
+ reason?: "unsupported_operation" | "stream_unsupported";
39
+ }
40
+
41
+ export interface PreparedUpstreamRequest {
42
+ path: string;
43
+ payload: Record<string, unknown>;
44
+ headers?: Record<string, string>;
45
+ skipDefaultAuth?: boolean;
46
+ }
47
+
48
+ export interface ProtocolBuildRequestContext {
49
+ paths: StoragePaths;
50
+ operation: ProtocolOperation;
51
+ stream: boolean;
52
+ path: string;
53
+ payload: Record<string, unknown>;
54
+ publicModel: string;
55
+ upstreamModel: string;
56
+ endpoint: EndpointDoc;
57
+ auth?: ProtocolAuthConfig;
58
+ config?: ProtocolAdapterConfig;
59
+ }
60
+
61
+ export interface ProtocolNormalizeResponseContext {
62
+ operation: ProtocolOperation;
63
+ stream: boolean;
64
+ path: string;
65
+ publicModel: string;
66
+ upstreamModel: string;
67
+ requestPayload: Record<string, unknown>;
68
+ upstreamResult: UpstreamResult;
69
+ config?: ProtocolAdapterConfig;
70
+ }
71
+
72
+ export interface ProtocolAdapter {
73
+ id: string;
74
+ supportedOperations: ProtocolOperation[];
75
+ streamSupportedOperations: ProtocolOperation[];
76
+ supports(context: ProtocolSupportContext): ProtocolSupportResult;
77
+ buildRequest(context: ProtocolBuildRequestContext): Promise<PreparedUpstreamRequest>;
78
+ normalizeResponse?(
79
+ context: ProtocolNormalizeResponseContext
80
+ ): Promise<UpstreamResult>;
81
+ }