qoder: a model is free only at a price of 0, as Qoder's own client shows it (01huadalang on Discord: Qoder shows Qwen3.8-Max at 0.5× credits with its off-peak 40% discount, magpie called it free) — Qoder's listing has is_free true on Qwen3.8-Max at a price_factor of 0.5, and the built-in took either is_free or a price of 0 for free. Now, as the plugin's freeOf does (magpie-community/plugins 6fd444c): a listed price_factor (priceFactor) says it, free only at 0; a price of 0 only while an active promotion lasts, with a price before it (before_promotion_price_factor > 0), is a discount, not free; a limited-time 0× (Qwen3.8-Flash, its 0.1× struck through) is free while it is; is_free counts only on a listing with no price. TestParseModelsFreeIsThePrice, with the listing's real entries (Qwen3.8-Max's off-peak promotion, Qwen3.8-Flash's original_price_factor), fails without the change (Qwen3.8-Max and the promotion-window models read free) and passes with it; TestParseModelsFree's is_free-at-price-1 now reads paid. No request was made to Qoder. Moved Qoder users get it from opencode-qoder-auth 0.2.1 (the mover's min stays 0.2.0 until that is on npm).

This commit is contained in:
yetone committed 2026-10-02 22:00:34 +08:00
1 parent 7464ee5075
commit 06584e15fc
2 files changed
+78 -12

No files matched your search

