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
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Run it as a full **PostgreSQL + Redis** production stack or as a single-containe

<table>
<tr><td width="210"><b>One compatible gateway</b></td><td>OpenAI-style Chat Completions / Responses / Images, Anthropic Messages, prefixless compatibility routes, and native Codex Responses forwarding are all exposed through one service.</td></tr>
<tr><td><b>Named upstream providers</b></td><td>Beyond Codex OAuth accounts, pool OpenAI-Responses-compatible gateway credentials as first-class upstreams — including a dedicated <b>OrcaRouter</b> provider type (<code>sk-orca-</code> keys, Base URL <code>https://api.orcarouter.ai/v1</code>) managed from the Accounts page.</td></tr>
<tr><td><b>Account-pool scheduler</b></td><td>Selection is driven by account status, health tier, scheduler score, dynamic concurrency, cooldown recovery, and recent usage so unhealthy accounts are avoided automatically. Supports <code>round_robin</code> and <code>remaining_quota</code> modes, with per-account credit billing flags.</td></tr>
<tr><td><b>Visual admin console</b></td><td>The embedded React / Vite dashboard covers account import and testing, API keys, proxy pools, image studio (text-to-image + image-to-image), prompt filtering, usage analytics, operations, scheduler board, and system settings.</td></tr>
<tr><td><b>Two deployment shapes</b></td><td>Use PostgreSQL + Redis for production or SQLite + Memory for lightweight single-node deployments; Docker images, source builds, local development, and the interactive deploy script are ready to use. SQLite mode binds to <code>127.0.0.1</code> by default for security.</td></tr>
Expand Down Expand Up @@ -365,6 +366,31 @@ curl -X POST http://localhost:8080/api/admin/accounts/at \
-d '{"access_token": "eyJtoken1...\neyJtoken2...\neyJtoken3..."}'
```

#### Add OrcaRouter Gateway Accounts

Codex2API can also pool **OrcaRouter** gateway credentials as a first-class upstream type. [OrcaRouter](https://www.orcarouter.ai) is an OpenAI-Responses compatible gateway: add a `sk-orca-` API key and a Base URL, list the models it exposes, and the pool will route `/v1/responses` traffic through it with the same scheduling, health scoring, and usage tracking as Codex OAuth accounts. It also runs gateway-level, zero-trust security for AI agents on the same endpoint — screening every prompt/response and governing every tool call on a default-deny basis, with no application code changes.

```bash
# Add an OrcaRouter gateway account
curl -X POST http://localhost:8080/api/admin/accounts/orcarouter \
-H "X-Admin-Key: your-admin-secret" \
-H "Content-Type: application/json" \
-d '{
"name": "orcarouter-pool",
"base_url": "https://api.orcarouter.ai/v1",
"api_key": "sk-orca-xxxxxxxxxxxx",
"models": ["orcarouter/auto"]
}'

# Fetch the model catalog exposed by a Base URL + key
curl -X POST http://localhost:8080/api/admin/accounts/orcarouter/models \
-H "X-Admin-Key: your-admin-secret" \
-H "Content-Type: application/json" \
-d '{"base_url": "https://api.orcarouter.ai/v1", "api_key": "sk-orca-xxxxxxxxxxxx"}'
```

OrcaRouter accounts can be managed from the Accounts page like any other upstream: add via the **OrcaRouter** tab, edit Base URL / model whitelist, test the connection, and monitor usage. The `platform` column is marked `orcarouter` so they are easy to identify in the admin dashboard.

#### File Import

```bash
Expand Down
25 changes: 25 additions & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,31 @@ curl -X POST http://localhost:8080/api/admin/accounts/at \
-d '{"access_token": "eyJtoken1...\neyJtoken2...\neyJtoken3..."}'
```

#### 添加 OrcaRouter 网关账号

Codex2API 也可以把 **OrcaRouter** 网关凭据作为一等上游类型加入账号池。[OrcaRouter](https://www.orcarouter.ai) 是 OpenAI-Responses 兼容网关:填入 `sk-orca-` API Key 与 Base URL,拉取它暴露的模型清单,账号池就会用与 Codex OAuth 账号相同的调度、健康评分与用量追踪来路由 `/v1/responses` 流量。它在同一端点还运行网关级、零信任的 AI Agent 安全能力——默认拒绝地审查每一条 prompt/response 并治理每一次工具调用,无需改动应用代码。

```bash
# 添加一个 OrcaRouter 网关账号
curl -X POST http://localhost:8080/api/admin/accounts/orcarouter \
-H "X-Admin-Key: your-admin-secret" \
-H "Content-Type: application/json" \
-d '{
"name": "orcarouter-pool",
"base_url": "https://api.orcarouter.ai/v1",
"api_key": "sk-orca-xxxxxxxxxxxx",
"models": ["orcarouter/auto"]
}'

