Files
2026-03-03 23:49:13 +01:00

213 lines
7.3 KiB
JavaScript

"use strict";
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.Google = void 0;
const fetchWithTimeout_1 = require("../fetchWithTimeout");
const types_1 = require("../types");
class Google {
name = 'google';
async complete(conversation, options) {
const contents = conversation.messages.map(toGeminiContent).flat();
const { response, error } = await create(options.model ?? 'gemini-2.5-pro', {
systemInstruction: {
role: 'system',
parts: [
{ text: systemPrompt(conversation.systemPrompt) }
]
},
contents,
tools: conversation.tools.length > 0 ? [{ functionDeclarations: conversation.tools.map(toGeminiTool) }] : undefined,
generationConfig: {
temperature: options.temperature,
maxOutputTokens: options.maxTokens
},
}, options);
const [candidate] = response?.candidates ?? [];
if (error || !response || !candidate)
return { result: (0, types_1.assistantMessageFromError)(error ?? 'No response from Google API'), usage: (0, types_1.emptyUsage)() };
const usage = {
input: response.usageMetadata?.promptTokenCount ?? 0,
output: response.usageMetadata?.candidatesTokenCount ?? 0,
};
const result = toAssistantMessage(candidate);
return { result, usage };
}
}
exports.Google = Google;
async function create(model, createParams, options) {
const debugBody = { ...createParams, tools: `${createParams.tools?.length ?? 0} tools` };
options.debug?.('lowire:google')('Request:', JSON.stringify(debugBody, null, 2));
const response = await (0, fetchWithTimeout_1.fetchWithTimeout)(options.apiEndpoint ?? `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-goog-api-key': options.apiKey,
},
body: JSON.stringify(createParams),
signal: options.signal,
timeout: options.apiTimeout
});
if (!response.ok) {
options.debug?.('lowire:google')('Response:', response.status);
return { error: `API error: ${response.status} ${response.statusText} ${await response.text()}` };
}
const responseBody = await response.json();
options.debug?.('lowire:google')('Response:', JSON.stringify(responseBody, null, 2));
return { response: responseBody };
}
function toGeminiTool(tool) {
return {
name: tool.name,
description: tool.description,
parameters: stripUnsupportedSchemaFields(tool.inputSchema),
};
}
function stripUnsupportedSchemaFields(schema) {
if (!schema || typeof schema !== 'object')
return schema;
const cleaned = Array.isArray(schema) ? [...schema] : { ...schema };
delete cleaned.additionalProperties;
delete cleaned.$schema;
for (const key in cleaned) {
if (cleaned[key] && typeof cleaned[key] === 'object')
cleaned[key] = stripUnsupportedSchemaFields(cleaned[key]);
}
return cleaned;
}
function toAssistantMessage(candidate) {
const stopReason = { code: 'ok' };
if (candidate.finishReason === 'MAX_TOKENS')
stopReason.code = 'max_tokens';
return {
role: 'assistant',
content: (candidate.content.parts || []).map(toContentPart).filter(Boolean),
stopReason,
};
}
function toContentPart(part) {
if (part.text) {
return {
type: 'text',
text: part.text,
googleThoughtSignature: part.thoughtSignature,
};
}
if (part.functionCall) {
return {
type: 'tool_call',
name: part.functionCall.name,
arguments: part.functionCall.args,
id: `call_${Math.random().toString(36).substring(2, 15)}`,
googleThoughtSignature: part.thoughtSignature,
};
}
return null;
}
function toGeminiContent(message) {
if (message.role === 'user') {
return [{
role: 'user',
parts: [{ text: message.content }]
}];
}
if (message.role === 'assistant') {
const parts = [];
const toolResults = [];
for (const part of message.content) {
if (part.type === 'text') {
parts.push({
text: part.text,
thoughtSignature: part.googleThoughtSignature,
});
continue;
}
if (part.type === 'tool_call') {
parts.push({
functionCall: {
name: part.name,
args: part.arguments
},
thoughtSignature: part.googleThoughtSignature,
});
if (part.result)
toolResults.push(...toGeminiToolResult(part, part.result));
}
}
if (message.toolError) {
toolResults.push({
role: 'user',
parts: [{
text: message.toolError,
}]
});
}
return [{
role: 'model',
parts
}, ...toolResults];
}
throw new Error(`Unsupported message role: ${message.role}`);
}
function toGeminiToolResult(call, toolResult) {
const responseContent = {};
const textParts = [];
const inlineDatas = [];
for (const part of toolResult.content) {
if (part.type === 'text') {
textParts.push(part.text);
}
else if (part.type === 'image') {
// Store image data for inclusion in response
inlineDatas.push({
inline_data: {
mime_type: part.mimeType,
data: part.data
}
});
}
}
if (textParts.length > 0)
responseContent.result = textParts.join('\n');
const result = [{
role: 'function',
parts: [{
functionResponse: {
name: call.name,
response: responseContent
}
}]
}];
if (inlineDatas.length > 0) {
result.push({
role: 'user',
parts: inlineDatas
});
}
return result;
}
const systemPrompt = (prompt) => `
### System instructions
${prompt}
### Tool calling instructions
- Make sure every message contains a tool call.
- When you use a tool, you may provide a brief thought or explanation in the content field
immediately before the tool_call. Do not split this into separate messages.
- Every reply must include a tool call.
`;