Files
yetone fa9daa6945 docs: every built-in subscription a plugin serves once moved says so in its code, and AGENTS.md says where a fix for it goes (user: 对内置的订阅 provider 在代码层面和文档层面加上注释,防止又有别的 agent 在改内置订阅 provider 的代码而不知道生效的其实是对应的插件代码) — a moved Cursor, Devin, Grok, Command Code plan, Kiro, Zed, Qoder, Factory, Xiaomi MiMo, ZCode or WorkBuddy (provider.Moved; since v0.1.642 also the default for a new sign-in, provider.Adopt) is signed in, listed, requested and metered by its @magpie-community plugin, and none of the built-in's code in internal/provider/<name>*.go, internal/gateway/<name>.go, internal/zed or internal/qoder serves it; a fix made there alone never reaches those users. Each of those files now opens with a PLUGIN-SERVED notice naming the subscription's id, its plugin package, where the plugin lives (magpie-community/plugins, packages/<name>) and the mover whose min to raise so moved users get the fixed plugin (keepMovedCurrent); Qoder's adds that Qoder CN isn't moved and still runs there. AGENTS.md (CLAUDE.md imports it) lists the moved built-ins with their packages and movers, how to tell whether a user's is moved (move.state "plugin", onPlugins), the order a fix goes in (the plugin, publish, raise min, the built-in only for those not moved), the magpie code that does run for a moved one (internal/plugin, plugins.go, plugin_usage.go, pluginsignin.go, migrate*.go, app.js's subOf/pluginSubs/startPluginSignIn), and the parity tests. movers in migrate.go and SUBS in app.js point there too. TestMovedBuiltinsSayTheirPlugin fails for a mover whose code carries no PLUGIN-SERVED notice naming its id and package (it failed while a wrapped package name was split). Comments and docs only, no behaviour changed. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestProvidersAnswerWhileListsComeIn and TestClaudeSignedOut failed once on their timing under load, pass alone; untouched).
2026-10-02 19:37:02 +08:00

136 lines
4.2 KiB
Go

package provider
// PLUGIN-SERVED (see AGENTS.md): Factory ("factory") is a deprecated
// built-in subscription served by its plugin,
// @magpie-community/opencode-factory-auth, once moved onto it
// (provider.Moved; the default for a new sign-in). A moved one's sign-ins,
// models, requests and usage are all the plugin's, never this code's (only
// the move, in migrate*.go, still reads its accounts). A fix here alone
// doesn't reach those users; fix the plugin
// (github.com/magpie-community/plugins, packages/factory) and raise the
// mover's min in internal/provider/migrate_factory.go.
// How much of a Factory plan an account has used, as droid's /status asks
// it: GET /api/billing/limits. Standard usage runs in rolling 5-hour, weekly
// and monthly windows, each a percent used and when it ends; Droid Core, the
// open models Factory hosts, has windows of its own; extra usage is a
// balance in cents.
import (
"context"
"encoding/json"
"errors"
"fmt"
"strconv"
"time"
)
type factoryWindow struct {
Used float64 `json:"usedPercent"`
End json.RawMessage `json:"windowEnd"`
}
type factoryPool struct {
FiveHour *factoryWindow `json:"fiveHour"`
Weekly *factoryWindow `json:"weekly"`
Monthly *factoryWindow `json:"monthly"`
}
type factoryLimits struct {
Limits struct {
Standard *factoryPool `json:"standard"`
Core *factoryPool `json:"core"`
} `json:"limits"`
Extra *float64 `json:"extraUsageBalanceCents"`
ExtraAllowed bool `json:"extraUsageAllowed"`
}
// factoryWhen reads a window's end: an ISO time or epoch milliseconds, as
// droid hands either to new Date.
func factoryWhen(raw json.RawMessage) *time.Time {
var s string
if json.Unmarshal(raw, &s) == nil && s != "" {
if t, err := time.Parse(time.RFC3339Nano, s); err == nil {
return &t
}
if n, err := strconv.ParseInt(s, 10, 64); err == nil && n > 0 {
t := time.UnixMilli(n).UTC()
return &t
}
return nil
}
var n float64
if json.Unmarshal(raw, &n) == nil && n > 0 {
t := time.UnixMilli(int64(n)).UTC()
return &t
}
return nil
}
// factoryQuota turns the limits into windows: standard's by their span,
// then Droid Core's, then the extra usage left. For routing, standard counts
// the vendors' models and Core the open ones Factory hosts; with extra usage
// to spend, a window used up stops neither.
func factoryQuota(l factoryLimits) []QuotaWindow {
var out []QuotaWindow
extra := l.ExtraAllowed && l.Extra != nil && *l.Extra > 0
add := func(p *factoryPool, prefix string, core bool) {
if p == nil {
return
}
for _, w := range []struct {
win *factoryWindow
name string
span time.Duration
}{
{p.FiveHour, "5 hours", 5 * time.Hour},
{p.Weekly, "7 days", 7 * 24 * time.Hour},
{p.Monthly, "30 days", 30 * 24 * time.Hour},
} {
if w.win == nil {
continue
}
out = append(out, QuotaWindow{Name: prefix + w.name, Used: min(max(w.win.Used, 0), 100),
Span: w.span, ResetsAt: factoryWhen(w.win.End), Aside: extra,
matches: func(model string) bool { return factoryCore(model) == core }})
}
}
add(l.Limits.Standard, "", false)
add(l.Limits.Core, "Droid Core · ", true)
if l.Extra != nil && *l.Extra > 0 {
out = append(out, QuotaWindow{Name: "Extra usage", Display: fmt.Sprintf("$%.2f", *l.Extra/100), Aside: true})
}
return out
}
func factoryLoginQuota(ctx context.Context, l Login) SubscriptionQuota {
q := SubscriptionQuota{Provider: "factory", Name: "Factory", Icon: "factory",
Plan: l.Plan, User: l.User, Windows: []QuotaWindow{}}
c, err := factoryFresh(ctx, l.User)
if err != nil {
q.Error = err.Error()
return q
}
ctx, cancel := context.WithTimeout(ctx, 15*time.Second)
defer cancel()
var lim factoryLimits
err = factoryGet(ctx, c, "/api/billing/limits", nil, &lim)
var st *factoryStatus
if errors.As(err, &st) && factoryMendOrg(ctx, l.User, st.Code, []byte(st.Msg)) {
// the org it named was put right: ask once more
if c, err = factoryFresh(ctx, l.User); err == nil {
err = factoryGet(ctx, c, "/api/billing/limits", nil, &lim)
}
}
if err != nil {
q.Error = err.Error()
return q
}
if lim.Limits.Standard == nil {
q.Error = "Factory: the account reported no limits"
return q
}
q.Windows = factoryQuota(lim)
return q
}