mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-03-30 08:16:43 +00:00
Merge branch 'main-upstream' into feature/passkey
# Conflicts: # web/src/components/settings/PersonalSetting.jsx # web/src/i18n/locales/en.json # web/src/i18n/locales/zh.json
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<p align="right">
|
||||
<a href="./README.md">中文</a> | <strong>English</strong>
|
||||
<a href="./README.md">中文</a> | <strong>English</strong> | <a href="./README.fr.md">Français</a>
|
||||
</p>
|
||||
<div align="center">
|
||||
|
||||
|
||||
216
README.fr.md
Normal file
216
README.fr.md
Normal file
@@ -0,0 +1,216 @@
|
||||
<p align="right">
|
||||
<a href="./README.md">中文</a> | <a href="./README.en.md">English</a> | <strong>Français</strong>
|
||||
</p>
|
||||
<div align="center">
|
||||
|
||||

|
||||
|
||||
# New API
|
||||
|
||||
🍥 Passerelle de modèles étendus de nouvelle génération et système de gestion d'actifs d'IA
|
||||
|
||||
<a href="https://trendshift.io/repositories/8227" target="_blank"><img src="https://trendshift.io/api/badge/repositories/8227" alt="Calcium-Ion%2Fnew-api | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://raw.githubusercontent.com/Calcium-Ion/new-api/main/LICENSE">
|
||||
<img src="https://img.shields.io/github/license/Calcium-Ion/new-api?color=brightgreen" alt="licence">
|
||||
</a>
|
||||
<a href="https://github.com/Calcium-Ion/new-api/releases/latest">
|
||||
<img src="https://img.shields.io/github/v/release/Calcium-Ion/new-api?color=brightgreen&include_prereleases" alt="version">
|
||||
</a>
|
||||
<a href="https://github.com/users/Calcium-Ion/packages/container/package/new-api">
|
||||
<img src="https://img.shields.io/badge/docker-ghcr.io-blue" alt="docker">
|
||||
</a>
|
||||
<a href="https://hub.docker.com/r/CalciumIon/new-api">
|
||||
<img src="https://img.shields.io/badge/docker-dockerHub-blue" alt="docker">
|
||||
</a>
|
||||
<a href="https://goreportcard.com/report/github.com/Calcium-Ion/new-api">
|
||||
<img src="https://goreportcard.com/badge/github.com/Calcium-Ion/new-api" alt="GoReportCard">
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
## 📝 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.
|
||||
|
||||
<h2>🤝 Partenaires de confiance</h2>
|
||||
<p id="premium-sponsors"> </p>
|
||||
<p align="center"><strong>Sans ordre particulier</strong></p>
|
||||
<p align="center">
|
||||
<a href="https://www.cherry-ai.com/" target=_blank><img
|
||||
src="./docs/images/cherry-studio.png" alt="Cherry Studio" height="120"
|
||||
/></a>
|
||||
<a href="https://bda.pku.edu.cn/" target=_blank><img
|
||||
src="./docs/images/pku.png" alt="Université de Pékin" height="120"
|
||||
/></a>
|
||||
<a href="https://www.compshare.cn/?ytag=GPU_yy_gh_newapi" target=_blank><img
|
||||
src="./docs/images/ucloud.png" alt="UCloud" height="120"
|
||||
/></a>
|
||||
<a href="https://www.aliyun.com/" target=_blank><img
|
||||
src="./docs/images/aliyun.png" alt="Alibaba Cloud" height="120"
|
||||
/></a>
|
||||
<a href="https://io.net/" target=_blank><img
|
||||
src="./docs/images/io-net.png" alt="IO.NET" height="120"
|
||||
/></a>
|
||||
</p>
|
||||
<p> </p>
|
||||
|
||||
## 📚 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 :
|
||||
[](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
|
||||
|
||||
[](https://star-history.com/#Calcium-Ion/new-api&Date)
|
||||
@@ -1,5 +1,5 @@
|
||||
<p align="right">
|
||||
<strong>中文</strong> | <a href="./README.en.md">English</a>
|
||||
<strong>中文</strong> | <a href="./README.en.md">English</a> | <a href="./README.fr.md">Français</a>
|
||||
</p>
|
||||
<div align="center">
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ func TelegramBind(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(302, "/setting")
|
||||
c.Redirect(302, "/console/personal")
|
||||
}
|
||||
|
||||
func TelegramLogin(c *gin.Context) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
5
main.go
5
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.")
|
||||
}
|
||||
}
|
||||
|
||||
// 加载环境变量
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
86
relay/channel/submodel/adaptor.go
Normal file
86
relay/channel/submodel/adaptor.go
Normal file
@@ -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
|
||||
}
|
||||
16
relay/channel/submodel/constants.go
Normal file
16
relay/channel/submodel/constants.go
Normal file
@@ -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"
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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{}
|
||||
|
||||
@@ -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-") {
|
||||
|
||||
@@ -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([]);
|
||||
|
||||
@@ -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 }) => {
|
||||
<GB title='English' className='!w-5 !h-auto' />
|
||||
<span>English</span>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
onClick={() => 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'}`}
|
||||
>
|
||||
<FR title='Français' className='!w-5 !h-auto' />
|
||||
<span>Français</span>
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
icon={<Languages size={18} />}
|
||||
aria-label={t('切换语言')}
|
||||
aria-label={t('common.changeLanguage')}
|
||||
theme='borderless'
|
||||
type='tertiary'
|
||||
className='!p-1.5 !text-current focus:!bg-semi-color-fill-1 dark:focus:!bg-gray-700 !rounded-full !bg-semi-color-fill-0 dark:!bg-semi-color-fill-1 hover:!bg-semi-color-fill-1 dark:hover:!bg-semi-color-fill-2'
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
prepareCredentialCreationOptions,
|
||||
buildRegistrationResult,
|
||||
isPasskeySupported,
|
||||
setUserData,
|
||||
} from '../../helpers';
|
||||
import { UserContext } from '../../context/User';
|
||||
import { Modal } from '@douyinfe/semi-ui';
|
||||
@@ -253,6 +254,7 @@ const PersonalSetting = () => {
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
userDispatch({ type: 'login', payload: data });
|
||||
setUserData(data);
|
||||
await loadPasskeyStatus();
|
||||
} else {
|
||||
showError(message);
|
||||
|
||||
@@ -105,6 +105,7 @@ const MODEL_FETCHABLE_TYPES = new Set([
|
||||
40,
|
||||
42,
|
||||
48,
|
||||
43,
|
||||
]);
|
||||
|
||||
function type2secretPrompt(type) {
|
||||
|
||||
@@ -118,6 +118,9 @@ const EditTagModal = (props) => {
|
||||
case 36:
|
||||
localModels = ['suno_music', 'suno_lyrics'];
|
||||
break;
|
||||
case 53:
|
||||
localModels = ['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'];
|
||||
break;
|
||||
default:
|
||||
localModels = getChannelModels(value);
|
||||
break;
|
||||
|
||||
@@ -159,6 +159,11 @@ export const CHANNEL_OPTIONS = [
|
||||
color: 'purple',
|
||||
label: 'Vidu',
|
||||
},
|
||||
{
|
||||
value: 53,
|
||||
color: 'blue',
|
||||
label: 'SubModel',
|
||||
},
|
||||
];
|
||||
|
||||
export const MODEL_TABLE_PAGE_SIZE = 10;
|
||||
|
||||
@@ -1200,25 +1200,25 @@ export function renderModelPrice(
|
||||
const extraServices = [
|
||||
webSearch && webSearchCallCount > 0
|
||||
? i18next.t(
|
||||
' + Web搜索 {{count}}次 / 1K 次 * ${{price}} * {{ratioType}} {{ratio}}',
|
||||
{
|
||||
count: webSearchCallCount,
|
||||
price: webSearchPrice,
|
||||
ratio: groupRatio,
|
||||
ratioType: ratioLabel,
|
||||
},
|
||||
)
|
||||
' + Web搜索 {{count}}次 / 1K 次 * ${{price}} * {{ratioType}} {{ratio}}',
|
||||
{
|
||||
count: webSearchCallCount,
|
||||
price: webSearchPrice,
|
||||
ratio: groupRatio,
|
||||
ratioType: ratioLabel,
|
||||
},
|
||||
)
|
||||
: '',
|
||||
fileSearch && fileSearchCallCount > 0
|
||||
? i18next.t(
|
||||
' + 文件搜索 {{count}}次 / 1K 次 * ${{price}} * {{ratioType}} {{ratio}}',
|
||||
{
|
||||
count: fileSearchCallCount,
|
||||
price: fileSearchPrice,
|
||||
ratio: groupRatio,
|
||||
ratioType: ratioLabel,
|
||||
},
|
||||
)
|
||||
' + 文件搜索 {{count}}次 / 1K 次 * ${{price}} * {{ratioType}} {{ratio}}',
|
||||
{
|
||||
count: fileSearchCallCount,
|
||||
price: fileSearchPrice,
|
||||
ratio: groupRatio,
|
||||
ratioType: ratioLabel,
|
||||
},
|
||||
)
|
||||
: '',
|
||||
imageGenerationCall && imageGenerationCallPrice > 0
|
||||
? i18next.t(
|
||||
@@ -1398,10 +1398,10 @@ export function renderAudioModelPrice(
|
||||
let audioPrice =
|
||||
(audioInputTokens / 1000000) * inputRatioPrice * audioRatio * groupRatio +
|
||||
(audioCompletionTokens / 1000000) *
|
||||
inputRatioPrice *
|
||||
audioRatio *
|
||||
audioCompletionRatio *
|
||||
groupRatio;
|
||||
inputRatioPrice *
|
||||
audioRatio *
|
||||
audioCompletionRatio *
|
||||
groupRatio;
|
||||
let price = textPrice + audioPrice;
|
||||
return (
|
||||
<>
|
||||
@@ -1457,27 +1457,27 @@ export function renderAudioModelPrice(
|
||||
<p>
|
||||
{cacheTokens > 0
|
||||
? i18next.t(
|
||||
'文字提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}',
|
||||
{
|
||||
nonCacheInput: inputTokens - cacheTokens,
|
||||
cacheInput: cacheTokens,
|
||||
cachePrice: inputRatioPrice * cacheRatio,
|
||||
price: inputRatioPrice,
|
||||
completion: completionTokens,
|
||||
compPrice: completionRatioPrice,
|
||||
total: textPrice.toFixed(6),
|
||||
},
|
||||
)
|
||||
'文字提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}',
|
||||
{
|
||||
nonCacheInput: inputTokens - cacheTokens,
|
||||
cacheInput: cacheTokens,
|
||||
cachePrice: inputRatioPrice * cacheRatio,
|
||||
price: inputRatioPrice,
|
||||
completion: completionTokens,
|
||||
compPrice: completionRatioPrice,
|
||||
total: textPrice.toFixed(6),
|
||||
},
|
||||
)
|
||||
: i18next.t(
|
||||
'文字提示 {{input}} tokens / 1M tokens * ${{price}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}',
|
||||
{
|
||||
input: inputTokens,
|
||||
price: inputRatioPrice,
|
||||
completion: completionTokens,
|
||||
compPrice: completionRatioPrice,
|
||||
total: textPrice.toFixed(6),
|
||||
},
|
||||
)}
|
||||
'文字提示 {{input}} tokens / 1M tokens * ${{price}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}',
|
||||
{
|
||||
input: inputTokens,
|
||||
price: inputRatioPrice,
|
||||
completion: completionTokens,
|
||||
compPrice: completionRatioPrice,
|
||||
total: textPrice.toFixed(6),
|
||||
},
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{i18next.t(
|
||||
@@ -1617,35 +1617,35 @@ export function renderClaudeModelPrice(
|
||||
<p>
|
||||
{cacheTokens > 0 || cacheCreationTokens > 0
|
||||
? i18next.t(
|
||||
'提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 缓存创建 {{cacheCreationInput}} tokens / 1M tokens * ${{cacheCreationPrice}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * {{ratioType}} {{ratio}} = ${{total}}',
|
||||
{
|
||||
nonCacheInput: nonCachedTokens,
|
||||
cacheInput: cacheTokens,
|
||||
cacheRatio: cacheRatio,
|
||||
cacheCreationInput: cacheCreationTokens,
|
||||
cacheCreationRatio: cacheCreationRatio,
|
||||
cachePrice: cacheRatioPrice,
|
||||
cacheCreationPrice: cacheCreationRatioPrice,
|
||||
price: inputRatioPrice,
|
||||
completion: completionTokens,
|
||||
compPrice: completionRatioPrice,
|
||||
ratio: groupRatio,
|
||||
ratioType: ratioLabel,
|
||||
total: price.toFixed(6),
|
||||
},
|
||||
)
|
||||
'提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 缓存创建 {{cacheCreationInput}} tokens / 1M tokens * ${{cacheCreationPrice}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * {{ratioType}} {{ratio}} = ${{total}}',
|
||||
{
|
||||
nonCacheInput: nonCachedTokens,
|
||||
cacheInput: cacheTokens,
|
||||
cacheRatio: cacheRatio,
|
||||
cacheCreationInput: cacheCreationTokens,
|
||||
cacheCreationRatio: cacheCreationRatio,
|
||||
cachePrice: cacheRatioPrice,
|
||||
cacheCreationPrice: cacheCreationRatioPrice,
|
||||
price: inputRatioPrice,
|
||||
completion: completionTokens,
|
||||
compPrice: completionRatioPrice,
|
||||
ratio: groupRatio,
|
||||
ratioType: ratioLabel,
|
||||
total: price.toFixed(6),
|
||||
},
|
||||
)
|
||||
: i18next.t(
|
||||
'提示 {{input}} tokens / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * {{ratioType}} {{ratio}} = ${{total}}',
|
||||
{
|
||||
input: inputTokens,
|
||||
price: inputRatioPrice,
|
||||
completion: completionTokens,
|
||||
compPrice: completionRatioPrice,
|
||||
ratio: groupRatio,
|
||||
ratioType: ratioLabel,
|
||||
total: price.toFixed(6),
|
||||
},
|
||||
)}
|
||||
'提示 {{input}} tokens / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * {{ratioType}} {{ratio}} = ${{total}}',
|
||||
{
|
||||
input: inputTokens,
|
||||
price: inputRatioPrice,
|
||||
completion: completionTokens,
|
||||
compPrice: completionRatioPrice,
|
||||
ratio: groupRatio,
|
||||
ratioType: ratioLabel,
|
||||
total: price.toFixed(6),
|
||||
},
|
||||
)}
|
||||
</p>
|
||||
<p>{i18next.t('仅供参考,以实际扣费为准')}</p>
|
||||
</article>
|
||||
|
||||
@@ -40,7 +40,7 @@ export const useHeaderBar = ({ onMobileMenuToggle, drawerOpen }) => {
|
||||
const location = useLocation();
|
||||
|
||||
const loading = statusState?.status === undefined;
|
||||
const isLoading = useMinimumLoadingTime(loading);
|
||||
const isLoading = useMinimumLoadingTime(loading, 200);
|
||||
|
||||
const systemName = getSystemName();
|
||||
const logo = getLogo();
|
||||
|
||||
@@ -17,7 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useMemo, useContext } from 'react';
|
||||
import { useState, useEffect, useMemo, useContext, useRef } from 'react';
|
||||
import { StatusContext } from '../../context/Status';
|
||||
import { API } from '../../helpers';
|
||||
|
||||
@@ -29,6 +29,13 @@ export const useSidebar = () => {
|
||||
const [statusState] = useContext(StatusContext);
|
||||
const [userConfig, setUserConfig] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const instanceIdRef = useRef(null);
|
||||
const hasLoadedOnceRef = useRef(false);
|
||||
|
||||
if (!instanceIdRef.current) {
|
||||
const randomPart = Math.random().toString(16).slice(2);
|
||||
instanceIdRef.current = `sidebar-${Date.now()}-${randomPart}`;
|
||||
}
|
||||
|
||||
// 默认配置
|
||||
const defaultAdminConfig = {
|
||||
@@ -74,9 +81,17 @@ export const useSidebar = () => {
|
||||
}, [statusState?.status?.SidebarModulesAdmin]);
|
||||
|
||||
// 加载用户配置的通用方法
|
||||
const loadUserConfig = async () => {
|
||||
const loadUserConfig = async ({ withLoading } = {}) => {
|
||||
const shouldShowLoader =
|
||||
typeof withLoading === 'boolean'
|
||||
? withLoading
|
||||
: !hasLoadedOnceRef.current;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
if (shouldShowLoader) {
|
||||
setLoading(true);
|
||||
}
|
||||
|
||||
const res = await API.get('/api/user/self');
|
||||
if (res.data.success && res.data.data.sidebar_modules) {
|
||||
let config;
|
||||
@@ -122,18 +137,25 @@ export const useSidebar = () => {
|
||||
});
|
||||
setUserConfig(defaultUserConfig);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
if (shouldShowLoader) {
|
||||
setLoading(false);
|
||||
}
|
||||
hasLoadedOnceRef.current = true;
|
||||
}
|
||||
};
|
||||
|
||||
// 刷新用户配置的方法(供外部调用)
|
||||
const refreshUserConfig = async () => {
|
||||
if (Object.keys(adminConfig).length > 0) {
|
||||
await loadUserConfig();
|
||||
if (Object.keys(adminConfig).length > 0) {
|
||||
await loadUserConfig({ withLoading: false });
|
||||
}
|
||||
|
||||
// 触发全局刷新事件,通知所有useSidebar实例更新
|
||||
sidebarEventTarget.dispatchEvent(new CustomEvent(SIDEBAR_REFRESH_EVENT));
|
||||
sidebarEventTarget.dispatchEvent(
|
||||
new CustomEvent(SIDEBAR_REFRESH_EVENT, {
|
||||
detail: { sourceId: instanceIdRef.current, skipLoader: true },
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
// 加载用户配置
|
||||
@@ -146,9 +168,15 @@ export const useSidebar = () => {
|
||||
|
||||
// 监听全局刷新事件
|
||||
useEffect(() => {
|
||||
const handleRefresh = () => {
|
||||
const handleRefresh = (event) => {
|
||||
if (event?.detail?.sourceId === instanceIdRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Object.keys(adminConfig).length > 0) {
|
||||
loadUserConfig();
|
||||
loadUserConfig({
|
||||
withLoading: event?.detail?.skipLoader ? false : undefined,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import { initReactI18next } from 'react-i18next';
|
||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
|
||||
import enTranslation from './locales/en.json';
|
||||
import frTranslation from './locales/fr.json';
|
||||
import zhTranslation from './locales/zh.json';
|
||||
|
||||
i18n
|
||||
@@ -36,6 +37,9 @@ i18n
|
||||
zh: {
|
||||
translation: zhTranslation,
|
||||
},
|
||||
fr: {
|
||||
translation: frTranslation,
|
||||
},
|
||||
},
|
||||
fallbackLng: 'zh',
|
||||
interpolation: {
|
||||
|
||||
@@ -1406,6 +1406,7 @@
|
||||
"Claude思考适配 BudgetTokens = MaxTokens * BudgetTokens 百分比": "Claude thinking adaptation BudgetTokens = MaxTokens * BudgetTokens percentage",
|
||||
"思考适配 BudgetTokens 百分比": "Thinking adaptation BudgetTokens percentage",
|
||||
"0.1-1之间的小数": "Decimal between 0.1 and 1",
|
||||
"0.1以上的小数": "Decimal above 0.1",
|
||||
"模型相关设置": "Model related settings",
|
||||
"收起侧边栏": "Collapse sidebar",
|
||||
"展开侧边栏": "Expand sidebar",
|
||||
@@ -2185,5 +2186,9 @@
|
||||
"允许的 Origins": "Allowed Origins",
|
||||
"留空将自动使用服务器地址,多个 Origin 用于支持多域名部署": "Leave blank to auto-use server address, multiple Origins for multi-domain deployment",
|
||||
"输入 Origin 后回车,如:https://example.com": "Enter Origin and press Enter, e.g.: https://example.com",
|
||||
"保存 Passkey 设置": "Save Passkey Settings"
|
||||
"保存 Passkey 设置": "Save Passkey Settings",
|
||||
"黑名单": "Blacklist",
|
||||
"common": {
|
||||
"changeLanguage": "Change Language"
|
||||
}
|
||||
}
|
||||
|
||||
2141
web/src/i18n/locales/fr.json
Normal file
2141
web/src/i18n/locales/fr.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -34,6 +34,9 @@
|
||||
"输入端口后回车,如:80 或 8000-8999": "输入端口后回车,如:80 或 8000-8999",
|
||||
"更新SSRF防护设置": "更新SSRF防护设置",
|
||||
"域名IP过滤详细说明": "⚠️此功能为实验性选项,域名可能解析到多个 IPv4/IPv6 地址,若开启,请确保 IP 过滤列表覆盖这些地址,否则可能导致访问失败。",
|
||||
"common": {
|
||||
"changeLanguage": "切换语言"
|
||||
},
|
||||
"允许在 Stripe 支付中输入促销码": "允许在 Stripe 支付中输入促销码",
|
||||
"Passkey 认证": "Passkey 认证",
|
||||
"已启用 Passkey,可进行无密码认证": "已启用 Passkey,可进行无密码认证",
|
||||
|
||||
@@ -202,9 +202,8 @@ export default function SettingClaudeModel(props) {
|
||||
label={t('思考适配 BudgetTokens 百分比')}
|
||||
field={'claude.thinking_adapter_budget_tokens_percentage'}
|
||||
initValue={''}
|
||||
extraText={t('0.1-1之间的小数')}
|
||||
extraText={t('0.1以上的小数')}
|
||||
min={0.1}
|
||||
max={1}
|
||||
onChange={(value) =>
|
||||
setInputs({
|
||||
...inputs,
|
||||
|
||||
Reference in New Issue
Block a user