+42 -9
View File
@@ -39,19 +39,37 @@ type ModelInfo struct {
AlwaysThinks bool `json:"-"`
DefaultEffort string `json:"-"`
// Free is set on a model that costs the plan no credits, as Qoder's
// client reads it: is_free, or a price_factor of 0.
// client shows it: a price_factor of 0 (see free).
Free bool `json:"-"`
}
// free reads whether the listing marks a model free: is_free true, or a
// price_factor (the credits a request costs, as a multiple) of 0. Either
// may come in snake or camel case.
// promotion is a listing entry's running discount: a price_factor of 0
// while it is active, with a price before it, is a discount, not a free
// model.
type promotion struct {
Active bool `json:"active"`
Before *float64 `json:"before_promotion_price_factor"`
BeforeC *float64 `json:"beforePromotionPriceFactor"`
}
// free reads whether a model costs the plan no credits: a price_factor (the
// credits a request costs, as a multiple; priceFactor in camel case) of 0,
// the price Qoder's own client shows ("0×"). is_free is no word on that:
// Qoder's listing has it true on Qwen3.8-Max at 0.5×, an off-peak discount
// (错峰 4 折) on it, and Qoder's client shows the price, not it. It is taken
// only from a listing with no price at all. A price of 0 for the while an
// active promotion lasts, with a price before it, is a discount, not a free
// model; a limited-time free model (Qwen3.8-Flash: 0×, its
// original_price_factor 0.1 struck through) is free while it is. The same
// rule as the plugin's freeOf (packages/qoder/index.mjs).
func (m *ModelInfo) free(raw json.RawMessage) {
var v struct {
IsFree *bool `json:"is_free"`
IsFreeC *bool `json:"isFree"`
PriceFactor *float64 `json:"price_factor"`
PriceC *float64 `json:"priceFactor"`
IsFree *bool `json:"is_free"`
IsFreeC *bool `json:"isFree"`
PriceFactor *float64 `json:"price_factor"`
PriceC *float64 `json:"priceFactor"`
Promotion *promotion `json:"promotion"`
PromotionT *promotion `json:"prommotion"`
}
if json.Unmarshal(raw, &v) != nil {
return
@@ -62,7 +80,22 @@ func (m *ModelInfo) free(raw json.RawMessage) {
if v.PriceFactor == nil {
v.PriceFactor = v.PriceC
}
m.Free = v.IsFree != nil && *v.IsFree || v.PriceFactor != nil && *v.PriceFactor == 0
if v.Promotion == nil {
v.Promotion = v.PromotionT
}
if v.PriceFactor != nil {
if *v.PriceFactor != 0 {
m.Free = false
return
}
p := v.Promotion
if p != nil && p.Before == nil {
p.Before = p.BeforeC
}
m.Free = !(p != nil && p.Active && p.Before != nil && *p.Before > 0)
return
}
m.Free = v.IsFree != nil && *v.IsFree
}
// effortOrder ranks Qoder's effort names, lowest first.
+36 -3
View File
@@ -2,8 +2,8 @@ package qoder
import "testing"
// The listing marks a model free with is_free or a price_factor of 0, in
// snake or camel case; Qoder's client shows either as free.
// A model at a price_factor of 0 is free, in snake or camel case; is_free
// counts only on a listing with no price.
func TestParseModelsFree(t *testing.T) {
body := []byte(`{"chat":[
{"key":"flag","enable":true,"is_free":true,"price_factor":1},
@@ -17,7 +17,40 @@ func TestParseModelsFree(t *testing.T) {
if err != nil {
t.Fatal(err)
}
want := map[string]bool{"flag": true, "camel-flag": true, "zero": true, "camel-zero": true, "paid": false, "unsaid": false}
want := map[string]bool{"flag": false, "camel-flag": true, "zero": true, "camel-zero": true, "paid": false, "unsaid": false}
if len(ms) != len(want) {
t.Fatalf("models %+v", ms)
}
for _, m := range ms {
if m.Free != want[m.ID] {
t.Errorf("%s: Free = %v, want %v", m.ID, m.Free, want[m.ID])
}
}
}
// Qoder's listing as it came on 2026-10-02: its app shows Qwen3.8-Max at
// 0.5× with an off-peak badge (错峰 4 折), Qwen3.8-Flash at 0× with 0.1×
// struck through — both carry is_free true. Only Flash is free.
func TestParseModelsFreeIsThePrice(t *testing.T) {
offPeak := `{"active":false,"badge":{"en":"Off-Peak 60% off","zh":"错峰 4 折"},"timezone":"Asia/Singapore","rule_id":"idle_time_model_credit_discount","discount_factor":0.4,"before_promotion_price_factor":0.5,"window_start":"22:00","window_end":"08:00"}`
body := []byte(`{"chat":[
{"key":"qmodel_38max","display_name":"Qwen3.8-Max","enable":true,"is_reasoning":true,"price_factor":0.5,"is_free":true,"promotion":` + offPeak + `},
{"key":"qfmodel","display_name":"Qwen3.8-Flash","enable":true,"is_reasoning":true,"price_factor":0.0,"original_price_factor":0.1,"is_free":true},
{"key":"qmodel_latest","display_name":"Qwen3.7-Max","enable":true,"price_factor":0.5,"original_price_factor":0.5},
{"key":"kmodel_latest","display_name":"Kimi-K3","enable":true,"price_factor":1.4,"is_free":false},
{"key":"window","enable":true,"price_factor":0,"is_free":true,"promotion":{"active":true,"discount_factor":0.0001,"before_promotion_price_factor":0.5}},
{"key":"window-camel","enable":true,"priceFactor":0,"promotion":{"active":true,"beforePromotionPriceFactor":1}},
{"key":"over","enable":true,"price_factor":0,"promotion":{"active":false,"before_promotion_price_factor":0.5}},
{"key":"nothing","enable":true,"price_factor":0,"promotion":{"active":true,"before_promotion_price_factor":0}},
{"key":"unpriced","enable":true,"is_free":true},
{"key":"unpriced2","enable":true,"is_free":false}
]}`)
ms, err := ParseModels(body, ProviderKey)
if err != nil {
t.Fatal(err)
}
want := map[string]bool{"qmodel_38max": false, "qfmodel": true, "qmodel_latest": false, "kmodel_latest": false,
"window": false, "window-camel": false, "over": true, "nothing": true, "unpriced": true, "unpriced2": false}
if len(ms) != len(want) {
t.Fatalf("models %+v", ms)
}