Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
176 changes: 176 additions & 0 deletions .github/workflows/docker-ghcr.yml
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions common/api_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions constant/api_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,6 @@ const (
APITypeAdvancedCustom
APITypeSub2API
APITypeNewAPI
APITypeBaichuan
APITypeDummy // this one is only for count, do not add any channel after this
)
3 changes: 3 additions & 0 deletions constant/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

)
Expand Down Expand Up @@ -124,6 +125,7 @@ var ChannelBaseURLs = []string{
"", //58
"", //59
"", //60
"https://api.baichuan-ai.com", //61
}

var ChannelTypeNames = map[int]string{
Expand Down Expand Up @@ -184,6 +186,7 @@ var ChannelTypeNames = map[int]string{
ChannelTypeAdvancedCustom: "Advanced Custom",
ChannelTypeSub2API: "Sub2API",
ChannelTypeNewAPI: "New API",
ChannelTypeBaichuan: "Baichuan",
}

func GetChannelTypeName(channelType int) string {
Expand Down
40 changes: 34 additions & 6 deletions controller/channel-test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"}]`)

Expand Down Expand Up @@ -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),
Expand All @@ -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
}
}
Expand Down Expand Up @@ -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") {
Expand Down
36 changes: 36 additions & 0 deletions controller/channel_upstream_update.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
31 changes: 20 additions & 11 deletions relay/channel/openai/adaptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions relay/common/relay_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions relay/relay_adaptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Loading