Skip to content

Commit a42a700

Browse files
authored
feat: add Anthropic Claude message format helpers, fix some types and change names (#105)
2 parents 1faa625 + 21e10c8 commit a42a700

File tree

294 files changed

+4090
-1151
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

294 files changed

+4090
-1151
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,4 @@
3333
/examples/nextjs-example/package-lock.json
3434
/examples/nextjs-example/package.lock.json
3535
/temp
36+
.codanna/**

.speakeasy/gen.lock

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

.speakeasy/gen.yaml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,15 @@ generation:
2525
schemas:
2626
allOfMergeStrategy: shallowMerge
2727
requestBodyFieldName: ""
28-
persistentEdits: {}
28+
persistentEdits:
29+
enabled: "true"
2930
tests:
3031
generateTests: false
3132
generateNewTests: true
3233
skipResponseBodyAssertions: false
3334
preApplyUnionDiscriminators: true
3435
typescript:
35-
version: 0.3.2
36+
version: 0.3.7
3637
acceptHeaderEnum: false
3738
additionalDependencies:
3839
dependencies: {}

.speakeasy/workflow.lock

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@ targets:
1313
sourceNamespace: open-router-chat-completions-api
1414
sourceRevisionDigest: sha256:9825a9d9dd018739535efcfcd22e5cad96efd1ce991aeaa4b4e7fe5b9d40f0fa
1515
sourceBlobDigest: sha256:6f9ef0c822dc240348641c51d5824e49681a2e3ac0132d2c19cd2abb3bcfdd03
16-
codeSamplesNamespace: open-router-chat-completions-api-typescript-code-samples
17-
codeSamplesRevisionDigest: sha256:4ce44ce842fb432db3bc01ef54b2728d4120c57a8dfc887399297d53dd79a774
1816
workflow:
1917
workflowVersion: 1.0.0
2018
speakeasyVersion: 1.680.0
Lines changed: 350 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,350 @@
1+
import dotenv from "dotenv";
2+
3+
dotenv.config();
4+
5+
/**
6+
* Example demonstrating a multi-turn conversation with images and tool use
7+
* using Claude-style message format with the anthropic-compat helper functions.
8+
*
9+
* This example shows how to:
10+
* 1. Send messages with image inputs (both URL and base64)
11+
* 2. Handle tool calls and tool results in conversation history
12+
* 3. Convert between Claude format and OpenResponses format across multiple turns
13+
* 4. Build a 3-turn conversation with complex multimodal interactions
14+
*
15+
* The conversation flow:
16+
* - Turn 1: User sends an image and asks about it
17+
* - Turn 2: Model uses tools to analyze the image, user provides tool results
18+
* - Turn 3: User asks follow-up question based on tool results
19+
*
20+
* To run this example from the examples directory with Bun:
21+
* bun run anthropic-multimodal-tools.example.ts
22+
*/
23+
24+
import type { ClaudeMessageParam } from "../src/models/claude-message.js";
25+
import {
26+
OpenRouter,
27+
fromClaudeMessages,
28+
toClaudeMessage,
29+
ToolType,
30+
} from "../src/index.js";
31+
import { z } from "zod/v4";
32+
33+
if (!process.env["OPENROUTER_API_KEY"]) {
34+
throw new Error("Missing OPENROUTER_API_KEY environment variable");
35+
}
36+
37+
const openRouter = new OpenRouter({
38+
apiKey: process.env["OPENROUTER_API_KEY"] ?? "",
39+
});
40+
41+
// Mock tool definition for image analysis
42+
const tools = [
43+
{
44+
type: ToolType.Function,
45+
function: {
46+
name: "analyze_image_details",
47+
description: "Analyzes detailed visual features of an image including colors, objects, and composition",
48+
inputSchema: z.object({
49+
image_id: z.string().describe("The ID of the image to analyze"),
50+
analysis_type: z.enum(["color_palette", "object_detection", "scene_classification"]).describe("Type of analysis to perform"),
51+
}),
52+
outputSchema: z.object({
53+
colors: z.array(z.string()).optional(),
54+
dominant_objects: z.array(z.string()).optional(),
55+
composition: z.string().optional(),
56+
lighting: z.string().optional(),
57+
}),
58+
},
59+
},
60+
{
61+
type: ToolType.Function,
62+
function: {
63+
name: "get_image_metadata",
64+
description: "Retrieves metadata about an image such as dimensions, format, and creation date",
65+
inputSchema: z.object({
66+
image_id: z.string().describe("The ID of the image"),
67+
}),
68+
outputSchema: z.object({
69+
width: z.number(),
70+
height: z.number(),
71+
format: z.string(),
72+
created: z.string(),
73+
file_size: z.string(),
74+
}),
75+
},
76+
},
77+
];
78+
79+
async function multiTurnMultimodalConversation() {
80+
// Using GPT-5 as requested
81+
const model = "openai/gpt-5";
82+
83+
// Initialize message history with Claude-style message format
84+
// Turn 1: User sends an image with a question
85+
const messages: ClaudeMessageParam[] = [
86+
{
87+
role: "user",
88+
content: [
89+
{
90+
type: "text",
91+
text: "I have this image of a sunset landscape. Can you analyze its visual features?",
92+
},
93+
{
94+
type: "image",
95+
source: {
96+
type: "url",
97+
url: "https://images.unsplash.com/photo-1506905925346-21bda4d32df4",
98+
},
99+
},
100+
],
101+
},
102+
];
103+
104+
console.log("=== Turn 1 ===");
105+
console.log("User: I have this image of a sunset landscape. Can you analyze its visual features?");
106+
console.log("User: [Image URL: https://images.unsplash.com/photo-1506905925346-21bda4d32df4]");
107+
console.log();
108+
109+
// First turn - convert Claude messages to OpenResponses format and call with tools
110+
const result1 = await openRouter.callModel({
111+
model,
112+
input: fromClaudeMessages(messages),
113+
tools,
114+
toolChoice: "auto",
115+
});
116+
117+
// Get the response and convert back to Claude format
118+
const response1 = await result1.getResponse();
119+
const claudeMessage1 = toClaudeMessage(response1);
120+
121+
console.log("Assistant response:");
122+
console.log("Stop reason:", claudeMessage1.stop_reason);
123+
124+
// Extract content and tool calls
125+
const textContent1: string[] = [];
126+
const toolCalls1: Array<{ id: string; name: string; input: Record<string, unknown> }> = [];
127+
128+
for (const block of claudeMessage1.content) {
129+
if (block.type === "text") {
130+
textContent1.push(block.text);
131+
} else if (block.type === "tool_use") {
132+
toolCalls1.push({
133+
id: block.id,
134+
name: block.name,
135+
input: block.input,
136+
});
137+
}
138+
}
139+
140+
if (textContent1.length > 0) {
141+
console.log("Text:", textContent1.join("\n"));
142+
}
143+
144+
if (toolCalls1.length > 0) {
145+
console.log("\nTool calls made:");
146+
for (const call of toolCalls1) {
147+
console.log(`- ${call.name} (${call.id})`);
148+
console.log(` Arguments:`, JSON.stringify(call.input, null, 2));
149+
}
150+
}
151+
152+
console.log();
153+
154+
// Add assistant response to history (as Claude-style message)
155+
messages.push({
156+
role: "assistant",
157+
content: claudeMessage1.content.map(block => {
158+
if (block.type === "text") {
159+
return { type: "text" as const, text: block.text };
160+
} else if (block.type === "tool_use") {
161+
return {
162+
type: "tool_use" as const,
163+
id: block.id,
164+
name: block.name,
165+
input: block.input,
166+
};
167+
}
168+
// Handle other block types if needed
169+
return { type: "text" as const, text: "" };
170+
}).filter(block => block.type !== "text" || block.text !== ""),
171+
});
172+
173+
// Turn 2: User provides tool results with an image result
174+
console.log("=== Turn 2 ===");
175+
console.log("User provides tool results:");
176+
177+
const toolResults: ClaudeMessageParam = {
178+
role: "user",
179+
content: toolCalls1.map((call, idx) => {
180+
if (call.name === "analyze_image_details") {
181+
// Simulate a tool result with text
182+
console.log(`Tool result for ${call.id}:`);
183+
console.log(" Analysis: The image shows warm orange and pink hues typical of sunset.");
184+
185+
return {
186+
type: "tool_result" as const,
187+
tool_use_id: call.id,
188+
content: JSON.stringify({
189+
colors: ["#FF6B35", "#F7931E", "#FDC830", "#F37335"],
190+
dominant_objects: ["sky", "clouds", "mountains", "horizon"],
191+
composition: "rule_of_thirds",
192+
lighting: "golden_hour",
193+
}),
194+
};
195+
} else if (call.name === "get_image_metadata") {
196+
console.log(`Tool result for ${call.id}:`);
197+
console.log(" Metadata: 3840x2160, JPEG format");
198+
199+
return {
200+
type: "tool_result" as const,
201+
tool_use_id: call.id,
202+
content: JSON.stringify({
203+
width: 3840,
204+
height: 2160,
205+
format: "JPEG",
206+
created: "2023-06-15T18:45:00Z",
207+
file_size: "2.4MB",
208+
}),
209+
};
210+
}
211+
return {
212+
type: "tool_result" as const,
213+
tool_use_id: call.id,
214+
content: "Tool execution successful",
215+
};
216+
}),
217+
};
218+
219+
messages.push(toolResults);
220+
console.log();
221+
222+
// Second API call with tool results
223+
const result2 = await openRouter.callModel({
224+
model,
225+
input: fromClaudeMessages(messages),
226+
tools,
227+
});
228+
229+
const response2 = await result2.getResponse();
230+
const claudeMessage2 = toClaudeMessage(response2);
231+
232+
console.log("Assistant response:");
233+
const textContent2: string[] = [];
234+
235+
for (const block of claudeMessage2.content) {
236+
if (block.type === "text") {
237+
textContent2.push(block.text);
238+
}
239+
}
240+
241+
console.log(textContent2.join("\n"));
242+
console.log();
243+
244+
// Add assistant response to history
245+
messages.push({
246+
role: "assistant",
247+
content: textContent2.join("\n"),
248+
});
249+
250+
// Turn 3: User asks a follow-up question with another image using base64
251+
console.log("=== Turn 3 ===");
252+
253+
// Create a simple base64 encoded 1x1 pixel PNG (for demonstration)
254+
const base64Image = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==";
255+
256+
messages.push({
257+
role: "user",
258+
content: [
259+
{
260+
type: "text",
261+
text: "Great! Now I have another image here. How would you compare these two images in terms of color composition?",
262+
},
263+
{
264+
type: "image",
265+
source: {
266+
type: "base64",
267+
media_type: "image/png",
268+
data: base64Image,
269+
},
270+
},
271+
],
272+
});
273+
274+
// Third API call
275+
const result3 = await openRouter.callModel({
276+
model,
277+
input: fromClaudeMessages(messages),
278+
tools,
279+
});
280+
281+
const response3 = await result3.getResponse();
282+
const claudeMessage3 = toClaudeMessage(response3);
283+
284+
console.log("Assistant response:");
285+
const textContent3: string[] = [];
286+
const toolCalls3: Array<{ id: string; name: string; input: Record<string, unknown> }> = [];
287+
288+
for (const block of claudeMessage3.content) {
289+
if (block.type === "text") {
290+
textContent3.push(block.text);
291+
} else if (block.type === "tool_use") {
292+
toolCalls3.push({
293+
id: block.id,
294+
name: block.name,
295+
input: block.input,
296+
});
297+
}
298+
}
299+
300+
console.log(textContent3.join("\n"));
301+
302+
if (toolCalls3.length > 0) {
303+
console.log("\nTool calls made:");
304+
for (const call of toolCalls3) {
305+
console.log(`- ${call.name} (${call.id})`);
306+
console.log(` Arguments:`, JSON.stringify(call.input, null, 2));
307+
}
308+
}
309+
310+
console.log();
311+
312+
// Add final assistant response to history
313+
messages.push({
314+
role: "assistant",
315+
content: claudeMessage3.content.map(block => {
316+
if (block.type === "text") {
317+
return { type: "text" as const, text: block.text };
318+
} else if (block.type === "tool_use") {
319+
return {
320+
type: "tool_use" as const,
321+
id: block.id,
322+
name: block.name,
323+
input: block.input,
324+
};
325+
}
326+
return { type: "text" as const, text: "" };
327+
}).filter(block => block.type !== "text" || block.text !== ""),
328+
});
329+
330+
console.log("=== Conversation Complete ===");
331+
console.log(`Total messages in history: ${messages.length}`);
332+
333+
// Show the final Claude message structure
334+
console.log("\n=== Final Claude Message Structure ===");
335+
console.log("Stop reason:", claudeMessage3.stop_reason);
336+
console.log("Model:", claudeMessage3.model);
337+
console.log("Usage:", claudeMessage3.usage);
338+
339+
return messages;
340+
}
341+
342+
async function main() {
343+
try {
344+
await multiTurnMultimodalConversation();
345+
} catch (error) {
346+
console.error("Error:", error);
347+
}
348+
}
349+
350+
main();

0 commit comments

Comments
 (0)