feat(protocol): 支持 Claude 与 Gemini 协议间的双向转换

实现了 Claude API 请求到 Gemini API 请求的转换,以及 Gemini API 响应到 Claude API 响应的转换。
同时支持了两种协议间流式响应的转换,增强了项目的兼容性和灵活性。

更新了 README 文件,新增模型协议关系图和 Star History。
优化了 Gemini 核心服务中的模型选择逻辑,当请求的模型不存在时,会使用默认模型。
This commit is contained in:
hex2077 2025-07-31 22:54:21 +08:00
parent 4dbba6a5ab
commit b031ca4286
5 changed files with 610 additions and 17 deletions

View file

@ -78,6 +78,55 @@ The project adopts multiple modern design patterns to ensure code maintainabilit
6. **Response Conversion**: Converts service response back to client expected format
7. **Streaming Processing**: Supports real-time streaming response transmission
### 🎨 Model Protocol and Provider Relationship Diagram
- OpenAI Protocol (P_OPENAI): Supports all MODEL_PROVIDER, including openai-custom, gemini-cli-oauth, claude-custom and
claude-kiro-oauth.
- Claude Protocol (P_CLAUDE): Supports claude-custom, claude-kiro-oauth and gemini-cli-oauth.
- Gemini Protocol (P_GEMINI): Supports gemini-cli-oauth.
```mermaid
graph TD
subgraph Core_Protocols
P_OPENAI(OpenAI Protocol)
P_GEMINI(Gemini Protocol)
P_CLAUDE(Claude Protocol)
end
subgraph Supported_Model_Providers
MP_OPENAI[openai-custom]
MP_GEMINI[gemini-cli-oauth]
MP_CLAUDE_C[claude-custom]
MP_CLAUDE_K[claude-kiro-oauth]
end
subgraph Internal_Conversion_Logic
direction LR
P_OPENAI <-->|Request/Response Conversion| P_GEMINI
P_OPENAI <-->|Request/Response Conversion| P_CLAUDE
P_GEMINI <-->|Request/Response Conversion| P_CLAUDE
end
P_OPENAI ---|Supports| MP_OPENAI
P_OPENAI ---|Supports| MP_GEMINI
P_OPENAI ---|Supports| MP_CLAUDE_C
P_OPENAI ---|Supports| MP_CLAUDE_K
P_GEMINI ---|Supports| MP_GEMINI
P_CLAUDE ---|Supports| MP_CLAUDE_C
P_CLAUDE ---|Supports| MP_CLAUDE_K
P_CLAUDE ---|Supports| MP_GEMINI
style P_OPENAI fill:#f9f,stroke:#333,stroke-width:2px
style P_GEMINI fill:#ccf,stroke:#333,stroke-width:2px
style P_CLAUDE fill:#cfc,stroke:#333,stroke-width:2px
```
---
### 🔧 Usage Instructions
* **MCP Support**: While the built-in command functions of the original Gemini CLI are not available, this project perfectly supports MCP (Model Context Protocol) and can work with MCP-compatible clients for more powerful functionality extensions.
@ -310,3 +359,7 @@ This project is licensed under the [**GNU General Public License v3 (GPLv3)**](h
## 🙏 Acknowledgements
The development of this project was greatly inspired by the official Google Gemini CLI, and referenced some code implementations from Cline 3.18.0's `gemini-cli.ts`. I would like to express my sincere gratitude to the official Google team and the Cline development team for their excellent work!
## 🌟 Star History
[![Star History Chart](https://api.star-history.com/svg?repos=justlovemaki/AIClient-2-API&type=Timeline)](https://www.star-history.com/#justlovemaki/AIClient-2-API&Timeline)

View file

@ -78,6 +78,53 @@
6. **响应转换**: 将服务响应转换回客户端期望格式
7. **流式处理**: 支持实时流式响应传输
### 🎨 模型协议与提供商关系图
- OpenAI 协议 (P_OPENAI): 支持所有 MODEL_PROVIDER包括 openai-custom、gemini-cli-oauth、claude-custom 和
claude-kiro-oauth。
- Claude 协议 (P_CLAUDE): 支持 claude-custom、claude-kiro-oauth 和 gemini-cli-oauth。
- Gemini 协议 (P_GEMINI): 支持 gemini-cli-oauth。
```mermaid
graph TD
subgraph Core_Protocols
P_OPENAI(OpenAI Protocol)
P_GEMINI(Gemini Protocol)
P_CLAUDE(Claude Protocol)
end
subgraph Supported_Model_Providers
MP_OPENAI[openai-custom]
MP_GEMINI[gemini-cli-oauth]
MP_CLAUDE_C[claude-custom]
MP_CLAUDE_K[claude-kiro-oauth]
end
subgraph Internal_Conversion_Logic
direction LR
P_OPENAI <-->|Request/Response Conversion| P_GEMINI
P_OPENAI <-->|Request/Response Conversion| P_CLAUDE
P_GEMINI <-->|Request/Response Conversion| P_CLAUDE
end
P_OPENAI ---|Supports| MP_OPENAI
P_OPENAI ---|Supports| MP_GEMINI
P_OPENAI ---|Supports| MP_CLAUDE_C
P_OPENAI ---|Supports| MP_CLAUDE_K
P_GEMINI ---|Supports| MP_GEMINI
P_CLAUDE ---|Supports| MP_CLAUDE_C
P_CLAUDE ---|Supports| MP_CLAUDE_K
P_CLAUDE ---|Supports| MP_GEMINI
style P_OPENAI fill:#f9f,stroke:#333,stroke-width:2px
style P_GEMINI fill:#ccf,stroke:#333,stroke-width:2px
style P_CLAUDE fill:#cfc,stroke:#333,stroke-width:2px
```
---
### 🔧 使用说明
@ -318,3 +365,7 @@
## 🙏 致谢
本项目的开发受到了官方 Google Gemini CLI 的极大启发并参考了Cline 3.18.0 版本 `gemini-cli.ts` 的部分代码实现。在此对 Google 官方团队和 Cline 开发团队的卓越工作表示衷心的感谢!
## 🌟 Star History
[![Star History Chart](https://api.star-history.com/svg?repos=justlovemaki/AIClient-2-API&type=Timeline)](https://www.star-history.com/#justlovemaki/AIClient-2-API&Timeline)

View file

@ -206,6 +206,8 @@ export async function handleStreamRequest(res, service, model, requestBody, from
// The service returns a stream in its native format (toProvider).
const nativeStream = await service.generateContentStream(model, requestBody);
const needsConversion = getProtocolPrefix(fromProvider) !== getProtocolPrefix(toProvider);
const isClaude = getProtocolPrefix(fromProvider) === MODEL_PROTOCOL_PREFIX.CLAUDE;
try {
for await (const nativeChunk of nativeStream) {
@ -215,20 +217,21 @@ export async function handleStreamRequest(res, service, model, requestBody, from
fullResponseText += chunkText;
}
let clientChunk = chunkText;
if (getProtocolPrefix(fromProvider) !== getProtocolPrefix(toProvider)) {
clientChunk = convertData(chunkText, 'streamChunk', toProvider, fromProvider, model);
if(!clientChunk){
continue;
}
res.write(`data: ${JSON.stringify(clientChunk)}\n\n`);
// console.log(`data: ${JSON.stringify(clientChunk)}\n`);
}else{
res.write(`data: ${JSON.stringify(nativeChunk)}\n\n`);
// console.log(`data-nv: ${JSON.stringify(nativeChunk)}\n`);
const chunkToSend = needsConversion
? convertData(chunkText, 'streamChunk', toProvider, fromProvider, model)
: nativeChunk;
if (!chunkToSend) {
continue;
}
if (isClaude) {
res.write(`event: ${chunkToSend.type}\n`);
// console.log(`event: ${chunkToSend.type}\n`);
}
res.write(`data: ${JSON.stringify(chunkToSend)}\n\n`);
// console.log(`data: ${JSON.stringify(chunkToSend)}\n`);
}
} catch (error) {

View file

@ -24,6 +24,7 @@ export function convertData(data, type, fromProvider, toProvider, model) {
},
[MODEL_PROTOCOL_PREFIX.GEMINI]: { // to Gemini protocol
[MODEL_PROTOCOL_PREFIX.OPENAI]: toGeminiRequestFromOpenAI, // from OpenAI protocol
[MODEL_PROTOCOL_PREFIX.CLAUDE]: toGeminiRequestFromClaude, // from Claude protocol
},
},
response: {
@ -31,12 +32,18 @@ export function convertData(data, type, fromProvider, toProvider, model) {
[MODEL_PROTOCOL_PREFIX.GEMINI]: toOpenAIChatCompletionFromGemini, // from Gemini protocol
[MODEL_PROTOCOL_PREFIX.CLAUDE]: toOpenAIChatCompletionFromClaude, // from Claude protocol
},
[MODEL_PROTOCOL_PREFIX.CLAUDE]: { // to Claude protocol
[MODEL_PROTOCOL_PREFIX.GEMINI]: toClaudeChatCompletionFromGemini, // from Gemini protocol
},
},
streamChunk: {
[MODEL_PROTOCOL_PREFIX.OPENAI]: { // to OpenAI protocol
[MODEL_PROTOCOL_PREFIX.GEMINI]: toOpenAIStreamChunkFromGemini, // from Gemini protocol
[MODEL_PROTOCOL_PREFIX.CLAUDE]: toOpenAIStreamChunkFromClaude, // from Claude protocol
},
[MODEL_PROTOCOL_PREFIX.CLAUDE]: { // to Claude protocol
[MODEL_PROTOCOL_PREFIX.GEMINI]: toClaudeStreamChunkFromGemini, // from Gemini protocol
},
},
modelList: {
[MODEL_PROTOCOL_PREFIX.OPENAI]: { // to OpenAI protocol
@ -958,3 +965,471 @@ function isValidAudioType(mimeType) {
];
return validTypes.includes(mimeType.toLowerCase());
}
/**
* Converts a Claude API request body to a Gemini API request body.
* Handles system instructions and multimodal content.
* @param {Object} claudeRequest - The request body from the Claude API.
* @returns {Object} The formatted request body for the Gemini API.
*/
/**
* Converts a Claude API request body to a Gemini API request body.
* Handles system instructions and multimodal content.
* @param {Object} claudeRequest - The request body from the Claude API.
* @returns {Object} The formatted request body for the Gemini API.
*/
export function toGeminiRequestFromClaude(claudeRequest) {
// Ensure claudeRequest is a valid object
if (!claudeRequest || typeof claudeRequest !== 'object') {
console.warn("Invalid claudeRequest provided to toGeminiRequestFromClaude.");
return { contents: [] };
}
const geminiRequest = {
contents: []
};
// Handle system instruction
if (claudeRequest.system) {
let incomingSystemText = null;
if (typeof claudeRequest.system === 'string') {
incomingSystemText = claudeRequest.system;
} else if (typeof claudeRequest.system === 'object') {
incomingSystemText = JSON.stringify(claudeRequest.system);
} else if (claudeRequest.messages?.length > 0) {
// Fallback to first user message if no system property
const userMessage = claudeRequest.messages.find(m => m.role === 'user');
if (userMessage) {
if (Array.isArray(userMessage.content)) {
incomingSystemText = userMessage.content.map(block => block.text).join('');
} else {
incomingSystemText = userMessage.content;
}
}
}
geminiRequest.systemInstruction = {
parts: [{ text: incomingSystemText}] // Ensure system is string
};
}
// Process messages
if (Array.isArray(claudeRequest.messages)) {
claudeRequest.messages.forEach(message => {
// Ensure message is a valid object and has a role and content
if (!message || typeof message !== 'object' || !message.role || !message.content) {
console.warn("Skipping invalid message in claudeRequest.messages.");
return;
}
const geminiRole = message.role === 'assistant' ? 'model' : 'user';
const processedParts = processClaudeContentToGeminiParts(message.content);
// If the processed parts contain a function response, it should be a 'function' role message
// Claude's tool_result block does not contain the function name, only tool_use_id.
// We need to infer the function name from the previous tool_use message.
// For simplicity in this conversion, we'll assume the tool_use_id is the function name
// or that the tool_result is always preceded by a tool_use with the correct name.
// A more robust solution would involve tracking tool_use_ids to function names.
const functionResponsePart = processedParts.find(part => part.functionResponse);
if (functionResponsePart) {
geminiRequest.contents.push({
role: 'function',
parts: [functionResponsePart]
});
} else if (processedParts.length > 0) { // Only push if there are actual parts
geminiRequest.contents.push({
role: geminiRole,
parts: processedParts
});
}
});
}
// Add generation config
const generationConfig = {};
if (claudeRequest.max_tokens !== undefined) {
generationConfig.maxOutputTokens = Number(claudeRequest.max_tokens); // Ensure number type
}
if (claudeRequest.temperature !== undefined) {
generationConfig.temperature = Number(claudeRequest.temperature); // Ensure number type
}
if (claudeRequest.top_p !== undefined) {
generationConfig.topP = Number(claudeRequest.top_p); // Ensure number type
}
if (Object.keys(generationConfig).length > 0) {
geminiRequest.generationConfig = generationConfig;
}
// Handle tools
if (Array.isArray(claudeRequest.tools)) {
geminiRequest.tools = [{
functionDeclarations: claudeRequest.tools.map(tool => {
// Ensure tool is a valid object and has a name
if (!tool || typeof tool !== 'object' || !tool.name) {
console.warn("Skipping invalid tool declaration in claudeRequest.tools.");
return null; // Return null for invalid tools, filter out later
}
delete tool.input_schema.$schema;
return {
name: String(tool.name), // Ensure name is string
description: String(tool.description || ''), // Ensure description is string
parameters: tool.input_schema && typeof tool.input_schema === 'object' ? tool.input_schema : { type: 'object', properties: {} }
};
}).filter(Boolean) // Filter out any nulls from invalid tool declarations
}];
// If no valid functionDeclarations, remove the tools array
if (geminiRequest.tools[0].functionDeclarations.length === 0) {
delete geminiRequest.tools;
}
}
// Handle tool_choice
if (claudeRequest.tool_choice) {
geminiRequest.toolConfig = buildGeminiToolConfigFromClaude(claudeRequest.tool_choice);
}
return geminiRequest;
}
/**
* Builds Gemini toolConfig from Claude tool_choice.
* @param {Object} claudeToolChoice - The tool_choice object from Claude API.
* @returns {Object|undefined} The formatted toolConfig for Gemini API, or undefined if invalid.
*/
function buildGeminiToolConfigFromClaude(claudeToolChoice) {
if (!claudeToolChoice || typeof claudeToolChoice !== 'object' || !claudeToolChoice.type) {
console.warn("Invalid claudeToolChoice provided to buildGeminiToolConfigFromClaude.");
return undefined;
}
switch (claudeToolChoice.type) {
case 'auto':
return { functionCallingConfig: { mode: 'AUTO' } };
case 'none':
return { functionCallingConfig: { mode: 'NONE' } };
case 'tool':
if (claudeToolChoice.name && typeof claudeToolChoice.name === 'string') {
return { functionCallingConfig: { mode: 'ANY', allowedFunctionNames: [claudeToolChoice.name] } };
}
console.warn("Invalid tool name in claudeToolChoice of type 'tool'.");
return undefined;
default:
console.warn(`Unsupported claudeToolChoice type: ${claudeToolChoice.type}`);
return undefined;
}
}
/**
* Processes Claude content to Gemini parts format with multimodal support.
* @param {string|Array} content - Claude message content.
* @returns {Array} Array of Gemini parts.
*/
function processClaudeContentToGeminiParts(content) {
if (!content) return [];
// Handle string content
if (typeof content === 'string') {
return [{ text: content }];
}
// Handle array content (multimodal)
if (Array.isArray(content)) {
const parts = [];
content.forEach(block => {
// Ensure block is a valid object and has a type
if (!block || typeof block !== 'object' || !block.type) {
console.warn("Skipping invalid content block in processClaudeContentToGeminiParts.");
return;
}
switch (block.type) {
case 'text':
if (typeof block.text === 'string') {
parts.push({ text: block.text });
} else {
console.warn("Invalid text content in Claude text block.");
}
break;
case 'image':
if (block.source && typeof block.source === 'object' && block.source.type === 'base64' &&
typeof block.source.media_type === 'string' && typeof block.source.data === 'string') {
parts.push({
inlineData: {
mimeType: block.source.media_type,
data: block.source.data
}
});
} else {
console.warn("Invalid image source in Claude image block.");
}
break;
case 'tool_use':
if (typeof block.name === 'string' && block.input && typeof block.input === 'object') {
parts.push({
functionCall: {
name: block.name,
args: block.input
}
});
} else {
console.warn("Invalid tool_use block in Claude content.");
}
break;
case 'tool_result':
// Claude's tool_result block does not contain the function name, only tool_use_id.
// Gemini's functionResponse requires a function name.
// For now, we'll use the tool_use_id as the name, but this is a potential point of failure
// if the tool_use_id is not the actual function name in Gemini's context.
// A more robust solution would involve tracking the function name from the tool_use block.
if (typeof block.tool_use_id === 'string') {
parts.push({
functionResponse: {
name: block.tool_use_id, // This might need to be the actual function name
response: { content: block.content } // content can be any JSON-serializable value
}
});
} else {
console.warn("Invalid tool_result block in Claude content: missing tool_use_id.");
}
break;
default:
// Handle any other content types as text if they have a text property
if (typeof block.text === 'string') {
parts.push({ text: block.text });
} else {
console.warn(`Unsupported Claude content block type: ${block.type}. Skipping.`);
}
}
});
return parts;
}
return [];
}
/**
* Converts a Gemini API response to a Claude API messages response.
* @param {Object} geminiResponse - The Gemini API response object.
* @param {string} model - The model name to include in the response.
* @returns {Object} The formatted Claude API messages response.
*/
export function toClaudeChatCompletionFromGemini(geminiResponse, model) {
// Handle cases where geminiResponse or candidates are missing or empty
if (!geminiResponse || !geminiResponse.candidates || geminiResponse.candidates.length === 0) {
return {
id: `msg_${uuidv4()}`,
type: "message",
role: "assistant",
content: [], // Empty content for no candidates
model: model,
stop_reason: "end_turn", // Default stop reason
stop_sequence: null,
usage: {
input_tokens: geminiResponse?.usageMetadata?.promptTokenCount || 0,
output_tokens: geminiResponse?.usageMetadata?.candidatesTokenCount || 0
}
};
}
const candidate = geminiResponse.candidates[0];
const content = processGeminiResponseToClaudeContent(geminiResponse);
const finishReason = candidate.finishReason;
let stopReason = "end_turn"; // Default stop reason
if (finishReason) {
switch (finishReason) {
case 'STOP':
stopReason = 'end_turn';
break;
case 'MAX_TOKENS':
stopReason = 'max_tokens';
break;
case 'SAFETY':
stopReason = 'safety';
break;
case 'RECITATION':
stopReason = 'recitation';
break;
case 'OTHER':
stopReason = 'other';
break;
default:
stopReason = 'end_turn';
}
}
return {
id: `msg_${uuidv4()}`,
type: "message",
role: "assistant",
content: content,
model: model,
stop_reason: stopReason,
stop_sequence: null,
usage: {
input_tokens: geminiResponse.usageMetadata?.promptTokenCount || 0,
output_tokens: geminiResponse.usageMetadata?.candidatesTokenCount || 0
}
};
}
/**
* Processes Gemini response content to Claude format.
* @param {Object} geminiResponse - The Gemini API response.
* @returns {Array} Array of Claude content blocks.
*/
function processGeminiResponseToClaudeContent(geminiResponse) {
if (!geminiResponse || !geminiResponse.candidates || geminiResponse.candidates.length === 0) return [];
const content = [];
geminiResponse.candidates.forEach(candidate => {
if (candidate.content && candidate.content.parts) {
candidate.content.parts.forEach(part => {
if (part.text) {
content.push({
type: 'text',
text: part.text
});
} else if (part.inlineData) {
content.push({
type: 'image',
source: {
type: 'base64',
media_type: part.inlineData.mimeType,
data: part.inlineData.data
}
});
} else if (part.functionCall) {
// Convert Gemini functionCall to Claude tool_use
content.push({
type: 'tool_use',
id: uuidv4(), // Generate a new ID for the tool use
name: part.functionCall.name,
input: part.functionCall.args || {}
});
}
});
}
});
return content;
}
/**
* Converts a Gemini API stream chunk to a Claude API messages stream chunk.
* @param {Object} geminiChunk - The Gemini API stream chunk object.
* @param {string} [model] - Optional model name to include in the response.
* @returns {Object} The formatted Claude API messages stream chunk.
*/
export function toClaudeStreamChunkFromGemini(geminiChunk, model) {
if (!geminiChunk) {
return null;
}
// Handle different types of Gemini stream events
if (geminiChunk.candidates && geminiChunk.candidates.length > 0) {
const candidate = geminiChunk.candidates[0];
if (candidate.content && candidate.content.parts) {
const textParts = candidate.content.parts
.filter(part => part.text)
.map(part => part.text);
const functionCallPart = candidate.content.parts.find(part => part.functionCall);
if (functionCallPart) {
// Handle tool_use
return {
type: "content_block_start",
index: 0,
content_block: {
type: "tool_use",
id: `toolu_${uuidv4()}`, // Claude tool use ID format
name: functionCallPart.functionCall.name,
input: functionCallPart.functionCall.args || {}
}
};
} else if (textParts.length > 0) {
return {
type: "content_block_delta",
index: 0,
delta: {
type: "text_delta",
text: textParts.join('')
}
};
}
}
// Handle finish reason
if (candidate.finishReason) {
let stopReason = "end_turn";
switch (candidate.finishReason) {
case 'STOP':
stopReason = 'end_turn';
break;
case 'MAX_TOKENS':
stopReason = 'max_tokens';
break;
case 'SAFETY':
stopReason = 'safety';
break;
case 'RECITATION':
stopReason = 'recitation';
break;
case 'OTHER':
stopReason = 'other';
break;
default:
stopReason = 'end_turn';
}
return {
type: "message_delta",
delta: {
stop_reason: stopReason,
stop_sequence: null
},
usage: geminiChunk.usageMetadata ? {
output_tokens: geminiChunk.usageMetadata.candidatesTokenCount || 0
} : undefined
};
}
}
// Handle usage metadata updates (only if no other content/finish reason)
if (geminiChunk.usageMetadata && (!geminiChunk.candidates || geminiChunk.candidates.length === 0)) {
return {
type: "message_delta",
delta: {},
usage: {
input_tokens: geminiChunk.usageMetadata.promptTokenCount || 0,
output_tokens: geminiChunk.usageMetadata.candidatesTokenCount || 0
}
};
}
// Default text delta for simple text chunks (should ideally be handled by candidate.content.parts)
// This case might occur if the geminiChunk is just a string, which is not typical for Gemini API.
// Added for robustness, but main logic should rely on geminiChunk.candidates.
if (typeof geminiChunk === 'string') {
return {
type: "content_block_delta",
index: 0,
delta: {
type: "text_delta",
text: geminiChunk
}
};
}
return null;
}

View file

@ -14,6 +14,7 @@ const CODE_ASSIST_ENDPOINT = 'https://cloudcode-pa.googleapis.com';
const CODE_ASSIST_API_VERSION = 'v1internal';
const OAUTH_CLIENT_ID = '681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com';
const OAUTH_CLIENT_SECRET = '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';
const GEMINI_MODELS = ['gemini-2.5-flash', 'gemini-2.5-pro'];
function toGeminiApiResponse(codeAssistResponse) {
if (!codeAssistResponse) return null;
@ -45,7 +46,7 @@ export class GeminiApiService {
this.projectId = await this.discoverProjectAndModels();
} else {
console.log(`[Gemini] Using provided Project ID: ${this.projectId}`);
this.availableModels = ['gemini-2.5-pro', 'gemini-2.5-flash'];
this.availableModels = GEMINI_MODELS;
console.log(`[Gemini] Using fixed models: [${this.availableModels.join(', ')}]`);
}
if (this.projectId === 'default') {
@ -155,7 +156,7 @@ export class GeminiApiService {
}
console.log('[Gemini] Discovering Project ID...');
this.availableModels = ['gemini-2.5-pro', 'gemini-2.5-flash'];
this.availableModels = GEMINI_MODELS;
console.log(`[Gemini] Using fixed models: [${this.availableModels.join(', ')}]`);
try {
const loadResponse = await this.callApi('loadCodeAssist', { metadata: { pluginType: 'GEMINI' } });
@ -298,16 +299,26 @@ export class GeminiApiService {
async generateContent(model, requestBody) {
console.log(`[Auth Token] Time until expiry: ${formatExpiryTime(this.authClient.credentials.expiry_date)}`);
let selectedModel = model;
if (!GEMINI_MODELS.includes(model)) {
console.warn(`[Gemini] Model '${model}' not found. Using default model: '${GEMINI_MODELS[0]}'`);
selectedModel = GEMINI_MODELS[0];
}
const processedRequestBody = ensureRolesInContents(requestBody);
const apiRequest = { model, project: this.projectId, request: processedRequestBody };
const apiRequest = { model: selectedModel, project: this.projectId, request: processedRequestBody };
const response = await this.callApi(API_ACTIONS.GENERATE_CONTENT, apiRequest);
return toGeminiApiResponse(response.response);
}
async * generateContentStream(model, requestBody) {
console.log(`[Auth Token] Time until expiry: ${formatExpiryTime(this.authClient.credentials.expiry_date)}`);
let selectedModel = model;
if (!GEMINI_MODELS.includes(model)) {
console.warn(`[Gemini] Model '${model}' not found. Using default model: '${GEMINI_MODELS[0]}'`);
selectedModel = GEMINI_MODELS[0];
}
const processedRequestBody = ensureRolesInContents(requestBody);
const apiRequest = { model, project: this.projectId, request: processedRequestBody };
const apiRequest = { model: selectedModel, project: this.projectId, request: processedRequestBody };
const stream = this.streamApi(API_ACTIONS.STREAM_GENERATE_CONTENT, apiRequest);
for await (const chunk of stream) {
yield toGeminiApiResponse(chunk.response);