Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
3e3f504
feat: add GPT Image 2 token calculator and corresponding tests
May 26, 2026
27fa487
fix: enhance image request handling and response options
May 26, 2026
85db015
fix: normalize image quality handling to default to medium
May 26, 2026
d055e57
feat: enhance image response handling with additional options and tok…
May 26, 2026
32aab0b
feat: add default quality handling for GPT Image 2 token calculations
May 26, 2026
5992eb7
feat: enhance image token estimation and response handling
May 26, 2026
1d3c50e
feat: add input token details for image and text tokens in usage metrics
May 26, 2026
b71746c
feat: remove revised_prompt from images REST response and update tests
May 26, 2026
63a9d41
feat: simplify image request handling and remove unused functions
May 26, 2026
add20b8
feat: update HTTP failure classification for 403 status and add corre…
May 27, 2026
83b55b4
feat: update usage metadata handling
May 27, 2026
a87d8cf
feat: refactor usage metadata handling and update related tests
May 27, 2026
10212c7
feat: update image pricing logic and metadata handling in usage calcu…
May 27, 2026
4a77ba3
Merge remote-tracking branch 'upstream/master' into dev
May 28, 2026
f9b7fec
Merge remote-tracking branch 'upstream/master' into dev
May 28, 2026
ad48695
feat: enhance session management with account ID and response ID hand…
May 29, 2026
0585e13
feat: 独立发版
May 29, 2026
08ae2fc
chore: use master as release branch
May 29, 2026
7a8c490
Merge remote-tracking branch 'upstream/master' into dev
May 29, 2026
9fe018e
fix: remove redundant edit annotation shrink
May 29, 2026
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on:
branches: [master]

env:
GOPRIVATE: github.com/DouDOU-start/airgate-sdk
GOPRIVATE: github.com/DevilGenius/airgate-sdk

jobs:
ci:
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ permissions:
contents: write

env:
GOPRIVATE: github.com/DouDOU-start/airgate-sdk
GOPRIVATE: github.com/DevilGenius/airgate-sdk

jobs:
build:
Expand Down Expand Up @@ -64,7 +64,7 @@ jobs:
echo "Injecting PluginVersion=${VERSION}"
mkdir -p bin
cd backend && go build -buildvcs=false -trimpath \
-ldflags "-X 'github.com/DouDOU-start/airgate-openai/backend/internal/gateway.PluginVersion=${VERSION}'" \
-ldflags "-X 'github.com/DevilGenius/airgate-openai/backend/internal/gateway.PluginVersion=${VERSION}'" \
-o ../bin/gateway-openai-${{ matrix.goos }}-${{ matrix.goarch }} .

- name: Generate SHA256
Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2026 DouDOU-start
Copyright (c) 2026 DevilGenius

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ lint: ## 代码检查(需要安装 golangci-lint)
fmt: ## 格式化代码
@cd backend && \
if command -v goimports > /dev/null 2>&1; then \
goimports -w -local github.com/DouDOU-start .; \
goimports -w -local github.com/DevilGenius .; \
else \
$(GO) fmt ./...; \
fi
Expand Down
24 changes: 12 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,17 @@
<p><strong>OpenAI / ChatGPT / Anthropic 协议三合一网关插件</strong></p>

<p>
<a href="https://github.com/DouDOU-start/airgate-openai/releases"><img src="https://img.shields.io/github/v/release/DouDOU-start/airgate-openai?style=flat-square" alt="release" /></a>
<a href="https://github.com/DouDOU-start/airgate-openai/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/DouDOU-start/airgate-openai/ci.yml?branch=master&style=flat-square&label=CI" alt="ci" /></a>
<a href="https://github.com/DouDOU-start/airgate-openai/blob/master/LICENSE"><img src="https://img.shields.io/github/license/DouDOU-start/airgate-openai?style=flat-square" alt="license" /></a>
<a href="https://github.com/DevilGenius/airgate-openai/releases"><img src="https://img.shields.io/github/v/release/DevilGenius/airgate-openai?style=flat-square" alt="release" /></a>
<a href="https://github.com/DevilGenius/airgate-openai/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/DevilGenius/airgate-openai/ci.yml?branch=master&style=flat-square&label=CI" alt="ci" /></a>
<a href="https://github.com/DevilGenius/airgate-openai/blob/master/LICENSE"><img src="https://img.shields.io/github/license/DevilGenius/airgate-openai?style=flat-square" alt="license" /></a>
<img src="https://img.shields.io/badge/Go-1.25-00ADD8?style=flat-square&logo=go" alt="go" />
<img src="https://img.shields.io/badge/React-19-61DAFB?style=flat-square&logo=react" alt="react" />
</p>
</div>

