provider: OpenRouter's decision models are listed with their window and image input, on the built-in OpenRouter too (ARNO on Discord: 手动添加的 systemone provider 填 openrouter,模型列表没有图像和上下文信息;默认的 openrouter provider 列表里又没有全部的 decision 模型) — three causes. A System One provider at OpenRouter whose list was fetched before v0.1.895 kept that list, without windows or input, since lists are fetched again only on Save or Refresh: FetchNew now fetches such a list again (decideListDue: on, at OpenRouter, missing or with no model's facts), once. One added with no list URL got typesafe/jev-router only, which is OpenRouter's chat router named like Jev: it now fetches OpenRouter's /models?output_modalities=decisions. And the built-in OpenRouter had no decision API at all: its preset (and a saved one, in Normalize as Zen's) gets Decide at https://openrouter.ai/api/v1, its decision list is fetched beside its chat list (kept at <id>.decisions, renamed with it), shown in its model list with context and image input (/api/providers deciders), offered to routing groups' classifier picker and asked at /systemone with the same key; Jev Router stays a chat model for agents (DecidesModel, app.js decidesModel). With the owner's real OpenRouter key in a sandbox HOME, FetchNew listed all 13 decision models with their facts (pplx-decider 262144 + images, solar-decide 524288, …) and the gateway's POST /v1/systemone with openrouter/inception/mercury-decide:free answered 200 from Inception. TestOpenRouterListsDecisionModels and TestOpenRouterDecisionListRefreshed fail on the old code ("OpenRouter has no decision API", "decision models: [typesafe/jev-router]"). system-one, gateway-systemone, gateway-decide-info, group-found-off, routing-reset-deadline pass on WebKit; group-from-model-add fails 2 the same on origin/main. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
This commit is contained in:
1 parent
60b301a9cd
commit
372316260d
8 files changed
+331
-8
No files matched your search
@@ -6457,7 +6457,9 @@ const PROTOS = [["chat", "OpenAI", "Chat Completions — most agents"], ["respon
|
||||
// apiLabel: the name an API (a protocol) goes by in the editor
|
||||
const apiLabel = (proto) => (PROTOS.find(([k]) => k === proto) || [])[1] || proto;
|
||||
const decideOnly = (p) => !!p?.decide && !(p.chat || p.responses || p.anthropic);
|
||||
const decidesModel = (p, id) => !!p.decide && (decideOnly(p) || id.split("/").some((s) => /^jev(?:-|$)/i.test(s)));
|
||||
// a provider that lists its decision models apart (OpenRouter) says which
|
||||
// they are: its Jev Router (typesafe/jev-router) is a chat model
|
||||
const decidesModel = (p, id) => !!p.decide && (decideOnly(p) || (p.deciders ? p.deciders.includes(id) : id.split("/").some((s) => /^jev(?:-|$)/i.test(s))));
|
||||
// modelAPIs: the APIs one of p's models can be asked on alone, those it has
|
||||
// a URL for when it has more than one — a custom provider's, a preset's or
|
||||
// a subscription's alike (01huadalang on Discord: 一个 api 里有很多模型但是不同协议;
|
||||
|
||||
@@ -99,6 +99,9 @@ type providerJSON struct {
|
||||
// System One question (provider.AsksDecideModels): a mixed
|
||||
// provider's Jev too, beside its conversation models
|
||||
DecideTest bool `json:"decideTest,omitempty"`
|
||||
// Deciders are its decision models when it lists them apart from its
|
||||
// chat models (OpenRouter's): those, and no Jev-named chat model
|
||||
Deciders []string `json:"deciders,omitempty"`
|
||||
|
||||
Key struct {
|
||||
Set bool `json:"set"`
|
||||
@@ -482,6 +485,15 @@ func providerInfo(p provider.Provider, agents []agentUse) providerJSON {
|
||||
seen[m.ID] = true
|
||||
out.Models = append(out.Models, named(m, exposed[m.ID]))
|
||||
}
|
||||
// OpenRouter's decision models, listed apart from its chat models
|
||||
// (ARNO on Discord), after them
|
||||
for _, m := range p.DecisionModels() {
|
||||
out.Deciders = append(out.Deciders, m.ID)
|
||||
if !seen[m.ID] {
|
||||
seen[m.ID] = true
|
||||
out.Models = append(out.Models, named(m, exposed[m.ID]))
|
||||
}
|
||||
}
|
||||
// picks the vendor list does not know go first, so they are visible
|
||||
for _, m := range p.Exposed() {
|
||||
if !seen[m.ID] {
|
||||
|
||||
@@ -72,7 +72,70 @@ func (p Provider) DecideOnly() bool {
|
||||
// DecidesModel distinguishes Jev from the conversation models a gateway
|
||||
// also serves. A dedicated decision API may use any model name.
|
||||
func (p Provider) DecidesModel(model string) bool {
|
||||
return p.Decides() && (p.DecideOnly() || jevID(model) || p.DecideVia() == ViaCloudflare && CloudflareClef(model))
|
||||
if !p.Decides() {
|
||||
return false
|
||||
}
|
||||
if p.DecideOnly() {
|
||||
return true
|
||||
}
|
||||
if p.listsDecisions() {
|
||||
// OpenRouter's Jev Router (typesafe/jev-router) is a chat model
|
||||
return slices.ContainsFunc(p.decideListed(), func(m catalog.Model) bool { return m.ID == model })
|
||||
}
|
||||
return jevID(model) || p.DecideVia() == ViaCloudflare && CloudflareClef(model)
|
||||
}
|
||||
|
||||
// OpenRouter lists its decision models apart from its chat models (ARNO
|
||||
// on Discord): its /models has the chat ones alone, Jev Router among them
|
||||
// (typesafe/jev-router, a chat model that picks one), and
|
||||
// /models?output_modalities=decisions the ones that answer System One's
|
||||
// questions at /systemone (liquid/d1, cloudflare/clef, ~typesafe/jev-latest
|
||||
// …), each with its context_length and input_modalities.
|
||||
func openRouterDecisions(base string) string {
|
||||
if HostOf(base) != "openrouter.ai" {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimRight(base, "/") + "/models?output_modalities=decisions"
|
||||
}
|
||||
|
||||
// listsDecisions reports whether p's decision models are a list of their
|
||||
// own, OpenRouter's, rather than Jev's ids among its chat models.
|
||||
func (p Provider) listsDecisions() bool { return openRouterDecisions(p.Decide) != "" }
|
||||
|
||||
// decisionsID is where a provider that serves conversations too keeps the
|
||||
// list of its decision models, beside its chat models' list.
|
||||
func decisionsID(id string) string { return id + ".decisions" }
|
||||
|
||||
// DecisionModels are the decision models a provider that serves
|
||||
// conversations too lists apart from them (OpenRouter's), for its editor
|
||||
// to show beside its chat models; nil for any other provider.
|
||||
func (p Provider) DecisionModels() []catalog.Model {
|
||||
if p.DecideOnly() || !p.listsDecisions() {
|
||||
return nil
|
||||
}
|
||||
return p.decideModels()
|
||||
}
|
||||
|
||||
// decideListDue reports whether p's list of decision models at OpenRouter
|
||||
// is still to be fetched, or was fetched before magpie kept each model's
|
||||
// window and input (they show on the Providers page and the Gateway list
|
||||
// only once it is fetched again).
|
||||
func (p Provider) decideListDue() bool {
|
||||
if !p.On() || !p.Decides() {
|
||||
return false
|
||||
}
|
||||
id := p.ID
|
||||
if p.DecideOnly() {
|
||||
if !p.listsDecisions() && HostOf(strings.TrimSpace(p.ModelsURL)) != "openrouter.ai" {
|
||||
return false
|
||||
}
|
||||
} else if p.listsDecisions() {
|
||||
id = decisionsID(p.ID)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
live, _, ok := catalog.Live(id)
|
||||
return !ok || !slices.ContainsFunc(live, func(m catalog.Model) bool { return m.Context > 0 || m.ImageInput != nil })
|
||||
}
|
||||
|
||||
// Cloudflare's own decision models on Workers AI (ARNO on Discord): Clef
|
||||
@@ -80,6 +143,9 @@ func (p Provider) DecidesModel(model string) bool {
|
||||
// run at its own address (…/ai/run/@cf/cloudflare/clef) and takes the
|
||||
// request as it is, its model named "clef" or "clef-flash", where Jev is
|
||||
// a run of typesafe/jev with the questions as its input.
|
||||
// OpenRouterJev is Jev as OpenRouter names it, the latest one.
|
||||
const OpenRouterJev = "~typesafe/jev-latest"
|
||||
|
||||
const (
|
||||
CloudflareJev = "typesafe/jev"
|
||||
CloudflareClefModel = "@cf/cloudflare/clef"
|
||||
@@ -140,6 +206,15 @@ func (p Provider) Jev() string {
|
||||
if own := p.ownDecideModels(); len(own) > 0 {
|
||||
return own[0].ID
|
||||
}
|
||||
// OpenRouter's Jev is ~typesafe/jev-latest, in its list of decision
|
||||
// models; its Jev Router is a chat model
|
||||
if p.listsDecisions() || p.DecideOnly() && HostOf(strings.TrimSpace(p.ModelsURL)) == "openrouter.ai" {
|
||||
ms := p.decideListed()
|
||||
if i := slices.IndexFunc(ms, func(m catalog.Model) bool { return jevID(m.ID) }); i >= 0 {
|
||||
return ms[i].ID
|
||||
}
|
||||
return OpenRouterJev
|
||||
}
|
||||
// a gateway that serves conversations too names its Jev its own way
|
||||
// (OpenCode Zen's jev-1.13): the one its list has, the free one where
|
||||
// it is asked with no key of the user's
|
||||
@@ -217,6 +292,12 @@ func withDecideFacts(ms []catalog.Model) []catalog.Model {
|
||||
}
|
||||
|
||||
func (p Provider) decideListed() []catalog.Model {
|
||||
if p.listsDecisions() && !p.DecideOnly() {
|
||||
if live, _, ok := catalog.Live(decisionsID(p.ID)); ok && len(live) > 0 {
|
||||
return live
|
||||
}
|
||||
return []catalog.Model{{ID: OpenRouterJev, Name: "Jev"}}
|
||||
}
|
||||
if live, _, ok := catalog.Live(p.ID); ok && len(live) > 0 {
|
||||
if p.DecideOnly() {
|
||||
return live
|
||||
@@ -374,7 +455,7 @@ func Deciders() []Entry {
|
||||
continue
|
||||
}
|
||||
ms := p.Exposed()
|
||||
if !p.DecideOnly() && len(p.Models) == 0 {
|
||||
if !p.DecideOnly() && (len(p.Models) == 0 || p.listsDecisions()) {
|
||||
ms = p.decideModels() // the conversation picker's limit does not hide Jev
|
||||
}
|
||||
for _, m := range ms {
|
||||
@@ -580,6 +661,8 @@ func (p Provider) fetchDecide(ctx context.Context) ([]catalog.Model, error) {
|
||||
list, all := p.Decide+"/models", false
|
||||
if u := strings.TrimSpace(p.ModelsURL); u != "" && p.DecideOnly() {
|
||||
list, all = u, true
|
||||
} else if u := openRouterDecisions(p.Decide); u != "" {
|
||||
list, all = u, true
|
||||
} else if bailianDecides(p.Decide) {
|
||||
// Bailian lists its chat models only: its decision model is asked
|
||||
return p.decideAsked(ctx)
|
||||
@@ -617,6 +700,9 @@ func (p Provider) fetchDecide(ctx context.Context) ([]catalog.Model, error) {
|
||||
return nil, fmt.Errorf("%s lists no models", p.Name)
|
||||
}
|
||||
if !p.DecideOnly() {
|
||||
if p.listsDecisions() {
|
||||
return ms, catalog.SaveLive(decisionsID(p.ID), p.Decide, ms)
|
||||
}
|
||||
return ms, nil // do not replace a mixed provider's full model list
|
||||
}
|
||||
return ms, catalog.SaveLive(p.ID, p.Decide, ms)
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"slices"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/yetone/magpie/internal/catalog"
|
||||
)
|
||||
|
||||
// fakeOpenRouter answers for openrouter.ai: its chat models at /models,
|
||||
// Jev Router among them, its decision models at
|
||||
// /models?output_modalities=decisions, and System One at /systemone.
|
||||
func fakeOpenRouter(t *testing.T) (asked func() []string, lists func() int) {
|
||||
t.Helper()
|
||||
var mu sync.Mutex
|
||||
var models []string
|
||||
decisions := 0
|
||||
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/api/v1/models":
|
||||
if r.URL.Query().Get("output_modalities") == "decisions" {
|
||||
mu.Lock()
|
||||
decisions++
|
||||
mu.Unlock()
|
||||
w.Write([]byte(`{"data":[{"id":"perplexity/pplx-decider-v1-27b","context_length":262144,"architecture":{"input_modalities":["text","image"],"output_modalities":["decisions"]}},{"id":"liquid/d1","context_length":65536,"architecture":{"input_modalities":["text"]}},{"id":"~typesafe/jev-latest","context_length":32000,"architecture":{"input_modalities":["text"]}}]}`))
|
||||
return
|
||||
}
|
||||
w.Write([]byte(`{"data":[{"id":"openai/gpt-5","context_length":400000},{"id":"typesafe/jev-router","context_length":1000000}]}`))
|
||||
case "/api/v1/systemone":
|
||||
var q struct{ Model string }
|
||||
json.NewDecoder(r.Body).Decode(&q)
|
||||
mu.Lock()
|
||||
models = append(models, q.Model)
|
||||
mu.Unlock()
|
||||
w.Write([]byte(`{"answers":{"ok":{"type":"noul","noul":0.9}}}`))
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}))
|
||||
t.Cleanup(srv.Close)
|
||||
addr := srv.Listener.Addr().String()
|
||||
tr := &http.Transport{
|
||||
DialContext: func(ctx context.Context, network, _ string) (net.Conn, error) {
|
||||
return (&net.Dialer{Timeout: 5 * time.Second}).DialContext(ctx, network, addr)
|
||||
},
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
}
|
||||
old := http.DefaultClient.Transport
|
||||
http.DefaultClient.Transport = tr
|
||||
t.Cleanup(func() { http.DefaultClient.Transport = old })
|
||||
return func() []string {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
return slices.Clone(models)
|
||||
}, func() int {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
return decisions
|
||||
}
|
||||
}
|
||||
|
||||
// OpenRouter's decision models (ARNO on Discord: the OpenRouter
|
||||
// provider's list didn't have them): they are listed apart from its chat
|
||||
// models, so the built-in OpenRouter fetches that list too and offers them
|
||||
// to routing groups, each with its window and input, asked at /systemone
|
||||
// with the same key. Its Jev Router, a chat model named like Jev, stays a
|
||||
// model agents talk to.
|
||||
func TestOpenRouterListsDecisionModels(t *testing.T) {
|
||||
h := t.TempDir()
|
||||
t.Setenv("HOME", h)
|
||||
t.Setenv("USERPROFILE", h)
|
||||
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
|
||||
t.Setenv("XDG_CACHE_HOME", t.TempDir())
|
||||
asked, _ := fakeOpenRouter(t)
|
||||
|
||||
// an OpenRouter provider saved before its preset had decisions
|
||||
if err := Save(Provider{ID: "openrouter", Name: "OpenRouter", Preset: "openrouter", Key: "k",
|
||||
Chat: "https://openrouter.ai/api/v1", Models: []string{"openai/gpt-5", "typesafe/jev-router"}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
p, err := Find("openrouter")
|
||||
if err != nil || !p.Decides() || p.DecideOnly() {
|
||||
t.Fatalf("OpenRouter has no decision API: %+v", p)
|
||||
}
|
||||
if _, err := p.Fetch(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
p, _ = Find("openrouter")
|
||||
if p == nil {
|
||||
t.Fatal("gone")
|
||||
}
|
||||
|
||||
if p.DecidesModel("typesafe/jev-router") || !p.DecidesModel("liquid/d1") || !p.DecidesModel("~typesafe/jev-latest") {
|
||||
t.Fatal("decision models are not OpenRouter's decision list")
|
||||
}
|
||||
var deciders []string
|
||||
for _, e := range Deciders() {
|
||||
deciders = append(deciders, e.ID)
|
||||
}
|
||||
if !slices.Equal(deciders, []string{"openrouter/perplexity/pplx-decider-v1-27b", "openrouter/liquid/d1", "openrouter/~typesafe/jev-latest"}) {
|
||||
t.Fatalf("deciders: %v", deciders)
|
||||
}
|
||||
var agents []string
|
||||
for _, e := range providerEntries() {
|
||||
agents = append(agents, e.Model)
|
||||
}
|
||||
if !slices.Contains(agents, "typesafe/jev-router") || !slices.Contains(agents, "openai/gpt-5") || slices.Contains(agents, "liquid/d1") {
|
||||
t.Fatalf("agents' models: %v", agents)
|
||||
}
|
||||
|
||||
facts := map[string][2]any{}
|
||||
for _, m := range p.DecisionModels() {
|
||||
facts[m.ID] = [2]any{ListedWindow(m), m.ImageInput != nil && *m.ImageInput}
|
||||
}
|
||||
if facts["perplexity/pplx-decider-v1-27b"] != [2]any{262144, true} || facts["liquid/d1"] != [2]any{65536, false} {
|
||||
t.Fatalf("facts: %v", facts)
|
||||
}
|
||||
|
||||
// a group classified by one is asked at OpenRouter's /systemone
|
||||
q, m, err := RouteDecider("openrouter/liquid/d1")
|
||||
if err != nil || q.ID != "openrouter" || m != "liquid/d1" {
|
||||
t.Fatalf("routed to %s %s: %v", q.ID, m, err)
|
||||
}
|
||||
if err := q.AskSystemOne(context.Background(), m); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, m, _ := RouteDecider("openrouter"); m != "~typesafe/jev-latest" {
|
||||
t.Fatalf("OpenRouter's Jev: %s", m)
|
||||
}
|
||||
if got := asked(); !slices.Equal(got, []string{"liquid/d1"}) {
|
||||
t.Fatalf("asked: %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// A System One provider at OpenRouter added by hand, with no list URL,
|
||||
// gets OpenRouter's decision models, not Jev Router alone; and one whose
|
||||
// list was fetched before magpie kept windows and input is fetched again
|
||||
// when lists are (ARNO: they showed neither).
|
||||
func TestOpenRouterDecisionListRefreshed(t *testing.T) {
|
||||
h := t.TempDir()
|
||||
t.Setenv("HOME", h)
|
||||
t.Setenv("USERPROFILE", h)
|
||||
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
|
||||
t.Setenv("XDG_CACHE_HOME", t.TempDir())
|
||||
_, lists := fakeOpenRouter(t)
|
||||
|
||||
bare := Provider{ID: "or-s1", Name: "OpenRouter S1", Key: "k", Decide: "https://openrouter.ai/api/v1"}
|
||||
if err := Save(bare); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ms, err := bare.Fetch(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var ids []string
|
||||
for _, m := range ms {
|
||||
ids = append(ids, m.ID)
|
||||
}
|
||||
if !slices.Equal(ids, []string{"perplexity/pplx-decider-v1-27b", "liquid/d1", "~typesafe/jev-latest"}) {
|
||||
t.Fatalf("decision models: %v", ids)
|
||||
}
|
||||
|
||||
old := Provider{ID: "or-s2", Name: "OpenRouter S2", Key: "k", Decide: "https://openrouter.ai/api/v1",
|
||||
ModelsURL: "https://openrouter.ai/api/v1/models?output_modalities=decisions"}
|
||||
if err := Save(old); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := catalog.SaveLive("or-s2", old.Decide, []catalog.Model{{ID: "perplexity/pplx-decider-v1-27b"}, {ID: "liquid/d1"}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
before := lists()
|
||||
FetchNew(10 * time.Second)
|
||||
if lists() == before {
|
||||
t.Fatal("the old list wasn't fetched again")
|
||||
}
|
||||
p, err := Find("or-s2")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, m := range p.Available() {
|
||||
if m.ID == "perplexity/pplx-decider-v1-27b" && (ListedWindow(m) != 262144 || !m.Images) {
|
||||
t.Fatalf("pplx-decider: %+v", m)
|
||||
}
|
||||
}
|
||||
// fetched now, it isn't fetched again at the next start
|
||||
before = lists()
|
||||
newFetches.Lock()
|
||||
clear(newFetches.m)
|
||||
newFetches.Unlock()
|
||||
FetchNew(10 * time.Second)
|
||||
if lists() != before {
|
||||
t.Fatal("a list with its facts was fetched again")
|
||||
}
|
||||
}
|
||||
@@ -163,6 +163,12 @@ func (p Provider) Refetch(ctx context.Context) ([]catalog.Model, []string, error
|
||||
}
|
||||
before := liveIDs(p.ID)
|
||||
ms, err := p.fetch(ctx)
|
||||
if err == nil && p.listsDecisions() {
|
||||
// OpenRouter's decision models, listed apart from its chat ones
|
||||
if _, derr := p.fetchDecide(p.Via(ctx)); derr != nil {
|
||||
log.Println(p.ID + ": " + derr.Error())
|
||||
}
|
||||
}
|
||||
if err != nil || len(before) == 0 {
|
||||
return ms, nil, err
|
||||
}
|
||||
@@ -366,11 +372,14 @@ func FetchNew(timeout time.Duration) {
|
||||
newFetches.Lock()
|
||||
defer newFetches.Unlock()
|
||||
for _, p := range All() {
|
||||
if p.Account == nil || !p.Ready() {
|
||||
// a list of decision models fetched before magpie kept their
|
||||
// windows and input, or not fetched yet (ARNO on Discord)
|
||||
decide := p.decideListDue()
|
||||
if !decide && (p.Account == nil || !p.Ready()) {
|
||||
continue
|
||||
}
|
||||
// a plugin's accounts were listed with the plugin's providers
|
||||
if _, ok := p.Listed(); ok {
|
||||
if _, ok := p.Listed(); ok && !decide {
|
||||
continue
|
||||
}
|
||||
if t, ok := newFetches.m[p.ID]; ok && time.Since(t) < newFetchRetry {
|
||||
@@ -378,7 +387,13 @@ func FetchNew(timeout time.Duration) {
|
||||
}
|
||||
newFetches.m[p.ID] = time.Now()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
if _, err := p.Fetch(ctx); err != nil {
|
||||
var err error
|
||||
if decide && p.Account == nil {
|
||||
_, err = p.fetchDecide(p.Via(ctx))
|
||||
} else {
|
||||
_, err = p.Fetch(ctx)
|
||||
}
|
||||
if err != nil {
|
||||
log.Println(p.ID + ": " + err.Error())
|
||||
}
|
||||
cancel()
|
||||
|
||||
@@ -339,6 +339,10 @@ var presets = []PresetDef{
|
||||
|
||||
{ID: "openrouter", Name: "OpenRouter", Icon: "openrouter", Kind: KindRelay, Catalog: "openrouter",
|
||||
Chat: "https://openrouter.ai/api/v1", Anthropic: "https://openrouter.ai/api",
|
||||
// its decision models (liquid/d1, cloudflare/clef, Jev …), listed
|
||||
// apart from its chat models, answer System One at /systemone, so
|
||||
// a routing group can be classified by one with the same key
|
||||
Decide: "https://openrouter.ai/api/v1",
|
||||
Website: "https://openrouter.ai", KeysURL: "https://openrouter.ai/keys",
|
||||
// app attribution, for OpenRouter's rankings and analytics
|
||||
HeaderHints: []string{"HTTP-Referer", "X-OpenRouter-Title"}},
|
||||
|
||||
@@ -828,8 +828,9 @@ func normalize(p Provider) Provider {
|
||||
p.Key = OpenCodeAnonymousKey
|
||||
}
|
||||
// a Zen provider saved before its preset had System One for Jev
|
||||
// (01huadalang: its Jev was asked as a chat model, and failed)
|
||||
if p.Preset == "opencode-zen" && p.Decide == "" && p.Chat != "" {
|
||||
// (01huadalang: its Jev was asked as a chat model, and failed), an
|
||||
// OpenRouter one before its decision models were listed (ARNO)
|
||||
if (p.Preset == "opencode-zen" || p.Preset == "openrouter") && p.Decide == "" && p.Chat != "" {
|
||||
if pr := Preset(p.Preset); pr != nil {
|
||||
p.Decide = pr.Decide
|
||||
}
|
||||
|
||||
@@ -96,6 +96,7 @@ func Rename(from, to string) error {
|
||||
}
|
||||
// the vendor's list last fetched goes with it
|
||||
os.Rename(catalog.LivePath(from), catalog.LivePath(to))
|
||||
os.Rename(catalog.LivePath(decisionsID(from)), catalog.LivePath(decisionsID(to)))
|
||||
if err := store(f); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user