providers: ClinePass lists the Cline API's free models beside the plan's (lml on Discord: Cline里面的免费模型是否也可以使用呢?) — a Cline key is served the API's ":free" models (qwen/qwen3.8-27b:free, google/gemma-4-31b-it:free, nvidia/nemotron-3-super-120b-a12b:free…) at no cost, so the preset's new FreeTag ":free" keeps the ids of the fetched list ending in it after the plan's cline-pass/ models (the plan's own list when the API names none of them, as before), marked Free, which the model list already badges FREE ("free: it doesn't use the plan's credits") as it does WorkBuddy's and Command Code's free models. Its paid models are still left out, and before the list is fetched only the plan's models show, as before: no free list is written down. ListPrice prices a FreeTag model at zero, where it was priced as the model without its tag (":free" is cut off when matching models.dev), so the usage ledger charges it nothing. The free models were not tried with a real Cline account (no key at hand). TestPlanModels fails without the change (the free models are dropped, none is marked Free, and one has no price). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./internal/provider/ ./internal/gateway/ pass.
This commit is contained in:
1 parent
0f41570859
commit
a396260204
3 files changed
+42
-4
No files matched your search
@@ -1,6 +1,7 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/yetone/magpie/internal/catalog"
|
||||
@@ -20,6 +21,21 @@ func TestPlanModels(t *testing.T) {
|
||||
if got := p.planModels([]catalog.Model{{ID: "x/y"}}); len(got) != len(Preset("clinepass").Models) || got[0].ID != "cline-pass/glm-5.3" {
|
||||
t.Fatalf("plan's: %+v", got)
|
||||
}
|
||||
// the Cline API's free models are kept beside the plan's, marked free,
|
||||
// while its paid ones are still left out (lml on Discord)
|
||||
got = p.planModels([]catalog.Model{{ID: "anthropic/claude-opus-5-5"}, {ID: "qwen/qwen3.8-27b:free"}, {ID: "google/gemma-4-31b-it:free"}})
|
||||
if n := len(Preset("clinepass").Models); len(got) != n+2 || got[0].ID != "cline-pass/glm-5.3" || got[n].ID != "qwen/qwen3.8-27b:free" || got[n+1].ID != "google/gemma-4-31b-it:free" {
|
||||
t.Fatalf("free: %+v", got)
|
||||
}
|
||||
for _, m := range got {
|
||||
if m.Free != strings.HasSuffix(m.ID, ":free") {
|
||||
t.Fatalf("free flag: %+v", m)
|
||||
}
|
||||
}
|
||||
// a free model costs nothing, not its tagless model's price
|
||||
if pr, ok := p.ListPrice("qwen/qwen3.8-27b:free"); !ok || pr != (catalog.Price{}) {
|
||||
t.Fatalf("free price: %+v %v", pr, ok)
|
||||
}
|
||||
// another provider's list is left as it is
|
||||
o, _ := FromPreset("opencode-go")
|
||||
if got := o.planModels([]catalog.Model{{ID: "a"}, {ID: "b"}}); len(got) != 2 {
|
||||
|
||||
@@ -308,10 +308,15 @@ func (p Provider) planModels(ms []catalog.Model) []catalog.Model {
|
||||
if pr == nil || pr.Only == "" && (len(pr.Models) == 0 || len(ms) > 0) {
|
||||
return ms
|
||||
}
|
||||
var out []catalog.Model
|
||||
var out, free []catalog.Model
|
||||
for _, m := range ms {
|
||||
if strings.HasPrefix(m.ID, pr.Only) {
|
||||
switch {
|
||||
case strings.HasPrefix(m.ID, pr.Only):
|
||||
out = append(out, m)
|
||||
case p.freeTagged(m.ID):
|
||||
// the vendor's free models, which the key is served too
|
||||
m.Free = true
|
||||
free = append(free, m)
|
||||
}
|
||||
}
|
||||
if len(out) == 0 {
|
||||
@@ -319,6 +324,7 @@ func (p Provider) planModels(ms []catalog.Model) []catalog.Model {
|
||||
out = append(out, catalog.Model{ID: id, Name: id})
|
||||
}
|
||||
}
|
||||
out = append(out, free...)
|
||||
for i, m := range out {
|
||||
// the vendor's window for its model, as models.dev has it
|
||||
if m.Context == 0 {
|
||||
@@ -331,6 +337,13 @@ func (p Provider) planModels(ms []catalog.Model) []catalog.Model {
|
||||
return out
|
||||
}
|
||||
|
||||
// freeTagged reports whether model is one of the vendor's free models p's
|
||||
// preset keeps beside its plan's (PresetDef.FreeTag): Cline's ":free" ones.
|
||||
func (p Provider) freeTagged(model string) bool {
|
||||
pr := Preset(p.Preset)
|
||||
return pr != nil && pr.Only != "" && pr.FreeTag != "" && strings.HasSuffix(model, pr.FreeTag)
|
||||
}
|
||||
|
||||
// fixV1 adds the /v1 an OpenAI-style base URL was given without, when
|
||||
// the models were found only under it: base/models didn't answer and
|
||||
// base/v1/models did. The list is asked for at both, but a request goes
|
||||
@@ -598,6 +611,10 @@ var makerCatalogs = sync.OnceValue(func() []string {
|
||||
// at gpt-6-astra's or gemini-3.8-flash's maker's price, as a Claude
|
||||
// account is at Anthropic's.
|
||||
func (p Provider) ListPrice(model string) (catalog.Price, bool) {
|
||||
if p.freeTagged(model) {
|
||||
// served at no cost: not at the price of the model it is a tag of
|
||||
return catalog.Price{}, true
|
||||
}
|
||||
for _, m := range pricedNames(model) {
|
||||
if pr, ok := catalog.PricedBy(p.Catalogs(), m); ok {
|
||||
return pr, true
|
||||
|
||||
@@ -39,6 +39,10 @@ type PresetDef struct {
|
||||
// Models are the plan's, for when the list has none of them.
|
||||
Only string `json:"only,omitempty"`
|
||||
Models []string `json:"models,omitempty"`
|
||||
// FreeTag, with Only, is what the ids of the vendor's free models end
|
||||
// in: they are kept beside the plan's, marked Free and priced at
|
||||
// nothing (Cline's ":free" models, which its key is served at no cost).
|
||||
FreeTag string `json:"freeTag,omitempty"`
|
||||
// NoList: the vendor has no list of models to ask for (Bedrock's
|
||||
// runtime serves no /models), so Models are its list
|
||||
NoList bool `json:"noList,omitempty"`
|
||||
@@ -285,12 +289,13 @@ var presets = []PresetDef{
|
||||
Note: "open coding models, $10/month",
|
||||
Website: "https://opencode.ai/docs/go", KeysURL: "https://opencode.ai/auth"},
|
||||
// Cline's plan for open models, at the Cline API with a Cline key: the
|
||||
// API lists only its paid models, not the plan's
|
||||
// API lists only its paid models, not the plan's, and its free ones
|
||||
// (":free"), which the key is served too
|
||||
{ID: "clinepass", Name: "ClinePass", Icon: "cline", Kind: KindRelay,
|
||||
Chat: "https://api.cline.bot/api/v1",
|
||||
Note: "open coding models, $9.99/month",
|
||||
Website: "https://docs.cline.bot/getting-started/clinepass", KeysURL: "https://app.cline.bot",
|
||||
Only: "cline-pass/",
|
||||
Only: "cline-pass/", FreeTag: ":free",
|
||||
Models: []string{"cline-pass/glm-5.3", "cline-pass/glm-5.3-flash", "cline-pass/kimi-k3", "cline-pass/deepseek-v4-pro",
|
||||
"cline-pass/deepseek-v4.1-flash", "cline-pass/mimo-v2.5", "cline-pass/mimo-v2.5-pro", "cline-pass/minimax-m3",
|
||||
"cline-pass/muse-spark-1.3-contributor", "cline-pass/qwen3.8-max", "cline-pass/qwen3.7-max", "cline-pass/qwen3.7-plus"}},
|
||||
|
||||
Reference in new issue
Block a user