rosetta-ai 1.5.0 โ 1.6.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 +3 -0
- package/dist/index.cjs +79 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +834 -0
- package/dist/index.d.ts +834 -0
- package/dist/index.js +79 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,6 +15,7 @@ Rosetta converts messages between different LLM providers using [**GenAI**](http
|
|
|
15
15
|
- ๐ Full TypeScript support with strict types
|
|
16
16
|
- โ
Runtime validation with Zod schemas
|
|
17
17
|
- ๐พ Preserve provider-specific metadata for lossless round-trips
|
|
18
|
+
- ๐ System message order preservation - system messages retain their original position in conversation when translating between providers
|
|
18
19
|
- ๐ Works in Node.js and browsers
|
|
19
20
|
- ๐ณ Tree-shakeable ESM build
|
|
20
21
|
|
|
@@ -292,6 +293,8 @@ if (result.error) {
|
|
|
292
293
|
- **fromGenAI** = Can translate *to* this provider from GenAI (target)
|
|
293
294
|
- **Separated System** = Provider separates system instructions from messages (use the `system` option if needed)
|
|
294
295
|
|
|
296
|
+
**System message order preservation**: When translating to a provider that separates system instructions (like GenAI), system messages are extracted from the conversation and returned in the `system` field. Rosetta preserves the original position of each system message so that when translating back to a provider with inline system messages (like Promptl or Vercel AI), the system messages are re-inserted at their original positions in the conversation.
|
|
297
|
+
|
|
295
298
|
### Universal Compatibility
|
|
296
299
|
|
|
297
300
|
The **Compat** provider is a universal fallback that handles messages from *any* LLM providerโeven ones not explicitly supported. When you call `translate()` without specifying a source provider, Rosetta tries to match against known provider schemas. If none match, it automatically falls back to Compat, which:
|
package/dist/index.cjs
CHANGED
|
@@ -221,10 +221,15 @@ function extractExtraFields(obj, knownKeys) {
|
|
|
221
221
|
* Reads metadata from an entity, checking both casings (snake_case and camelCase).
|
|
222
222
|
* This is necessary because messages may have been previously translated by Rosetta
|
|
223
223
|
* with different target providers (GenAI uses snake_case, VercelAI uses camelCase).
|
|
224
|
+
* Returns undefined if metadata is not present or is an empty object.
|
|
224
225
|
*/
|
|
225
226
|
function readMetadata(entity) {
|
|
226
227
|
// biome-ignore lint/complexity/useLiteralKeys: required for index signature access
|
|
227
|
-
|
|
228
|
+
const metadata = (entity["_provider_metadata"] ?? entity["_providerMetadata"]);
|
|
229
|
+
// Return undefined for empty objects to prevent empty metadata from propagating
|
|
230
|
+
if (!metadata || Object.keys(metadata).length === 0)
|
|
231
|
+
return undefined;
|
|
232
|
+
return metadata;
|
|
228
233
|
}
|
|
229
234
|
/**
|
|
230
235
|
* Extracts known fields from metadata, checking both casings.
|
|
@@ -240,6 +245,7 @@ function getKnownFields(metadata) {
|
|
|
240
245
|
isError: known?.isError,
|
|
241
246
|
isRefusal: known?.isRefusal,
|
|
242
247
|
originalType: known?.originalType,
|
|
248
|
+
messageIndex: known?.messageIndex,
|
|
243
249
|
};
|
|
244
250
|
}
|
|
245
251
|
/**
|
|
@@ -298,7 +304,8 @@ function storeMetadata(existingMetadata, extraFields, knownFields) {
|
|
|
298
304
|
* @returns The entity with metadata applied according to the mode
|
|
299
305
|
*/
|
|
300
306
|
function applyMetadataMode(entity, metadata, mode, useCamelCase = true) {
|
|
301
|
-
if
|
|
307
|
+
// Return early if no metadata or empty metadata object
|
|
308
|
+
if (!metadata || Object.keys(metadata).length === 0)
|
|
302
309
|
return entity;
|
|
303
310
|
switch (mode) {
|
|
304
311
|
case "strip":
|
|
@@ -1567,6 +1574,8 @@ const KnownFieldsSchema = zod.z
|
|
|
1567
1574
|
isRefusal: zod.z.boolean().optional(),
|
|
1568
1575
|
/** Original type when mapping to a different GenAI type (for lossy conversions) */
|
|
1569
1576
|
originalType: zod.z.string().optional(),
|
|
1577
|
+
/** Original message index for system parts extracted into a separate system array */
|
|
1578
|
+
messageIndex: zod.z.number().optional(),
|
|
1570
1579
|
})
|
|
1571
1580
|
.passthrough();
|
|
1572
1581
|
/**
|
|
@@ -1731,18 +1740,41 @@ const GenAISpecification = {
|
|
|
1731
1740
|
}
|
|
1732
1741
|
const parsedSystem = GenAISystemSchema.optional().parse(system);
|
|
1733
1742
|
if (parsedSystem && parsedSystem.length > 0) {
|
|
1734
|
-
|
|
1743
|
+
const partsByIndex = groupPartsByMessageIndex(parsedSystem);
|
|
1744
|
+
if (partsByIndex) {
|
|
1745
|
+
// Insert system messages at their original positions (lowest index first).
|
|
1746
|
+
// Ascending order is correct because messageIndex refers to the position in the full
|
|
1747
|
+
// conversation (including system messages). As we insert left-to-right, each insertion
|
|
1748
|
+
// naturally shifts subsequent positions to account for the added messages.
|
|
1749
|
+
const sortedIndices = [...partsByIndex.keys()].sort((a, b) => a - b);
|
|
1750
|
+
for (const index of sortedIndices) {
|
|
1751
|
+
// biome-ignore lint/style/noNonNullAssertion: key comes from the map
|
|
1752
|
+
const parts = partsByIndex.get(index);
|
|
1753
|
+
// Clamp to valid range: -1 (no index) and 0 both go to position 0
|
|
1754
|
+
const insertAt = index < 0 ? 0 : Math.min(index, parsedMessages.length);
|
|
1755
|
+
parsedMessages.splice(insertAt, 0, { role: "system", parts });
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
else {
|
|
1759
|
+
// No parts have messageIndex - fall back to prepending all as one system message
|
|
1760
|
+
parsedMessages.unshift({ role: "system", parts: parsedSystem });
|
|
1761
|
+
}
|
|
1735
1762
|
}
|
|
1736
1763
|
return { messages: parsedMessages };
|
|
1737
1764
|
},
|
|
1738
1765
|
fromGenAI({ messages, providerMetadata }) {
|
|
1739
1766
|
const system = [];
|
|
1740
1767
|
const filtered = [];
|
|
1741
|
-
for (
|
|
1768
|
+
for (let i = 0; i < messages.length; i++) {
|
|
1769
|
+
// biome-ignore lint/style/noNonNullAssertion: index is within bounds
|
|
1770
|
+
const message = messages[i];
|
|
1742
1771
|
// Apply metadata mode to message and its parts (handles _partsMetadata restoration)
|
|
1743
1772
|
const transformed = applyMetadataModeToGenAIMessage(message, providerMetadata);
|
|
1744
1773
|
if (message.role === "system") {
|
|
1745
|
-
|
|
1774
|
+
// Store the original message index on each part so the position can be reconstructed
|
|
1775
|
+
for (const part of transformed.parts) {
|
|
1776
|
+
system.push(storeMessageIndexOnPart(part, i));
|
|
1777
|
+
}
|
|
1746
1778
|
}
|
|
1747
1779
|
else {
|
|
1748
1780
|
filtered.push(transformed);
|
|
@@ -1774,6 +1806,48 @@ function applyMetadataModeToGenAIMessage(message, mode) {
|
|
|
1774
1806
|
const baseWithParts = { ...baseMessage, parts: transformedParts };
|
|
1775
1807
|
return applyMetadataMode(baseWithParts, msgMetadata, mode, false);
|
|
1776
1808
|
}
|
|
1809
|
+
/** Store the original message index on a system part via _provider_metadata._known_fields. */
|
|
1810
|
+
function storeMessageIndexOnPart(part, index) {
|
|
1811
|
+
const existingMetadata = readMetadata(part);
|
|
1812
|
+
const metadata = storeMetadata(existingMetadata ?? {}, {}, { messageIndex: index });
|
|
1813
|
+
if (!metadata)
|
|
1814
|
+
return part;
|
|
1815
|
+
return { ...part, _provider_metadata: metadata };
|
|
1816
|
+
}
|
|
1817
|
+
/**
|
|
1818
|
+
* Groups system parts by their messageIndex known field.
|
|
1819
|
+
* Returns a Map from index to parts array, or null if no parts have a messageIndex.
|
|
1820
|
+
*/
|
|
1821
|
+
function groupPartsByMessageIndex(parts) {
|
|
1822
|
+
let hasAnyIndex = false;
|
|
1823
|
+
const grouped = new Map();
|
|
1824
|
+
for (const part of parts) {
|
|
1825
|
+
const metadata = readMetadata(part);
|
|
1826
|
+
const known = getKnownFields(metadata);
|
|
1827
|
+
if (known.messageIndex !== undefined) {
|
|
1828
|
+
hasAnyIndex = true;
|
|
1829
|
+
const idx = known.messageIndex;
|
|
1830
|
+
const existing = grouped.get(idx);
|
|
1831
|
+
if (existing) {
|
|
1832
|
+
existing.push(part);
|
|
1833
|
+
}
|
|
1834
|
+
else {
|
|
1835
|
+
grouped.set(idx, [part]);
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
else {
|
|
1839
|
+
// Parts without an index go into a group keyed by -1 (will be prepended)
|
|
1840
|
+
const existing = grouped.get(-1);
|
|
1841
|
+
if (existing) {
|
|
1842
|
+
existing.push(part);
|
|
1843
|
+
}
|
|
1844
|
+
else {
|
|
1845
|
+
grouped.set(-1, [part]);
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
return hasAnyIndex ? grouped : null;
|
|
1850
|
+
}
|
|
1777
1851
|
|
|
1778
1852
|
/**
|
|
1779
1853
|
* Google Gemini API Schemas
|