@mushinjs/plugin-provider-gemini 0.0.0-stage → 0.2.0-next.20261113103537
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/LICENSE +21 -0
- package/dist/convert.d.ts +95 -0
- package/dist/convert.js +156 -0
- package/dist/convert.js.map +1 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +180 -0
- package/dist/index.js.map +1 -0
- package/package.json +45 -3
- package/README.md +0 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Setobox
|
|
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.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { ChatResponse, FinishReason, LlmMessage, ToolDefinition } from "@mushinjs/plugin-providers";
|
|
2
|
+
/** `RawTurn.type` of assistant turns from this provider; their parts are replayed verbatim. */
|
|
3
|
+
export declare const TYPE = "gemini";
|
|
4
|
+
export interface GeminiPart {
|
|
5
|
+
text?: string;
|
|
6
|
+
/** True for a thought summary; it is not part of the reply text. */
|
|
7
|
+
thought?: boolean;
|
|
8
|
+
/** Opaque reasoning state; it must go back with its part on the next request. */
|
|
9
|
+
thoughtSignature?: string;
|
|
10
|
+
inlineData?: {
|
|
11
|
+
mimeType: string;
|
|
12
|
+
data: string;
|
|
13
|
+
};
|
|
14
|
+
fileData?: {
|
|
15
|
+
mimeType?: string;
|
|
16
|
+
fileUri: string;
|
|
17
|
+
};
|
|
18
|
+
functionCall?: {
|
|
19
|
+
id?: string;
|
|
20
|
+
name: string;
|
|
21
|
+
args?: Record<string, unknown>;
|
|
22
|
+
};
|
|
23
|
+
functionResponse?: {
|
|
24
|
+
id?: string;
|
|
25
|
+
name: string;
|
|
26
|
+
response: Record<string, unknown>;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export interface GeminiContent {
|
|
30
|
+
role: 'user' | 'model';
|
|
31
|
+
parts: GeminiPart[];
|
|
32
|
+
}
|
|
33
|
+
export interface GeminiRequest {
|
|
34
|
+
contents: GeminiContent[];
|
|
35
|
+
systemInstruction?: {
|
|
36
|
+
parts: GeminiPart[];
|
|
37
|
+
};
|
|
38
|
+
tools?: {
|
|
39
|
+
functionDeclarations: {
|
|
40
|
+
name: string;
|
|
41
|
+
description: string;
|
|
42
|
+
parametersJsonSchema: unknown;
|
|
43
|
+
}[];
|
|
44
|
+
}[];
|
|
45
|
+
generationConfig?: {
|
|
46
|
+
maxOutputTokens?: number;
|
|
47
|
+
temperature?: number;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
export interface GeminiResponse {
|
|
51
|
+
candidates?: {
|
|
52
|
+
content?: {
|
|
53
|
+
role?: string;
|
|
54
|
+
parts?: GeminiPart[];
|
|
55
|
+
};
|
|
56
|
+
finishReason?: string;
|
|
57
|
+
}[];
|
|
58
|
+
promptFeedback?: {
|
|
59
|
+
blockReason?: string;
|
|
60
|
+
};
|
|
61
|
+
usageMetadata?: {
|
|
62
|
+
promptTokenCount?: number;
|
|
63
|
+
candidatesTokenCount?: number;
|
|
64
|
+
thoughtsTokenCount?: number;
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/** Turns an image URL into a part; `http(s)` URLs are handed to `download`, which returns inline data. */
|
|
68
|
+
export type ImageLoader = (url: string) => Promise<{
|
|
69
|
+
mimeType: string;
|
|
70
|
+
data: string;
|
|
71
|
+
}>;
|
|
72
|
+
/**
|
|
73
|
+
* Converts neutral messages to a Gemini request. Leading system text becomes `systemInstruction`;
|
|
74
|
+
* turns from this provider are replayed verbatim so thought signatures stay valid; consecutive
|
|
75
|
+
* user-side parts (tool results, then user text) share one `user` content.
|
|
76
|
+
*/
|
|
77
|
+
export declare function toGeminiRequest(messages: LlmMessage[], options?: {
|
|
78
|
+
tools?: ToolDefinition[];
|
|
79
|
+
maxTokens?: number;
|
|
80
|
+
temperature?: number;
|
|
81
|
+
download?: ImageLoader;
|
|
82
|
+
}): Promise<GeminiRequest>;
|
|
83
|
+
export declare function toFinishReason(reason: string | undefined, hasCalls: boolean): FinishReason | undefined;
|
|
84
|
+
/**
|
|
85
|
+
* Appends streamed parts to `parts`. Plain text continues the previous text part of the same kind,
|
|
86
|
+
* so a streamed turn replays as a few parts rather than one per chunk; a signature stays with its text.
|
|
87
|
+
*/
|
|
88
|
+
export declare function mergeParts(parts: GeminiPart[], incoming: GeminiPart[]): GeminiPart[];
|
|
89
|
+
/** Builds the neutral response from a whole turn's parts and the last chunk's metadata. */
|
|
90
|
+
export declare function fromParts(parts: GeminiPart[], meta: {
|
|
91
|
+
finishReason?: string;
|
|
92
|
+
blockReason?: string;
|
|
93
|
+
usage?: GeminiResponse['usageMetadata'];
|
|
94
|
+
}): ChatResponse;
|
|
95
|
+
export declare function fromResponse(response: GeminiResponse): ChatResponse;
|
package/dist/convert.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// src/convert.ts
|
|
2
|
+
/** `RawTurn.type` of assistant turns from this provider; their parts are replayed verbatim. */
|
|
3
|
+
const TYPE = "gemini";
|
|
4
|
+
/** Prefix of tool call ids made up locally, for models that return function calls without an id. */
|
|
5
|
+
const LOCAL_ID = "gemini-call-";
|
|
6
|
+
async function toImagePart(url, download) {
|
|
7
|
+
const match = /^data:([^;,]+);base64,(.+)$/s.exec(url);
|
|
8
|
+
if (match) return { inlineData: {
|
|
9
|
+
mimeType: match[1],
|
|
10
|
+
data: match[2]
|
|
11
|
+
} };
|
|
12
|
+
if (/^https?:/i.test(url) && download) return { inlineData: await download(url) };
|
|
13
|
+
return { fileData: { fileUri: url } };
|
|
14
|
+
}
|
|
15
|
+
function safeParse(json) {
|
|
16
|
+
try {
|
|
17
|
+
const value = JSON.parse(json);
|
|
18
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : { value };
|
|
19
|
+
} catch {
|
|
20
|
+
return {};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Converts neutral messages to a Gemini request. Leading system text becomes `systemInstruction`;
|
|
25
|
+
* turns from this provider are replayed verbatim so thought signatures stay valid; consecutive
|
|
26
|
+
* user-side parts (tool results, then user text) share one `user` content.
|
|
27
|
+
*/
|
|
28
|
+
async function toGeminiRequest(messages, options = {}) {
|
|
29
|
+
const system = [];
|
|
30
|
+
const contents = [];
|
|
31
|
+
const callNames = /* @__PURE__ */ new Map();
|
|
32
|
+
const push = (role, parts) => {
|
|
33
|
+
if (!parts.length) return;
|
|
34
|
+
const last = contents.at(-1);
|
|
35
|
+
if (last?.role === role) last.parts.push(...parts);
|
|
36
|
+
else contents.push({
|
|
37
|
+
role,
|
|
38
|
+
parts
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
for (const message of messages) switch (message.role) {
|
|
42
|
+
case "system":
|
|
43
|
+
if (!contents.length) system.push(message.content);
|
|
44
|
+
else push("user", [{ text: message.content }]);
|
|
45
|
+
break;
|
|
46
|
+
case "user":
|
|
47
|
+
push("user", typeof message.content === "string" ? [{ text: message.content }] : await Promise.all(message.content.map((part) => part.type === "text" ? { text: part.text } : toImagePart(part.url, options.download))));
|
|
48
|
+
break;
|
|
49
|
+
case "assistant": {
|
|
50
|
+
for (const call of message.toolCalls ?? []) callNames.set(call.id, call.name);
|
|
51
|
+
if (message.raw?.type === "gemini") {
|
|
52
|
+
push("model", structuredClone(message.raw.content));
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
const parts = [];
|
|
56
|
+
if (message.content) parts.push({ text: message.content });
|
|
57
|
+
for (const call of message.toolCalls ?? []) parts.push({ functionCall: {
|
|
58
|
+
...call.id.startsWith(LOCAL_ID) ? {} : { id: call.id },
|
|
59
|
+
name: call.name,
|
|
60
|
+
args: safeParse(call.arguments)
|
|
61
|
+
} });
|
|
62
|
+
push("model", parts);
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
case "tool": push("user", [{ functionResponse: {
|
|
66
|
+
...message.toolCallId.startsWith(LOCAL_ID) ? {} : { id: message.toolCallId },
|
|
67
|
+
name: callNames.get(message.toolCallId) ?? message.toolCallId,
|
|
68
|
+
response: message.isError ? { error: message.content } : { result: message.content }
|
|
69
|
+
} }]);
|
|
70
|
+
}
|
|
71
|
+
const generationConfig = {
|
|
72
|
+
...options.maxTokens !== void 0 ? { maxOutputTokens: options.maxTokens } : {},
|
|
73
|
+
...options.temperature !== void 0 ? { temperature: options.temperature } : {}
|
|
74
|
+
};
|
|
75
|
+
return {
|
|
76
|
+
contents,
|
|
77
|
+
...system.length ? { systemInstruction: { parts: [{ text: system.join("\n\n") }] } } : {},
|
|
78
|
+
...options.tools?.length ? { tools: [{ functionDeclarations: options.tools.map((tool) => ({
|
|
79
|
+
name: tool.name,
|
|
80
|
+
description: tool.description,
|
|
81
|
+
parametersJsonSchema: {
|
|
82
|
+
type: "object",
|
|
83
|
+
...tool.parameters
|
|
84
|
+
}
|
|
85
|
+
})) }] } : {},
|
|
86
|
+
...Object.keys(generationConfig).length ? { generationConfig } : {}
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function toFinishReason(reason, hasCalls) {
|
|
90
|
+
switch (reason) {
|
|
91
|
+
case void 0: return;
|
|
92
|
+
case "STOP": return hasCalls ? "tool_calls" : "stop";
|
|
93
|
+
case "MAX_TOKENS": return "length";
|
|
94
|
+
case "SAFETY":
|
|
95
|
+
case "RECITATION":
|
|
96
|
+
case "BLOCKLIST":
|
|
97
|
+
case "PROHIBITED_CONTENT":
|
|
98
|
+
case "SPII":
|
|
99
|
+
case "IMAGE_SAFETY":
|
|
100
|
+
case "IMAGE_PROHIBITED_CONTENT": return "refusal";
|
|
101
|
+
default: return "other";
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Appends streamed parts to `parts`. Plain text continues the previous text part of the same kind,
|
|
106
|
+
* so a streamed turn replays as a few parts rather than one per chunk; a signature stays with its text.
|
|
107
|
+
*/
|
|
108
|
+
function mergeParts(parts, incoming) {
|
|
109
|
+
for (const part of incoming) {
|
|
110
|
+
const last = parts.at(-1);
|
|
111
|
+
const isText = (item) => item?.text !== void 0 && Object.keys(item).every((key) => key === "text" || key === "thought" || key === "thoughtSignature");
|
|
112
|
+
if (isText(part) && isText(last) && !last.thoughtSignature && !!last.thought === !!part.thought) {
|
|
113
|
+
last.text += part.text;
|
|
114
|
+
if (part.thoughtSignature) last.thoughtSignature = part.thoughtSignature;
|
|
115
|
+
} else parts.push({ ...part });
|
|
116
|
+
}
|
|
117
|
+
return parts;
|
|
118
|
+
}
|
|
119
|
+
/** Builds the neutral response from a whole turn's parts and the last chunk's metadata. */
|
|
120
|
+
function fromParts(parts, meta) {
|
|
121
|
+
let text = "";
|
|
122
|
+
const toolCalls = [];
|
|
123
|
+
for (const part of parts) {
|
|
124
|
+
if (part.text !== void 0 && !part.thought) text += part.text;
|
|
125
|
+
if (part.functionCall) toolCalls.push({
|
|
126
|
+
id: part.functionCall.id || `${LOCAL_ID}${toolCalls.length + 1}`,
|
|
127
|
+
name: part.functionCall.name,
|
|
128
|
+
arguments: JSON.stringify(part.functionCall.args ?? {})
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
const usage = meta.usage;
|
|
132
|
+
return {
|
|
133
|
+
text,
|
|
134
|
+
toolCalls,
|
|
135
|
+
finishReason: meta.blockReason && !parts.length ? "refusal" : toFinishReason(meta.finishReason, toolCalls.length > 0),
|
|
136
|
+
...usage ? { usage: {
|
|
137
|
+
inputTokens: usage.promptTokenCount ?? 0,
|
|
138
|
+
outputTokens: (usage.candidatesTokenCount ?? 0) + (usage.thoughtsTokenCount ?? 0)
|
|
139
|
+
} } : {},
|
|
140
|
+
...parts.length ? { raw: {
|
|
141
|
+
type: TYPE,
|
|
142
|
+
content: parts
|
|
143
|
+
} } : {}
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function fromResponse(response) {
|
|
147
|
+
const candidate = response.candidates?.[0];
|
|
148
|
+
return fromParts(candidate?.content?.parts ?? [], {
|
|
149
|
+
finishReason: candidate?.finishReason,
|
|
150
|
+
blockReason: response.promptFeedback?.blockReason,
|
|
151
|
+
usage: response.usageMetadata
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
export { TYPE, fromParts, fromResponse, mergeParts, toFinishReason, toGeminiRequest };
|
|
155
|
+
|
|
156
|
+
//# sourceMappingURL=convert.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"convert.js","names":[],"sources":["../src/convert.ts"],"sourcesContent":["import type { ChatResponse, FinishReason, LlmMessage, ToolCall, ToolDefinition } from '@mushinjs/plugin-providers'\n\n/** `RawTurn.type` of assistant turns from this provider; their parts are replayed verbatim. */\nexport const TYPE = 'gemini'\n\n/** Prefix of tool call ids made up locally, for models that return function calls without an id. */\nconst LOCAL_ID = 'gemini-call-'\n\nexport interface GeminiPart {\n text?: string\n /** True for a thought summary; it is not part of the reply text. */\n thought?: boolean\n /** Opaque reasoning state; it must go back with its part on the next request. */\n thoughtSignature?: string\n inlineData?: { mimeType: string; data: string }\n fileData?: { mimeType?: string; fileUri: string }\n functionCall?: { id?: string; name: string; args?: Record<string, unknown> }\n functionResponse?: { id?: string; name: string; response: Record<string, unknown> }\n}\n\nexport interface GeminiContent {\n role: 'user' | 'model'\n parts: GeminiPart[]\n}\n\nexport interface GeminiRequest {\n contents: GeminiContent[]\n systemInstruction?: { parts: GeminiPart[] }\n tools?: { functionDeclarations: { name: string; description: string; parametersJsonSchema: unknown }[] }[]\n generationConfig?: { maxOutputTokens?: number; temperature?: number }\n}\n\nexport interface GeminiResponse {\n candidates?: { content?: { role?: string; parts?: GeminiPart[] }; finishReason?: string }[]\n promptFeedback?: { blockReason?: string }\n usageMetadata?: { promptTokenCount?: number; candidatesTokenCount?: number; thoughtsTokenCount?: number }\n}\n\n/** Turns an image URL into a part; `http(s)` URLs are handed to `download`, which returns inline data. */\nexport type ImageLoader = (url: string) => Promise<{ mimeType: string; data: string }>\n\nasync function toImagePart(url: string, download?: ImageLoader): Promise<GeminiPart> {\n const match = /^data:([^;,]+);base64,(.+)$/s.exec(url)\n if (match) return { inlineData: { mimeType: match[1], data: match[2] } }\n if (/^https?:/i.test(url) && download) return { inlineData: await download(url) }\n // Files API URIs (and anything else) are passed as references.\n return { fileData: { fileUri: url } }\n}\n\nfunction safeParse(json: string): Record<string, unknown> {\n try {\n const value = JSON.parse(json)\n return value && typeof value === 'object' && !Array.isArray(value) ? value : { value }\n } catch {\n return {}\n }\n}\n\n/**\n * Converts neutral messages to a Gemini request. Leading system text becomes `systemInstruction`;\n * turns from this provider are replayed verbatim so thought signatures stay valid; consecutive\n * user-side parts (tool results, then user text) share one `user` content.\n */\nexport async function toGeminiRequest(messages: LlmMessage[], options: {\n tools?: ToolDefinition[]\n maxTokens?: number\n temperature?: number\n download?: ImageLoader\n} = {}): Promise<GeminiRequest> {\n const system: string[] = []\n const contents: GeminiContent[] = []\n // functionResponse needs the function name, which a tool message does not carry.\n const callNames = new Map<string, string>()\n const push = (role: GeminiContent['role'], parts: GeminiPart[]) => {\n if (!parts.length) return\n const last = contents.at(-1)\n if (last?.role === role) last.parts.push(...parts)\n else contents.push({ role, parts })\n }\n\n for (const message of messages) {\n switch (message.role) {\n case 'system':\n if (!contents.length) system.push(message.content)\n else push('user', [{ text: message.content }])\n break\n case 'user':\n push('user', typeof message.content === 'string'\n ? [{ text: message.content }]\n : await Promise.all(message.content.map((part) => part.type === 'text'\n ? { text: part.text }\n : toImagePart(part.url, options.download))))\n break\n case 'assistant': {\n for (const call of message.toolCalls ?? []) callNames.set(call.id, call.name)\n if (message.raw?.type === TYPE) {\n push('model', structuredClone(message.raw.content as GeminiPart[]))\n break\n }\n const parts: GeminiPart[] = []\n if (message.content) parts.push({ text: message.content })\n for (const call of message.toolCalls ?? []) {\n parts.push({ functionCall: { ...(call.id.startsWith(LOCAL_ID) ? {} : { id: call.id }), name: call.name, args: safeParse(call.arguments) } })\n }\n push('model', parts)\n break\n }\n case 'tool':\n push('user', [{\n functionResponse: {\n ...(message.toolCallId.startsWith(LOCAL_ID) ? {} : { id: message.toolCallId }),\n name: callNames.get(message.toolCallId) ?? message.toolCallId,\n response: message.isError ? { error: message.content } : { result: message.content },\n },\n }])\n break\n }\n }\n\n const generationConfig = {\n ...(options.maxTokens !== undefined ? { maxOutputTokens: options.maxTokens } : {}),\n ...(options.temperature !== undefined ? { temperature: options.temperature } : {}),\n }\n return {\n contents,\n ...(system.length ? { systemInstruction: { parts: [{ text: system.join('\\n\\n') }] } } : {}),\n ...(options.tools?.length ? {\n tools: [{\n functionDeclarations: options.tools.map((tool) => ({\n name: tool.name,\n description: tool.description,\n parametersJsonSchema: { type: 'object', ...tool.parameters },\n })),\n }],\n } : {}),\n ...(Object.keys(generationConfig).length ? { generationConfig } : {}),\n }\n}\n\nexport function toFinishReason(reason: string | undefined, hasCalls: boolean): FinishReason | undefined {\n switch (reason) {\n case undefined:\n return\n case 'STOP':\n return hasCalls ? 'tool_calls' : 'stop'\n case 'MAX_TOKENS':\n return 'length'\n case 'SAFETY':\n case 'RECITATION':\n case 'BLOCKLIST':\n case 'PROHIBITED_CONTENT':\n case 'SPII':\n case 'IMAGE_SAFETY':\n case 'IMAGE_PROHIBITED_CONTENT':\n return 'refusal'\n default:\n return 'other'\n }\n}\n\n/**\n * Appends streamed parts to `parts`. Plain text continues the previous text part of the same kind,\n * so a streamed turn replays as a few parts rather than one per chunk; a signature stays with its text.\n */\nexport function mergeParts(parts: GeminiPart[], incoming: GeminiPart[]) {\n for (const part of incoming) {\n const last = parts.at(-1)\n const isText = (item?: GeminiPart) => item?.text !== undefined && Object.keys(item).every((key) => key === 'text' || key === 'thought' || key === 'thoughtSignature')\n if (isText(part) && isText(last) && !last!.thoughtSignature && !!last!.thought === !!part.thought) {\n last!.text += part.text!\n if (part.thoughtSignature) last!.thoughtSignature = part.thoughtSignature\n } else {\n parts.push({ ...part })\n }\n }\n return parts\n}\n\n/** Builds the neutral response from a whole turn's parts and the last chunk's metadata. */\nexport function fromParts(parts: GeminiPart[], meta: { finishReason?: string; blockReason?: string; usage?: GeminiResponse['usageMetadata'] }): ChatResponse {\n let text = ''\n const toolCalls: ToolCall[] = []\n for (const part of parts) {\n if (part.text !== undefined && !part.thought) text += part.text\n if (part.functionCall) {\n toolCalls.push({\n id: part.functionCall.id || `${LOCAL_ID}${toolCalls.length + 1}`,\n name: part.functionCall.name,\n arguments: JSON.stringify(part.functionCall.args ?? {}),\n })\n }\n }\n const usage = meta.usage\n return {\n text,\n toolCalls,\n finishReason: meta.blockReason && !parts.length ? 'refusal' : toFinishReason(meta.finishReason, toolCalls.length > 0),\n ...(usage ? {\n usage: {\n inputTokens: usage.promptTokenCount ?? 0,\n outputTokens: (usage.candidatesTokenCount ?? 0) + (usage.thoughtsTokenCount ?? 0),\n },\n } : {}),\n ...(parts.length ? { raw: { type: TYPE, content: parts } } : {}),\n }\n}\n\nexport function fromResponse(response: GeminiResponse): ChatResponse {\n const candidate = response.candidates?.[0]\n return fromParts(candidate?.content?.parts ?? [], {\n finishReason: candidate?.finishReason,\n blockReason: response.promptFeedback?.blockReason,\n usage: response.usageMetadata,\n })\n}\n"],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { GeminiContent, GeminiPart, GeminiRequest, GeminiResponse, ImageLoader, TYPE, fromParts, fromResponse, mergeParts, toFinishReason, toGeminiRequest } from "./convert.js";
|
|
2
|
+
import { Context, z } from "@mushinjs/core";
|
|
3
|
+
import { ChatRequest, ChatResponse, EmbedRequest, EmbedResponse, ProviderClient, StreamEvent } from "@mushinjs/plugin-providers";
|
|
4
|
+
export declare const name = "provider-gemini";
|
|
5
|
+
export declare const inject: string[];
|
|
6
|
+
export declare const ProviderConfig: z.ZodObject<{
|
|
7
|
+
apiKey: z.ZodString;
|
|
8
|
+
baseURL: z.ZodDefault<z.ZodURL>;
|
|
9
|
+
}, z.core.$strip>;
|
|
10
|
+
export type ProviderConfig = z.infer<typeof ProviderConfig>;
|
|
11
|
+
/** An HTTP error from the Gemini API, with its status code. */
|
|
12
|
+
export declare class GeminiError extends Error {
|
|
13
|
+
status: number;
|
|
14
|
+
constructor(status: number, message: string);
|
|
15
|
+
}
|
|
16
|
+
export declare class GeminiClient implements ProviderClient {
|
|
17
|
+
private config;
|
|
18
|
+
private fetcher;
|
|
19
|
+
private baseURL;
|
|
20
|
+
constructor(config: ProviderConfig, fetcher?: typeof fetch);
|
|
21
|
+
private request;
|
|
22
|
+
/** Downloads an http(s) image so it can go inline; the API does not fetch arbitrary URLs itself. */
|
|
23
|
+
private download;
|
|
24
|
+
private body;
|
|
25
|
+
chat(request: ChatRequest): Promise<ChatResponse>;
|
|
26
|
+
stream(request: ChatRequest): AsyncIterable<StreamEvent>;
|
|
27
|
+
embed(request: EmbedRequest): Promise<EmbedResponse>;
|
|
28
|
+
/** Models that can chat, without the `models/` prefix. */
|
|
29
|
+
listModels(signal?: AbortSignal): Promise<string[]>;
|
|
30
|
+
}
|
|
31
|
+
export declare function apply(ctx: Context): void;
|
|
32
|
+
export { GeminiContent, GeminiPart, GeminiRequest, GeminiResponse, ImageLoader, TYPE, fromParts, fromResponse, mergeParts, toFinishReason, toGeminiRequest };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { TYPE, fromParts, fromResponse, mergeParts, toFinishReason, toGeminiRequest } from "./convert.js";
|
|
2
|
+
import { field, z } from "@mushinjs/core";
|
|
3
|
+
// src/index.ts
|
|
4
|
+
const name = "provider-gemini";
|
|
5
|
+
const inject = ["llm"];
|
|
6
|
+
const OFFICIAL_BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
|
|
7
|
+
const ProviderConfig = z.object({
|
|
8
|
+
apiKey: z.string().min(1).meta(field("API key", "Gemini API key from Google AI Studio", { "zh-CN": {
|
|
9
|
+
title: "API 密钥",
|
|
10
|
+
description: "在 Google AI Studio 获取的 Gemini API 密钥"
|
|
11
|
+
} })),
|
|
12
|
+
baseURL: z.url().default(OFFICIAL_BASE_URL).meta(field("Base URL", "Change only when routing through a proxy; ends in /v1beta", { "zh-CN": {
|
|
13
|
+
title: "接口地址",
|
|
14
|
+
description: "仅在使用代理时修改,以 /v1beta 结尾"
|
|
15
|
+
} }))
|
|
16
|
+
});
|
|
17
|
+
/** An HTTP error from the Gemini API, with its status code. */
|
|
18
|
+
var GeminiError = class extends Error {
|
|
19
|
+
status;
|
|
20
|
+
constructor(status, message) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.status = status;
|
|
23
|
+
this.name = "GeminiError";
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
/** Accepts `gemini-x` and `models/gemini-x`. */
|
|
27
|
+
function modelPath(model) {
|
|
28
|
+
return model.startsWith("models/") ? model : `models/${model}`;
|
|
29
|
+
}
|
|
30
|
+
/** Reads `data:` lines from a server-sent event stream as parsed JSON. */
|
|
31
|
+
async function* readEvents(body) {
|
|
32
|
+
const decoder = new TextDecoder();
|
|
33
|
+
let buffer = "";
|
|
34
|
+
const reader = body.getReader();
|
|
35
|
+
try {
|
|
36
|
+
while (true) {
|
|
37
|
+
const { done, value } = await reader.read();
|
|
38
|
+
buffer += decoder.decode(value, { stream: !done });
|
|
39
|
+
let index;
|
|
40
|
+
while ((index = buffer.search(/\r?\n\r?\n/)) >= 0) {
|
|
41
|
+
const event = buffer.slice(0, index);
|
|
42
|
+
buffer = buffer.slice(index).replace(/^\r?\n\r?\n/, "");
|
|
43
|
+
const data = event.split(/\r?\n/).filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trimStart()).join("\n");
|
|
44
|
+
if (data) yield JSON.parse(data);
|
|
45
|
+
}
|
|
46
|
+
if (done) break;
|
|
47
|
+
}
|
|
48
|
+
const data = buffer.split(/\r?\n/).filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trimStart()).join("\n");
|
|
49
|
+
if (data) yield JSON.parse(data);
|
|
50
|
+
} finally {
|
|
51
|
+
reader.releaseLock();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
var GeminiClient = class {
|
|
55
|
+
config;
|
|
56
|
+
fetcher;
|
|
57
|
+
baseURL;
|
|
58
|
+
constructor(config, fetcher = fetch) {
|
|
59
|
+
this.config = config;
|
|
60
|
+
this.fetcher = fetcher;
|
|
61
|
+
this.baseURL = config.baseURL.replace(/\/+$/, "");
|
|
62
|
+
}
|
|
63
|
+
async request(path, init = {}) {
|
|
64
|
+
const response = await this.fetcher(`${this.baseURL}/${path}`, {
|
|
65
|
+
method: init.method ?? (init.body === void 0 ? "GET" : "POST"),
|
|
66
|
+
headers: {
|
|
67
|
+
"x-goog-api-key": this.config.apiKey,
|
|
68
|
+
...init.body === void 0 ? {} : { "content-type": "application/json" }
|
|
69
|
+
},
|
|
70
|
+
...init.body === void 0 ? {} : { body: JSON.stringify(init.body) },
|
|
71
|
+
signal: init.signal
|
|
72
|
+
});
|
|
73
|
+
if (!response.ok) {
|
|
74
|
+
const text = await response.text().catch(() => "");
|
|
75
|
+
let message = text || response.statusText;
|
|
76
|
+
try {
|
|
77
|
+
message = JSON.parse(text).error?.message ?? message;
|
|
78
|
+
} catch {}
|
|
79
|
+
throw new GeminiError(response.status, `Gemini API ${response.status}: ${message}`);
|
|
80
|
+
}
|
|
81
|
+
return response;
|
|
82
|
+
}
|
|
83
|
+
/** Downloads an http(s) image so it can go inline; the API does not fetch arbitrary URLs itself. */
|
|
84
|
+
download = async (url) => {
|
|
85
|
+
const response = await this.fetcher(url);
|
|
86
|
+
if (!response.ok) throw new Error(`Could not download image ${url}: HTTP ${response.status}`);
|
|
87
|
+
return {
|
|
88
|
+
mimeType: response.headers.get("content-type")?.split(";")[0].trim() || "image/jpeg",
|
|
89
|
+
data: Buffer.from(await response.arrayBuffer()).toString("base64")
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
body(request) {
|
|
93
|
+
return toGeminiRequest(request.messages, {
|
|
94
|
+
tools: request.tools,
|
|
95
|
+
maxTokens: request.maxTokens,
|
|
96
|
+
temperature: request.temperature,
|
|
97
|
+
download: this.download
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
async chat(request) {
|
|
101
|
+
const response = await this.request(`${modelPath(request.model)}:generateContent`, {
|
|
102
|
+
body: await this.body(request),
|
|
103
|
+
signal: request.signal
|
|
104
|
+
});
|
|
105
|
+
return fromResponse(await response.json());
|
|
106
|
+
}
|
|
107
|
+
async *stream(request) {
|
|
108
|
+
const response = await this.request(`${modelPath(request.model)}:streamGenerateContent?alt=sse`, {
|
|
109
|
+
body: await this.body(request),
|
|
110
|
+
signal: request.signal
|
|
111
|
+
});
|
|
112
|
+
if (!response.body) throw new GeminiError(response.status, "Gemini API returned an empty stream");
|
|
113
|
+
const parts = [];
|
|
114
|
+
let finishReason;
|
|
115
|
+
let blockReason;
|
|
116
|
+
let usage;
|
|
117
|
+
for await (const chunk of readEvents(response.body)) {
|
|
118
|
+
const candidate = chunk.candidates?.[0];
|
|
119
|
+
const incoming = candidate?.content?.parts ?? [];
|
|
120
|
+
for (const part of incoming) if (part.text && !part.thought) yield {
|
|
121
|
+
type: "delta",
|
|
122
|
+
text: part.text
|
|
123
|
+
};
|
|
124
|
+
mergeParts(parts, incoming);
|
|
125
|
+
finishReason = candidate?.finishReason ?? finishReason;
|
|
126
|
+
blockReason = chunk.promptFeedback?.blockReason ?? blockReason;
|
|
127
|
+
usage = chunk.usageMetadata ?? usage;
|
|
128
|
+
}
|
|
129
|
+
yield {
|
|
130
|
+
type: "done",
|
|
131
|
+
response: fromParts(parts, {
|
|
132
|
+
finishReason,
|
|
133
|
+
blockReason,
|
|
134
|
+
usage
|
|
135
|
+
})
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
async embed(request) {
|
|
139
|
+
const model = modelPath(request.model);
|
|
140
|
+
return { vectors: ((await (await this.request(`${model}:batchEmbedContents`, {
|
|
141
|
+
body: { requests: request.input.map((text) => ({
|
|
142
|
+
model,
|
|
143
|
+
content: { parts: [{ text }] }
|
|
144
|
+
})) },
|
|
145
|
+
signal: request.signal
|
|
146
|
+
})).json()).embeddings ?? []).map((item) => item.values) };
|
|
147
|
+
}
|
|
148
|
+
/** Models that can chat, without the `models/` prefix. */
|
|
149
|
+
async listModels(signal) {
|
|
150
|
+
const models = [];
|
|
151
|
+
let pageToken;
|
|
152
|
+
do {
|
|
153
|
+
const query = new URLSearchParams({
|
|
154
|
+
pageSize: "1000",
|
|
155
|
+
...pageToken ? { pageToken } : {}
|
|
156
|
+
});
|
|
157
|
+
const data = await (await this.request(`models?${query}`, { signal })).json();
|
|
158
|
+
for (const model of data.models ?? []) if (model.supportedGenerationMethods?.includes("generateContent")) models.push(model.name.replace(/^models\//, ""));
|
|
159
|
+
pageToken = data.nextPageToken;
|
|
160
|
+
} while (pageToken);
|
|
161
|
+
return models.sort();
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
function apply(ctx) {
|
|
165
|
+
ctx.llm.defineType({
|
|
166
|
+
id: TYPE,
|
|
167
|
+
displayName: "Google Gemini",
|
|
168
|
+
config: ProviderConfig,
|
|
169
|
+
secrets: ["apiKey"],
|
|
170
|
+
presets: [{
|
|
171
|
+
id: "gemini",
|
|
172
|
+
label: "Google Gemini",
|
|
173
|
+
config: { baseURL: OFFICIAL_BASE_URL }
|
|
174
|
+
}],
|
|
175
|
+
create: (config) => new GeminiClient(config)
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
export { GeminiClient, GeminiError, ProviderConfig, TYPE, apply, fromParts, fromResponse, inject, mergeParts, name, toFinishReason, toGeminiRequest };
|
|
179
|
+
|
|
180
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["import { type Context, field, z } from '@mushinjs/core'\nimport type {\n ChatRequest, ChatResponse, EmbedRequest, EmbedResponse, ProviderClient, StreamEvent,\n} from '@mushinjs/plugin-providers'\nimport { fromParts, fromResponse, type GeminiPart, type GeminiResponse, mergeParts, toGeminiRequest, TYPE } from './convert'\n\nexport * from './convert'\n\nexport const name = 'provider-gemini'\nexport const inject = ['llm']\n\nconst OFFICIAL_BASE_URL = 'https://generativelanguage.googleapis.com/v1beta'\n\nexport const ProviderConfig = z.object({\n apiKey: z.string().min(1).meta(field('API key', 'Gemini API key from Google AI Studio', {\n 'zh-CN': { title: 'API 密钥', description: '在 Google AI Studio 获取的 Gemini API 密钥' },\n })),\n baseURL: z.url().default(OFFICIAL_BASE_URL).meta(field('Base URL', 'Change only when routing through a proxy; ends in /v1beta', {\n 'zh-CN': { title: '接口地址', description: '仅在使用代理时修改,以 /v1beta 结尾' },\n })),\n})\n\nexport type ProviderConfig = z.infer<typeof ProviderConfig>\n\n/** An HTTP error from the Gemini API, with its status code. */\nexport class GeminiError extends Error {\n constructor(public status: number, message: string) {\n super(message)\n this.name = 'GeminiError'\n }\n}\n\n/** Accepts `gemini-x` and `models/gemini-x`. */\nfunction modelPath(model: string) {\n return model.startsWith('models/') ? model : `models/${model}`\n}\n\n/** Reads `data:` lines from a server-sent event stream as parsed JSON. */\nasync function* readEvents(body: ReadableStream<Uint8Array>): AsyncGenerator<GeminiResponse> {\n const decoder = new TextDecoder()\n let buffer = ''\n const reader = body.getReader()\n try {\n while (true) {\n const { done, value } = await reader.read()\n buffer += decoder.decode(value, { stream: !done })\n let index: number\n while ((index = buffer.search(/\\r?\\n\\r?\\n/)) >= 0) {\n const event = buffer.slice(0, index)\n buffer = buffer.slice(index).replace(/^\\r?\\n\\r?\\n/, '')\n const data = event.split(/\\r?\\n/).filter((line) => line.startsWith('data:')).map((line) => line.slice(5).trimStart()).join('\\n')\n if (data) yield JSON.parse(data)\n }\n if (done) break\n }\n const data = buffer.split(/\\r?\\n/).filter((line) => line.startsWith('data:')).map((line) => line.slice(5).trimStart()).join('\\n')\n if (data) yield JSON.parse(data)\n } finally {\n reader.releaseLock()\n }\n}\n\nexport class GeminiClient implements ProviderClient {\n private baseURL: string\n\n constructor(private config: ProviderConfig, private fetcher: typeof fetch = fetch) {\n this.baseURL = config.baseURL.replace(/\\/+$/, '')\n }\n\n private async request(path: string, init: { method?: string; body?: unknown; signal?: AbortSignal } = {}) {\n const response = await this.fetcher(`${this.baseURL}/${path}`, {\n method: init.method ?? (init.body === undefined ? 'GET' : 'POST'),\n headers: { 'x-goog-api-key': this.config.apiKey, ...(init.body === undefined ? {} : { 'content-type': 'application/json' }) },\n ...(init.body === undefined ? {} : { body: JSON.stringify(init.body) }),\n signal: init.signal,\n })\n if (!response.ok) {\n const text = await response.text().catch(() => '')\n let message = text || response.statusText\n try {\n message = JSON.parse(text).error?.message ?? message\n } catch {}\n throw new GeminiError(response.status, `Gemini API ${response.status}: ${message}`)\n }\n return response\n }\n\n /** Downloads an http(s) image so it can go inline; the API does not fetch arbitrary URLs itself. */\n private download = async (url: string) => {\n const response = await this.fetcher(url)\n if (!response.ok) throw new Error(`Could not download image ${url}: HTTP ${response.status}`)\n const mimeType = response.headers.get('content-type')?.split(';')[0].trim() || 'image/jpeg'\n const data = Buffer.from(await response.arrayBuffer()).toString('base64')\n return { mimeType, data }\n }\n\n private body(request: ChatRequest) {\n return toGeminiRequest(request.messages, {\n tools: request.tools,\n maxTokens: request.maxTokens,\n temperature: request.temperature,\n download: this.download,\n })\n }\n\n async chat(request: ChatRequest): Promise<ChatResponse> {\n const response = await this.request(`${modelPath(request.model)}:generateContent`, {\n body: await this.body(request),\n signal: request.signal,\n })\n return fromResponse(await response.json() as GeminiResponse)\n }\n\n async *stream(request: ChatRequest): AsyncIterable<StreamEvent> {\n const response = await this.request(`${modelPath(request.model)}:streamGenerateContent?alt=sse`, {\n body: await this.body(request),\n signal: request.signal,\n })\n if (!response.body) throw new GeminiError(response.status, 'Gemini API returned an empty stream')\n const parts: GeminiPart[] = []\n let finishReason: string | undefined\n let blockReason: string | undefined\n let usage: GeminiResponse['usageMetadata']\n for await (const chunk of readEvents(response.body)) {\n const candidate = chunk.candidates?.[0]\n const incoming = candidate?.content?.parts ?? []\n for (const part of incoming) {\n if (part.text && !part.thought) yield { type: 'delta', text: part.text }\n }\n mergeParts(parts, incoming)\n finishReason = candidate?.finishReason ?? finishReason\n blockReason = chunk.promptFeedback?.blockReason ?? blockReason\n usage = chunk.usageMetadata ?? usage\n }\n yield { type: 'done', response: fromParts(parts, { finishReason, blockReason, usage }) }\n }\n\n async embed(request: EmbedRequest): Promise<EmbedResponse> {\n const model = modelPath(request.model)\n const response = await this.request(`${model}:batchEmbedContents`, {\n body: { requests: request.input.map((text) => ({ model, content: { parts: [{ text }] } })) },\n signal: request.signal,\n })\n const data = await response.json() as { embeddings?: { values: number[] }[] }\n return { vectors: (data.embeddings ?? []).map((item) => item.values) }\n }\n\n /** Models that can chat, without the `models/` prefix. */\n async listModels(signal?: AbortSignal) {\n const models: string[] = []\n let pageToken: string | undefined\n do {\n const query = new URLSearchParams({ pageSize: '1000', ...(pageToken ? { pageToken } : {}) })\n const response = await this.request(`models?${query}`, { signal })\n const data = await response.json() as { models?: { name: string; supportedGenerationMethods?: string[] }[]; nextPageToken?: string }\n for (const model of data.models ?? []) {\n if (model.supportedGenerationMethods?.includes('generateContent')) models.push(model.name.replace(/^models\\//, ''))\n }\n pageToken = data.nextPageToken\n } while (pageToken)\n return models.sort()\n }\n}\n\nexport function apply(ctx: Context) {\n ctx.llm.defineType({\n id: TYPE,\n displayName: 'Google Gemini',\n config: ProviderConfig,\n secrets: ['apiKey'],\n presets: [{ id: 'gemini', label: 'Google Gemini', config: { baseURL: OFFICIAL_BASE_URL } }],\n create: (config) => new GeminiClient(config),\n })\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,48 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mushinjs/plugin-provider-gemini",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.2.0-next.20261113103537",
|
|
4
|
+
"description": "Google Gemini provider type",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/mushinjs/mushin.git",
|
|
10
|
+
"directory": "packages/plugin-provider-gemini"
|
|
11
|
+
},
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./package.json": "./package.json"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"mushin-plugin"
|
|
27
|
+
],
|
|
28
|
+
"mushin": {
|
|
29
|
+
"kind": "app",
|
|
30
|
+
"displayName": {
|
|
31
|
+
"en": "Gemini provider",
|
|
32
|
+
"zh-CN": "Gemini 提供商"
|
|
33
|
+
},
|
|
34
|
+
"description": {
|
|
35
|
+
"en": "Google Gemini models through the native Gemini API, with tools, images and embeddings",
|
|
36
|
+
"zh-CN": "通过 Gemini 原生 API 使用 Google Gemini 模型,支持工具调用、图片与向量嵌入"
|
|
37
|
+
},
|
|
38
|
+
"requires": {
|
|
39
|
+
"plugins": {
|
|
40
|
+
"@mushinjs/plugin-providers": "*"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@mushinjs/core": "0.2.0-next.20261113103537",
|
|
46
|
+
"@mushinjs/plugin-providers": "0.2.0-next.20261113103537"
|
|
47
|
+
}
|
|
6
48
|
}
|
package/README.md
DELETED