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,109 @@
1
+ import test from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { buildUserPayload, findNonDataImageUrls, toApiMessage } from './agentPlaygroundPayload'
4
+
5
+ const DATA_URL_1 = 'data:image/png;base64,AAA'
6
+ const DATA_URL_2 = 'data:image/jpeg;base64,BBB'
7
+
8
+ test('uses inline data URLs for request even when display refs are cached URLs', () => {
9
+ const payload = buildUserPayload({
10
+ callModeEnabled: false,
11
+ text: 'what does the picture say',
12
+ requestImageUrls: [DATA_URL_1],
13
+ displayImageRefs: ['/admin/media/abc123'],
14
+ })
15
+
16
+ const apiMessage = toApiMessage({
17
+ role: 'user',
18
+ content: payload.content,
19
+ images: payload.images,
20
+ requestImages: payload.requestImages,
21
+ })
22
+
23
+ assert.equal(Array.isArray(apiMessage.content), true)
24
+ const imagePart = (apiMessage.content as Array<{ type: string; image_url?: { url: string } }>)[1]
25
+ assert.equal(imagePart.type, 'image_url')
26
+ assert.equal(imagePart.image_url?.url, DATA_URL_1)
27
+ })
28
+
29
+ test('falls back to inline data URLs for display and request when cache misses', () => {
30
+ const payload = buildUserPayload({
31
+ callModeEnabled: false,
32
+ text: 'describe',
33
+ requestImageUrls: [DATA_URL_1],
34
+ })
35
+
36
+ assert.deepEqual(payload.images, [DATA_URL_1])
37
+ assert.deepEqual(payload.requestImages, [DATA_URL_1])
38
+ })
39
+
40
+ test('preserves image ordering across multiple images', () => {
41
+ const payload = buildUserPayload({
42
+ callModeEnabled: false,
43
+ text: '',
44
+ requestImageUrls: [DATA_URL_1, DATA_URL_2],
45
+ displayImageRefs: ['/admin/media/one', '/admin/media/two'],
46
+ })
47
+ const apiMessage = toApiMessage({
48
+ role: 'user',
49
+ content: payload.content,
50
+ images: payload.images,
51
+ requestImages: payload.requestImages,
52
+ })
53
+ const content = apiMessage.content as Array<{ type: string; image_url?: { url: string } }>
54
+ assert.equal(content[0].image_url?.url, DATA_URL_1)
55
+ assert.equal(content[1].image_url?.url, DATA_URL_2)
56
+ })
57
+
58
+ test('call mode keeps audio and uses inline image URL', () => {
59
+ const payload = buildUserPayload({
60
+ callModeEnabled: true,
61
+ text: 'read this',
62
+ requestImageUrls: [DATA_URL_1],
63
+ displayImageRefs: ['/admin/media/cached'],
64
+ audioRef: '/admin/media/audio',
65
+ })
66
+
67
+ const apiMessage = toApiMessage({
68
+ role: 'user',
69
+ content: payload.content,
70
+ images: payload.images,
71
+ requestImages: payload.requestImages,
72
+ })
73
+
74
+ const content = apiMessage.content as Array<{ type: string; image_url?: { url: string }; input_audio?: { url?: string } }>
75
+ assert.equal(content[0].type, 'input_audio')
76
+ assert.equal(content[0].input_audio?.url, '/admin/media/audio')
77
+ assert.equal(content[1].type, 'image_url')
78
+ assert.equal(content[1].image_url?.url, DATA_URL_1)
79
+ assert.equal(content[2].type, 'text')
80
+ })
81
+
82
+ test('text-only payload does not inject image parts', () => {
83
+ const payload = buildUserPayload({
84
+ callModeEnabled: false,
85
+ text: 'hello world',
86
+ requestImageUrls: [],
87
+ })
88
+ const apiMessage = toApiMessage({
89
+ role: 'user',
90
+ content: payload.content,
91
+ images: payload.images,
92
+ requestImages: payload.requestImages,
93
+ })
94
+
95
+ assert.equal(typeof apiMessage.content, 'string')
96
+ assert.equal(apiMessage.content, 'hello world')
97
+ })
98
+
99
+ test('debug detector flags non-data image URLs', () => {
100
+ const apiMessage = toApiMessage({
101
+ role: 'user',
102
+ content: 'hello',
103
+ images: ['/admin/media/abc'],
104
+ requestImages: undefined,
105
+ })
106
+
107
+ const invalid = findNonDataImageUrls([apiMessage])
108
+ assert.deepEqual(invalid, ['/admin/media/abc'])
109
+ })
@@ -0,0 +1,97 @@
1
+ import type { ChatMessage as ApiChatMessage, ContentPart } from '../api/client'
2
+
3
+ export interface PayloadMessage {
4
+ role: ApiChatMessage['role']
5
+ content: string | ContentPart[] | null
6
+ images?: string[]
7
+ requestImages?: string[]
8
+ }
9
+
10
+ export interface BuildUserPayloadInput {
11
+ callModeEnabled: boolean
12
+ text: string
13
+ requestImageUrls: string[]
14
+ displayImageRefs?: string[]
15
+ audioRef?: string
16
+ }
17
+
18
+ export interface BuildUserPayloadResult {
19
+ content: string | ContentPart[]
20
+ images?: string[]
21
+ requestImages?: string[]
22
+ }
23
+
24
+ export function buildUserPayload(input: BuildUserPayloadInput): BuildUserPayloadResult {
25
+ const requestImages = input.requestImageUrls.filter(Boolean)
26
+ const displayImages = input.displayImageRefs?.length ? input.displayImageRefs : requestImages
27
+ const trimmedText = input.text.trim()
28
+
29
+ const content: string | ContentPart[] = input.callModeEnabled
30
+ ? [
31
+ ...(input.audioRef ? [{ type: 'input_audio' as const, input_audio: { url: input.audioRef } }] : []),
32
+ ...requestImages.map((img) => ({ type: 'image_url' as const, image_url: { url: img } })),
33
+ ...(trimmedText ? [{ type: 'text' as const, text: trimmedText }] : []),
34
+ ]
35
+ : trimmedText
36
+
37
+ return {
38
+ content,
39
+ images: displayImages.length > 0 ? displayImages : undefined,
40
+ requestImages: requestImages.length > 0 ? requestImages : undefined,
41
+ }
42
+ }
43
+
44
+ export function toApiMessage(message: PayloadMessage): ApiChatMessage {
45
+ if (Array.isArray(message.content)) {
46
+ return {
47
+ role: message.role,
48
+ content: message.content as unknown as ApiChatMessage['content'],
49
+ }
50
+ }
51
+
52
+ const imagesForRequest = message.requestImages ?? message.images
53
+ if (imagesForRequest && imagesForRequest.length > 0) {
54
+ return {
55
+ role: message.role,
56
+ content: [
57
+ ...(typeof message.content === 'string' && message.content
58
+ ? [{ type: 'text' as const, text: message.content }]
59
+ : []),
60
+ ...imagesForRequest.map((img) => ({
61
+ type: 'image_url' as const,
62
+ image_url: { url: img },
63
+ })),
64
+ ],
65
+ }
66
+ }
67
+
68
+ return { role: message.role, content: message.content }
69
+ }
70
+
71
+ export function findNonDataImageUrls(messages: ApiChatMessage[]): string[] {
72
+ const urls: string[] = []
73
+ for (const message of messages) {
74
+ if (!Array.isArray(message.content)) {
75
+ continue
76
+ }
77
+ for (const part of message.content) {
78
+ if (
79
+ part &&
80
+ typeof part === 'object' &&
81
+ 'type' in part &&
82
+ part.type === 'image_url' &&
83
+ 'image_url' in part &&
84
+ part.image_url &&
85
+ typeof part.image_url === 'object' &&
86
+ 'url' in part.image_url &&
87
+ typeof part.image_url.url === 'string'
88
+ ) {
89
+ const url = part.image_url.url
90
+ if (!url.startsWith('data:image/')) {
91
+ urls.push(url)
92
+ }
93
+ }
94
+ }
95
+ }
96
+ return urls
97
+ }
@@ -0,0 +1,50 @@
1
+ import test from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import {
4
+ applyThinkingChunk,
5
+ createThinkingStreamState,
6
+ toDisplayContent,
7
+ toFinalContent,
8
+ } from './agentThinkingContent'
9
+
10
+ test('reasoning then answer content yields wrapped think block', () => {
11
+ let state = createThinkingStreamState()
12
+ state = applyThinkingChunk(state, { reasoning: 'I will inspect text.' })
13
+ state = applyThinkingChunk(state, { content: 'It says hello.' })
14
+
15
+ assert.equal(toDisplayContent(state), '<think>I will inspect text.</think>\n\nIt says hello.')
16
+ assert.equal(toFinalContent(state), '<think>I will inspect text.</think>\n\nIt says hello.')
17
+ })
18
+
19
+ test('multiple reasoning and content chunks preserve order', () => {
20
+ let state = createThinkingStreamState()
21
+ state = applyThinkingChunk(state, { reasoning: 'Step 1. ' })
22
+ state = applyThinkingChunk(state, { reasoning: 'Step 2. ' })
23
+ state = applyThinkingChunk(state, { content: 'Answer ' })
24
+ state = applyThinkingChunk(state, { content: 'final.' })
25
+
26
+ assert.equal(toFinalContent(state), '<think>Step 1. Step 2. </think>\n\nAnswer final.')
27
+ })
28
+
29
+ test('reasoning without content closes at finalize', () => {
30
+ let state = createThinkingStreamState()
31
+ state = applyThinkingChunk(state, { reasoning: 'Only internal chain.' })
32
+
33
+ assert.equal(toDisplayContent(state), '<think>Only internal chain.')
34
+ assert.equal(toFinalContent(state), '<think>Only internal chain.</think>')
35
+ })
36
+
37
+ test('no reasoning and no tags remains plain content', () => {
38
+ let state = createThinkingStreamState()
39
+ state = applyThinkingChunk(state, { content: 'Plain output' })
40
+
41
+ assert.equal(toDisplayContent(state), 'Plain output')
42
+ assert.equal(toFinalContent(state), 'Plain output')
43
+ })
44
+
45
+ test('literal think tags in content pass through unchanged', () => {
46
+ let state = createThinkingStreamState()
47
+ state = applyThinkingChunk(state, { content: '<think>raw</think>\n\nanswer' })
48
+
49
+ assert.equal(toFinalContent(state), '<think>raw</think>\n\nanswer')
50
+ })
@@ -0,0 +1,57 @@
1
+ export interface ThinkingStreamState {
2
+ regularContent: string
3
+ reasoningContent: string
4
+ hasReasoning: boolean
5
+ reasoningClosed: boolean
6
+ }
7
+
8
+ export function createThinkingStreamState(): ThinkingStreamState {
9
+ return {
10
+ regularContent: '',
11
+ reasoningContent: '',
12
+ hasReasoning: false,
13
+ reasoningClosed: false,
14
+ }
15
+ }
16
+
17
+ export function applyThinkingChunk(
18
+ state: ThinkingStreamState,
19
+ chunk: { content?: string; reasoning?: string }
20
+ ): ThinkingStreamState {
21
+ const next: ThinkingStreamState = { ...state }
22
+
23
+ if (chunk.reasoning) {
24
+ if (!next.hasReasoning) {
25
+ next.hasReasoning = true
26
+ next.reasoningContent = chunk.reasoning
27
+ } else {
28
+ next.reasoningContent += chunk.reasoning
29
+ }
30
+ }
31
+
32
+ if (chunk.content) {
33
+ if (next.hasReasoning && !next.reasoningClosed && next.reasoningContent) {
34
+ next.reasoningClosed = true
35
+ }
36
+ next.regularContent += chunk.content
37
+ }
38
+
39
+ return next
40
+ }
41
+
42
+ export function toDisplayContent(state: ThinkingStreamState): string {
43
+ if (!state.hasReasoning) {
44
+ return state.regularContent
45
+ }
46
+ const closingTag = state.reasoningClosed ? '</think>' : ''
47
+ const answerBody = state.regularContent ? `\n\n${state.regularContent}` : ''
48
+ return `<think>${state.reasoningContent}${closingTag}${answerBody}`
49
+ }
50
+
51
+ export function toFinalContent(state: ThinkingStreamState): string {
52
+ if (!state.hasReasoning) {
53
+ return state.regularContent
54
+ }
55
+ const answerBody = state.regularContent ? `\n\n${state.regularContent}` : ''
56
+ return `<think>${state.reasoningContent}</think>${answerBody}`
57
+ }
@@ -0,0 +1,66 @@
1
+ import test from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { buildDashboardTokenChartData, buildDashboardTokenMetadata } from './dashboardTokenUsage'
4
+
5
+ test('dashboard token chart rows use total tokens only', () => {
6
+ const rows = buildDashboardTokenChartData({
7
+ window: '24h',
8
+ totalTokens: 300,
9
+ totalInputTokens: 120,
10
+ totalOutputTokens: 180,
11
+ totalRequests: 2,
12
+ avgTokensPerRequest: 150,
13
+ tokenEstimatedCount: 1,
14
+ tokenEstimatedRate: 0.5,
15
+ splitUnknownCount: 1,
16
+ splitUnknownRate: 0.5,
17
+ bucketGranularity: 'hour',
18
+ bucketTimeZone: 'America/Chicago',
19
+ byDay: [
20
+ {
21
+ date: '2026-03-17T12:00',
22
+ count: 2,
23
+ tokens: 300,
24
+ estimated: 1,
25
+ inputTokens: 120,
26
+ outputTokens: 180,
27
+ splitUnknown: 1,
28
+ },
29
+ ],
30
+ })
31
+
32
+ assert.deepEqual(rows, [
33
+ {
34
+ date: '2026-03-17T12:00',
35
+ count: 2,
36
+ tokens: 300,
37
+ estimated: 1,
38
+ },
39
+ ])
40
+ })
41
+
42
+ test('dashboard token metadata keeps non-split status messaging', () => {
43
+ const metadata = buildDashboardTokenMetadata(
44
+ {
45
+ window: '7d',
46
+ totalTokens: 500,
47
+ totalInputTokens: 0,
48
+ totalOutputTokens: 0,
49
+ totalRequests: 5,
50
+ avgTokensPerRequest: 100,
51
+ tokenEstimatedCount: 2,
52
+ tokenEstimatedRate: 0.4,
53
+ bucketGranularity: 'day',
54
+ bucketTimeZone: 'UTC',
55
+ byDay: [],
56
+ },
57
+ 'America/Chicago',
58
+ )
59
+
60
+ assert.deepEqual(metadata, {
61
+ estimatedCount: 2,
62
+ estimatedRate: 0.4,
63
+ granularityLabel: 'daily',
64
+ timeZoneLabel: 'UTC',
65
+ })
66
+ })
@@ -0,0 +1,36 @@
1
+ import type { TokenUsage } from '../api/client'
2
+
3
+ export interface DashboardTokenChartRow {
4
+ date: string
5
+ count: number
6
+ tokens: number
7
+ estimated: number
8
+ }
9
+
10
+ export interface DashboardTokenMetadata {
11
+ estimatedCount: number
12
+ estimatedRate: number
13
+ granularityLabel: string
14
+ timeZoneLabel: string
15
+ }
16
+
17
+ export function buildDashboardTokenChartData(tokenUsage: TokenUsage | null | undefined): DashboardTokenChartRow[] {
18
+ return (tokenUsage?.byDay ?? []).map((row) => ({
19
+ date: row.date,
20
+ count: row.count,
21
+ tokens: row.tokens,
22
+ estimated: row.estimated,
23
+ }))
24
+ }
25
+
26
+ export function buildDashboardTokenMetadata(
27
+ tokenUsage: TokenUsage | null | undefined,
28
+ browserTimeZone: string,
29
+ ): DashboardTokenMetadata {
30
+ return {
31
+ estimatedCount: tokenUsage?.tokenEstimatedCount ?? 0,
32
+ estimatedRate: tokenUsage?.tokenEstimatedRate ?? 0,
33
+ granularityLabel: tokenUsage?.bucketGranularity === 'hour' ? 'hourly' : 'daily',
34
+ timeZoneLabel: tokenUsage?.bucketTimeZone ?? browserTimeZone,
35
+ }
36
+ }
@@ -0,0 +1,39 @@
1
+ import test from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import {
4
+ calculateContainedImageSize,
5
+ MAX_UPLOAD_IMAGE_HEIGHT,
6
+ MAX_UPLOAD_IMAGE_WIDTH,
7
+ } from './imageUpload'
8
+
9
+ test('does not resize images already within the upload bounds', () => {
10
+ assert.deepEqual(calculateContainedImageSize(640, 960), {
11
+ width: 640,
12
+ height: 960,
13
+ resized: false,
14
+ })
15
+ })
16
+
17
+ test('resizes portrait images to stay within the upload bounds', () => {
18
+ assert.deepEqual(calculateContainedImageSize(1200, 2400), {
19
+ width: 640,
20
+ height: 1280,
21
+ resized: true,
22
+ })
23
+ })
24
+
25
+ test('resizes landscape images to stay within the upload bounds', () => {
26
+ assert.deepEqual(calculateContainedImageSize(2400, 1200), {
27
+ width: 720,
28
+ height: 360,
29
+ resized: true,
30
+ })
31
+ })
32
+
33
+ test('preserves aspect ratio while fitting the bounding box', () => {
34
+ const resized = calculateContainedImageSize(2000, 1000)
35
+ assert.equal(resized.resized, true)
36
+ assert.ok(resized.width <= MAX_UPLOAD_IMAGE_WIDTH)
37
+ assert.ok(resized.height <= MAX_UPLOAD_IMAGE_HEIGHT)
38
+ assert.ok(Math.abs(resized.width / resized.height - 2) < 0.01)
39
+ })
@@ -0,0 +1,71 @@
1
+ export const MAX_UPLOAD_IMAGE_WIDTH = 720
2
+ export const MAX_UPLOAD_IMAGE_HEIGHT = 1280
3
+
4
+ export interface ImageDimensions {
5
+ width: number
6
+ height: number
7
+ }
8
+
9
+ export interface ResizedImageDimensions extends ImageDimensions {
10
+ resized: boolean
11
+ }
12
+
13
+ export function calculateContainedImageSize(
14
+ width: number,
15
+ height: number,
16
+ maxWidth: number = MAX_UPLOAD_IMAGE_WIDTH,
17
+ maxHeight: number = MAX_UPLOAD_IMAGE_HEIGHT,
18
+ ): ResizedImageDimensions {
19
+ if (width <= 0 || height <= 0) {
20
+ return { width: Math.max(1, Math.floor(width)), height: Math.max(1, Math.floor(height)), resized: false }
21
+ }
22
+
23
+ const needsResize = width > maxWidth || height > maxHeight
24
+ if (!needsResize) {
25
+ return { width, height, resized: false }
26
+ }
27
+
28
+ const scale = Math.min(maxWidth / width, maxHeight / height)
29
+ return {
30
+ width: Math.max(1, Math.floor(width * scale)),
31
+ height: Math.max(1, Math.floor(height * scale)),
32
+ resized: true,
33
+ }
34
+ }
35
+
36
+ export async function fileToDataUrl(file: File): Promise<string> {
37
+ return new Promise((resolve, reject) => {
38
+ const reader = new FileReader()
39
+ reader.onerror = () => reject(reader.error)
40
+ reader.onload = () => resolve(reader.result as string)
41
+ reader.readAsDataURL(file)
42
+ })
43
+ }
44
+
45
+ export async function compressImageFileForUpload(file: File): Promise<string> {
46
+ const objectUrl = URL.createObjectURL(file)
47
+ try {
48
+ const image = new Image()
49
+ image.src = objectUrl
50
+ await image.decode()
51
+
52
+ const target = calculateContainedImageSize(image.width, image.height)
53
+ if (!target.resized) {
54
+ return await fileToDataUrl(file)
55
+ }
56
+
57
+ const canvas = document.createElement('canvas')
58
+ canvas.width = target.width
59
+ canvas.height = target.height
60
+ const context = canvas.getContext('2d')
61
+ if (!context) {
62
+ return await fileToDataUrl(file)
63
+ }
64
+
65
+ context.drawImage(image, 0, 0, target.width, target.height)
66
+ const mimeType = file.type === 'image/jpeg' || file.type === 'image/png' ? file.type : 'image/png'
67
+ return canvas.toDataURL(mimeType, mimeType === 'image/jpeg' ? 0.9 : undefined)
68
+ } finally {
69
+ URL.revokeObjectURL(objectUrl)
70
+ }
71
+ }
@@ -0,0 +1,29 @@
1
+ import test from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { filterCaptureRecords, isGetModelsRoute } from './peekFilters'
4
+ import type { CaptureRecordSummary } from '@/api/client'
5
+
6
+ test('isGetModelsRoute matches GET /v1/models routes', () => {
7
+ assert.equal(isGetModelsRoute({ method: 'GET', route: '/v1/models' }), true)
8
+ assert.equal(isGetModelsRoute({ method: 'get', route: '/v1/models?limit=20' }), true)
9
+ assert.equal(isGetModelsRoute({ method: 'POST', route: '/v1/models' }), false)
10
+ assert.equal(isGetModelsRoute({ method: 'GET', route: '/v1/chat/completions' }), false)
11
+ })
12
+
13
+ test('filterCaptureRecords removes only GET /v1/models calls when enabled', () => {
14
+ const records = [
15
+ { id: '1', method: 'GET', route: '/v1/models', timestamp: '', statusCode: 200, latencyMs: 10 },
16
+ { id: '2', method: 'GET', route: '/v1/models?limit=5', timestamp: '', statusCode: 200, latencyMs: 11 },
17
+ { id: '3', method: 'POST', route: '/v1/models', timestamp: '', statusCode: 200, latencyMs: 12 },
18
+ { id: '4', method: 'GET', route: '/v1/chat/completions', timestamp: '', statusCode: 200, latencyMs: 13 },
19
+ ] satisfies CaptureRecordSummary[]
20
+
21
+ const filtered = filterCaptureRecords(records, true)
22
+ assert.deepEqual(
23
+ filtered.map((record) => record.id),
24
+ ['3', '4'],
25
+ )
26
+
27
+ const unfiltered = filterCaptureRecords(records, false)
28
+ assert.equal(unfiltered.length, records.length)
29
+ })
@@ -0,0 +1,13 @@
1
+ import type { CaptureRecordSummary } from '@/api/client'
2
+
3
+ export function isGetModelsRoute(record: Pick<CaptureRecordSummary, 'method' | 'route'>): boolean {
4
+ return record.method.toUpperCase() === 'GET' && record.route.startsWith('/v1/models')
5
+ }
6
+
7
+ export function filterCaptureRecords(
8
+ records: CaptureRecordSummary[],
9
+ ignoreModels: boolean,
10
+ ): CaptureRecordSummary[] {
11
+ if (!ignoreModels) return records
12
+ return records.filter((record) => !isGetModelsRoute(record))
13
+ }
@@ -0,0 +1,58 @@
1
+ import test from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { extractEmbeddedMedia, redactEmbeddedMedia } from './peekMedia'
4
+
5
+ test('extractEmbeddedMedia treats b64_json payloads as image media', () => {
6
+ const payload = {
7
+ created: 1773782955,
8
+ data: [
9
+ {
10
+ b64_json: 'AQID',
11
+ },
12
+ ],
13
+ }
14
+
15
+ const media = extractEmbeddedMedia('response', payload)
16
+
17
+ assert.equal(media.length, 1)
18
+ assert.equal(media[0]?.path, '$.data[0].b64_json')
19
+ assert.equal(media[0]?.source, 'response')
20
+ assert.equal(media[0]?.kind, 'image')
21
+ assert.equal(media[0]?.mime, 'image/png')
22
+ assert.equal(media[0]?.url, 'data:image/png;base64,AQID')
23
+ })
24
+
25
+ test('extractEmbeddedMedia prefers sibling data-url mime metadata', () => {
26
+ const payload = {
27
+ data: [
28
+ {
29
+ mime: 'image/webp',
30
+ b64_json: 'BBBB',
31
+ url: 'data:image/webp;base64,BBBB',
32
+ },
33
+ ],
34
+ }
35
+
36
+ const media = extractEmbeddedMedia('response', payload)
37
+
38
+ assert.equal(media.length, 1)
39
+ assert.equal(media[0]?.mime, 'image/webp')
40
+ assert.equal(media[0]?.url, 'data:image/webp;base64,BBBB')
41
+ assert.equal(media[0]?.origin, 'b64_json')
42
+ })
43
+
44
+ test('redactEmbeddedMedia removes inline binary payloads from pretty json', () => {
45
+ const payload = {
46
+ data: [
47
+ {
48
+ b64_json: 'AQID',
49
+ url: 'data:image/png;base64,AQID',
50
+ },
51
+ ],
52
+ }
53
+
54
+ const redacted = redactEmbeddedMedia(payload) as { data: Array<{ b64_json: string; url: string }> }
55
+
56
+ assert.match(redacted.data[0]!.b64_json, /^\[base64 media omitted: image\/png, 4 chars\]$/)
57
+ assert.match(redacted.data[0]!.url, /^\[data URL omitted: image\/png, 4 chars\]$/)
58
+ })