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:
1 parent
7464ee5075
commit
06584e15fc
2 files changed
+78
-12
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user