
@@ -75,7 +79,7 @@ New API offers a wide range of features, please refer to [Features Introduction]
1. 🎨 Brand new UI interface
2. 🌍 Multi-language support
-3. 💰 Online recharge functionality (YiPay)
+3. 💰 Online recharge functionality, currently supports EPay and Stripe
4. 🔍 Support for querying usage quotas with keys (works with [neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool))
5. 🔄 Compatible with the original One API database
6. 💵 Support for pay-per-use model pricing
@@ -96,7 +100,11 @@ New API offers a wide range of features, please refer to [Features Introduction]
- Add `-thinking` suffix to enable thinking mode (e.g.: `claude-3-7-sonnet-20250219-thinking`)
16. 🔄 Thinking-to-content functionality
17. 🔄 Model rate limiting for users
-18. 💰 Cache billing support, which allows billing at a set ratio when cache is hit:
+18. 🔄 Request format conversion functionality, supporting the following three format conversions:
+ 1. OpenAI Chat Completions => Claude Messages
+ 2. Claude Messages => OpenAI Chat Completions (can be used for Claude Code to call third-party models)
+ 3. OpenAI Chat Completions => Gemini Chat
+19. 💰 Cache billing support, which allows billing at a set ratio when cache is hit:
1. Set the `Prompt Cache Ratio` option in `System Settings-Operation Settings`
2. Set `Prompt Cache Ratio` in the channel, range 0-1, e.g., setting to 0.5 means billing at 50% when cache is hit
3. Supported channels:
@@ -115,7 +123,9 @@ This version supports multiple models, please refer to [API Documentation-Relay
4. Custom channels, supporting full call address input
5. Rerank models ([Cohere](https://cohere.ai/) and [Jina](https://jina.ai/)), [API Documentation](https://docs.newapi.pro/api/jinaai-rerank)
6. Claude Messages format, [API Documentation](https://docs.newapi.pro/api/anthropic-chat)
-7. Dify, currently only supports chatflow
+7. Google Gemini format, [API Documentation](https://docs.newapi.pro/api/google-gemini-chat/)
+8. Dify, currently only supports chatflow
+9. For more interfaces, please refer to [API Documentation](https://docs.newapi.pro/api)
## Environment Variable Configuration
@@ -192,7 +202,8 @@ For detailed API documentation, please refer to [API Documentation](https://docs
- [Image API](https://docs.newapi.pro/api/openai-image)
- [Rerank API](https://docs.newapi.pro/api/jinaai-rerank)
- [Realtime API](https://docs.newapi.pro/api/openai-realtime)
-- [Claude Chat API (messages)](https://docs.newapi.pro/api/anthropic-chat)
+- [Claude Chat API](https://docs.newapi.pro/api/anthropic-chat)
+- [Google Gemini Chat API](https://docs.newapi.pro/api/google-gemini-chat)
## Related Projects
- [One API](https://github.com/songquanpeng/one-api): Original project
diff --git a/README.fr.md b/README.fr.md
index de788ede4..d06980053 100644
--- a/README.fr.md
+++ b/README.fr.md
@@ -1,6 +1,10 @@
+
+> [!NOTE]
+> **MT (Traduction Automatique)**: Ce document est traduit automatiquement. Pour les informations les plus précises, veuillez vous référer à la [version chinoise](./README.md).
+

@@ -75,7 +79,7 @@ New API offre un large éventail de fonctionnalités, veuillez vous référer à
1. 🎨 Nouvelle interface utilisateur
2. 🌍 Prise en charge multilingue
-3. 💰 Fonctionnalité de recharge en ligne (YiPay)
+3. 💰 Fonctionnalité de recharge en ligne, prend actuellement en charge EPay et Stripe
4. 🔍 Prise en charge de la recherche de quotas d'utilisation avec des clés (fonctionne avec [neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool))
5. 🔄 Compatible avec la base de données originale de One API
6. 💵 Prise en charge de la tarification des modèles de paiement à l'utilisation
@@ -96,7 +100,11 @@ New API offre un large éventail de fonctionnalités, veuillez vous référer à
- Ajouter le suffixe `-thinking` pour activer le mode de pensée (par exemple : `claude-3-7-sonnet-20250219-thinking`)
16. 🔄 Fonctionnalité de la pensée au contenu
17. 🔄 Limitation du débit du modèle pour les utilisateurs
-18. 💰 Prise en charge de la facturation du cache, qui permet de facturer à un ratio défini lorsque le cache est atteint :
+18. 🔄 Fonctionnalité de conversion de format de requête, prenant en charge les trois conversions de format suivantes :
+ 1. OpenAI Chat Completions => Claude Messages
+ 2. Claude Messages => OpenAI Chat Completions (peut être utilisé pour Claude Code pour appeler des modèles tiers)
+ 3. OpenAI Chat Completions => Gemini Chat
+19. 💰 Prise en charge de la facturation du cache, qui permet de facturer à un ratio défini lorsque le cache est atteint :
1. Définir l'option `Ratio de cache d'invite` dans `Paramètres système->Paramètres de fonctionnement`
2. Définir le `Ratio de cache d'invite` dans le canal, plage de 0 à 1, par exemple, le définir sur 0,5 signifie facturer à 50 % lorsque le cache est atteint
3. Canaux pris en charge :
@@ -115,7 +123,9 @@ Cette version prend en charge plusieurs modèles, veuillez vous référer à [Do
4. Canaux personnalisés, prenant en charge la saisie complète de l'adresse d'appel
5. Modèles Rerank ([Cohere](https://cohere.ai/) et [Jina](https://jina.ai/)), [Documentation de l'API](https://docs.newapi.pro/api/jinaai-rerank)
6. Format de messages Claude, [Documentation de l'API](https://docs.newapi.pro/api/anthropic-chat)
-7. Dify, ne prend actuellement en charge que chatflow
+7. Format Google Gemini, [Documentation de l'API](https://docs.newapi.pro/api/google-gemini-chat/)
+8. Dify, ne prend actuellement en charge que chatflow
+9. Pour plus d'interfaces, veuillez vous référer à la [Documentation de l'API](https://docs.newapi.pro/api)
## Configuration des variables d'environnement
@@ -192,7 +202,8 @@ Pour une documentation détaillée de l'API, veuillez vous référer à [Documen
- [API d'image](https://docs.newapi.pro/api/openai-image)
- [API de rerank](https://docs.newapi.pro/api/jinaai-rerank)
- [API en temps réel](https://docs.newapi.pro/api/openai-realtime)
-- [API de discussion Claude (messages)](https://docs.newapi.pro/api/anthropic-chat)
+- [API de discussion Claude](https://docs.newapi.pro/api/anthropic-chat)
+- [API de discussion Google Gemini](https://docs.newapi.pro/api/google-gemini-chat)
## Projets connexes
- [One API](https://github.com/songquanpeng/one-api) : Projet original
diff --git a/README.ja.md b/README.ja.md
new file mode 100644
index 000000000..13049e86d
--- /dev/null
+++ b/README.ja.md
@@ -0,0 +1,224 @@
+
+ 中文 | English | Français | 日本語
+
+
+> [!NOTE]
+> **MT(機械翻訳)**: この文書は機械翻訳されています。最も正確な情報については、[中国語版](./README.md)を参照してください。
+
+
+
+
+
+# New API
+
+🍥次世代大規模モデルゲートウェイとAI資産管理システム
+
+

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## 📝 プロジェクト説明
+
+> [!NOTE]
+> 本プロジェクトは、[One API](https://github.com/songquanpeng/one-api)をベースに二次開発されたオープンソースプロジェクトです
+
+> [!IMPORTANT]
+> - 本プロジェクトは個人学習用のみであり、安定性の保証や技術サポートは提供しません。
+> - ユーザーは、OpenAIの[利用規約](https://openai.com/policies/terms-of-use)および**法律法規**を遵守する必要があり、違法な目的で使用してはいけません。
+> - [《生成式人工智能服务管理暂行办法》](http://www.cac.gov.cn/2023-07/13/c_1690898327029107.htm)の要求に従い、中国地域の公衆に未登録の生成式AI サービスを提供しないでください。
+
+
🤝 信頼できるパートナー
+
+
順不同
+
+
+
+
+
+
+
+
+
+## 📚 ドキュメント
+
+詳細なドキュメントは公式Wikiをご覧ください:[https://docs.newapi.pro/](https://docs.newapi.pro/)
+
+AIが生成したDeepWikiにもアクセスできます:
+[](https://deepwiki.com/QuantumNous/new-api)
+
+## ✨ 主な機能
+
+New APIは豊富な機能を提供しています。詳細な機能については[機能説明](https://docs.newapi.pro/wiki/features-introduction)を参照してください:
+
+1. 🎨 全く新しいUIインターフェース
+2. 🌍 多言語サポート
+3. 💰 オンラインチャージ機能をサポート、現在EPayとStripeをサポート
+4. 🔍 キーによる使用量クォータの照会をサポート([neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool)と連携)
+5. 🔄 オリジナルのOne APIデータベースと互換性あり
+6. 💵 モデルの従量課金をサポート
+7. ⚖️ チャネルの重み付けランダムをサポート
+8. 📈 データダッシュボード(コンソール)
+9. 🔒 トークングループ化、モデル制限
+10. 🤖 より多くの認証ログイン方法をサポート(LinuxDO、Telegram、OIDC)
+11. 🔄 Rerankモデルをサポート(CohereとJina)、[API ドキュメント](https://docs.newapi.pro/api/jinaai-rerank)
+12. ⚡ OpenAI Realtime APIをサポート(Azureチャネルを含む)、[APIドキュメント](https://docs.newapi.pro/api/openai-realtime)
+13. ⚡ Claude Messages形式をサポート、[APIドキュメント](https://docs.newapi.pro/api/anthropic-chat)
+14. /chat2linkルートを使用してチャット画面に入ることをサポート
+15. 🧠 モデル名のサフィックスを通じてreasoning effortを設定することをサポート:
+ 1. OpenAI oシリーズモデル
+ - `-high`サフィックスを追加してhigh reasoning effortに設定(例:`o3-mini-high`)
+ - `-medium`サフィックスを追加してmedium reasoning effortに設定(例:`o3-mini-medium`)
+ - `-low`サフィックスを追加してlow reasoning effortに設定(例:`o3-mini-low`)
+ 2. Claude思考モデル
+ - `-thinking`サフィックスを追加して思考モードを有効にする(例:`claude-3-7-sonnet-20250219-thinking`)
+16. 🔄 思考からコンテンツへの機能
+17. 🔄 ユーザーに対するモデルレート制限機能
+18. 🔄 リクエストフォーマット変換機能、以下の3つのフォーマット変換をサポート:
+ 1. OpenAI Chat Completions => Claude Messages
+ 2. Claude Messages => OpenAI Chat Completions(Claude Codeがサードパーティモデルを呼び出す際に使用可能)
+ 3. OpenAI Chat Completions => Gemini Chat
+19. 💰 キャッシュ課金サポート、有効にするとキャッシュがヒットした際に設定された比率で課金できます:
+ 1. `システム設定-運営設定`で`プロンプトキャッシュ倍率`オプションを設定
+ 2. チャネルで`プロンプトキャッシュ倍率`を設定、範囲は0-1、例えば0.5に設定するとキャッシュがヒットした際に50%で課金
+ 3. サポートされているチャネル:
+ - [x] OpenAI
+ - [x] Azure
+ - [x] DeepSeek
+ - [x] Claude
+
+## モデルサポート
+
+このバージョンは複数のモデルをサポートしています。詳細は[APIドキュメント-中継インターフェース](https://docs.newapi.pro/api)を参照してください:
+
+1. サードパーティモデル **gpts**(gpt-4-gizmo-*)
+2. サードパーティチャネル[Midjourney-Proxy(Plus)](https://github.com/novicezk/midjourney-proxy)インターフェース、[APIドキュメント](https://docs.newapi.pro/api/midjourney-proxy-image)
+3. サードパーティチャネル[Suno API](https://github.com/Suno-API/Suno-API)インターフェース、[APIドキュメント](https://docs.newapi.pro/api/suno-music)
+4. カスタムチャネル、完全な呼び出しアドレスの入力をサポート
+5. Rerankモデル([Cohere](https://cohere.ai/)と[Jina](https://jina.ai/))、[APIドキュメント](https://docs.newapi.pro/api/jinaai-rerank)
+6. Claude Messages形式、[APIドキュメント](https://docs.newapi.pro/api/anthropic-chat)
+7. Google Gemini形式、[APIドキュメント](https://docs.newapi.pro/api/google-gemini-chat/)
+8. Dify、現在はchatflowのみをサポート
+9. その他のインターフェースについては[APIドキュメント](https://docs.newapi.pro/api)を参照してください
+
+## 環境変数設定
+
+詳細な設定説明については[インストールガイド-環境変数設定](https://docs.newapi.pro/installation/environment-variables)を参照してください:
+
+- `GENERATE_DEFAULT_TOKEN`:新規登録ユーザーに初期トークンを生成するかどうか、デフォルトは`false`
+- `STREAMING_TIMEOUT`:ストリーミング応答のタイムアウト時間、デフォルトは300秒
+- `DIFY_DEBUG`:Difyチャネルがワークフローとノード情報を出力するかどうか、デフォルトは`true`
+- `GET_MEDIA_TOKEN`:画像トークンを統計するかどうか、デフォルトは`true`
+- `GET_MEDIA_TOKEN_NOT_STREAM`:非ストリーミングの場合に画像トークンを統計するかどうか、デフォルトは`true`
+- `UPDATE_TASK`:非同期タスク(Midjourney、Suno)を更新するかどうか、デフォルトは`true`
+- `GEMINI_VISION_MAX_IMAGE_NUM`:Geminiモデルの最大画像数、デフォルトは`16`
+- `MAX_FILE_DOWNLOAD_MB`: 最大ファイルダウンロードサイズ、単位MB、デフォルトは`20`
+- `CRYPTO_SECRET`:暗号化キー、Redisデータベースの内容を暗号化するために使用
+- `AZURE_DEFAULT_API_VERSION`:Azureチャネルのデフォルトのバージョン、デフォルトは`2025-04-01-preview`
+- `NOTIFICATION_LIMIT_DURATION_MINUTE`:メールなどの通知制限の継続時間、デフォルトは`10`分
+- `NOTIFY_LIMIT_COUNT`:指定された継続時間内のユーザー通知の最大数、デフォルトは`2`
+- `ERROR_LOG_ENABLED=true`: エラーログを記録して表示するかどうか、デフォルトは`false`
+
+## デプロイ
+
+詳細なデプロイガイドについては[インストールガイド-デプロイ方法](https://docs.newapi.pro/installation)を参照してください:
+
+> [!TIP]
+> 最新のDockerイメージ:`calciumion/new-api:latest`
+
+### マルチマシンデプロイの注意事項
+- 環境変数`SESSION_SECRET`を設定する必要があります。そうしないとマルチマシンデプロイ時にログイン状態が不一致になります
+- Redisを共有する場合、`CRYPTO_SECRET`を設定する必要があります。そうしないとマルチマシンデプロイ時にRedisの内容を取得できません
+
+### デプロイ要件
+- ローカルデータベース(デフォルト):SQLite(Dockerデプロイの場合は`/data`ディレクトリをマウントする必要があります)
+- リモートデータベース:MySQLバージョン >= 5.7.8、PgSQLバージョン >= 9.6
+
+### デプロイ方法
+
+#### 宝塔パネルのDocker機能を使用してデプロイ
+宝塔パネル(**9.2.0バージョン**以上)をインストールし、アプリケーションストアで**New-API**を見つけてインストールします。
+[画像付きチュートリアル](./docs/BT.md)
+
+#### Docker Composeを使用してデプロイ(推奨)
+```shell
+# プロジェクトをダウンロード
+git clone https://github.com/Calcium-Ion/new-api.git
+cd new-api
+# 必要に応じてdocker-compose.ymlを編集
+# 起動
+docker-compose up -d
+```
+
+#### Dockerイメージを直接使用
+```shell
+# SQLiteを使用
+docker run --name new-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -v /home/ubuntu/data/new-api:/data calciumion/new-api:latest
+
+# MySQLを使用
+docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:123456@tcp(localhost:3306)/oneapi" -e TZ=Asia/Shanghai -v /home/ubuntu/data/new-api:/data calciumion/new-api:latest
+```
+
+## チャネルリトライとキャッシュ
+チャネルリトライ機能はすでに実装されており、`設定->運営設定->一般設定->失敗リトライ回数`でリトライ回数を設定できます。**キャッシュ機能を有効にすることを推奨します**。
+
+### キャッシュ設定方法
+1. `REDIS_CONN_STRING`:Redisをキャッシュとして設定
+2. `MEMORY_CACHE_ENABLED`:メモリキャッシュを有効にする(Redisを設定した場合は手動設定不要)
+
+## APIドキュメント
+
+詳細なAPIドキュメントについては[APIドキュメント](https://docs.newapi.pro/api)を参照してください:
+
+- [チャットインターフェース(Chat)](https://docs.newapi.pro/api/openai-chat)
+- [画像インターフェース(Image)](https://docs.newapi.pro/api/openai-image)
+- [再ランク付けインターフェース(Rerank)](https://docs.newapi.pro/api/jinaai-rerank)
+- [リアルタイム対話インターフェース(Realtime)](https://docs.newapi.pro/api/openai-realtime)
+- [Claudeチャットインターフェース](https://docs.newapi.pro/api/anthropic-chat)
+- [Google Geminiチャットインターフェース](https://docs.newapi.pro/api/google-gemini-chat)
+
+## 関連プロジェクト
+- [One API](https://github.com/songquanpeng/one-api):オリジナルプロジェクト
+- [Midjourney-Proxy](https://github.com/novicezk/midjourney-proxy):Midjourneyインターフェースサポート
+- [neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool):キーを使用して使用量クォータを照会
+
+New APIベースのその他のプロジェクト:
+- [new-api-horizon](https://github.com/Calcium-Ion/new-api-horizon):New API高性能最適化版
+
+## ヘルプサポート
+
+問題がある場合は、[ヘルプサポート](https://docs.newapi.pro/support)を参照してください:
+- [コミュニティ交流](https://docs.newapi.pro/support/community-interaction)
+- [問題のフィードバック](https://docs.newapi.pro/support/feedback-issues)
+- [よくある質問](https://docs.newapi.pro/support/faq)
+
+## 🌟 Star History
+
+[](https://star-history.com/#Calcium-Ion/new-api&Date)
+
diff --git a/README.md b/README.md
index 2103fe8fc..af2b64b44 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
- 中文 | English | Français
+ 中文 | English | Français | 日本語
@@ -75,7 +75,7 @@ New API提供了丰富的功能,详细特性请参考[特性说明](https://do
1. 🎨 全新的UI界面
2. 🌍 多语言支持
-3. 💰 支持在线充值功能(易支付)
+3. 💰 支持在线充值功能,当前支持易支付和Stripe
4. 🔍 支持用key查询使用额度(配合[neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool))
5. 🔄 兼容原版One API的数据库
6. 💵 支持模型按次数收费
@@ -119,7 +119,9 @@ New API提供了丰富的功能,详细特性请参考[特性说明](https://do
4. 自定义渠道,支持填入完整调用地址
5. Rerank模型([Cohere](https://cohere.ai/)和[Jina](https://jina.ai/)),[接口文档](https://docs.newapi.pro/api/jinaai-rerank)
6. Claude Messages 格式,[接口文档](https://docs.newapi.pro/api/anthropic-chat)
-7. Dify,当前仅支持chatflow
+7. Google Gemini格式,[接口文档](https://docs.newapi.pro/api/google-gemini-chat/)
+8. Dify,当前仅支持chatflow
+9. 更多接口请参考[接口文档](https://docs.newapi.pro/api)
## 环境变量配置
@@ -128,16 +130,14 @@ New API提供了丰富的功能,详细特性请参考[特性说明](https://do
- `GENERATE_DEFAULT_TOKEN`:是否为新注册用户生成初始令牌,默认为 `false`
- `STREAMING_TIMEOUT`:流式回复超时时间,默认300秒
- `DIFY_DEBUG`:Dify渠道是否输出工作流和节点信息,默认 `true`
-- `FORCE_STREAM_OPTION`:是否覆盖客户端stream_options参数,默认 `true`
- `GET_MEDIA_TOKEN`:是否统计图片token,默认 `true`
- `GET_MEDIA_TOKEN_NOT_STREAM`:非流情况下是否统计图片token,默认 `true`
- `UPDATE_TASK`:是否更新异步任务(Midjourney、Suno),默认 `true`
-- `COHERE_SAFETY_SETTING`:Cohere模型安全设置,可选值为 `NONE`, `CONTEXTUAL`, `STRICT`,默认 `NONE`
- `GEMINI_VISION_MAX_IMAGE_NUM`:Gemini模型最大图片数量,默认 `16`
- `MAX_FILE_DOWNLOAD_MB`: 最大文件下载大小,单位MB,默认 `20`
-- `CRYPTO_SECRET`:加密密钥,用于加密数据库内容
+- `CRYPTO_SECRET`:加密密钥,用于加密Redis数据库内容
- `AZURE_DEFAULT_API_VERSION`:Azure渠道默认API版本,默认 `2025-04-01-preview`
-- `NOTIFICATION_LIMIT_DURATION_MINUTE`:通知限制持续时间,默认 `10`分钟
+- `NOTIFICATION_LIMIT_DURATION_MINUTE`:邮件等通知限制持续时间,默认 `10`分钟
- `NOTIFY_LIMIT_COUNT`:用户通知在指定持续时间内的最大数量,默认 `2`
- `ERROR_LOG_ENABLED=true`: 是否记录并显示错误日志,默认`false`
@@ -182,7 +182,7 @@ docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:1234
```
## 渠道重试与缓存
-渠道重试功能已经实现,可以在`设置->运营设置->通用设置`设置重试次数,**建议开启缓存**功能。
+渠道重试功能已经实现,可以在`设置->运营设置->通用设置->失败重试次数`设置重试次数,**建议开启缓存**功能。
### 缓存设置方法
1. `REDIS_CONN_STRING`:设置Redis作为缓存
@@ -196,12 +196,12 @@ docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:1234
- [图像接口(Image)](https://docs.newapi.pro/api/openai-image)
- [重排序接口(Rerank)](https://docs.newapi.pro/api/jinaai-rerank)
- [实时对话接口(Realtime)](https://docs.newapi.pro/api/openai-realtime)
-- [Claude聊天接口(messages)](https://docs.newapi.pro/api/anthropic-chat)
+- [Claude聊天接口](https://docs.newapi.pro/api/anthropic-chat)
+- [Google Gemini聊天接口](https://docs.newapi.pro/api/google-gemini-chat)
## 相关项目
- [One API](https://github.com/songquanpeng/one-api):原版项目
- [Midjourney-Proxy](https://github.com/novicezk/midjourney-proxy):Midjourney接口支持
-- [chatnio](https://github.com/Deeptrain-Community/chatnio):下一代AI一站式B/C端解决方案
- [neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool):用key查询使用额度
其他基于New API的项目:
diff --git a/constant/channel.go b/constant/channel.go
index 34fb20f46..7d8893c1d 100644
--- a/constant/channel.go
+++ b/constant/channel.go
@@ -51,9 +51,9 @@ const (
ChannelTypeJimeng = 51
ChannelTypeVidu = 52
ChannelTypeSubmodel = 53
+ ChannelTypeDoubaoVideo = 54
ChannelTypeDummy // this one is only for count, do not add any channel after this
-
)
var ChannelBaseURLs = []string{
@@ -111,4 +111,5 @@ var ChannelBaseURLs = []string{
"https://visual.volcengineapi.com", //51
"https://api.vidu.cn", //52
"https://llm.submodel.ai", //53
+ "https://ark.cn-beijing.volces.com", //54
}
diff --git a/controller/channel-test.go b/controller/channel-test.go
index b3a3be4eb..ff1e8cef4 100644
--- a/controller/channel-test.go
+++ b/controller/channel-test.go
@@ -70,6 +70,12 @@ func testChannel(channel *model.Channel, testModel string, endpointType string)
newAPIError: nil,
}
}
+ if channel.Type == constant.ChannelTypeDoubaoVideo {
+ return testResult{
+ localErr: errors.New("doubao video channel test is not supported"),
+ newAPIError: nil,
+ }
+ }
if channel.Type == constant.ChannelTypeVidu {
return testResult{
localErr: errors.New("vidu channel test is not supported"),
diff --git a/controller/task_video.go b/controller/task_video.go
index 73d5c39b1..ded011fe9 100644
--- a/controller/task_video.go
+++ b/controller/task_video.go
@@ -13,6 +13,7 @@ import (
"one-api/relay"
"one-api/relay/channel"
relaycommon "one-api/relay/common"
+ "one-api/setting/ratio_setting"
"time"
)
@@ -120,6 +121,91 @@ func updateVideoSingleTask(ctx context.Context, adaptor channel.TaskAdaptor, cha
if !(len(taskResult.Url) > 5 && taskResult.Url[:5] == "data:") {
task.FailReason = taskResult.Url
}
+
+ // 如果返回了 total_tokens 并且配置了模型倍率(非固定价格),则重新计费
+ if taskResult.TotalTokens > 0 {
+ // 获取模型名称
+ var taskData map[string]interface{}
+ if err := json.Unmarshal(task.Data, &taskData); err == nil {
+ if modelName, ok := taskData["model"].(string); ok && modelName != "" {
+ // 获取模型价格和倍率
+ modelRatio, hasRatioSetting, _ := ratio_setting.GetModelRatio(modelName)
+
+ // 只有配置了倍率(非固定价格)时才按 token 重新计费
+ if hasRatioSetting && modelRatio > 0 {
+ // 获取用户和组的倍率信息
+ user, err := model.GetUserById(task.UserId, false)
+ if err == nil {
+ groupRatio := ratio_setting.GetGroupRatio(user.Group)
+ userGroupRatio, hasUserGroupRatio := ratio_setting.GetGroupGroupRatio(user.Group, user.Group)
+
+ var finalGroupRatio float64
+ if hasUserGroupRatio {
+ finalGroupRatio = userGroupRatio
+ } else {
+ finalGroupRatio = groupRatio
+ }
+
+ // 计算实际应扣费额度: totalTokens * modelRatio * groupRatio
+ actualQuota := int(float64(taskResult.TotalTokens) * modelRatio * finalGroupRatio)
+
+ // 计算差额
+ preConsumedQuota := task.Quota
+ quotaDelta := actualQuota - preConsumedQuota
+
+ if quotaDelta > 0 {
+ // 需要补扣费
+ logger.LogInfo(ctx, fmt.Sprintf("视频任务 %s 预扣费后补扣费:%s(实际消耗:%s,预扣费:%s,tokens:%d)",
+ task.TaskID,
+ logger.LogQuota(quotaDelta),
+ logger.LogQuota(actualQuota),
+ logger.LogQuota(preConsumedQuota),
+ taskResult.TotalTokens,
+ ))
+ if err := model.DecreaseUserQuota(task.UserId, quotaDelta); err != nil {
+ logger.LogError(ctx, fmt.Sprintf("补扣费失败: %s", err.Error()))
+ } else {
+ model.UpdateUserUsedQuotaAndRequestCount(task.UserId, quotaDelta)
+ model.UpdateChannelUsedQuota(task.ChannelId, quotaDelta)
+ task.Quota = actualQuota // 更新任务记录的实际扣费额度
+
+ // 记录消费日志
+ logContent := fmt.Sprintf("视频任务成功补扣费,模型倍率 %.2f,分组倍率 %.2f,tokens %d,预扣费 %s,实际扣费 %s,补扣费 %s",
+ modelRatio, finalGroupRatio, taskResult.TotalTokens,
+ logger.LogQuota(preConsumedQuota), logger.LogQuota(actualQuota), logger.LogQuota(quotaDelta))
+ model.RecordLog(task.UserId, model.LogTypeSystem, logContent)
+ }
+ } else if quotaDelta < 0 {
+ // 需要退还多扣的费用
+ refundQuota := -quotaDelta
+ logger.LogInfo(ctx, fmt.Sprintf("视频任务 %s 预扣费后返还:%s(实际消耗:%s,预扣费:%s,tokens:%d)",
+ task.TaskID,
+ logger.LogQuota(refundQuota),
+ logger.LogQuota(actualQuota),
+ logger.LogQuota(preConsumedQuota),
+ taskResult.TotalTokens,
+ ))
+ if err := model.IncreaseUserQuota(task.UserId, refundQuota, false); err != nil {
+ logger.LogError(ctx, fmt.Sprintf("退还预扣费失败: %s", err.Error()))
+ } else {
+ task.Quota = actualQuota // 更新任务记录的实际扣费额度
+
+ // 记录退款日志
+ logContent := fmt.Sprintf("视频任务成功退还多扣费用,模型倍率 %.2f,分组倍率 %.2f,tokens %d,预扣费 %s,实际扣费 %s,退还 %s",
+ modelRatio, finalGroupRatio, taskResult.TotalTokens,
+ logger.LogQuota(preConsumedQuota), logger.LogQuota(actualQuota), logger.LogQuota(refundQuota))
+ model.RecordLog(task.UserId, model.LogTypeSystem, logContent)
+ }
+ } else {
+ // quotaDelta == 0, 预扣费刚好准确
+ logger.LogInfo(ctx, fmt.Sprintf("视频任务 %s 预扣费准确(%s,tokens:%d)",
+ task.TaskID, logger.LogQuota(actualQuota), taskResult.TotalTokens))
+ }
+ }
+ }
+ }
+ }
+ }
case model.TaskStatusFailure:
task.Status = model.TaskStatusFailure
task.Progress = "100%"
diff --git a/docker-compose.yml b/docker-compose.yml
index d98fd706e..e657390a7 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,4 +1,18 @@
-version: '3.4'
+# New-API Docker Compose Configuration
+#
+# Quick Start:
+# 1. docker-compose up -d
+# 2. Access at http://localhost:3000
+#
+# Using MySQL instead of PostgreSQL:
+# 1. Comment out the postgres service and SQL_DSN line 15
+# 2. Uncomment the mysql service and SQL_DSN line 16
+# 3. Uncomment mysql in depends_on (line 28)
+# 4. Uncomment mysql_data in volumes section (line 64)
+#
+# ⚠️ IMPORTANT: Change all default passwords before deploying to production!
+
+version: '3.4' # For compatibility with older Docker versions
services:
new-api:
@@ -12,21 +26,22 @@ services:
- ./data:/data
- ./logs:/app/logs
environment:
- - SQL_DSN=root:123456@tcp(mysql:3306)/new-api # Point to the mysql service
+ - SQL_DSN=postgresql://root:123456@postgres:5432/new-api # ⚠️ IMPORTANT: Change the password in production!
+# - SQL_DSN=root:123456@tcp(mysql:3306)/new-api # Point to the mysql service, uncomment if using MySQL
- REDIS_CONN_STRING=redis://redis
- TZ=Asia/Shanghai
- ERROR_LOG_ENABLED=true # 是否启用错误日志记录
- # - STREAMING_TIMEOUT=300 # 流模式无响应超时时间,单位秒,默认120秒,如果出现空补全可以尝试改为更大值
- # - SESSION_SECRET=random_string # 多机部署时设置,必须修改这个随机字符串!!!!!!!
- # - NODE_TYPE=slave # Uncomment for slave node in multi-node deployment
- # - SYNC_FREQUENCY=60 # Uncomment if regular database syncing is needed
- # - FRONTEND_BASE_URL=https://openai.justsong.cn # Uncomment for multi-node deployment with front-end URL
+ - BATCH_UPDATE_ENABLED=true # 是否启用批量更新 batch update enabled
+# - STREAMING_TIMEOUT=300 # 流模式无响应超时时间,单位秒,默认120秒,如果出现空补全可以尝试改为更大值 Streaming timeout in seconds, default is 120s. Increase if experiencing empty completions
+# - SESSION_SECRET=random_string # 多机部署时设置,必须修改这个随机字符串!! multi-node deployment, set this to a random string!!!!!!!
+# - SYNC_FREQUENCY=60 # Uncomment if regular database syncing is needed
depends_on:
- redis
- - mysql
+ - postgres
+# - mysql # Uncomment if using MySQL
healthcheck:
- test: ["CMD-SHELL", "wget -q -O - http://localhost:3000/api/status | grep -o '\"success\":\\s*true' | awk -F: '{print $$2}'"]
+ test: ["CMD-SHELL", "wget -q -O - http://localhost:3000/api/status | grep -o '\"success\":\\s*true' || exit 1"]
interval: 30s
timeout: 10s
retries: 3
@@ -36,17 +51,31 @@ services:
container_name: redis
restart: always
- mysql:
- image: mysql:8.2
- container_name: mysql
+ postgres:
+ image: postgres:15
+ container_name: postgres
restart: always
environment:
- MYSQL_ROOT_PASSWORD: 123456 # Ensure this matches the password in SQL_DSN
- MYSQL_DATABASE: new-api
+ POSTGRES_USER: root
+ POSTGRES_PASSWORD: 123456 # ⚠️ IMPORTANT: Change this password in production!
+ POSTGRES_DB: new-api
volumes:
- - mysql_data:/var/lib/mysql
- # ports:
- # - "3306:3306" # If you want to access MySQL from outside Docker, uncomment
+ - pg_data:/var/lib/postgresql/data
+# ports:
+# - "5432:5432" # Uncomment if you need to access PostgreSQL from outside Docker
+
+# mysql:
+# image: mysql:8.2
+# container_name: mysql
+# restart: always
+# environment:
+# MYSQL_ROOT_PASSWORD: 123456 # ⚠️ IMPORTANT: Change this password in production!
+# MYSQL_DATABASE: new-api
+# volumes:
+# - mysql_data:/var/lib/mysql
+# ports:
+# - "3306:3306" # Uncomment if you need to access MySQL from outside Docker
volumes:
- mysql_data:
+ pg_data:
+# mysql_data:
diff --git a/dto/gemini.go b/dto/gemini.go
index 80552aade..fdeb2793d 100644
--- a/dto/gemini.go
+++ b/dto/gemini.go
@@ -2,11 +2,12 @@ package dto
import (
"encoding/json"
- "github.com/gin-gonic/gin"
"one-api/common"
"one-api/logger"
"one-api/types"
"strings"
+
+ "github.com/gin-gonic/gin"
)
type GeminiChatRequest struct {
@@ -273,6 +274,7 @@ type GeminiChatGenerationConfig struct {
ResponseModalities []string `json:"responseModalities,omitempty"`
ThinkingConfig *GeminiThinkingConfig `json:"thinkingConfig,omitempty"`
SpeechConfig json.RawMessage `json:"speechConfig,omitempty"` // RawMessage to allow flexible speech config
+ ImageConfig json.RawMessage `json:"imageConfig,omitempty"` // RawMessage to allow flexible image config
}
type MediaResolution string
diff --git a/go.mod b/go.mod
index 66a452cee..4e743d231 100644
--- a/go.mod
+++ b/go.mod
@@ -1,9 +1,7 @@
module one-api
// +heroku goVersion go1.18
-go 1.24.0
-
-toolchain go1.24.6
+go 1.25.1
require (
github.com/Calcium-Ion/go-epay v0.0.4
@@ -13,7 +11,7 @@ require (
github.com/aws/aws-sdk-go-v2/credentials v1.17.11
github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.33.0
github.com/aws/smithy-go v1.22.5
- github.com/bytedance/gopkg v0.0.0-20220118071334-3db87571198b
+ github.com/bytedance/gopkg v0.1.3
github.com/gin-contrib/cors v1.7.2
github.com/gin-contrib/gzip v0.0.6
github.com/gin-contrib/sessions v0.0.5
@@ -53,11 +51,10 @@ require (
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.2 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.2 // indirect
github.com/boombuler/barcode v1.1.0 // indirect
- github.com/bytedance/sonic v1.11.6 // indirect
- github.com/bytedance/sonic/loader v0.1.1 // indirect
+ github.com/bytedance/sonic v1.14.1 // indirect
+ github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
- github.com/cloudwego/base64x v0.1.4 // indirect
- github.com/cloudwego/iasm v0.2.0 // indirect
+ github.com/cloudwego/base64x v0.1.6 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
@@ -84,7 +81,7 @@ require (
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
- github.com/klauspost/cpuid/v2 v2.2.9 // indirect
+ github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
@@ -100,7 +97,7 @@ require (
github.com/ugorji/go/codec v1.2.12 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/yusufpapurcu/wmi v1.2.3 // indirect
- golang.org/x/arch v0.12.0 // indirect
+ golang.org/x/arch v0.21.0 // indirect
golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.29.0 // indirect
diff --git a/go.sum b/go.sum
index a62b83210..bea3261b9 100644
--- a/go.sum
+++ b/go.sum
@@ -23,18 +23,16 @@ github.com/aws/smithy-go v1.22.5/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/boombuler/barcode v1.1.0 h1:ChaYjBR63fr4LFyGn8E8nt7dBSt3MiU3zMOZqFvVkHo=
github.com/boombuler/barcode v1.1.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
-github.com/bytedance/gopkg v0.0.0-20220118071334-3db87571198b h1:LTGVFpNmNHhj0vhOlfgWueFJ32eK9blaIlHR2ciXOT0=
-github.com/bytedance/gopkg v0.0.0-20220118071334-3db87571198b/go.mod h1:2ZlV9BaUH4+NXIBF0aMdKKAnHTzqH+iMU4KUjAbL23Q=
-github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
-github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
-github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
-github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
+github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
+github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
+github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w=
+github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc=
+github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
+github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
-github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
-github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
-github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
-github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
+github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
+github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -142,10 +140,8 @@ github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwA
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
-github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
-github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
-github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
-github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
+github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
+github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
@@ -248,9 +244,8 @@ github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFi
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
-golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
-golang.org/x/arch v0.12.0 h1:UsYJhbzPYGsT0HbEdmYcqtCv8UNGvnaL561NnIUvaKg=
-golang.org/x/arch v0.12.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
+golang.org/x/arch v0.21.0 h1:iTC9o7+wP6cPWpDWkivCvQFGAHDQ59SrSxsLPcnkArw=
+golang.org/x/arch v0.21.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
@@ -262,7 +257,6 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
-golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -272,7 +266,6 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220110181412-a018aaa089fe/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -320,5 +313,3 @@ modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM=
modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
-nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
-rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
diff --git a/relay/channel/api_request.go b/relay/channel/api_request.go
index 79a0f7060..548e720d9 100644
--- a/relay/channel/api_request.go
+++ b/relay/channel/api_request.go
@@ -14,6 +14,7 @@ import (
"one-api/service"
"one-api/setting/operation_setting"
"one-api/types"
+ "strings"
"sync"
"time"
@@ -36,6 +37,26 @@ func SetupApiRequestHeader(info *common.RelayInfo, c *gin.Context, req *http.Hea
}
}
+// processHeaderOverride 处理请求头覆盖,支持变量替换
+// 支持的变量:{api_key}
+func processHeaderOverride(info *common.RelayInfo) (map[string]string, error) {
+ headerOverride := make(map[string]string)
+ for k, v := range info.HeadersOverride {
+ str, ok := v.(string)
+ if !ok {
+ return nil, types.NewError(nil, types.ErrorCodeChannelHeaderOverrideInvalid)
+ }
+
+ // 替换支持的变量
+ if strings.Contains(str, "{api_key}") {
+ str = strings.ReplaceAll(str, "{api_key}", info.ApiKey)
+ }
+
+ headerOverride[k] = str
+ }
+ return headerOverride, nil
+}
+
func DoApiRequest(a Adaptor, c *gin.Context, info *common.RelayInfo, requestBody io.Reader) (*http.Response, error) {
fullRequestURL, err := a.GetRequestURL(info)
if err != nil {
@@ -49,13 +70,9 @@ func DoApiRequest(a Adaptor, c *gin.Context, info *common.RelayInfo, requestBody
return nil, fmt.Errorf("new request failed: %w", err)
}
headers := req.Header
- headerOverride := make(map[string]string)
- for k, v := range info.HeadersOverride {
- if str, ok := v.(string); ok {
- headerOverride[k] = str
- } else {
- return nil, types.NewError(err, types.ErrorCodeChannelHeaderOverrideInvalid)
- }
+ headerOverride, err := processHeaderOverride(info)
+ if err != nil {
+ return nil, err
}
for key, value := range headerOverride {
headers.Set(key, value)
@@ -86,13 +103,9 @@ func DoFormRequest(a Adaptor, c *gin.Context, info *common.RelayInfo, requestBod
// set form data
req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type"))
headers := req.Header
- headerOverride := make(map[string]string)
- for k, v := range info.HeadersOverride {
- if str, ok := v.(string); ok {
- headerOverride[k] = str
- } else {
- return nil, types.NewError(err, types.ErrorCodeChannelHeaderOverrideInvalid)
- }
+ headerOverride, err := processHeaderOverride(info)
+ if err != nil {
+ return nil, err
}
for key, value := range headerOverride {
headers.Set(key, value)
@@ -114,6 +127,13 @@ func DoWssRequest(a Adaptor, c *gin.Context, info *common.RelayInfo, requestBody
return nil, fmt.Errorf("get request url failed: %w", err)
}
targetHeader := http.Header{}
+ headerOverride, err := processHeaderOverride(info)
+ if err != nil {
+ return nil, err
+ }
+ for key, value := range headerOverride {
+ targetHeader.Set(key, value)
+ }
err = a.SetupRequestHeader(c, &targetHeader, info)
if err != nil {
return nil, fmt.Errorf("setup request header failed: %w", err)
diff --git a/relay/channel/aws/adaptor.go b/relay/channel/aws/adaptor.go
index 6202c9fc4..92d60df48 100644
--- a/relay/channel/aws/adaptor.go
+++ b/relay/channel/aws/adaptor.go
@@ -7,7 +7,6 @@ import (
"one-api/dto"
"one-api/relay/channel/claude"
relaycommon "one-api/relay/common"
- "one-api/setting/model_setting"
"one-api/types"
"github.com/gin-gonic/gin"
@@ -52,11 +51,7 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
}
func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *relaycommon.RelayInfo) error {
- anthropicBeta := c.Request.Header.Get("anthropic-beta")
- if anthropicBeta != "" {
- req.Set("anthropic-beta", anthropicBeta)
- }
- model_setting.GetClaudeSettings().WriteHeaders(info.OriginModelName, req)
+ claude.CommonClaudeHeadersOperation(c, req, info)
return nil
}
diff --git a/relay/channel/claude/adaptor.go b/relay/channel/claude/adaptor.go
index 362f09e77..17e7cbd2b 100644
--- a/relay/channel/claude/adaptor.go
+++ b/relay/channel/claude/adaptor.go
@@ -64,6 +64,15 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
return baseURL, nil
}
+func CommonClaudeHeadersOperation(c *gin.Context, req *http.Header, info *relaycommon.RelayInfo) {
+ // common headers operation
+ anthropicBeta := c.Request.Header.Get("anthropic-beta")
+ if anthropicBeta != "" {
+ req.Set("anthropic-beta", anthropicBeta)
+ }
+ model_setting.GetClaudeSettings().WriteHeaders(info.OriginModelName, req)
+}
+
func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *relaycommon.RelayInfo) error {
channel.SetupApiRequestHeader(info, c, req)
req.Set("x-api-key", info.ApiKey)
@@ -72,11 +81,7 @@ func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *rel
anthropicVersion = "2023-06-01"
}
req.Set("anthropic-version", anthropicVersion)
- anthropicBeta := c.Request.Header.Get("anthropic-beta")
- if anthropicBeta != "" {
- req.Set("anthropic-beta", anthropicBeta)
- }
- model_setting.GetClaudeSettings().WriteHeaders(info.OriginModelName, req)
+ CommonClaudeHeadersOperation(c, req, info)
return nil
}
diff --git a/relay/channel/task/doubao/adaptor.go b/relay/channel/task/doubao/adaptor.go
new file mode 100644
index 000000000..8cc1fa4f5
--- /dev/null
+++ b/relay/channel/task/doubao/adaptor.go
@@ -0,0 +1,248 @@
+package doubao
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "one-api/constant"
+ "one-api/dto"
+ "one-api/model"
+ "one-api/relay/channel"
+ relaycommon "one-api/relay/common"
+ "one-api/service"
+
+ "github.com/gin-gonic/gin"
+ "github.com/pkg/errors"
+)
+
+// ============================
+// Request / Response structures
+// ============================
+
+type ContentItem struct {
+ Type string `json:"type"` // "text" or "image_url"
+ Text string `json:"text,omitempty"` // for text type
+ ImageURL *ImageURL `json:"image_url,omitempty"` // for image_url type
+}
+
+type ImageURL struct {
+ URL string `json:"url"`
+}
+
+type requestPayload struct {
+ Model string `json:"model"`
+ Content []ContentItem `json:"content"`
+}
+
+type responsePayload struct {
+ ID string `json:"id"` // task_id
+}
+
+type responseTask struct {
+ ID string `json:"id"`
+ Model string `json:"model"`
+ Status string `json:"status"`
+ Content struct {
+ VideoURL string `json:"video_url"`
+ } `json:"content"`
+ Seed int `json:"seed"`
+ Resolution string `json:"resolution"`
+ Duration int `json:"duration"`
+ Ratio string `json:"ratio"`
+ FramesPerSecond int `json:"framespersecond"`
+ Usage struct {
+ CompletionTokens int `json:"completion_tokens"`
+ TotalTokens int `json:"total_tokens"`
+ } `json:"usage"`
+ CreatedAt int64 `json:"created_at"`
+ UpdatedAt int64 `json:"updated_at"`
+}
+
+// ============================
+// Adaptor implementation
+// ============================
+
+type TaskAdaptor struct {
+ ChannelType int
+ apiKey string
+ baseURL string
+}
+
+func (a *TaskAdaptor) Init(info *relaycommon.RelayInfo) {
+ a.ChannelType = info.ChannelType
+ a.baseURL = info.ChannelBaseUrl
+ a.apiKey = info.ApiKey
+}
+
+// ValidateRequestAndSetAction parses body, validates fields and sets default action.
+func (a *TaskAdaptor) ValidateRequestAndSetAction(c *gin.Context, info *relaycommon.RelayInfo) (taskErr *dto.TaskError) {
+ // Accept only POST /v1/video/generations as "generate" action.
+ return relaycommon.ValidateBasicTaskRequest(c, info, constant.TaskActionGenerate)
+}
+
+// BuildRequestURL constructs the upstream URL.
+func (a *TaskAdaptor) BuildRequestURL(info *relaycommon.RelayInfo) (string, error) {
+ return fmt.Sprintf("%s/api/v3/contents/generations/tasks", a.baseURL), nil
+}
+
+// BuildRequestHeader sets required headers.
+func (a *TaskAdaptor) BuildRequestHeader(c *gin.Context, req *http.Request, info *relaycommon.RelayInfo) error {
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Set("Accept", "application/json")
+ req.Header.Set("Authorization", "Bearer "+a.apiKey)
+ return nil
+}
+
+// BuildRequestBody converts request into Doubao specific format.
+func (a *TaskAdaptor) BuildRequestBody(c *gin.Context, info *relaycommon.RelayInfo) (io.Reader, error) {
+ v, exists := c.Get("task_request")
+ if !exists {
+ return nil, fmt.Errorf("request not found in context")
+ }
+ req := v.(relaycommon.TaskSubmitReq)
+
+ body, err := a.convertToRequestPayload(&req)
+ if err != nil {
+ return nil, errors.Wrap(err, "convert request payload failed")
+ }
+ data, err := json.Marshal(body)
+ if err != nil {
+ return nil, err
+ }
+ return bytes.NewReader(data), nil
+}
+
+// DoRequest delegates to common helper.
+func (a *TaskAdaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (*http.Response, error) {
+ return channel.DoTaskApiRequest(a, c, info, requestBody)
+}
+
+// DoResponse handles upstream response, returns taskID etc.
+func (a *TaskAdaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (taskID string, taskData []byte, taskErr *dto.TaskError) {
+ responseBody, err := io.ReadAll(resp.Body)
+ if err != nil {
+ taskErr = service.TaskErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError)
+ return
+ }
+ _ = resp.Body.Close()
+
+ // Parse Doubao response
+ var dResp responsePayload
+ if err := json.Unmarshal(responseBody, &dResp); err != nil {
+ taskErr = service.TaskErrorWrapper(errors.Wrapf(err, "body: %s", responseBody), "unmarshal_response_body_failed", http.StatusInternalServerError)
+ return
+ }
+
+ if dResp.ID == "" {
+ taskErr = service.TaskErrorWrapper(fmt.Errorf("task_id is empty"), "invalid_response", http.StatusInternalServerError)
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"task_id": dResp.ID})
+ return dResp.ID, responseBody, nil
+}
+
+// FetchTask fetch task status
+func (a *TaskAdaptor) FetchTask(baseUrl, key string, body map[string]any) (*http.Response, error) {
+ taskID, ok := body["task_id"].(string)
+ if !ok {
+ return nil, fmt.Errorf("invalid task_id")
+ }
+
+ uri := fmt.Sprintf("%s/api/v3/contents/generations/tasks/%s", baseUrl, taskID)
+
+ req, err := http.NewRequest(http.MethodGet, uri, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ req.Header.Set("Accept", "application/json")
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Set("Authorization", "Bearer "+key)
+
+ return service.GetHttpClient().Do(req)
+}
+
+func (a *TaskAdaptor) GetModelList() []string {
+ return ModelList
+}
+
+func (a *TaskAdaptor) GetChannelName() string {
+ return ChannelName
+}
+
+func (a *TaskAdaptor) convertToRequestPayload(req *relaycommon.TaskSubmitReq) (*requestPayload, error) {
+ r := requestPayload{
+ Model: req.Model,
+ Content: []ContentItem{},
+ }
+
+ // Add text prompt
+ if req.Prompt != "" {
+ r.Content = append(r.Content, ContentItem{
+ Type: "text",
+ Text: req.Prompt,
+ })
+ }
+
+ // Add images if present
+ if req.HasImage() {
+ for _, imgURL := range req.Images {
+ r.Content = append(r.Content, ContentItem{
+ Type: "image_url",
+ ImageURL: &ImageURL{
+ URL: imgURL,
+ },
+ })
+ }
+ }
+
+ // TODO: Add support for additional parameters from metadata
+ // such as ratio, duration, seed, etc.
+ // metadata := req.Metadata
+ // if metadata != nil {
+ // // Parse and apply metadata parameters
+ // }
+
+ return &r, nil
+}
+
+func (a *TaskAdaptor) ParseTaskResult(respBody []byte) (*relaycommon.TaskInfo, error) {
+ resTask := responseTask{}
+ if err := json.Unmarshal(respBody, &resTask); err != nil {
+ return nil, errors.Wrap(err, "unmarshal task result failed")
+ }
+
+ taskResult := relaycommon.TaskInfo{
+ Code: 0,
+ }
+
+ // Map Doubao status to internal status
+ switch resTask.Status {
+ case "pending", "queued":
+ taskResult.Status = model.TaskStatusQueued
+ taskResult.Progress = "10%"
+ case "processing":
+ taskResult.Status = model.TaskStatusInProgress
+ taskResult.Progress = "50%"
+ case "succeeded":
+ taskResult.Status = model.TaskStatusSuccess
+ taskResult.Progress = "100%"
+ taskResult.Url = resTask.Content.VideoURL
+ // 解析 usage 信息用于按倍率计费
+ taskResult.CompletionTokens = resTask.Usage.CompletionTokens
+ taskResult.TotalTokens = resTask.Usage.TotalTokens
+ case "failed":
+ taskResult.Status = model.TaskStatusFailure
+ taskResult.Progress = "100%"
+ taskResult.Reason = "task failed"
+ default:
+ // Unknown status, treat as processing
+ taskResult.Status = model.TaskStatusInProgress
+ taskResult.Progress = "30%"
+ }
+
+ return &taskResult, nil
+}
diff --git a/relay/channel/task/doubao/constants.go b/relay/channel/task/doubao/constants.go
new file mode 100644
index 000000000..74b416c6d
--- /dev/null
+++ b/relay/channel/task/doubao/constants.go
@@ -0,0 +1,9 @@
+package doubao
+
+var ModelList = []string{
+ "doubao-seedance-1-0-pro-250528",
+ "doubao-seedance-1-0-lite-t2v",
+ "doubao-seedance-1-0-lite-i2v",
+}
+
+var ChannelName = "doubao-video"
diff --git a/relay/common/relay_info.go b/relay/common/relay_info.go
index 35f8ad191..cc860abd9 100644
--- a/relay/common/relay_info.go
+++ b/relay/common/relay_info.go
@@ -500,12 +500,14 @@ func (t TaskSubmitReq) HasImage() bool {
}
type TaskInfo struct {
- Code int `json:"code"`
- TaskID string `json:"task_id"`
- Status string `json:"status"`
- Reason string `json:"reason,omitempty"`
- Url string `json:"url,omitempty"`
- Progress string `json:"progress,omitempty"`
+ Code int `json:"code"`
+ TaskID string `json:"task_id"`
+ Status string `json:"status"`
+ Reason string `json:"reason,omitempty"`
+ Url string `json:"url,omitempty"`
+ Progress string `json:"progress,omitempty"`
+ CompletionTokens int `json:"completion_tokens,omitempty"` // 用于按倍率计费
+ TotalTokens int `json:"total_tokens,omitempty"` // 用于按倍率计费
}
// RemoveDisabledFields 从请求 JSON 数据中移除渠道设置中禁用的字段
diff --git a/relay/relay_adaptor.go b/relay/relay_adaptor.go
index 406074c58..c8fd51a11 100644
--- a/relay/relay_adaptor.go
+++ b/relay/relay_adaptor.go
@@ -1,6 +1,7 @@
package relay
import (
+ "github.com/gin-gonic/gin"
"one-api/constant"
"one-api/relay/channel"
"one-api/relay/channel/ali"
@@ -24,6 +25,8 @@ import (
"one-api/relay/channel/palm"
"one-api/relay/channel/perplexity"
"one-api/relay/channel/siliconflow"
+ "one-api/relay/channel/submodel"
+ taskdoubao "one-api/relay/channel/task/doubao"
taskjimeng "one-api/relay/channel/task/jimeng"
"one-api/relay/channel/task/kling"
"one-api/relay/channel/task/suno"
@@ -37,8 +40,6 @@ import (
"one-api/relay/channel/zhipu"
"one-api/relay/channel/zhipu_4v"
"strconv"
- "one-api/relay/channel/submodel"
- "github.com/gin-gonic/gin"
)
func GetAdaptor(apiType int) channel.Adaptor {
@@ -134,6 +135,8 @@ func GetTaskAdaptor(platform constant.TaskPlatform) channel.TaskAdaptor {
return &taskvertex.TaskAdaptor{}
case constant.ChannelTypeVidu:
return &taskVidu.TaskAdaptor{}
+ case constant.ChannelTypeDoubaoVideo:
+ return &taskdoubao.TaskAdaptor{}
}
}
return nil
diff --git a/web/jsconfig.json b/web/jsconfig.json
index ced4d0543..170a7cb4c 100644
--- a/web/jsconfig.json
+++ b/web/jsconfig.json
@@ -6,4 +6,4 @@
}
},
"include": ["src/**/*"]
-}
\ No newline at end of file
+}
diff --git a/web/src/components/common/modals/TwoFactorAuthModal.jsx b/web/src/components/common/modals/TwoFactorAuthModal.jsx
index 2a9a8b25b..082e63d79 100644
--- a/web/src/components/common/modals/TwoFactorAuthModal.jsx
+++ b/web/src/components/common/modals/TwoFactorAuthModal.jsx
@@ -135,7 +135,9 @@ const TwoFactorAuthModal = ({
autoFocus
/>
- {t('支持6位TOTP验证码或8位备用码,可到`个人设置-安全设置-两步验证设置`配置或查看。')}
+ {t(
+ '支持6位TOTP验证码或8位备用码,可到`个人设置-安全设置-两步验证设置`配置或查看。',
+ )}
diff --git a/web/src/components/layout/Footer.jsx b/web/src/components/layout/Footer.jsx
index 5c210fca8..c827a581b 100644
--- a/web/src/components/layout/Footer.jsx
+++ b/web/src/components/layout/Footer.jsx
@@ -142,14 +142,6 @@ const FooterBar = () => {
>
Midjourney-Proxy
-