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