Skip to content

Commit 5b70841

Browse files
committed
pretty-print anthropic logs
1 parent ac417fb commit 5b70841

3 files changed

Lines changed: 332 additions & 62 deletions

File tree

anthropic.html

Lines changed: 284 additions & 0 deletions
Large diffs are not rendered by default.

dist/opencode-trace.ts

Lines changed: 24 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -89,61 +89,67 @@ function short(s) {
8989
}
9090
9191
/**
92-
* Interprets the content array of from an OpenAI payload into a string for display.
92+
* Interprets a message-content array into text for display.
9393
* If we're given undefined, returns an empty string.
9494
*/
95-
function content(contents) {
95+
function contentText(contents) {
9696
if (typeof contents === 'string') return contents;
97+
if (!Array.isArray(contents)) return JSON.stringify(contents ?? '');
9798
let r = [];
9899
for (const c of contents ?? []) {
99100
if (c.type === 'input_text') r.push(c.text);
100101
else if (c.type === 'output_text') r.push(c.text);
102+
else if (c.type === 'text') r.push(c.text);
101103
else r.push(\`[\${c.type}]\`);
102104
}
103105
return r.join('\\n');
104106
}
105107
106108
/**
107-
* Renders a sequence of elements from an OpenAI payload: messages, function_calls, function_call_outputs.
109+
* Renders a sequence of payload elements from either OpenAI or Anthropic payloads.
108110
*/
109111
function renderPayload(elements) {
112+
if (!Array.isArray(elements)) return [];
110113
const payload = [];
111114
for (const e of elements) {
112115
if (e === '...') {
113116
payload.push({[TITLE]: 'Added...'});
114117
} else if (e === '---') {
115118
payload.push({[TITLE]: 'Removed...'});
116-
} else if (e.type === 'message') {
119+
} else if (e.type === 'message' || (e.type === undefined && e.role !== undefined && e.content !== undefined)) {
117120
payload.push({
118121
[TITLE]: \`\${payload.length}: message(\${esc(e.role)}): \`,
119-
[INLINE]: esc(short(content(e.content))),
120-
body: content(e.content),
122+
[INLINE]: esc(short(contentText(e.content))),
123+
body: contentText(e.content),
121124
});
122-
} else if (e.type === 'input_text' || e.type === 'output_text') {
125+
} else if (e.type === 'input_text' || e.type === 'output_text' || e.type === 'text') {
123126
payload.push({
124127
[TITLE]: \`\${payload.length}: \${e.type}: \`,
125128
[INLINE]: esc(short(e.text)),
126129
body: e.text,
127130
});
128131
129-
} else if (e.type === 'function_call_output') {
132+
} else if (e.type === 'function_call_output' || e.type === 'tool_result') {
133+
const result = (e.type === 'function_call_output') ?
134+
(typeof e.output === 'string' ? e.output : JSON.stringify(e.output ?? ''))
135+
: typeof e.content === 'string' ? e.content : contentText(e.content);
130136
payload.push({
131137
[TITLE]: \`\${payload.length}: \`,
132-
[INLINE]: \`function_call_output: \${esc(short(e.output))}\`,
138+
[INLINE]: \`\${esc(e.type)}: \${esc(short(result))}\`,
133139
body: e,
134140
});
135-
} else if (e.type === 'function_call') {
136-
let arg;
141+
} else if (e.type === 'function_call' || e.type === 'tool_use') {
142+
let arg = "";
137143
try {
138-
const j = JSON.parse(e.arguments);
139-
arg = j.cmd ?? j.pattern;
140-
arg = \`"\${esc(short(arg))}"\`;
144+
const raw = e.type === 'function_call' ? JSON.parse(e.arguments) : e.input;
145+
const rawArg = raw?.cmd ?? raw?.pattern ?? raw;
146+
arg = typeof rawArg === 'string' ? rawArg : JSON.stringify(rawArg ?? '');
141147
} catch (e) {
142148
arg = '...';
143149
}
144150
payload.push({
145151
[TITLE]: \`\${payload.length}: \`,
146-
[INLINE]: \`function_call: \${esc(e.name)}(\${esc(short(arg))})\`,
152+
[INLINE]: \`\${esc(e.type)}: \${esc(e.name ?? '???')}(\${esc(short(arg))})\`,
147153
body: e,
148154
});
149155
} else {
@@ -163,24 +169,13 @@ function renderPayload(elements) {
163169
* Otherwise, renders primtives, objects, arrays in the obvious way.
164170
*/
165171
function render(data, label) {
172+
const deltaField = (key) => data[key] ?? data[\`\${key}+\`] ?? data[\`\${key}-\`] ?? data[\`*\${key}\`];
166173
const id = data?._id !== undefined ? \` #\${data._id}\` : '';
167174
const purpose = data?._purpose ? \` \${data._purpose}\` : '';
168175
if (data?.[TITLE] !== undefined) {
169176
return data;
170177
} else if (data?._kind === 'request') {
171-
const input = data['input'] ?? data['input+'] ?? data['input-'] ?? data['*input'] ?? [];
172-
// OpenCode request bodies can mix message wrappers like
173-
// \`{role:'user', content:[{type:'input_text', text:'...'}]}\` with already-typed items like
174-
// \`{type:'function_call', ...}\`. Flatten wrapper objects to their \`content\` items so \`payload\`
175-
// is always a list of typed entries like \`{type:'input_text', ...}\` or \`{type:'function_call', ...}\`.
176-
const payload = input.flatMap(e =>
177-
e && typeof e === 'object' && !Array.isArray(e) && e.type === undefined && Array.isArray(e.content)
178-
? e.content
179-
: e && typeof e === 'object' && !Array.isArray(e) && e.type === undefined && typeof e.content === 'string'
180-
? [{type: 'input_text', text: e.content}]
181-
: [e],
182-
);
183-
const rendered = renderPayload(payload);
178+
const rendered = renderPayload(deltaField('input') ?? deltaField('messages'));
184179
const raw = {...data};
185180
delete raw._kind;
186181
return {
@@ -189,9 +184,7 @@ function render(data, label) {
189184
open: true,
190185
};
191186
} else if (data?._kind === 'response') {
192-
const payload = renderPayload(
193-
data['output'] ?? data['output+'] ?? data['output-'] ?? data['*output'] ?? [],
194-
);
187+
const payload = renderPayload(deltaField('output') ?? deltaField('content'));
195188
const raw = {...data};
196189
delete raw._kind;
197190
return {

opencode-trace.js

Lines changed: 24 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -55,61 +55,67 @@ function short(s) {
5555
}
5656

5757
/**
58-
* Interprets the content array of from an OpenAI payload into a string for display.
58+
* Interprets a message-content array into text for display.
5959
* If we're given undefined, returns an empty string.
6060
*/
61-
function content(contents) {
61+
function contentText(contents) {
6262
if (typeof contents === 'string') return contents;
63+
if (!Array.isArray(contents)) return JSON.stringify(contents ?? '');
6364
let r = [];
6465
for (const c of contents ?? []) {
6566
if (c.type === 'input_text') r.push(c.text);
6667
else if (c.type === 'output_text') r.push(c.text);
68+
else if (c.type === 'text') r.push(c.text);
6769
else r.push(`[${c.type}]`);
6870
}
6971
return r.join('\n');
7072
}
7173

7274
/**
73-
* Renders a sequence of elements from an OpenAI payload: messages, function_calls, function_call_outputs.
75+
* Renders a sequence of payload elements from either OpenAI or Anthropic payloads.
7476
*/
7577
function renderPayload(elements) {
78+
if (!Array.isArray(elements)) return [];
7679
const payload = [];
7780
for (const e of elements) {
7881
if (e === '...') {
7982
payload.push({[TITLE]: 'Added...'});
8083
} else if (e === '---') {
8184
payload.push({[TITLE]: 'Removed...'});
82-
} else if (e.type === 'message') {
85+
} else if (e.type === 'message' || (e.type === undefined && e.role !== undefined && e.content !== undefined)) {
8386
payload.push({
8487
[TITLE]: `${payload.length}: message(${esc(e.role)}): `,
85-
[INLINE]: esc(short(content(e.content))),
86-
body: content(e.content),
88+
[INLINE]: esc(short(contentText(e.content))),
89+
body: contentText(e.content),
8790
});
88-
} else if (e.type === 'input_text' || e.type === 'output_text') {
91+
} else if (e.type === 'input_text' || e.type === 'output_text' || e.type === 'text') {
8992
payload.push({
9093
[TITLE]: `${payload.length}: ${e.type}: `,
9194
[INLINE]: esc(short(e.text)),
9295
body: e.text,
9396
});
9497

95-
} else if (e.type === 'function_call_output') {
98+
} else if (e.type === 'function_call_output' || e.type === 'tool_result') {
99+
const result = (e.type === 'function_call_output') ?
100+
(typeof e.output === 'string' ? e.output : JSON.stringify(e.output ?? ''))
101+
: typeof e.content === 'string' ? e.content : contentText(e.content);
96102
payload.push({
97103
[TITLE]: `${payload.length}: `,
98-
[INLINE]: `function_call_output: ${esc(short(e.output))}`,
104+
[INLINE]: `${esc(e.type)}: ${esc(short(result))}`,
99105
body: e,
100106
});
101-
} else if (e.type === 'function_call') {
102-
let arg;
107+
} else if (e.type === 'function_call' || e.type === 'tool_use') {
108+
let arg = "";
103109
try {
104-
const j = JSON.parse(e.arguments);
105-
arg = j.cmd ?? j.pattern;
106-
arg = `"${esc(short(arg))}"`;
110+
const raw = e.type === 'function_call' ? JSON.parse(e.arguments) : e.input;
111+
const rawArg = raw?.cmd ?? raw?.pattern ?? raw;
112+
arg = typeof rawArg === 'string' ? rawArg : JSON.stringify(rawArg ?? '');
107113
} catch (e) {
108114
arg = '...';
109115
}
110116
payload.push({
111117
[TITLE]: `${payload.length}: `,
112-
[INLINE]: `function_call: ${esc(e.name)}(${esc(short(arg))})`,
118+
[INLINE]: `${esc(e.type)}: ${esc(e.name ?? '???')}(${esc(short(arg))})`,
113119
body: e,
114120
});
115121
} else {
@@ -129,24 +135,13 @@ function renderPayload(elements) {
129135
* Otherwise, renders primtives, objects, arrays in the obvious way.
130136
*/
131137
function render(data, label) {
138+
const deltaField = (key) => data[key] ?? data[`${key}+`] ?? data[`${key}-`] ?? data[`*${key}`];
132139
const id = data?._id !== undefined ? ` #${data._id}` : '';
133140
const purpose = data?._purpose ? ` ${data._purpose}` : '';
134141
if (data?.[TITLE] !== undefined) {
135142
return data;
136143
} else if (data?._kind === 'request') {
137-
const input = data['input'] ?? data['input+'] ?? data['input-'] ?? data['*input'] ?? [];
138-
// OpenCode request bodies can mix message wrappers like
139-
// `{role:'user', content:[{type:'input_text', text:'...'}]}` with already-typed items like
140-
// `{type:'function_call', ...}`. Flatten wrapper objects to their `content` items so `payload`
141-
// is always a list of typed entries like `{type:'input_text', ...}` or `{type:'function_call', ...}`.
142-
const payload = input.flatMap(e =>
143-
e && typeof e === 'object' && !Array.isArray(e) && e.type === undefined && Array.isArray(e.content)
144-
? e.content
145-
: e && typeof e === 'object' && !Array.isArray(e) && e.type === undefined && typeof e.content === 'string'
146-
? [{type: 'input_text', text: e.content}]
147-
: [e],
148-
);
149-
const rendered = renderPayload(payload);
144+
const rendered = renderPayload(deltaField('input') ?? deltaField('messages'));
150145
const raw = {...data};
151146
delete raw._kind;
152147
return {
@@ -155,9 +150,7 @@ function render(data, label) {
155150
open: true,
156151
};
157152
} else if (data?._kind === 'response') {
158-
const payload = renderPayload(
159-
data['output'] ?? data['output+'] ?? data['output-'] ?? data['*output'] ?? [],
160-
);
153+
const payload = renderPayload(deltaField('output') ?? deltaField('content'));
161154
const raw = {...data};
162155
delete raw._kind;
163156
return {

0 commit comments

Comments
 (0)