diff --git a/README.en.md b/README.en.md index 69fd32f8..2349104a 100644 --- a/README.en.md +++ b/README.en.md @@ -1,5 +1,5 @@

- 中文 | English + 中文 | English | Français

diff --git a/README.fr.md b/README.fr.md new file mode 100644 index 00000000..de788ede --- /dev/null +++ b/README.fr.md @@ -0,0 +1,216 @@ +

+ 中文 | English | Français +

+
+ +![new-api](/web/public/logo.png) + +# New API + +🍥 Passerelle de modèles étendus de nouvelle génération et système de gestion d'actifs d'IA + +Calcium-Ion%2Fnew-api | Trendshift + +

+ + licence + + + version + + + docker + + + docker + + + GoReportCard + +

+
+ +## 📝 Description du projet + +> [!NOTE] +> Il s'agit d'un projet open-source développé sur la base de [One API](https://github.com/songquanpeng/one-api) + +> [!IMPORTANT] +> - Ce projet est uniquement destiné à des fins d'apprentissage personnel, sans garantie de stabilité ni de support technique. +> - Les utilisateurs doivent se conformer aux [Conditions d'utilisation](https://openai.com/policies/terms-of-use) d'OpenAI et aux **lois et réglementations applicables**, et ne doivent pas l'utiliser à des fins illégales. +> - Conformément aux [《Mesures provisoires pour la gestion des services d'intelligence artificielle générative》](http://www.cac.gov.cn/2023-07/13/c_1690898327029107.htm), veuillez ne fournir aucun service d'IA générative non enregistré au public en Chine. + +

🤝 Partenaires de confiance

+

 

+

Sans ordre particulier

+

+ Cherry Studio + Université de Pékin + UCloud + Alibaba Cloud + IO.NET +

+

 

