478 lines
19 KiB
Go
478 lines
19 KiB
Go
package gateway
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/yetone/magpie/internal/catalog"
|
|
"github.com/yetone/magpie/internal/provider"
|
|
)
|
|
|
|
func cursorFrame(msg pb) []byte { return connectFrame(msg) }
|
|
|
|
func cursorEnd(js string) []byte {
|
|
b := connectFrame([]byte(js))
|
|
b[0] = 2
|
|
return b
|
|
}
|
|
|
|
func cursorUpdate(num int, msg pb) []byte {
|
|
return cursorFrame(pb{}.bytes(1, pb{}.bytes(num, msg)))
|
|
}
|
|
|
|
// cursorCallFrame is the server handing the client a call of an MCP tool.
|
|
func cursorCallFrame(id uint64, callID, name, city string) []byte {
|
|
arg := pb{}.str(1, "city").bytes(2, pb{}.str(3, city))
|
|
return cursorFrame(pb{}.bytes(2, pb{}.varint(1, id).str(15, "exec-1").
|
|
bytes(11, pb{}.str(1, "magpie-"+name).bytes(2, arg).str(3, callID).str(5, name))))
|
|
}
|
|
|
|
// cursorDecoded runs decode over frames, and is what it said and what it
|
|
// sent back.
|
|
func cursorDecoded(t *testing.T, blobs map[string][]byte, frames ...[]byte) ([]Event, [][]pbField, int) {
|
|
t.Helper()
|
|
pr, pw := io.Pipe()
|
|
st := &cursorStream{pw: pw, blobs: blobs}
|
|
sent := make(chan [][]pbField)
|
|
go func() {
|
|
var got [][]pbField
|
|
br := bufio.NewReader(pr)
|
|
for {
|
|
f, err := readConnectFrame(br)
|
|
if err != nil {
|
|
sent <- got
|
|
return
|
|
}
|
|
got = append(got, pbFields(f.data))
|
|
}
|
|
}()
|
|
out := make(chan Event, 256)
|
|
st.decode(context.Background(), bufio.NewReader(bytes.NewReader(bytes.Join(frames, nil))), out, nil)
|
|
pw.Close()
|
|
var evs []Event
|
|
for ev := range out {
|
|
evs = append(evs, ev)
|
|
}
|
|
return evs, <-sent, st.status
|
|
}
|
|
|
|
func TestCursorDecodesTextAndCalls(t *testing.T) {
|
|
evs, sent, _ := cursorDecoded(t, map[string][]byte{"k1": []byte("blob")},
|
|
cursorFrame(pb{}.bytes(4, pb{}.varint(1, 7).bytes(2, pb{}.str(1, "k1")))),
|
|
cursorFrame(pb{}.bytes(4, pb{}.varint(1, 8).bytes(3, pb{}.str(1, "k2")))),
|
|
cursorUpdate(4, pb{}.str(1, "Hmm.")),
|
|
cursorUpdate(1, pb{}.str(1, "Let me ")),
|
|
cursorUpdate(1, pb{}.str(1, "look.")),
|
|
cursorUpdate(27, pb{}.varint(1, 2)),
|
|
cursorCallFrame(3, "call_a\nfc_1", "get_weather", "Paris"),
|
|
cursorCallFrame(4, "call_b\nfc_2", "get_weather", "Lima"),
|
|
cursorUpdate(1, pb{}.str(1, "never read")),
|
|
)
|
|
want := `|think:Hmm.|text:Let me look.|call:call_a__fc_1/get_weather|args:{"city":"Paris"}|call:call_b__fc_2/get_weather|args:{"city":"Lima"}|stop:tool`
|
|
if got := said(evs); got != want {
|
|
t.Fatalf("got %s\nwant %s", got, want)
|
|
}
|
|
// the blob asked for is given, the one to keep acked
|
|
if len(sent) != 2 || sent[0][0].num != 3 || sent[1][0].num != 3 {
|
|
t.Fatalf("sent %v", sent)
|
|
}
|
|
got := pbFields(sent[0][0].data)
|
|
if got[0].n != 7 || string(pbFields(got[1].data)[0].data) != "blob" {
|
|
t.Fatalf("get_blob answered %v", got)
|
|
}
|
|
if cursorCallID("call_a__fc_1") != "call_a\nfc_1" || cursorCallID("toolu_x__fc_1") != "toolu_x__fc_1" {
|
|
t.Fatal("call ids don't come back")
|
|
}
|
|
}
|
|
|
|
func TestCursorDecodesTheEnd(t *testing.T) {
|
|
evs, _, _ := cursorDecoded(t, nil,
|
|
cursorUpdate(1, pb{}.str(1, "Hi")),
|
|
cursorUpdate(14, pb{}.varint(1, 120).varint(2, 30).varint(3, 7)),
|
|
)
|
|
if got := said(evs); got != "|text:Hi|stop:stop" {
|
|
t.Fatal(got)
|
|
}
|
|
for _, ev := range evs {
|
|
if ev.Kind == KUsage && (ev.Usage.Input != 120 || ev.Usage.Output != 30 || ev.Usage.CacheRead != 7) {
|
|
t.Fatalf("usage %+v", ev.Usage)
|
|
}
|
|
}
|
|
|
|
evs, _, status := cursorDecoded(t, nil, cursorEnd(`{"error":{"code":"resource_exhausted","message":"Error","details":[{"debug":{"details":{"title":"Usage limit","detail":"You're out."}}}]}}`))
|
|
if got := said(evs); !strings.HasSuffix(got, "Usage limit: You're out.") || status != 429 {
|
|
t.Fatal(got, status)
|
|
}
|
|
}
|
|
|
|
func TestCursorMessages(t *testing.T) {
|
|
r := &Request{
|
|
System: "Be brief.",
|
|
Tools: []Tool{{Name: "read", Description: "Read a file", Schema: json.RawMessage(`{"type":"object"}`)}},
|
|
Messages: []Message{
|
|
{Role: "user", Parts: []Part{{Kind: ToolResult, CallID: "orphan", Text: "x"}, {Kind: Text, Text: "look at a"}}},
|
|
{Role: "assistant", Parts: []Part{
|
|
{Kind: Text, Text: "Reading."},
|
|
{Kind: ToolCall, ID: "call_1__fc_1", Name: "read", Args: json.RawMessage(`{"p":"a"}`)},
|
|
{Kind: ToolCall, ID: "c2", Name: "gone"},
|
|
}},
|
|
{Role: "user", Parts: []Part{
|
|
{Kind: ToolResult, CallID: "call_1__fc_1", Text: `{"ok":1}`, IsError: true},
|
|
{Kind: Image, MediaType: "image/png", Data: "AAE="},
|
|
{Kind: Text, Text: "and this"},
|
|
}},
|
|
},
|
|
}
|
|
var got []string
|
|
for _, m := range cursorMessages(r, bridgeTools(r)) {
|
|
got = append(got, string(m))
|
|
}
|
|
lt, gt := `\u003c`, `\u003e` // as encoding/json escapes < and >
|
|
sys := `{"content":"Be brief.\n\n` + lt + "dynamic_tool_catalog" + gt
|
|
if !strings.HasPrefix(got[0], sys) || !strings.Contains(got[0], lt+`tool name=\"read\"`+gt+`\nRead a file\ninput schema: {\"type\":\"object\"}`) {
|
|
t.Fatalf("system %s", got[0])
|
|
}
|
|
want := []string{
|
|
`{"content":[{"text":"look at a","type":"text"}],"role":"user"}`,
|
|
`{"content":[{"text":"Reading.","type":"text"},{"args":{"arguments":{"p":"a"},"namespace":"magpie","toolName":"read"},"toolCallId":"call_1\nfc_1","toolName":"CallDynamicTool","type":"tool-call"},{"args":{"arguments":{},"namespace":"magpie","toolName":"gone"},"toolCallId":"c2","toolName":"CallDynamicTool","type":"tool-call"}],"role":"assistant"}`,
|
|
`{"content":[{"experimental_content":[{"text":"{\"ok\":1}","type":"text"}],"isError":true,"result":{"ok":1},"toolCallId":"call_1\nfc_1","toolName":"CallDynamicTool","type":"tool-result"},{"experimental_content":[{"text":"` + devinNoResult + `","type":"text"}],"isError":true,"result":"` + devinNoResult + `","toolCallId":"c2","toolName":"CallDynamicTool","type":"tool-result"}],"role":"tool"}`,
|
|
`{"content":[{"image":{"__type":"Uint8Array","hex":"0001"},"mimeType":"image/png","type":"image"},{"text":"and this","type":"text"}],"role":"user"}`,
|
|
}
|
|
if strings.Join(got[1:], "\n") != strings.Join(want, "\n") {
|
|
t.Fatalf("got\n%s\nwant\n%s", strings.Join(got[1:], "\n"), strings.Join(want, "\n"))
|
|
}
|
|
}
|
|
|
|
func TestBuildCursorRun(t *testing.T) {
|
|
msgs := [][]byte{[]byte(`{"role":"user","content":"hi"}`)}
|
|
tools := []bridgeTool{{Name: "read", InputSchema: json.RawMessage(`{"type":"object","required":["p"]}`)}}
|
|
run, blobs := buildCursorRun(msgs, "hi", tools, "gpt-5.4")
|
|
rr := pbFields(pbFields(run)[0].data)
|
|
var state []pbField
|
|
var models, mcp []string
|
|
for _, f := range rr {
|
|
switch f.num {
|
|
case 1:
|
|
state = pbFields(f.data)
|
|
case 3, 9:
|
|
models = append(models, string(pbFields(f.data)[0].data))
|
|
case 4:
|
|
mcp = append(mcp, string(pbFields(pbFields(f.data)[0].data)[0].data))
|
|
}
|
|
}
|
|
if strings.Join(models, ",") != "gpt-5.4,gpt-5.4" || strings.Join(mcp, ",") != "read" {
|
|
t.Fatalf("models %v, tools %v", models, mcp)
|
|
}
|
|
var ids int
|
|
for _, f := range state {
|
|
switch f.num {
|
|
case 1:
|
|
ids++
|
|
if string(blobs[string(f.data)]) != string(msgs[0]) {
|
|
t.Fatal("the message's blob is missing")
|
|
}
|
|
case 8: // the turn, whose user message names the words
|
|
turn := pbFields(blobs[string(f.data)])
|
|
user := blobs[string(pbFields(turn[0].data)[0].data)]
|
|
if string(pbFields(user)[0].data) != "hi" {
|
|
t.Fatalf("turn %q", user)
|
|
}
|
|
}
|
|
}
|
|
if ids != 1 || len(blobs) != 3 {
|
|
t.Fatalf("%d ids, %d blobs", ids, len(blobs))
|
|
}
|
|
// a schema goes as a google.protobuf.Value too, and reads back the same
|
|
def := pbFields(cursorToolDef(tools[0]))
|
|
b, _ := json.Marshal(pbAny(def[1].data))
|
|
if string(b) != `{"required":["p"],"type":"object"}` {
|
|
t.Fatal(string(b))
|
|
}
|
|
}
|
|
|
|
func TestServeCursor(t *testing.T) {
|
|
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
|
|
t.Setenv("XDG_CACHE_HOME", t.TempDir())
|
|
var reply []byte
|
|
up := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/agent.v1.AgentService/Run" || r.Header.Get("Authorization") != "Bearer tok" || r.Header.Get("x-cursor-client-type") != "cli" {
|
|
http.Error(w, `{"code":"unauthenticated","message":"no"}`, 401)
|
|
return
|
|
}
|
|
f, err := readConnectFrame(bufio.NewReader(r.Body))
|
|
if err != nil || len(pbFields(f.data)) == 0 {
|
|
http.Error(w, `{"code":"invalid_argument","message":"no run"}`, 400)
|
|
return
|
|
}
|
|
// the Run's body stays open: the reply is flushed, not held until
|
|
// the body is read to its end
|
|
http.NewResponseController(w).EnableFullDuplex()
|
|
w.Header().Set("Content-Type", "application/connect+proto")
|
|
w.Write(reply)
|
|
w.(http.Flusher).Flush()
|
|
}))
|
|
defer up.Close()
|
|
tok, ver, agent, api := cursorToken, cursorVersion, cursorAgent, cursorAPI
|
|
cursorToken = func() (string, error) { return "tok", nil }
|
|
cursorVersion = func() string { return "cli-test" }
|
|
cursorAgent, cursorAPI = up.URL, up.URL // no server config there: the global API
|
|
defer func() { cursorToken, cursorVersion, cursorAgent, cursorAPI = tok, ver, agent, api }()
|
|
forgetCursorEndpoint(t)
|
|
|
|
serve := func(from provider.Protocol, body string) *httptest.ResponseRecorder {
|
|
w := httptest.NewRecorder()
|
|
var u Usage
|
|
New().serveCursor(w, httptest.NewRequest("POST", "/", strings.NewReader(body)), from, "auto", []byte(body), &u)
|
|
return w
|
|
}
|
|
reply = bytes.Join([][]byte{
|
|
cursorUpdate(1, pb{}.str(1, "Hello")),
|
|
cursorUpdate(27, pb{}.varint(1, 1)),
|
|
cursorCallFrame(1, "toolu_1", "read", "x"),
|
|
}, nil)
|
|
w := serve(provider.Anthropic, `{"model":"x","max_tokens":10,"tools":[{"name":"read","input_schema":{"type":"object"}}],"messages":[{"role":"user","content":"hi"}]}`)
|
|
var msg struct {
|
|
Content []struct {
|
|
Text string `json:"text"`
|
|
ID string `json:"id"`
|
|
Input json.RawMessage `json:"input"`
|
|
} `json:"content"`
|
|
StopReason string `json:"stop_reason"`
|
|
}
|
|
json.Unmarshal(w.Body.Bytes(), &msg)
|
|
if w.Code != 200 || len(msg.Content) != 2 || msg.Content[0].Text != "Hello" || msg.Content[1].ID != "toolu_1" ||
|
|
string(msg.Content[1].Input) != `{"city":"x"}` || msg.StopReason != "tool_use" {
|
|
t.Fatalf("%d %s", w.Code, w.Body)
|
|
}
|
|
|
|
reply = cursorEnd(`{"error":{"code":"resource_exhausted","message":"out of credits"}}`)
|
|
if w = serve(provider.Chat, `{"model":"x","messages":[{"role":"user","content":"hi"}]}`); w.Code != 429 {
|
|
t.Fatalf("%d %s", w.Code, w.Body)
|
|
}
|
|
}
|
|
|
|
func forgetCursorEndpoint(t *testing.T) {
|
|
t.Helper()
|
|
reset := func() {
|
|
cursorEndpoint.Lock()
|
|
cursorEndpoint.key, cursorEndpoint.url, cursorEndpoint.listed = "", "", false
|
|
cursorEndpoint.Unlock()
|
|
}
|
|
reset()
|
|
t.Cleanup(reset)
|
|
}
|
|
|
|
// cursorConfigServer is Cursor's server config, naming agent as the agent
|
|
// API; it counts how often it is asked.
|
|
func cursorConfigServer(t *testing.T, agent *string, asked *int) *httptest.Server {
|
|
t.Helper()
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/aiserver.v1.ServerConfigService/GetServerConfig" || r.Header.Get("Authorization") == "" ||
|
|
r.Header.Get("Content-Type") != "application/json" {
|
|
http.Error(w, `{"code":"not_found"}`, 404)
|
|
return
|
|
}
|
|
*asked++
|
|
if *agent == "" {
|
|
http.Error(w, `{"code":"internal"}`, 500)
|
|
return
|
|
}
|
|
json.NewEncoder(w).Encode(map[string]any{"configVersion": "1",
|
|
"agentUrlConfig": map[string]any{"agentUrl": *agent, "agentnUrl": *agent + "/n"}})
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
return srv
|
|
}
|
|
|
|
// The agent API is the one the server config names, asked for once a
|
|
// token; the global one when the config can't be had.
|
|
func TestCursorAgentURL(t *testing.T) {
|
|
forgetCursorEndpoint(t)
|
|
agent, asked := "https://agent.us.example", 0
|
|
srv := cursorConfigServer(t, &agent, &asked)
|
|
api, global := cursorAPI, cursorAgent
|
|
cursorAPI, cursorAgent = srv.URL, "https://global.example"
|
|
defer func() { cursorAPI, cursorAgent = api, global }()
|
|
s, ctx := New(), context.Background()
|
|
|
|
if u := s.cursorAgentURL(ctx, "tok-a", false); u != "https://agent.us.example" || asked != 1 {
|
|
t.Fatal(u, asked) // privacy mode: agentUrl, not agentnUrl
|
|
}
|
|
if u := s.cursorAgentURL(ctx, "tok-a", false); u != "https://agent.us.example" || asked != 1 {
|
|
t.Fatal("not kept:", u, asked)
|
|
}
|
|
agent = "https://agent.eu.example"
|
|
if u := s.cursorAgentURL(ctx, "tok-b", false); u != "https://agent.eu.example" || asked != 2 {
|
|
t.Fatal("another account, the same endpoint:", u, asked)
|
|
}
|
|
agent = "https://agent.ap.example"
|
|
if u := s.cursorAgentURL(ctx, "tok-b", true); u != "https://agent.ap.example" || asked != 3 {
|
|
t.Fatal("fresh wasn't asked:", u, asked)
|
|
}
|
|
agent = ""
|
|
if u := s.cursorAgentURL(ctx, "tok-c", false); u != "https://global.example" || asked != 4 {
|
|
t.Fatal("no config, no global:", u, asked)
|
|
}
|
|
if u := s.cursorAgentURL(ctx, "tok-c", false); u != "https://global.example" || asked != 4 {
|
|
t.Fatal("a failure is asked again at once:", u, asked)
|
|
}
|
|
}
|
|
|
|
// A region the team isn't served in is asked of the config once more, and
|
|
// the Run tried again where it now says; still turned away, the error says
|
|
// so, not to sign in.
|
|
func TestServeCursorRegion(t *testing.T) {
|
|
forgetCursorEndpoint(t)
|
|
regionErr := `{"code":"unauthenticated","message":"Error","details":[{"debug":{"details":{"title":"Unauthorized request.","detail":"This region is not yet available for your team"}}}]}`
|
|
var ran []string
|
|
run := func(name string, ok bool) *httptest.Server {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
ran = append(ran, name)
|
|
readConnectFrame(bufio.NewReader(r.Body))
|
|
http.NewResponseController(w).EnableFullDuplex()
|
|
w.Header().Set("Content-Type", "application/connect+proto")
|
|
if !ok { // a stream's error comes at its end
|
|
w.Write(cursorEnd(`{"error":` + regionErr + `}`))
|
|
} else {
|
|
w.Write(cursorUpdate(1, pb{}.str(1, "Hello")))
|
|
w.Write(cursorUpdate(14, pb{}.varint(1, 3).varint(2, 1)))
|
|
}
|
|
w.(http.Flusher).Flush()
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
return srv
|
|
}
|
|
global, us := run("global", false), run("us", true)
|
|
agent, asked := global.URL, 0
|
|
srv := cursorConfigServer(t, &agent, &asked)
|
|
tok, ver, api, glob := cursorToken, cursorVersion, cursorAPI, cursorAgent
|
|
cursorToken = func() (string, error) { return "tok", nil }
|
|
cursorVersion = func() string { return "cli-test" }
|
|
cursorAPI, cursorAgent = srv.URL, global.URL
|
|
defer func() { cursorToken, cursorVersion, cursorAPI, cursorAgent = tok, ver, api, glob }()
|
|
|
|
serve := func() *httptest.ResponseRecorder {
|
|
body := `{"model":"x","messages":[{"role":"user","content":"hi"}]}`
|
|
w := httptest.NewRecorder()
|
|
var u Usage
|
|
New().serveCursor(w, httptest.NewRequest("POST", "/", strings.NewReader(body)), provider.Chat, "auto", []byte(body), &u)
|
|
return w
|
|
}
|
|
// the config kept from before names global; asked again, it says us
|
|
s := New()
|
|
s.cursorAgentURL(context.Background(), "tok", false)
|
|
agent = us.URL
|
|
if w := serve(); w.Code != 200 || !strings.Contains(w.Body.String(), "Hello") || strings.Join(ran, ",") != "global,us" || asked != 2 {
|
|
t.Fatalf("%d %s %v %d", w.Code, w.Body, ran, asked)
|
|
}
|
|
// the config still says global: no second Run, and plain words
|
|
forgetCursorEndpoint(t)
|
|
agent, ran = global.URL, nil
|
|
w := serve()
|
|
if w.Code != 403 || strings.Join(ran, ",") != "global" || !strings.Contains(w.Body.String(), "region is not yet available") ||
|
|
strings.Contains(strings.ToLower(w.Body.String()), "sign in") || strings.Contains(w.Body.String(), "Devin") {
|
|
t.Fatalf("%d %s %v", w.Code, w.Body, ran)
|
|
}
|
|
}
|
|
|
|
func TestCursorFailureWords(t *testing.T) {
|
|
for _, c := range []struct {
|
|
status int
|
|
body string
|
|
code int
|
|
has string
|
|
not []string
|
|
}{
|
|
{401, `{"code":"unauthenticated","message":"Error","details":[{"debug":{"details":{"title":"Unauthorized request.","detail":"This region is not yet available for your team"}}}]}`,
|
|
403, "This region is not yet available for your team", []string{"sign in", "Devin"}},
|
|
{401, `{"code":"unauthenticated","message":"token expired"}`, 401, "token expired — sign in to Cursor again in magpie", []string{"Devin"}},
|
|
{401, `Unauthorized`, 401, "sign in to Cursor again", []string{"Devin"}},
|
|
{403, `{"code":"permission_denied","message":"Your team doesn't allow this model"}`, 403, "Your team doesn't allow this model", []string{"sign in"}},
|
|
{200, `{"error":{"code":"resource_exhausted","message":"out of credits"}}`, 429, "usage limit reached: out of credits", nil},
|
|
} {
|
|
status, msg := cursorFailure(c.status, []byte(c.body))
|
|
if status != c.code || !strings.Contains(msg, c.has) {
|
|
t.Errorf("%s: %d %q", c.body, status, msg)
|
|
}
|
|
for _, n := range c.not {
|
|
if strings.Contains(strings.ToLower(msg), strings.ToLower(n)) {
|
|
t.Errorf("%s: %q says %q", c.body, msg, n)
|
|
}
|
|
}
|
|
}
|
|
if status, msg := cursorFailure(200, []byte(`{}`)); status != 200 || msg != "" {
|
|
t.Fatal("a stream that ended well:", status, msg)
|
|
}
|
|
}
|
|
|
|
// A model magpie offers goes to Cursor as the id its effort picks; one of
|
|
// Cursor's own ids at an effort goes at the effort asked for, where Cursor
|
|
// has that one, else as it is.
|
|
func TestCursorModelID(t *testing.T) {
|
|
t.Setenv("XDG_CACHE_HOME", t.TempDir())
|
|
var raw []catalog.Model
|
|
for _, l := range strings.Split(strings.TrimSpace(`
|
|
grok-4.7-low - Grok 4.7 Low
|
|
grok-4.7-low-fast - Grok 4.7 Low Fast
|
|
grok-4.7-medium - Grok 4.7 Medium
|
|
grok-4.7-medium-fast - Grok 4.7 Medium Fast
|
|
grok-4.7-high - Grok 4.7 High
|
|
grok-4.7-high-fast - Grok 4.7 High Fast
|
|
grok-4.7-xhigh - Grok 4.7 Extra High
|
|
grok-4.7-xhigh-fast - Grok 4.7 Extra High Fast
|
|
gpt-5.2-low - GPT-5.2 Low
|
|
gpt-5.2 - GPT-5.2
|
|
gpt-5.2-high - GPT-5.2 High
|
|
gpt-5.2-xhigh - GPT-5.2 Extra High
|
|
claude-4.6-opus-high - Claude Opus 4.6 1M
|
|
claude-4.6-opus-max - Claude Opus 4.6 1M Max
|
|
claude-4.6-opus-high-thinking - Claude Opus 4.6 1M Thinking
|
|
claude-4.6-opus-max-thinking - Claude Opus 4.6 1M Max Thinking
|
|
gpt-5.5-medium - GPT-5.5 1M
|
|
gpt-5.5-extra-high - GPT-5.5 1M Extra High
|
|
claude-4.5-sonnet - Claude Sonnet 4.5`), "\n") {
|
|
id, name, _ := strings.Cut(l, " - ")
|
|
raw = append(raw, catalog.Model{ID: id, Name: name})
|
|
}
|
|
if err := catalog.SaveLive("cursor", "", raw); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, c := range []struct {
|
|
model, effort string
|
|
fast bool
|
|
want string
|
|
}{
|
|
{"grok-4.7", "", false, "grok-4.7-medium"}, // every name says its effort: medium
|
|
{"grok-4.7", "high", false, "grok-4.7-high"},
|
|
{"grok-4.7", "max", false, "grok-4.7-xhigh"},
|
|
{"grok-4.7", "high", true, "grok-4.7-high-fast"},
|
|
{"grok-4.7-fast", "low", false, "grok-4.7-low-fast"},
|
|
{"gpt-5.2", "medium", false, "gpt-5.2"}, // between low and high: the unnamed one
|
|
{"gpt-5.2", "max", false, "gpt-5.2-xhigh"},
|
|
{"gpt-5.2", "low", true, "gpt-5.2-low"}, // no fast one
|
|
{"claude-4.6-opus", "medium", false, "claude-4.6-opus-high"},
|
|
{"claude-4.6-opus-thinking", "xhigh", false, "claude-4.6-opus-max-thinking"},
|
|
{"gpt-5.5", "xhigh", false, "gpt-5.5-extra-high"},
|
|
{"grok-4.7-low-fast", "high", false, "grok-4.7-high-fast"}, // Cursor's own id: the effort asked for wins
|
|
{"grok-4.7-low", "high", false, "grok-4.7-high"},
|
|
{"grok-4.7-low", "", false, "grok-4.7-low"},
|
|
{"grok-4.7-low", "", true, "grok-4.7-low-fast"},
|
|
{"grok-4.7-low", "minimal", false, "grok-4.7-low"}, // Cursor has none at minimal
|
|
{"gpt-5.2-high", "low", true, "gpt-5.2-low"}, // no fast one
|
|
{"claude-4.6-opus-high-thinking", "max", false, "claude-4.6-opus-max-thinking"},
|
|
{"claude-4.5-sonnet", "high", false, "claude-4.5-sonnet"},
|
|
{"auto", "", false, "auto"},
|
|
} {
|
|
if got := cursorModelID(c.model, c.effort, c.fast); got != c.want {
|
|
t.Errorf("%s at %q fast %v: %s, want %s", c.model, c.effort, c.fast, got, c.want)
|
|
}
|
|
}
|
|
}
|