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
41 changes: 32 additions & 9 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,31 +1,51 @@
FROM --platform=$BUILDPLATFORM node:16 AS builder

# 配置npm使用淘宝镜像源
RUN npm config set registry https://registry.npmmirror.com && \
npm config set disturl https://npmmirror.com/dist && \
npm config set sass_binary_site https://npmmirror.com/mirrors/node-sass/ && \
npm config set electron_mirror https://npmmirror.com/mirrors/electron/ && \
npm config set puppeteer_download_host https://npmmirror.com/mirrors && \
npm config set chromedriver_cdnurl https://npmmirror.com/mirrors/chromedriver && \
npm config set operadriver_cdnurl https://npmmirror.com/mirrors/operadriver && \
npm config set phantomjs_cdnurl https://npmmirror.com/mirrors/phantomjs && \
npm config set selenium_cdnurl https://npmmirror.com/mirrors/selenium && \
npm config set node_inspector_cdnurl https://npmmirror.com/mirrors/node-inspector

WORKDIR /web
COPY ./VERSION .
COPY ./web .

RUN npm install --prefix /web/default & \
npm install --prefix /web/berry & \
npm install --prefix /web/air & \
# 并行安装npm依赖,提高构建速度
RUN npm install --prefix /web/default --prefer-offline --no-audit & \
npm install --prefix /web/berry --prefer-offline --no-audit & \
npm install --prefix /web/air --prefer-offline --no-audit & \
wait

# 并行构建前端项目,提高构建速度
RUN DISABLE_ESLINT_PLUGIN='true' REACT_APP_VERSION=$(cat ./VERSION) npm run build --prefix /web/default & \
DISABLE_ESLINT_PLUGIN='true' REACT_APP_VERSION=$(cat ./VERSION) npm run build --prefix /web/berry & \
DISABLE_ESLINT_PLUGIN='true' REACT_APP_VERSION=$(cat ./VERSION) npm run build --prefix /web/air & \
wait

FROM golang:alpine AS builder2

RUN apk add --no-cache \
# 配置Go使用国内镜像源
ENV GOPROXY=https://goproxy.cn,direct \
GOSUMDB=sum.golang.google.cn \
GO111MODULE=on \
CGO_ENABLED=1 \
GOOS=linux

# 使用阿里云 Alpine 源以加速 apk 包安装
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && \
apk update && \
apk add --no-cache \
gcc \
musl-dev \
sqlite-dev \
build-base

ENV GO111MODULE=on \
CGO_ENABLED=1 \
GOOS=linux

WORKDIR /build

ADD go.mod go.sum ./
Expand All @@ -38,7 +58,10 @@ RUN go build -trimpath -ldflags "-s -w -X 'github.com/songquanpeng/one-api/commo

FROM alpine:latest

RUN apk add --no-cache ca-certificates tzdata
# 使用阿里云 Alpine 源以加速 apk 包安装
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && \
apk update && \
apk add --no-cache ca-certificates tzdata

COPY --from=builder2 /build/one-api /

Expand Down
2 changes: 2 additions & 0 deletions controller/relay.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ func relayHelper(c *gin.Context, relayMode int) *model.ErrorWithStatusCode {
err = controller.RelayAudioHelper(c, relayMode)
case relaymode.Proxy:
err = controller.RelayProxyHelper(c, relayMode)
case relaymode.AnthropicMessages:
err = controller.RelayAnthropicHelper(c)
default:
err = controller.RelayTextHelper(c)
}
Expand Down
6 changes: 3 additions & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
version: '3.4'

services:
one-api:
image: "${REGISTRY:-docker.io}/justsong/one-api:latest"
# image: "${REGISTRY:-docker.io}/justsong/one-api:latest"
build: .
container_name: one-api
platform: linux/amd64
restart: always
command: --log-dir /app/logs
ports:
Expand Down
12 changes: 11 additions & 1 deletion middleware/recover.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ import (
"runtime/debug"
)

const (
// MaxRequestBodyLogLength is the maximum length of request body to log
// 2KB is sufficient for debugging while keeping logs readable
MaxRequestBodyLogLength = 2 * 1024
)