+ +## 📚 Documentation + +Pour une documentation détaillée, veuillez consulter notre Wiki officiel : [https://docs.newapi.pro/](https://docs.newapi.pro/) + +Vous pouvez également accéder au DeepWiki généré par l'IA : +[![Demander à DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/QuantumNous/new-api) + +## ✨ Fonctionnalités clés + +New API offre un large éventail de fonctionnalités, veuillez vous référer à [Présentation des fonctionnalités](https://docs.newapi.pro/wiki/features-introduction) pour plus de détails : + +1. 🎨 Nouvelle interface utilisateur +2. 🌍 Prise en charge multilingue +3. 💰 Fonctionnalité de recharge en ligne (YiPay) +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 +7. ⚖️ Prise en charge de la sélection aléatoire pondérée des canaux +8. 📈 Tableau de bord des données (console) +9. 🔒 Regroupement de jetons et restrictions de modèles +10. 🤖 Prise en charge de plus de méthodes de connexion par autorisation (LinuxDO, Telegram, OIDC) +11. 🔄 Prise en charge des modèles Rerank (Cohere et Jina), [Documentation de l'API](https://docs.newapi.pro/api/jinaai-rerank) +12. ⚡ Prise en charge de l'API OpenAI Realtime (y compris les canaux Azure), [Documentation de l'API](https://docs.newapi.pro/api/openai-realtime) +13. ⚡ Prise en charge du format Claude Messages, [Documentation de l'API](https://docs.newapi.pro/api/anthropic-chat) +14. Prise en charge de l'accès à l'interface de discussion via la route /chat2link +15. 🧠 Prise en charge de la définition de l'effort de raisonnement via les suffixes de nom de modèle : + 1. Modèles de la série o d'OpenAI + - Ajouter le suffixe `-high` pour un effort de raisonnement élevé (par exemple : `o3-mini-high`) + - Ajouter le suffixe `-medium` pour un effort de raisonnement moyen (par exemple : `o3-mini-medium`) + - Ajouter le suffixe `-low` pour un effort de raisonnement faible (par exemple : `o3-mini-low`) + 2. Modèles de pensée de Claude + - 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 : + 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 : + - [x] OpenAI + - [x] Azure + - [x] DeepSeek + - [x] Claude + +## Prise en charge des modèles + +Cette version prend en charge plusieurs modèles, veuillez vous référer à [Documentation de l'API-Interface de relais](https://docs.newapi.pro/api) pour plus de détails : + +1. Modèles tiers **gpts** (gpt-4-gizmo-*) +2. Canal tiers [Midjourney-Proxy(Plus)](https://github.com/novicezk/midjourney-proxy), [Documentation de l'API](https://docs.newapi.pro/api/midjourney-proxy-image) +3. Canal tiers [Suno API](https://github.com/Suno-API/Suno-API), [Documentation de l'API](https://docs.newapi.pro/api/suno-music) +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 + +## Configuration des variables d'environnement + +Pour des instructions de configuration détaillées, veuillez vous référer à [Guide d'installation-Configuration des variables d'environnement](https://docs.newapi.pro/installation/environment-variables) : + +- `GENERATE_DEFAULT_TOKEN` : S'il faut générer des jetons initiaux pour les utilisateurs nouvellement enregistrés, la valeur par défaut est `false` +- `STREAMING_TIMEOUT` : Délai d'expiration de la réponse en streaming, la valeur par défaut est de 300 secondes +- `DIFY_DEBUG` : S'il faut afficher les informations sur le flux de travail et les nœuds pour les canaux Dify, la valeur par défaut est `true` +- `FORCE_STREAM_OPTION` : S'il faut remplacer le paramètre client stream_options, la valeur par défaut est `true` +- `GET_MEDIA_TOKEN` : S'il faut compter les jetons d'image, la valeur par défaut est `true` +- `GET_MEDIA_TOKEN_NOT_STREAM` : S'il faut compter les jetons d'image dans les cas sans streaming, la valeur par défaut est `true` +- `UPDATE_TASK` : S'il faut mettre à jour les tâches asynchrones (Midjourney, Suno), la valeur par défaut est `true` +- `COHERE_SAFETY_SETTING` : Paramètres de sécurité du modèle Cohere, les options sont `NONE`, `CONTEXTUAL`, `STRICT`, la valeur par défaut est `NONE` +- `GEMINI_VISION_MAX_IMAGE_NUM` : Nombre maximum d'images pour les modèles Gemini, la valeur par défaut est `16` +- `MAX_FILE_DOWNLOAD_MB` : Taille maximale de téléchargement de fichier en Mo, la valeur par défaut est `20` +- `CRYPTO_SECRET` : Clé de chiffrement utilisée pour chiffrer le contenu de la base de données +- `AZURE_DEFAULT_API_VERSION` : Version de l'API par défaut du canal Azure, la valeur par défaut est `2025-04-01-preview` +- `NOTIFICATION_LIMIT_DURATION_MINUTE` : Durée de la limite de notification, la valeur par défaut est de `10` minutes +- `NOTIFY_LIMIT_COUNT` : Nombre maximal de notifications utilisateur dans la durée spécifiée, la valeur par défaut est `2` +- `ERROR_LOG_ENABLED=true` : S'il faut enregistrer et afficher les journaux d'erreurs, la valeur par défaut est `false` + +## Déploiement + +Pour des guides de déploiement détaillés, veuillez vous référer à [Guide d'installation-Méthodes de déploiement](https://docs.newapi.pro/installation) : + +> [!TIP] +> Dernière image Docker : `calciumion/new-api:latest` + +### Considérations sur le déploiement multi-machines +- La variable d'environnement `SESSION_SECRET` doit être définie, sinon l'état de connexion sera incohérent sur plusieurs machines +- Si vous partagez Redis, `CRYPTO_SECRET` doit être défini, sinon le contenu de Redis ne pourra pas être consulté sur plusieurs machines + +### Exigences de déploiement +- Base de données locale (par défaut) : SQLite (le déploiement Docker doit monter le répertoire `/data`) +- Base de données distante : MySQL version >= 5.7.8, PgSQL version >= 9.6 + +### Méthodes de déploiement + +#### Utilisation de la fonctionnalité Docker du panneau BaoTa +Installez le panneau BaoTa (version **9.2.0** ou supérieure), recherchez **New-API** dans le magasin d'applications et installez-le. +[Tutoriel avec des images](./docs/BT.md) + +#### Utilisation de Docker Compose (recommandé) +```shell +# Télécharger le projet +git clone https://github.com/Calcium-Ion/new-api.git +cd new-api +# Modifier docker-compose.yml si nécessaire +# Démarrer +docker-compose up -d +``` + +#### Utilisation directe de l'image Docker +```shell +# Utilisation de 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 + +# Utilisation de 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 +``` + +## Nouvelle tentative de canal et cache +La fonctionnalité de nouvelle tentative de canal a été implémentée, vous pouvez définir le nombre de tentatives dans `Paramètres->Paramètres de fonctionnement->Paramètres généraux`. Il est **recommandé d'activer la mise en cache**. + +### Méthode de configuration du cache +1. `REDIS_CONN_STRING` : Définir Redis comme cache +2. `MEMORY_CACHE_ENABLED` : Activer le cache mémoire (pas besoin de le définir manuellement si Redis est défini) + +## Documentation de l'API + +Pour une documentation détaillée de l'API, veuillez vous référer à [Documentation de l'API](https://docs.newapi.pro/api) : + +- [API de discussion](https://docs.newapi.pro/api/openai-chat) +- [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) + +## Projets connexes +- [One API](https://github.com/songquanpeng/one-api) : Projet original +- [Midjourney-Proxy](https://github.com/novicezk/midjourney-proxy) : Prise en charge de l'interface Midjourney +- [chatnio](https://github.com/Deeptrain-Community/chatnio) : Solution B/C unique d'IA de nouvelle génération +- [neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool) : Interroger le quota d'utilisation avec une clé + +Autres projets basés sur New API : +- [new-api-horizon](https://github.com/Calcium-Ion/new-api-horizon) : Version optimisée hautes performances de New API +- [VoAPI](https://github.com/VoAPI/VoAPI) : Version embellie du frontend basée sur New API + +## Aide et support + +Si vous avez des questions, veuillez vous référer à [Aide et support](https://docs.newapi.pro/support) : +- [Interaction avec la communauté](https://docs.newapi.pro/support/community-interaction) +- [Commentaires sur les problèmes](https://docs.newapi.pro/support/feedback-issues) +- [FAQ](https://docs.newapi.pro/support/faq) + +## 🌟 Historique des étoiles + +[![Graphique de l'historique des étoiles](https://api.star-history.com/svg?repos=Calcium-Ion/new-api&type=Date)](https://star-history.com/#Calcium-Ion/new-api&Date) \ No newline at end of file diff --git a/README.md b/README.md index d68b3e13..2103fe8f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- 中文 | English + 中文 | English | Français

