Skip to content

feat(keypool): admin-key + content-key + sticky-by-body + multi-key rotation - #30

Merged
6Kmfi6HP merged 9 commits into
mainfrom
feat/keypool
Sep 27, 2026
Merged

6Kmfi6HP merged 9 commits into
mainfrom
feat/keypool

Conversation

@6Kmfi6HP

@6Kmfi6HP 6Kmfi6HP commented Sep 27, 2026 •

Copy link
Copy Markdown
Owner

概览

本 PR 集合了 4 个 commit,落地以下三块协同改动:

  1. keypool multi-key 管理(,原有 4481897 + 7d0cf28)

    • 池策略 round_robin / weighted / sticky(默认 sticky)。
    • 布败 failover:401/402/403/429/5xx 按 cooldown_secs 冷却,blacklist_after 连胡 N 次 → 15min 黑名单(重启清空),池打空才 5xx。
    • 池管理面板 Tab + config.json 1:1 同步;saveConfig 收紧 0600 权限。
  2. admin password 触发 key_pool(80ee3f4 + 27a552e)

    • Authorization: Bearer <adminPassword> 或 x-api-key: <adminPassword> → AuthRouteAdmin 键跳过 sk- 前缀校验,强制走 key_pool。
    • adminPassword="" 或 launch 模式(未设置 env 时)该路径恒不命中。
    • preferContentKey / stickyByBodyEnabled 默认沿用真实 opencode key(sk- / oc_sk-);默认保持安全语义:静默认作 AuthRouteAdmin 不触发真实 key 透传上游。
  3. prompt_cache_key 精确命中 (3a5303c)

    • 免费层(无真实 key)默认启用内容派生 prompt_cache_key = sha256(prefix),稳定跨 launch,不再让每个 session 耗新缓存槽位。
    • OPENCODE2API_PROMPT_CACHE_KEY=session 显式回退。
    • OPENCODE2API_STICKY_BY_BODY=off 显式回退 sticky by-body。
    • 修正 parseCacheUsage 识别 OpenAI Responses input_tokens_details.cached_tokens 口径(此前 cache_read_tokens 统计恒为 0)。

服务端假设(请参照 keypool-design.md §2)

  • Authorization: Bearer <真实 key>(sk-/oc_sk-)→ 池接管,上游用池选的 key;token 仅记 source日志。
  • Authorization: Bearer <public/短占位> 或空 → 永不走池,防匿名蹭付费额度。
  • go:/zen: 前缀仍然控制 surface,token 部分被池 key 替换。

测试证据

  • go test ./... 全过(含新增 TestKeyPool_*、TestExtractUpstreamAuth_AdminPasswordTriggersPool、TestNormalizeKeyPool_DefaultsToSticky、TestParseCacheUsageInputTokensDetailsCached 等)。
  • launch codex/claude 端到端(5 key 池 + admin password 作为 Bearer)各场景命中/未命中曲线符合预期;stats.json 正确累计 cache_read_tokens。

附带

  • 本 PR 依赖 keypool-design.md 的 P0 冻结约定;不改变上游转发 HTTP shape,不进 DB/不签发二级 token。

6Kmfi6HP and others added 6 commits September 27, 2026 17:10
- Key pool with round_robin/weighted/sticky strategies, zen/go group
  filter, string-shorthand keys, atomic RR selection
- Failover in callOpenCodeEndpoint: 429/5xx cooldown, 401/billing 15min
  cooldown, pool failover budget 1+max_retries, per-attempt key switch
  with key_id-only logging
- Admin: Keys tab with inline edit, batch add via POST /api/key_parse
  canonical grammar, per-key status; GET /api/key_status
- Per-key usage stats in stats.json; saveConfig tightened to 0600