# 拉取 Base URL + key 暴露的模型目录
curl -X POST http://localhost:8080/api/admin/accounts/orcarouter/models \
-H "X-Admin-Key: your-admin-secret" \
-H "Content-Type: application/json" \
-d '{"base_url": "https://api.orcarouter.ai/v1", "api_key": "sk-orca-xxxxxxxxxxxx"}'
```

OrcaRouter 账号可以在 Accounts 页像其他上游一样管理:通过 **OrcaRouter** 标签添加、编辑 Base URL / 模型白名单、测试连通性并查看用量。`platform` 列标记为 `orcarouter`,便于在管理后台识别。

#### 文件批量导入

```bash
Expand Down
13 changes: 8 additions & 5 deletions admin/account_response_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ func (h *Handler) buildAccountResponse(
) accountResponse {
upstreamType := strings.TrimSpace(row.GetCredential("upstream_type"))
isOpenAIResponsesAccount := strings.EqualFold(upstreamType, auth.UpstreamOpenAIResponses)
isOrcaRouterAccount := strings.EqualFold(upstreamType, auth.UpstreamOrcaRouter)
isRelayResponsesAccount := isOpenAIResponsesAccount || isOrcaRouterAccount
isGrokAccount := strings.EqualFold(upstreamType, auth.UpstreamGrok)
grokAuthKind := ""
var grokBilling json.RawMessage
Expand All @@ -43,11 +45,11 @@ func (h *Handler) buildAccountResponse(
}
email := row.GetCredential("email")
baseURL := row.GetCredential("base_url")
if isOpenAIResponsesAccount && email == "" {
if isRelayResponsesAccount && email == "" {
email = baseURL
}
planType := row.GetCredential("plan_type")
if isOpenAIResponsesAccount && planType == "" {
if isRelayResponsesAccount && planType == "" {
planType = "api"
}
if isGrokAccount && grokAuthKind == auth.GrokAuthKindAPIKey {
Expand All @@ -65,12 +67,12 @@ func (h *Handler) buildAccountResponse(
}
}
codexClientMetadataMode := ""
if isOpenAIResponsesAccount && includeDetails {
if isRelayResponsesAccount && includeDetails {
codexClientMetadataMode = auth.NormalizeCodexClientMetadataMode(row.GetCredential("codex_client_metadata_mode"))
}
// 指纹收敛只作用于 Codex 官方出站路径,中转/Grok 账号不暴露该字段。
codexFingerprintMode := ""
if !isOpenAIResponsesAccount && !isGrokAccount {
if !isRelayResponsesAccount && !isGrokAccount {
codexFingerprintMode = auth.NormalizeCodexFingerprintMode(row.GetCredential(auth.CodexFingerprintModeCredentialKey))
}
ignoreUsageLimitStatusOverride := row.GetCredentialOptionalBool("ignore_usage_limit_status_override")
Expand Down Expand Up @@ -107,13 +109,14 @@ func (h *Handler) buildAccountResponse(
SubscriptionExpiresAt: row.GetCredential("subscription_expires_at"),
Status: row.Status,
ErrorMessage: row.ErrorMessage,
ATOnly: !isOpenAIResponsesAccount && !isGrokAccount && row.GetCredential("refresh_token") == "" && row.GetCredential("access_token") != "",
ATOnly: !isRelayResponsesAccount && !isGrokAccount && row.GetCredential("refresh_token") == "" && row.GetCredential("access_token") != "",
CreditEnabled: row.CreditEnabled,
CreditSkipUsageWindow: row.CreditSkipUsageWindow,
SkipWarmTier: row.SkipWarmTier,
AccountType: row.Type,
AccessTokenType: accountAccessTokenType(row),
OpenAIResponsesAPI: isOpenAIResponsesAccount,
OrcaRouterAPI: isOrcaRouterAccount,
GrokAPI: isGrokAccount,
AgentIdentity: isAgentIdentityCredentialRow(row),
GrokAuthKind: grokAuthKind,
Expand Down
2 changes: 1 addition & 1 deletion admin/accounts_paged.go
Original file line number Diff line number Diff line change
Expand Up @@ -676,7 +676,7 @@ func (h *Handler) installAccountListSnapshot(channel string, snapshot *accountLi
func (h *Handler) buildAccountListSnapshotItem(row *database.AccountRow, requestCounts map[int64]*database.AccountRequestCount, todayUsage map[int64]*database.AccountTimeRangeUsage, groupNames, groupSort map[int64]string) *accountListSnapshotItem {
upstreamType := strings.TrimSpace(row.GetCredential("upstream_type"))
isGrok := strings.EqualFold(upstreamType, auth.UpstreamGrok)
isOpenAIResponses := strings.EqualFold(upstreamType, auth.UpstreamOpenAIResponses)
isOpenAIResponses := strings.EqualFold(upstreamType, auth.UpstreamOpenAIResponses) || strings.EqualFold(upstreamType, auth.UpstreamOrcaRouter)
email := row.GetCredential("email")
if isOpenAIResponses && email == "" {
email = row.GetCredential("base_url")
Expand Down
Loading