func RelayPanicRecover() gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
Expand All @@ -18,7 +24,11 @@ func RelayPanicRecover() gin.HandlerFunc {
logger.Errorf(ctx, fmt.Sprintf("stacktrace from panic: %s", string(debug.Stack())))
logger.Errorf(ctx, fmt.Sprintf("request: %s %s", c.Request.Method, c.Request.URL.Path))
body, _ := common.GetRequestBody(c)
logger.Errorf(ctx, fmt.Sprintf("request body: %s", string(body)))
bodyStr := string(body)
if len(bodyStr) > MaxRequestBodyLogLength {
bodyStr = bodyStr[:MaxRequestBodyLogLength] + "... (truncated)"
}
logger.Errorf(ctx, fmt.Sprintf("request body: %s", bodyStr))
c.JSON(http.StatusInternalServerError, gin.H{
"error": gin.H{
"message": fmt.Sprintf("Panic detected, error: %v. Please submit an issue with the related log here: https://github.com/songquanpeng/one-api", err),
Expand Down
49 changes: 48 additions & 1 deletion relay/adaptor/anthropic/adaptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ import (
"github.com/songquanpeng/one-api/relay/adaptor"
"github.com/songquanpeng/one-api/relay/meta"
"github.com/songquanpeng/one-api/relay/model"
"github.com/songquanpeng/one-api/relay/relaymode"
)

const (
// NativeAnthropicEndpoint is the endpoint for native Anthropic API
NativeAnthropicEndpoint = "/v1/messages"
// ThirdPartyAnthropicEndpoint is the endpoint for third-party providers supporting Anthropic protocol
ThirdPartyAnthropicEndpoint = "/anthropic/v1/messages"
)

type Adaptor struct {
Expand All @@ -21,7 +29,26 @@ func (a *Adaptor) Init(meta *meta.Meta) {
}

func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) {
return fmt.Sprintf("%s/v1/messages", meta.BaseURL), nil
// For native Anthropic API
if strings.Contains(meta.BaseURL, "api.anthropic.com") {
return fmt.Sprintf("%s%s", meta.BaseURL, NativeAnthropicEndpoint), nil
}

// For third-party providers supporting Anthropic protocol
// Common scenario: BaseURL ends with /v1 (e.g., https://api.deepseek.com/v1)
// ThirdPartyAnthropicEndpoint is /anthropic/v1/messages
// We need to avoid: /v1/anthropic/v1/messages (double /v1)
baseURL := strings.TrimSuffix(meta.BaseURL, "/")

// Smart handling: if ThirdPartyAnthropicEndpoint already contains /v1/,
// and baseURL ends with /v1, remove it from baseURL to prevent duplication
if strings.HasPrefix(ThirdPartyAnthropicEndpoint, "/") &&
strings.Contains(ThirdPartyAnthropicEndpoint, "/v1/") &&
strings.HasSuffix(baseURL, "/v1") {
baseURL = strings.TrimSuffix(baseURL, "/v1")
}

return fmt.Sprintf("%s%s", baseURL, ThirdPartyAnthropicEndpoint), nil
}

func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Request, meta *meta.Meta) error {
Expand All @@ -47,6 +74,15 @@ func (a *Adaptor) ConvertRequest(c *gin.Context, relayMode int, request *model.G
if request == nil {
return nil, errors.New("request is nil")
}

// For native Anthropic protocol requests, return the request as-is (no conversion needed)
if relayMode == relaymode.AnthropicMessages {
// The request should already be in Anthropic format, so we pass it through
// This will be handled by the caller which already has the anthropic request
return request, nil
}

// For OpenAI to Anthropic conversion (existing functionality)
return ConvertRequest(*request), nil
}

Expand All @@ -62,6 +98,17 @@ func (a *Adaptor) DoRequest(c *gin.Context, meta *meta.Meta, requestBody io.Read
}

func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, meta *meta.Meta) (usage *model.Usage, err *model.ErrorWithStatusCode) {
// For native Anthropic protocol requests, handle response directly without conversion
if meta.Mode == relaymode.AnthropicMessages {
if meta.IsStream {
err, usage = DirectStreamHandler(c, resp)
} else {
err, usage = DirectHandler(c, resp, meta.PromptTokens, meta.ActualModelName)
}
return
}

// For OpenAI to Anthropic conversion (existing functionality)
if meta.IsStream {
err, usage = StreamHandler(c, resp)
} else {
Expand Down
28 changes: 28 additions & 0 deletions relay/adaptor/anthropic/constants.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package anthropic

// ModelList contains all supported Claude models
var ModelList = []string{
"claude-instant-1.2", "claude-2.0", "claude-2.1",
"claude-3-haiku-20240307",
Expand All @@ -11,3 +12,30 @@ var ModelList = []string{
"claude-3-5-sonnet-20241022",
"claude-3-5-sonnet-latest",
}

// Performance optimization constants for streaming
const (
// StreamBufferInitialSize is the initial buffer size for streaming scanner
// 64KB is sufficient for most SSE events while minimizing memory usage
StreamBufferInitialSize = 64 * 1024

// StreamBufferMaxSize is the maximum buffer size for streaming scanner
// 512KB allows handling large message blocks without frequent allocations
StreamBufferMaxSize = 512 * 1024

// StreamFlushThreshold is the data accumulation threshold before flushing
// 4KB provides a good balance between latency and efficiency
StreamFlushThreshold = 4 * 1024

// MinEventDataLength is the minimum data length to check for event types
// 50 bytes is enough to contain "data:{"type":"message_start"...}" prefix
MinEventDataLength = 50

// EventTypeCheckRange is the range to check for event type in data
// 100 bytes covers most event types without scanning entire string
EventTypeCheckRange = 100

// MinDataPrefixLength is the minimum length for data prefix check
// 6 bytes is the length of "data:"
MinDataPrefixLength = 6
)
Loading