diff --git a/.github/workflows/docker-ghcr.yml b/.github/workflows/docker-ghcr.yml new file mode 100644 index 000000000000..b3b4770ca6f7 --- /dev/null +++ b/.github/workflows/docker-ghcr.yml @@ -0,0 +1,176 @@ +name: Publish Docker image to GHCR + +# 基于官方 docker-image-branch.yml 改造: +# - 登录从 Docker Hub 改为 GitHub Container Registry (GITHUB_TOKEN 自动认证) +# - 镜像名从 calciumion/new-api 改为 ghcr.io/${{ github.repository }} +# - 触发:推送到 main 分支自动构建(也支持手动 workflow_dispatch) +# - 去掉 cosign 签名(fork 自用不需要) +# 多架构构建逻辑(单架构 build + manifest 合并)与官方一致。 + +on: + push: + branches: [main] + workflow_dispatch: + inputs: + branch: + description: "Branch name to build (默认 main)" + required: false + default: main + type: string + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + prepare: + name: Prepare Docker tags + runs-on: ubuntu-latest + outputs: + branch: ${{ steps.version.outputs.branch }} + sha: ${{ steps.version.outputs.sha }} + tag_prefix: ${{ steps.version.outputs.tag_prefix }} + version: ${{ steps.version.outputs.version }} + permissions: + contents: read + steps: + - name: Check out branch + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + fetch-depth: 1 + ref: ${{ inputs.branch || github.ref }} + + - name: Resolve Docker tags + id: version + env: + BRANCH_NAME: ${{ inputs.branch || github.ref_name }} + run: | + TAG_PREFIX=$(printf '%s' "$BRANCH_NAME" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9_.-]+/-/g; s/^[.-]+//; s/[.-]+$//') + TAG_PREFIX=${TAG_PREFIX:0:105} + TAG_PREFIX=$(printf '%s' "$TAG_PREFIX" | sed -E 's/[.-]+$//') + if [ -z "$TAG_PREFIX" ]; then + echo "::error::Branch '$BRANCH_NAME' cannot be converted to a valid Docker tag prefix" + exit 1 + fi + + SHA=$(git rev-parse HEAD) + SHORT_SHA=$(git rev-parse --short HEAD) + VERSION="${TAG_PREFIX}-$(date +'%Y%m%d')-${SHORT_SHA}" + + echo "branch=$BRANCH_NAME" >> "$GITHUB_OUTPUT" + echo "sha=$SHA" >> "$GITHUB_OUTPUT" + echo "tag_prefix=$TAG_PREFIX" >> "$GITHUB_OUTPUT" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "Prepared Docker tags for $BRANCH_NAME at $SHORT_SHA" + + build_single_arch: + name: Build & push (${{ matrix.arch }}) [native] + needs: [prepare] + strategy: + fail-fast: false + matrix: + include: + - arch: amd64 + platform: linux/amd64 + runner: ubuntu-latest + - arch: arm64 + platform: linux/arm64 + runner: ubuntu-24.04-arm + runs-on: ${{ matrix.runner }} + permissions: + contents: read + packages: write + steps: + - name: Check out branch + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + fetch-depth: 1 + ref: ${{ needs.prepare.outputs.sha }} + + - name: Write VERSION + run: | + echo "${{ needs.prepare.outputs.version }}" > VERSION + echo "Publishing version: ${{ needs.prepare.outputs.version }} for ${{ matrix.arch }}" + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 + + - name: Log in to GHCR + uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4.3.0 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (labels) + id: meta + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + + - name: Build & push single-arch + id: build + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 + with: + context: . + platforms: ${{ matrix.platform }} + push: true + tags: | + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.prepare.outputs.tag_prefix }}-${{ matrix.arch }} + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.prepare.outputs.version }}-${{ matrix.arch }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Output digest + run: | + echo "### Docker Image Digest (${{ matrix.arch }})" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.prepare.outputs.tag_prefix }}-${{ matrix.arch }}" >> $GITHUB_STEP_SUMMARY + echo "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.prepare.outputs.version }}-${{ matrix.arch }}" >> $GITHUB_STEP_SUMMARY + echo "${{ steps.build.outputs.digest }}" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + + create_manifests: + name: Create multi-arch manifests (GHCR) + needs: [prepare, build_single_arch] + runs-on: ubuntu-latest + permissions: + packages: write + steps: + - name: Log in to GHCR + uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4.3.0 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Create & push manifest (GHCR - branch) + run: | + docker buildx imagetools create \ + -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.prepare.outputs.tag_prefix }} \ + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.prepare.outputs.tag_prefix }}-amd64 \ + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.prepare.outputs.tag_prefix }}-arm64 + + - name: Create & push manifest (GHCR - versioned) + run: | + docker buildx imagetools create \ + -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.prepare.outputs.version }} \ + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.prepare.outputs.version }}-amd64 \ + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.prepare.outputs.version }}-arm64 + + - name: Create & push manifest (latest) + run: | + docker buildx imagetools create \ + -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest \ + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.prepare.outputs.tag_prefix }}-amd64 \ + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.prepare.outputs.tag_prefix }}-arm64 + + - name: Output manifest digest + run: | + echo "### Multi-arch Manifest Digests" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.prepare.outputs.tag_prefix }} >> $GITHUB_STEP_SUMMARY + echo "---" >> $GITHUB_STEP_SUMMARY + docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY diff --git a/common/api_type.go b/common/api_type.go index 44841e11b8fc..7e8ef4f7cc52 100644 --- a/common/api_type.go +++ b/common/api_type.go @@ -81,6 +81,8 @@ func ChannelType2APIType(channelType int) (int, bool) { apiType = constant.APITypeSub2API case constant.ChannelTypeNewAPI: apiType = constant.APITypeNewAPI + case constant.ChannelTypeBaichuan: + apiType = constant.APITypeBaichuan } if apiType == -1 { return constant.APITypeOpenAI, false diff --git a/constant/api_type.go b/constant/api_type.go index 2a561c6d2bfd..f649c3336d3f 100644 --- a/constant/api_type.go +++ b/constant/api_type.go @@ -39,5 +39,6 @@ const ( APITypeAdvancedCustom APITypeSub2API APITypeNewAPI + APITypeBaichuan 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 2a6c4a31c138..371664f88835 100644 --- a/constant/channel.go +++ b/constant/channel.go @@ -58,6 +58,7 @@ const ( ChannelTypeAdvancedCustom = 58 ChannelTypeSub2API = 59 ChannelTypeNewAPI = 60 + ChannelTypeBaichuan = 61 ChannelTypeDummy // this one is only for count, do not add any channel after this ) @@ -124,6 +125,7 @@ var ChannelBaseURLs = []string{ "", //58 "", //59 "", //60 + "https://api.baichuan-ai.com", //61 } var ChannelTypeNames = map[int]string{ @@ -184,6 +186,7 @@ var ChannelTypeNames = map[int]string{ ChannelTypeAdvancedCustom: "Advanced Custom", ChannelTypeSub2API: "Sub2API", ChannelTypeNewAPI: "New API", + ChannelTypeBaichuan: "Baichuan", } func GetChannelTypeName(channelType int) string { diff --git a/controller/channel-test.go b/controller/channel-test.go index f6e6bd7f1163..9258f30475c2 100644 --- a/controller/channel-test.go +++ b/controller/channel-test.go @@ -692,6 +692,24 @@ func detectErrorMessageFromJSONBytes(jsonBytes []byte) string { return message } +// useMaxCompletionTokens reports whether the channel test request should send +// max_completion_tokens instead of max_tokens for the given model. The rule is +// kept in sync with the relay-side conversion in relay/channel/openai/adaptor.go +// (o-series and gpt-5-series always use max_completion_tokens). Azure OpenAI +// additionally rejects max_tokens for the gpt-4o / gpt-4.1 family, so those are +// routed through max_completion_tokens as well when the channel is Azure. +func useMaxCompletionTokens(model string, channel *model.Channel) bool { + if dto.IsOpenAIReasoningOModel(model) || dto.IsOpenAIGPT5Model(model) { + return true + } + if channel != nil && channel.Type == constant.ChannelTypeAzure { + if strings.HasPrefix(model, "gpt-4o") || strings.HasPrefix(model, "gpt-4.1") { + return true + } + } + return false +} + func buildTestRequest(model string, endpointType string, channel *model.Channel, isStream bool) dto.Request { testResponsesInput := json.RawMessage(`[{"role":"user","content":"hi"}]`) @@ -735,10 +753,6 @@ func buildTestRequest(model string, endpointType string, channel *model.Channel, } case constant.EndpointTypeAnthropic, constant.EndpointTypeGemini, constant.EndpointTypeOpenAI: // 返回 GeneralOpenAIRequest - maxTokens := uint(16) - if constant.EndpointType(endpointType) == constant.EndpointTypeGemini { - maxTokens = 3000 - } req := &dto.GeneralOpenAIRequest{ Model: model, Stream: lo.ToPtr(isStream), @@ -748,11 +762,20 @@ func buildTestRequest(model string, endpointType string, channel *model.Channel, Content: "hi", }, }, - MaxTokens: lo.ToPtr(maxTokens), } if isStream { req.StreamOptions = &dto.StreamOptions{IncludeUsage: true} } + // Gemini 期望较大的 max_tokens;其余模型默认 16。对要求 + // max_completion_tokens 的模型(o 系列 / gpt-5 系列,以及 Azure 上 + // 的 gpt-4o / gpt-4.1 系列),改用 MaxCompletionTokens。 + if constant.EndpointType(endpointType) == constant.EndpointTypeGemini { + req.MaxTokens = lo.ToPtr(uint(3000)) + } else if useMaxCompletionTokens(model, channel) { + req.MaxCompletionTokens = lo.ToPtr(uint(16)) + } else { + req.MaxTokens = lo.ToPtr(uint(16)) + } return req } } @@ -810,7 +833,12 @@ func buildTestRequest(model string, endpointType string, channel *model.Channel, testRequest.StreamOptions = &dto.StreamOptions{IncludeUsage: true} } - if dto.IsOpenAIReasoningOModel(model) { + // Determine which max-tokens parameter to use. This must stay aligned with + // the relay-side conversion in relay/channel/openai/adaptor.go (o-series and + // gpt-5-series use max_completion_tokens). Azure additionally rejects + // max_tokens for the gpt-4o / gpt-4.1 family, so route those through + // max_completion_tokens too when the channel is Azure. + if useMaxCompletionTokens(model, channel) { testRequest.MaxCompletionTokens = lo.ToPtr(uint(16)) } else if strings.Contains(model, "thinking") { if !strings.Contains(model, "claude") { diff --git a/controller/channel_upstream_update.go b/controller/channel_upstream_update.go index 71ab0e53fafe..46b1ec33dda6 100644 --- a/controller/channel_upstream_update.go +++ b/controller/channel_upstream_update.go @@ -412,6 +412,42 @@ func fetchChannelUpstreamModelIDs(channel *model.Channel) ([]string, error) { return nil, sanitizeFetchModelsError(err, key) } + // Baichuan returns model names under data[].model instead of the OpenAI-standard data[].id + if channel.Type == constant.ChannelTypeBaichuan { + var baichuanResult struct { + Data []struct { + Model string `json:"model"` + } `json:"data"` + } + if err := common.Unmarshal(body, &baichuanResult); err != nil { + return nil, err + } + ids := lo.Map(baichuanResult.Data, func(item struct { + Model string `json:"model"` + }, _ int) string { + return item.Model + }) + return normalizeModelNames(ids), nil + } + + // Cohere returns {models: [{name, endpoints, ...}]} instead of {data: [{id}]}. Import all names. + if channel.Type == constant.ChannelTypeCohere { + var cohereResult struct { + Models []struct { + Name string `json:"name"` + } `json:"models"` + } + if err := common.Unmarshal(body, &cohereResult); err != nil { + return nil, err + } + ids := lo.Map(cohereResult.Models, func(item struct { + Name string `json:"name"` + }, _ int) string { + return item.Name + }) + return normalizeModelNames(ids), nil + } + var result OpenAIModelsResponse if err := common.Unmarshal(body, &result); err != nil { return nil, err diff --git a/relay/channel/openai/adaptor.go b/relay/channel/openai/adaptor.go index 4f1c42863dba..93caa976a005 100644 --- a/relay/channel/openai/adaptor.go +++ b/relay/channel/openai/adaptor.go @@ -153,17 +153,26 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) { return relaycommon.GetFullRequestURL(info.ChannelBaseUrl, requestURL, info.ChannelType), nil } - model_ := info.UpstreamModelName - // 2025年5月10日后创建的渠道不移除. - if info.ChannelCreateTime < constant.AzureNoRemoveDotTime { - model_ = strings.Replace(model_, ".", "", -1) - } - // https://github.com/songquanpeng/one-api/issues/67 - requestURL = fmt.Sprintf("/openai/deployments/%s/%s", model_, task) - if info.RelayMode == relayconstant.RelayModeRealtime { - requestURL = fmt.Sprintf("/openai/realtime?deployment=%s&api-version=%s", model_, apiVersion) - } - return relaycommon.GetFullRequestURL(info.ChannelBaseUrl, requestURL, info.ChannelType), nil + model_ := info.UpstreamModelName + // 2025年5月10日后创建的渠道不移除. + if info.ChannelCreateTime < constant.AzureNoRemoveDotTime { + model_ = strings.Replace(model_, ".", "", -1) + } + // 新版 Azure AI Foundry 资源使用标准 OpenAI 路径(/openai/v1/chat/completions), + // 模型名放在请求体里而非 URL,也不需要 api-version 查询参数。 + // 判断依据:用户在端点 URL 末尾填了 /openai(如 https://xxx.services.ai.azure.com/openai), + // 则走新版分支——把请求路径(/v1/chat/completions)直接追加到 base URL 之后。 + // 经典 Azure OpenAI 资源(端点 URL 只填域名)仍走 deployment 路径。 + baseUrl := strings.TrimRight(info.ChannelBaseUrl, "/") + if strings.HasSuffix(baseUrl, "/openai") { + return relaycommon.GetFullRequestURL(baseUrl, info.RequestURLPath, info.ChannelType), nil + } + // https://github.com/songquanpeng/one-api/issues/67 + requestURL = fmt.Sprintf("/openai/deployments/%s/%s", model_, task) + if info.RelayMode == relayconstant.RelayModeRealtime { + requestURL = fmt.Sprintf("/openai/realtime?deployment=%s&api-version=%s", model_, apiVersion) + } + return relaycommon.GetFullRequestURL(info.ChannelBaseUrl, requestURL, info.ChannelType), nil //case constant.ChannelTypeMiniMax: // return minimax.GetRequestURL(info) case constant.ChannelTypeCustom: diff --git a/relay/common/relay_info.go b/relay/common/relay_info.go index 45ae30bf9fe8..0236f776db84 100644 --- a/relay/common/relay_info.go +++ b/relay/common/relay_info.go @@ -348,6 +348,7 @@ var streamSupportedChannels = map[int]bool{ constant.ChannelTypeSub2API: true, constant.ChannelTypeNewAPI: true, constant.ChannelTypeTencent: true, + constant.ChannelTypeBaichuan: true, } func GenRelayInfoWs(c *gin.Context, ws *websocket.Conn) *RelayInfo { diff --git a/relay/relay_adaptor.go b/relay/relay_adaptor.go index e6298dc034f3..e3d3a81d5e17 100644 --- a/relay/relay_adaptor.go +++ b/relay/relay_adaptor.go @@ -129,6 +129,8 @@ func GetAdaptor(apiType int) channel.Adaptor { return &sub2api.Adaptor{} case constant.APITypeNewAPI: return &newapi.Adaptor{} + case constant.APITypeBaichuan: + return &openai.Adaptor{} } return nil } diff --git a/web/src/features/channels/constants.ts b/web/src/features/channels/constants.ts index a3cb726d5418..26e0da4eebb7 100644 --- a/web/src/features/channels/constants.ts +++ b/web/src/features/channels/constants.ts @@ -22,6 +22,7 @@ For commercial licensing, please contact support@quantumnous.com // ============================================================================ export const CHANNEL_TYPE_NEW_API = 60 +export const CHANNEL_TYPE_BAICHUAN = 61 export const CHANNEL_TYPES = { 0: 'Unknown', @@ -81,12 +82,13 @@ export const CHANNEL_TYPES = { 58: 'Advanced Custom', 59: 'Sub2API', 60: 'New API', + 61: 'Baichuan', } as const const CHANNEL_TYPE_DISPLAY_ORDER: number[] = [ - 1, 14, 33, 24, 43, 3, 41, 48, 60, 58, 42, 34, 20, 4, 40, 27, 25, 17, 26, 15, - 46, 23, 18, 45, 31, 35, 49, 19, 47, 37, 38, 39, 11, 8, 57, 59, 22, 21, 44, 2, - 5, 36, 50, 51, 52, 53, 54, 55, 56, + 1, 14, 33, 24, 43, 3, 41, 48, 60, 61, 58, 42, 34, 20, 4, 40, 27, 25, 17, 26, + 15, 46, 23, 18, 45, 31, 35, 49, 19, 47, 37, 38, 39, 11, 8, 57, 59, 22, 21, 44, + 2, 5, 36, 50, 51, 52, 53, 54, 55, 56, ] export const CHANNEL_TYPE_OPTIONS: { value: number; label: string }[] = (() => { @@ -389,7 +391,7 @@ export const FIELD_DESCRIPTIONS = { export const MODEL_FETCHABLE_TYPES = new Set([ 1, 4, 14, 17, 20, 23, 24, 25, 26, 27, 31, 34, 35, 40, 42, 43, 47, 48, 57, 58, - 59, 60, + 59, 60, 61, ]) export const TYPE_TO_KEY_PROMPT: Record = { diff --git a/web/src/features/channels/lib/channel-utils.ts b/web/src/features/channels/lib/channel-utils.ts index 9424a8521b6f..16fe39865cf2 100644 --- a/web/src/features/channels/lib/channel-utils.ts +++ b/web/src/features/channels/lib/channel-utils.ts @@ -78,6 +78,7 @@ export function getChannelTypeIcon(type: number): string { 23: 'Hunyuan', // Tencent 19: 'Ai360', // 360 25: 'Moonshot', // Moonshot + 61: 'Baichuan', // Baichuan 31: 'Yi', // LingYiWanWu 35: 'Minimax', // MiniMax 45: 'Volcengine', // VolcEngine diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index b48096a7fcfa..f1bc6abf8752 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -560,6 +560,7 @@ "Badge Color": "Badge Color", "Baidu": "Baidu", "Baidu V2": "Baidu V2", + "Baichuan": "Baichuan", "Balance": "Balance", "Balance and top-up management": "Balance and top-up management", "Balance depleted": "Balance depleted", diff --git a/web/src/i18n/locales/fr.json b/web/src/i18n/locales/fr.json index 638cf9abadd1..f958d6e244df 100644 --- a/web/src/i18n/locales/fr.json +++ b/web/src/i18n/locales/fr.json @@ -560,6 +560,7 @@ "Badge Color": "Couleur du badge", "Baidu": "Baidu", "Baidu V2": "Baidu V2", + "Baichuan": "Baichuan", "Balance": "Solde", "Balance and top-up management": "Gestion du solde et des recharges", "Balance depleted": "Solde épuisé", diff --git a/web/src/i18n/locales/ja.json b/web/src/i18n/locales/ja.json index 3b9d4ce693cc..276558c0f06d 100644 --- a/web/src/i18n/locales/ja.json +++ b/web/src/i18n/locales/ja.json @@ -560,6 +560,7 @@ "Badge Color": "バッジの色", "Baidu": "Baidu", "Baidu V2": "Baidu V 2", + "Baichuan": "Baichuan", "Balance": "残高", "Balance and top-up management": "残高とチャージ管理", "Balance depleted": "残高なし", diff --git a/web/src/i18n/locales/ru.json b/web/src/i18n/locales/ru.json index 853dc874d463..a45f88f93b49 100644 --- a/web/src/i18n/locales/ru.json +++ b/web/src/i18n/locales/ru.json @@ -560,6 +560,7 @@ "Badge Color": "Цвет значка", "Baidu": "Baidu", "Baidu V2": "Baidu V2", + "Baichuan": "Baichuan", "Balance": "Баланс", "Balance and top-up management": "Управление балансом и пополнением", "Balance depleted": "Баланс исчерпан", diff --git a/web/src/i18n/locales/vi.json b/web/src/i18n/locales/vi.json index cdc1a12626e1..9e3fbea221f0 100644 --- a/web/src/i18n/locales/vi.json +++ b/web/src/i18n/locales/vi.json @@ -560,6 +560,7 @@ "Badge Color": "Màu huy hiệu", "Baidu": "Baidu", "Baidu V2": "Baidu V2", + "Baichuan": "Baichuan", "Balance": "Cân bằng", "Balance and top-up management": "Quản lý số dư và nạp tiền", "Balance depleted": "Đã hết số dư", diff --git a/web/src/i18n/locales/zh-TW.json b/web/src/i18n/locales/zh-TW.json index f8b5fafd8d2c..451cc224a7d2 100644 --- a/web/src/i18n/locales/zh-TW.json +++ b/web/src/i18n/locales/zh-TW.json @@ -560,6 +560,7 @@ "Badge Color": "徽章顏色", "Baidu": "百度", "Baidu V2": "百度 V2", + "Baichuan": "百川", "Balance": "餘額", "Balance and top-up management": "餘額儲值管理", "Balance depleted": "餘額已耗盡", diff --git a/web/src/i18n/locales/zh.json b/web/src/i18n/locales/zh.json index 842991e714f0..075779b4074d 100644 --- a/web/src/i18n/locales/zh.json +++ b/web/src/i18n/locales/zh.json @@ -560,6 +560,7 @@ "Badge Color": "徽章颜色", "Baidu": "百度", "Baidu V2": "百度 V2", + "Baichuan": "百川", "Balance": "余额", "Balance and top-up management": "余额充值管理", "Balance depleted": "余额已耗尽",