213 lines
7.3 KiB
JavaScript
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.
|
|
`;
|