diff --git a/common/api_type.go b/common/api_type.go index 5ac46c86..855eef84 100644 --- a/common/api_type.go +++ b/common/api_type.go @@ -67,6 +67,8 @@ func ChannelType2APIType(channelType int) (int, bool) { apiType = constant.APITypeJimeng case constant.ChannelTypeMoonshot: apiType = constant.APITypeMoonshot + case constant.ChannelTypeSubmodel: + apiType = constant.APITypeSubmodel } if apiType == -1 { return constant.APITypeOpenAI, false diff --git a/constant/api_type.go b/constant/api_type.go index f62d91d5..0ea5048f 100644 --- a/constant/api_type.go +++ b/constant/api_type.go @@ -31,6 +31,7 @@ const ( APITypeXai APITypeCoze APITypeJimeng - APITypeMoonshot // this one is only for count, do not add any channel after this - APITypeDummy // this one is only for count, do not add any channel after this + APITypeMoonshot + APITypeSubmodel + APITypeDummy // this one is only for count, do not add any channel after this ) diff --git a/constant/channel.go b/constant/channel.go index 2e1cc5b0..34fb20f4 100644 --- a/constant/channel.go +++ b/constant/channel.go @@ -50,8 +50,10 @@ const ( ChannelTypeKling = 50 ChannelTypeJimeng = 51 ChannelTypeVidu = 52 + ChannelTypeSubmodel = 53 ChannelTypeDummy // this one is only for count, do not add any channel after this + ) var ChannelBaseURLs = []string{ @@ -108,4 +110,5 @@ var ChannelBaseURLs = []string{ "https://api.klingai.com", //50 "https://visual.volcengineapi.com", //51 "https://api.vidu.cn", //52 + "https://llm.submodel.ai", //53 } diff --git a/controller/telegram.go b/controller/telegram.go index 8d07fc94..2b1ec4fc 100644 --- a/controller/telegram.go +++ b/controller/telegram.go @@ -65,7 +65,7 @@ func TelegramBind(c *gin.Context) { return } - c.Redirect(302, "/setting") + c.Redirect(302, "/console/personal") } func TelegramLogin(c *gin.Context) { diff --git a/controller/user.go b/controller/user.go index 982329ce..c03afa32 100644 --- a/controller/user.go +++ b/controller/user.go @@ -450,6 +450,10 @@ func GetSelf(c *gin.Context) { "role": user.Role, "status": user.Status, "email": user.Email, + "github_id": user.GitHubId, + "oidc_id": user.OidcId, + "wechat_id": user.WeChatId, + "telegram_id": user.TelegramId, "group": user.Group, "quota": user.Quota, "used_quota": user.UsedQuota, diff --git a/dto/claude.go b/dto/claude.go index 963e588b..42774226 100644 --- a/dto/claude.go +++ b/dto/claude.go @@ -196,10 +196,11 @@ type ClaudeRequest struct { TopP float64 `json:"top_p,omitempty"` TopK int `json:"top_k,omitempty"` //ClaudeMetadata `json:"metadata,omitempty"` - Stream bool `json:"stream,omitempty"` - Tools any `json:"tools,omitempty"` - ToolChoice any `json:"tool_choice,omitempty"` - Thinking *Thinking `json:"thinking,omitempty"` + Stream bool `json:"stream,omitempty"` + Tools any `json:"tools,omitempty"` + ContextManagement json.RawMessage `json:"context_management,omitempty"` + ToolChoice any `json:"tool_choice,omitempty"` + Thinking *Thinking `json:"thinking,omitempty"` } func (c *ClaudeRequest) GetTokenCountMeta() *types.TokenCountMeta { diff --git a/main.go b/main.go index e12dddc5..ba96d209 100644 --- a/main.go +++ b/main.go @@ -185,8 +185,9 @@ func InitResources() error { // This is a placeholder function for future resource initialization err := godotenv.Load(".env") if err != nil { - common.SysLog("未找到 .env 文件,使用默认环境变量,如果需要,请创建 .env 文件并设置相关变量") - common.SysLog("No .env file found, using default environment variables. If needed, please create a .env file and set the relevant variables.") + if common.DebugEnabled { + common.SysLog("No .env file found, using default environment variables. If needed, please create a .env file and set the relevant variables.") + } } // 加载环境变量 diff --git a/model/task.go b/model/task.go index 4c64a529..8e2b6d0b 100644 --- a/model/task.go +++ b/model/task.go @@ -24,7 +24,7 @@ type Task struct { ID int64 `json:"id" gorm:"primary_key;AUTO_INCREMENT"` CreatedAt int64 `json:"created_at" gorm:"index"` UpdatedAt int64 `json:"updated_at"` - TaskID string `json:"task_id" gorm:"type:varchar(50);index"` // 第三方id,不一定有/ song id\ Task id + TaskID string `json:"task_id" gorm:"type:varchar(191);index"` // 第三方id,不一定有/ song id\ Task id Platform constant.TaskPlatform `json:"platform" gorm:"type:varchar(30);index"` // 平台 UserId int `json:"user_id" gorm:"index"` ChannelId int `json:"channel_id" gorm:"index"` diff --git a/model/user.go b/model/user.go index ea0584c5..d3e40fa3 100644 --- a/model/user.go +++ b/model/user.go @@ -18,7 +18,7 @@ import ( // Otherwise, the sensitive information will be saved on local storage in plain text! type User struct { Id int `json:"id"` - Username string `json:"username" gorm:"unique;index" validate:"max=12"` + Username string `json:"username" gorm:"unique;index" validate:"max=20"` Password string `json:"password" gorm:"not null;" validate:"min=8,max=20"` OriginalPassword string `json:"original_password" gorm:"-:all"` // this field is only for Password change verification, don't save it to database! DisplayName string `json:"display_name" gorm:"index" validate:"max=20"` diff --git a/relay/channel/aws/adaptor.go b/relay/channel/aws/adaptor.go index 9d5e5891..6202c9fc 100644 --- a/relay/channel/aws/adaptor.go +++ b/relay/channel/aws/adaptor.go @@ -52,6 +52,10 @@ 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) return nil } diff --git a/relay/channel/aws/constants.go b/relay/channel/aws/constants.go index 5ac7ce99..45112d23 100644 --- a/relay/channel/aws/constants.go +++ b/relay/channel/aws/constants.go @@ -16,6 +16,7 @@ var awsModelIDMap = map[string]string{ "claude-sonnet-4-20250514": "anthropic.claude-sonnet-4-20250514-v1:0", "claude-opus-4-20250514": "anthropic.claude-opus-4-20250514-v1:0", "claude-opus-4-1-20250805": "anthropic.claude-opus-4-1-20250805-v1:0", + "claude-sonnet-4-5-20250929": "anthropic.claude-sonnet-4-5-20250929-v1:0", // Nova models "nova-micro-v1:0": "amazon.nova-micro-v1:0", "nova-lite-v1:0": "amazon.nova-lite-v1:0", @@ -69,6 +70,11 @@ var awsModelCanCrossRegionMap = map[string]map[string]bool{ "anthropic.claude-opus-4-1-20250805-v1:0": { "us": true, }, + "anthropic.claude-sonnet-4-5-20250929-v1:0": { + "us": true, + "ap": true, + "eu": true, + }, // Nova models - all support three major regions "amazon.nova-micro-v1:0": { "us": true, diff --git a/relay/channel/claude/adaptor.go b/relay/channel/claude/adaptor.go index 959327e1..362f09e7 100644 --- a/relay/channel/claude/adaptor.go +++ b/relay/channel/claude/adaptor.go @@ -52,11 +52,16 @@ func (a *Adaptor) Init(info *relaycommon.RelayInfo) { } func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) { + baseURL := "" if a.RequestMode == RequestModeMessage { - return fmt.Sprintf("%s/v1/messages", info.ChannelBaseUrl), nil + baseURL = fmt.Sprintf("%s/v1/messages", info.ChannelBaseUrl) } else { - return fmt.Sprintf("%s/v1/complete", info.ChannelBaseUrl), nil + baseURL = fmt.Sprintf("%s/v1/complete", info.ChannelBaseUrl) } + if info.IsClaudeBetaQuery { + baseURL = baseURL + "?beta=true" + } + return baseURL, nil } func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *relaycommon.RelayInfo) error { @@ -67,6 +72,10 @@ 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) return nil } diff --git a/relay/channel/claude/constants.go b/relay/channel/claude/constants.go index a23543d2..d0b36fe4 100644 --- a/relay/channel/claude/constants.go +++ b/relay/channel/claude/constants.go @@ -19,6 +19,8 @@ var ModelList = []string{ "claude-opus-4-20250514-thinking", "claude-opus-4-1-20250805", "claude-opus-4-1-20250805-thinking", + "claude-sonnet-4-5-20250929", + "claude-sonnet-4-5-20250929-thinking", } var ChannelName = "claude" diff --git a/relay/channel/submodel/adaptor.go b/relay/channel/submodel/adaptor.go new file mode 100644 index 00000000..db58fe64 --- /dev/null +++ b/relay/channel/submodel/adaptor.go @@ -0,0 +1,86 @@ +package submodel + +import ( + "errors" + "io" + "net/http" + "one-api/dto" + "one-api/relay/channel" + "one-api/relay/channel/openai" + relaycommon "one-api/relay/common" + "one-api/types" + + "github.com/gin-gonic/gin" +) + +type Adaptor struct { +} + +func (a *Adaptor) ConvertGeminiRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeminiChatRequest) (any, error) { + return nil, errors.New("submodel channel: endpoint not supported") +} + +func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) { + return nil, errors.New("submodel channel: endpoint not supported") +} + +func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) { + return nil, errors.New("submodel channel: endpoint not supported") +} + +func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.ImageRequest) (any, error) { + return nil, errors.New("submodel channel: endpoint not supported") +} + +func (a *Adaptor) Init(info *relaycommon.RelayInfo) { +} + +func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) { + return relaycommon.GetFullRequestURL(info.ChannelBaseUrl, info.RequestURLPath, info.ChannelType), nil +} + +func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *relaycommon.RelayInfo) error { + channel.SetupApiRequestHeader(info, c, req) + req.Set("Authorization", "Bearer "+info.ApiKey) + return nil +} + +func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { + if request == nil { + return nil, errors.New("request is nil") + } + return request, nil +} + +func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dto.RerankRequest) (any, error) { + return nil, errors.New("submodel channel: endpoint not supported") +} + +func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.EmbeddingRequest) (any, error) { + return nil, errors.New("submodel channel: endpoint not supported") +} + +func (a *Adaptor) ConvertOpenAIResponsesRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.OpenAIResponsesRequest) (any, error) { + return nil, errors.New("submodel channel: endpoint not supported") +} + +func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) { + return channel.DoApiRequest(a, c, info, requestBody) +} + +func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *types.NewAPIError) { + if info.IsStream { + usage, err = openai.OaiStreamHandler(c, info, resp) + } else { + usage, err = openai.OpenaiHandler(c, info, resp) + } + return +} + +func (a *Adaptor) GetModelList() []string { + return ModelList +} + +func (a *Adaptor) GetChannelName() string { + return ChannelName +} diff --git a/relay/channel/submodel/constants.go b/relay/channel/submodel/constants.go new file mode 100644 index 00000000..f5e1feb8 --- /dev/null +++ b/relay/channel/submodel/constants.go @@ -0,0 +1,16 @@ +package submodel + +var ModelList = []string{ + "NousResearch/Hermes-4-405B-FP8", + "Qwen/Qwen3-235B-A22B-Thinking-2507", + "Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8", + "Qwen/Qwen3-235B-A22B-Instruct-2507", + "zai-org/GLM-4.5-FP8", + "openai/gpt-oss-120b", + "deepseek-ai/DeepSeek-R1-0528", + "deepseek-ai/DeepSeek-R1", + "deepseek-ai/DeepSeek-V3-0324", + "deepseek-ai/DeepSeek-V3.1", +} + +const ChannelName = "submodel" \ No newline at end of file diff --git a/relay/channel/vertex/adaptor.go b/relay/channel/vertex/adaptor.go index a424cb1a..91a7f88c 100644 --- a/relay/channel/vertex/adaptor.go +++ b/relay/channel/vertex/adaptor.go @@ -37,6 +37,7 @@ var claudeModelMap = map[string]string{ "claude-sonnet-4-20250514": "claude-sonnet-4@20250514", "claude-opus-4-20250514": "claude-opus-4@20250514", "claude-opus-4-1-20250805": "claude-opus-4-1@20250805", + "claude-sonnet-4-5-20250929": "claude-sonnet-4-5@20250929", } const anthropicVersion = "vertex-2023-10-16" diff --git a/relay/common/relay_info.go b/relay/common/relay_info.go index 99925dc5..f4ffaee2 100644 --- a/relay/common/relay_info.go +++ b/relay/common/relay_info.go @@ -105,7 +105,8 @@ type RelayInfo struct { UserQuota int RelayFormat types.RelayFormat SendResponseCount int - FinalPreConsumedQuota int // 最终预消耗的配额 + FinalPreConsumedQuota int // 最终预消耗的配额 + IsClaudeBetaQuery bool // /v1/messages?beta=true PriceData types.PriceData @@ -279,6 +280,9 @@ func GenRelayInfoClaude(c *gin.Context, request dto.Request) *RelayInfo { info.ClaudeConvertInfo = &ClaudeConvertInfo{ LastMessagesType: LastMessageTypeNone, } + if c.Query("beta") == "true" { + info.IsClaudeBetaQuery = true + } return info } diff --git a/relay/relay_adaptor.go b/relay/relay_adaptor.go index 0c271210..406074c5 100644 --- a/relay/relay_adaptor.go +++ b/relay/relay_adaptor.go @@ -37,7 +37,7 @@ import ( "one-api/relay/channel/zhipu" "one-api/relay/channel/zhipu_4v" "strconv" - + "one-api/relay/channel/submodel" "github.com/gin-gonic/gin" ) @@ -103,6 +103,8 @@ func GetAdaptor(apiType int) channel.Adaptor { return &jimeng.Adaptor{} case constant.APITypeMoonshot: return &moonshot.Adaptor{} // Moonshot uses Claude API + case constant.APITypeSubmodel: + return &submodel.Adaptor{} } return nil } diff --git a/service/http_client.go b/service/http_client.go index d8fcfae0..c1d6880c 100644 --- a/service/http_client.go +++ b/service/http_client.go @@ -110,6 +110,6 @@ func NewProxyHttpClient(proxyURL string) (*http.Client, error) { return client, nil default: - return nil, fmt.Errorf("unsupported proxy scheme: %s", parsedURL.Scheme) + return nil, fmt.Errorf("unsupported proxy scheme: %s, must be http, https, socks5 or socks5h", parsedURL.Scheme) } } diff --git a/setting/ratio_setting/cache_ratio.go b/setting/ratio_setting/cache_ratio.go index 5993cdee..8e4b227a 100644 --- a/setting/ratio_setting/cache_ratio.go +++ b/setting/ratio_setting/cache_ratio.go @@ -52,6 +52,8 @@ var defaultCacheRatio = map[string]float64{ "claude-opus-4-20250514-thinking": 0.1, "claude-opus-4-1-20250805": 0.1, "claude-opus-4-1-20250805-thinking": 0.1, + "claude-sonnet-4-5-20250929": 0.1, + "claude-sonnet-4-5-20250929-thinking": 0.1, } var defaultCreateCacheRatio = map[string]float64{ @@ -69,6 +71,8 @@ var defaultCreateCacheRatio = map[string]float64{ "claude-opus-4-20250514-thinking": 1.25, "claude-opus-4-1-20250805": 1.25, "claude-opus-4-1-20250805-thinking": 1.25, + "claude-sonnet-4-5-20250929": 1.25, + "claude-sonnet-4-5-20250929-thinking": 1.25, } //var defaultCreateCacheRatio = map[string]float64{} diff --git a/setting/ratio_setting/model_ratio.go b/setting/ratio_setting/model_ratio.go index 362c6fa1..0c073718 100644 --- a/setting/ratio_setting/model_ratio.go +++ b/setting/ratio_setting/model_ratio.go @@ -141,6 +141,7 @@ var defaultModelRatio = map[string]float64{ "claude-3-7-sonnet-20250219": 1.5, "claude-3-7-sonnet-20250219-thinking": 1.5, "claude-sonnet-4-20250514": 1.5, + "claude-sonnet-4-5-20250929": 1.5, "claude-3-opus-20240229": 7.5, // $15 / 1M tokens "claude-opus-4-20250514": 7.5, "claude-opus-4-1-20250805": 7.5, @@ -251,6 +252,17 @@ var defaultModelRatio = map[string]float64{ "grok-vision-beta": 2.5, "grok-3-fast-beta": 2.5, "grok-3-mini-fast-beta": 0.3, + // submodel + "NousResearch/Hermes-4-405B-FP8": 0.8, + "Qwen/Qwen3-235B-A22B-Thinking-2507": 0.6, + "Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8": 0.8, + "Qwen/Qwen3-235B-A22B-Instruct-2507": 0.3, + "zai-org/GLM-4.5-FP8": 0.8, + "openai/gpt-oss-120b": 0.5, + "deepseek-ai/DeepSeek-R1-0528": 0.8, + "deepseek-ai/DeepSeek-R1": 0.8, + "deepseek-ai/DeepSeek-V3-0324": 0.8, + "deepseek-ai/DeepSeek-V3.1": 0.8, } var defaultModelPrice = map[string]float64{ @@ -501,7 +513,6 @@ func GetCompletionRatio(name string) float64 { } func getHardcodedCompletionModelRatio(name string) (float64, bool) { - lowercaseName := strings.ToLower(name) isReservedModel := strings.HasSuffix(name, "-all") || strings.HasSuffix(name, "-gizmo-*") if isReservedModel { @@ -594,9 +605,6 @@ func getHardcodedCompletionModelRatio(name string) (float64, bool) { } } // hint 只给官方上4倍率,由于开源模型供应商自行定价,不对其进行补全倍率进行强制对齐 - if lowercaseName == "deepseek-chat" || lowercaseName == "deepseek-reasoner" { - return 4, true - } if strings.HasPrefix(name, "ERNIE-Speed-") { return 2, true } else if strings.HasPrefix(name, "ERNIE-Lite-") { diff --git a/web/src/components/layout/SiderBar.jsx b/web/src/components/layout/SiderBar.jsx index 793e4835..39d6d448 100644 --- a/web/src/components/layout/SiderBar.jsx +++ b/web/src/components/layout/SiderBar.jsx @@ -58,7 +58,7 @@ const SiderBar = ({ onNavigate = () => {} }) => { loading: sidebarLoading, } = useSidebar(); - const showSkeleton = useMinimumLoadingTime(sidebarLoading); + const showSkeleton = useMinimumLoadingTime(sidebarLoading, 200); const [selectedKeys, setSelectedKeys] = useState(['home']); const [chatItems, setChatItems] = useState([]); diff --git a/web/src/components/layout/headerbar/LanguageSelector.jsx b/web/src/components/layout/headerbar/LanguageSelector.jsx index cbfd69b3..17bfe5c5 100644 --- a/web/src/components/layout/headerbar/LanguageSelector.jsx +++ b/web/src/components/layout/headerbar/LanguageSelector.jsx @@ -20,7 +20,7 @@ For commercial licensing, please contact support@quantumnous.com import React from 'react'; import { Button, Dropdown } from '@douyinfe/semi-ui'; import { Languages } from 'lucide-react'; -import { CN, GB } from 'country-flag-icons/react/3x2'; +import { CN, GB, FR } from 'country-flag-icons/react/3x2'; const LanguageSelector = ({ currentLang, onLanguageChange, t }) => { return ( @@ -42,12 +42,19 @@ const LanguageSelector = ({ currentLang, onLanguageChange, t }) => { English + onLanguageChange('fr')} + className={`!flex !items-center !gap-2 !px-3 !py-1.5 !text-sm !text-semi-color-text-0 dark:!text-gray-200 ${currentLang === 'fr' ? '!bg-semi-color-primary-light-default dark:!bg-blue-600 !font-semibold' : 'hover:!bg-semi-color-fill-1 dark:hover:!bg-gray-600'}`} + > + + Français + } >