rosetta-ai 1.4.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 +9 -0
- package/dist/index.cjs +224 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5282 -22
- package/dist/index.d.ts +5282 -22
- package/dist/index.js +224 -20
- 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:
|
|
@@ -368,6 +371,10 @@ const message: GenAIMessage = {
|
|
|
368
371
|
toolName: "get_weather", // Tool name (GenAI schema doesn't include it)
|
|
369
372
|
isError: true, // Error indicator
|
|
370
373
|
},
|
|
374
|
+
// Parts metadata - collapsed part-level metadata (for providers with string-only content)
|
|
375
|
+
_partsMetadata: {
|
|
376
|
+
_promptlSourceMap: [...], // Part metadata moved to message level
|
|
377
|
+
},
|
|
371
378
|
// Extra fields - any other provider-specific data
|
|
372
379
|
annotations: [...],
|
|
373
380
|
},
|
|
@@ -375,6 +382,8 @@ const message: GenAIMessage = {
|
|
|
375
382
|
};
|
|
376
383
|
```
|
|
377
384
|
|
|
385
|
+
**Note on `_partsMetadata`**: Some providers require string content for certain message types (e.g., VercelAI system messages). When translating to these providers, part-level metadata is collected and stored in `_partsMetadata` at the message level. When translating back to a provider that supports structured content, this metadata is automatically restored to the first content part. **Important**: In passthrough mode, if the target provider doesn't support structured content (like VercelAI system messages), part-level metadata stored in `_partsMetadata` will be lost. Use **preserve** mode if you need to retain this metadata through round-trips.
|
|
386
|
+
|
|
378
387
|
#### Provider Metadata Mode
|
|
379
388
|
|
|
380
389
|
The `providerMetadata` option controls how metadata (extra fields) is handled in the output.
|
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,8 +245,23 @@ 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
|
}
|
|
251
|
+
/**
|
|
252
|
+
* Extracts parts metadata from message metadata, checking both casings.
|
|
253
|
+
* Parts metadata is used to preserve part-level metadata when converting to providers
|
|
254
|
+
* that require string content (e.g., VercelAI system messages).
|
|
255
|
+
*
|
|
256
|
+
* @param metadata - The message metadata to extract from
|
|
257
|
+
* @returns The parts metadata object, or undefined if not present
|
|
258
|
+
*/
|
|
259
|
+
function getPartsMetadata(metadata) {
|
|
260
|
+
if (!metadata)
|
|
261
|
+
return undefined;
|
|
262
|
+
// biome-ignore lint/complexity/useLiteralKeys: required for index signature access
|
|
263
|
+
return (metadata["_parts_metadata"] ?? metadata["_partsMetadata"]);
|
|
264
|
+
}
|
|
245
265
|
/**
|
|
246
266
|
* Stores metadata on a GenAI entity, merging with any existing metadata.
|
|
247
267
|
* This is used in toGenAI to build the _provider_metadata field.
|
|
@@ -284,7 +304,8 @@ function storeMetadata(existingMetadata, extraFields, knownFields) {
|
|
|
284
304
|
* @returns The entity with metadata applied according to the mode
|
|
285
305
|
*/
|
|
286
306
|
function applyMetadataMode(entity, metadata, mode, useCamelCase = true) {
|
|
287
|
-
if
|
|
307
|
+
// Return early if no metadata or empty metadata object
|
|
308
|
+
if (!metadata || Object.keys(metadata).length === 0)
|
|
288
309
|
return entity;
|
|
289
310
|
switch (mode) {
|
|
290
311
|
case "strip":
|
|
@@ -294,20 +315,30 @@ function applyMetadataMode(entity, metadata, mode, useCamelCase = true) {
|
|
|
294
315
|
// Add metadata field with target provider's casing
|
|
295
316
|
const metadataKey = useCamelCase ? "_providerMetadata" : "_provider_metadata";
|
|
296
317
|
const knownFieldsKey = useCamelCase ? "_knownFields" : "_known_fields";
|
|
297
|
-
|
|
298
|
-
|
|
318
|
+
const partsMetadataKey = useCamelCase ? "_partsMetadata" : "_parts_metadata";
|
|
319
|
+
// Normalize the known fields and parts metadata keys in the metadata
|
|
320
|
+
const { _known_fields, _knownFields, _parts_metadata, _partsMetadata, ...rest } = metadata;
|
|
299
321
|
const knownFields = _known_fields ?? _knownFields;
|
|
322
|
+
const partsMetadata = _parts_metadata ?? _partsMetadata;
|
|
300
323
|
const hasKnownFields = knownFields && Object.keys(knownFields).length > 0;
|
|
301
|
-
const
|
|
324
|
+
const hasPartsMetadata = partsMetadata && Object.keys(partsMetadata).length > 0;
|
|
325
|
+
const normalizedMetadata = { ...rest };
|
|
326
|
+
if (hasKnownFields) {
|
|
327
|
+
normalizedMetadata[knownFieldsKey] = knownFields;
|
|
328
|
+
}
|
|
329
|
+
if (hasPartsMetadata) {
|
|
330
|
+
normalizedMetadata[partsMetadataKey] = partsMetadata;
|
|
331
|
+
}
|
|
302
332
|
// Only add metadata field if there's something to store
|
|
303
333
|
if (Object.keys(normalizedMetadata).length === 0)
|
|
304
334
|
return entity;
|
|
305
335
|
return { ...entity, [metadataKey]: normalizedMetadata };
|
|
306
336
|
}
|
|
307
337
|
case "passthrough": {
|
|
308
|
-
// Spread all fields EXCEPT known fields (either casing)
|
|
309
|
-
|
|
310
|
-
|
|
338
|
+
// Spread all extra fields EXCEPT known fields and parts metadata (either casing)
|
|
339
|
+
// _partsMetadata should be restored to parts by the provider's fromGenAI, not spread at entity level
|
|
340
|
+
const { _known_fields, _knownFields, _parts_metadata, _partsMetadata, ...extraFields } = metadata;
|
|
341
|
+
// Only spread if there are extra fields to add
|
|
311
342
|
if (Object.keys(extraFields).length === 0)
|
|
312
343
|
return entity;
|
|
313
344
|
return { ...entity, ...extraFields };
|
|
@@ -1543,12 +1574,27 @@ const KnownFieldsSchema = zod.z
|
|
|
1543
1574
|
isRefusal: zod.z.boolean().optional(),
|
|
1544
1575
|
/** Original type when mapping to a different GenAI type (for lossy conversions) */
|
|
1545
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(),
|
|
1579
|
+
})
|
|
1580
|
+
.passthrough();
|
|
1581
|
+
/**
|
|
1582
|
+
* Schema for collapsed part-level metadata (used when target doesn't support structured content).
|
|
1583
|
+
* Has the same structure as part metadata: known fields + extra fields.
|
|
1584
|
+
*/
|
|
1585
|
+
const PartsMetadataSchema = zod.z
|
|
1586
|
+
.object({
|
|
1587
|
+
/** Known fields from collapsed parts */
|
|
1588
|
+
_known_fields: KnownFieldsSchema.optional(),
|
|
1589
|
+
/** Also check camelCase version */
|
|
1590
|
+
_knownFields: KnownFieldsSchema.optional(),
|
|
1546
1591
|
})
|
|
1547
1592
|
.passthrough();
|
|
1548
1593
|
/**
|
|
1549
1594
|
* Provider metadata schema for preserving provider-specific data.
|
|
1550
1595
|
* This is a flat structure with:
|
|
1551
1596
|
* - `_known_fields`: Internal fields used for building correct translations
|
|
1597
|
+
* - `_parts_metadata`: Collapsed part-level metadata (when target uses string content)
|
|
1552
1598
|
* - All other fields: Extra data from the source provider, passed through
|
|
1553
1599
|
*
|
|
1554
1600
|
* The metadata is stored at `_provider_metadata` on GenAI entities.
|
|
@@ -1561,6 +1607,10 @@ const ProviderMetadataSchema = zod.z
|
|
|
1561
1607
|
_known_fields: KnownFieldsSchema.optional(),
|
|
1562
1608
|
/** Also check camelCase version (from VercelAI/Promptl targets) */
|
|
1563
1609
|
_knownFields: KnownFieldsSchema.optional(),
|
|
1610
|
+
/** Collapsed part-level metadata (when target uses string content) */
|
|
1611
|
+
_parts_metadata: PartsMetadataSchema.optional(),
|
|
1612
|
+
/** Also check camelCase version (from VercelAI/Promptl targets) */
|
|
1613
|
+
_partsMetadata: PartsMetadataSchema.optional(),
|
|
1564
1614
|
})
|
|
1565
1615
|
.passthrough();
|
|
1566
1616
|
/** Role of the entity that created the message. */
|
|
@@ -1690,22 +1740,44 @@ const GenAISpecification = {
|
|
|
1690
1740
|
}
|
|
1691
1741
|
const parsedSystem = GenAISystemSchema.optional().parse(system);
|
|
1692
1742
|
if (parsedSystem && parsedSystem.length > 0) {
|
|
1693
|
-
|
|
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
|
+
}
|
|
1694
1762
|
}
|
|
1695
1763
|
return { messages: parsedMessages };
|
|
1696
1764
|
},
|
|
1697
1765
|
fromGenAI({ messages, providerMetadata }) {
|
|
1698
1766
|
const system = [];
|
|
1699
1767
|
const filtered = [];
|
|
1700
|
-
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];
|
|
1771
|
+
// Apply metadata mode to message and its parts (handles _partsMetadata restoration)
|
|
1772
|
+
const transformed = applyMetadataModeToGenAIMessage(message, providerMetadata);
|
|
1701
1773
|
if (message.role === "system") {
|
|
1702
|
-
//
|
|
1703
|
-
const
|
|
1704
|
-
|
|
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
|
+
}
|
|
1705
1778
|
}
|
|
1706
1779
|
else {
|
|
1707
|
-
|
|
1708
|
-
filtered.push(applyMetadataModeToGenAIMessage(message, providerMetadata));
|
|
1780
|
+
filtered.push(transformed);
|
|
1709
1781
|
}
|
|
1710
1782
|
}
|
|
1711
1783
|
return { messages: filtered, system: system.length > 0 ? system : undefined };
|
|
@@ -1722,11 +1794,60 @@ function applyMetadataModeToGenAI(part, mode) {
|
|
|
1722
1794
|
function applyMetadataModeToGenAIMessage(message, mode) {
|
|
1723
1795
|
const msgMetadata = readMetadata(message);
|
|
1724
1796
|
const transformedParts = message.parts.map((part) => applyMetadataModeToGenAI(part, mode));
|
|
1797
|
+
// Extract _partsMetadata from message metadata and apply to first part
|
|
1798
|
+
// This restores part-level metadata that was merged when converting to string content
|
|
1799
|
+
const partsMetadata = getPartsMetadata(msgMetadata);
|
|
1800
|
+
if (partsMetadata && transformedParts.length > 0) {
|
|
1801
|
+
// biome-ignore lint/style/noNonNullAssertion: length check guarantees element exists
|
|
1802
|
+
transformedParts[0] = applyMetadataMode(transformedParts[0], partsMetadata, mode, false);
|
|
1803
|
+
}
|
|
1725
1804
|
// Remove existing metadata keys before applying mode
|
|
1726
1805
|
const { _provider_metadata, _providerMetadata, parts, ...baseMessage } = message;
|
|
1727
1806
|
const baseWithParts = { ...baseMessage, parts: transformedParts };
|
|
1728
1807
|
return applyMetadataMode(baseWithParts, msgMetadata, mode, false);
|
|
1729
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
|
+
}
|
|
1730
1851
|
|
|
1731
1852
|
/**
|
|
1732
1853
|
* Google Gemini API Schemas
|
|
@@ -3684,6 +3805,15 @@ function genAIMessageToPromptl(message, toolCallNameMap, mode) {
|
|
|
3684
3805
|
toolMessages.push(createToolMessageFromPart(part, toolCallNameMap, mode));
|
|
3685
3806
|
}
|
|
3686
3807
|
}
|
|
3808
|
+
// Apply _partsMetadata to the first tool message's content if present
|
|
3809
|
+
const partsMetadata = getPartsMetadata(msgMetadata);
|
|
3810
|
+
if (partsMetadata && toolMessages.length > 0) {
|
|
3811
|
+
const firstMsg = toolMessages[0];
|
|
3812
|
+
if (firstMsg.content.length > 0) {
|
|
3813
|
+
// biome-ignore lint/style/noNonNullAssertion: length check guarantees element exists
|
|
3814
|
+
firstMsg.content[0] = applyMetadataMode(firstMsg.content[0], partsMetadata, mode, true);
|
|
3815
|
+
}
|
|
3816
|
+
}
|
|
3687
3817
|
return toolMessages.length > 0 ? toolMessages : [];
|
|
3688
3818
|
}
|
|
3689
3819
|
// Handle assistant role - check if any parts are tool_call_response
|
|
@@ -3707,10 +3837,27 @@ function genAIMessageToPromptl(message, toolCallNameMap, mode) {
|
|
|
3707
3837
|
}
|
|
3708
3838
|
}
|
|
3709
3839
|
if (content.length > 0) {
|
|
3840
|
+
// Extract _partsMetadata from message metadata and apply to first content part
|
|
3841
|
+
const partsMetadata = getPartsMetadata(msgMetadata);
|
|
3842
|
+
if (partsMetadata) {
|
|
3843
|
+
// biome-ignore lint/style/noNonNullAssertion: length check guarantees element exists
|
|
3844
|
+
content[0] = applyMetadataMode(content[0], partsMetadata, mode, true);
|
|
3845
|
+
}
|
|
3710
3846
|
const assistantMsg = { role: "assistant", content };
|
|
3711
3847
|
result.unshift(applyMetadataMode(assistantMsg, msgMetadata, mode, true));
|
|
3712
3848
|
}
|
|
3713
3849
|
}
|
|
3850
|
+
else {
|
|
3851
|
+
// No other parts - apply _partsMetadata to the first tool message's content
|
|
3852
|
+
const partsMetadata = getPartsMetadata(msgMetadata);
|
|
3853
|
+
if (partsMetadata && result.length > 0) {
|
|
3854
|
+
const firstMsg = result[0];
|
|
3855
|
+
if (firstMsg.content.length > 0) {
|
|
3856
|
+
// biome-ignore lint/style/noNonNullAssertion: length check guarantees element exists
|
|
3857
|
+
firstMsg.content[0] = applyMetadataMode(firstMsg.content[0], partsMetadata, mode, true);
|
|
3858
|
+
}
|
|
3859
|
+
}
|
|
3860
|
+
}
|
|
3714
3861
|
return result;
|
|
3715
3862
|
}
|
|
3716
3863
|
}
|
|
@@ -3722,6 +3869,13 @@ function genAIMessageToPromptl(message, toolCallNameMap, mode) {
|
|
|
3722
3869
|
content.push(converted);
|
|
3723
3870
|
}
|
|
3724
3871
|
}
|
|
3872
|
+
// Extract _partsMetadata from message metadata and apply to first content part
|
|
3873
|
+
// This restores part-level metadata that was merged when converting to string content
|
|
3874
|
+
const partsMetadata = getPartsMetadata(msgMetadata);
|
|
3875
|
+
if (partsMetadata && content.length > 0) {
|
|
3876
|
+
// biome-ignore lint/style/noNonNullAssertion: length check guarantees element exists
|
|
3877
|
+
content[0] = applyMetadataMode(content[0], partsMetadata, mode, true);
|
|
3878
|
+
}
|
|
3725
3879
|
// Map role - Promptl only accepts specific roles
|
|
3726
3880
|
let role;
|
|
3727
3881
|
if (message.role === "system") {
|
|
@@ -4472,6 +4626,14 @@ function genAIMessageToVercelAI(message, toolCallNameMap, mode) {
|
|
|
4472
4626
|
const msgMetadata = readMetadata(message);
|
|
4473
4627
|
// Helper to apply metadata mode to a message
|
|
4474
4628
|
const applyMode = (msg) => applyMetadataMode(msg, msgMetadata, mode, true);
|
|
4629
|
+
// Helper to check if any part has metadata that should be preserved
|
|
4630
|
+
const hasPartMetadata = () => message.parts.some((p) => {
|
|
4631
|
+
const meta = readMetadata(p);
|
|
4632
|
+
return meta && Object.keys(meta).length > 0;
|
|
4633
|
+
});
|
|
4634
|
+
// Helper to determine if we should collapse single text part to string
|
|
4635
|
+
// Only collapse if mode is "strip" OR no parts have metadata
|
|
4636
|
+
const shouldCollapseToString = () => mode === "strip" || !hasPartMetadata();
|
|
4475
4637
|
// Handle system messages
|
|
4476
4638
|
if (message.role === "system") {
|
|
4477
4639
|
// Extract text content
|
|
@@ -4479,8 +4641,21 @@ function genAIMessageToVercelAI(message, toolCallNameMap, mode) {
|
|
|
4479
4641
|
.filter((p) => p.type === "text")
|
|
4480
4642
|
.map((p) => p.content)
|
|
4481
4643
|
.join("\n");
|
|
4644
|
+
// System messages must use string content, so collect part metadata into _partsMetadata
|
|
4645
|
+
let combinedMeta = msgMetadata ? { ...msgMetadata } : undefined;
|
|
4646
|
+
let partsMetadata;
|
|
4647
|
+
for (const part of message.parts.filter((p) => p.type === "text")) {
|
|
4648
|
+
const partMeta = readMetadata(part);
|
|
4649
|
+
if (partMeta && Object.keys(partMeta).length > 0) {
|
|
4650
|
+
partsMetadata = { ...partsMetadata, ...partMeta };
|
|
4651
|
+
}
|
|
4652
|
+
}
|
|
4653
|
+
if (partsMetadata) {
|
|
4654
|
+
combinedMeta = { ...combinedMeta, _partsMetadata: partsMetadata };
|
|
4655
|
+
}
|
|
4656
|
+
const applyModeCombined = (msg) => applyMetadataMode(msg, combinedMeta, mode, true);
|
|
4482
4657
|
return [
|
|
4483
|
-
|
|
4658
|
+
applyModeCombined({
|
|
4484
4659
|
role: "system",
|
|
4485
4660
|
content: textContent,
|
|
4486
4661
|
}),
|
|
@@ -4498,6 +4673,12 @@ function genAIMessageToVercelAI(message, toolCallNameMap, mode) {
|
|
|
4498
4673
|
if (toolParts.length === 0) {
|
|
4499
4674
|
return [];
|
|
4500
4675
|
}
|
|
4676
|
+
// Apply _partsMetadata to the first tool part if present
|
|
4677
|
+
const partsMetadata = getPartsMetadata(msgMetadata);
|
|
4678
|
+
if (partsMetadata && toolParts.length > 0) {
|
|
4679
|
+
// biome-ignore lint/style/noNonNullAssertion: length check guarantees element exists
|
|
4680
|
+
toolParts[0] = applyMetadataMode(toolParts[0], partsMetadata, mode, true);
|
|
4681
|
+
}
|
|
4501
4682
|
return [
|
|
4502
4683
|
applyMode({
|
|
4503
4684
|
role: "tool",
|
|
@@ -4514,8 +4695,10 @@ function genAIMessageToVercelAI(message, toolCallNameMap, mode) {
|
|
|
4514
4695
|
userParts.push(converted);
|
|
4515
4696
|
}
|
|
4516
4697
|
}
|
|
4517
|
-
//
|
|
4518
|
-
|
|
4698
|
+
// Check for _partsMetadata - if present, we need array content to apply it
|
|
4699
|
+
const partsMetadata = getPartsMetadata(msgMetadata);
|
|
4700
|
+
// If only one text part and no metadata to preserve (including _partsMetadata), use string content
|
|
4701
|
+
if (userParts.length === 1 && userParts[0]?.type === "text" && shouldCollapseToString() && !partsMetadata) {
|
|
4519
4702
|
return [
|
|
4520
4703
|
applyMode({
|
|
4521
4704
|
role: "user",
|
|
@@ -4523,6 +4706,11 @@ function genAIMessageToVercelAI(message, toolCallNameMap, mode) {
|
|
|
4523
4706
|
}),
|
|
4524
4707
|
];
|
|
4525
4708
|
}
|
|
4709
|
+
// Apply _partsMetadata to the first user part if present
|
|
4710
|
+
if (partsMetadata && userParts.length > 0) {
|
|
4711
|
+
// biome-ignore lint/style/noNonNullAssertion: length check guarantees element exists
|
|
4712
|
+
userParts[0] = applyMetadataMode(userParts[0], partsMetadata, mode, true);
|
|
4713
|
+
}
|
|
4526
4714
|
return [
|
|
4527
4715
|
applyMode({
|
|
4528
4716
|
role: "user",
|
|
@@ -4539,8 +4727,13 @@ function genAIMessageToVercelAI(message, toolCallNameMap, mode) {
|
|
|
4539
4727
|
assistantParts.push(converted);
|
|
4540
4728
|
}
|
|
4541
4729
|
}
|
|
4542
|
-
//
|
|
4543
|
-
|
|
4730
|
+
// Check for _partsMetadata - if present, we need array content to apply it
|
|
4731
|
+
const partsMetadata = getPartsMetadata(msgMetadata);
|
|
4732
|
+
// If only one text part and no metadata to preserve (including _partsMetadata), use string content
|
|
4733
|
+
if (assistantParts.length === 1 &&
|
|
4734
|
+
assistantParts[0]?.type === "text" &&
|
|
4735
|
+
shouldCollapseToString() &&
|
|
4736
|
+
!partsMetadata) {
|
|
4544
4737
|
return [
|
|
4545
4738
|
applyMode({
|
|
4546
4739
|
role: "assistant",
|
|
@@ -4548,6 +4741,11 @@ function genAIMessageToVercelAI(message, toolCallNameMap, mode) {
|
|
|
4548
4741
|
}),
|
|
4549
4742
|
];
|
|
4550
4743
|
}
|
|
4744
|
+
// Apply _partsMetadata to the first assistant part if present
|
|
4745
|
+
if (partsMetadata && assistantParts.length > 0) {
|
|
4746
|
+
// biome-ignore lint/style/noNonNullAssertion: length check guarantees element exists
|
|
4747
|
+
assistantParts[0] = applyMetadataMode(assistantParts[0], partsMetadata, mode, true);
|
|
4748
|
+
}
|
|
4551
4749
|
return [
|
|
4552
4750
|
applyMode({
|
|
4553
4751
|
role: "assistant",
|
|
@@ -4560,6 +4758,12 @@ function genAIMessageToVercelAI(message, toolCallNameMap, mode) {
|
|
|
4560
4758
|
.filter((p) => p.type === "text")
|
|
4561
4759
|
.map((p) => ({ type: "text", text: p.content }));
|
|
4562
4760
|
if (textParts.length > 0) {
|
|
4761
|
+
// Apply _partsMetadata to the first text part if present
|
|
4762
|
+
const partsMetadata = getPartsMetadata(msgMetadata);
|
|
4763
|
+
if (partsMetadata) {
|
|
4764
|
+
// biome-ignore lint/style/noNonNullAssertion: length check guarantees element exists
|
|
4765
|
+
textParts[0] = applyMetadataMode(textParts[0], partsMetadata, mode, true);
|
|
4766
|
+
}
|
|
4563
4767
|
return [
|
|
4564
4768
|
applyMode({
|
|
4565
4769
|
role: "user",
|