llm-messages 0.1.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/CHANGELOG.md +20 -0
- package/LICENSE +21 -0
- package/README.md +132 -0
- package/dist/index.cjs +419 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +179 -0
- package/dist/index.d.ts +179 -0
- package/dist/index.js +388 -0
- package/dist/index.js.map +1 -0
- package/package.json +75 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format is based on
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project adheres
|
|
5
|
+
to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [0.1.0] - 2026-06-01
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Initial release.
|
|
12
|
+
- `toAnthropic` / `fromAnthropic`, `toGemini` / `fromGemini` convert conversations
|
|
13
|
+
between the OpenAI canonical format and each provider.
|
|
14
|
+
- `convert(conversation, { from, to })` converts between any two providers, fully
|
|
15
|
+
typed via the `from` / `to` pair.
|
|
16
|
+
- Correct handling of system prompts, the `assistant`/`model` role difference,
|
|
17
|
+
tool-call argument serialization, tool-result placement and grouping, Gemini
|
|
18
|
+
id/name matching, and consecutive same-role merging.
|
|
19
|
+
- Optional `onWarning` reporting for every non fatal conversion event.
|
|
20
|
+
- Zero runtime dependencies.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sebastian Legarraga
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# llm-messages
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/llm-messages)
|
|
4
|
+
[](https://github.com/slegarraga/llm-messages/actions/workflows/ci.yml)
|
|
5
|
+
[](./LICENSE)
|
|
6
|
+
[](./package.json)
|
|
7
|
+
|
|
8
|
+
Convert chat conversations between **OpenAI**, **Anthropic** and **Gemini** message formats. Tool calls, system prompts and roles handled correctly. Zero dependencies.
|
|
9
|
+
|
|
10
|
+
Switching an agent from one provider to another (or running fallback across providers) means rewriting the whole conversation, and the differences are subtle enough to break at runtime:
|
|
11
|
+
|
|
12
|
+
- The **system prompt** is a message in OpenAI, a top-level `system` field in Anthropic, and `systemInstruction` in Gemini.
|
|
13
|
+
- The assistant role is `assistant` in OpenAI and Anthropic but `model` in Gemini.
|
|
14
|
+
- Tool-call arguments are a **JSON string** in OpenAI but a **parsed object** in Anthropic and Gemini.
|
|
15
|
+
- Tool results are a standalone `role: "tool"` message in OpenAI, a `tool_result` block inside a user turn in Anthropic, and a `functionResponse` part in Gemini.
|
|
16
|
+
- Gemini matches tool calls to results **by function name**, while OpenAI and Anthropic use ids.
|
|
17
|
+
- Anthropic and Gemini reject consecutive same-role turns; OpenAI does not.
|
|
18
|
+
|
|
19
|
+
`llm-messages` handles all of it. Write the conversation once, send it to any provider.
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
npm install llm-messages
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Requires Node 18+. Ships ESM and CommonJS with full TypeScript types.
|
|
28
|
+
|
|
29
|
+
## Quick start
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { toAnthropic, toGemini } from 'llm-messages';
|
|
33
|
+
|
|
34
|
+
// A normal OpenAI Chat Completions conversation
|
|
35
|
+
const messages = [
|
|
36
|
+
{ role: 'system', content: 'You are a weather assistant.' },
|
|
37
|
+
{ role: 'user', content: "What's the weather in Paris?" },
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
const anthropic = toAnthropic(messages);
|
|
41
|
+
// -> { system: 'You are a weather assistant.', messages: [{ role: 'user', content: "What's the weather in Paris?" }] }
|
|
42
|
+
|
|
43
|
+
const gemini = toGemini(messages);
|
|
44
|
+
// -> { systemInstruction: { parts: [{ text: 'You are a weather assistant.' }] },
|
|
45
|
+
// contents: [{ role: 'user', parts: [{ text: "What's the weather in Paris?" }] }] }
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## The canonical hub
|
|
49
|
+
|
|
50
|
+
OpenAI Chat Completions is the canonical format. Every conversion routes through
|
|
51
|
+
it, so you get a function for each direction:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import { toAnthropic, fromAnthropic, toGemini, fromGemini, convert } from 'llm-messages';
|
|
55
|
+
|
|
56
|
+
toAnthropic(openaiMessages); // OpenAI -> Anthropic
|
|
57
|
+
fromAnthropic(anthropicBody); // Anthropic -> OpenAI
|
|
58
|
+
toGemini(openaiMessages); // OpenAI -> Gemini
|
|
59
|
+
fromGemini(geminiBody); // Gemini -> OpenAI
|
|
60
|
+
|
|
61
|
+
// Or convert between any two providers in one call:
|
|
62
|
+
convert(anthropicBody, { from: 'anthropic', to: 'gemini' });
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`convert` is fully typed: the input and output shapes are inferred from the
|
|
66
|
+
`from` and `to` providers.
|
|
67
|
+
|
|
68
|
+
## Tool calls round trip losslessly
|
|
69
|
+
|
|
70
|
+
The hard part is tool use, and it survives a full round trip unchanged:
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
const messages = [
|
|
74
|
+
{
|
|
75
|
+
role: 'assistant',
|
|
76
|
+
content: null,
|
|
77
|
+
tool_calls: [
|
|
78
|
+
{ id: 'call_abc', type: 'function', function: { name: 'get_weather', arguments: '{"location":"Paris"}' } },
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
{ role: 'tool', tool_call_id: 'call_abc', content: '15C partly cloudy' },
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
fromGemini(toGemini(messages)); // deep-equals the original `messages`
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Arguments are parsed and re-serialized, ids are preserved (and regenerated
|
|
88
|
+
deterministically when a Gemini payload omits them), and parallel tool results
|
|
89
|
+
are grouped into the single user turn each provider expects.
|
|
90
|
+
|
|
91
|
+
## Conversion report
|
|
92
|
+
|
|
93
|
+
Conversions never throw on malformed input. Instead they make a deterministic
|
|
94
|
+
choice and optionally report it, so you can surface or log what happened:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
toGemini(messages, {
|
|
98
|
+
onWarning: (w) => console.warn(`[${w.code}] ${w.message}`),
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Warning codes: `generated-id`, `unmapped-tool-result`, `merged-role`,
|
|
103
|
+
`dropped-content`, `invalid-json-arguments`, `system-midstream`.
|
|
104
|
+
|
|
105
|
+
## Format cheatsheet
|
|
106
|
+
|
|
107
|
+
| | OpenAI | Anthropic | Gemini |
|
|
108
|
+
| ---------------- | ------------------------ | -------------------------------- | ------------------------------- |
|
|
109
|
+
| System prompt | `role: "system"` message | top-level `system` | `systemInstruction` |
|
|
110
|
+
| Assistant role | `assistant` | `assistant` | `model` |
|
|
111
|
+
| Tool call | `tool_calls[].function` | `tool_use` block | `functionCall` part |
|
|
112
|
+
| Call arguments | JSON string | object (`input`) | object (`args`) |
|
|
113
|
+
| Tool result | `role: "tool"` message | `tool_result` block in user turn | `functionResponse` part in user |
|
|
114
|
+
| Match key | `tool_call_id` | `tool_use_id` | function `name` (id optional) |
|
|
115
|
+
| Role alternation | not required | strict | strict |
|
|
116
|
+
|
|
117
|
+
## Scope
|
|
118
|
+
|
|
119
|
+
Version 0.x covers text, system prompts, and tool calls/results, which is the
|
|
120
|
+
core of every agent loop. Multimodal parts (images, audio) are passed through
|
|
121
|
+
where possible and reported as `dropped-content` otherwise; first-class
|
|
122
|
+
multimodal mapping is on the roadmap.
|
|
123
|
+
|
|
124
|
+
## Part of a set
|
|
125
|
+
|
|
126
|
+
`llm-messages` pairs with [`tool-schema`](https://github.com/slegarraga/tool-schema),
|
|
127
|
+
which converts your tool/function **schemas** across the same providers. Together
|
|
128
|
+
they let you write an agent once and run it on any LLM.
|
|
129
|
+
|
|
130
|
+
## License
|
|
131
|
+
|
|
132
|
+
MIT (c) Sebastian Legarraga. See [LICENSE](./LICENSE).
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
convert: () => convert,
|
|
24
|
+
fromAnthropic: () => fromAnthropic,
|
|
25
|
+
fromGemini: () => fromGemini,
|
|
26
|
+
toAnthropic: () => toAnthropic,
|
|
27
|
+
toGemini: () => toGemini
|
|
28
|
+
});
|
|
29
|
+
module.exports = __toCommonJS(index_exports);
|
|
30
|
+
|
|
31
|
+
// src/util.ts
|
|
32
|
+
var Reporter = class {
|
|
33
|
+
constructor(options = {}) {
|
|
34
|
+
this.options = options;
|
|
35
|
+
}
|
|
36
|
+
options;
|
|
37
|
+
warn(code, message) {
|
|
38
|
+
this.options.onWarning?.({ code, message });
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
function isRecord(value) {
|
|
42
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
43
|
+
}
|
|
44
|
+
function textOf(content) {
|
|
45
|
+
if (typeof content === "string") return content;
|
|
46
|
+
if (Array.isArray(content)) {
|
|
47
|
+
return content.map((part) => {
|
|
48
|
+
if (typeof part === "string") return part;
|
|
49
|
+
if (isRecord(part) && typeof part.text === "string") return part.text;
|
|
50
|
+
return "";
|
|
51
|
+
}).join("");
|
|
52
|
+
}
|
|
53
|
+
return "";
|
|
54
|
+
}
|
|
55
|
+
function tryParseJson(input) {
|
|
56
|
+
try {
|
|
57
|
+
return { ok: true, value: JSON.parse(input) };
|
|
58
|
+
} catch {
|
|
59
|
+
return { ok: false };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function parseArguments(args, reporter, fnName) {
|
|
63
|
+
const parsed = tryParseJson(args);
|
|
64
|
+
if (parsed.ok && isRecord(parsed.value)) return parsed.value;
|
|
65
|
+
reporter.warn(
|
|
66
|
+
"invalid-json-arguments",
|
|
67
|
+
`Tool call '${fnName}' had arguments that were not a JSON object; used an empty object instead.`
|
|
68
|
+
);
|
|
69
|
+
return {};
|
|
70
|
+
}
|
|
71
|
+
function wrapResponse(content) {
|
|
72
|
+
const parsed = tryParseJson(content);
|
|
73
|
+
if (parsed.ok && isRecord(parsed.value)) return parsed.value;
|
|
74
|
+
return { result: content };
|
|
75
|
+
}
|
|
76
|
+
function unwrapResponse(response) {
|
|
77
|
+
const keys = Object.keys(response);
|
|
78
|
+
if (keys.length === 1 && keys[0] === "result" && typeof response.result === "string") {
|
|
79
|
+
return response.result;
|
|
80
|
+
}
|
|
81
|
+
return JSON.stringify(response);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// src/providers/openai.ts
|
|
85
|
+
function isSystem(message) {
|
|
86
|
+
return message.role === "system" || message.role === "developer";
|
|
87
|
+
}
|
|
88
|
+
function splitSystem(messages, reporter) {
|
|
89
|
+
const systemParts = [];
|
|
90
|
+
const rest = [];
|
|
91
|
+
let started = false;
|
|
92
|
+
for (const message of messages) {
|
|
93
|
+
if (isSystem(message)) {
|
|
94
|
+
if (started) {
|
|
95
|
+
reporter.warn(
|
|
96
|
+
"system-midstream",
|
|
97
|
+
"A system message appeared mid conversation; it was merged into the top-level system prompt."
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
systemParts.push(textOf(message.content));
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
started = true;
|
|
104
|
+
rest.push(message);
|
|
105
|
+
}
|
|
106
|
+
const system = systemParts.length > 0 ? systemParts.join("\n\n") : void 0;
|
|
107
|
+
return system === void 0 ? { rest } : { system, rest };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// src/providers/anthropic.ts
|
|
111
|
+
function toAnthropic(messages, options = {}) {
|
|
112
|
+
const reporter = new Reporter(options);
|
|
113
|
+
const { system, rest } = splitSystem(messages, reporter);
|
|
114
|
+
const out = [];
|
|
115
|
+
for (let i = 0; i < rest.length; i++) {
|
|
116
|
+
const message = rest[i];
|
|
117
|
+
if (message.role === "tool") {
|
|
118
|
+
const blocks = [];
|
|
119
|
+
let j = i;
|
|
120
|
+
while (j < rest.length && rest[j].role === "tool") {
|
|
121
|
+
const tool = rest[j];
|
|
122
|
+
blocks.push({ type: "tool_result", tool_use_id: tool.tool_call_id, content: textOf(tool.content) });
|
|
123
|
+
j++;
|
|
124
|
+
}
|
|
125
|
+
out.push({ role: "user", content: blocks });
|
|
126
|
+
i = j - 1;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (message.role === "user") {
|
|
130
|
+
out.push({ role: "user", content: userContent(message.content, reporter) });
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
out.push({ role: "assistant", content: assistantContent(message, reporter) });
|
|
134
|
+
}
|
|
135
|
+
const merged = mergeConsecutive(out, reporter);
|
|
136
|
+
return system === void 0 ? { messages: merged } : { system, messages: merged };
|
|
137
|
+
}
|
|
138
|
+
function userContent(content, reporter) {
|
|
139
|
+
if (typeof content === "string") return content;
|
|
140
|
+
if (!Array.isArray(content)) return textOf(content);
|
|
141
|
+
const blocks = [];
|
|
142
|
+
for (const part of content) {
|
|
143
|
+
if (isRecord(part) && part.type === "text" && typeof part.text === "string") {
|
|
144
|
+
blocks.push({ type: "text", text: part.text });
|
|
145
|
+
} else {
|
|
146
|
+
reporter.warn("dropped-content", "Dropped a non-text user content part not supported by this converter.");
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return blocks;
|
|
150
|
+
}
|
|
151
|
+
function assistantContent(message, reporter) {
|
|
152
|
+
const text = textOf(message.content ?? "");
|
|
153
|
+
const toolCalls = message.tool_calls ?? [];
|
|
154
|
+
if (toolCalls.length === 0) return text;
|
|
155
|
+
const blocks = [];
|
|
156
|
+
if (text) blocks.push({ type: "text", text });
|
|
157
|
+
for (const call of toolCalls) {
|
|
158
|
+
blocks.push({
|
|
159
|
+
type: "tool_use",
|
|
160
|
+
id: call.id,
|
|
161
|
+
name: call.function.name,
|
|
162
|
+
input: parseArguments(call.function.arguments, reporter, call.function.name)
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
return blocks;
|
|
166
|
+
}
|
|
167
|
+
function mergeConsecutive(messages, reporter) {
|
|
168
|
+
const result = [];
|
|
169
|
+
for (const message of messages) {
|
|
170
|
+
const previous = result[result.length - 1];
|
|
171
|
+
if (previous && previous.role === message.role) {
|
|
172
|
+
previous.content = [...asBlocks(previous.content), ...asBlocks(message.content)];
|
|
173
|
+
reporter.warn(
|
|
174
|
+
"merged-role",
|
|
175
|
+
`Merged consecutive '${message.role}' turns (Anthropic requires alternating roles).`
|
|
176
|
+
);
|
|
177
|
+
} else {
|
|
178
|
+
result.push({ role: message.role, content: message.content });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return result;
|
|
182
|
+
}
|
|
183
|
+
function asBlocks(content) {
|
|
184
|
+
if (typeof content !== "string") return content;
|
|
185
|
+
return content ? [{ type: "text", text: content }] : [];
|
|
186
|
+
}
|
|
187
|
+
function fromAnthropic(conversation, options = {}) {
|
|
188
|
+
void options;
|
|
189
|
+
const out = [];
|
|
190
|
+
if (conversation.system) {
|
|
191
|
+
out.push({ role: "system", content: textOf(conversation.system) });
|
|
192
|
+
}
|
|
193
|
+
for (const message of conversation.messages) {
|
|
194
|
+
const blocks = asBlocks(message.content);
|
|
195
|
+
if (message.role === "user") {
|
|
196
|
+
const toolResults = blocks.filter((b) => b.type === "tool_result");
|
|
197
|
+
const textBlocks = blocks.filter((b) => b.type !== "tool_result");
|
|
198
|
+
for (const block of toolResults) {
|
|
199
|
+
out.push({
|
|
200
|
+
role: "tool",
|
|
201
|
+
tool_call_id: String(block.tool_use_id ?? ""),
|
|
202
|
+
content: textOf(block.content)
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
if (textBlocks.length > 0) {
|
|
206
|
+
out.push({ role: "user", content: textOf(textBlocks) });
|
|
207
|
+
}
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
const text = textOf(blocks.filter((b) => b.type === "text"));
|
|
211
|
+
const toolUses = blocks.filter((b) => b.type === "tool_use");
|
|
212
|
+
const assistant = { role: "assistant", content: text || null };
|
|
213
|
+
if (toolUses.length > 0) {
|
|
214
|
+
assistant.tool_calls = toolUses.map((block) => {
|
|
215
|
+
const b = block;
|
|
216
|
+
return { id: b.id, type: "function", function: { name: b.name, arguments: JSON.stringify(b.input ?? {}) } };
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
out.push(assistant);
|
|
220
|
+
}
|
|
221
|
+
return out;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// src/providers/gemini.ts
|
|
225
|
+
function toGemini(messages, options = {}) {
|
|
226
|
+
const reporter = new Reporter(options);
|
|
227
|
+
const { system, rest } = splitSystem(messages, reporter);
|
|
228
|
+
const idToName = /* @__PURE__ */ new Map();
|
|
229
|
+
for (const message of rest) {
|
|
230
|
+
if (message.role === "assistant" && message.tool_calls) {
|
|
231
|
+
for (const call of message.tool_calls) idToName.set(call.id, call.function.name);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const contents = [];
|
|
235
|
+
for (let i = 0; i < rest.length; i++) {
|
|
236
|
+
const message = rest[i];
|
|
237
|
+
if (message.role === "tool") {
|
|
238
|
+
const parts = [];
|
|
239
|
+
let j = i;
|
|
240
|
+
while (j < rest.length && rest[j].role === "tool") {
|
|
241
|
+
const tool = rest[j];
|
|
242
|
+
const name = idToName.get(tool.tool_call_id);
|
|
243
|
+
if (!name) {
|
|
244
|
+
reporter.warn(
|
|
245
|
+
"unmapped-tool-result",
|
|
246
|
+
`Tool result '${tool.tool_call_id}' has no matching call; used the id as the function name.`
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
parts.push({
|
|
250
|
+
functionResponse: {
|
|
251
|
+
id: tool.tool_call_id,
|
|
252
|
+
name: name ?? tool.tool_call_id,
|
|
253
|
+
response: wrapResponse(textOf(tool.content))
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
j++;
|
|
257
|
+
}
|
|
258
|
+
contents.push({ role: "user", parts });
|
|
259
|
+
i = j - 1;
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (message.role === "user") {
|
|
263
|
+
contents.push({ role: "user", parts: userParts(message.content, reporter) });
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
contents.push({ role: "model", parts: assistantParts(message, reporter) });
|
|
267
|
+
}
|
|
268
|
+
const merged = mergeConsecutive2(contents, reporter);
|
|
269
|
+
return system === void 0 ? { contents: merged } : { systemInstruction: { parts: [{ text: system }] }, contents: merged };
|
|
270
|
+
}
|
|
271
|
+
function userParts(content, reporter) {
|
|
272
|
+
if (typeof content === "string") return [{ text: content }];
|
|
273
|
+
const parts = [];
|
|
274
|
+
for (const part of content) {
|
|
275
|
+
if (isRecord(part) && part.type === "text" && typeof part.text === "string") {
|
|
276
|
+
parts.push({ text: part.text });
|
|
277
|
+
} else {
|
|
278
|
+
reporter.warn("dropped-content", "Dropped a non-text user content part not supported by this converter.");
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return parts.length > 0 ? parts : [{ text: "" }];
|
|
282
|
+
}
|
|
283
|
+
function assistantParts(message, reporter) {
|
|
284
|
+
const parts = [];
|
|
285
|
+
const text = textOf(message.content ?? "");
|
|
286
|
+
if (text) parts.push({ text });
|
|
287
|
+
for (const call of message.tool_calls ?? []) {
|
|
288
|
+
parts.push({
|
|
289
|
+
functionCall: {
|
|
290
|
+
id: call.id,
|
|
291
|
+
name: call.function.name,
|
|
292
|
+
args: parseArguments(call.function.arguments, reporter, call.function.name)
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
return parts.length > 0 ? parts : [{ text: "" }];
|
|
297
|
+
}
|
|
298
|
+
function mergeConsecutive2(contents, reporter) {
|
|
299
|
+
const result = [];
|
|
300
|
+
for (const content of contents) {
|
|
301
|
+
const previous = result[result.length - 1];
|
|
302
|
+
if (previous && previous.role === content.role) {
|
|
303
|
+
previous.parts = [...previous.parts, ...content.parts];
|
|
304
|
+
reporter.warn("merged-role", `Merged consecutive '${content.role}' turns (Gemini requires alternating roles).`);
|
|
305
|
+
} else {
|
|
306
|
+
result.push({ role: content.role, parts: [...content.parts] });
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return result;
|
|
310
|
+
}
|
|
311
|
+
function fromGemini(conversation, options = {}) {
|
|
312
|
+
const reporter = new Reporter(options);
|
|
313
|
+
const out = [];
|
|
314
|
+
if (conversation.systemInstruction) {
|
|
315
|
+
const text = textOf(conversation.systemInstruction.parts);
|
|
316
|
+
if (text) out.push({ role: "system", content: text });
|
|
317
|
+
}
|
|
318
|
+
const pending = [];
|
|
319
|
+
let counter = 0;
|
|
320
|
+
const generateId = (name) => `call_${name.replace(/[^a-zA-Z0-9_-]/g, "_")}_${counter++}`;
|
|
321
|
+
for (const content of conversation.contents) {
|
|
322
|
+
const parts = Array.isArray(content.parts) ? content.parts : [];
|
|
323
|
+
if (content.role === "model") {
|
|
324
|
+
const textPieces2 = [];
|
|
325
|
+
const toolCalls = [];
|
|
326
|
+
for (const part of parts) {
|
|
327
|
+
if (isRecord(part) && isRecord(part.functionCall)) {
|
|
328
|
+
const fc = part.functionCall;
|
|
329
|
+
const id = fc.id ?? generateId(fc.name);
|
|
330
|
+
if (!fc.id) reporter.warn("generated-id", `Gemini functionCall '${fc.name}' had no id; generated '${id}'.`);
|
|
331
|
+
toolCalls.push({
|
|
332
|
+
id,
|
|
333
|
+
type: "function",
|
|
334
|
+
function: { name: fc.name, arguments: JSON.stringify(fc.args ?? {}) }
|
|
335
|
+
});
|
|
336
|
+
pending.push({ id, name: fc.name });
|
|
337
|
+
} else if (isRecord(part) && typeof part.text === "string") {
|
|
338
|
+
textPieces2.push(part.text);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
const text2 = textPieces2.join("");
|
|
342
|
+
const assistant = { role: "assistant", content: text2 || null };
|
|
343
|
+
if (toolCalls.length > 0) assistant.tool_calls = toolCalls;
|
|
344
|
+
out.push(assistant);
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
const textPieces = [];
|
|
348
|
+
for (const part of parts) {
|
|
349
|
+
if (isRecord(part) && isRecord(part.functionResponse)) {
|
|
350
|
+
const fr = part.functionResponse;
|
|
351
|
+
const id = resolveResponseId(fr, pending, reporter, generateId);
|
|
352
|
+
out.push({ role: "tool", tool_call_id: id, content: unwrapResponse(fr.response ?? {}) });
|
|
353
|
+
} else if (isRecord(part) && typeof part.text === "string") {
|
|
354
|
+
textPieces.push(part.text);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
const text = textPieces.join("");
|
|
358
|
+
if (text) out.push({ role: "user", content: text });
|
|
359
|
+
}
|
|
360
|
+
return out;
|
|
361
|
+
}
|
|
362
|
+
function resolveResponseId(response, pending, reporter, generateId) {
|
|
363
|
+
if (response.id) {
|
|
364
|
+
const index2 = pending.findIndex((p) => p.id === response.id);
|
|
365
|
+
if (index2 >= 0) pending.splice(index2, 1);
|
|
366
|
+
return response.id;
|
|
367
|
+
}
|
|
368
|
+
const index = pending.findIndex((p) => p.name === response.name);
|
|
369
|
+
if (index >= 0) {
|
|
370
|
+
const { id: id2 } = pending[index];
|
|
371
|
+
pending.splice(index, 1);
|
|
372
|
+
return id2;
|
|
373
|
+
}
|
|
374
|
+
const id = generateId(response.name);
|
|
375
|
+
reporter.warn(
|
|
376
|
+
"unmapped-tool-result",
|
|
377
|
+
`Gemini functionResponse for '${response.name}' had no matching call; generated '${id}'.`
|
|
378
|
+
);
|
|
379
|
+
return id;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// src/convert.ts
|
|
383
|
+
function convert(conversation, route, options = {}) {
|
|
384
|
+
const canonical = toCanonical(conversation, route.from, options);
|
|
385
|
+
return fromCanonical(canonical, route.to, options);
|
|
386
|
+
}
|
|
387
|
+
function toCanonical(conversation, from, options) {
|
|
388
|
+
switch (from) {
|
|
389
|
+
case "openai":
|
|
390
|
+
return conversation;
|
|
391
|
+
case "anthropic":
|
|
392
|
+
return fromAnthropic(conversation, options);
|
|
393
|
+
case "gemini":
|
|
394
|
+
return fromGemini(conversation, options);
|
|
395
|
+
default:
|
|
396
|
+
throw new Error(`Unknown source provider: ${String(from)}`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
function fromCanonical(canonical, to, options) {
|
|
400
|
+
switch (to) {
|
|
401
|
+
case "openai":
|
|
402
|
+
return canonical;
|
|
403
|
+
case "anthropic":
|
|
404
|
+
return toAnthropic(canonical, options);
|
|
405
|
+
case "gemini":
|
|
406
|
+
return toGemini(canonical, options);
|
|
407
|
+
default:
|
|
408
|
+
throw new Error(`Unknown target provider: ${String(to)}`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
412
|
+
0 && (module.exports = {
|
|
413
|
+
convert,
|
|
414
|
+
fromAnthropic,
|
|
415
|
+
fromGemini,
|
|
416
|
+
toAnthropic,
|
|
417
|
+
toGemini
|
|
418
|
+
});
|
|
419
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/util.ts","../src/providers/openai.ts","../src/providers/anthropic.ts","../src/providers/gemini.ts","../src/convert.ts"],"sourcesContent":["export { toAnthropic, fromAnthropic } from './providers/anthropic.js';\nexport { toGemini, fromGemini } from './providers/gemini.js';\nexport { convert } from './convert.js';\nexport type { ConversationOf } from './convert.js';\n\nexport type {\n Provider,\n Warning,\n WarningCode,\n ConvertOptions,\n OpenAIMessage,\n OpenAISystemMessage,\n OpenAIUserMessage,\n OpenAIAssistantMessage,\n OpenAIToolMessage,\n OpenAIToolCall,\n OpenAIContentPart,\n OpenAITextPart,\n AnthropicConversation,\n AnthropicMessage,\n AnthropicContentBlock,\n AnthropicTextBlock,\n AnthropicToolUseBlock,\n AnthropicToolResultBlock,\n GeminiConversation,\n GeminiContent,\n GeminiPart,\n GeminiTextPart,\n GeminiFunctionCallPart,\n GeminiFunctionResponsePart,\n} from './types.js';\n","import type { ConvertOptions, OpenAITextPart, Warning, WarningCode } from './types.js';\n\n/** Thin wrapper around the optional `onWarning` callback. */\nexport class Reporter {\n constructor(private readonly options: ConvertOptions = {}) {}\n\n warn(code: WarningCode, message: string): void {\n this.options.onWarning?.({ code, message } satisfies Warning);\n }\n}\n\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/**\n * Flattens any supported text content (a plain string or an array of text\n * parts/blocks) into a single string. Non text parts are ignored.\n */\nexport function textOf(content: unknown): string {\n if (typeof content === 'string') return content;\n if (Array.isArray(content)) {\n return content\n .map((part) => {\n if (typeof part === 'string') return part;\n if (isRecord(part) && typeof part.text === 'string') return part.text;\n return '';\n })\n .join('');\n }\n return '';\n}\n\n/** Wraps a string as a single OpenAI/Anthropic text part array. */\nexport function textPart(text: string): OpenAITextPart[] {\n return [{ type: 'text', text }];\n}\n\n/** Safely parses a JSON string, returning a discriminated result. */\nexport function tryParseJson(input: string): { ok: true; value: unknown } | { ok: false } {\n try {\n return { ok: true, value: JSON.parse(input) };\n } catch {\n return { ok: false };\n }\n}\n\n/**\n * Parses an OpenAI tool-call `arguments` JSON string into an object. Reports and\n * returns `{}` when the string is not valid JSON object syntax.\n */\nexport function parseArguments(args: string, reporter: Reporter, fnName: string): Record<string, unknown> {\n const parsed = tryParseJson(args);\n if (parsed.ok && isRecord(parsed.value)) return parsed.value;\n reporter.warn(\n 'invalid-json-arguments',\n `Tool call '${fnName}' had arguments that were not a JSON object; used an empty object instead.`,\n );\n return {};\n}\n\n/**\n * Converts an OpenAI tool result string into a Gemini `functionResponse.response`\n * object. A JSON object string is used directly; anything else is wrapped as\n * `{ result: <text> }`, which {@link unwrapResponse} reverses.\n */\nexport function wrapResponse(content: string): Record<string, unknown> {\n const parsed = tryParseJson(content);\n if (parsed.ok && isRecord(parsed.value)) return parsed.value;\n return { result: content };\n}\n\n/** Reverses {@link wrapResponse}: a lone `{ result: string }` becomes that string. */\nexport function unwrapResponse(response: Record<string, unknown>): string {\n const keys = Object.keys(response);\n if (keys.length === 1 && keys[0] === 'result' && typeof response.result === 'string') {\n return response.result;\n }\n return JSON.stringify(response);\n}\n","import type {\n OpenAIAssistantMessage,\n OpenAIMessage,\n OpenAISystemMessage,\n OpenAIToolMessage,\n OpenAIUserMessage,\n} from '../types.js';\nimport { Reporter, textOf } from '../util.js';\n\n/** Any canonical message other than a system/developer prompt. */\nexport type NonSystemMessage = OpenAIUserMessage | OpenAIAssistantMessage | OpenAIToolMessage;\n\nfunction isSystem(message: OpenAIMessage): message is OpenAISystemMessage {\n return message.role === 'system' || message.role === 'developer';\n}\n\n/**\n * Splits a canonical OpenAI conversation into its system prompt and the\n * remaining messages. All `system` / `developer` messages are folded into a\n * single string (joined by blank lines) because Anthropic and Gemini carry the\n * system prompt as one top-level field. A system message that appears after the\n * conversation has started is still folded in, but reported as `system-midstream`.\n */\nexport function splitSystem(\n messages: OpenAIMessage[],\n reporter: Reporter,\n): { system?: string; rest: NonSystemMessage[] } {\n const systemParts: string[] = [];\n const rest: NonSystemMessage[] = [];\n let started = false;\n\n for (const message of messages) {\n if (isSystem(message)) {\n if (started) {\n reporter.warn(\n 'system-midstream',\n 'A system message appeared mid conversation; it was merged into the top-level system prompt.',\n );\n }\n systemParts.push(textOf(message.content));\n continue;\n }\n started = true;\n rest.push(message);\n }\n\n const system = systemParts.length > 0 ? systemParts.join('\\n\\n') : undefined;\n return system === undefined ? { rest } : { system, rest };\n}\n","import type {\n AnthropicContentBlock,\n AnthropicConversation,\n AnthropicMessage,\n ConvertOptions,\n OpenAIAssistantMessage,\n OpenAIContentPart,\n OpenAIMessage,\n OpenAIToolMessage,\n} from '../types.js';\nimport { Reporter, isRecord, parseArguments, textOf } from '../util.js';\nimport { splitSystem } from './openai.js';\n\n/* ------------------------------------------------------------------ */\n/* OpenAI (canonical) -> Anthropic */\n/* ------------------------------------------------------------------ */\n\n/**\n * Converts a canonical OpenAI conversation into an Anthropic request fragment\n * (`{ system, messages }`). System messages move to the top-level `system`\n * field, tool-call arguments are JSON parsed into `input` objects, tool results\n * are folded into `tool_result` blocks inside a user turn, and consecutive\n * same-role turns are merged to satisfy Anthropic's strict alternation.\n */\nexport function toAnthropic(messages: OpenAIMessage[], options: ConvertOptions = {}): AnthropicConversation {\n const reporter = new Reporter(options);\n const { system, rest } = splitSystem(messages, reporter);\n const out: AnthropicMessage[] = [];\n\n for (let i = 0; i < rest.length; i++) {\n const message = rest[i];\n\n if (message.role === 'tool') {\n const blocks: AnthropicContentBlock[] = [];\n let j = i;\n while (j < rest.length && rest[j].role === 'tool') {\n const tool = rest[j] as OpenAIToolMessage;\n blocks.push({ type: 'tool_result', tool_use_id: tool.tool_call_id, content: textOf(tool.content) });\n j++;\n }\n out.push({ role: 'user', content: blocks });\n i = j - 1;\n continue;\n }\n\n if (message.role === 'user') {\n out.push({ role: 'user', content: userContent(message.content, reporter) });\n continue;\n }\n\n out.push({ role: 'assistant', content: assistantContent(message, reporter) });\n }\n\n const merged = mergeConsecutive(out, reporter);\n return system === undefined ? { messages: merged } : { system, messages: merged };\n}\n\nfunction userContent(content: string | OpenAIContentPart[], reporter: Reporter): string | AnthropicContentBlock[] {\n if (typeof content === 'string') return content;\n if (!Array.isArray(content)) return textOf(content);\n const blocks: AnthropicContentBlock[] = [];\n for (const part of content) {\n if (isRecord(part) && part.type === 'text' && typeof part.text === 'string') {\n blocks.push({ type: 'text', text: part.text });\n } else {\n reporter.warn('dropped-content', 'Dropped a non-text user content part not supported by this converter.');\n }\n }\n return blocks;\n}\n\nfunction assistantContent(message: OpenAIAssistantMessage, reporter: Reporter): string | AnthropicContentBlock[] {\n const text = textOf(message.content ?? '');\n const toolCalls = message.tool_calls ?? [];\n if (toolCalls.length === 0) return text;\n\n const blocks: AnthropicContentBlock[] = [];\n if (text) blocks.push({ type: 'text', text });\n for (const call of toolCalls) {\n blocks.push({\n type: 'tool_use',\n id: call.id,\n name: call.function.name,\n input: parseArguments(call.function.arguments, reporter, call.function.name),\n });\n }\n return blocks;\n}\n\n/** Merges adjacent same-role messages by concatenating their content blocks. */\nfunction mergeConsecutive(messages: AnthropicMessage[], reporter: Reporter): AnthropicMessage[] {\n const result: AnthropicMessage[] = [];\n for (const message of messages) {\n const previous = result[result.length - 1];\n if (previous && previous.role === message.role) {\n previous.content = [...asBlocks(previous.content), ...asBlocks(message.content)];\n reporter.warn(\n 'merged-role',\n `Merged consecutive '${message.role}' turns (Anthropic requires alternating roles).`,\n );\n } else {\n result.push({ role: message.role, content: message.content });\n }\n }\n return result;\n}\n\nfunction asBlocks(content: string | AnthropicContentBlock[]): AnthropicContentBlock[] {\n if (typeof content !== 'string') return content;\n return content ? [{ type: 'text', text: content }] : [];\n}\n\n/* ------------------------------------------------------------------ */\n/* Anthropic -> OpenAI (canonical) */\n/* ------------------------------------------------------------------ */\n\n/**\n * Converts an Anthropic request fragment back into a canonical OpenAI message\n * array. The top-level `system` becomes a leading system message, `tool_use`\n * blocks become `tool_calls` (with `input` re-serialized to a JSON string), and\n * `tool_result` blocks become standalone `role: 'tool'` messages.\n */\nexport function fromAnthropic(conversation: AnthropicConversation, options: ConvertOptions = {}): OpenAIMessage[] {\n void options;\n const out: OpenAIMessage[] = [];\n if (conversation.system) {\n out.push({ role: 'system', content: textOf(conversation.system) });\n }\n\n for (const message of conversation.messages) {\n const blocks = asBlocks(message.content);\n\n if (message.role === 'user') {\n const toolResults = blocks.filter((b) => b.type === 'tool_result');\n const textBlocks = blocks.filter((b) => b.type !== 'tool_result');\n for (const block of toolResults) {\n out.push({\n role: 'tool',\n tool_call_id: String((block as { tool_use_id?: string }).tool_use_id ?? ''),\n content: textOf((block as { content?: unknown }).content),\n });\n }\n if (textBlocks.length > 0) {\n out.push({ role: 'user', content: textOf(textBlocks) });\n }\n continue;\n }\n\n // assistant\n const text = textOf(blocks.filter((b) => b.type === 'text'));\n const toolUses = blocks.filter((b) => b.type === 'tool_use');\n const assistant: OpenAIAssistantMessage = { role: 'assistant', content: text || null };\n if (toolUses.length > 0) {\n assistant.tool_calls = toolUses.map((block) => {\n const b = block as { id: string; name: string; input: Record<string, unknown> };\n return { id: b.id, type: 'function', function: { name: b.name, arguments: JSON.stringify(b.input ?? {}) } };\n });\n }\n out.push(assistant);\n }\n\n return out;\n}\n","import type {\n ConvertOptions,\n GeminiContent,\n GeminiConversation,\n GeminiPart,\n OpenAIAssistantMessage,\n OpenAIContentPart,\n OpenAIMessage,\n OpenAIToolCall,\n OpenAIToolMessage,\n} from '../types.js';\nimport { Reporter, isRecord, parseArguments, textOf, unwrapResponse, wrapResponse } from '../util.js';\nimport { splitSystem } from './openai.js';\n\n/* ------------------------------------------------------------------ */\n/* OpenAI (canonical) -> Gemini */\n/* ------------------------------------------------------------------ */\n\n/**\n * Converts a canonical OpenAI conversation into a Gemini request fragment\n * (`{ systemInstruction, contents }`). The assistant role becomes `model`,\n * tool-call arguments are JSON parsed into `args` objects, tool results become\n * `functionResponse` parts whose `name` is recovered from the matching call, and\n * consecutive same-role turns are merged for Gemini's strict alternation.\n *\n * The OpenAI tool-call `id` is carried through as `functionCall.id` so that a\n * round trip (OpenAI -> Gemini -> OpenAI) preserves ids exactly.\n */\nexport function toGemini(messages: OpenAIMessage[], options: ConvertOptions = {}): GeminiConversation {\n const reporter = new Reporter(options);\n const { system, rest } = splitSystem(messages, reporter);\n\n const idToName = new Map<string, string>();\n for (const message of rest) {\n if (message.role === 'assistant' && message.tool_calls) {\n for (const call of message.tool_calls) idToName.set(call.id, call.function.name);\n }\n }\n\n const contents: GeminiContent[] = [];\n for (let i = 0; i < rest.length; i++) {\n const message = rest[i];\n\n if (message.role === 'tool') {\n const parts: GeminiPart[] = [];\n let j = i;\n while (j < rest.length && rest[j].role === 'tool') {\n const tool = rest[j] as OpenAIToolMessage;\n const name = idToName.get(tool.tool_call_id);\n if (!name) {\n reporter.warn(\n 'unmapped-tool-result',\n `Tool result '${tool.tool_call_id}' has no matching call; used the id as the function name.`,\n );\n }\n parts.push({\n functionResponse: {\n id: tool.tool_call_id,\n name: name ?? tool.tool_call_id,\n response: wrapResponse(textOf(tool.content)),\n },\n });\n j++;\n }\n contents.push({ role: 'user', parts });\n i = j - 1;\n continue;\n }\n\n if (message.role === 'user') {\n contents.push({ role: 'user', parts: userParts(message.content, reporter) });\n continue;\n }\n\n contents.push({ role: 'model', parts: assistantParts(message, reporter) });\n }\n\n const merged = mergeConsecutive(contents, reporter);\n return system === undefined\n ? { contents: merged }\n : { systemInstruction: { parts: [{ text: system }] }, contents: merged };\n}\n\nfunction userParts(content: string | OpenAIContentPart[], reporter: Reporter): GeminiPart[] {\n if (typeof content === 'string') return [{ text: content }];\n const parts: GeminiPart[] = [];\n for (const part of content) {\n if (isRecord(part) && part.type === 'text' && typeof part.text === 'string') {\n parts.push({ text: part.text });\n } else {\n reporter.warn('dropped-content', 'Dropped a non-text user content part not supported by this converter.');\n }\n }\n return parts.length > 0 ? parts : [{ text: '' }];\n}\n\nfunction assistantParts(message: OpenAIAssistantMessage, reporter: Reporter): GeminiPart[] {\n const parts: GeminiPart[] = [];\n const text = textOf(message.content ?? '');\n if (text) parts.push({ text });\n for (const call of message.tool_calls ?? []) {\n parts.push({\n functionCall: {\n id: call.id,\n name: call.function.name,\n args: parseArguments(call.function.arguments, reporter, call.function.name),\n },\n });\n }\n return parts.length > 0 ? parts : [{ text: '' }];\n}\n\n/** Merges adjacent same-role contents by concatenating their `parts` arrays. */\nfunction mergeConsecutive(contents: GeminiContent[], reporter: Reporter): GeminiContent[] {\n const result: GeminiContent[] = [];\n for (const content of contents) {\n const previous = result[result.length - 1];\n if (previous && previous.role === content.role) {\n previous.parts = [...previous.parts, ...content.parts];\n reporter.warn('merged-role', `Merged consecutive '${content.role}' turns (Gemini requires alternating roles).`);\n } else {\n result.push({ role: content.role, parts: [...content.parts] });\n }\n }\n return result;\n}\n\n/* ------------------------------------------------------------------ */\n/* Gemini -> OpenAI (canonical) */\n/* ------------------------------------------------------------------ */\n\n/**\n * Converts a Gemini request fragment back into a canonical OpenAI message array.\n * Because Gemini matches tool calls and responses by function name (the `id`\n * field is optional), this maintains a queue of pending calls and resolves each\n * `functionResponse` by id when present, otherwise by name in call order,\n * generating a deterministic id as a last resort.\n */\nexport function fromGemini(conversation: GeminiConversation, options: ConvertOptions = {}): OpenAIMessage[] {\n const reporter = new Reporter(options);\n const out: OpenAIMessage[] = [];\n\n if (conversation.systemInstruction) {\n const text = textOf(conversation.systemInstruction.parts);\n if (text) out.push({ role: 'system', content: text });\n }\n\n const pending: { id: string; name: string }[] = [];\n let counter = 0;\n const generateId = (name: string): string => `call_${name.replace(/[^a-zA-Z0-9_-]/g, '_')}_${counter++}`;\n\n for (const content of conversation.contents) {\n const parts = Array.isArray(content.parts) ? content.parts : [];\n\n if (content.role === 'model') {\n const textPieces: string[] = [];\n const toolCalls: OpenAIToolCall[] = [];\n for (const part of parts) {\n if (isRecord(part) && isRecord(part.functionCall)) {\n const fc = part.functionCall as { id?: string; name: string; args?: Record<string, unknown> };\n const id = fc.id ?? generateId(fc.name);\n if (!fc.id) reporter.warn('generated-id', `Gemini functionCall '${fc.name}' had no id; generated '${id}'.`);\n toolCalls.push({\n id,\n type: 'function',\n function: { name: fc.name, arguments: JSON.stringify(fc.args ?? {}) },\n });\n pending.push({ id, name: fc.name });\n } else if (isRecord(part) && typeof part.text === 'string') {\n textPieces.push(part.text);\n }\n }\n const text = textPieces.join('');\n const assistant: OpenAIAssistantMessage = { role: 'assistant', content: text || null };\n if (toolCalls.length > 0) assistant.tool_calls = toolCalls;\n out.push(assistant);\n continue;\n }\n\n // role 'user' or unspecified\n const textPieces: string[] = [];\n for (const part of parts) {\n if (isRecord(part) && isRecord(part.functionResponse)) {\n const fr = part.functionResponse as { id?: string; name: string; response?: Record<string, unknown> };\n const id = resolveResponseId(fr, pending, reporter, generateId);\n out.push({ role: 'tool', tool_call_id: id, content: unwrapResponse(fr.response ?? {}) });\n } else if (isRecord(part) && typeof part.text === 'string') {\n textPieces.push(part.text);\n }\n }\n const text = textPieces.join('');\n if (text) out.push({ role: 'user', content: text });\n }\n\n return out;\n}\n\nfunction resolveResponseId(\n response: { id?: string; name: string },\n pending: { id: string; name: string }[],\n reporter: Reporter,\n generateId: (name: string) => string,\n): string {\n if (response.id) {\n const index = pending.findIndex((p) => p.id === response.id);\n if (index >= 0) pending.splice(index, 1);\n return response.id;\n }\n const index = pending.findIndex((p) => p.name === response.name);\n if (index >= 0) {\n const { id } = pending[index];\n pending.splice(index, 1);\n return id;\n }\n const id = generateId(response.name);\n reporter.warn(\n 'unmapped-tool-result',\n `Gemini functionResponse for '${response.name}' had no matching call; generated '${id}'.`,\n );\n return id;\n}\n","import type { AnthropicConversation, ConvertOptions, GeminiConversation, OpenAIMessage, Provider } from './types.js';\nimport { fromAnthropic, toAnthropic } from './providers/anthropic.js';\nimport { fromGemini, toGemini } from './providers/gemini.js';\n\n/** Maps a provider to the conversation shape it accepts and returns. */\nexport type ConversationOf<P extends Provider> = P extends 'openai'\n ? OpenAIMessage[]\n : P extends 'anthropic'\n ? AnthropicConversation\n : P extends 'gemini'\n ? GeminiConversation\n : never;\n\n/**\n * Converts a conversation from one provider format to another. Every conversion\n * routes through the canonical OpenAI representation, so any source/target pair\n * is supported, including same-provider normalization.\n *\n * @example\n * const gemini = convert(openaiMessages, { from: 'openai', to: 'gemini' });\n * const openai = convert(anthropicBody, { from: 'anthropic', to: 'openai' });\n */\nexport function convert<From extends Provider, To extends Provider>(\n conversation: ConversationOf<From>,\n route: { from: From; to: To },\n options: ConvertOptions = {},\n): ConversationOf<To> {\n const canonical = toCanonical(conversation, route.from, options);\n return fromCanonical(canonical, route.to, options) as ConversationOf<To>;\n}\n\nfunction toCanonical(conversation: unknown, from: Provider, options: ConvertOptions): OpenAIMessage[] {\n switch (from) {\n case 'openai':\n return conversation as OpenAIMessage[];\n case 'anthropic':\n return fromAnthropic(conversation as AnthropicConversation, options);\n case 'gemini':\n return fromGemini(conversation as GeminiConversation, options);\n default:\n throw new Error(`Unknown source provider: ${String(from)}`);\n }\n}\n\nfunction fromCanonical(canonical: OpenAIMessage[], to: Provider, options: ConvertOptions): ConversationOf<Provider> {\n switch (to) {\n case 'openai':\n return canonical;\n case 'anthropic':\n return toAnthropic(canonical, options);\n case 'gemini':\n return toGemini(canonical, options);\n default:\n throw new Error(`Unknown target provider: ${String(to)}`);\n }\n}\n"],"mappings":"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","names":["mergeConsecutive","textPieces","text","index","id"]}
|