feat(keypool): admin-key + content-key + sticky-by-body + multi-key rotation - #30
Merged
Merged
Conversation
- 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>
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
概览
本 PR 集合了 4 个 commit,落地以下三块协同改动:
keypool multi-key 管理(,原有 4481897 + 7d0cf28)
blacklist_after连胡 N 次 → 15min 黑名单(重启清空),池打空才 5xx。config.json1:1 同步;saveConfig收紧 0600 权限。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 透传上游。prompt_cache_key 精确命中 (3a5303c)
prompt_cache_key = sha256(prefix),稳定跨 launch,不再让每个 session 耗新缓存槽位。OPENCODE2API_PROMPT_CACHE_KEY=session显式回退。OPENCODE2API_STICKY_BY_BODY=off显式回退 sticky by-body。parseCacheUsage识别 OpenAI Responsesinput_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 等)。附带