@ai-sdk/devtools 1.0.7 → 1.0.9
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/dist/client/assets/index-CltCAda7.css +1 -0
- package/dist/client/assets/index-DgZG5_rI.js +220 -0
- package/dist/client/index.html +2 -2
- package/dist/index.js +91 -9
- package/package.json +3 -3
- package/src/integration.ts +4 -3
- package/src/middleware.ts +13 -6
- package/src/serialize.ts +111 -0
- package/src/viewer/client/components/media-components.tsx +127 -0
- package/src/viewer/client/components/message-components.tsx +34 -4
- package/src/viewer/client/components/output-components.tsx +26 -6
- package/src/viewer/client/components/shared-components.tsx +25 -4
- package/src/viewer/client/components/step-card.tsx +5 -2
- package/src/viewer/client/components/trace-timeline.tsx +16 -4
- package/src/viewer/client/media.ts +440 -0
- package/src/viewer/client/types.ts +19 -1
- package/src/viewer/client/utils.ts +44 -0
- package/dist/client/assets/index-Beo4T2zg.js +0 -200
- package/dist/client/assets/index-CIZI8r26.css +0 -1
|
@@ -8,16 +8,27 @@ import type {
|
|
|
8
8
|
TextContentPart,
|
|
9
9
|
ReasoningContentPart,
|
|
10
10
|
} from '../types';
|
|
11
|
-
import {
|
|
12
|
-
|
|
11
|
+
import {
|
|
12
|
+
safeParseJson,
|
|
13
|
+
formatToolParams,
|
|
14
|
+
getOutputToolResults,
|
|
15
|
+
} from '../utils';
|
|
16
|
+
import {
|
|
17
|
+
MediaAwareValue,
|
|
18
|
+
ReasoningBlock,
|
|
19
|
+
TextBlock,
|
|
20
|
+
} from './shared-components';
|
|
21
|
+
import { MediaPreviewList } from './media-components';
|
|
13
22
|
|
|
14
23
|
export function OutputDisplay({
|
|
15
24
|
output,
|
|
16
|
-
|
|
25
|
+
fallbackToolResults = [],
|
|
17
26
|
}: {
|
|
18
27
|
output: ParsedOutput;
|
|
19
|
-
|
|
28
|
+
fallbackToolResults?: ContentPart[];
|
|
20
29
|
}) {
|
|
30
|
+
const toolResults = getOutputToolResults(output, fallbackToolResults);
|
|
31
|
+
|
|
21
32
|
const getToolResult = (toolCallId: string) => {
|
|
22
33
|
return toolResults.find(
|
|
23
34
|
(r): r is ToolResultContentPart =>
|
|
@@ -62,6 +73,15 @@ export function OutputDisplay({
|
|
|
62
73
|
<TextBlock content={textContent} defaultExpanded={!!isTextOnly} />
|
|
63
74
|
)}
|
|
64
75
|
|
|
76
|
+
<MediaPreviewList
|
|
77
|
+
data={output?.content?.filter(
|
|
78
|
+
part =>
|
|
79
|
+
part.type === 'file' ||
|
|
80
|
+
part.type === 'reasoning-file' ||
|
|
81
|
+
part.type === 'image',
|
|
82
|
+
)}
|
|
83
|
+
/>
|
|
84
|
+
|
|
65
85
|
{toolCalls.map((call, i) => {
|
|
66
86
|
const result = call.toolCallId
|
|
67
87
|
? getToolResult(call.toolCallId)
|
|
@@ -127,7 +147,7 @@ function ToolCallCard({
|
|
|
127
147
|
<div className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground mb-2">
|
|
128
148
|
Input
|
|
129
149
|
</div>
|
|
130
|
-
<
|
|
150
|
+
<MediaAwareValue data={parsedArgs} />
|
|
131
151
|
</div>
|
|
132
152
|
|
|
133
153
|
{parsedResult != null && (
|
|
@@ -135,7 +155,7 @@ function ToolCallCard({
|
|
|
135
155
|
<div className="text-[10px] font-medium uppercase tracking-wider text-success mb-2">
|
|
136
156
|
Output
|
|
137
157
|
</div>
|
|
138
|
-
<
|
|
158
|
+
<MediaAwareValue data={parsedResult} />
|
|
139
159
|
</div>
|
|
140
160
|
)}
|
|
141
161
|
</>
|
|
@@ -29,21 +29,33 @@ import {
|
|
|
29
29
|
getInputTokenBreakdown,
|
|
30
30
|
getOutputTokenBreakdown,
|
|
31
31
|
} from '../utils';
|
|
32
|
+
import { MediaPreviewList } from './media-components';
|
|
32
33
|
|
|
33
34
|
export function JsonBlock({
|
|
34
35
|
data,
|
|
35
36
|
compact = false,
|
|
36
37
|
size = 'sm',
|
|
38
|
+
maxStringLength,
|
|
37
39
|
}: {
|
|
38
40
|
data: unknown;
|
|
39
41
|
compact?: boolean;
|
|
40
42
|
size?: 'sm' | 'base' | 'lg';
|
|
43
|
+
maxStringLength?: number;
|
|
41
44
|
}) {
|
|
42
45
|
const [copied, setCopied] = useState(false);
|
|
43
46
|
|
|
44
|
-
const
|
|
47
|
+
const replacer =
|
|
48
|
+
maxStringLength == null
|
|
49
|
+
? undefined
|
|
50
|
+
: (_key: string, value: unknown) =>
|
|
51
|
+
typeof value === 'string' && value.length > maxStringLength
|
|
52
|
+
? `${value.slice(0, maxStringLength)}… [${value.length - maxStringLength} characters omitted from the viewer]`
|
|
53
|
+
: value;
|
|
54
|
+
const jsonString = JSON.stringify(data, replacer, 2);
|
|
45
55
|
const displayString =
|
|
46
|
-
compact && jsonString.length > 200
|
|
56
|
+
compact && jsonString.length > 200
|
|
57
|
+
? JSON.stringify(data, replacer)
|
|
58
|
+
: jsonString;
|
|
47
59
|
|
|
48
60
|
const sizeClasses = {
|
|
49
61
|
sm: 'text-xs',
|
|
@@ -81,6 +93,15 @@ export function JsonBlock({
|
|
|
81
93
|
);
|
|
82
94
|
}
|
|
83
95
|
|
|
96
|
+
export function MediaAwareValue({ data }: { data: unknown }) {
|
|
97
|
+
return (
|
|
98
|
+
<div className="space-y-3">
|
|
99
|
+
<MediaPreviewList data={data} />
|
|
100
|
+
<JsonBlock data={data} maxStringLength={16 * 1024} />
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
84
105
|
export function RawDataSection({
|
|
85
106
|
rawRequest,
|
|
86
107
|
rawResponse,
|
|
@@ -516,7 +537,7 @@ export function CollapsibleToolCall({
|
|
|
516
537
|
</button>
|
|
517
538
|
{expanded && (
|
|
518
539
|
<div className="p-3 border-t bg-card/50 border-tool/30">
|
|
519
|
-
<
|
|
540
|
+
<MediaAwareValue data={parsedData} />
|
|
520
541
|
</div>
|
|
521
542
|
)}
|
|
522
543
|
</div>
|
|
@@ -559,7 +580,7 @@ export function CollapsibleToolResult({
|
|
|
559
580
|
</button>
|
|
560
581
|
{expanded && (
|
|
561
582
|
<div className="p-3 border-t bg-card/50 border-success/30">
|
|
562
|
-
<
|
|
583
|
+
<MediaAwareValue data={data} />
|
|
563
584
|
</div>
|
|
564
585
|
)}
|
|
565
586
|
</div>
|
|
@@ -256,7 +256,7 @@ export function StepDetailContent({
|
|
|
256
256
|
const nextInput = nextStep
|
|
257
257
|
? (parseJson(nextStep.input) as ParsedInput | null)
|
|
258
258
|
: null;
|
|
259
|
-
const
|
|
259
|
+
const fallbackToolResults: ContentPart[] =
|
|
260
260
|
nextInput?.prompt
|
|
261
261
|
?.filter((msg: PromptMessage) => msg.role === 'tool')
|
|
262
262
|
?.flatMap((msg: PromptMessage) =>
|
|
@@ -290,7 +290,10 @@ export function StepDetailContent({
|
|
|
290
290
|
{step.error}
|
|
291
291
|
</div>
|
|
292
292
|
) : output ? (
|
|
293
|
-
<OutputDisplay
|
|
293
|
+
<OutputDisplay
|
|
294
|
+
output={output}
|
|
295
|
+
fallbackToolResults={fallbackToolResults}
|
|
296
|
+
/>
|
|
294
297
|
) : isActiveStep ? (
|
|
295
298
|
<div className="flex items-center gap-2 text-sm text-info">
|
|
296
299
|
<Loader2 className="size-4 animate-spin" />
|
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
SpanKind,
|
|
15
15
|
TraceSpan,
|
|
16
16
|
ParseJson,
|
|
17
|
+
ParsedInput,
|
|
17
18
|
ParsedOutput,
|
|
18
19
|
ContentPart,
|
|
19
20
|
ToolCallContentPart,
|
|
@@ -21,11 +22,12 @@ import type {
|
|
|
21
22
|
} from '../types';
|
|
22
23
|
import {
|
|
23
24
|
buildTraceSpans,
|
|
25
|
+
getOutputToolResults,
|
|
24
26
|
safeParseJson,
|
|
25
27
|
SPAN_COLORS,
|
|
26
28
|
SPAN_COLORS_MUTED,
|
|
27
29
|
} from '../utils';
|
|
28
|
-
import {
|
|
30
|
+
import { MediaAwareValue } from './shared-components';
|
|
29
31
|
import { StepDetailContent } from './step-card';
|
|
30
32
|
|
|
31
33
|
export function TraceTimeline({
|
|
@@ -424,7 +426,17 @@ function SpanDetailPanel({
|
|
|
424
426
|
'toolCallId' in p &&
|
|
425
427
|
p.toolCallId === span.toolCallId,
|
|
426
428
|
);
|
|
427
|
-
const
|
|
429
|
+
const nextStep = steps[stepIndex + 1];
|
|
430
|
+
const nextInput = nextStep
|
|
431
|
+
? (parseJson(nextStep.input) as ParsedInput | null)
|
|
432
|
+
: null;
|
|
433
|
+
const fallbackToolResults =
|
|
434
|
+
nextInput?.prompt
|
|
435
|
+
?.filter(message => message.role === 'tool')
|
|
436
|
+
.flatMap(message =>
|
|
437
|
+
Array.isArray(message.content) ? message.content : [],
|
|
438
|
+
) ?? [];
|
|
439
|
+
const toolResult = getOutputToolResults(output, fallbackToolResults).find(
|
|
428
440
|
(p): p is ToolResultContentPart =>
|
|
429
441
|
p.type === 'tool-result' &&
|
|
430
442
|
'toolCallId' in p &&
|
|
@@ -450,7 +462,7 @@ function SpanDetailPanel({
|
|
|
450
462
|
<h4 className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground mb-2">
|
|
451
463
|
Input
|
|
452
464
|
</h4>
|
|
453
|
-
<
|
|
465
|
+
<MediaAwareValue data={parsedArgs} />
|
|
454
466
|
</div>
|
|
455
467
|
)}
|
|
456
468
|
{parsedResult != null && (
|
|
@@ -458,7 +470,7 @@ function SpanDetailPanel({
|
|
|
458
470
|
<h4 className="text-[11px] font-semibold uppercase tracking-wider text-success mb-2">
|
|
459
471
|
Output
|
|
460
472
|
</h4>
|
|
461
|
-
<
|
|
473
|
+
<MediaAwareValue data={parsedResult} />
|
|
462
474
|
</div>
|
|
463
475
|
)}
|
|
464
476
|
{!parsedArgs && !parsedResult && (
|
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
export type MediaKind = 'image' | 'audio' | 'video' | 'file';
|
|
2
|
+
|
|
3
|
+
export interface MediaPreviewData {
|
|
4
|
+
filename?: string;
|
|
5
|
+
kind: MediaKind;
|
|
6
|
+
mediaType: string;
|
|
7
|
+
source?: string;
|
|
8
|
+
sourceType?: 'inline' | 'remote';
|
|
9
|
+
unavailableReason?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface MediaPreviewLimits {
|
|
13
|
+
maxCount: number;
|
|
14
|
+
maxDepth: number;
|
|
15
|
+
maxInlineBytes: number;
|
|
16
|
+
maxNodes: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const DEFAULT_MEDIA_PREVIEW_LIMITS: MediaPreviewLimits = {
|
|
20
|
+
maxCount: 8,
|
|
21
|
+
maxDepth: 12,
|
|
22
|
+
maxInlineBytes: 5 * 1024 * 1024,
|
|
23
|
+
maxNodes: 1000,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const safeInlineMediaTypes = new Set([
|
|
27
|
+
'audio/m4a',
|
|
28
|
+
'audio/mp3',
|
|
29
|
+
'audio/mp4',
|
|
30
|
+
'audio/mpeg',
|
|
31
|
+
'audio/ogg',
|
|
32
|
+
'audio/wav',
|
|
33
|
+
'audio/webm',
|
|
34
|
+
'image/avif',
|
|
35
|
+
'image/bmp',
|
|
36
|
+
'image/gif',
|
|
37
|
+
'image/jpeg',
|
|
38
|
+
'image/png',
|
|
39
|
+
'image/webp',
|
|
40
|
+
'image/x-icon',
|
|
41
|
+
'video/mp4',
|
|
42
|
+
'video/ogg',
|
|
43
|
+
'video/webm',
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
const mediaTypesByExtension: Record<string, string> = {
|
|
47
|
+
avif: 'image/avif',
|
|
48
|
+
bmp: 'image/bmp',
|
|
49
|
+
gif: 'image/gif',
|
|
50
|
+
ico: 'image/x-icon',
|
|
51
|
+
jpeg: 'image/jpeg',
|
|
52
|
+
jpg: 'image/jpeg',
|
|
53
|
+
m4a: 'audio/mp4',
|
|
54
|
+
mp3: 'audio/mpeg',
|
|
55
|
+
mp4: 'video/mp4',
|
|
56
|
+
oga: 'audio/ogg',
|
|
57
|
+
ogg: 'audio/ogg',
|
|
58
|
+
ogv: 'video/ogg',
|
|
59
|
+
png: 'image/png',
|
|
60
|
+
wav: 'audio/wav',
|
|
61
|
+
webm: 'video/webm',
|
|
62
|
+
webp: 'image/webp',
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
66
|
+
return value != null && typeof value === 'object' && !Array.isArray(value);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function getKind(mediaType: string): MediaKind {
|
|
70
|
+
const topLevelType = mediaType.toLowerCase().split('/')[0];
|
|
71
|
+
return topLevelType === 'image' ||
|
|
72
|
+
topLevelType === 'audio' ||
|
|
73
|
+
topLevelType === 'video'
|
|
74
|
+
? topLevelType
|
|
75
|
+
: 'file';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function getBase64Prefix(data: string, maxBytes = 16): number[] {
|
|
79
|
+
const alphabet =
|
|
80
|
+
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
81
|
+
const bytes: number[] = [];
|
|
82
|
+
let buffer = 0;
|
|
83
|
+
let bitCount = 0;
|
|
84
|
+
|
|
85
|
+
for (const character of data) {
|
|
86
|
+
if (character === '=') break;
|
|
87
|
+
|
|
88
|
+
const value = alphabet.indexOf(character);
|
|
89
|
+
if (value === -1) return [];
|
|
90
|
+
|
|
91
|
+
buffer = (buffer << 6) | value;
|
|
92
|
+
bitCount += 6;
|
|
93
|
+
|
|
94
|
+
if (bitCount >= 8) {
|
|
95
|
+
bitCount -= 8;
|
|
96
|
+
bytes.push((buffer >> bitCount) & 0xff);
|
|
97
|
+
if (bytes.length >= maxBytes) break;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return bytes;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function hasBytes(bytes: number[], expected: number[], offset = 0): boolean {
|
|
105
|
+
return expected.every((value, index) => bytes[offset + index] === value);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function detectInlineMediaType({
|
|
109
|
+
data,
|
|
110
|
+
mediaType,
|
|
111
|
+
}: {
|
|
112
|
+
data: string;
|
|
113
|
+
mediaType: string;
|
|
114
|
+
}): string | undefined {
|
|
115
|
+
const normalizedMediaType = mediaType.toLowerCase();
|
|
116
|
+
if (safeInlineMediaTypes.has(normalizedMediaType)) {
|
|
117
|
+
return normalizedMediaType;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const bytes = getBase64Prefix(data);
|
|
121
|
+
|
|
122
|
+
if (normalizedMediaType === 'image') {
|
|
123
|
+
if (hasBytes(bytes, [0x89, 0x50, 0x4e, 0x47])) return 'image/png';
|
|
124
|
+
if (hasBytes(bytes, [0xff, 0xd8, 0xff])) return 'image/jpeg';
|
|
125
|
+
if (hasBytes(bytes, [0x47, 0x49, 0x46, 0x38])) return 'image/gif';
|
|
126
|
+
if (
|
|
127
|
+
hasBytes(bytes, [0x52, 0x49, 0x46, 0x46]) &&
|
|
128
|
+
hasBytes(bytes, [0x57, 0x45, 0x42, 0x50], 8)
|
|
129
|
+
) {
|
|
130
|
+
return 'image/webp';
|
|
131
|
+
}
|
|
132
|
+
if (hasBytes(bytes, [0x42, 0x4d])) return 'image/bmp';
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (normalizedMediaType === 'audio') {
|
|
136
|
+
if (hasBytes(bytes, [0x49, 0x44, 0x33])) return 'audio/mpeg';
|
|
137
|
+
if (bytes[0] === 0xff && bytes[1] != null && (bytes[1] & 0xe0) === 0xe0) {
|
|
138
|
+
return 'audio/mpeg';
|
|
139
|
+
}
|
|
140
|
+
if (
|
|
141
|
+
hasBytes(bytes, [0x52, 0x49, 0x46, 0x46]) &&
|
|
142
|
+
hasBytes(bytes, [0x57, 0x41, 0x56, 0x45], 8)
|
|
143
|
+
) {
|
|
144
|
+
return 'audio/wav';
|
|
145
|
+
}
|
|
146
|
+
if (hasBytes(bytes, [0x4f, 0x67, 0x67, 0x53])) return 'audio/ogg';
|
|
147
|
+
if (hasBytes(bytes, [0x1a, 0x45, 0xdf, 0xa3])) return 'audio/webm';
|
|
148
|
+
if (hasBytes(bytes, [0x66, 0x74, 0x79, 0x70], 4)) return 'audio/mp4';
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (normalizedMediaType === 'video') {
|
|
152
|
+
if (hasBytes(bytes, [0x4f, 0x67, 0x67, 0x53])) return 'video/ogg';
|
|
153
|
+
if (hasBytes(bytes, [0x1a, 0x45, 0xdf, 0xa3])) return 'video/webm';
|
|
154
|
+
if (hasBytes(bytes, [0x66, 0x74, 0x79, 0x70], 4)) return 'video/mp4';
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function inferMediaTypeFromUrl(source: unknown): string | undefined {
|
|
161
|
+
if (typeof source !== 'string') return undefined;
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
const extension = new URL(source).pathname.split('.').pop()?.toLowerCase();
|
|
165
|
+
return extension == null ? undefined : mediaTypesByExtension[extension];
|
|
166
|
+
} catch {
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function getBase64ByteLength(value: string): number | undefined {
|
|
172
|
+
if (
|
|
173
|
+
value.length === 0 ||
|
|
174
|
+
value.length % 4 !== 0 ||
|
|
175
|
+
!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(
|
|
176
|
+
value,
|
|
177
|
+
)
|
|
178
|
+
) {
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const padding = value.endsWith('==') ? 2 : value.endsWith('=') ? 1 : 0;
|
|
183
|
+
return (value.length / 4) * 3 - padding;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function getSafeSource({
|
|
187
|
+
source,
|
|
188
|
+
mediaType,
|
|
189
|
+
maxInlineBytes,
|
|
190
|
+
}: {
|
|
191
|
+
source: unknown;
|
|
192
|
+
mediaType: string;
|
|
193
|
+
maxInlineBytes: number;
|
|
194
|
+
}): Pick<MediaPreviewData, 'source' | 'sourceType' | 'unavailableReason'> {
|
|
195
|
+
if (typeof source !== 'string') {
|
|
196
|
+
return {};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (source.startsWith('data:')) {
|
|
200
|
+
const match = /^data:([^;,]+);base64,(.*)$/i.exec(source);
|
|
201
|
+
const sourceMediaType = match?.[1]?.toLowerCase();
|
|
202
|
+
const byteLength =
|
|
203
|
+
match?.[2] == null ? undefined : getBase64ByteLength(match[2]);
|
|
204
|
+
const declaredTopLevelType = mediaType.toLowerCase().split('/')[0];
|
|
205
|
+
if (
|
|
206
|
+
sourceMediaType == null ||
|
|
207
|
+
byteLength == null ||
|
|
208
|
+
(sourceMediaType !== mediaType.toLowerCase() &&
|
|
209
|
+
sourceMediaType.split('/')[0] !== declaredTopLevelType) ||
|
|
210
|
+
!safeInlineMediaTypes.has(sourceMediaType)
|
|
211
|
+
) {
|
|
212
|
+
return {};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (byteLength > maxInlineBytes) {
|
|
216
|
+
return {
|
|
217
|
+
unavailableReason: `Inline preview exceeds the ${maxInlineBytes}-byte limit.`,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return { source, sourceType: 'inline' };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
const url = new URL(source);
|
|
226
|
+
return (url.protocol === 'http:' || url.protocol === 'https:') &&
|
|
227
|
+
url.username === '' &&
|
|
228
|
+
url.password === ''
|
|
229
|
+
? { source: url.toString(), sourceType: 'remote' }
|
|
230
|
+
: {};
|
|
231
|
+
} catch {
|
|
232
|
+
const inlineMediaType = detectInlineMediaType({
|
|
233
|
+
data: source,
|
|
234
|
+
mediaType,
|
|
235
|
+
});
|
|
236
|
+
const byteLength = getBase64ByteLength(source);
|
|
237
|
+
if (inlineMediaType == null || byteLength == null) {
|
|
238
|
+
return {};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (byteLength > maxInlineBytes) {
|
|
242
|
+
return {
|
|
243
|
+
unavailableReason: `Inline preview exceeds the ${maxInlineBytes}-byte limit.`,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
source: `data:${inlineMediaType};base64,${source}`,
|
|
249
|
+
sourceType: 'inline',
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function getFileData(
|
|
255
|
+
value: Record<string, unknown>,
|
|
256
|
+
maxInlineBytes: number,
|
|
257
|
+
): Pick<MediaPreviewData, 'source' | 'sourceType' | 'unavailableReason'> {
|
|
258
|
+
const generatedFile = isRecord(value.file) ? value.file : undefined;
|
|
259
|
+
const mediaType =
|
|
260
|
+
typeof value.mediaType === 'string'
|
|
261
|
+
? value.mediaType
|
|
262
|
+
: typeof generatedFile?.mediaType === 'string'
|
|
263
|
+
? generatedFile.mediaType
|
|
264
|
+
: undefined;
|
|
265
|
+
if (mediaType == null) {
|
|
266
|
+
return {};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const data = value.data;
|
|
270
|
+
if (isRecord(data)) {
|
|
271
|
+
if (data.type === 'data') {
|
|
272
|
+
return getSafeSource({ source: data.data, mediaType, maxInlineBytes });
|
|
273
|
+
}
|
|
274
|
+
if (data.type === 'url') {
|
|
275
|
+
return getSafeSource({ source: data.url, mediaType, maxInlineBytes });
|
|
276
|
+
}
|
|
277
|
+
return {};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const generatedFileData =
|
|
281
|
+
generatedFile?.base64 ??
|
|
282
|
+
generatedFile?.base64Data ??
|
|
283
|
+
generatedFile?.uint8Array ??
|
|
284
|
+
generatedFile?.uint8ArrayData;
|
|
285
|
+
|
|
286
|
+
return getSafeSource({
|
|
287
|
+
source: data ?? generatedFileData,
|
|
288
|
+
mediaType,
|
|
289
|
+
maxInlineBytes,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function parseMediaPart(
|
|
294
|
+
value: unknown,
|
|
295
|
+
maxInlineBytes: number,
|
|
296
|
+
): MediaPreviewData | undefined {
|
|
297
|
+
if (!isRecord(value) || typeof value.type !== 'string') {
|
|
298
|
+
return undefined;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (value.type === 'file' || value.type === 'reasoning-file') {
|
|
302
|
+
const generatedFile = isRecord(value.file) ? value.file : undefined;
|
|
303
|
+
const mediaType =
|
|
304
|
+
typeof value.mediaType === 'string'
|
|
305
|
+
? value.mediaType
|
|
306
|
+
: typeof generatedFile?.mediaType === 'string'
|
|
307
|
+
? generatedFile.mediaType
|
|
308
|
+
: undefined;
|
|
309
|
+
if (mediaType == null) {
|
|
310
|
+
return undefined;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return {
|
|
314
|
+
filename:
|
|
315
|
+
typeof value.filename === 'string'
|
|
316
|
+
? value.filename
|
|
317
|
+
: typeof generatedFile?.filename === 'string'
|
|
318
|
+
? generatedFile.filename
|
|
319
|
+
: undefined,
|
|
320
|
+
kind: getKind(mediaType),
|
|
321
|
+
mediaType,
|
|
322
|
+
...getFileData(value, maxInlineBytes),
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (value.type === 'image') {
|
|
327
|
+
const mediaType =
|
|
328
|
+
typeof value.mediaType === 'string' ? value.mediaType : 'image/png';
|
|
329
|
+
const image = value.image;
|
|
330
|
+
const source =
|
|
331
|
+
isRecord(image) && image.type === 'url'
|
|
332
|
+
? image.url
|
|
333
|
+
: isRecord(image) && image.type === 'data'
|
|
334
|
+
? image.data
|
|
335
|
+
: image;
|
|
336
|
+
|
|
337
|
+
return {
|
|
338
|
+
kind: 'image',
|
|
339
|
+
mediaType,
|
|
340
|
+
...getSafeSource({ source, mediaType, maxInlineBytes }),
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (
|
|
345
|
+
value.type === 'media' ||
|
|
346
|
+
value.type === 'file-data' ||
|
|
347
|
+
value.type === 'image-data'
|
|
348
|
+
) {
|
|
349
|
+
const mediaType =
|
|
350
|
+
typeof value.mediaType === 'string'
|
|
351
|
+
? value.mediaType
|
|
352
|
+
: value.type === 'image-data'
|
|
353
|
+
? 'image/png'
|
|
354
|
+
: undefined;
|
|
355
|
+
if (mediaType == null) {
|
|
356
|
+
return undefined;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return {
|
|
360
|
+
filename: typeof value.filename === 'string' ? value.filename : undefined,
|
|
361
|
+
kind: getKind(mediaType),
|
|
362
|
+
mediaType,
|
|
363
|
+
...getSafeSource({
|
|
364
|
+
source: value.data,
|
|
365
|
+
mediaType,
|
|
366
|
+
maxInlineBytes,
|
|
367
|
+
}),
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (value.type === 'image-url' || value.type === 'file-url') {
|
|
372
|
+
const mediaType =
|
|
373
|
+
typeof value.mediaType === 'string'
|
|
374
|
+
? value.mediaType
|
|
375
|
+
: value.type === 'image-url'
|
|
376
|
+
? 'image'
|
|
377
|
+
: (inferMediaTypeFromUrl(value.url) ?? 'application/octet-stream');
|
|
378
|
+
|
|
379
|
+
return {
|
|
380
|
+
filename: typeof value.filename === 'string' ? value.filename : undefined,
|
|
381
|
+
kind: getKind(mediaType),
|
|
382
|
+
mediaType,
|
|
383
|
+
...getSafeSource({ source: value.url, mediaType, maxInlineBytes }),
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return undefined;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export function findMediaPreviews(
|
|
391
|
+
value: unknown,
|
|
392
|
+
limitOverrides: Partial<MediaPreviewLimits> = {},
|
|
393
|
+
): MediaPreviewData[] {
|
|
394
|
+
const limits = { ...DEFAULT_MEDIA_PREVIEW_LIMITS, ...limitOverrides };
|
|
395
|
+
const previews: MediaPreviewData[] = [];
|
|
396
|
+
const visited = new WeakSet<object>();
|
|
397
|
+
const pending: Array<{ value: unknown; depth: number }> = [
|
|
398
|
+
{ value, depth: 0 },
|
|
399
|
+
];
|
|
400
|
+
let visitedNodes = 0;
|
|
401
|
+
|
|
402
|
+
while (
|
|
403
|
+
pending.length > 0 &&
|
|
404
|
+
previews.length < limits.maxCount &&
|
|
405
|
+
visitedNodes < limits.maxNodes
|
|
406
|
+
) {
|
|
407
|
+
const { value: current, depth } = pending.pop()!;
|
|
408
|
+
visitedNodes++;
|
|
409
|
+
|
|
410
|
+
const preview = parseMediaPart(current, limits.maxInlineBytes);
|
|
411
|
+
if (preview != null) {
|
|
412
|
+
previews.push(preview);
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (
|
|
417
|
+
depth >= limits.maxDepth ||
|
|
418
|
+
current == null ||
|
|
419
|
+
typeof current !== 'object'
|
|
420
|
+
) {
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (visited.has(current)) {
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
visited.add(current);
|
|
428
|
+
|
|
429
|
+
const children = Array.isArray(current)
|
|
430
|
+
? current
|
|
431
|
+
: isRecord(current)
|
|
432
|
+
? Object.values(current)
|
|
433
|
+
: [];
|
|
434
|
+
for (let index = children.length - 1; index >= 0; index--) {
|
|
435
|
+
pending.push({ value: children[index], depth: depth + 1 });
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return previews;
|
|
440
|
+
}
|
|
@@ -137,11 +137,25 @@ export interface ReasoningContentPart {
|
|
|
137
137
|
toolCallId?: string;
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
export interface MediaContentPart {
|
|
141
|
+
type:
|
|
142
|
+
| 'file'
|
|
143
|
+
| 'reasoning-file'
|
|
144
|
+
| 'image'
|
|
145
|
+
| 'media'
|
|
146
|
+
| 'file-data'
|
|
147
|
+
| 'file-url'
|
|
148
|
+
| 'image-data'
|
|
149
|
+
| 'image-url';
|
|
150
|
+
[key: string]: unknown;
|
|
151
|
+
}
|
|
152
|
+
|
|
140
153
|
export type ContentPart =
|
|
141
154
|
| TextContentPart
|
|
142
155
|
| ToolCallContentPart
|
|
143
156
|
| ToolResultContentPart
|
|
144
|
-
| ReasoningContentPart
|
|
157
|
+
| ReasoningContentPart
|
|
158
|
+
| MediaContentPart;
|
|
145
159
|
|
|
146
160
|
export type MessageRole = 'user' | 'assistant' | 'system' | 'tool';
|
|
147
161
|
|
|
@@ -169,9 +183,13 @@ export interface ToolDefinition {
|
|
|
169
183
|
export interface ParsedOutput {
|
|
170
184
|
finishReason?: string | { unified?: string; raw?: string };
|
|
171
185
|
toolCalls?: ToolCallContentPart[];
|
|
186
|
+
toolResults?: ToolResultContentPart[];
|
|
172
187
|
textParts?: TextContentPart[];
|
|
173
188
|
reasoningParts?: ReasoningContentPart[];
|
|
174
189
|
content?: ContentPart[];
|
|
190
|
+
response?: {
|
|
191
|
+
messages?: PromptMessage[];
|
|
192
|
+
};
|
|
175
193
|
}
|
|
176
194
|
|
|
177
195
|
export interface ParsedUsage {
|