Skip to content
Merged
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
7 changes: 7 additions & 0 deletions changelog/unreleased/qoder-quota-refresh-status.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
### English

- Make Qoder quota refresh failures visible to the account refresh endpoint while keeping account readiness intact, and run quota and model catalog refreshes independently.

### 中文

- 让 Qoder 额度刷新失败在账号刷新接口中可见,同时保持账号就绪状态不受影响,并让额度与模型目录刷新相互独立执行。
16 changes: 15 additions & 1 deletion internal/runtime/manager_probe.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ func (m *Manager) RefreshAll(ctx context.Context, forceQuota bool) error {
var joined error
for _, item := range m.pool.Items() {
if err := m.refreshOne(ctx, item, forceQuota); err != nil {
var quotaErr *QuotaRefreshError
if errors.As(err, &quotaErr) {
// Background maintenance must remain best-effort. The manual
// single-account endpoint still returns this typed error so the
// console can show that quota refresh did not complete.
continue
}
joined = errors.Join(joined, err)
}
}
Expand Down Expand Up @@ -75,8 +82,15 @@ func (m *Manager) refreshOne(ctx context.Context, item Item, forceQuota bool) er
// never changes readiness. A successful exhausted snapshot may still
// keep the account out of request routing.
if health.Hot || ready {
m.fetchQuota(ctx, item.ID, item.URL, forceQuota)
// Quota and model discovery are independent upstream operations. Run
// them concurrently so a quota outage cannot prevent catalog refresh.
quotaDone := make(chan error, 1)
go func() { quotaDone <- m.fetchQuota(ctx, item.ID, item.URL, forceQuota) }()
m.fetchAccountModels(ctx, item)
quotaErr := <-quotaDone
if quotaErr != nil {
return quotaErr
}
}
return nil
}
Expand Down
19 changes: 17 additions & 2 deletions internal/runtime/manager_quota.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package runtime

import (
"context"
"fmt"
"log"
"net/http"
"strings"
Expand All @@ -12,6 +13,13 @@ import (
"github.com/caigee-cmd/cli2api/internal/providers/qoder"
)

// QuotaRefreshError reports an upstream quota refresh failure without
// implying that the account itself is unhealthy or unavailable.
type QuotaRefreshError struct{ Err error }

func (e *QuotaRefreshError) Error() string { return e.Err.Error() }
func (e *QuotaRefreshError) Unwrap() error { return e.Err }

// Quota snapshots: pool MergeQuota then Store.SaveQuota. Worker JSON is Qoder-only;
// in-process providers use AccountProber.Quota.

Expand Down Expand Up @@ -132,14 +140,21 @@ func (m *Manager) persistQuota(ctx context.Context, accountID string, quota *Quo
}
}

func (m *Manager) fetchQuota(ctx context.Context, accountID, workerURL string, force bool) {
func (m *Manager) fetchQuota(ctx context.Context, accountID, workerURL string, force bool) error {
if strings.TrimSpace(workerURL) == "" {
return fmt.Errorf("quota worker URL is empty")
}
client := qoder.WorkerClient{
HTTP: &http.Client{Timeout: 5 * time.Second},
ProxyAPIKey: m.ProxyAPIKey(),
}
quota, err := client.Quota(ctx, workerURL, force)
if err != nil || quota == nil {
return
if err != nil {
return &QuotaRefreshError{Err: fmt.Errorf("fetch quota account=%s: %w", accountID, err)}
}
return &QuotaRefreshError{Err: fmt.Errorf("fetch quota account=%s: empty response", accountID)}
}
m.persistQuota(ctx, accountID, quota)
return nil
}
Loading