Files
magpie/internal/provider/migrate_qoder.go
T
yetone b3ab3e42e9 gateway: a request well past the model's window that a vendor turns away without saying why reaches the agent as too long, at once; Qoder's plugin to 0.2.4 for its token usage (found testing real accounts) — Devin answered a conversation of about 300k tokens for swe-1.7-lightning (a 202,752 window) with a 502 "We are currently experiencing capacity issues…", Qoder one of 1.8M for qmodel (1M) with a 400 "Error in upstream response", each answered at a size the model holds; magpie asked Devin's again in place three times and handed the agent a 502 api_error, so Claude Code and Codex never compacted. tooLongUnsaid (fallback.go) takes a 400/413/422/5xx that says nothing of quota, a rate limit, the account, the model or the conversation's length, for a request whose estimate is 1.2 times the model's window or more (the user's context, else the catalog's; estimate counts four bytes a token, which most text runs under: Devin counted 173,954 tokens of one estimated at 151,851), as one too long: "prompt is too long: about N tokens, more than <model>'s context window of M (<vendor> answered <status>: …)", sent the agent's way (context_length_exceeded, Anthropic's "prompt is too long"), not asked again of it or another account, and nobody rests. TestOverflowTheVendorDidntSay fails on the old code (a 502 asked three times, a 400 handed on) and passes; a request near the window, a small one, a 429, a 401 and a used-up balance keep their errors. Qoder's plugin read its stream only up to the finish, and Qoder sends the usage in a chunk of its own after it, so every Qoder request was counted as 0 tokens: @magpie-community/opencode-qoder-auth 0.2.4 reads it, and the mover's min is raised to it. Tried with real accounts in a sandbox magpie: Devin (chat and Anthropic) and Qoder now give 400 context_length_exceeded after one try, a WorkBuddy and Kiro overflow as before, small requests to all three answer, and Qoder reports its tokens (streamed, not, and with a tool call). go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass but a provider timing test that passes rerun.
2026-10-03 16:14:38 +08:00

102 lines
3.2 KiB
Go

package provider
import (
"encoding/json"
"errors"
"time"
"github.com/yetone/magpie/internal/qoder"
)
// Qoder's and Qoder CN's accounts on @magpie-community/opencode-qoder-auth,
// which serves both sites as providers "qoder" and "qoder-cn", as its device
// sign-in keeps one: the job token pair as access and refresh (the refresh
// token is spent on use, so it moves rather than being copied), the device
// token pair and machine id beside them. A Qoder CN account that chats on
// its device token (DeviceChat) says so, its device pair as access and
// refresh.
func init() {
for _, id := range []string{"qoder", QoderCNID} {
movers[id] = qoderMover(id)
}
}
func qoderMover(id string) *mover {
return &mover{
pkg: "@magpie-community/opencode-qoder-auth",
min: "0.2.4", // Qoder CN as "qoder-cn", its device-token chat as the built-in's; each model's credit rate; the sign-in renewed ahead of time through auth.refresh; token usage read
agents: []string{id},
out: func() ([]Moving, error) {
// no refresh of the built-in's runs while the pairs are read
ls := qoderLoginsOf(id)
qoderMu.Lock()
defer qoderMu.Unlock()
var out []Moving
for _, l := range ls {
c, ok, _ := qoderCurrent(l.saved)
if !ok {
continue
}
out = append(out, Moving{User: l.User, First: l.Active, On: l.On, Lapsed: l.Lapsed != "", Plan: l.Plan, Auth: map[string]any{
"type": "oauth",
"access": c.Token,
"refresh": c.RefreshToken,
"expires": c.ExpiresAt,
"accountId": l.User,
"uid": c.UID,
"email": c.Email,
"name": c.Name,
"machineId": c.MachineID,
"deviceToken": c.DeviceToken,
"deviceRefresh": c.DeviceRefresh,
}})
if c.DeviceChat {
out[len(out)-1].Auth["deviceChat"] = true
}
}
return out, nil
},
back: func(ls []savedLogin, user string, auth map[string]any) ([]savedLogin, string, error) {
access, uid := str(auth["access"]), str(auth["uid"])
if access == "" || uid == "" {
return ls, "", errors.New(qoder.SiteOf(id).Name + ": an unreadable plugin sign-in")
}
if user == "" {
user = firstNonEmpty(str(auth["accountId"]), str(auth["email"]), uid)
}
i := backInto(&ls, id, user)
c, _ := qoderSaved(ls[i])
c.UID, c.Token, c.RefreshToken, c.ExpiresAt = uid, access, str(auth["refresh"]), num(auth["expires"])
c.Email = firstNonEmpty(str(auth["email"]), c.Email)
c.Name = firstNonEmpty(str(auth["name"]), c.Name)
c.MachineID = firstNonEmpty(str(auth["machineId"]), c.MachineID)
c.DeviceToken = firstNonEmpty(str(auth["deviceToken"]), c.DeviceToken)
c.DeviceRefresh = firstNonEmpty(str(auth["deviceRefresh"]), c.DeviceRefresh)
c.DeviceChat = auth["deviceChat"] == true
b, err := json.Marshal(c)
if err != nil {
return ls, "", err
}
ls[i].Auth, ls[i].Lapsed, ls[i].Seen = b, "", time.Now().UTC().Truncate(time.Second)
return ls, ls[i].User, nil
},
}
}
// num is a JSON number read into any (a float64, or an int64 before it
// was written out) as an int64.
func num(v any) int64 {
switch n := v.(type) {
case float64:
return int64(n)
case int64:
return n
case int:
return int64(n)
case json.Number:
i, _ := n.Int64()
return i
}
return 0
}