From ee3fca6375f7a59ac4074e5d43cd71408f9d3fec Mon Sep 17 00:00:00 2001 From: yetone Date: Sun, 4 Oct 2026 21:51:36 +0800 Subject: [PATCH] =?UTF-8?q?decide:=20Cloudflare's=20Clef=20and=20Clef=20Fl?= =?UTF-8?q?ash=20are=20offered=20on=20Workers=20AI=20beside=20Jev=20(ARNO?= =?UTF-8?q?=20on=20Discord:=20cloudflare=20workers-ai=20=E7=8E=B0=E5=9C=A8?= =?UTF-8?q?=E6=9C=89=E4=BA=86=E6=96=B0=E7=9A=84=20decision=20=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B)=20=E2=80=94=20Workers=20AI=20now=20serves=20Cloudfla?= =?UTF-8?q?re's=20own=20decision=20models,=20@cf/cloudflare/clef=20(27B)?= =?UTF-8?q?=20and=20@cf/cloudflare/clef-flash,=20which=20answer=20System?= =?UTF-8?q?=20One's=20questions=20as=20Jev=20does.=20The=20cloudflare-jev?= =?UTF-8?q?=20provider=20(now=20named=20Cloudflare=20Workers=20AI=20=C2=B7?= =?UTF-8?q?=20Jev,=20Clef)=20lists=20them=20beside=20typesafe/jev,=20so=20?= =?UTF-8?q?a=20routing=20group=20can=20pick=20either=20as=20its=20classifi?= =?UTF-8?q?er=20(cloudflare-jev/@cf/cloudflare/clef),=20and=20magpie's=20S?= =?UTF-8?q?ystem=20One=20API=20routes=20to=20them=20by=20name.=20Each=20is?= =?UTF-8?q?=20asked=20at=20its=20own=20run=20(=E2=80=A6/ai/run/@cf/cloudfl?= =?UTF-8?q?are/clef),=20with=20the=20request=20as=20it=20is=20and=20its=20?= =?UTF-8?q?model=20named=20clef=20or=20clef-flash,=20as=20Cloudflare's=20d?= =?UTF-8?q?ocs=20give=20it;=20Jev=20is=20still=20a=20run=20of=20typesafe/j?= =?UTF-8?q?ev=20with=20the=20questions=20as=20its=20input,=20and=20stays?= =?UTF-8?q?=20the=20default.=20TestCloudflareClef=20(offered,=20routed,=20?= =?UTF-8?q?each=20one's=20URL,=20a=20Clef=20id=20outside=20Workers=20AI=20?= =?UTF-8?q?no=20decider)=20and=20TestClefOnCloudflare=20(a=20group=20class?= =?UTF-8?q?ified=20by=20Clef=20Flash=20through=20Workers=20AI)=20fail=20on?= =?UTF-8?q?=20the=20old=20code.=20go=20vet,=20builds=20(darwin,=20GOOS=3Dl?= =?UTF-8?q?inux=20nogui,=20GOOS=3Dwindows)=20and=20go=20test=20-tags=20nog?= =?UTF-8?q?ui=20-count=3D1=20./...=20under=20a=20temp=20HOME=20pass.=20Not?= =?UTF-8?q?=20tried=20against=20Cloudflare:=20no=20Workers=20AI=20key=20in?= =?UTF-8?q?=20the=20owner's=20config.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/gateway/decide.go | 10 +++++-- internal/gateway/decide_test.go | 40 ++++++++++++++++++++++++++ internal/provider/decide.go | 44 ++++++++++++++++++++++++++--- internal/provider/decide_test.go | 48 ++++++++++++++++++++++++++++++++ internal/provider/presets.go | 4 ++- 5 files changed, 139 insertions(+), 7 deletions(-) diff --git a/internal/gateway/decide.go b/internal/gateway/decide.go index 68413ad2..45b5f729 100644 --- a/internal/gateway/decide.go +++ b/internal/gateway/decide.go @@ -354,7 +354,7 @@ func (s *Server) systemOne(ctx context.Context, p provider.Provider, model strin // they are; a transport error is named with p. func (s *Server) postDecide(ctx context.Context, p provider.Provider, model string, body []byte) (int, []byte, string, error) { ctx = p.Via(ctx) - u, err := p.DecideURL(ctx) + u, err := p.DecideModelURL(ctx, model) if err != nil { return 0, nil, "", err } @@ -488,7 +488,8 @@ func decideSeat(p provider.Provider, model string) Weighed { // decideAsk is a System One request (body) as via takes it: Vercel's // TypeSafe API takes it as it is; its evaluation models name the model in // a header and ask a noul as a boolean; Workers AI takes the state and -// questions as the input of a run of the model. +// questions as the input of a run of the model, and a Clef there takes +// the request as it is, its model named as Clef names itself. func decideAsk(via, model string, body []byte) []byte { var q map[string]any if via == provider.ViaSystemOne || via == provider.ViaVercel || json.Unmarshal(body, &q) != nil { @@ -506,6 +507,11 @@ func decideAsk(via, model string, body []byte) []byte { b, _ := json.Marshal(q) return b case provider.ViaCloudflare: + if provider.CloudflareClef(model) { + q["model"] = strings.TrimPrefix(model, "@cf/cloudflare/") + b, _ := json.Marshal(q) + return b + } b, _ := json.Marshal(map[string]any{"model": model, "input": q}) return b } diff --git a/internal/gateway/decide_test.go b/internal/gateway/decide_test.go index e28e0155..ac5c8c29 100644 --- a/internal/gateway/decide_test.go +++ b/internal/gateway/decide_test.go @@ -9,6 +9,7 @@ import ( "io" "net/http" "net/http/httptest" + "slices" "strings" "sync" "testing" @@ -733,3 +734,42 @@ func TestJevConfidence(t *testing.T) { } } } + +// A Clef on Workers AI (ARNO on Discord) is asked at its own run, with +// the System One request as it is and the model named as Clef names +// itself, and answers in Cloudflare's envelope. +func TestClefOnCloudflare(t *testing.T) { + j := &jevUp{choice: "crashes", sure: 0.9, score: 1} + var models []string + var mu sync.Mutex + up := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/client/v4/accounts/acc7/ai/run/@cf/cloudflare/clef-flash" || r.Header.Get("Authorization") != "Bearer kj" { + http.Error(w, `{"success":false,"errors":[{"message":"No route for that URI"}]}`, 400) + return + } + b, _ := io.ReadAll(r.Body) + var q map[string]any + json.Unmarshal(b, &q) + if _, ok := q["input"]; ok || q["questions"] == nil { + http.Error(w, `{"success":false,"errors":[{"message":"bad input"}]}`, 400) + return + } + mu.Lock() + models = append(models, fmt.Sprint(q["model"])) + mu.Unlock() + rec := httptest.NewRecorder() + j.ServeHTTP(rec, httptest.NewRequest("POST", "/v1/systemone", bytes.NewReader(b))) + w.Write([]byte(`{"success":true,"errors":[],"messages":[],"result":` + rec.Body.String() + `}`)) + })) + defer up.Close() + s := jevAt(t, up.URL+"/client/v4/accounts/acc7/ai/run", "jv/@cf/cloudflare/clef-flash", provider.Rule{Use: "b/big", Intent: "crashes"}) + out, r := postOK(t, s, "s1", chat("why does this crash?", nil, 0, `,"reasoning_effort":"low"`)) + if c := r.Rule.Classified; !strings.Contains(out, "from kb") || c.Intent != "crashes" || c.Sure != 0.9 { + t.Fatalf("%s %+v %+v", out, r.Rule, c) + } + mu.Lock() + defer mu.Unlock() + if len(models) == 0 || slices.ContainsFunc(models, func(m string) bool { return m != "clef-flash" }) { + t.Fatalf("Clef asked as %v", models) + } +} diff --git a/internal/provider/decide.go b/internal/provider/decide.go index 88e535ff..7a88b57c 100644 --- a/internal/provider/decide.go +++ b/internal/provider/decide.go @@ -71,7 +71,30 @@ 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)) + return p.Decides() && (p.DecideOnly() || jevID(model) || p.DecideVia() == ViaCloudflare && CloudflareClef(model)) +} + +// Cloudflare's own decision models on Workers AI (ARNO on Discord): Clef +// and Clef Flash answer System One's questions as Jev does, but each is +// 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. +const ( + CloudflareJev = "typesafe/jev" + CloudflareClefModel = "@cf/cloudflare/clef" + CloudflareClefFlash = "@cf/cloudflare/clef-flash" +) + +// CloudflareClef reports whether model is one of Cloudflare's Clef models. +func CloudflareClef(model string) bool { + return model == CloudflareClefModel || model == CloudflareClefFlash +} + +// cloudflareDecideModels are the decision models Workers AI serves. +var cloudflareDecideModels = []catalog.Model{ + {ID: CloudflareJev, Name: "Jev"}, + {ID: CloudflareClefModel, Name: "Clef"}, + {ID: CloudflareClefFlash, Name: "Clef Flash"}, } // The ways a decision API is asked, by where it is (DecideVia): TypeSafe's @@ -111,7 +134,7 @@ func (p Provider) Jev() string { case ViaVercel, ViaVercelEval: return "typesafe-ai/jev" case ViaCloudflare: - return "typesafe/jev" + return CloudflareJev } if own := p.ownDecideModels(); len(own) > 0 { return own[0].ID @@ -155,7 +178,10 @@ func (p Provider) decideModels() []catalog.Model { } return out } - if p.DecideVia() != ViaSystemOne { + switch p.DecideVia() { + case ViaCloudflare: + return slices.Clone(cloudflareDecideModels) + case ViaVercel, ViaVercelEval: return []catalog.Model{{ID: p.Jev(), Name: "Jev"}} } if own := p.ownDecideModels(); len(own) > 0 { @@ -201,6 +227,16 @@ func (p Provider) DecideURL(ctx context.Context) (string, error) { return p.Decide + "/systemone", nil } +// DecideModelURL is where a question for model is posted: DecideURL, or, +// for a Clef on Workers AI, its own run under it. +func (p Provider) DecideModelURL(ctx context.Context, model string) (string, error) { + u, err := p.DecideURL(ctx) + if err == nil && p.DecideVia() == ViaCloudflare && CloudflareClef(model) { + u += "/" + model + } + return u, err +} + // cloudflareBase splits p's Workers AI base into Cloudflare's API // (…/client/v4) and the account it names, if any. Cloudflare's docs give // …/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/ai/run, and a token made @@ -555,7 +591,7 @@ func (p Provider) decideAsked(ctx context.Context) ([]catalog.Model, error) { // AskSystemOne sends model the smallest System One question at p's // decision API, and says why it wasn't answered. func (p Provider) AskSystemOne(ctx context.Context, model string) error { - u, err := p.DecideURL(ctx) + u, err := p.DecideModelURL(ctx, model) if err != nil { return err } diff --git a/internal/provider/decide_test.go b/internal/provider/decide_test.go index ae972037..45e00436 100644 --- a/internal/provider/decide_test.go +++ b/internal/provider/decide_test.go @@ -533,3 +533,51 @@ func TestTinyStreamsStreamOnly(t *testing.T) { p.Account = nil } } + +// Workers AI's own decision models, Clef and Clef Flash, are offered +// beside Jev, routed to by name, and each asked at its own run. +func TestCloudflareClef(t *testing.T) { + h := t.TempDir() + t.Setenv("HOME", h) + t.Setenv("USERPROFILE", h) + t.Setenv("XDG_CONFIG_HOME", filepath.Join(h, ".config")) + t.Setenv("XDG_CACHE_HOME", filepath.Join(h, ".cache")) + p, err := FromPreset("cloudflare-jev") + if err != nil { + t.Fatal(err) + } + p.Key, p.Decide = "k", "https://api.cloudflare.com/client/v4/accounts/acc7/ai/run" + var ids []string + for _, m := range p.Available() { + ids = append(ids, m.ID) + } + if strings.Join(ids, " ") != "typesafe/jev @cf/cloudflare/clef @cf/cloudflare/clef-flash" || p.Jev() != "typesafe/jev" { + t.Fatalf("offered %v, default %s", ids, p.Jev()) + } + if err := Save(p); err != nil { + t.Fatal(err) + } + for _, name := range []string{"cloudflare-jev/@cf/cloudflare/clef-flash", "@cf/cloudflare/clef"} { + got, model, err := RouteDecider(name) + if err != nil || got.ID != "cloudflare-jev" || !strings.HasSuffix(name, model) { + t.Fatalf("%s: %s %s %v", name, got.ID, model, err) + } + } + if !IsDecider("cloudflare-jev/@cf/cloudflare/clef") { + t.Fatal("Clef is not a classifier") + } + for model, want := range map[string]string{ + "typesafe/jev": "https://api.cloudflare.com/client/v4/accounts/acc7/ai/run", + "@cf/cloudflare/clef": "https://api.cloudflare.com/client/v4/accounts/acc7/ai/run/@cf/cloudflare/clef", + "@cf/cloudflare/clef-flash": "https://api.cloudflare.com/client/v4/accounts/acc7/ai/run/@cf/cloudflare/clef-flash", + } { + if u, err := p.DecideModelURL(context.Background(), model); err != nil || u != want { + t.Errorf("%s at %s %v", model, u, err) + } + } + // another System One provider's model named so is no Clef + o := Provider{ID: "o", Name: "O", Key: "k", Decide: "https://api.typesafe.ai/v1", Chat: "https://api.typesafe.ai/v1"} + if o.DecidesModel("@cf/cloudflare/clef") { + t.Fatal("a Clef id outside Workers AI decides") + } +} diff --git a/internal/provider/presets.go b/internal/provider/presets.go index ec921de0..71aec2a5 100644 --- a/internal/provider/presets.go +++ b/internal/provider/presets.go @@ -447,7 +447,9 @@ var presets = []PresetDef{ Decide: "https://ai-gateway.vercel.sh/typesafe", Note: "routes groups · picks model and effort", Website: "https://vercel.com/ai-gateway/models/jev", KeysURL: "https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fai-gateway%2Fapi-keys"}, - {ID: "cloudflare-jev", Name: "Jev · Cloudflare Workers AI", Icon: "cloudflare-color", Kind: KindRelay, + // Workers AI also serves Cloudflare's own decision models, Clef and + // Clef Flash, on the same key + {ID: "cloudflare-jev", Name: "Cloudflare Workers AI · Jev, Clef", Icon: "cloudflare-color", Kind: KindRelay, Decide: "https://api.cloudflare.com/client/v4", Note: "routes groups · picks model and effort", Website: "https://developers.cloudflare.com/ai/models/typesafe/jev/", KeysURL: "https://dash.cloudflare.com/profile/api-tokens"},