rosetta-ai 1.0.1 → 1.2.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/README.md CHANGED
@@ -2,7 +2,9 @@
2
2
 
3
3
  The translation layer for LLM provider messages.
4
4
 
5
- Rosetta converts messages between different LLM providers using a standardized intermediate format (GenAI). Just pass in messages from any provider—OpenAI, Anthropic, Google, or even custom formats—and get consistent output. No manual mapping required.
5
+ Rosetta converts messages between different LLM providers using [**GenAI**](https://opentelemetry.io/docs/specs/semconv/registry/attributes/gen-ai/), a standardized intermediate format. Just pass in messages from any provider—OpenAI, Anthropic, Google, or even custom formats—and get consistent output. No manual mapping required.
6
+
7
+ > Rosetta was made by [Latitude](https://latitude.so?utm_source=github&utm_medium=oss&utm_campaign=rosetta_ai) as an effort to standardize the observability layer for any LLM application!
6
8
 
7
9
  ## Features
8
10
 
@@ -29,92 +31,265 @@ yarn add rosetta-ai
29
31
  ## Quick Start
30
32
 
31
33
  ```typescript
32
- import { translate, Provider } from "rosetta-ai";
34
+ import { translate } from "rosetta-ai";
33
35
 
34
- // Your messages
35
- const messages = [
36
- { role: "user", parts: [{ type: "text", content: "Hello!" }] },
36
+ // Translate any LLM messages - provider is auto-detected
37
+ const openAIMessages = [
38
+ { role: "system", content: "You are a helpful assistant." },
39
+ { role: "user", content: "Hello!" },
40
+ { role: "assistant", content: "Hi there! How can I help you today?" },
37
41
  ];
38
42
 
39
- // Convert to GenAI (intermediate format) - auto-infers source
40
- const { messages: genaiMessages } = translate(messages);
43
+ const { messages, system } = translate(openAIMessages);
44
+ // messages: GenAI format messages (user + assistant)
45
+ // system: extracted system instructions
46
+ ```
41
47
 
42
- // Specify source provider explicitly
43
- const { messages: result } = translate(messages, {
44
- from: Provider.GenAI,
45
- to: Provider.GenAI,
46
- });
48
+ Works with messages from any provider:
49
+
50
+ ```typescript
51
+ // OpenAI Chat Completions
52
+ const openAI = [{ role: "user", content: "Hello" }];
53
+ translate(openAI); // Just works
54
+
55
+ // Anthropic
56
+ const anthropic = [{ role: "user", content: [{ type: "text", text: "Hello" }] }];
57
+ translate(anthropic); // Just works
58
+
59
+ // Vercel AI SDK
60
+ const vercelAI = [{ role: "user", content: "Hello" }];
61
+ translate(vercelAI); // Just works
62
+
63
+ // More providers...
64
+
65
+ // Unknown provider? Also works (uses Compat fallback)
66
+ const unknown = [{ role: "user", content: "Hello" }];
67
+ translate(unknown); // Still works
47
68
  ```
48
69
 
49
70
  ## API
50
71
 
51
- ### translate / safeTranslate
72
+ ### translate
73
+
74
+ The main function for translating messages between providers.
52
75
 
53
76
  ```typescript
54
- import { translate, safeTranslate, Provider } from "rosetta-ai";
77
+ import { translate, Provider } from "rosetta-ai";
55
78
 
56
- // translate throws on error
57
79
  const { messages, system } = translate(inputMessages, {
58
- from: Provider.GenAI, // Source provider (optional, auto-inferred if not provided)
59
- to: Provider.GenAI, // Target provider (optional, defaults to GenAI)
60
- system: systemInstructions, // Separated system instructions (optional)
61
- direction: "input", // "input" (default) or "output" - affects role interpretation (e.g. "user" vs "assistant")
80
+ from: Provider.OpenAICompletions, // Source provider (optional, auto-detected if omitted)
81
+ to: Provider.GenAI, // Target provider (optional, defaults to GenAI)
82
+ system: "You are helpful", // Separated system instructions (optional)
83
+ direction: "input", // "input" (default) or "output"
62
84
  });
85
+ ```
63
86
 
64
- // safeTranslate returns error instead of throwing
65
- const result = safeTranslate(inputMessages);
87
+ **Options:**
88
+
89
+ | Option | Type | Default | Description |
90
+ |--------|------|---------|-------------|
91
+ | `from` | `Provider` | auto-detected | Source provider format |
92
+ | `to` | `Provider` | `Provider.GenAI` | Target provider format |
93
+ | `system` | `string \| object \| object[]` | - | System instructions (for providers that separate them) |
94
+ | `direction` | `"input" \| "output"` | `"input"` | Affects role interpretation when translating strings |
95
+
96
+ **Returns:** `{ messages, system? }` - translated messages and optional system instructions
97
+
98
+ ### safeTranslate
99
+
100
+ Same as `translate`, but returns an error object instead of throwing.
101
+
102
+ ```typescript
103
+ import { safeTranslate } from "rosetta-ai";
104
+
105
+ const result = safeTranslate(messages, options);
66
106
 
67
107
  if (result.error) {
68
- console.error("Translation failed:", result.error.message);
108
+ // Handle error: result.error is Error
69
109
  } else {
70
- console.log("Translated:", result.messages);
110
+ // Use result.messages (properly typed)
71
111
  }
72
112
  ```
73
113
 
114
+ ### Translator Class
115
+
116
+ For advanced configuration, create a `Translator` instance:
117
+
118
+ ```typescript
119
+ import { Translator, Provider } from "rosetta-ai";
120
+
121
+ const translator = new Translator({
122
+ // Custom priority order for provider auto-detection
123
+ inferPriority: [Provider.OpenAICompletions, Provider.Anthropic, Provider.GenAI],
124
+
125
+ // Filter out empty messages during translation (default: false)
126
+ filterEmptyMessages: true,
127
+ });
128
+
129
+ const { messages } = translator.translate(inputMessages);
130
+ const safeResult = translator.safeTranslate(inputMessages);
131
+ ```
132
+
133
+ **Configuration Options:**
134
+
135
+ | Option | Type | Default | Description |
136
+ |--------|------|---------|-------------|
137
+ | `inferPriority` | `Provider[]` | `DEFAULT_INFER_PRIORITY` | Priority order for provider auto-detection |
138
+ | `filterEmptyMessages` | `boolean` | `false` | Remove empty messages (no parts, or only empty text) during translation |
139
+
74
140
  ### Input Flexibility
75
141
 
76
- Messages and system instructions accept flexible input formats:
142
+ Messages and system instructions accept flexible formats:
77
143
 
78
144
  ```typescript
79
- // Messages: string or array of provider messages
80
- translate("Hello!"); // Simple string
81
- translate([{ role: "user", content: "Hello!" }]); // Provider message array
145
+ // Messages: string or array
146
+ translate("Hello!"); // String → single message
147
+ translate([{ role: "user", content: "Hello!" }]); // Array of provider messages
82
148
 
83
- // System: string, single object, or array
149
+ // System: string, object, or array
84
150
  translate(messages, { system: "You are helpful" });
85
151
  translate(messages, { system: { type: "text", content: "Be helpful" } });
86
- translate(messages, { system: [{ type: "text", content: "Instructions" }] });
152
+ translate(messages, { system: [{ type: "text", content: "Part 1" }, { type: "text", content: "Part 2" }] });
87
153
  ```
88
154
 
89
- Each provider validates messages with its own Zod schema at runtime.
155
+ ## Common Use Cases
90
156
 
91
- ### Translator Class
157
+ ### Translate API responses for storage or display
158
+
159
+ ```typescript
160
+ import OpenAI from "openai";
161
+ import { translate, Provider } from "rosetta-ai";
162
+
163
+ const openai = new OpenAI();
164
+ const completion = await openai.chat.completions.create({
165
+ model: "gpt-4o",
166
+ messages: [{ role: "user", content: "What's the weather?" }],
167
+ });
168
+
169
+ // Translate OpenAI response to unified GenAI format
170
+ const { messages } = translate([completion.choices[0].message], {
171
+ from: Provider.OpenAICompletions,
172
+ });
173
+
174
+ // Now you have a consistent format regardless of which provider you used
175
+ console.log(messages[0].parts[0]); // { type: "text", content: "..." }
176
+ ```
92
177
 
93
- For advanced configuration, use the `Translator` class:
178
+ ### Cross-provider translation
94
179
 
95
180
  ```typescript
96
- import { Translator, Provider } from "rosetta-ai";
181
+ import { translate, Provider } from "rosetta-ai";
97
182
 
98
- const translator = new Translator({
99
- // Custom priority order for provider inference
100
- inferPriority: [Provider.GenAI],
183
+ // Translate OpenAI messages to Vercel AI SDK format
184
+ const openAIMessages = [
185
+ { role: "system", content: "You are helpful." },
186
+ { role: "user", content: "Hello!" },
187
+ ];
188
+
189
+ const { messages } = translate(openAIMessages, {
190
+ from: Provider.OpenAICompletions,
191
+ to: Provider.VercelAI,
101
192
  });
193
+ // Result: Vercel AI SDK compatible messages
194
+ ```
102
195
 
103
- const { messages } = translator.translate(inputMessages);
196
+ ### Handle tool calls across providers
197
+
198
+ ```typescript
199
+ import { translate, Provider } from "rosetta-ai";
200
+
201
+ // OpenAI tool call format
202
+ const openAIWithToolCall = [
203
+ {
204
+ role: "assistant",
205
+ content: null,
206
+ tool_calls: [{
207
+ id: "call_abc123",
208
+ type: "function",
209
+ function: { name: "get_weather", arguments: '{"location":"Paris"}' },
210
+ }],
211
+ },
212
+ {
213
+ role: "tool",
214
+ tool_call_id: "call_abc123",
215
+ content: '{"temp": 20}',
216
+ },
217
+ ];
218
+
219
+ // Translates to unified GenAI format with tool_call and tool_call_response parts
220
+ const { messages } = translate(openAIWithToolCall, {
221
+ from: Provider.OpenAICompletions,
222
+ });
223
+
224
+ // Tool call part
225
+ messages[0].parts[0]; // { type: "tool_call", name: "get_weather", arguments: { location: "Paris" }, ... }
226
+
227
+ // Tool response part
228
+ messages[1].parts[0]; // { type: "tool_call_response", call_id: "call_abc123", content: {...}, ... }
229
+ ```
230
+
231
+ ### Translate multimodal content
232
+
233
+ ```typescript
234
+ import { translate, Provider } from "rosetta-ai";
235
+
236
+ const anthropicWithImage = [
237
+ {
238
+ role: "user",
239
+ content: [
240
+ { type: "text", text: "What's in this image?" },
241
+ {
242
+ type: "image",
243
+ source: {
244
+ type: "base64",
245
+ media_type: "image/png",
246
+ data: "iVBORw0KGgo...",
247
+ },
248
+ },
249
+ ],
250
+ },
251
+ ];
252
+
253
+ const { messages } = translate(anthropicWithImage, {
254
+ from: Provider.Anthropic,
255
+ });
256
+
257
+ // Image converted to blob part
258
+ messages[0].parts[1]; // { type: "blob", modality: "image", mime_type: "image/png", content: "..." }
259
+ ```
260
+
261
+ ### Safe translation with error handling
262
+
263
+ ```typescript
264
+ import { safeTranslate } from "rosetta-ai";
265
+
266
+ const result = safeTranslate(unknownMessages);
267
+
268
+ if (result.error) {
269
+ console.error("Translation failed:", result.error.message);
270
+ } else {
271
+ console.log("Translated:", result.messages);
272
+ }
104
273
  ```
105
274
 
275
+
276
+
106
277
  ## Supported Providers
107
278
 
108
- | Provider | Status | toGenAI | fromGenAI |
109
- | ------------------ | ------------ | ------- | --------- |
110
- | GenAI | ✅ Available | ✅ | |
111
- | Promptl | ✅ Available | ✅ | |
112
- | VercelAI | ✅ Available | ✅ | |
113
- | OpenAI Completions | ✅ Available | | - |
114
- | OpenAI Responses | ✅ Available | | - |
115
- | Anthropic | ✅ Available | | - |
116
- | Google Gemini | ✅ Available | | - |
117
- | Compat | ✅ Available | | - |
279
+ | Provider | toGenAI | fromGenAI | Separated System | Description |
280
+ |----------|---------|-----------|-----------------|-------------|
281
+ | GenAI | ✅ | ✅ | Optional | Intermediate format (default target) |
282
+ | Promptl | ✅ | ✅ | - | [promptl-ai](https://github.com/latitude-dev/promptl) format |
283
+ | Vercel AI | ✅ | ✅ | - | Vercel AI SDK messages |
284
+ | OpenAI Completions | ✅ | - | - | Chat Completions API |
285
+ | OpenAI Responses | ✅ | - | - | Responses API |
286
+ | Anthropic | ✅ | - | Yes | Messages API |
287
+ | Google Gemini | ✅ | - | Yes | GenerateContent API |
288
+ | Compat | ✅ | - | Optional | Universal fallback |
289
+
290
+ - **toGenAI** = Can translate *from* this provider to GenAI (source)
291
+ - **fromGenAI** = Can translate *to* this provider from GenAI (target)
292
+ - **Separated System** = Provider separates system instructions from messages (use the `system` option if needed)
118
293
 
119
294
  ### Universal Compatibility
120
295
 
@@ -127,29 +302,31 @@ The **Compat** provider is a universal fallback that handles messages from *any*
127
302
 
128
303
  ```typescript
129
304
  // Works with any provider - no need to specify the source
130
- const weirdMessages = [
305
+ const messages = [
131
306
  { role: "user", content: "Hello" },
132
- { role: "assistant", tool_calls: [{ id: "1", function: { name: "search", arguments: "{}" } }] },
307
+ { role: "assistant", toolCalls: [{ id: "1", function: { name: "search", arguments: "{}" } }] },
133
308
  ];
134
309
 
135
- const { messages } = translate(weirdMessages); // Just works™
310
+ const { messages: translated } = translate(messages); // Auto-detected and translated
136
311
  ```
137
312
 
138
313
  More providers will be added. See [AGENTS.md](./AGENTS.md) for contribution guidelines.
139
314
 
140
315
  ## GenAI Format
141
316
 
142
- GenAI is the intermediate format used for translation. It provides a unified representation of LLM messages:
317
+ GenAI is the intermediate format used for translation, inspired by the [OpenTelemetry GenAI semantic conventions](https://opentelemetry.io/docs/specs/semconv/registry/attributes/gen-ai/). It provides a unified representation of LLM messages across all providers:
143
318
 
144
319
  ```typescript
145
- import type { GenAIMessage, GenAIPart, GenAISystem } from "rosetta-ai";
320
+ import type { GenAIMessage, GenAISystem } from "rosetta-ai";
146
321
 
147
322
  const message: GenAIMessage = {
148
- role: "user",
149
- parts: [
150
- { type: "text", content: "Hello!" },
151
- { type: "blob", modality: "image", content: "base64...", mime_type: "image/png" },
323
+ role: "user", // "user" | "assistant" | "system" | "tool" | string
324
+ parts: [ // Array of content parts
325
+ { type: "text", content: "What's in this image?" },
326
+ { type: "uri", uri: "https://example.com/cat.jpg", modality: "image" },
152
327
  ],
328
+ name: "Alice", // Optional: participant name
329
+ finish_reason: "stop", // Optional: why the model stopped
153
330
  };
154
331
 
155
332
  const system: GenAISystem = [
@@ -159,32 +336,90 @@ const system: GenAISystem = [
159
336
 
160
337
  ### Part Types
161
338
 
162
- - `text` - Plain text content
163
- - `blob` - Binary data (base64 encoded)
164
- - `file` - File reference by ID
165
- - `uri` - URI reference
166
- - `reasoning` - Model reasoning/thinking
167
- - `tool_call` - Tool call request
168
- - `tool_call_response` - Tool call result
169
- - `generic` - Custom/extensible part type
339
+ | Type | Description | Key Fields |
340
+ |------|-------------|------------|
341
+ | `text` | Plain text content | `content` |
342
+ | `blob` | Binary data (base64) | `content`, `mime_type`, `modality` |
343
+ | `file` | File reference by ID | `file_id`, `modality` |
344
+ | `uri` | URL reference | `uri`, `modality` |
345
+ | `reasoning` | Model thinking/reasoning | `content` |
346
+ | `tool_call` | Tool/function call request | `call_id`, `name`, `arguments` |
347
+ | `tool_call_response` | Tool/function result | `call_id`, `content` |
348
+ | `generic` | Custom/extensible type | `content`, any additional fields |
170
349
 
171
350
  ### Provider Metadata
172
351
 
173
- All GenAI entities support `_provider_metadata` to preserve provider-specific data:
352
+ All GenAI entities support `_provider_metadata` to preserve data during translation. The metadata has two types of fields:
353
+
354
+ 1. **Root-level shared fields** (camelCase): Cross-provider semantic data accessible to any target provider
355
+ 2. **Provider-specific slots** (snake_case): Data for same-provider round-trips only
174
356
 
175
357
  ```typescript
176
358
  const message: GenAIMessage = {
177
- role: "assistant",
178
- parts: [{ type: "text", content: "Hello!" }],
179
- _provider_metadata: {
180
- genai: { custom: "data" },
181
- },
359
+ role: "tool",
360
+ parts: [{
361
+ type: "tool_call_response",
362
+ id: "call_123",
363
+ response: "Error occurred",
364
+ _provider_metadata: {
365
+ // Root-level shared fields - any target provider can read these
366
+ toolName: "get_weather", // Tool name (GenAI schema doesn't include it)
367
+ isError: true, // Error indicator
368
+
369
+ // Provider-specific slot - only for same-provider round-trips
370
+ openai_completions: { annotations: [...] },
371
+ },
372
+ }],
182
373
  };
183
374
  ```
184
375
 
376
+ **Shared fields**: `toolName`, `isError`, `isRefusal`, `originalType`
377
+
378
+ **Provider slots**: `openai_completions`, `openai_responses`, `anthropic`, `google`, `vercel_ai`, `promptl`, `compat`
379
+
380
+ This design enables lossless cross-provider translation while keeping providers isolated from each other.
381
+
382
+ ## TypeScript Support
383
+
384
+ All types are exported for type-safe usage:
385
+
386
+ ```typescript
387
+ import {
388
+ // Core types
389
+ type GenAIMessage,
390
+ type GenAIPart,
391
+ type GenAISystem,
392
+
393
+ // API types
394
+ type TranslateOptions,
395
+ type TranslateResult,
396
+
397
+ // Provider types
398
+ Provider,
399
+ type ProviderMessage,
400
+ type ProviderSystem,
401
+ } from "rosetta-ai";
402
+
403
+ // Type-safe translation
404
+ const result: TranslateResult<Provider.GenAI> = translate(messages);
405
+
406
+ // Access provider-specific message types
407
+ type OpenAIMsg = ProviderMessage<Provider.OpenAICompletions>;
408
+ ```
409
+
185
410
  ## Examples
186
411
 
187
- Check out the [examples](./examples) folder for usage examples (requires building the package first).
412
+ The [examples](./examples) folder contains E2E tests demonstrating real-world usage with actual provider SDKs:
413
+
414
+ ```bash
415
+ cd examples
416
+ pnpm install
417
+ pnpm test # Runs tests (imports directly from src, no build needed)
418
+ ```
419
+
420
+ Tests include:
421
+ - **Real API calls** (when API keys are set) - validates against actual provider responses
422
+ - **Hardcoded messages** - runs without API keys for fast iteration
188
423
 
189
424
  ## Development
190
425