Tests: go test ./... PASS, go vet clean, gofmt clean
Resolve opencode.go conflicts on top of main's atomic session snapshot
(#29): keep selectPoolKey/targetAuth failover on the
buildOCRequestWithSubpathAndState(sessionState) path, restore
up.WithContext(ctx) cancel propagation, and keep both
statsx + singleflight imports.

Register GET /api/key_status (keyPoolStatusHandler existed but was
unreachable via buildMux).
…e-tier default

- preferContentKey: 免费层(无 token)默认按请求体 SHA-256 派生稳定
  prompt_cache_key;OPENCODE2API_PROMPT_CACHE_KEY=session 回退。付费层
  保持 session 派生,不抢占上游账号路由。
- stickyByBodyEnabled: 默认把 prompt_cache_key 作为 sticky 出口键
  (跨 launch 稳定 → 同内容锁同一缓存分片);OPENCODE2API_STICKY_BY_BODY=off
  回退原 session-头绑定。
- parseCacheUsage: 补 input_tokens_details.cached_tokens(OpenAI Responses
  原生透传口径),cache_read_tokens 统计此前始终为 0。
- readJSONRequestBody 把 UpstreamAuth 注入 ctx,供缓存路径判断付费/免费。
- relayResponsesStream: OPENCODE2API_CACHE_DEBUG=1 时落 cache_debug_stream_usage,
  排查未来缓存回归。

实测(4 轮 launch codex,同目录):
  基线(session_key, 4 次): cached = [11633, 0, 0, 11633]  → 50%
  默认开(content_key, 4 次): cached = [11633, 11633, 11633, 11633] → 100%

Co-Authored-By: Claude Code <noreply@anthropic.com>
真实 opencode 网关同时发行两种前缀的 key;现有 isValidOpenCodeKey 只认
sk- 开头,导致 oc_sk- 用户 key 被判为占位串回落 public,整个 key_pool
付费路径(轮询/weighted/sticky/failover)对这些 key 不生效。

新增 TestIsValidOpenCodeKeyOcSk 覆盖 5 个 oc_sk_ 真实 key 形态 + sk-/sk-ant-
边界。

Co-Authored-By: Claude Code <noreply@anthropic.com>
- extractUpstreamAuth: 命中 adminPassword(constant-time compare)→
  Mode=AuthRouteAdmin, Source="admin",跳过 sk- 校验;客户端 bearer 不再发上游。
- keypool.selectPoolKey: Mode=AuthRouteAdmin 强制走池 (不再要求 sk- token)。
- keypool.normalizeKeyPool: Strategy 空 → sticky(与 prompt cache 亲和)。
  round_robin/weighted 仍可通过 config.json 显式启用。
- 新增测试覆盖:adminPassword 三态、恒定时间比较、AuthRouteAdmin→池接管、
  normalizeKeyPool 默认 sticky。
- docs/keypool-design.md §9 补充契约、安全前缀优先级、空密码禁用。

Co-Authored-By: Claude Code <noreply@anthropic.com>
…e to honor OPENCODE2API_ADMIN_PASSWORD

- config.example.json: 加 admin_password 注释、key_pool default strategy
  改为 sticky。
- launch.configureLaunchGlobals: 不再硬清 adminPassword;空值时仍不启
  用 admin-as-key,OPENCODE2API_ADMIN_PASSWORD 显式给值则可用
  Bearer 密码触发 key_pool(适合 CI / 多机脚本场景)。

Co-Authored-By: Claude Code <noreply@anthropic.com>
@6Kmfi6HP 6Kmfi6HP changed the title feat(keypool): multi-key rotation, failover and admin Keys tab feat(keypool): admin-key + content-key + sticky-by-body + multi-key rotation Sep 27, 2026
6Kmfi6HP and others added 3 commits September 27, 2026 21:11
launch.configureLaunchGlobals 已把 adminPassword 置空(除非显式
OPENCODE2API_ADMIN_PASSWORD),但 buildMux 仍注册 /api/* 与 /login;
requireAuth 在 adminPassword== 时又直通,等于本地 launch 上 admin
路径完全无鉴权可改 key_pool。

改成:
- buildMux 只在 adminAPIEnabled()(adminPassword != )时才注册
  /login /logout /api/config /api/stats /api/key_parse /api/key_status
  /api/reload 与 / 面板。launch 模式下默认空密码 → 这些端点 404,
  本地代理只保留 /v1/* 知识线 + /health。
- requireAuth 语义不变:adminPassword== 时直通(避免 server
  在密码未启用时造成死循环)。

server 模式(默认 -password=123456)行为未改;显式 OPENCODE2API_ADMIN_PASSWORD
可让 launch 进入 admin-enabled 状态。

Co-Authored-By: Claude Code <noreply@anthropic.com>
…status

- stats.KeyStats 增加 CacheHitRequests / CacheMissRequests / CacheHitRatio。
  RecordKeyCacheResult(keyID, cachedTokens>0) 在每次请求后按 usage 记录;
  selectPoolKey → RecordKeyUsage 同步使用。
- /api/key_status 额外返回 pool.strategy/sticky/egress 快照,便于诊断
  big-pickle 这种命中率不稳定问题。

Co-Authored-By: Claude Code <noreply@anthropic.com>
- getUpstreamBaseURLs: retain ordering rather than shuffle; caller picks by
  own policy. Pool failover previously relied on shuffled ordering; now it
  uses (attemptSeed + hash) % n 来保证失败重试时粘性不再被新 feedback
  序重置。
- requireAuth 改为在 adminAPIEnabled() 为 false 时直通 → 即便有密码,
  server 模式注册的 /api/* 仍按既有语义, launch 下不注册则不拦截。
  admin-as-key 路径保留原语义: adminPassword 在 launch/server 都可触发
  AuthRouteAdmin。
- /api/key_status 的返回体补 key_id / mode / last_used_unix 到 PoolStatusEntry,
  stats.go 里 KeyStats 新增 CacheHitRequests / CacheMissRequests / CacheHitRatio
  (由调用方传递 usage.cached_tokens 更新), 现在 /api/stats 里能直接看到
  命中统计。

Co-Authored-By: Claude Code <noreply@anthropic.com>
@6Kmfi6HP
6Kmfi6HP merged commit 715b5cd into main Sep 27, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant