Skip to content

fix(openai): honor OpenCode Go usage reset durations - #5658

Open
william-drakemond wants to merge 1 commit into
Wei-Shaw:mainfrom
william-drakemond:fix/opencode-go-usage-limit-reset
Open

fix(openai): honor OpenCode Go usage reset durations#5658
william-drakemond wants to merge 1 commit into
Wei-Shaw:mainfrom
william-drakemond:fix/opencode-go-usage-limit-reset

Conversation

@william-drakemond

Copy link
Copy Markdown

Summary

  • Recognize OpenCode Go GoUsageLimitError responses as account usage limits.
  • Parse reset durations from messages such as Resets in 2 days and composite values such as Resets in 4hr 59min.
  • Keep the existing 429 fallback behavior for malformed payloads and unrelated error types.

Problem

OpenCode Go usage-limit responses do not provide the reset through the OpenAI fields already handled by sub2api, such as resets_at or resets_in_seconds. The parser therefore returned no reset time. API-key accounts then fell through to the configured 429 cooldown, which defaults to 5 seconds, became schedulable again, and repeatedly hit the same exhausted account before its quota actually reset.

OpenCode rolling-window messages can contain more than one duration component. Parsing only the first component would turn 4hr 59min into 4h and release the account up to 59 minutes too early.

Fix

parseOpenAIRateLimitResetTime now recognizes GoUsageLimitError and, only for that error type, parses the reset duration from its human-readable message.

The duration parser:

  • Requires the Resets in marker.
  • Supports seconds, minutes, hours, days, and weeks, including common abbreviations.
  • Accumulates contiguous components such as 4hr 59min.
  • Rejects non-positive, malformed, and overflowing values so the established fallback remains in control.

The computed timestamp flows through the existing 429 path. That path persists the rate limit with SetRateLimited and notifies the runtime blocker, so API-key accounts stay out of scheduling until the parsed reset time. Unknown error types continue to return no parsed reset.

Tests

Added coverage for:

  • Day and hour reset messages.
  • Composite hour/minute reset messages.
  • Rejection of unrelated error types.
  • The full API-key 429 path, including persistence and runtime blocking.

Validated with:

  • go test -tags=unit ./internal/service -count=1
  • go test -tags=integration ./...
  • golangci-lint v2.9.0 run ./internal/service --timeout=10m (0 issues)

@github-actions

github-actions Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA. ✅
Posted by the CLA Assistant Lite bot.

Recognize GoUsageLimitError responses and parse both single and composite reset durations from their human-readable messages. Keep API-key accounts rate-limited until the parsed reset instead of falling back to the short default cooldown.
@william-drakemond
william-drakemond force-pushed the fix/opencode-go-usage-limit-reset branch from d87d4c8 to 5ea482f Compare August 15, 2026 00:01
@william-drakemond

Copy link
Copy Markdown
Author

I have read the CLA Document and I hereby sign the CLA

github-actions Bot added a commit that referenced this pull request Aug 15, 2026
ziyue67 added a commit to ziyue67/sub2api that referenced this pull request Aug 15, 2026
…i-Shaw#5487 (#29)

* ci: skip golangci-lint config verify to avoid schema fetch flakes

The action runs `golangci-lint config verify` before linting, which
downloads the JSON schema from golangci-lint.run at runtime. That fetch
timed out on main (run 31835621724, 21s failure) even though the code
and config were fine. Set verify: false so lint results no longer depend
on that external request.

* feat(opencode): 支持 OpenCode Go 官方用量窗口查询与自动刷新

OpenCode Go 上游(https://opencode.ai/zen/go/v1)官方提供用量端点
GET /zen/go/v1/usage(Bearer 复用消费 key),返回 rolling(5h)/weekly/monthly
三窗口的 percent 与 resetsAt。本提交为 openai apikey 账号(base_url 为
opencode.ai/zen/go/v1)接入该用量窗口:

- 后端:OpenCodeGoUsageService——手动刷新(每账号 10s 节流)、
  周期自动刷新(全局开关 + 账号开关双开启,interval 5-1440 分钟,
  leader lock + 每轮 20 账号 + 并发 4)、失败指数退避(上限 24h)、
  Retry-After 尊重;401→unauthorized、403→failed;snapshot 持久化于
  accounts.extra(单账号写入,无分组 CAS、无会话加密)
- admin API:settings GET/PUT、/:id/opencode-go-usage GET、
  auto-refresh PUT、refresh POST(与 Ollama Cloud usage 同构)
- 前端:账号列表行内三窗口进度条(复用 UsageProgressBar)、
  编辑弹窗内配置面板(详情+手动刷新+auto-refresh)、全局设置卡片、
  en/zh i18n

验证:
- 上游端点已用生产真实 Go key 实测 200,响应
  {"usage":{"rolling":{"status":"ok","percent":6,...},...}}(2026-08-13)
- go build ./... OK;service/handler/repo/dto/cmd 全量回归 OK
- 新增单测 20 例(解析 401/403/畸形 JSON、退避、节流等)全绿
- frontend vitest 新增 7 例 + Ollama 回归 4 例全绿;vue-tsc 零错误

* feat(opencode): 用量窗口支持同 Key 组共享、活动防抖与超窗强刷

与 Ollama Cloud usage 对齐:OpenCode Go 作为同 Key 聚合订阅,
多个 openai/apikey/base_url=opencode.ai/zen/go/v1 账号共享同一份
用量状态与刷新节奏:

- 组共享:组指纹 sha256("opencode.ai\0"+api_key);auto_refresh 开关
  与 snapshot 按组写(事务 + FOR NO KEY UPDATE + 锚点 CAS + 纯合并,
  写 snapshot 不会抹掉开关);列表/详情经 ResolveAccounts 组内共享;
  账号换 Key → IdentityChanged → 组级关闭 auto_refresh 并清快照
- 活动防抖 + 超窗强刷:settings 新增 debounce_minutes(默认 1,
  1-60,必须小于 interval_minutes);模型请求活动经网关 5 处调用点
  驱动刷新,due = min(lastUsed+debounce, fetchedAt+maxWait),
  成功路径有 5 分钟最小抓取间隔,失败路径退避优先
- 手动刷新限频 10s→30s 且按组;singleflight/RunDue 按组去重;
  ListDue 在 SQL 内按组算 due(CTE 分组取组内 MAX(last_used_at))

验证:go build + 五包回归 OK;新增组共享/三态 due/组单飞/
IdentityChanged/debounce 校验单测全绿;vitest 11 例全绿;
vue-tsc 零错误。

* fix(openai): honor OpenCode Go usage reset durations

Recognize GoUsageLimitError responses and parse both single and composite reset durations from their human-readable messages. Keep API-key accounts rate-limited until the parsed reset instead of falling back to the short default cooldown.

* fix(apicompat): Anthropic→Chat 桥回传工具调用的 thinking,修复 DeepSeek 多轮 400

/v1/messages 打到只支持 Chat Completions 的 OpenAI 兼容上游时,
anthropicAssistantToChatMessages 把历史 assistant 消息里的 thinking 块整块丢弃。
DeepSeek 的 thinking mode 要求产生工具调用的 reasoning_content 随该 assistant
消息回传,缺失即 400 —— 表现为「单轮正常,一进多轮工具对话必现失败」。

这些 thinking 块本来就是同一个文件的出站方向
(chatMessageToAnthropicBlocks) 用上游 reasoning_content 生成的,客户端只是
原样回传:桥自己造出来的东西,回来时被自己丢掉。

兄弟路径 Responses→Chat (buildChatMessagesFromItems 的 pendingReasoning) 早已
处理同一约束。本次把同一策略补到漏掉的这条桥上,作用域严格对齐:reasoning 只挂
到带 tool_calls 的 assistant 消息,纯文本轮次维持现状。

Fixes Wei-Shaw#5528

* fix(apicompat): chat/completions file part 不再被静默丢弃,转换为 Responses input_file

/v1/chat/completions → Responses 转换层此前只处理 text 和 image_url 两种
content part,type:"file"(PDF 附件)被静默丢弃:请求返回 200、模型正常
回答,但 prompt 里没有文件(prompt_tokens 只剩纯文字)。

现将 file part 映射为 Responses API 的 input_file(filename/file_data/
file_id 透传),与同网关 /v1/responses + input_file 实测可用的格式一致。
无 file_data 且无 file_id 的空 file part 跳过,与空 image URL 行为一致。

参考上游 PR Wei-Shaw#2497(因混入无关改动未合并)。

* fix: preserve fork dependency wiring when porting upstream PRs

---------

Co-authored-by: ziyue67 <ziyue67@users.noreply.github.com>
Co-authored-by: alfadb <alfadb@163.com>
Co-authored-by: William Drake <william.drakemond@gmail.com>
Co-authored-by: li <fengshao1227@gmail.com>
Co-authored-by: Pengap <penganpingprivte@gmail.com>
ziyue67 added a commit to ziyue67/sub2api that referenced this pull request Aug 15, 2026
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