From b031ca428634986cbece985b13941a1076c376b8 Mon Sep 17 00:00:00 2001 From: hex2077 Date: Thu, 31 Jul 2025 22:54:21 +0800 Subject: [PATCH] =?UTF-8?q?feat(protocol):=20=E6=94=AF=E6=8C=81=20Claude?= =?UTF-8?q?=20=E4=B8=8E=20Gemini=20=E5=8D=8F=E8=AE=AE=E9=97=B4=E7=9A=84?= =?UTF-8?q?=E5=8F=8C=E5=90=91=E8=BD=AC=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实现了 Claude API 请求到 Gemini API 请求的转换,以及 Gemini API 响应到 Claude API 响应的转换。 同时支持了两种协议间流式响应的转换,增强了项目的兼容性和灵活性。 更新了 README 文件,新增模型协议关系图和 Star History。 优化了 Gemini 核心服务中的模型选择逻辑,当请求的模型不存在时,会使用默认模型。 --- README-EN.md | 53 +++++ README.md | 51 ++++ src/common.js | 29 +-- src/convert.js | 475 ++++++++++++++++++++++++++++++++++++++ src/gemini/gemini-core.js | 19 +- 5 files changed, 610 insertions(+), 17 deletions(-) diff --git a/README-EN.md b/README-EN.md index 54062f1..93a2fd7 100644 --- a/README-EN.md +++ b/README-EN.md @@ -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) diff --git a/README.md b/README.md index fde6275..d371c29 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/src/common.js b/src/common.js index 6b93ef5..fa16303 100644 --- a/src/common.js +++ b/src/common.js @@ -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) { diff --git a/src/convert.js b/src/convert.js index 108a877..8c39069 100644 --- a/src/convert.js +++ b/src/convert.js @@ -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; +} diff --git a/src/gemini/gemini-core.js b/src/gemini/gemini-core.js index 21156ed..c4453f0 100644 --- a/src/gemini/gemini-core.js +++ b/src/gemini/gemini-core.js @@ -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);