llm-messages 0.1.0 → 0.3.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 +28 -0
- package/README.md +48 -4
- package/dist/index.cjs +242 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +82 -6
- package/dist/index.d.ts +82 -6
- package/dist/index.js +236 -16
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
package/dist/index.d.cts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
/** A supported provider. */
|
|
11
11
|
type Provider = 'openai' | 'anthropic' | 'gemini';
|
|
12
12
|
/** A stable, machine readable code describing a non fatal conversion event. */
|
|
13
|
-
type WarningCode = 'generated-id' | 'unmapped-tool-result' | 'merged-role' | 'dropped-content' | 'invalid-json-arguments' | 'system-midstream';
|
|
13
|
+
type WarningCode = 'generated-id' | 'unmapped-tool-result' | 'merged-role' | 'dropped-content' | 'invalid-json-arguments' | 'system-midstream' | 'gemini-url-image';
|
|
14
14
|
/** A non fatal event raised during conversion. */
|
|
15
15
|
interface Warning {
|
|
16
16
|
code: WarningCode;
|
|
@@ -25,8 +25,16 @@ interface OpenAITextPart {
|
|
|
25
25
|
type: 'text';
|
|
26
26
|
text: string;
|
|
27
27
|
}
|
|
28
|
-
|
|
29
|
-
type
|
|
28
|
+
interface OpenAIImagePart {
|
|
29
|
+
type: 'image_url';
|
|
30
|
+
/** `url` is a remote https URL or a `data:<mediaType>;base64,<data>` data URL. */
|
|
31
|
+
image_url: {
|
|
32
|
+
url: string;
|
|
33
|
+
detail?: 'auto' | 'low' | 'high' | 'original';
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/** A content part. Unknown part types (audio, files) are preserved verbatim. */
|
|
37
|
+
type OpenAIContentPart = OpenAITextPart | OpenAIImagePart | {
|
|
30
38
|
type: string;
|
|
31
39
|
[key: string]: unknown;
|
|
32
40
|
};
|
|
@@ -80,7 +88,18 @@ interface AnthropicToolResultBlock {
|
|
|
80
88
|
content: string | AnthropicTextBlock[];
|
|
81
89
|
is_error?: boolean;
|
|
82
90
|
}
|
|
83
|
-
|
|
91
|
+
interface AnthropicImageBlock {
|
|
92
|
+
type: 'image';
|
|
93
|
+
source: {
|
|
94
|
+
type: 'base64';
|
|
95
|
+
media_type: string;
|
|
96
|
+
data: string;
|
|
97
|
+
} | {
|
|
98
|
+
type: 'url';
|
|
99
|
+
url: string;
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
type AnthropicContentBlock = AnthropicTextBlock | AnthropicToolUseBlock | AnthropicToolResultBlock | AnthropicImageBlock | {
|
|
84
103
|
type: string;
|
|
85
104
|
[key: string]: unknown;
|
|
86
105
|
};
|
|
@@ -112,7 +131,19 @@ interface GeminiFunctionResponsePart {
|
|
|
112
131
|
response: Record<string, unknown>;
|
|
113
132
|
};
|
|
114
133
|
}
|
|
115
|
-
|
|
134
|
+
interface GeminiInlineDataPart {
|
|
135
|
+
inlineData: {
|
|
136
|
+
mimeType: string;
|
|
137
|
+
data: string;
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
interface GeminiFileDataPart {
|
|
141
|
+
fileData: {
|
|
142
|
+
mimeType?: string;
|
|
143
|
+
fileUri: string;
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
type GeminiPart = GeminiTextPart | GeminiFunctionCallPart | GeminiFunctionResponsePart | GeminiInlineDataPart | GeminiFileDataPart | Record<string, unknown>;
|
|
116
147
|
interface GeminiContent {
|
|
117
148
|
/** `model` is Gemini's name for the assistant role. */
|
|
118
149
|
role?: 'user' | 'model';
|
|
@@ -176,4 +207,49 @@ declare function convert<From extends Provider, To extends Provider>(conversatio
|
|
|
176
207
|
to: To;
|
|
177
208
|
}, options?: ConvertOptions): ConversationOf<To>;
|
|
178
209
|
|
|
179
|
-
|
|
210
|
+
/**
|
|
211
|
+
* A provider-neutral image. `base64` carries the raw bytes plus media type
|
|
212
|
+
* (mapping to a data URL, an Anthropic base64 source or a Gemini `inlineData`
|
|
213
|
+
* part); `url` carries a remote reference.
|
|
214
|
+
*/
|
|
215
|
+
type NormalizedImage = {
|
|
216
|
+
kind: 'base64';
|
|
217
|
+
mediaType: string;
|
|
218
|
+
data: string;
|
|
219
|
+
} | {
|
|
220
|
+
kind: 'url';
|
|
221
|
+
url: string;
|
|
222
|
+
};
|
|
223
|
+
/** Decomposes a `data:<mediaType>;base64,<data>` URL. Returns null otherwise. */
|
|
224
|
+
declare function parseDataUrl(url: string): {
|
|
225
|
+
mediaType: string;
|
|
226
|
+
data: string;
|
|
227
|
+
} | null;
|
|
228
|
+
/** Reassembles a base64 data URL. The inverse of {@link parseDataUrl}. */
|
|
229
|
+
declare function toDataUrl(mediaType: string, data: string): string;
|
|
230
|
+
|
|
231
|
+
/** A provider-neutral finish reason. */
|
|
232
|
+
type FinishReason = 'stop' | 'tool_calls' | 'length' | 'content_filter' | 'unknown';
|
|
233
|
+
/** Provider-neutral token usage. */
|
|
234
|
+
interface Usage {
|
|
235
|
+
inputTokens: number;
|
|
236
|
+
outputTokens: number;
|
|
237
|
+
}
|
|
238
|
+
/** A provider response normalized to the canonical OpenAI assistant shape. */
|
|
239
|
+
interface NormalizedResponse {
|
|
240
|
+
message: OpenAIAssistantMessage;
|
|
241
|
+
finishReason: FinishReason;
|
|
242
|
+
usage: Usage;
|
|
243
|
+
}
|
|
244
|
+
/** Normalizes an OpenAI Chat Completions response body. */
|
|
245
|
+
declare function responseFromOpenAI(body: unknown): NormalizedResponse;
|
|
246
|
+
/** Normalizes an Anthropic Messages response body. */
|
|
247
|
+
declare function responseFromAnthropic(body: unknown): NormalizedResponse;
|
|
248
|
+
/** Normalizes a Gemini generateContent response body. */
|
|
249
|
+
declare function responseFromGemini(body: unknown, options?: ConvertOptions): NormalizedResponse;
|
|
250
|
+
/** Normalizes a provider response body into the canonical shape. */
|
|
251
|
+
declare function normalizeResponse(body: unknown, route: {
|
|
252
|
+
from: Provider;
|
|
253
|
+
}, options?: ConvertOptions): NormalizedResponse;
|
|
254
|
+
|
|
255
|
+
export { type AnthropicContentBlock, type AnthropicConversation, type AnthropicImageBlock, type AnthropicMessage, type AnthropicTextBlock, type AnthropicToolResultBlock, type AnthropicToolUseBlock, type ConversationOf, type ConvertOptions, type FinishReason, type GeminiContent, type GeminiConversation, type GeminiFileDataPart, type GeminiFunctionCallPart, type GeminiFunctionResponsePart, type GeminiInlineDataPart, type GeminiPart, type GeminiTextPart, type NormalizedImage, type NormalizedResponse, type OpenAIAssistantMessage, type OpenAIContentPart, type OpenAIImagePart, type OpenAIMessage, type OpenAISystemMessage, type OpenAITextPart, type OpenAIToolCall, type OpenAIToolMessage, type OpenAIUserMessage, type Provider, type Usage, type Warning, type WarningCode, convert, fromAnthropic, fromGemini, normalizeResponse, parseDataUrl, responseFromAnthropic, responseFromGemini, responseFromOpenAI, toAnthropic, toDataUrl, toGemini };
|
package/dist/index.d.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
/** A supported provider. */
|
|
11
11
|
type Provider = 'openai' | 'anthropic' | 'gemini';
|
|
12
12
|
/** A stable, machine readable code describing a non fatal conversion event. */
|
|
13
|
-
type WarningCode = 'generated-id' | 'unmapped-tool-result' | 'merged-role' | 'dropped-content' | 'invalid-json-arguments' | 'system-midstream';
|
|
13
|
+
type WarningCode = 'generated-id' | 'unmapped-tool-result' | 'merged-role' | 'dropped-content' | 'invalid-json-arguments' | 'system-midstream' | 'gemini-url-image';
|
|
14
14
|
/** A non fatal event raised during conversion. */
|
|
15
15
|
interface Warning {
|
|
16
16
|
code: WarningCode;
|
|
@@ -25,8 +25,16 @@ interface OpenAITextPart {
|
|
|
25
25
|
type: 'text';
|
|
26
26
|
text: string;
|
|
27
27
|
}
|
|
28
|
-
|
|
29
|
-
type
|
|
28
|
+
interface OpenAIImagePart {
|
|
29
|
+
type: 'image_url';
|
|
30
|
+
/** `url` is a remote https URL or a `data:<mediaType>;base64,<data>` data URL. */
|
|
31
|
+
image_url: {
|
|
32
|
+
url: string;
|
|
33
|
+
detail?: 'auto' | 'low' | 'high' | 'original';
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/** A content part. Unknown part types (audio, files) are preserved verbatim. */
|
|
37
|
+
type OpenAIContentPart = OpenAITextPart | OpenAIImagePart | {
|
|
30
38
|
type: string;
|
|
31
39
|
[key: string]: unknown;
|
|
32
40
|
};
|
|
@@ -80,7 +88,18 @@ interface AnthropicToolResultBlock {
|
|
|
80
88
|
content: string | AnthropicTextBlock[];
|
|
81
89
|
is_error?: boolean;
|
|
82
90
|
}
|
|
83
|
-
|
|
91
|
+
interface AnthropicImageBlock {
|
|
92
|
+
type: 'image';
|
|
93
|
+
source: {
|
|
94
|
+
type: 'base64';
|
|
95
|
+
media_type: string;
|
|
96
|
+
data: string;
|
|
97
|
+
} | {
|
|
98
|
+
type: 'url';
|
|
99
|
+
url: string;
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
type AnthropicContentBlock = AnthropicTextBlock | AnthropicToolUseBlock | AnthropicToolResultBlock | AnthropicImageBlock | {
|
|
84
103
|
type: string;
|
|
85
104
|
[key: string]: unknown;
|
|
86
105
|
};
|
|
@@ -112,7 +131,19 @@ interface GeminiFunctionResponsePart {
|
|
|
112
131
|
response: Record<string, unknown>;
|
|
113
132
|
};
|
|
114
133
|
}
|
|
115
|
-
|
|
134
|
+
interface GeminiInlineDataPart {
|
|
135
|
+
inlineData: {
|
|
136
|
+
mimeType: string;
|
|
137
|
+
data: string;
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
interface GeminiFileDataPart {
|
|
141
|
+
fileData: {
|
|
142
|
+
mimeType?: string;
|
|
143
|
+
fileUri: string;
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
type GeminiPart = GeminiTextPart | GeminiFunctionCallPart | GeminiFunctionResponsePart | GeminiInlineDataPart | GeminiFileDataPart | Record<string, unknown>;
|
|
116
147
|
interface GeminiContent {
|
|
117
148
|
/** `model` is Gemini's name for the assistant role. */
|
|
118
149
|
role?: 'user' | 'model';
|
|
@@ -176,4 +207,49 @@ declare function convert<From extends Provider, To extends Provider>(conversatio
|
|
|
176
207
|
to: To;
|
|
177
208
|
}, options?: ConvertOptions): ConversationOf<To>;
|
|
178
209
|
|
|
179
|
-
|
|
210
|
+
/**
|
|
211
|
+
* A provider-neutral image. `base64` carries the raw bytes plus media type
|
|
212
|
+
* (mapping to a data URL, an Anthropic base64 source or a Gemini `inlineData`
|
|
213
|
+
* part); `url` carries a remote reference.
|
|
214
|
+
*/
|
|
215
|
+
type NormalizedImage = {
|
|
216
|
+
kind: 'base64';
|
|
217
|
+
mediaType: string;
|
|
218
|
+
data: string;
|
|
219
|
+
} | {
|
|
220
|
+
kind: 'url';
|
|
221
|
+
url: string;
|
|
222
|
+
};
|
|
223
|
+
/** Decomposes a `data:<mediaType>;base64,<data>` URL. Returns null otherwise. */
|
|
224
|
+
declare function parseDataUrl(url: string): {
|
|
225
|
+
mediaType: string;
|
|
226
|
+
data: string;
|
|
227
|
+
} | null;
|
|
228
|
+
/** Reassembles a base64 data URL. The inverse of {@link parseDataUrl}. */
|
|
229
|
+
declare function toDataUrl(mediaType: string, data: string): string;
|
|
230
|
+
|
|
231
|
+
/** A provider-neutral finish reason. */
|
|
232
|
+
type FinishReason = 'stop' | 'tool_calls' | 'length' | 'content_filter' | 'unknown';
|
|
233
|
+
/** Provider-neutral token usage. */
|
|
234
|
+
interface Usage {
|
|
235
|
+
inputTokens: number;
|
|
236
|
+
outputTokens: number;
|
|
237
|
+
}
|
|
238
|
+
/** A provider response normalized to the canonical OpenAI assistant shape. */
|
|
239
|
+
interface NormalizedResponse {
|
|
240
|
+
message: OpenAIAssistantMessage;
|
|
241
|
+
finishReason: FinishReason;
|
|
242
|
+
usage: Usage;
|
|
243
|
+
}
|
|
244
|
+
/** Normalizes an OpenAI Chat Completions response body. */
|
|
245
|
+
declare function responseFromOpenAI(body: unknown): NormalizedResponse;
|
|
246
|
+
/** Normalizes an Anthropic Messages response body. */
|
|
247
|
+
declare function responseFromAnthropic(body: unknown): NormalizedResponse;
|
|
248
|
+
/** Normalizes a Gemini generateContent response body. */
|
|
249
|
+
declare function responseFromGemini(body: unknown, options?: ConvertOptions): NormalizedResponse;
|
|
250
|
+
/** Normalizes a provider response body into the canonical shape. */
|
|
251
|
+
declare function normalizeResponse(body: unknown, route: {
|
|
252
|
+
from: Provider;
|
|
253
|
+
}, options?: ConvertOptions): NormalizedResponse;
|
|
254
|
+
|
|
255
|
+
export { type AnthropicContentBlock, type AnthropicConversation, type AnthropicImageBlock, type AnthropicMessage, type AnthropicTextBlock, type AnthropicToolResultBlock, type AnthropicToolUseBlock, type ConversationOf, type ConvertOptions, type FinishReason, type GeminiContent, type GeminiConversation, type GeminiFileDataPart, type GeminiFunctionCallPart, type GeminiFunctionResponsePart, type GeminiInlineDataPart, type GeminiPart, type GeminiTextPart, type NormalizedImage, type NormalizedResponse, type OpenAIAssistantMessage, type OpenAIContentPart, type OpenAIImagePart, type OpenAIMessage, type OpenAISystemMessage, type OpenAITextPart, type OpenAIToolCall, type OpenAIToolMessage, type OpenAIUserMessage, type Provider, type Usage, type Warning, type WarningCode, convert, fromAnthropic, fromGemini, normalizeResponse, parseDataUrl, responseFromAnthropic, responseFromGemini, responseFromOpenAI, toAnthropic, toDataUrl, toGemini };
|
package/dist/index.js
CHANGED
|
@@ -51,6 +51,68 @@ function unwrapResponse(response) {
|
|
|
51
51
|
return JSON.stringify(response);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
// src/image.ts
|
|
55
|
+
var DATA_URL = /^data:([^;,]+);base64,(.*)$/s;
|
|
56
|
+
function parseDataUrl(url) {
|
|
57
|
+
const match = DATA_URL.exec(url);
|
|
58
|
+
if (!match) return null;
|
|
59
|
+
return { mediaType: match[1], data: match[2] };
|
|
60
|
+
}
|
|
61
|
+
function toDataUrl(mediaType, data) {
|
|
62
|
+
return `data:${mediaType};base64,${data}`;
|
|
63
|
+
}
|
|
64
|
+
function imageFromOpenAI(part) {
|
|
65
|
+
if (!isRecord(part) || part.type !== "image_url" || !isRecord(part.image_url)) return null;
|
|
66
|
+
const url = part.image_url.url;
|
|
67
|
+
if (typeof url !== "string") return null;
|
|
68
|
+
const parsed = parseDataUrl(url);
|
|
69
|
+
return parsed ? { kind: "base64", ...parsed } : { kind: "url", url };
|
|
70
|
+
}
|
|
71
|
+
function imageToOpenAI(image) {
|
|
72
|
+
const url = image.kind === "base64" ? toDataUrl(image.mediaType, image.data) : image.url;
|
|
73
|
+
return { type: "image_url", image_url: { url } };
|
|
74
|
+
}
|
|
75
|
+
function imageFromAnthropic(block) {
|
|
76
|
+
if (!isRecord(block) || block.type !== "image" || !isRecord(block.source)) return null;
|
|
77
|
+
const source = block.source;
|
|
78
|
+
if (source.type === "base64" && typeof source.media_type === "string" && typeof source.data === "string") {
|
|
79
|
+
return { kind: "base64", mediaType: source.media_type, data: source.data };
|
|
80
|
+
}
|
|
81
|
+
if (source.type === "url" && typeof source.url === "string") {
|
|
82
|
+
return { kind: "url", url: source.url };
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
function imageToAnthropic(image) {
|
|
87
|
+
if (image.kind === "base64") {
|
|
88
|
+
return { type: "image", source: { type: "base64", media_type: image.mediaType, data: image.data } };
|
|
89
|
+
}
|
|
90
|
+
return { type: "image", source: { type: "url", url: image.url } };
|
|
91
|
+
}
|
|
92
|
+
function imageFromGemini(part) {
|
|
93
|
+
if (isRecord(part) && isRecord(part.inlineData)) {
|
|
94
|
+
const data = part.inlineData;
|
|
95
|
+
if (typeof data.mimeType === "string" && typeof data.data === "string") {
|
|
96
|
+
return { kind: "base64", mediaType: data.mimeType, data: data.data };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (isRecord(part) && isRecord(part.fileData)) {
|
|
100
|
+
const data = part.fileData;
|
|
101
|
+
if (typeof data.fileUri === "string") return { kind: "url", url: data.fileUri };
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
function imageToGemini(image, reporter) {
|
|
106
|
+
if (image.kind === "base64") {
|
|
107
|
+
return { inlineData: { mimeType: image.mediaType, data: image.data } };
|
|
108
|
+
}
|
|
109
|
+
reporter.warn(
|
|
110
|
+
"gemini-url-image",
|
|
111
|
+
"A remote image URL was emitted as Gemini fileData.fileUri; Gemini may require the Files API for non-Google URIs."
|
|
112
|
+
);
|
|
113
|
+
return { fileData: { fileUri: image.url } };
|
|
114
|
+
}
|
|
115
|
+
|
|
54
116
|
// src/providers/openai.ts
|
|
55
117
|
function isSystem(message) {
|
|
56
118
|
return message.role === "system" || message.role === "developer";
|
|
@@ -112,9 +174,14 @@ function userContent(content, reporter) {
|
|
|
112
174
|
for (const part of content) {
|
|
113
175
|
if (isRecord(part) && part.type === "text" && typeof part.text === "string") {
|
|
114
176
|
blocks.push({ type: "text", text: part.text });
|
|
115
|
-
|
|
116
|
-
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
const image = imageFromOpenAI(part);
|
|
180
|
+
if (image) {
|
|
181
|
+
blocks.push(imageToAnthropic(image));
|
|
182
|
+
continue;
|
|
117
183
|
}
|
|
184
|
+
reporter.warn("dropped-content", "Dropped an unsupported user content part.");
|
|
118
185
|
}
|
|
119
186
|
return blocks;
|
|
120
187
|
}
|
|
@@ -164,7 +231,7 @@ function fromAnthropic(conversation, options = {}) {
|
|
|
164
231
|
const blocks = asBlocks(message.content);
|
|
165
232
|
if (message.role === "user") {
|
|
166
233
|
const toolResults = blocks.filter((b) => b.type === "tool_result");
|
|
167
|
-
const
|
|
234
|
+
const contentBlocks = blocks.filter((b) => b.type !== "tool_result");
|
|
168
235
|
for (const block of toolResults) {
|
|
169
236
|
out.push({
|
|
170
237
|
role: "tool",
|
|
@@ -172,8 +239,8 @@ function fromAnthropic(conversation, options = {}) {
|
|
|
172
239
|
content: textOf(block.content)
|
|
173
240
|
});
|
|
174
241
|
}
|
|
175
|
-
if (
|
|
176
|
-
out.push({ role: "user", content:
|
|
242
|
+
if (contentBlocks.length > 0) {
|
|
243
|
+
out.push({ role: "user", content: userContentToOpenAI(contentBlocks) });
|
|
177
244
|
}
|
|
178
245
|
continue;
|
|
179
246
|
}
|
|
@@ -190,6 +257,20 @@ function fromAnthropic(conversation, options = {}) {
|
|
|
190
257
|
}
|
|
191
258
|
return out;
|
|
192
259
|
}
|
|
260
|
+
function userContentToOpenAI(blocks) {
|
|
261
|
+
const hasImage = blocks.some((block) => imageFromAnthropic(block) !== null);
|
|
262
|
+
if (!hasImage) return textOf(blocks);
|
|
263
|
+
const parts = [];
|
|
264
|
+
for (const block of blocks) {
|
|
265
|
+
const image = imageFromAnthropic(block);
|
|
266
|
+
if (image) {
|
|
267
|
+
parts.push(imageToOpenAI(image));
|
|
268
|
+
} else if (isRecord(block) && block.type === "text" && typeof block.text === "string") {
|
|
269
|
+
parts.push({ type: "text", text: block.text });
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return parts;
|
|
273
|
+
}
|
|
193
274
|
|
|
194
275
|
// src/providers/gemini.ts
|
|
195
276
|
function toGemini(messages, options = {}) {
|
|
@@ -244,9 +325,14 @@ function userParts(content, reporter) {
|
|
|
244
325
|
for (const part of content) {
|
|
245
326
|
if (isRecord(part) && part.type === "text" && typeof part.text === "string") {
|
|
246
327
|
parts.push({ text: part.text });
|
|
247
|
-
|
|
248
|
-
reporter.warn("dropped-content", "Dropped a non-text user content part not supported by this converter.");
|
|
328
|
+
continue;
|
|
249
329
|
}
|
|
330
|
+
const image = imageFromOpenAI(part);
|
|
331
|
+
if (image) {
|
|
332
|
+
parts.push(imageToGemini(image, reporter));
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
reporter.warn("dropped-content", "Dropped an unsupported user content part.");
|
|
250
336
|
}
|
|
251
337
|
return parts.length > 0 ? parts : [{ text: "" }];
|
|
252
338
|
}
|
|
@@ -291,7 +377,7 @@ function fromGemini(conversation, options = {}) {
|
|
|
291
377
|
for (const content of conversation.contents) {
|
|
292
378
|
const parts = Array.isArray(content.parts) ? content.parts : [];
|
|
293
379
|
if (content.role === "model") {
|
|
294
|
-
const
|
|
380
|
+
const textPieces = [];
|
|
295
381
|
const toolCalls = [];
|
|
296
382
|
for (const part of parts) {
|
|
297
383
|
if (isRecord(part) && isRecord(part.functionCall)) {
|
|
@@ -305,27 +391,42 @@ function fromGemini(conversation, options = {}) {
|
|
|
305
391
|
});
|
|
306
392
|
pending.push({ id, name: fc.name });
|
|
307
393
|
} else if (isRecord(part) && typeof part.text === "string") {
|
|
308
|
-
|
|
394
|
+
textPieces.push(part.text);
|
|
309
395
|
}
|
|
310
396
|
}
|
|
311
|
-
const
|
|
312
|
-
const assistant = { role: "assistant", content:
|
|
397
|
+
const text = textPieces.join("");
|
|
398
|
+
const assistant = { role: "assistant", content: text || null };
|
|
313
399
|
if (toolCalls.length > 0) assistant.tool_calls = toolCalls;
|
|
314
400
|
out.push(assistant);
|
|
315
401
|
continue;
|
|
316
402
|
}
|
|
317
|
-
const
|
|
403
|
+
const contentParts = [];
|
|
404
|
+
let hasImage = false;
|
|
318
405
|
for (const part of parts) {
|
|
319
406
|
if (isRecord(part) && isRecord(part.functionResponse)) {
|
|
320
407
|
const fr = part.functionResponse;
|
|
321
408
|
const id = resolveResponseId(fr, pending, reporter, generateId);
|
|
322
409
|
out.push({ role: "tool", tool_call_id: id, content: unwrapResponse(fr.response ?? {}) });
|
|
323
|
-
|
|
324
|
-
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
const image = imageFromGemini(part);
|
|
413
|
+
if (image) {
|
|
414
|
+
contentParts.push(imageToOpenAI(image));
|
|
415
|
+
hasImage = true;
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
if (isRecord(part) && typeof part.text === "string") {
|
|
419
|
+
contentParts.push({ type: "text", text: part.text });
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
if (contentParts.length > 0) {
|
|
423
|
+
if (hasImage) {
|
|
424
|
+
out.push({ role: "user", content: contentParts });
|
|
425
|
+
} else {
|
|
426
|
+
const text = textOf(contentParts);
|
|
427
|
+
if (text) out.push({ role: "user", content: text });
|
|
325
428
|
}
|
|
326
429
|
}
|
|
327
|
-
const text = textPieces.join("");
|
|
328
|
-
if (text) out.push({ role: "user", content: text });
|
|
329
430
|
}
|
|
330
431
|
return out;
|
|
331
432
|
}
|
|
@@ -378,11 +479,130 @@ function fromCanonical(canonical, to, options) {
|
|
|
378
479
|
throw new Error(`Unknown target provider: ${String(to)}`);
|
|
379
480
|
}
|
|
380
481
|
}
|
|
482
|
+
|
|
483
|
+
// src/response.ts
|
|
484
|
+
var num = (value) => typeof value === "number" ? value : 0;
|
|
485
|
+
function buildMessage(text, toolCalls) {
|
|
486
|
+
const message = { role: "assistant", content: text ? text : null };
|
|
487
|
+
if (toolCalls.length > 0) message.tool_calls = toolCalls;
|
|
488
|
+
return message;
|
|
489
|
+
}
|
|
490
|
+
function finalReason(mapped, toolCalls) {
|
|
491
|
+
return toolCalls.length > 0 ? "tool_calls" : mapped;
|
|
492
|
+
}
|
|
493
|
+
var OPENAI_FINISH = {
|
|
494
|
+
stop: "stop",
|
|
495
|
+
length: "length",
|
|
496
|
+
tool_calls: "tool_calls",
|
|
497
|
+
content_filter: "content_filter",
|
|
498
|
+
function_call: "tool_calls"
|
|
499
|
+
};
|
|
500
|
+
function responseFromOpenAI(body) {
|
|
501
|
+
const root = isRecord(body) ? body : {};
|
|
502
|
+
const choice = Array.isArray(root.choices) && isRecord(root.choices[0]) ? root.choices[0] : {};
|
|
503
|
+
const message = isRecord(choice.message) ? choice.message : {};
|
|
504
|
+
const text = typeof message.content === "string" ? message.content : textOf(message.content);
|
|
505
|
+
const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
|
|
506
|
+
const usage = isRecord(root.usage) ? root.usage : {};
|
|
507
|
+
return {
|
|
508
|
+
message: buildMessage(text, toolCalls),
|
|
509
|
+
finishReason: finalReason(OPENAI_FINISH[String(choice.finish_reason)] ?? "unknown", toolCalls),
|
|
510
|
+
usage: { inputTokens: num(usage.prompt_tokens), outputTokens: num(usage.completion_tokens) }
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
var ANTHROPIC_FINISH = {
|
|
514
|
+
end_turn: "stop",
|
|
515
|
+
stop_sequence: "stop",
|
|
516
|
+
tool_use: "tool_calls",
|
|
517
|
+
max_tokens: "length",
|
|
518
|
+
refusal: "content_filter",
|
|
519
|
+
pause_turn: "unknown"
|
|
520
|
+
};
|
|
521
|
+
function responseFromAnthropic(body) {
|
|
522
|
+
const root = isRecord(body) ? body : {};
|
|
523
|
+
const blocks = Array.isArray(root.content) ? root.content : [];
|
|
524
|
+
const textPieces = [];
|
|
525
|
+
const toolCalls = [];
|
|
526
|
+
for (const block of blocks) {
|
|
527
|
+
if (!isRecord(block)) continue;
|
|
528
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
529
|
+
textPieces.push(block.text);
|
|
530
|
+
} else if (block.type === "tool_use" && typeof block.name === "string") {
|
|
531
|
+
toolCalls.push({
|
|
532
|
+
id: typeof block.id === "string" ? block.id : "",
|
|
533
|
+
type: "function",
|
|
534
|
+
function: { name: block.name, arguments: JSON.stringify(block.input ?? {}) }
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
const usage = isRecord(root.usage) ? root.usage : {};
|
|
539
|
+
return {
|
|
540
|
+
message: buildMessage(textPieces.join(""), toolCalls),
|
|
541
|
+
finishReason: finalReason(ANTHROPIC_FINISH[String(root.stop_reason)] ?? "unknown", toolCalls),
|
|
542
|
+
usage: { inputTokens: num(usage.input_tokens), outputTokens: num(usage.output_tokens) }
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
var GEMINI_FINISH = {
|
|
546
|
+
STOP: "stop",
|
|
547
|
+
MAX_TOKENS: "length",
|
|
548
|
+
SAFETY: "content_filter",
|
|
549
|
+
RECITATION: "content_filter",
|
|
550
|
+
MALFORMED_FUNCTION_CALL: "content_filter"
|
|
551
|
+
};
|
|
552
|
+
function responseFromGemini(body, options = {}) {
|
|
553
|
+
const reporter = new Reporter(options);
|
|
554
|
+
const root = isRecord(body) ? body : {};
|
|
555
|
+
const candidate = Array.isArray(root.candidates) && isRecord(root.candidates[0]) ? root.candidates[0] : {};
|
|
556
|
+
const content = isRecord(candidate.content) ? candidate.content : {};
|
|
557
|
+
const parts = Array.isArray(content.parts) ? content.parts : [];
|
|
558
|
+
const textPieces = [];
|
|
559
|
+
const toolCalls = [];
|
|
560
|
+
let counter = 0;
|
|
561
|
+
for (const part of parts) {
|
|
562
|
+
if (!isRecord(part)) continue;
|
|
563
|
+
if (isRecord(part.functionCall)) {
|
|
564
|
+
const call = part.functionCall;
|
|
565
|
+
const name = typeof call.name === "string" ? call.name : "function";
|
|
566
|
+
let id = call.id;
|
|
567
|
+
if (!id) {
|
|
568
|
+
id = `call_${name.replace(/[^a-zA-Z0-9_-]/g, "_")}_${counter++}`;
|
|
569
|
+
reporter.warn("generated-id", `Gemini functionCall '${name}' had no id; generated '${id}'.`);
|
|
570
|
+
}
|
|
571
|
+
toolCalls.push({ id, type: "function", function: { name, arguments: JSON.stringify(call.args ?? {}) } });
|
|
572
|
+
} else if (typeof part.text === "string") {
|
|
573
|
+
textPieces.push(part.text);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
const usage = isRecord(root.usageMetadata) ? root.usageMetadata : {};
|
|
577
|
+
return {
|
|
578
|
+
message: buildMessage(textPieces.join(""), toolCalls),
|
|
579
|
+
finishReason: finalReason(GEMINI_FINISH[String(candidate.finishReason)] ?? "unknown", toolCalls),
|
|
580
|
+
usage: { inputTokens: num(usage.promptTokenCount), outputTokens: num(usage.candidatesTokenCount) }
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
function normalizeResponse(body, route, options = {}) {
|
|
584
|
+
switch (route.from) {
|
|
585
|
+
case "openai":
|
|
586
|
+
return responseFromOpenAI(body);
|
|
587
|
+
case "anthropic":
|
|
588
|
+
return responseFromAnthropic(body);
|
|
589
|
+
case "gemini":
|
|
590
|
+
return responseFromGemini(body, options);
|
|
591
|
+
default:
|
|
592
|
+
throw new Error(`Unknown source provider: ${String(route.from)}`);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
381
595
|
export {
|
|
382
596
|
convert,
|
|
383
597
|
fromAnthropic,
|
|
384
598
|
fromGemini,
|
|
599
|
+
normalizeResponse,
|
|
600
|
+
parseDataUrl,
|
|
601
|
+
responseFromAnthropic,
|
|
602
|
+
responseFromGemini,
|
|
603
|
+
responseFromOpenAI,
|
|
385
604
|
toAnthropic,
|
|
605
|
+
toDataUrl,
|
|
386
606
|
toGemini
|
|
387
607
|
};
|
|
388
608
|
//# sourceMappingURL=index.js.map
|