diff --git a/README-JA.md b/README-JA.md
index bc9c239..a5bb59b 100644
--- a/README-JA.md
+++ b/README-JA.md
@@ -31,6 +31,9 @@
>
> **📅 バージョン更新ログ**
>
+>
+> クリックして詳細なバージョン履歴を展開
+>
> - **2026.01.03** - テーマ切替機能を追加し、プロバイダープール初期化を最適化、プロバイダーのデフォルト設定を使用するフォールバック戦略を削除
> - **2025.12.30** - メインプロセス管理と自動更新機能を追加
> - **2025.12.25** - 設定ファイル統一管理:すべての設定を `configs/` ディレクトリに集約。Dockerユーザーはマウントパスを `-v "ローカルパス:/app/configs"` に更新が必要
@@ -47,6 +50,7 @@
> - **開発済み履歴**
> - Gemini CLI、Kiroなどのクライアント2APIをサポート
> - OpenAI、Claude、Geminiの3つのプロトコル相互変換、自動インテリジェント切り替え
+>
---
@@ -204,6 +208,9 @@ docker compose up -d
### 🔐 認証設定ガイド
+
+クリックして各プロバイダーの認証設定詳細手順を展開
+
> **💡 ヒント**:最適な体験を得るために、**Web UIコンソール**を通じてビジュアルに認証管理を行うことを推奨します。
#### 🌐 Web UI クイック認証 (推奨)
@@ -245,8 +252,13 @@ Web UI管理インターフェースでは、極めて迅速に認証設定を
3. **起動パラメータ設定**:`--provider-pools-file ` パラメータを使用してプール設定ファイルのパスを指定します
4. **ヘルスチェック**:システムは定期的にヘルスチェックを自動実行し、健全でないプロバイダーを使用しません
+
+
### 📁 認証ファイル保存パス
+
+クリックして各サービスの認証情報のデフォルト保存場所を展開
+
各サービスの認証情報ファイルのデフォルト保存場所:
| サービス | デフォルトパス | 説明 |
@@ -257,9 +269,11 @@ Web UI管理インターフェースでは、極めて迅速に認証設定を
| **Antigravity** | `~/.antigravity/oauth_creds.json` | Antigravity OAuth認証情報 (Claude 4.5 Opus サポート) |
> **説明**:`~`はユーザーホームディレクトリを表します(Windows: `C:\Users\ユーザー名`、Linux/macOS: `/home/ユーザー名`または`/Users/ユーザー名`)
->
+
> **カスタムパス**:設定ファイルの関連パラメータまたは環境変数でカスタム保存場所を指定可能
+
+
---
### 🦙 Ollamaプロトコル使用例
@@ -270,13 +284,15 @@ Web UI管理インターフェースでは、極めて迅速に認証設定を
1. **利用可能なすべてのモデルをリスト表示**:
```bash
-curl http://localhost:3000/ollama/api/tags
+curl http://localhost:3000/ollama/api/tags \
+ -H "Authorization: Bearer your-api-key"
```
2. **チャットインターフェース**:
```bash
curl http://localhost:3000/ollama/api/chat \
-H "Content-Type: application/json" \
+ -H "Authorization: Bearer your-api-key" \
-d '{
"model": "[Claude] claude-sonnet-4.5",
"messages": [
@@ -296,6 +312,9 @@ curl http://localhost:3000/ollama/api/chat \
### 高度な設定
+
+クリックしてプロキシ設定、モデルフィルタリング、Fallbackなどの高度な設定を展開
+
#### 1. プロキシ設定
本プロジェクトは柔軟なプロキシ設定をサポートしており、異なるプロバイダーに統一プロキシを設定したり、プロバイダー独自のプロキシ済みエンドポイントを使用したりできます。
@@ -410,10 +429,15 @@ curl http://localhost:3000/ollama/api/chat \
- フォールバックはプロトコル互換タイプ間でのみ発生します(例:`gemini-*` 間、`claude-*` 間)
- システムは自動的にターゲットProvider Typeがリクエストされたモデルをサポートしているか確認します
+
+
---
## ❓ よくある質問
+
+クリックしてよくある質問と解決策を展開(ポート占有、Docker起動、429エラーなど)
+
### 1. OAuth認証失敗
**問題の説明**:「認証生成」をクリックした後、ブラウザで認証ページが開きますが、認証が失敗するか完了できません。
@@ -532,6 +556,8 @@ kill -9
- **リクエストヘッダー形式を確認**:リクエストに正しい形式のAuthorizationヘッダーが含まれていることを確認、例:`Authorization: Bearer your-api-key`
- **サービスログを確認**:Web UIの「リアルタイムログ」ページで詳細なエラーメッセージを確認し、具体的な原因を特定
+
+
---
## 📄 オープンソースライセンス
diff --git a/README-ZH.md b/README-ZH.md
index 7fedf91..9591bbb 100644
--- a/README-ZH.md
+++ b/README-ZH.md
@@ -31,6 +31,9 @@
>
> **📅 版本更新日志**
>
+>
+> 点击展开查看详细版本历史
+>
> - **2026.01.03** - 新增主题切换功能并优化提供商池初始化,移除使用提供商默认配置的降级策略
> - **2025.12.30** - 添加主进程管理和自动更新功能
> - **2025.12.25** - 配置文件统一管理:所有配置集中到 `configs/` 目录,Docker 用户需更新挂载路径为 `-v "本地路径:/app/configs"`
@@ -47,6 +50,7 @@
> - **历史已开发**
> - 支持 Gemini CLI、Kiro 等客户端2API
> - OpenAI ,Claude ,Gemini 三协议互转,自动智能切换
+>
---
## 💡 核心优势
@@ -203,6 +207,9 @@ docker compose up -d
### 🔐 授权配置指南
+
+点击展开查看各提供商授权配置详细步骤
+
> **💡 提示**:为了获得最佳体验,建议通过 **Web UI 控制台** 进行可视化授权管理。
#### 🌐 Web UI 快捷授权 (推荐)
@@ -244,8 +251,13 @@ docker compose up -d
3. **启动参数配置**:使用 `--provider-pools-file ` 参数指定号池配置文件路径
4. **健康检查**:系统会定期自动执行健康检查,不使用不健康的提供商
+
+
### 📁 授权文件存储路径
+
+点击展开查看各服务授权凭据的默认存储位置
+
各服务的授权凭据文件默认存储位置:
| 服务 | 默认路径 | 说明 |
@@ -259,6 +271,8 @@ docker compose up -d
> **自定义路径**:可通过配置文件中的相关参数或环境变量指定自定义存储位置
+
+
---
### 🦙 Ollama 协议使用示例
@@ -269,13 +283,15 @@ docker compose up -d
1. **列出所有可用模型**:
```bash
-curl http://localhost:3000/ollama/api/tags
+curl http://localhost:3000/ollama/api/tags \
+ -H "Authorization: Bearer your-api-key"
```
2. **聊天接口**:
```bash
curl http://localhost:3000/ollama/api/chat \
-H "Content-Type: application/json" \
+ -H "Authorization: Bearer your-api-key" \
-d '{
"model": "[Claude] claude-sonnet-4.5",
"messages": [
@@ -295,6 +311,9 @@ curl http://localhost:3000/ollama/api/chat \
### 高级配置
+
+点击展开查看代理配置、模型过滤及 Fallback 等高级设置
+
#### 1. 代理配置
本项目支持灵活的代理配置,可以为不同的提供商配置统一代理或使用提供商自带的已代理端点。
@@ -409,10 +428,15 @@ curl http://localhost:3000/ollama/api/chat \
- Fallback 只会在协议兼容的类型之间进行(如 `gemini-*` 之间、`claude-*` 之间)
- 系统会自动检查目标 Provider Type 是否支持当前请求的模型
+
+
---
## ❓ 常见问题
+
+点击展开查看常见问题及解决方案(端口占用、Docker 启动、429 错误等)
+
### 1. OAuth 授权失败
**问题描述**:点击"生成授权"后,浏览器打开授权页面但授权失败或无法完成。
@@ -531,6 +555,8 @@ kill -9
- **检查请求头格式**:确保请求中包含正确格式的 Authorization 头,如 `Authorization: Bearer your-api-key`
- **查看服务日志**:在 Web UI 的"实时日志"页面查看详细错误信息,定位具体原因
+
+
---
## 📄 开源许可
diff --git a/README.md b/README.md
index 50a1d1b..827b139 100644
--- a/README.md
+++ b/README.md
@@ -31,6 +31,9 @@
>
> **📅 Version Update Log**
>
+>
+> Click to expand detailed version history
+>
> - **2026.01.03** - Added theme switching functionality and optimized provider pool initialization, removed the fallback strategy of using provider default configuration
> - **2025.12.30** - Added main process management and automatic update functionality
> - **2025.12.25** - Unified configuration management: All configs centralized to `configs/` directory. Docker users need to update mount path to `-v "local_path:/app/configs"`
@@ -47,6 +50,7 @@
> - **History Developed**
> - Support Gemini CLI, Kiro and other client2API
> - OpenAI, Claude, Gemini three-protocol mutual conversion, automatic intelligent switching
+>
---
@@ -204,6 +208,9 @@ Seamlessly support the following latest large models, just configure the corresp
### 🔐 Authorization Configuration Guide
+
+Click to expand detailed authorization configuration steps for each provider
+
> **💡 Tip**: For the best experience, it is recommended to manage authorization visually through the **Web UI console**.
#### 🌐 Web UI Quick Authorization (Recommended)
@@ -245,8 +252,13 @@ In the Web UI management interface, you can complete authorization configuration
3. **Startup Parameter Configuration**: Use the `--provider-pools-file ` parameter to specify the pool configuration file path
4. **Health Check**: The system will automatically perform periodic health checks and avoid using unhealthy providers
+
+
### 📁 Authorization File Storage Paths
+
+Click to expand default storage locations for authorization credentials
+
Default storage locations for authorization credential files of each service:
| Service | Default Path | Description |
@@ -257,9 +269,11 @@ Default storage locations for authorization credential files of each service:
| **Antigravity** | `~/.antigravity/oauth_creds.json` | Antigravity OAuth credentials (supports Claude 4.5 Opus) |
> **Note**: `~` represents the user home directory (Windows: `C:\Users\username`, Linux/macOS: `/home/username` or `/Users/username`)
->
+
> **Custom Path**: Can specify custom storage location via relevant parameters in configuration file or environment variables
+
+
---
### 🦙 Ollama Protocol Usage Examples
@@ -270,13 +284,15 @@ This project supports the Ollama protocol, allowing access to all supported mode
1. **List all available models**:
```bash
-curl http://localhost:3000/ollama/api/tags
+curl http://localhost:3000/ollama/api/tags \
+ -H "Authorization: Bearer your-api-key"
```
2. **Chat interface**:
```bash
curl http://localhost:3000/ollama/api/chat \
-H "Content-Type: application/json" \
+ -H "Authorization: Bearer your-api-key" \
-d '{
"model": "[Claude] claude-sonnet-4.5",
"messages": [
@@ -296,6 +312,9 @@ curl http://localhost:3000/ollama/api/chat \
### Advanced Configuration
+
+Click to expand proxy configuration, model filtering, and Fallback advanced settings
+
#### 1. Proxy Configuration
This project supports flexible proxy configuration, allowing you to configure a unified proxy for different providers or use provider-specific proxied endpoints.
@@ -410,10 +429,15 @@ When all accounts under a Provider Type (e.g., `gemini-cli-oauth`) are exhausted
- Fallback only occurs between protocol-compatible types (e.g., between `gemini-*`, between `claude-*`)
- The system automatically checks if the target Provider Type supports the requested model
+
+
---
## ❓ FAQ
+
+Click to expand FAQ and solutions (port occupation, Docker startup, 429 errors, etc.)
+
### 1. OAuth Authorization Failed
**Problem Description**: After clicking "Generate Authorization", the browser opens the authorization page but authorization fails or cannot be completed.
@@ -532,6 +556,8 @@ Or modify the port configuration in `configs/config.json` to use a different por
- **Check Request Header Format**: Ensure the request contains the correct Authorization header format, such as `Authorization: Bearer your-api-key`
- **Check Service Logs**: View detailed error messages on the "Real-time Logs" page in Web UI to locate the specific cause
+
+
---
## 📄 Open Source License
diff --git a/src/claude/claude-kiro.js b/src/claude/claude-kiro.js
index 73c165e..6da932e 100644
--- a/src/claude/claude-kiro.js
+++ b/src/claude/claude-kiro.js
@@ -308,6 +308,7 @@ export class KiroApiService {
this.modelName = KIRO_CONSTANTS.DEFAULT_MODEL_NAME;
this.axiosInstance = null; // Initialize later in async method
+ this.axiosSocialRefreshInstance = null;
}
async initialize() {
@@ -361,6 +362,10 @@ export class KiroApiService {
configureAxiosProxy(axiosConfig, this.config, 'claude-kiro-oauth');
this.axiosInstance = axios.create(axiosConfig);
+
+ axiosConfig.headers = new Headers();
+ axiosConfig.headers.set('Content-Type', KIRO_CONSTANTS.CONTENT_TYPE_JSON);
+ this.axiosSocialRefreshInstance = axios.create(axiosConfig);
this.isInitialized = true;
}
@@ -496,8 +501,15 @@ async initializeAuth(forceRefresh = false) {
requestBody.clientSecret = this.clientSecret;
requestBody.grantType = 'refresh_token';
}
- const response = await this.axiosInstance.post(refreshUrl, requestBody);
- console.log('[Kiro Auth] Token refresh response: ok');
+
+ let response = null;
+ if (this.authMethod === KIRO_CONSTANTS.AUTH_METHOD_SOCIAL) {
+ response = await this.axiosSocialRefreshInstance.post(refreshUrl, requestBody);
+ console.log('[Kiro Auth] Token refresh social response: ok');
+ }else{
+ response = await this.axiosInstance.post(refreshUrl, requestBody);
+ console.log('[Kiro Auth] Token refresh idc response: ok');
+ }
if (response.data && response.data.accessToken) {
this.accessToken = response.data.accessToken;
@@ -659,9 +671,15 @@ async initializeAuth(forceRefresh = false) {
}
}
- // Add remaining user/assistant messages to history
+ // 保留最近 5 条历史消息中的图片
+ const keepImageThreshold = 5;
for (let i = startIndex; i < processedMessages.length - 1; i++) {
const message = processedMessages[i];
+ // 计算当前消息距离最后一条消息的位置(从后往前数)
+ const distanceFromEnd = (processedMessages.length - 1) - i;
+ // 如果距离末尾不超过 5 条,则保留图片
+ const shouldKeepImages = distanceFromEnd <= keepImageThreshold;
+
if (message.role === 'user') {
let userInputMessage = {
content: '',
@@ -670,6 +688,7 @@ async initializeAuth(forceRefresh = false) {
};
let imageCount = 0;
let toolResults = [];
+ let images = [];
if (Array.isArray(message.content)) {
for (const part of message.content) {
@@ -682,22 +701,37 @@ async initializeAuth(forceRefresh = false) {
toolUseId: part.tool_use_id
});
} else if (part.type === 'image') {
- // 历史消息中的图片不保留 base64 数据,只记录数量
- // 避免请求体过大导致 400 错误
- imageCount++;
+ if (shouldKeepImages) {
+ // 最近 5 条消息内的图片保留原始数据
+ images.push({
+ format: part.source.media_type.split('/')[1],
+ source: {
+ bytes: part.source.data
+ }
+ });
+ } else {
+ // 超过 5 条历史记录的图片只记录数量
+ imageCount++;
+ }
}
}
} else {
userInputMessage.content = this.getContentText(message);
}
- // 如果历史消息中有图片,添加占位符说明
+ // 如果有保留的图片,添加到消息中
+ if (images.length > 0) {
+ userInputMessage.images = images;
+ console.log(`[Kiro] Kept ${images.length} image(s) in recent history message (distance from end: ${distanceFromEnd})`);
+ }
+
+ // 如果有被替换的图片,添加占位符说明
if (imageCount > 0) {
const imagePlaceholder = `[此消息包含 ${imageCount} 张图片,已在历史记录中省略]`;
- userInputMessage.content = userInputMessage.content
- ? `${userInputMessage.content}\n${imagePlaceholder}`
+ userInputMessage.content = userInputMessage.content
+ ? `${userInputMessage.content}\n${imagePlaceholder}`
: imagePlaceholder;
- console.log(`[Kiro] Replaced ${imageCount} image(s) with placeholder in history message`);
+ console.log(`[Kiro] Replaced ${imageCount} image(s) with placeholder in old history message (distance from end: ${distanceFromEnd})`);
}
if (toolResults.length > 0) {
diff --git a/src/common.js b/src/common.js
index 147a329..b2f0cd8 100644
--- a/src/common.js
+++ b/src/common.js
@@ -482,58 +482,49 @@ export function extractPromptText(requestBody, provider) {
return strategy.extractPromptText(requestBody);
}
-export function handleError(res, error) {
- const statusCode = error.response?.status || 500;
+export function handleError(res, error, provider = null) {
+ const statusCode = error.response?.status || error.statusCode || error.status || error.code || 500;
let errorMessage = error.message;
let suggestions = [];
+ // 仅在没有传入错误信息时,才使用默认消息;否则只添加建议
+ const hasOriginalMessage = error.message && error.message.trim() !== '';
+
+ // 根据提供商获取适配的错误信息和建议
+ const providerSuggestions = _getProviderSpecificSuggestions(statusCode, provider);
+
// Provide detailed information and suggestions for different error types
switch (statusCode) {
case 401:
errorMessage = 'Authentication failed. Please check your credentials.';
- suggestions = [
- 'Verify your OAuth credentials are valid',
- 'Try re-authenticating by deleting the credentials file',
- 'Check if your Google Cloud project has the necessary permissions'
- ];
+ suggestions = providerSuggestions.auth;
break;
case 403:
errorMessage = 'Access forbidden. Insufficient permissions.';
- suggestions = [
- 'Ensure your Google Cloud project has the Code Assist API enabled',
- 'Check if your account has the necessary permissions',
- 'Verify the project ID is correct'
- ];
+ suggestions = providerSuggestions.permission;
break;
case 429:
errorMessage = 'Too many requests. Rate limit exceeded.';
- suggestions = [
- 'The request has been automatically retried with exponential backoff',
- 'If the issue persists, try reducing the request frequency',
- 'Consider upgrading your API quota if available'
- ];
+ suggestions = providerSuggestions.rateLimit;
break;
case 500:
case 502:
case 503:
case 504:
errorMessage = 'Server error occurred. This is usually temporary.';
- suggestions = [
- 'The request has been automatically retried',
- 'If the issue persists, try again in a few minutes',
- 'Check Google Cloud status page for service outages'
- ];
+ suggestions = providerSuggestions.serverError;
break;
default:
if (statusCode >= 400 && statusCode < 500) {
errorMessage = `Client error (${statusCode}): ${error.message}`;
- suggestions = ['Check your request format and parameters'];
+ suggestions = providerSuggestions.clientError;
} else if (statusCode >= 500) {
errorMessage = `Server error (${statusCode}): ${error.message}`;
- suggestions = ['This is a server-side issue, please try again later'];
+ suggestions = providerSuggestions.serverError;
}
}
+ errorMessage = hasOriginalMessage ? error.message.trim() : errorMessage;
console.error(`\n[Server] Request failed (${statusCode}): ${errorMessage}`);
if (suggestions.length > 0) {
console.error('[Server] Suggestions:');
@@ -558,6 +549,168 @@ export function handleError(res, error) {
res.end(JSON.stringify(errorPayload));
}
+/**
+ * 根据提供商类型获取适配的错误建议
+ * @param {number} statusCode - HTTP 状态码
+ * @param {string|null} provider - 提供商类型
+ * @returns {Object} 包含各类错误建议的对象
+ */
+function _getProviderSpecificSuggestions(statusCode, provider) {
+ const protocolPrefix = provider ? getProtocolPrefix(provider) : null;
+
+ // 默认/通用建议
+ const defaultSuggestions = {
+ auth: [
+ 'Verify your API key or credentials are valid',
+ 'Check if your credentials have expired',
+ 'Ensure the API key has the necessary permissions'
+ ],
+ permission: [
+ 'Check if your account has the necessary permissions',
+ 'Verify the API endpoint is accessible with your credentials',
+ 'Contact your administrator if permissions are restricted'
+ ],
+ rateLimit: [
+ 'The request has been automatically retried with exponential backoff',
+ 'If the issue persists, try reducing the request frequency',
+ 'Consider upgrading your API quota if available'
+ ],
+ serverError: [
+ 'The request has been automatically retried',
+ 'If the issue persists, try again in a few minutes',
+ 'Check the service status page for outages'
+ ],
+ clientError: [
+ 'Check your request format and parameters',
+ 'Verify the model name is correct',
+ 'Ensure all required fields are provided'
+ ]
+ };
+
+ // 根据提供商返回特定建议
+ switch (protocolPrefix) {
+ case MODEL_PROTOCOL_PREFIX.GEMINI:
+ return {
+ auth: [
+ 'Verify your OAuth credentials are valid',
+ 'Try re-authenticating by deleting the credentials file',
+ 'Check if your Google Cloud project has the necessary permissions'
+ ],
+ permission: [
+ 'Ensure your Google Cloud project has the Gemini API enabled',
+ 'Check if your account has the necessary permissions',
+ 'Verify the project ID is correct'
+ ],
+ rateLimit: [
+ 'The request has been automatically retried with exponential backoff',
+ 'If the issue persists, try reducing the request frequency',
+ 'Consider upgrading your Google Cloud API quota'
+ ],
+ serverError: [
+ 'The request has been automatically retried',
+ 'If the issue persists, try again in a few minutes',
+ 'Check Google Cloud status page for service outages'
+ ],
+ clientError: [
+ 'Check your request format and parameters',
+ 'Verify the model name is a valid Gemini model',
+ 'Ensure all required fields are provided'
+ ]
+ };
+
+ case MODEL_PROTOCOL_PREFIX.OPENAI:
+ case MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES:
+ return {
+ auth: [
+ 'Verify your OpenAI API key is valid',
+ 'Check if your API key has expired or been revoked',
+ 'Ensure the API key is correctly formatted (starts with sk-)'
+ ],
+ permission: [
+ 'Check if your OpenAI account has access to the requested model',
+ 'Verify your organization settings allow this operation',
+ 'Ensure you have sufficient credits in your account'
+ ],
+ rateLimit: [
+ 'The request has been automatically retried with exponential backoff',
+ 'If the issue persists, try reducing the request frequency',
+ 'Consider upgrading your OpenAI usage tier for higher limits'
+ ],
+ serverError: [
+ 'The request has been automatically retried',
+ 'If the issue persists, try again in a few minutes',
+ 'Check OpenAI status page (status.openai.com) for outages'
+ ],
+ clientError: [
+ 'Check your request format and parameters',
+ 'Verify the model name is a valid OpenAI model',
+ 'Ensure the message format is correct (role and content fields)'
+ ]
+ };
+
+ case MODEL_PROTOCOL_PREFIX.CLAUDE:
+ return {
+ auth: [
+ 'Verify your Anthropic API key is valid',
+ 'Check if your API key has expired or been revoked',
+ 'Ensure the x-api-key header is correctly set'
+ ],
+ permission: [
+ 'Check if your Anthropic account has access to the requested model',
+ 'Verify your account is in good standing',
+ 'Ensure you have sufficient credits in your account'
+ ],
+ rateLimit: [
+ 'The request has been automatically retried with exponential backoff',
+ 'If the issue persists, try reducing the request frequency',
+ 'Consider upgrading your Anthropic usage tier for higher limits'
+ ],
+ serverError: [
+ 'The request has been automatically retried',
+ 'If the issue persists, try again in a few minutes',
+ 'Check Anthropic status page for service outages'
+ ],
+ clientError: [
+ 'Check your request format and parameters',
+ 'Verify the model name is a valid Claude model',
+ 'Ensure the message format follows Anthropic API specifications'
+ ]
+ };
+
+ case MODEL_PROTOCOL_PREFIX.OLLAMA:
+ return {
+ auth: [
+ 'Ollama typically does not require authentication',
+ 'If using a custom setup, verify your credentials',
+ 'Check if the Ollama server requires authentication'
+ ],
+ permission: [
+ 'Verify the Ollama server is accessible',
+ 'Check if the requested model is available locally',
+ 'Ensure the Ollama server allows the requested operation'
+ ],
+ rateLimit: [
+ 'The local Ollama server may be overloaded',
+ 'Try reducing concurrent requests',
+ 'Consider increasing server resources if running locally'
+ ],
+ serverError: [
+ 'Check if the Ollama server is running',
+ 'Verify the server address and port are correct',
+ 'Check Ollama server logs for detailed error information'
+ ],
+ clientError: [
+ 'Check your request format and parameters',
+ 'Verify the model name is available in your Ollama installation',
+ 'Try pulling the model first with: ollama pull '
+ ]
+ };
+
+ default:
+ return defaultSuggestions;
+ }
+}
+
/**
* 从请求体中提取系统提示词。
* @param {Object} requestBody - 请求体对象。
diff --git a/src/converters/strategies/ClaudeConverter.js b/src/converters/strategies/ClaudeConverter.js
index 15a0bd9..5960054 100644
--- a/src/converters/strategies/ClaudeConverter.js
+++ b/src/converters/strategies/ClaudeConverter.js
@@ -742,6 +742,9 @@ export class ClaudeConverter extends BaseConverter {
// Claude -> Gemini 转换
// =========================================================================
+ // Gemini Claude thought signature constant
+ static GEMINI_CLAUDE_THOUGHT_SIGNATURE = "skip_thought_signature_validator";
+
/**
* Claude请求 -> Gemini请求
*/
@@ -755,40 +758,148 @@ export class ClaudeConverter extends BaseConverter {
contents: []
};
- // 处理系统指令
+ // 处理系统指令 - 支持数组和字符串格式
if (claudeRequest.system) {
- let incomingSystemText = null;
- if (typeof claudeRequest.system === 'string') {
- incomingSystemText = claudeRequest.system;
+ if (Array.isArray(claudeRequest.system)) {
+ // 数组格式的系统指令
+ const systemParts = [];
+ claudeRequest.system.forEach(systemPrompt => {
+ if (systemPrompt && systemPrompt.type === 'text' && typeof systemPrompt.text === 'string') {
+ systemParts.push({ text: systemPrompt.text });
+ }
+ });
+ if (systemParts.length > 0) {
+ geminiRequest.systemInstruction = {
+ role: 'user',
+ parts: systemParts
+ };
+ }
+ } else if (typeof claudeRequest.system === 'string') {
+ // 字符串格式的系统指令
+ geminiRequest.systemInstruction = {
+ parts: [{ text: claudeRequest.system }]
+ };
} else if (typeof claudeRequest.system === 'object') {
- incomingSystemText = JSON.stringify(claudeRequest.system);
+ // 对象格式的系统指令
+ geminiRequest.systemInstruction = {
+ parts: [{ text: JSON.stringify(claudeRequest.system) }]
+ };
}
- geminiRequest.systemInstruction = {
- parts: [{ text: incomingSystemText }]
- };
}
// 处理消息
if (Array.isArray(claudeRequest.messages)) {
claudeRequest.messages.forEach(message => {
- if (!message || typeof message !== 'object' || !message.role || !message.content) {
+ if (!message || typeof message !== 'object' || !message.role) {
console.warn("Skipping invalid message in claudeRequest.messages.");
return;
}
const geminiRole = message.role === 'assistant' ? 'model' : 'user';
- const processedParts = this.processClaudeContentToGeminiParts(message.content);
+ const content = message.content;
- const functionResponsePart = processedParts.find(part => part.functionResponse);
- if (functionResponsePart) {
- geminiRequest.contents.push({
- role: 'function',
- parts: [functionResponsePart]
+ // 处理内容
+ if (Array.isArray(content)) {
+ const parts = [];
+
+ content.forEach(block => {
+ if (!block || typeof block !== 'object') return;
+
+ switch (block.type) {
+ case 'text':
+ if (typeof block.text === 'string') {
+ parts.push({ text: block.text });
+ }
+ break;
+
+ case 'tool_use':
+ // 转换为 Gemini functionCall 格式
+ if (block.name && block.input) {
+ const args = typeof block.input === 'string'
+ ? block.input
+ : JSON.stringify(block.input);
+
+ // 验证 args 是有效的 JSON 对象
+ try {
+ const parsedArgs = JSON.parse(args);
+ if (parsedArgs && typeof parsedArgs === 'object') {
+ parts.push({
+ thoughtSignature: ClaudeConverter.GEMINI_CLAUDE_THOUGHT_SIGNATURE,
+ functionCall: {
+ name: block.name,
+ args: parsedArgs
+ }
+ });
+ }
+ } catch (e) {
+ // 如果解析失败,尝试直接使用 input
+ if (block.input && typeof block.input === 'object') {
+ parts.push({
+ thoughtSignature: ClaudeConverter.GEMINI_CLAUDE_THOUGHT_SIGNATURE,
+ functionCall: {
+ name: block.name,
+ args: block.input
+ }
+ });
+ }
+ }
+ }
+ break;
+
+ case 'tool_result':
+ // 转换为 Gemini functionResponse 格式
+ const toolCallId = block.tool_use_id;
+ if (toolCallId) {
+ // 从 tool_use_id 中提取函数名
+ // 格式通常是 "funcName-uuid" 或直接是函数名
+ let funcName = toolCallId;
+ const toolCallIdParts = toolCallId.split('-');
+ if (toolCallIdParts.length > 1) {
+ // 移除最后一个部分(UUID),保留函数名
+ funcName = toolCallIdParts.slice(0, -1).join('-');
+ }
+
+ // 获取响应数据
+ let responseData = block.content;
+ if (typeof responseData !== 'string') {
+ responseData = JSON.stringify(responseData);
+ }
+
+ parts.push({
+ functionResponse: {
+ name: funcName,
+ response: {
+ result: responseData
+ }
+ }
+ });
+ }
+ break;
+
+ case 'image':
+ if (block.source && block.source.type === 'base64') {
+ parts.push({
+ inlineData: {
+ mimeType: block.source.media_type,
+ data: block.source.data
+ }
+ });
+ }
+ break;
+ }
});
- } else if (processedParts.length > 0) {
+
+ if (parts.length > 0) {
+ geminiRequest.contents.push({
+ role: geminiRole,
+ parts: parts
+ });
+ }
+ } else if (typeof content === 'string') {
+ // 字符串内容
geminiRequest.contents.push({
role: geminiRole,
- parts: processedParts
+ parts: [{ text: content }]
});
}
});
@@ -796,36 +907,74 @@ export class ClaudeConverter extends BaseConverter {
// 添加生成配置
const generationConfig = {};
- generationConfig.maxOutputTokens = checkAndAssignOrDefault(claudeRequest.max_tokens, GEMINI_DEFAULT_MAX_TOKENS);
- generationConfig.temperature = checkAndAssignOrDefault(claudeRequest.temperature, GEMINI_DEFAULT_TEMPERATURE);
- generationConfig.topP = checkAndAssignOrDefault(claudeRequest.top_p, GEMINI_DEFAULT_TOP_P);
+
+ if (claudeRequest.max_tokens !== undefined) {
+ generationConfig.maxOutputTokens = claudeRequest.max_tokens;
+ }
+ if (claudeRequest.temperature !== undefined) {
+ generationConfig.temperature = claudeRequest.temperature;
+ }
+ if (claudeRequest.top_p !== undefined) {
+ generationConfig.topP = claudeRequest.top_p;
+ }
+ if (claudeRequest.top_k !== undefined) {
+ generationConfig.topK = claudeRequest.top_k;
+ }
+
+ // 处理 thinking 配置 - 转换为 Gemini thinkingBudget
+ if (claudeRequest.thinking && claudeRequest.thinking.type === 'enabled') {
+ if (claudeRequest.thinking.budget_tokens !== undefined) {
+ const budget = claudeRequest.thinking.budget_tokens;
+ if (!generationConfig.thinkingConfig) {
+ generationConfig.thinkingConfig = {};
+ }
+ generationConfig.thinkingConfig.thinkingBudget = budget;
+ generationConfig.thinkingConfig.include_thoughts = true;
+ }
+ }
if (Object.keys(generationConfig).length > 0) {
geminiRequest.generationConfig = generationConfig;
}
- // 处理工具
- if (Array.isArray(claudeRequest.tools)) {
- geminiRequest.tools = [{
- functionDeclarations: claudeRequest.tools.map(tool => {
- if (!tool || typeof tool !== 'object' || !tool.name) {
- console.warn("Skipping invalid tool declaration in claudeRequest.tools.");
- return null;
- }
-
- delete tool.input_schema.$schema;
- return {
- name: String(tool.name),
- description: String(tool.description || ''),
- parameters: tool.input_schema && typeof tool.input_schema === 'object'
- ? tool.input_schema
- : { type: 'object', properties: {} }
- };
- }).filter(Boolean)
- }];
+ // 处理工具 - 使用 parametersJsonSchema 格式
+ if (Array.isArray(claudeRequest.tools) && claudeRequest.tools.length > 0) {
+ const functionDeclarations = [];
- if (geminiRequest.tools[0].functionDeclarations.length === 0) {
- delete geminiRequest.tools;
+ claudeRequest.tools.forEach(tool => {
+ if (!tool || typeof tool !== 'object' || !tool.name) {
+ console.warn("Skipping invalid tool declaration in claudeRequest.tools.");
+ return;
+ }
+
+ // 清理 input_schema
+ let inputSchema = tool.input_schema;
+ if (inputSchema && typeof inputSchema === 'object') {
+ // 创建副本以避免修改原始对象
+ inputSchema = JSON.parse(JSON.stringify(inputSchema));
+ // 清理不需要的字段
+ delete inputSchema.$schema;
+ // 清理 URL 格式(Gemini 不支持)
+ this.cleanUrlFormatFromSchema(inputSchema);
+ }
+
+ const funcDecl = {
+ name: String(tool.name),
+ description: String(tool.description || '')
+ };
+
+ // 使用 parametersJsonSchema 而不是 parameters
+ if (inputSchema) {
+ funcDecl.parametersJsonSchema = inputSchema;
+ }
+
+ functionDeclarations.push(funcDecl);
+ });
+
+ if (functionDeclarations.length > 0) {
+ geminiRequest.tools = [{
+ functionDeclarations: functionDeclarations
+ }];
}
}
@@ -834,9 +983,55 @@ export class ClaudeConverter extends BaseConverter {
geminiRequest.toolConfig = this.buildGeminiToolConfigFromClaude(claudeRequest.tool_choice);
}
+ // 添加默认安全设置
+ geminiRequest.safetySettings = this.getDefaultSafetySettings();
+
return geminiRequest;
}
+ /**
+ * 清理 JSON Schema 中的 URL 格式
+ * Gemini 不支持 "format": "uri"
+ */
+ cleanUrlFormatFromSchema(schema) {
+ if (!schema || typeof schema !== 'object') return;
+
+ // 如果是属性对象,检查并清理 format
+ if (schema.type === 'string' && schema.format === 'uri') {
+ delete schema.format;
+ }
+
+ // 递归处理 properties
+ if (schema.properties && typeof schema.properties === 'object') {
+ Object.values(schema.properties).forEach(prop => {
+ this.cleanUrlFormatFromSchema(prop);
+ });
+ }
+
+ // 递归处理 items(数组类型)
+ if (schema.items) {
+ this.cleanUrlFormatFromSchema(schema.items);
+ }
+
+ // 递归处理 additionalProperties
+ if (schema.additionalProperties && typeof schema.additionalProperties === 'object') {
+ this.cleanUrlFormatFromSchema(schema.additionalProperties);
+ }
+ }
+
+ /**
+ * 获取默认的 Gemini 安全设置
+ */
+ getDefaultSafetySettings() {
+ return [
+ { category: "HARM_CATEGORY_HARASSMENT", threshold: "OFF" },
+ { category: "HARM_CATEGORY_HATE_SPEECH", threshold: "OFF" },
+ { category: "HARM_CATEGORY_SEXUALLY_EXPLICIT", threshold: "OFF" },
+ { category: "HARM_CATEGORY_DANGEROUS_CONTENT", threshold: "OFF" },
+ { category: "HARM_CATEGORY_CIVIC_INTEGRITY", threshold: "OFF" }
+ ];
+ }
+
/**
* Claude响应 -> Gemini响应
*/
diff --git a/src/converters/strategies/OllamaConverter.js b/src/converters/strategies/OllamaConverter.js
index 3bd8ffd..733592e 100644
--- a/src/converters/strategies/OllamaConverter.js
+++ b/src/converters/strategies/OllamaConverter.js
@@ -228,11 +228,32 @@ export class OllamaConverter extends BaseConverter {
ollamaResponse.done_reason = response.stop_reason === 'end_turn' ? 'stop' : response.stop_reason;
}
}
+ // Handle Gemini format (candidates array)
+ else if (response.candidates && response.candidates.length > 0) {
+ const candidate = response.candidates[0];
+ let textContent = '';
+ if (candidate.content && candidate.content.parts) {
+ textContent = candidate.content.parts
+ .filter(part => part.text)
+ .map(part => part.text)
+ .join('');
+ }
+
+ ollamaResponse.message = {
+ role: candidate.content?.role || 'assistant',
+ content: textContent
+ };
+
+ if (candidate.finishReason) {
+ ollamaResponse.done_reason = candidate.finishReason.toLowerCase();
+ }
+ }
// Add usage statistics if available
- if (response.usage) {
- ollamaResponse.prompt_eval_count = response.usage.prompt_tokens || response.usage.input_tokens || 0;
- ollamaResponse.eval_count = response.usage.completion_tokens || response.usage.output_tokens || 0;
+ const usage = response.usage || response.usageMetadata;
+ if (usage) {
+ ollamaResponse.prompt_eval_count = usage.prompt_tokens || usage.input_tokens || usage.promptTokenCount || 0;
+ ollamaResponse.eval_count = usage.completion_tokens || usage.output_tokens || usage.candidatesTokenCount || 0;
ollamaResponse.total_duration = 0;
ollamaResponse.load_duration = 0;
ollamaResponse.prompt_eval_duration = 0;
@@ -275,11 +296,28 @@ export class OllamaConverter extends BaseConverter {
ollamaResponse.done_reason = response.stop_reason === 'end_turn' ? 'stop' : response.stop_reason;
}
}
+ // Handle Gemini format
+ else if (response.candidates && response.candidates.length > 0) {
+ const candidate = response.candidates[0];
+ let textContent = '';
+ if (candidate.content && candidate.content.parts) {
+ textContent = candidate.content.parts
+ .filter(part => part.text)
+ .map(part => part.text)
+ .join('');
+ }
+ ollamaResponse.response = textContent;
+
+ if (candidate.finishReason) {
+ ollamaResponse.done_reason = candidate.finishReason.toLowerCase();
+ }
+ }
// Add usage statistics
- if (response.usage) {
- ollamaResponse.prompt_eval_count = response.usage.prompt_tokens || response.usage.input_tokens || 0;
- ollamaResponse.eval_count = response.usage.completion_tokens || response.usage.output_tokens || 0;
+ const genUsage = response.usage || response.usageMetadata;
+ if (genUsage) {
+ ollamaResponse.prompt_eval_count = genUsage.prompt_tokens || genUsage.input_tokens || genUsage.promptTokenCount || 0;
+ ollamaResponse.eval_count = genUsage.completion_tokens || genUsage.output_tokens || genUsage.candidatesTokenCount || 0;
ollamaResponse.total_duration = 0;
ollamaResponse.load_duration = 0;
ollamaResponse.prompt_eval_duration = 0;
diff --git a/src/converters/strategies/OpenAIConverter.js b/src/converters/strategies/OpenAIConverter.js
index d970f22..913802b 100644
--- a/src/converters/strategies/OpenAIConverter.js
+++ b/src/converters/strategies/OpenAIConverter.js
@@ -547,124 +547,532 @@ export class OpenAIConverter extends BaseConverter {
// OpenAI -> Gemini 转换
// =========================================================================
+ // Gemini Openai thought signature constant
+ static GEMINI_OPENAI_THOUGHT_SIGNATURE = "skip_thought_signature_validator";
/**
* OpenAI请求 -> Gemini请求
*/
toGeminiRequest(openaiRequest) {
const messages = openaiRequest.messages || [];
- const { systemInstruction, nonSystemMessages } = extractSystemMessages(messages);
+ const model = openaiRequest.model || '';
+
+ // 构建 tool_call_id -> function_name 映射
+ const tcID2Name = {};
+ for (const message of messages) {
+ if (message.role === 'assistant' && message.tool_calls) {
+ for (const tc of message.tool_calls) {
+ if (tc.type === 'function' && tc.id && tc.function?.name) {
+ tcID2Name[tc.id] = tc.function.name;
+ }
+ }
+ }
+ }
+
+ // 构建 tool_call_id -> response 映射
+ const toolResponses = {};
+ for (const message of messages) {
+ if (message.role === 'tool' && message.tool_call_id) {
+ toolResponses[message.tool_call_id] = message.content;
+ }
+ }
const processedMessages = [];
- let lastMessage = null;
+ let systemInstruction = null;
- for (const message of nonSystemMessages) {
- const geminiRole = message.role === 'assistant' ? 'model' : message.role;
+ for (let i = 0; i < messages.length; i++) {
+ const message = messages[i];
+ const role = message.role;
+ const content = message.content;
- if (geminiRole === 'tool') {
- // Save previous model response with functionCall
- if (lastMessage) {
- processedMessages.push(lastMessage);
- lastMessage = null;
+ if (role === 'system') {
+ // system -> system_instruction
+ if (messages.length > 1) {
+ if (typeof content === 'string') {
+ systemInstruction = {
+ role: 'user',
+ parts: [{ text: content }]
+ };
+ } else if (Array.isArray(content)) {
+ const parts = content
+ .filter(item => item.type === 'text' && item.text)
+ .map(item => ({ text: item.text }));
+ if (parts.length > 0) {
+ systemInstruction = {
+ role: 'user',
+ parts: parts
+ };
+ }
+ } else if (typeof content === 'object' && content.type === 'text') {
+ systemInstruction = {
+ role: 'user',
+ parts: [{ text: content.text }]
+ };
+ }
+ } else {
+ // 只有一条 system 消息时,作为 user 消息处理
+ const node = { role: 'user', parts: [] };
+ if (typeof content === 'string') {
+ node.parts.push({ text: content });
+ } else if (Array.isArray(content)) {
+ for (const item of content) {
+ if (item.type === 'text' && item.text) {
+ node.parts.push({ text: item.text });
+ }
+ }
+ }
+ if (node.parts.length > 0) {
+ processedMessages.push(node);
+ }
}
-
- // Get function name from message.name or via tool_call_id
- let functionName = message.name;
- if (!functionName && message.tool_call_id) {
- const currentIndex = nonSystemMessages.indexOf(message);
- for (let i = currentIndex - 1; i >= 0; i--) {
- const prevMsg = nonSystemMessages[i];
- if (prevMsg.role === 'assistant' && prevMsg.tool_calls) {
- const toolCall = prevMsg.tool_calls.find(tc => tc.id === message.tool_call_id);
- if (toolCall?.function?.name) {
- functionName = toolCall.function.name;
+ } else if (role === 'user') {
+ // user -> user content
+ const node = { role: 'user', parts: [] };
+ if (typeof content === 'string') {
+ node.parts.push({ text: content });
+ } else if (Array.isArray(content)) {
+ for (const item of content) {
+ if (!item) continue;
+ switch (item.type) {
+ case 'text':
+ if (item.text) {
+ node.parts.push({ text: item.text });
+ }
break;
+ case 'image_url':
+ if (item.image_url) {
+ const imageUrl = typeof item.image_url === 'string'
+ ? item.image_url
+ : item.image_url.url;
+ if (imageUrl && imageUrl.startsWith('data:')) {
+ const commaIndex = imageUrl.indexOf(',');
+ if (commaIndex > 5) {
+ const header = imageUrl.substring(5, commaIndex);
+ const semicolonIndex = header.indexOf(';');
+ if (semicolonIndex > 0) {
+ const mimeType = header.substring(0, semicolonIndex);
+ const data = imageUrl.substring(commaIndex + 1);
+ node.parts.push({
+ inlineData: {
+ mimeType: mimeType,
+ data: data
+ },
+ thoughtSignature: OpenAIConverter.GEMINI_OPENAI_THOUGHT_SIGNATURE
+ });
+ }
+ }
+ } else if (imageUrl) {
+ node.parts.push({
+ fileData: {
+ mimeType: 'image/jpeg',
+ fileUri: imageUrl
+ }
+ });
+ }
+ }
+ break;
+ case 'file':
+ if (item.file) {
+ const filename = item.file.filename || '';
+ const fileData = item.file.file_data || '';
+ const ext = filename.includes('.')
+ ? filename.split('.').pop().toLowerCase()
+ : '';
+ const mimeTypes = {
+ 'pdf': 'application/pdf',
+ 'txt': 'text/plain',
+ 'html': 'text/html',
+ 'css': 'text/css',
+ 'js': 'application/javascript',
+ 'json': 'application/json',
+ 'xml': 'application/xml',
+ 'csv': 'text/csv',
+ 'md': 'text/markdown',
+ 'py': 'text/x-python',
+ 'java': 'text/x-java',
+ 'c': 'text/x-c',
+ 'cpp': 'text/x-c++',
+ 'h': 'text/x-c',
+ 'hpp': 'text/x-c++',
+ 'go': 'text/x-go',
+ 'rs': 'text/x-rust',
+ 'ts': 'text/typescript',
+ 'tsx': 'text/typescript',
+ 'jsx': 'text/javascript',
+ 'png': 'image/png',
+ 'jpg': 'image/jpeg',
+ 'jpeg': 'image/jpeg',
+ 'gif': 'image/gif',
+ 'webp': 'image/webp',
+ 'svg': 'image/svg+xml',
+ 'mp3': 'audio/mpeg',
+ 'wav': 'audio/wav',
+ 'mp4': 'video/mp4',
+ 'webm': 'video/webm'
+ };
+ const mimeType = mimeTypes[ext];
+ if (mimeType && fileData) {
+ node.parts.push({
+ inlineData: {
+ mimeType: mimeType,
+ data: fileData
+ }
+ });
+ }
+ }
+ break;
+ }
+ }
+ }
+ if (node.parts.length > 0) {
+ processedMessages.push(node);
+ }
+ } else if (role === 'assistant') {
+ // assistant -> model content
+ const node = { role: 'model', parts: [] };
+
+ // 处理文本内容
+ if (typeof content === 'string' && content) {
+ node.parts.push({ text: content });
+ } else if (Array.isArray(content)) {
+ for (const item of content) {
+ if (!item) continue;
+ if (item.type === 'text' && item.text) {
+ node.parts.push({ text: item.text });
+ } else if (item.type === 'image_url' && item.image_url) {
+ const imageUrl = typeof item.image_url === 'string'
+ ? item.image_url
+ : item.image_url.url;
+ if (imageUrl && imageUrl.startsWith('data:')) {
+ const commaIndex = imageUrl.indexOf(',');
+ if (commaIndex > 5) {
+ const header = imageUrl.substring(5, commaIndex);
+ const semicolonIndex = header.indexOf(';');
+ if (semicolonIndex > 0) {
+ const mimeType = header.substring(0, semicolonIndex);
+ const data = imageUrl.substring(commaIndex + 1);
+ node.parts.push({
+ inlineData: {
+ mimeType: mimeType,
+ data: data
+ },
+ thoughtSignature: OpenAIConverter.GEMINI_OPENAI_THOUGHT_SIGNATURE
+ });
+ }
+ }
}
}
}
}
- // Build functionResponse according to Gemini API spec
- const parsedContent = safeParseJSON(message.content);
- const contentStr = typeof parsedContent === 'string' ? parsedContent : JSON.stringify(parsedContent);
-
- processedMessages.push({
- role: 'user',
- parts: [{
- functionResponse: {
- name: functionName || 'unknown',
- response: {
- name: functionName || 'unknown',
- content: contentStr
- }
+ // 处理 tool_calls -> functionCall
+ if (message.tool_calls && Array.isArray(message.tool_calls)) {
+ const functionCallIds = [];
+ for (const tc of message.tool_calls) {
+ if (tc.type !== 'function') continue;
+ const fid = tc.id || '';
+ const fname = tc.function?.name || '';
+ const fargs = tc.function?.arguments || '{}';
+
+ let argsObj;
+ try {
+ argsObj = typeof fargs === 'string' ? JSON.parse(fargs) : fargs;
+ } catch (e) {
+ argsObj = {};
}
- }]
- });
- lastMessage = null;
- continue;
- }
-
- let processedContent = this.processOpenAIContentToGeminiParts(message.content);
-
- // Add tool_calls as functionCall to parts
- if (message.tool_calls && Array.isArray(message.tool_calls)) {
- for (const toolCall of message.tool_calls) {
- if (toolCall.function) {
- processedContent.push({
+
+ node.parts.push({
functionCall: {
- name: toolCall.function.name,
- args: safeParseJSON(toolCall.function.arguments)
- }
+ name: fname,
+ args: argsObj
+ },
+ thoughtSignature: OpenAIConverter.GEMINI_OPENAI_THOUGHT_SIGNATURE
});
+
+ if (fid) {
+ functionCallIds.push(fid);
+ }
+ }
+
+ // 添加 model 消息
+ if (node.parts.length > 0) {
+ processedMessages.push(node);
+ }
+
+ // 添加对应的 functionResponse(作为 user 消息)
+ if (functionCallIds.length > 0) {
+ const toolNode = { role: 'user', parts: [] };
+ for (const fid of functionCallIds) {
+ const name = tcID2Name[fid];
+ if (name) {
+ let resp = toolResponses[fid] || '{}';
+ // 确保 resp 是字符串
+ if (typeof resp !== 'string') {
+ resp = JSON.stringify(resp);
+ }
+ toolNode.parts.push({
+ functionResponse: {
+ name: name,
+ response: {
+ result: resp
+ }
+ }
+ });
+ }
+ }
+ if (toolNode.parts.length > 0) {
+ processedMessages.push(toolNode);
+ }
+ }
+ } else {
+ // 没有 tool_calls,直接添加
+ if (node.parts.length > 0) {
+ processedMessages.push(node);
}
}
}
-
- if (lastMessage && lastMessage.role === geminiRole && !message.tool_calls &&
- Array.isArray(processedContent) && processedContent.every(p => p.text) &&
- Array.isArray(lastMessage.parts) && lastMessage.parts.every(p => p.text)) {
- lastMessage.parts.push(...processedContent);
- continue;
- }
-
- if (lastMessage) processedMessages.push(lastMessage);
- lastMessage = { role: geminiRole, parts: processedContent };
+ // tool 消息已经在 assistant 的 tool_calls 处理中合并了,这里跳过
}
- if (lastMessage) processedMessages.push(lastMessage);
+ // 构建 Gemini 请求
const geminiRequest = {
contents: processedMessages.filter(item => item.parts && item.parts.length > 0)
};
- if (systemInstruction) geminiRequest.systemInstruction = systemInstruction;
+ // 添加 model
+ if (model) {
+ geminiRequest.model = model;
+ }
- if (openaiRequest.tools?.length) {
- geminiRequest.tools = [{
- functionDeclarations: openaiRequest.tools.map(t => {
- if (!t || typeof t !== 'object' || !t.function) return null;
- const func = t.function;
- const parameters = cleanJsonSchema(func.parameters || {});
- return {
- name: String(func.name || ''),
- description: String(func.description || ''),
- parameters: parameters
- };
- }).filter(Boolean)
- }];
- if (geminiRequest.tools[0].functionDeclarations.length === 0) {
- delete geminiRequest.tools;
+ // 添加 system_instruction
+ if (systemInstruction) {
+ geminiRequest.system_instruction = systemInstruction;
+ }
+
+ // 处理 reasoning_effort -> thinkingConfig
+ if (openaiRequest.reasoning_effort) {
+ const effort = String(openaiRequest.reasoning_effort).toLowerCase().trim();
+ if (this.modelSupportsThinking(model)) {
+ if (this.isGemini3Model(model)) {
+ // Gemini 3 模型使用 thinkingLevel
+ if (effort === 'none') {
+ // 不添加 thinkingConfig
+ } else if (effort === 'auto') {
+ geminiRequest.generationConfig = geminiRequest.generationConfig || {};
+ geminiRequest.generationConfig.thinkingConfig = {
+ includeThoughts: true
+ };
+ } else {
+ const level = this.validateGemini3ThinkingLevel(model, effort);
+ if (level) {
+ geminiRequest.generationConfig = geminiRequest.generationConfig || {};
+ geminiRequest.generationConfig.thinkingConfig = {
+ thinkingLevel: level
+ };
+ }
+ }
+ } else if (!this.modelUsesThinkingLevels(model)) {
+ // 使用 thinkingBudget 的模型
+ geminiRequest.generationConfig = geminiRequest.generationConfig || {};
+ geminiRequest.generationConfig.thinkingConfig = this.applyReasoningEffortToGemini(effort);
+ }
}
}
+ // 处理 extra_body.google.thinking_config(Cherry Studio 扩展)
+ if (!openaiRequest.reasoning_effort && openaiRequest.extra_body?.google?.thinking_config) {
+ const tc = openaiRequest.extra_body.google.thinking_config;
+ if (this.modelSupportsThinking(model) && !this.modelUsesThinkingLevels(model)) {
+ geminiRequest.generationConfig = geminiRequest.generationConfig || {};
+ geminiRequest.generationConfig.thinkingConfig = geminiRequest.generationConfig.thinkingConfig || {};
+
+ let setBudget = false;
+ let budget = 0;
+
+ if (tc.thinkingBudget !== undefined) {
+ budget = parseInt(tc.thinkingBudget, 10);
+ geminiRequest.generationConfig.thinkingConfig.thinkingBudget = budget;
+ setBudget = true;
+ } else if (tc.thinking_budget !== undefined) {
+ budget = parseInt(tc.thinking_budget, 10);
+ geminiRequest.generationConfig.thinkingConfig.thinkingBudget = budget;
+ setBudget = true;
+ }
+
+ if (tc.includeThoughts !== undefined) {
+ geminiRequest.generationConfig.thinkingConfig.includeThoughts = tc.includeThoughts;
+ } else if (tc.include_thoughts !== undefined) {
+ geminiRequest.generationConfig.thinkingConfig.includeThoughts = tc.include_thoughts;
+ } else if (setBudget && budget !== 0) {
+ geminiRequest.generationConfig.thinkingConfig.includeThoughts = true;
+ }
+ }
+ }
+
+ // 处理 modalities -> responseModalities
+ if (openaiRequest.modalities && Array.isArray(openaiRequest.modalities)) {
+ const responseMods = [];
+ for (const m of openaiRequest.modalities) {
+ const mod = String(m).toLowerCase();
+ if (mod === 'text') {
+ responseMods.push('TEXT');
+ } else if (mod === 'image') {
+ responseMods.push('IMAGE');
+ }
+ }
+ if (responseMods.length > 0) {
+ geminiRequest.generationConfig = geminiRequest.generationConfig || {};
+ geminiRequest.generationConfig.responseModalities = responseMods;
+ }
+ }
+
+ // 处理 image_config(OpenRouter 风格)
+ if (openaiRequest.image_config) {
+ const imgCfg = openaiRequest.image_config;
+ if (imgCfg.aspect_ratio) {
+ geminiRequest.generationConfig = geminiRequest.generationConfig || {};
+ geminiRequest.generationConfig.imageConfig = geminiRequest.generationConfig.imageConfig || {};
+ geminiRequest.generationConfig.imageConfig.aspectRatio = imgCfg.aspect_ratio;
+ }
+ if (imgCfg.image_size) {
+ geminiRequest.generationConfig = geminiRequest.generationConfig || {};
+ geminiRequest.generationConfig.imageConfig = geminiRequest.generationConfig.imageConfig || {};
+ geminiRequest.generationConfig.imageConfig.imageSize = imgCfg.image_size;
+ }
+ }
+
+ // 处理 tools -> functionDeclarations
+ if (openaiRequest.tools?.length) {
+ const functionDeclarations = [];
+ let hasGoogleSearch = false;
+
+ for (const t of openaiRequest.tools) {
+ if (!t || typeof t !== 'object') continue;
+
+ if (t.type === 'function' && t.function) {
+ const func = t.function;
+ let fnDecl = {
+ name: String(func.name || ''),
+ description: String(func.description || '')
+ };
+
+ // 处理 parameters -> parametersJsonSchema
+ if (func.parameters) {
+ fnDecl.parametersJsonSchema = cleanJsonSchema(func.parameters);
+ } else {
+ fnDecl.parametersJsonSchema = {
+ type: 'object',
+ properties: {}
+ };
+ }
+
+ functionDeclarations.push(fnDecl);
+ }
+
+ // 处理 google_search 工具
+ if (t.google_search) {
+ hasGoogleSearch = true;
+ }
+ }
+
+ if (functionDeclarations.length > 0 || hasGoogleSearch) {
+ geminiRequest.tools = [{}];
+ if (functionDeclarations.length > 0) {
+ geminiRequest.tools[0].functionDeclarations = functionDeclarations;
+ }
+ if (hasGoogleSearch) {
+ const googleSearchTool = openaiRequest.tools.find(t => t.google_search);
+ geminiRequest.tools[0].googleSearch = googleSearchTool.google_search;
+ }
+ }
+ }
+
+ // 处理 tool_choice
if (openaiRequest.tool_choice) {
geminiRequest.toolConfig = this.buildGeminiToolConfig(openaiRequest.tool_choice);
}
- const config = this.buildGeminiGenerationConfig(openaiRequest, openaiRequest.model);
- if (Object.keys(config).length) geminiRequest.generationConfig = config;
+ // 构建 generationConfig
+ const config = this.buildGeminiGenerationConfig(openaiRequest, model);
+ if (Object.keys(config).length) {
+ geminiRequest.generationConfig = {
+ ...config,
+ ...(geminiRequest.generationConfig || {})
+ };
+ }
+
+ // 添加默认安全设置
+ geminiRequest.safetySettings = this.getDefaultSafetySettings();
return geminiRequest;
}
+ /**
+ * 检查模型是否支持 thinking
+ */
+ modelSupportsThinking(model) {
+ if (!model) return false;
+ const m = model.toLowerCase();
+ return m.includes('2.5') || m.includes('thinking') || m.includes('2.0-flash-thinking');
+ }
+
+ /**
+ * 检查是否是 Gemini 3 模型
+ */
+ isGemini3Model(model) {
+ if (!model) return false;
+ const m = model.toLowerCase();
+ return m.includes('gemini-3') || m.includes('gemini3');
+ }
+
+ /**
+ * 检查模型是否使用 thinking levels(而不是 budget)
+ */
+ modelUsesThinkingLevels(model) {
+ if (!model) return false;
+ // Gemini 3 模型使用 levels,其他使用 budget
+ return this.isGemini3Model(model);
+ }
+
+ /**
+ * 验证 Gemini 3 thinking level
+ */
+ validateGemini3ThinkingLevel(model, effort) {
+ const validLevels = ['low', 'medium', 'high'];
+ if (validLevels.includes(effort)) {
+ return effort.toUpperCase();
+ }
+ return null;
+ }
+
+ /**
+ * 将 reasoning_effort 转换为 Gemini thinkingConfig
+ */
+ applyReasoningEffortToGemini(effort) {
+ const effortToBudget = {
+ 'low': 1024,
+ 'medium': 8192,
+ 'high': 24576
+ };
+ const budget = effortToBudget[effort] || effortToBudget['medium'];
+ return {
+ thinkingBudget: budget,
+ includeThoughts: true
+ };
+ }
+
+ /**
+ * 获取默认安全设置
+ */
+ getDefaultSafetySettings() {
+ return [
+ { category: "HARM_CATEGORY_HARASSMENT", threshold: "OFF" },
+ { category: "HARM_CATEGORY_HATE_SPEECH", threshold: "OFF" },
+ { category: "HARM_CATEGORY_SEXUALLY_EXPLICIT", threshold: "OFF" },
+ { category: "HARM_CATEGORY_DANGEROUS_CONTENT", threshold: "OFF" },
+ { category: "HARM_CATEGORY_CIVIC_INTEGRITY", threshold: "OFF" }
+ ];
+ }
+
/**
* 处理OpenAI内容到Gemini parts
*/
diff --git a/src/converters/utils.js b/src/converters/utils.js
index 7797a5e..55c5933 100644
--- a/src/converters/utils.js
+++ b/src/converters/utils.js
@@ -229,6 +229,12 @@ export function extractAndProcessSystemMessages(messages) {
/**
* 清理JSON Schema属性(移除Gemini不支持的属性)
+ * Google Gemini API 只支持有限的 JSON Schema 属性,不支持以下属性:
+ * - exclusiveMinimum, exclusiveMaximum, minimum, maximum
+ * - minLength, maxLength, minItems, maxItems
+ * - pattern, format, default, const
+ * - additionalProperties, $schema, $ref, $id
+ * - allOf, anyOf, oneOf, not
* @param {Object} schema - JSON Schema
* @returns {Object} 清理后的JSON Schema
*/
@@ -237,23 +243,39 @@ export function cleanJsonSchemaProperties(schema) {
return schema;
}
+ // 如果是数组,递归处理每个元素
+ if (Array.isArray(schema)) {
+ return schema.map(item => cleanJsonSchemaProperties(item));
+ }
+
+ // Gemini 支持的 JSON Schema 属性白名单
+ const allowedKeys = [
+ "type",
+ "description",
+ "properties",
+ "required",
+ "enum",
+ "items",
+ "nullable"
+ ];
+
const sanitized = {};
for (const [key, value] of Object.entries(schema)) {
- if (["type", "description", "properties", "required", "enum", "items"].includes(key)) {
- sanitized[key] = value;
+ if (allowedKeys.includes(key)) {
+ // 对于需要递归处理的属性
+ if (key === 'properties' && typeof value === 'object' && value !== null) {
+ const cleanProperties = {};
+ for (const [propName, propSchema] of Object.entries(value)) {
+ cleanProperties[propName] = cleanJsonSchemaProperties(propSchema);
+ }
+ sanitized[key] = cleanProperties;
+ } else if (key === 'items') {
+ sanitized[key] = cleanJsonSchemaProperties(value);
+ } else {
+ sanitized[key] = value;
+ }
}
- }
-
- if (sanitized.properties && typeof sanitized.properties === 'object') {
- const cleanProperties = {};
- for (const [propName, propSchema] of Object.entries(sanitized.properties)) {
- cleanProperties[propName] = cleanJsonSchemaProperties(propSchema);
- }
- sanitized.properties = cleanProperties;
- }
-
- if (sanitized.items) {
- sanitized.items = cleanJsonSchemaProperties(sanitized.items);
+ // 其他属性(如 exclusiveMinimum, minimum, maximum, pattern 等)被忽略
}
return sanitized;
diff --git a/src/gemini/antigravity-core.js b/src/gemini/antigravity-core.js
index a039f7b..27d43a3 100644
--- a/src/gemini/antigravity-core.js
+++ b/src/gemini/antigravity-core.js
@@ -1,6 +1,8 @@
+
import { OAuth2Client } from 'google-auth-library';
import * as http from 'http';
import * as https from 'https';
+import * as crypto from 'crypto';
import { promises as fs } from 'fs';
import * as path from 'path';
import * as os from 'os';
@@ -11,6 +13,7 @@ import { formatExpiryTime } from '../common.js';
import { getProviderModels } from '../provider-models.js';
import { handleGeminiAntigravityOAuth } from '../oauth-handlers.js';
import { getProxyConfigForProvider, getGoogleAuthProxyConfig } from '../proxy-utils.js';
+import { cleanJsonSchemaProperties } from '../converters/utils.js';
// 配置 HTTP/HTTPS agent 限制连接池大小,避免资源泄漏
const httpAgent = new http.Agent({
@@ -29,35 +32,44 @@ const httpsAgent = new https.Agent({
// --- Constants ---
const CREDENTIALS_DIR = '.antigravity';
const CREDENTIALS_FILE = 'oauth_creds.json';
-const DEFAULT_ANTIGRAVITY_BASE_URL_DAILY = 'https://daily-cloudcode-pa.sandbox.googleapis.com';
-const DEFAULT_ANTIGRAVITY_BASE_URL_AUTOPUSH = 'https://autopush-cloudcode-pa.sandbox.googleapis.com';
+
+// Base URLs - 按照 Go 代码的降级顺序
+const ANTIGRAVITY_BASE_URL_DAILY = 'https://daily-cloudcode-pa.googleapis.com';
+const ANTIGRAVITY_SANDBOX_BASE_URL_DAILY = 'https://daily-cloudcode-pa.sandbox.googleapis.com';
+const ANTIGRAVITY_BASE_URL_PROD = 'https://autopush-cloudcode-pa.sandbox.googleapis.com';
+
const ANTIGRAVITY_API_VERSION = 'v1internal';
const OAUTH_CLIENT_ID = '1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com';
const OAUTH_CLIENT_SECRET = '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';
-const DEFAULT_USER_AGENT = 'antigravity/1.11.5 windows/amd64';
+const DEFAULT_USER_AGENT = 'antigravity/1.104.0 darwin/arm64';
const REFRESH_SKEW = 3000; // 3000秒(50分钟)提前刷新Token
+// Thinking 配置相关常量
+const DEFAULT_THINKING_MIN = 1024;
+const DEFAULT_THINKING_MAX = 100000;
+
// 获取 Antigravity 模型列表
const ANTIGRAVITY_MODELS = getProviderModels('gemini-antigravity');
-// 模型别名映射
+// 模型别名映射 - 别名 -> 真实模型名
const MODEL_ALIAS_MAP = {
'gemini-2.5-computer-use-preview-10-2025': 'rev19-uic3-1p',
'gemini-3-pro-image-preview': 'gemini-3-pro-image',
'gemini-3-pro-preview': 'gemini-3-pro-high',
'gemini-3-flash-preview': 'gemini-3-flash',
- 'gemini-2.5-flash': 'gemini-2.5-flash',
+ 'gemini-2.5-flash-preview': 'gemini-2.5-flash',
'gemini-claude-sonnet-4-5': 'claude-sonnet-4-5',
'gemini-claude-sonnet-4-5-thinking': 'claude-sonnet-4-5-thinking',
'gemini-claude-opus-4-5-thinking': 'claude-opus-4-5-thinking'
};
+// 真实模型名 -> 别名
const MODEL_NAME_MAP = {
'rev19-uic3-1p': 'gemini-2.5-computer-use-preview-10-2025',
'gemini-3-pro-image': 'gemini-3-pro-image-preview',
'gemini-3-pro-high': 'gemini-3-pro-preview',
'gemini-3-flash': 'gemini-3-flash-preview',
- 'gemini-2.5-flash': 'gemini-2.5-flash',
+ 'gemini-2.5-flash': 'gemini-2.5-flash-preview',
'claude-sonnet-4-5': 'gemini-claude-sonnet-4-5',
'claude-sonnet-4-5-thinking': 'gemini-claude-sonnet-4-5-thinking',
'claude-opus-4-5-thinking': 'gemini-claude-opus-4-5-thinking'
@@ -65,6 +77,8 @@ const MODEL_NAME_MAP = {
/**
* 将别名转换为真实模型名
+ * @param {string} modelName - 模型别名
+ * @returns {string} 真实模型名
*/
function alias2ModelName(modelName) {
return MODEL_ALIAS_MAP[modelName];
@@ -72,13 +86,48 @@ function alias2ModelName(modelName) {
/**
* 将真实模型名转换为别名
+ * @param {string} modelName - 真实模型名
+ * @returns {string|null} 模型别名,如果不支持则返回 null
*/
function modelName2Alias(modelName) {
return MODEL_NAME_MAP[modelName];
}
+/**
+ * 检查模型是否为 Claude 模型
+ * @param {string} modelName - 模型名称
+ * @returns {boolean}
+ */
+function isClaude(modelName) {
+ return modelName && modelName.toLowerCase().includes('claude');
+}
+
+/**
+ * 检查是否为图像模型
+ * @param {string} modelName - 模型名称
+ * @returns {boolean}
+ */
+function isImageModel(modelName) {
+ return modelName && modelName.toLowerCase().includes('image');
+}
+
+/**
+ * 检查模型是否支持 Thinking
+ * @param {string} modelName - 模型名称
+ * @returns {boolean}
+ */
+function modelSupportsThinking(modelName) {
+ if (!modelName) return false;
+ const name = modelName.toLowerCase();
+ // 支持 thinking 的模型:gemini-3-*, gemini-2.5-*, claude-*-thinking
+ return name.startsWith('gemini-3-') ||
+ name.startsWith('gemini-2.5-') ||
+ name.includes('-thinking');
+}
+
/**
* 生成随机请求ID
+ * @returns {string}
*/
function generateRequestID() {
return 'agent-' + uuidv4();
@@ -86,14 +135,44 @@ function generateRequestID() {
/**
* 生成随机会话ID
+ * @returns {string}
*/
function generateSessionID() {
- const n = Math.floor(Math.random() * 9000000000000000000);
+ const n = Math.floor(Math.random() * 9000);
return '-' + n.toString();
}
+/**
+ * 基于请求内容生成稳定的会话ID
+ * 使用第一个用户消息的 SHA256 哈希值
+ * @param {Object} payload - 请求体
+ * @returns {string} 稳定的会话ID
+ */
+function generateStableSessionID(payload) {
+ try {
+ const contents = payload?.request?.contents;
+ if (Array.isArray(contents)) {
+ for (const content of contents) {
+ if (content.role === 'user') {
+ const text = content.parts?.[0]?.text;
+ if (text) {
+ const hash = crypto.createHash('sha256').update(text).digest();
+ // 取前8字节转换为 BigInt,然后取正数
+ const n = hash.readBigUInt64BE(0) & BigInt('0x7FFFFFFFFFFFFFFF');
+ return '-' + n.toString();
+ }
+ }
+ }
+ }
+ } catch (e) {
+ // 如果解析失败,回退到随机会话ID
+ }
+ return generateSessionID();
+}
+
/**
* 生成随机项目ID
+ * @returns {string}
*/
function generateProjectID() {
const adjectives = ['useful', 'bright', 'swift', 'calm', 'bold'];
@@ -104,12 +183,83 @@ function generateProjectID() {
return `${adj}-${noun}-${randomPart}`;
}
+/**
+ * 规范化 Thinking Budget
+ * @param {string} modelName - 模型名称
+ * @param {number} budget - 原始 budget 值
+ * @returns {number} 规范化后的 budget
+ */
+function normalizeThinkingBudget(modelName, budget) {
+ // -1 表示动态/无限制
+ if (budget === -1) return -1;
+
+ // 获取模型的 thinking 限制
+ const min = DEFAULT_THINKING_MIN;
+ const max = DEFAULT_THINKING_MAX;
+
+ // 限制在有效范围内
+ if (budget < min) return min;
+ if (budget > max) return max;
+ return budget;
+}
+
+/**
+ * 规范化 Antigravity Thinking 配置
+ * 对于 Claude 模型,确保 thinking budget < max_tokens
+ * @param {string} modelName - 模型名称
+ * @param {Object} payload - 请求体
+ * @param {boolean} isClaudeModel - 是否为 Claude 模型
+ * @returns {Object} 处理后的请求体
+ */
+function normalizeAntigravityThinking(modelName, payload, isClaudeModel) {
+ // 如果模型不支持 thinking,移除 thinking 配置
+ if (!modelSupportsThinking(modelName)) {
+ if (payload?.request?.generationConfig?.thinkingConfig) {
+ delete payload.request.generationConfig.thinkingConfig;
+ }
+ return payload;
+ }
+
+ const thinkingConfig = payload?.request?.generationConfig?.thinkingConfig;
+ if (!thinkingConfig) return payload;
+
+ const budget = thinkingConfig.thinkingBudget;
+ if (budget === undefined) return payload;
+
+ let normalizedBudget = normalizeThinkingBudget(modelName, budget);
+
+ // 对于 Claude 模型,确保 thinking budget < max_tokens
+ if (isClaudeModel) {
+ const maxTokens = payload?.request?.generationConfig?.maxOutputTokens;
+ if (maxTokens && maxTokens > 0 && normalizedBudget >= maxTokens) {
+ normalizedBudget = maxTokens - 1;
+ }
+
+ // 检查最小 budget
+ const minBudget = DEFAULT_THINKING_MIN;
+ if (normalizedBudget >= 0 && normalizedBudget < minBudget) {
+ // Budget 低于最小值,移除 thinking 配置
+ delete payload.request.generationConfig.thinkingConfig;
+ return payload;
+ }
+ }
+
+ payload.request.generationConfig.thinkingConfig.thinkingBudget = normalizedBudget;
+ return payload;
+}
+
/**
* 将 Gemini 格式请求转换为 Antigravity 格式
+ * @param {string} modelName - 模型名称
+ * @param {Object} payload - 请求体
+ * @param {string} projectId - 项目ID
+ * @returns {Object} 转换后的请求体
*/
function geminiToAntigravity(modelName, payload, projectId) {
// 深拷贝请求体,避免修改原始对象
let template = JSON.parse(JSON.stringify(payload));
+
+ const isClaudeModel = isClaude(modelName);
// 设置基本字段
template.model = modelName;
@@ -122,8 +272,8 @@ function geminiToAntigravity(modelName, payload, projectId) {
template.request = {};
}
- // 设置会话ID
- template.request.sessionId = generateSessionID();
+ // 设置会话ID - 使用稳定的会话ID
+ template.request.sessionId = generateStableSessionID(template);
// 删除安全设置
if (template.request.safetySettings) {
@@ -131,19 +281,24 @@ function geminiToAntigravity(modelName, payload, projectId) {
}
// 设置工具配置
- if (template.request.toolConfig) {
- if (!template.request.toolConfig.functionCallingConfig) {
- template.request.toolConfig.functionCallingConfig = {};
- }
- template.request.toolConfig.functionCallingConfig.mode = 'VALIDATED';
+ if (!template.request.toolConfig) {
+ template.request.toolConfig = {};
}
+ if (!template.request.toolConfig.functionCallingConfig) {
+ template.request.toolConfig.functionCallingConfig = {};
+ }
+ template.request.toolConfig.functionCallingConfig.mode = 'VALIDATED';
- // 删除 maxOutputTokens
- if (template.request.generationConfig && template.request.generationConfig.maxOutputTokens) {
- delete template.request.generationConfig.maxOutputTokens;
+ // 对于非 Claude 模型,删除 maxOutputTokens
+ // Claude 模型需要保留 maxOutputTokens
+ if (!isClaudeModel) {
+ if (template.request.generationConfig && template.request.generationConfig.maxOutputTokens) {
+ delete template.request.generationConfig.maxOutputTokens;
+ }
}
// 处理 Thinking 配置
+ // 对于非 gemini-3-* 模型,将 thinkingLevel 转换为 thinkingBudget
if (!modelName.startsWith('gemini-3-')) {
if (template.request.generationConfig &&
template.request.generationConfig.thinkingConfig &&
@@ -153,28 +308,281 @@ function geminiToAntigravity(modelName, payload, projectId) {
}
}
- // 处理 Claude 模型的工具声明 (包括 sonnet 和 opus)
- if (modelName.startsWith('claude-sonnet-') || modelName.startsWith('claude-opus-')) {
- if (template.request.tools && Array.isArray(template.request.tools)) {
- template.request.tools.forEach(tool => {
- if (tool.functionDeclarations && Array.isArray(tool.functionDeclarations)) {
- tool.functionDeclarations.forEach(funcDecl => {
- if (funcDecl.parametersJsonSchema) {
- funcDecl.parameters = funcDecl.parametersJsonSchema;
- delete funcDecl.parameters.$schema;
- delete funcDecl.parametersJsonSchema;
- }
- });
- }
- });
- }
+ // 清理所有工具声明中的 JSON Schema 属性(移除 Google API 不支持的属性如 exclusiveMinimum 等)
+ if (template.request.tools && Array.isArray(template.request.tools)) {
+ template.request.tools.forEach((tool) => {
+ if (tool.functionDeclarations && Array.isArray(tool.functionDeclarations)) {
+ tool.functionDeclarations.forEach((funcDecl) => {
+ // 对于 Claude 模型,处理 parametersJsonSchema
+ if (isClaudeModel && funcDecl.parametersJsonSchema) {
+ funcDecl.parameters = cleanJsonSchemaProperties(funcDecl.parametersJsonSchema);
+ delete funcDecl.parameters.$schema;
+ delete funcDecl.parametersJsonSchema;
+ } else if (funcDecl.parameters) {
+ funcDecl.parameters = cleanJsonSchemaProperties(funcDecl.parameters);
+ }
+ });
+ }
+ });
}
+ // 如果是图像模型,增加参数 "generationConfig.imageConfig.imageSize": "4K"
+ if (isImageModel(modelName)) {
+ if (!template.request.generationConfig) {
+ template.request.generationConfig = {};
+ }
+
+ if (!template.request.generationConfig.imageConfig) {
+ template.request.generationConfig.imageConfig = {};
+ }
+ template.request.generationConfig.imageConfig.imageSize = '4K';
+ if (!template.request.generationConfig.thinkingConfig) {
+ template.request.generationConfig.thinkingConfig = {};
+ }
+ template.request.generationConfig.thinkingConfig.includeThoughts = false;
+ }
+
+ // 规范化 Thinking 配置
+ template = normalizeAntigravityThinking(modelName, template, isClaudeModel);
+
return template;
}
+/**
+ * 过滤 SSE 中的 usageMetadata(仅在最终块中保留)
+ * @param {string} line - SSE 行数据
+ * @returns {string} 过滤后的行数据
+ */
+function filterSSEUsageMetadata(line) {
+ if (!line || typeof line !== 'string') return line;
+
+ // 检查是否是 data: 开头的 SSE 数据
+ if (!line.startsWith('data: ')) return line;
+
+ try {
+ const jsonStr = line.slice(6); // 移除 'data: ' 前缀
+ const data = JSON.parse(jsonStr);
+
+ // 检查是否有 finishReason,如果没有则移除 usageMetadata
+ const hasFinishReason = data?.response?.candidates?.[0]?.finishReason ||
+ data?.candidates?.[0]?.finishReason;
+
+ if (!hasFinishReason) {
+ // 移除 usageMetadata
+ if (data.response) {
+ delete data.response.usageMetadata;
+ }
+ if (data.usageMetadata) {
+ delete data.usageMetadata;
+ }
+ return 'data: ' + JSON.stringify(data);
+ }
+ } catch (e) {
+ // 解析失败,返回原始数据
+ }
+
+ return line;
+}
+
+/**
+ * 将流式响应转换为非流式响应
+ * 用于 Claude 模型的非流式请求(实际上是流式请求然后合并)
+ * @param {Buffer|string} stream - 流式响应数据
+ * @returns {Object} 合并后的非流式响应
+ */
+function convertStreamToNonStream(stream) {
+ const lines = stream.toString().split('\n');
+
+ let responseTemplate = '';
+ let traceId = '';
+ let finishReason = '';
+ let modelVersion = '';
+ let responseId = '';
+ let role = '';
+ let usageRaw = null;
+ const parts = [];
+
+ // 用于合并连续的 text 和 thought 部分
+ let pendingKind = '';
+ let pendingText = '';
+ let pendingThoughtSig = '';
+
+ const flushPending = () => {
+ if (!pendingKind) return;
+
+ const text = pendingText;
+ if (pendingKind === 'text') {
+ if (text.trim()) {
+ parts.push({ text: text });
+ }
+ } else if (pendingKind === 'thought') {
+ if (text.trim() || pendingThoughtSig) {
+ const part = { thought: true, text: text };
+ if (pendingThoughtSig) {
+ part.thoughtSignature = pendingThoughtSig;
+ }
+ parts.push(part);
+ }
+ }
+
+ pendingKind = '';
+ pendingText = '';
+ pendingThoughtSig = '';
+ };
+
+ const normalizePart = (part) => {
+ const m = { ...part };
+ // 处理 thoughtSignature / thought_signature
+ const sig = part.thoughtSignature || part.thought_signature;
+ if (sig) {
+ m.thoughtSignature = sig;
+ delete m.thought_signature;
+ }
+ // 处理 inline_data -> inlineData
+ if (m.inline_data) {
+ m.inlineData = m.inline_data;
+ delete m.inline_data;
+ }
+ return m;
+ };
+
+ for (const line of lines) {
+ const trimmed = line.trim();
+ if (!trimmed) continue;
+
+ let data;
+ try {
+ data = JSON.parse(trimmed);
+ } catch (e) {
+ continue;
+ }
+
+ let responseNode = data.response;
+ if (!responseNode) {
+ if (data.candidates) {
+ responseNode = data;
+ } else {
+ continue;
+ }
+ }
+ responseTemplate = JSON.stringify(responseNode);
+
+ if (data.traceId) {
+ traceId = data.traceId;
+ }
+
+ if (responseNode.candidates?.[0]?.content?.role) {
+ role = responseNode.candidates[0].content.role;
+ }
+
+ if (responseNode.candidates?.[0]?.finishReason) {
+ finishReason = responseNode.candidates[0].finishReason;
+ }
+
+ if (responseNode.modelVersion) {
+ modelVersion = responseNode.modelVersion;
+ }
+
+ if (responseNode.responseId) {
+ responseId = responseNode.responseId;
+ }
+
+ if (responseNode.usageMetadata) {
+ usageRaw = responseNode.usageMetadata;
+ } else if (data.usageMetadata) {
+ usageRaw = data.usageMetadata;
+ }
+
+ const partsArray = responseNode.candidates?.[0]?.content?.parts;
+ if (Array.isArray(partsArray)) {
+ for (const part of partsArray) {
+ const hasFunctionCall = part.functionCall !== undefined;
+ const hasInlineData = part.inlineData !== undefined || part.inline_data !== undefined;
+ const sig = part.thoughtSignature || part.thought_signature || '';
+ const text = part.text || '';
+ const thought = part.thought || false;
+
+ if (hasFunctionCall || hasInlineData) {
+ flushPending();
+ parts.push(normalizePart(part));
+ continue;
+ }
+
+ if (thought || part.text !== undefined) {
+ const kind = thought ? 'thought' : 'text';
+ if (pendingKind && pendingKind !== kind) {
+ flushPending();
+ }
+ pendingKind = kind;
+ pendingText += text;
+ if (kind === 'thought' && sig) {
+ pendingThoughtSig = sig;
+ }
+ continue;
+ }
+
+ flushPending();
+ parts.push(normalizePart(part));
+ }
+ }
+ }
+
+ flushPending();
+
+ // 构建最终响应
+ if (!responseTemplate) {
+ responseTemplate = '{"candidates":[{"content":{"role":"model","parts":[]}}]}';
+ }
+
+ let result = JSON.parse(responseTemplate);
+
+ // 设置 parts
+ if (!result.candidates) {
+ result.candidates = [{ content: { role: 'model', parts: [] } }];
+ }
+ if (!result.candidates[0]) {
+ result.candidates[0] = { content: { role: 'model', parts: [] } };
+ }
+ if (!result.candidates[0].content) {
+ result.candidates[0].content = { role: 'model', parts: [] };
+ }
+ result.candidates[0].content.parts = parts;
+
+ if (role) {
+ result.candidates[0].content.role = role;
+ }
+ if (finishReason) {
+ result.candidates[0].finishReason = finishReason;
+ }
+ if (modelVersion) {
+ result.modelVersion = modelVersion;
+ }
+ if (responseId) {
+ result.responseId = responseId;
+ }
+ if (usageRaw) {
+ result.usageMetadata = usageRaw;
+ } else if (!result.usageMetadata) {
+ result.usageMetadata = {
+ promptTokenCount: 0,
+ candidatesTokenCount: 0,
+ totalTokenCount: 0
+ };
+ }
+
+ // 包装为最终格式
+ const output = {
+ response: result,
+ traceId: traceId || ''
+ };
+
+ return output;
+}
+
/**
* 将 Antigravity 响应转换为 Gemini 格式
+ * @param {Object} antigravityResponse - Antigravity 响应
+ * @returns {Object|null} Gemini 格式响应
*/
function toGeminiApiResponse(antigravityResponse) {
if (!antigravityResponse) return null;
@@ -200,6 +608,8 @@ function toGeminiApiResponse(antigravityResponse) {
/**
* 确保请求体中的内容部分都有角色属性
+ * @param {Object} requestBody - 请求体
+ * @returns {Object} 处理后的请求体
*/
function ensureRolesInContents(requestBody) {
delete requestBody.model;
@@ -254,21 +664,32 @@ export class AntigravityApiService {
this.userAgent = DEFAULT_USER_AGENT; // 支持通用 USER_AGENT 配置
this.projectId = config.PROJECT_ID;
- // Initialize instance-specific endpoints
- this.baseUrlDaily = config.ANTIGRAVITY_BASE_URL_DAILY || DEFAULT_ANTIGRAVITY_BASE_URL_DAILY;
- this.baseUrlAutopush = config.ANTIGRAVITY_BASE_URL_AUTOPUSH || DEFAULT_ANTIGRAVITY_BASE_URL_AUTOPUSH;
-
- // 多环境降级顺序
- this.baseURLs = [
- this.baseUrlDaily,
- this.baseUrlAutopush
- // ANTIGRAVITY_BASE_URL_PROD // 生产环境已注释
- ];
+ // 多环境降级顺序 - 按照 Go 代码的顺序
+ this.baseURLs = this.getBaseURLFallbackOrder(config);
// 保存代理配置供后续使用
this.proxyConfig = getProxyConfigForProvider(config, 'gemini-antigravity');
}
+ /**
+ * 获取 Base URL 降级顺序
+ * @param {Object} config - 配置对象
+ * @returns {string[]} Base URL 列表
+ */
+ getBaseURLFallbackOrder(config) {
+ // 如果配置了自定义 base_url,只使用该 URL
+ if (config.ANTIGRAVITY_BASE_URL) {
+ return [config.ANTIGRAVITY_BASE_URL.replace(/\/$/, '')];
+ }
+
+ // 默认降级顺序:daily -> sandbox -> prod
+ return [
+ ANTIGRAVITY_BASE_URL_DAILY,
+ ANTIGRAVITY_SANDBOX_BASE_URL_DAILY,
+ ANTIGRAVITY_BASE_URL_PROD
+ ];
+ }
+
async initialize() {
if (this.isInitialized) return;
console.log('[Antigravity] Initializing Antigravity API Service...');
@@ -478,12 +899,13 @@ export class AntigravityApiService {
};
const res = await this.authClient.request(requestOptions);
- console.log(`[Antigravity] Raw response from ${baseURL}:`, Object.keys(res.data.models));
+ // console.log(`[Antigravity] Raw response from ${baseURL}:`, Object.keys(res.data.models));
if (res.data && res.data.models) {
const models = Object.keys(res.data.models);
this.availableModels = models
.map(modelName2Alias)
- .filter(alias => alias !== undefined && alias !== '' && alias !== null);
+ .filter(alias => alias !== undefined && alias !== '' && alias !== null)
+ .filter(alias => ANTIGRAVITY_MODELS.includes(alias));
console.log(`[Antigravity] Available models: [${this.availableModels.join(', ')}]`);
return;
@@ -680,8 +1102,10 @@ export class AntigravityApiService {
});
let buffer = [];
- for await (const line of rl) {
+ for await (let line of rl) {
if (line.startsWith('data: ')) {
+ // 过滤 usageMetadata(仅在最终块中保留)
+ line = filterSSEUsageMetadata(line);
buffer.push(line.slice(6));
} else if (line === '' && buffer.length > 0) {
try {
@@ -714,6 +1138,7 @@ export class AntigravityApiService {
// 深拷贝请求体
const processedRequestBody = ensureRolesInContents(JSON.parse(JSON.stringify(requestBody)));
const actualModelName = alias2ModelName(selectedModel);
+ const isClaudeModel = isClaude(actualModelName);
// 将处理后的请求体转换为 Antigravity 格式
const payload = geminiToAntigravity(actualModelName, { request: processedRequestBody }, this.projectId);
@@ -721,10 +1146,42 @@ export class AntigravityApiService {
// 设置模型名称为实际模型名
payload.model = actualModelName;
+ // 对于 Claude 模型,使用流式请求然后转换为非流式响应
+ if (isClaudeModel) {
+ return await this.executeClaudeNonStream(payload);
+ }
+
const response = await this.callApi('generateContent', payload);
return toGeminiApiResponse(response.response);
}
+ /**
+ * 执行 Claude 非流式请求
+ * Claude 模型实际上使用流式请求,然后将结果合并为非流式响应
+ * @param {Object} payload - 请求体
+ * @returns {Object} 非流式响应
+ */
+ async executeClaudeNonStream(payload) {
+ const chunks = [];
+
+ try {
+ const stream = this.streamApi('streamGenerateContent', payload);
+ for await (const chunk of stream) {
+ if (chunk) {
+ chunks.push(JSON.stringify(chunk));
+ }
+ }
+
+ // 将流式响应转换为非流式响应
+ const streamData = chunks.join('\n');
+ const nonStreamResponse = convertStreamToNonStream(streamData);
+ return toGeminiApiResponse(nonStreamResponse.response);
+ } catch (error) {
+ console.error('[Antigravity] Claude non-stream execution error:', error.message);
+ throw error;
+ }
+ }
+
async * generateContentStream(model, requestBody) {
console.log(`[Antigravity Auth Token] Time until expiry: ${formatExpiryTime(this.authClient.credentials.expiry_date)}`);
@@ -819,7 +1276,7 @@ export class AntigravityApiService {
// 遍历模型数据,提取配额信息
for (const [modelId, modelData] of Object.entries(modelsData)) {
const aliasName = modelName2Alias(modelId);
- if (aliasName == null ||aliasName === '') continue; // 跳过不支持的模型
+ if (aliasName == null || aliasName === '') continue; // 跳过不支持的模型
const modelInfo = {
remaining: 0,
@@ -843,7 +1300,6 @@ export class AntigravityApiService {
sortedModels[key] = result.models[key];
});
result.models = sortedModels;
- // console.log(`[Antigravity] Sorted Models:`, sortedModels);
console.log(`[Antigravity] Successfully fetched quotas for ${Object.keys(result.models).length} models`);
break; // 成功获取后退出循环
}
diff --git a/src/gemini/gemini-core.js b/src/gemini/gemini-core.js
index cd14ac3..065ff28 100644
--- a/src/gemini/gemini-core.js
+++ b/src/gemini/gemini-core.js
@@ -639,7 +639,7 @@ export class GeminiApiService {
};
const res = await this.authClient.request(requestOptions);
- console.log(`[Gemini] retrieveUserQuota success`);
+ // console.log(`[Gemini] retrieveUserQuota success`, JSON.stringify(res.data));
if (res.data && res.data.buckets) {
const buckets = res.data.buckets;
diff --git a/src/ollama-handler.js b/src/ollama-handler.js
index d427e97..8f78086 100644
--- a/src/ollama-handler.js
+++ b/src/ollama-handler.js
@@ -377,12 +377,15 @@ export async function handleOllamaEndpointsAfterAuth(method, path, req, res, api
/**
* 处理 Ollama /api/tags 端点(列出模型)
+ * Note: apiService can be null when called before provider selection (e.g., from /ollama/api/tags)
+ * In this case, we fetch models from all healthy providers in the pool
*/
export async function handleOllamaTags(req, res, apiService, currentConfig, providerPoolManager) {
try {
console.log('[Ollama] Handling /api/tags request');
const ollamaConverter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.OLLAMA);
+ const { getServiceAdapter } = await import('./adapter.js');
// Helper to fetch and convert models from a provider
const fetchProviderModels = async (providerType, service) => {
@@ -402,31 +405,53 @@ export async function handleOllamaTags(req, res, apiService, currentConfig, prov
};
// Collect fetch promises
- const fetchPromises = [fetchProviderModels(currentConfig.MODEL_PROVIDER, apiService)];
+ const fetchPromises = [];
+ const processedProviderTypes = new Set();
- // Add provider pool fetches
+ // If apiService is provided, use it for the default provider
+ if (apiService) {
+ fetchPromises.push(fetchProviderModels(currentConfig.MODEL_PROVIDER, apiService));
+ processedProviderTypes.add(currentConfig.MODEL_PROVIDER);
+ }
+
+ // Add provider pool fetches (for all healthy providers)
if (providerPoolManager?.providerPools) {
- const { getServiceAdapter } = await import('./adapter.js');
-
for (const [providerType, providers] of Object.entries(providerPoolManager.providerPools)) {
- if (providerType === currentConfig.MODEL_PROVIDER) continue;
+ // Skip if already processed
+ if (processedProviderTypes.has(providerType)) continue;
- const healthyProvider = providers.find(p => p.isHealthy);
+ const healthyProvider = providers.find(p => p.isHealthy && !p.isDisabled);
if (healthyProvider) {
const tempConfig = { ...currentConfig, ...healthyProvider, MODEL_PROVIDER: providerType };
const service = getServiceAdapter(tempConfig);
fetchPromises.push(fetchProviderModels(providerType, service));
+ processedProviderTypes.add(providerType);
}
}
}
+ // If no providers available, return empty list
+ if (fetchPromises.length === 0) {
+ console.warn('[Ollama] No healthy providers available to fetch models');
+ const response = { models: [] };
+ res.writeHead(200, {
+ 'Content-Type': 'application/json',
+ 'Access-Control-Allow-Origin': '*',
+ 'Server': `ollama/${OLLAMA_VERSION}`
+ });
+ res.end(JSON.stringify(response));
+ return;
+ }
+
// Execute all fetches in parallel
const results = await Promise.all(fetchPromises);
const allModels = results.flat();
+ console.log(`[Ollama] Fetched ${allModels.length} models from ${processedProviderTypes.size} provider(s)`);
+
const response = { models: allModels };
- res.writeHead(200, {
+ res.writeHead(200, {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
'Server': `ollama/${OLLAMA_VERSION}`
@@ -434,7 +459,7 @@ export async function handleOllamaTags(req, res, apiService, currentConfig, prov
res.end(JSON.stringify(response));
} catch (error) {
console.error('[Ollama Tags Error]', error);
- handleError(res, error);
+ handleError(res, error, MODEL_PROTOCOL_PREFIX.OLLAMA);
}
}
@@ -459,7 +484,7 @@ export async function handleOllamaShow(req, res) {
res.end(JSON.stringify(showResponse));
} catch (error) {
console.error('[Ollama Show Error]', error);
- handleError(res, error);
+ handleError(res, error, MODEL_PROTOCOL_PREFIX.OLLAMA);
}
}
@@ -478,18 +503,20 @@ export function handleOllamaVersion(res) {
res.end(JSON.stringify(response));
} catch (error) {
console.error('[Ollama Version Error]', error);
- handleError(res, error);
+ handleError(res, error, MODEL_PROTOCOL_PREFIX.OLLAMA);
}
}
/**
* 处理 Ollama /api/chat 端点
+ * Note: apiService can be null when called before provider selection
*/
export async function handleOllamaChat(req, res, apiService, currentConfig, providerPoolManager) {
try {
console.log('[Ollama] Handling /api/chat request');
const ollamaRequest = await getRequestBody(req);
+ const { getServiceAdapter } = await import('./adapter.js');
// Determine provider based on model name
const rawModelName = ollamaRequest.model;
@@ -499,26 +526,35 @@ export async function handleOllamaChat(req, res, apiService, currentConfig, prov
console.log(`[Ollama] Model: ${modelName}, Detected provider: ${detectedProvider}`);
- // If provider is different, get the appropriate service
+ // Get the appropriate service based on detected provider
let actualApiService = apiService;
let actualConfig = currentConfig;
- if (detectedProvider !== currentConfig.MODEL_PROVIDER && providerPoolManager) {
- // Select provider from pool
- const providerConfig = providerPoolManager.selectProvider(detectedProvider, modelName, { skipUsageCount: true });
- if (providerConfig) {
- actualConfig = {
- ...currentConfig,
- ...providerConfig,
- MODEL_PROVIDER: detectedProvider
- };
-
- // Get service adapter for the detected provider
- const { getServiceAdapter } = await import('./adapter.js');
+ // If apiService is null or provider is different, get the appropriate service from pool
+ if (!apiService || detectedProvider !== currentConfig.MODEL_PROVIDER) {
+ if (providerPoolManager) {
+ // Select provider from pool
+ const providerConfig = providerPoolManager.selectProvider(detectedProvider, modelName, { skipUsageCount: true });
+ if (providerConfig) {
+ actualConfig = {
+ ...currentConfig,
+ ...providerConfig,
+ MODEL_PROVIDER: detectedProvider
+ };
+ actualApiService = getServiceAdapter(actualConfig);
+ console.log(`[Ollama] Using provider from pool: ${detectedProvider}`);
+ } else {
+ // No healthy provider in pool, try to create service directly
+ console.warn(`[Ollama] No healthy provider found for ${detectedProvider} in pool`);
+ if (!apiService) {
+ throw new Error(`No healthy provider available for ${detectedProvider}`);
+ }
+ }
+ } else if (!apiService) {
+ // No pool manager and no apiService, try to create service directly
+ actualConfig = { ...currentConfig, MODEL_PROVIDER: detectedProvider };
actualApiService = getServiceAdapter(actualConfig);
- console.log(`[Ollama] Switched to provider: ${detectedProvider}`);
- } else {
- console.warn(`[Ollama] No healthy provider found for ${detectedProvider}, using default`);
+ console.log(`[Ollama] Created service adapter for: ${detectedProvider}`);
}
}
@@ -574,18 +610,20 @@ export async function handleOllamaChat(req, res, apiService, currentConfig, prov
}
} catch (error) {
console.error('[Ollama Chat Error]', error);
- handleError(res, error);
+ handleError(res, error, MODEL_PROTOCOL_PREFIX.OLLAMA);
}
}
/**
* 处理 Ollama /api/generate 端点
+ * Note: apiService can be null when called before provider selection
*/
export async function handleOllamaGenerate(req, res, apiService, currentConfig, providerPoolManager) {
try {
console.log('[Ollama] Handling /api/generate request');
const ollamaRequest = await getRequestBody(req);
+ const { getServiceAdapter } = await import('./adapter.js');
// Determine provider based on model name
const rawModelName = ollamaRequest.model;
@@ -595,26 +633,35 @@ export async function handleOllamaGenerate(req, res, apiService, currentConfig,
console.log(`[Ollama] Model: ${modelName}, Detected provider: ${detectedProvider}`);
- // If provider is different, get the appropriate service
+ // Get the appropriate service based on detected provider
let actualApiService = apiService;
let actualConfig = currentConfig;
- if (detectedProvider !== currentConfig.MODEL_PROVIDER && providerPoolManager) {
- // Select provider from pool
- const providerConfig = providerPoolManager.selectProvider(detectedProvider, modelName, { skipUsageCount: true });
- if (providerConfig) {
- actualConfig = {
- ...currentConfig,
- ...providerConfig,
- MODEL_PROVIDER: detectedProvider
- };
-
- // Get service adapter for the detected provider
- const { getServiceAdapter } = await import('./adapter.js');
+ // If apiService is null or provider is different, get the appropriate service from pool
+ if (!apiService || detectedProvider !== currentConfig.MODEL_PROVIDER) {
+ if (providerPoolManager) {
+ // Select provider from pool
+ const providerConfig = providerPoolManager.selectProvider(detectedProvider, modelName, { skipUsageCount: true });
+ if (providerConfig) {
+ actualConfig = {
+ ...currentConfig,
+ ...providerConfig,
+ MODEL_PROVIDER: detectedProvider
+ };
+ actualApiService = getServiceAdapter(actualConfig);
+ console.log(`[Ollama] Using provider from pool: ${detectedProvider}`);
+ } else {
+ // No healthy provider in pool, try to create service directly
+ console.warn(`[Ollama] No healthy provider found for ${detectedProvider} in pool`);
+ if (!apiService) {
+ throw new Error(`No healthy provider available for ${detectedProvider}`);
+ }
+ }
+ } else if (!apiService) {
+ // No pool manager and no apiService, try to create service directly
+ actualConfig = { ...currentConfig, MODEL_PROVIDER: detectedProvider };
actualApiService = getServiceAdapter(actualConfig);
- console.log(`[Ollama] Switched to provider: ${detectedProvider}`);
- } else {
- console.warn(`[Ollama] No healthy provider found for ${detectedProvider}, using default`);
+ console.log(`[Ollama] Created service adapter for: ${detectedProvider}`);
}
}
@@ -670,7 +717,7 @@ export async function handleOllamaGenerate(req, res, apiService, currentConfig,
}
} catch (error) {
console.error('[Ollama Generate Error]', error);
- handleError(res, error);
+ handleError(res, error, MODEL_PROTOCOL_PREFIX.OLLAMA);
}
}
diff --git a/src/provider-models.js b/src/provider-models.js
index 4c2e666..48471fb 100644
--- a/src/provider-models.js
+++ b/src/provider-models.js
@@ -18,7 +18,7 @@ export const PROVIDER_MODELS = {
'gemini-3-pro-image-preview',
'gemini-3-pro-preview',
'gemini-3-flash-preview',
- 'gemini-2.5-flash',
+ 'gemini-2.5-flash-preview',
'gemini-claude-sonnet-4-5',
'gemini-claude-sonnet-4-5-thinking',
'gemini-claude-opus-4-5-thinking'
diff --git a/src/request-handler.js b/src/request-handler.js
index 77b1e8c..bcc1de8 100644
--- a/src/request-handler.js
+++ b/src/request-handler.js
@@ -110,7 +110,7 @@ export function createRequestHandler(config, providerPoolManager) {
return true;
} catch (error) {
console.log(`[Server] req provider_health error: ${error.message}`);
- handleError(res, { statusCode: 500, message: `Failed to get providers health: ${error.message}` });
+ handleError(res, { statusCode: 500, message: `Failed to get providers health: ${error.message}` }, currentConfig.MODEL_PROVIDER);
return;
}
}
@@ -125,8 +125,11 @@ export function createRequestHandler(config, providerPoolManager) {
}
// Check if the first path segment matches a MODEL_PROVIDER and switch if it does
+ // Note: 'ollama' is not a valid MODEL_PROVIDER, it's a protocol prefix for Ollama API compatibility
const pathSegments = path.split('/').filter(segment => segment.length > 0);
- if (pathSegments.length > 0) {
+ const isOllamaPath = pathSegments[0] === 'ollama' || path.startsWith('/api/');
+
+ if (pathSegments.length > 0 && !isOllamaPath) {
const firstSegment = pathSegments[0];
const isValidProvider = Object.values(MODEL_PROVIDER).includes(firstSegment);
if (firstSegment && isValidProvider) {
@@ -140,12 +143,28 @@ export function createRequestHandler(config, providerPoolManager) {
}
}
+ // Check authentication for API requests (before Ollama handling to allow unauthenticated Ollama endpoints)
+ if (!isAuthorized(req, requestUrl, currentConfig.REQUIRED_API_KEY)) {
+ res.writeHead(401, { 'Content-Type': 'application/json' });
+ res.end(JSON.stringify({ error: { message: 'Unauthorized: API key is invalid or missing.' } }));
+ return;
+ }
+
+ // Handle Ollama request BEFORE getting apiService (Ollama endpoints handle their own provider selection)
+ // This is important because Ollama /api/tags aggregates models from ALL providers, not just the default one
+ if (isOllamaPath) {
+ const { handled, normalizedPath } = await handleOllamaRequest(method, path, requestUrl, req, res, null, currentConfig, providerPoolManager);
+ if (handled) return;
+ // If not handled by Ollama handler, continue with normal flow
+ path = normalizedPath;
+ }
+
// 获取或选择 API Service 实例
let apiService;
try {
apiService = await getApiService(currentConfig);
} catch (error) {
- handleError(res, { statusCode: 500, message: `Failed to get API service: ${error.message}` });
+ handleError(res, { statusCode: 500, message: `Failed to get API service: ${error.message}` }, currentConfig.MODEL_PROVIDER);
const poolManager = getProviderPoolManager();
if (poolManager) {
poolManager.markProviderUnhealthy(currentConfig.MODEL_PROVIDER, {
@@ -155,13 +174,6 @@ export function createRequestHandler(config, providerPoolManager) {
return;
}
- // Check authentication for API requests
- if (!isAuthorized(req, requestUrl, currentConfig.REQUIRED_API_KEY)) {
- res.writeHead(401, { 'Content-Type': 'application/json' });
- res.end(JSON.stringify({ error: { message: 'Unauthorized: API key is invalid or missing.' } }));
- return;
- }
-
// Handle count_tokens requests (Anthropic API compatible)
if (path.includes('/count_tokens') && method === 'POST') {
try {
@@ -188,18 +200,13 @@ export function createRequestHandler(config, providerPoolManager) {
return true;
} catch (error) {
console.error(`[Server] count_tokens error: ${error.message}`);
- handleError(res, { statusCode: 500, message: `Failed to count tokens: ${error.message}` });
+ handleError(res, { statusCode: 500, message: `Failed to count tokens: ${error.message}` }, currentConfig.MODEL_PROVIDER);
return;
}
}
try {
- // Handle Ollama request (normalize path and route to appropriate endpoints)
- const { handled, normalizedPath } = await handleOllamaRequest(method, path, requestUrl, req, res, apiService, currentConfig, providerPoolManager);
- if (handled) return;
- path = normalizedPath;
-
- // Handle API requests
+ // Handle API requests (Ollama requests are already handled above before apiService is obtained)
const apiHandled = await handleAPIRequests(method, path, req, res, currentConfig, apiService, providerPoolManager, PROMPT_LOG_FILENAME);
if (apiHandled) return;
@@ -207,7 +214,7 @@ export function createRequestHandler(config, providerPoolManager) {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: { message: 'Not Found' } }));
} catch (error) {
- handleError(res, error);
+ handleError(res, error, currentConfig.MODEL_PROVIDER);
}
};
}
\ No newline at end of file
diff --git a/static/app/i18n.js b/static/app/i18n.js
index 0d6eec0..6d5f46d 100644
--- a/static/app/i18n.js
+++ b/static/app/i18n.js
@@ -3,6 +3,8 @@ const translations = {
'zh-CN': {
// Header
'header.title': 'AIClient2API 管理控制台',
+ 'header.github': 'GitHub 仓库',
+ 'header.themeToggle': '切换主题',
'header.status.connecting': '连接中...',
'header.status.connected': '已连接',
'header.status.disconnected': '连接断开',
@@ -423,6 +425,8 @@ const translations = {
'en-US': {
// Header
'header.title': 'AIClient2API Management Console',
+ 'header.github': 'GitHub Repository',
+ 'header.themeToggle': 'Toggle Theme',
'header.status.connecting': 'Connecting...',
'header.status.connected': 'Connected',
'header.status.disconnected': 'Disconnected',
diff --git a/static/index.html b/static/index.html
index f39fe41..819194b 100644
--- a/static/index.html
+++ b/static/index.html
@@ -17,12 +17,12 @@