---

AirGate OpenAI 不是又一个"OpenAI 转发服务",而是 [airgate-core](https://github.com/DouDOU-start/airgate-core) 的旗舰网关插件,也是 [airgate-sdk](https://github.com/DouDOU-start/airgate-sdk) 的官方参考实现。它在一个 gRPC 子进程里同时承载:
AirGate OpenAI 不是又一个"OpenAI 转发服务",而是 [airgate-core](https://github.com/DevilGenius/airgate-core) 的旗舰网关插件,也是 [airgate-sdk](https://github.com/DevilGenius/airgate-sdk) 的官方参考实现。它在一个 gRPC 子进程里同时承载:

- **OpenAI Responses / Chat Completions API** 转发(Codex 核心端点)
- **ChatGPT OAuth 浏览器授权账号** 接入(PKCE + WebSocket 桥接)
Expand Down Expand Up @@ -128,19 +128,19 @@ AirGate OpenAI 不是又一个"OpenAI 转发服务",而是 [airgate-core](http
```text
1. 插件市场 → 点击「安装」 (从 GitHub Release 自动拉取,匹配当前架构)
2. 上传安装 → 拖入二进制文件 (适合内部环境 / 自建二进制)
3. GitHub 安装 → 输入 DouDOU-start/airgate-openai
3. GitHub 安装 → 输入 DevilGenius/airgate-openai
```

market 会**定时从 GitHub API 同步**最新 release(默认 6h,使用 ETag 不消耗 API 配额),新 tag push 后通常几分钟内即可在市场看到。

### 方式 2:源码运行(开发)

需要 Go 1.25+、Node 22+,以及兄弟目录 [`airgate-sdk`](https://github.com/DouDOU-start/airgate-sdk) 与 [`airgate-core`](https://github.com/DouDOU-start/airgate-core):
需要 Go 1.25+、Node 22+,以及兄弟目录 [`airgate-sdk`](https://github.com/DevilGenius/airgate-sdk) 与 [`airgate-core`](https://github.com/DevilGenius/airgate-core):

```bash
git clone https://github.com/DouDOU-start/airgate-sdk.git
git clone https://github.com/DouDOU-start/airgate-core.git
git clone https://github.com/DouDOU-start/airgate-openai.git
git clone https://github.com/DevilGenius/airgate-sdk.git
git clone https://github.com/DevilGenius/airgate-core.git
git clone https://github.com/DevilGenius/airgate-openai.git
cd airgate-openai

make install # 装 web 依赖与 Go 模块
Expand Down Expand Up @@ -248,9 +248,9 @@ git push origin v0.2.0

## 🤝 贡献 / 反馈

- Bug / Feature: [Issues](https://github.com/DouDOU-start/airgate-openai/issues)
- 主仓库: [airgate-core](https://github.com/DouDOU-start/airgate-core)
- 插件 SDK: [airgate-sdk](https://github.com/DouDOU-start/airgate-sdk)
- Bug / Feature: [Issues](https://github.com/DevilGenius/airgate-openai/issues)
- 主仓库: [airgate-core](https://github.com/DevilGenius/airgate-core)
- 插件 SDK: [airgate-sdk](https://github.com/DevilGenius/airgate-sdk)

## 📜 License

Expand Down
2 changes: 1 addition & 1 deletion backend/.golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ formatters:
settings:
goimports:
local-prefixes:
- github.com/DouDOU-start
- github.com/DevilGenius

issues:
max-issues-per-linter: 0
Expand Down
2 changes: 1 addition & 1 deletion backend/cmd/chat/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
"os"
"strings"

"github.com/DouDOU-start/airgate-openai/backend/internal/gateway"
"github.com/DevilGenius/airgate-openai/backend/internal/gateway"
)

func main() {
Expand Down
4 changes: 2 additions & 2 deletions backend/cmd/chat/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import (

"github.com/gorilla/websocket"

"github.com/DouDOU-start/airgate-openai/backend/internal/gateway"
"github.com/DouDOU-start/airgate-openai/backend/resources"
"github.com/DevilGenius/airgate-openai/backend/internal/gateway"
"github.com/DevilGenius/airgate-openai/backend/resources"
)

// ──────────────────────────────────────────────────────
Expand Down
4 changes: 2 additions & 2 deletions backend/cmd/devserver/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import (
"log"
"net/http"

"github.com/DouDOU-start/airgate-sdk/devkit/devserver"
"github.com/DevilGenius/airgate-sdk/devkit/devserver"

"github.com/DouDOU-start/airgate-openai/backend/internal/gateway"
"github.com/DevilGenius/airgate-openai/backend/internal/gateway"
)

func main() {
Expand Down
6 changes: 3 additions & 3 deletions backend/cmd/genmanifest/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import (

"gopkg.in/yaml.v3"

"github.com/DouDOU-start/airgate-openai/backend/internal/gateway"
"github.com/DouDOU-start/airgate-openai/backend/internal/model"
sdk "github.com/DouDOU-start/airgate-sdk/sdkgo"
"github.com/DevilGenius/airgate-openai/backend/internal/gateway"
"github.com/DevilGenius/airgate-openai/backend/internal/model"
sdk "github.com/DevilGenius/airgate-sdk/sdkgo"
)

const generatedComment = "# 本文件由 backend/cmd/genmanifest 自动生成,请勿手工修改。\n\n"
Expand Down
4 changes: 2 additions & 2 deletions backend/go.mod
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
module github.com/DouDOU-start/airgate-openai/backend
module github.com/DevilGenius/airgate-openai/backend

go 1.25.7

require (
github.com/DouDOU-start/airgate-sdk v0.2.1
github.com/DevilGenius/airgate-sdk v0.2.1
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3
github.com/lib/pq v1.10.9
Expand Down
2 changes: 0 additions & 2 deletions backend/go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
github.com/DouDOU-start/airgate-sdk v0.2.1 h1:MylrFMifIJy5mT1wSFdXcSvAXjIcB6itp4dw8pP4pes=
github.com/DouDOU-start/airgate-sdk v0.2.1/go.mod h1:784vC4lIfCnUdOroWuvn55Dv/b9TucVMpFwuu40gc0Q=
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw=
Expand Down
2 changes: 1 addition & 1 deletion backend/internal/gateway/anthropic_compat_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (

"github.com/tidwall/gjson"

sdk "github.com/DouDOU-start/airgate-sdk/sdkgo"
sdk "github.com/DevilGenius/airgate-sdk/sdkgo"
)

func TestEstimateAnthropicInputTokensCountsSystemMessagesAndTools(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion backend/internal/gateway/anthropic_count_tokens.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"encoding/json"
"net/http"

sdk "github.com/DouDOU-start/airgate-sdk/sdkgo"
sdk "github.com/DevilGenius/airgate-sdk/sdkgo"
)

// forwardAnthropicCountTokens 返回本地估算,避免客户端把 404 当作不可计数并继续发送过长请求。
Expand Down
9 changes: 5 additions & 4 deletions backend/internal/gateway/anthropic_forward.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"

sdk "github.com/DouDOU-start/airgate-sdk/sdkgo"
sdk "github.com/DevilGenius/airgate-sdk/sdkgo"
)

// ──────────────────────────────────────────────────────
Expand All @@ -26,7 +26,7 @@ func (g *OpenAIGateway) forwardAnthropicMessage(ctx context.Context, req *sdk.Fo
start := time.Now()
body := req.Body
strategy := resolveAnthropicUpstreamStrategy(req.Account)
session := resolveOpenAISession(req.Headers, req.Body)
session := resolveOpenAISession(req.Headers, req.Body, req.Account.ID)
session.DigestChain = buildAnthropicDigestChain(body)
if session.SessionKey == "" {
if reusedSessionID, matchedChain, ok := findAnthropicDigestSession(req.Account.ID, session.DigestChain); ok {
Expand All @@ -41,7 +41,7 @@ func (g *OpenAIGateway) forwardAnthropicMessage(ctx context.Context, req *sdk.Fo
session.SessionSource = "anthropic_digest_new"
}
}
updateSessionStateFromRequest(session)
updateSessionStateFromRequest(session, req.Account.ID)

logger := sdk.LoggerFromContext(ctx)
logger.Debug("anthropic_request_received",
Expand Down Expand Up @@ -516,7 +516,7 @@ func (g *OpenAIGateway) handleAnthropicNonStreamFromResponses(
return transientOutcome(reason), fmt.Errorf("%s", reason)
}
if session.SessionKey != "" && wsResult.ResponseID != "" {
updateSessionStateResponseID(session.SessionKey, wsResult.ResponseID)
updateSessionStateResponseID(session.SessionKey, wsResult.ResponseID, accountID)
}
if session.SessionID != "" && session.DigestChain != "" {
saveAnthropicDigestSession(accountID, session.DigestChain, session.SessionID, session.MatchedDigest)
Expand Down Expand Up @@ -554,6 +554,7 @@ func (g *OpenAIGateway) handleAnthropicNonStreamFromResponses(
elapsed.Milliseconds(),
)
fillUsageCost(usage)
setUsageResponseID(usage, wsResult.ResponseID)
return sdk.ForwardOutcome{
Kind: sdk.OutcomeSuccess,
Upstream: sdk.UpstreamResponse{StatusCode: http.StatusOK, Headers: upstreamHeaders, Body: anthropicBody},
Expand Down
9 changes: 7 additions & 2 deletions backend/internal/gateway/anthropic_response.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"

sdk "github.com/DouDOU-start/airgate-sdk/sdkgo"
sdk "github.com/DevilGenius/airgate-sdk/sdkgo"
)

// ──────────────────────────────────────────────────────
Expand Down Expand Up @@ -649,6 +649,7 @@ func translateResponsesSSEToAnthropicSSE(
serviceTier := firstNonEmptyTier(requestServiceTier)
skipCurrentOutput := false
firstTokenRecorded := false
responseID := ""

for scanner.Scan() {
skipCurrentOutput = false
Expand Down Expand Up @@ -687,9 +688,12 @@ func translateResponsesSSEToAnthropicSSE(
}
if session.SessionKey != "" {
if responseID := gjson.Get(data, "response.id").String(); strings.TrimSpace(responseID) != "" {
updateSessionStateResponseID(session.SessionKey, responseID)
updateSessionStateResponseID(session.SessionKey, responseID, session.AccountID)
}
}
if id := strings.TrimSpace(gjson.Get(data, "response.id").String()); id != "" {
responseID = id
}
if eventType == "response.completed" || eventType == "response.done" {
if serviceTier == "" {
serviceTier = firstNonEmptyTier(gjson.Get(data, "response.service_tier").String(), defaultServiceTier)
Expand Down Expand Up @@ -802,6 +806,7 @@ done:
}

fillUsageCost(usage)
setUsageResponseID(usage, responseID)
return sdk.ForwardOutcome{
Kind: sdk.OutcomeSuccess,
Upstream: sdk.UpstreamResponse{StatusCode: http.StatusOK},
Expand Down
2 changes: 1 addition & 1 deletion backend/internal/gateway/anthropic_strategy.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package gateway

import (
sdk "github.com/DouDOU-start/airgate-sdk/sdkgo"
sdk "github.com/DevilGenius/airgate-sdk/sdkgo"
)

type anthropicUpstreamStrategy string
Expand Down
8 changes: 4 additions & 4 deletions backend/internal/gateway/chat_completions_oauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (

"github.com/tidwall/gjson"

sdk "github.com/DouDOU-start/airgate-sdk/sdkgo"
sdk "github.com/DevilGenius/airgate-sdk/sdkgo"
)

// OAuth/Codex 上游永远返回 Responses API 的 SSE 流,但客户端走
Expand Down Expand Up @@ -157,7 +157,7 @@ func (s *chatCompletionsStreamWriter) OnRawEvent(eventType string, data []byte)
case "response.created", "response.completed", "response.done":
if s.sessionKey != "" {
if responseID := gjson.GetBytes(data, "response.id").String(); strings.TrimSpace(responseID) != "" {
updateSessionStateResponseID(s.sessionKey, responseID)
updateSessionStateResponseID(s.sessionKey, responseID, s.accountID)
}
}
}
Expand Down Expand Up @@ -506,7 +506,7 @@ func (h *chatCompletionsSilentHandler) OnRawEvent(eventType string, data []byte)
case "response.created", "response.completed", "response.done":
if h.sessionKey != "" {
if responseID := gjson.GetBytes(data, "response.id").String(); strings.TrimSpace(responseID) != "" {
updateSessionStateResponseID(h.sessionKey, responseID)
updateSessionStateResponseID(h.sessionKey, responseID, h.accountID)
}
}
}
Expand Down Expand Up @@ -557,7 +557,7 @@ func (h *responsesSilentHandler) OnRawEvent(eventType string, data []byte) {
case "response.created", "response.completed", "response.done":
if h.sessionKey != "" {
if responseID := gjson.GetBytes(data, "response.id").String(); strings.TrimSpace(responseID) != "" {
updateSessionStateResponseID(h.sessionKey, responseID)
updateSessionStateResponseID(h.sessionKey, responseID, h.accountID)
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion backend/internal/gateway/chat_completions_oauth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"testing"
"time"

sdk "github.com/DouDOU-start/airgate-sdk/sdkgo"
sdk "github.com/DevilGenius/airgate-sdk/sdkgo"
)

// fakeResponseWriter 用于单测:捕获写回的 SSE / JSON 响应体。
Expand Down
9 changes: 6 additions & 3 deletions backend/internal/gateway/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"

sdk "github.com/DouDOU-start/airgate-sdk/sdkgo"
sdk "github.com/DevilGenius/airgate-sdk/sdkgo"
)

// 统一错误分类工具(跨 OpenAI / Anthropic 协议共用)。
Expand All @@ -20,7 +20,8 @@ import (
// 返回的 Kind 决定 Core 如何处置账号:
//
// 429 → AccountRateLimited
// 401 / 403 → AccountDead(附加消息关键词检查"usage limit" / "rate limit" 等会降级为 RateLimited)
// 401 → AccountDead
// 403 → AccountUnavailable(明确 disabled/deactivated/suspended 仍升级为 AccountDead)
// 400 + 消息含限流关键词 → AccountRateLimited(部分上游用 400 返回 usage_limit_reached)
// 400 + 消息含 disabled/deactivated → AccountDead
// 5xx → UpstreamTransient
Expand All @@ -35,8 +36,10 @@ func classifyHTTPFailure(statusCode int, message string) sdk.OutcomeKind {
switch statusCode {
case 429:
return sdk.OutcomeAccountRateLimited
case 401, 403:
case 401:
return sdk.OutcomeAccountDead
case 403:
return sdk.OutcomeAccountUnavailable
}
if statusCode >= 500 {
return sdk.OutcomeUpstreamTransient
Expand Down
Loading
Loading