@magpie-community/opencode-cursor-auth 0.2.23 → 0.2.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -0
- package/index.mjs +157 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -197,6 +197,24 @@ minutes), the way pi-cursor-sdk lists Cursor's catalog:
|
|
|
197
197
|
has. A model the usable list has nothing of is the picker's alone
|
|
198
198
|
(GLM-5.3) and is kept whole; so is every model when Cursor can't give
|
|
199
199
|
the usable list.
|
|
200
|
+
- A family the usable list has and the picker doesn't (an account whose
|
|
201
|
+
picker lacks Claude Opus 5.5 while `cursor-agent models` lists
|
|
202
|
+
`claude-opus-5-5-low` to `-max`) is listed once under its family id
|
|
203
|
+
(`claude-opus-5-5`), its efforts as variants, and goes as the usable id
|
|
204
|
+
those parameters pick, never as the family id Cursor doesn't know. A
|
|
205
|
+
picker model whose chosen variant isn't one the account can use goes as
|
|
206
|
+
its family's nearest usable id the same way. Such a family is named
|
|
207
|
+
without the tier Cursor's names carry ("Claude Opus 5 1M Medium
|
|
208
|
+
Thinking" is listed as Claude Opus 5 Thinking): the tier is the
|
|
209
|
+
reasoning level the agent asks for (low to max, as the account has
|
|
210
|
+
them), fast the request's `service_tier`.
|
|
211
|
+
- An id for a model with thinking that the account lists only without it
|
|
212
|
+
(`claude-haiku-5-5-thinking` when the list has Claude Haiku 5.5 alone),
|
|
213
|
+
or the other way round, runs as the model the account has.
|
|
214
|
+
- An `@<size>` id the list no longer has (`claude-opus-5-5@1m` when only
|
|
215
|
+
300k is listed, or the model is listed unsized) runs at the model's
|
|
216
|
+
nearest size, in Max Mode when the size asked for is over the model's
|
|
217
|
+
window, never as the literal `@<size>` id.
|
|
200
218
|
- An id from before 0.2.0 still runs. A variant's own id
|
|
201
219
|
(`claude-opus-5-5-high`) is that variant; a family 0.1.x listed
|
|
202
220
|
(`claude-opus-5-5`, `claude-opus-5-5-fast`, `claude-haiku-5-5-thinking`,
|
package/index.mjs
CHANGED
|
@@ -735,12 +735,89 @@ function served(m, ids) {
|
|
|
735
735
|
return got.some((v) => v.legacySlug) ? got : vs
|
|
736
736
|
}
|
|
737
737
|
|
|
738
|
+
// familyOf is the family one of Cursor's own ids is of, fast or not:
|
|
739
|
+
// claude-opus-5-5-low-fast is of claude-opus-5-5, at low, fast.
|
|
740
|
+
function familyOf(id) {
|
|
741
|
+
let [family, effort] = splitID(id)
|
|
742
|
+
const fast = family.endsWith("-fast") && family.length > "-fast".length
|
|
743
|
+
if (fast) family = family.slice(0, -"-fast".length)
|
|
744
|
+
return { family, effort, fast }
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// familiesOf is the usable list (GetUsableModels, what cursor-agent models
|
|
748
|
+
// shows: claude-opus-5-5-low, claude-opus-5-5-low-fast, …) by family: each
|
|
749
|
+
// id with its effort, whether it is fast, whether Cursor runs it in Max
|
|
750
|
+
// Mode, and its name.
|
|
751
|
+
function familiesOf(listed) {
|
|
752
|
+
const fams = new Map()
|
|
753
|
+
for (const u of listed ?? []) {
|
|
754
|
+
const id = String(u?.modelId ?? "")
|
|
755
|
+
if (!id) continue
|
|
756
|
+
const { family, effort, fast } = familyOf(id)
|
|
757
|
+
if (!fams.has(family)) fams.set(family, [])
|
|
758
|
+
fams.get(family).push({ id, effort, fast, maxMode: u.maxMode === true, name: clean(u.displayName || id) })
|
|
759
|
+
}
|
|
760
|
+
return fams
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// fromUsable is the id of a family of the usable list's that a request
|
|
764
|
+
// runs as: fast as asked (when the family has both), at the effort asked or
|
|
765
|
+
// the nearest it has; with none asked, the one without an effort in its
|
|
766
|
+
// id, else medium, as Cursor's default is (claude-opus-5-5-medium), else
|
|
767
|
+
// the first.
|
|
768
|
+
function fromUsable(fam, effort, fast) {
|
|
769
|
+
let vs = fam.filter((u) => u.fast === !!fast)
|
|
770
|
+
if (!vs.length) vs = fam
|
|
771
|
+
if (effort === "off") effort = "none"
|
|
772
|
+
const at = effort ? fitEffort(effort, vs.map((u) => u.effort).filter(Boolean)) : ""
|
|
773
|
+
return (at && vs.find((u) => u.effort === at)) || vs.find((u) => u.effort === "") || vs.find((u) => u.effort === "medium") || vs[0]
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// usableTitle is a usable id's name without what the id says apart: its
|
|
777
|
+
// effort, fast, and the 1M Cursor names every Max-capable Claude with
|
|
778
|
+
// ("Claude Opus 5.5 1M Low" is Claude Opus 5.5), wherever Cursor puts
|
|
779
|
+
// them: "Claude Opus 5 1M Medium Thinking" is Claude Opus 5 Thinking, as
|
|
780
|
+
// its efforts are magpie's reasoning levels and not models of their own
|
|
781
|
+
// (ysm119 on Discord read "Medium" in the name as the only tier). A word
|
|
782
|
+
// of the model's own name stays: GPT-5.1 Codex Max at high is "GPT-5.1
|
|
783
|
+
// Codex Max".
|
|
784
|
+
const EFFORT_WORDS = { none: ["None"], minimal: ["Minimal"], low: ["Low"], medium: ["Medium"], high: ["High"], xhigh: ["Extra High", "XHigh"], max: ["Max"] }
|
|
785
|
+
function usableTitle(u) {
|
|
786
|
+
const ws = u.name.split(" ")
|
|
787
|
+
// the last time a phrase is said, past the name's first word
|
|
788
|
+
const cutLast = (phrase) => {
|
|
789
|
+
const p = phrase.toLowerCase().split(" ")
|
|
790
|
+
for (let i = ws.length - p.length; i >= 1; i--) {
|
|
791
|
+
if (p.every((w, k) => ws[i + k].toLowerCase() === w)) {
|
|
792
|
+
ws.splice(i, p.length)
|
|
793
|
+
return true
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
return false
|
|
797
|
+
}
|
|
798
|
+
for (const p of EFFORT_WORDS[u.effort] ?? []) if (cutLast(p)) break
|
|
799
|
+
if (u.fast) cutLast("fast")
|
|
800
|
+
return ws.filter((w, i) => i === 0 || !/^\(?1m\)?$/i.test(w)).join(" ") || u.name
|
|
801
|
+
}
|
|
802
|
+
|
|
738
803
|
// modelsOf is the picker's models as offered: one entry for each context
|
|
739
804
|
// size a model has, or one with Cursor's limit when it has none. A model
|
|
740
805
|
// served in part is offered at the sizes and efforts it is served at.
|
|
741
|
-
|
|
806
|
+
// listed is the usable list: a family of its ids the picker has nothing of
|
|
807
|
+
// (a Team's picker can leave out what cursor-agent models lists, magpie
|
|
808
|
+
// #3150 ysm119 on Discord) is offered too, once, with its efforts, and each
|
|
809
|
+
// entry keeps its family's usable ids, so a request goes as one of those
|
|
810
|
+
// and never as a name Cursor would answer "AI Model Not Found".
|
|
811
|
+
function modelsOf(picker, ids, listed = []) {
|
|
742
812
|
const out = []
|
|
743
813
|
const used = new Set()
|
|
814
|
+
const fams = familiesOf(listed)
|
|
815
|
+
// every id the picker names at all, shown or left out
|
|
816
|
+
const named = new Set()
|
|
817
|
+
for (const m of picker) {
|
|
818
|
+
if (!m?.name) continue
|
|
819
|
+
for (const x of [m.name, ...(m.legacySlugs ?? []), ...(m.idAliases ?? []), ...(m.variants ?? []).map((v) => v.legacySlug)]) if (x) named.add(x)
|
|
820
|
+
}
|
|
744
821
|
for (const m of picker) {
|
|
745
822
|
if (!m?.name || m.isHidden || m.isChatOnly || m.onlySupportsCmdK || m.supportsAgent === false || PICKER_SKIP.has(m.name)) continue
|
|
746
823
|
const defs = paramDefs(m)
|
|
@@ -750,7 +827,8 @@ function modelsOf(picker, ids) {
|
|
|
750
827
|
const def = defaultVariant({ variants })
|
|
751
828
|
const { pid, levels: all } = effortsOf(defs)
|
|
752
829
|
const levels = Object.fromEntries(Object.entries(all).filter(([, value]) => has(pid, value)))
|
|
753
|
-
const
|
|
830
|
+
const fam = fams.get(familyOf(def?.legacySlug || m.name).family) ?? []
|
|
831
|
+
const base = { run: m.name, title: clean(m.clientDisplayName || m.name), pid, levels, fast: !!defs.fast?.length, variants, def, partial, model: m, fam, ids }
|
|
754
832
|
const contexts = (defs.context ?? []).filter((c) => has("context", c))
|
|
755
833
|
if (contexts.length) {
|
|
756
834
|
for (const c of contexts) {
|
|
@@ -765,6 +843,15 @@ function modelsOf(picker, ids) {
|
|
|
765
843
|
out.push({ ...base, id: m.name === "default" ? "auto" : m.name, name: base.title, context: limit > 0 ? limit : DEFAULT_CONTEXT, size: "", offersFast: fastAt(variants, "") })
|
|
766
844
|
}
|
|
767
845
|
}
|
|
846
|
+
for (const [family, fam] of fams) {
|
|
847
|
+
if (fam.some((u) => named.has(u.id)) || named.has(family) || used.has(family)) continue
|
|
848
|
+
used.add(family)
|
|
849
|
+
const levels = Object.fromEntries(fam.filter((u) => LEVEL_RANK.includes(u.effort)).map((u) => [u.effort, u.effort]))
|
|
850
|
+
const def = fromUsable(fam, "", false)
|
|
851
|
+
// Cursor's context for it isn't known: its default's, as for a model
|
|
852
|
+
// the picker gives no limit for
|
|
853
|
+
out.push({ id: family, run: family, name: usableTitle(def), title: usableTitle(def), pid: "", levels, fast: false, variants: [], def: undefined, partial: false, model: { name: family }, fam, ids, usableOnly: true, context: DEFAULT_CONTEXT, size: "", offersFast: fam.some((u) => u.fast) })
|
|
854
|
+
}
|
|
768
855
|
return out
|
|
769
856
|
}
|
|
770
857
|
|
|
@@ -797,6 +884,11 @@ function nearest(vs, want) {
|
|
|
797
884
|
// variant the account has. seed is the variant an id of Cursor's own named
|
|
798
885
|
// (request), in the default's place.
|
|
799
886
|
function pick(entry, effort, fast, seed) {
|
|
887
|
+
// a family the usable list has and the picker doesn't: its own id
|
|
888
|
+
if (entry.usableOnly) {
|
|
889
|
+
const u = fromUsable(entry.fam, effort, fast)
|
|
890
|
+
return { id: u.id, params: [], maxMode: u.maxMode }
|
|
891
|
+
}
|
|
800
892
|
const def = entry.def
|
|
801
893
|
const want = { ...valuesOf(seed ?? def) }
|
|
802
894
|
if (entry.size) want.context = entry.size
|
|
@@ -824,6 +916,14 @@ function pick(entry, effort, fast, seed) {
|
|
|
824
916
|
// cursor-agent models lists it: claude-opus-5-5-high), not the picker's
|
|
825
917
|
// model name; the default's when no variant matched
|
|
826
918
|
const id = v?.legacySlug || def?.legacySlug || entry.run
|
|
919
|
+
// an id the usable list doesn't have, when it has the family's (a
|
|
920
|
+
// picker model without variants goes by its name, claude-opus-5-5,
|
|
921
|
+
// which Cursor doesn't serve): the family's usable id at the effort
|
|
922
|
+
if (entry.ids && !entry.ids.has(id) && entry.fam?.length) {
|
|
923
|
+
const level = entry.pid && entry.pid !== "thinking" ? String(want[entry.pid] ?? "").replace("extra-high", "xhigh") : ""
|
|
924
|
+
const u = fromUsable(entry.fam, effort || level, fast)
|
|
925
|
+
return { id: u.id, params: [], maxMode: maxMode || u.maxMode }
|
|
926
|
+
}
|
|
827
927
|
return { id, params, maxMode }
|
|
828
928
|
}
|
|
829
929
|
|
|
@@ -878,17 +978,15 @@ async function usable(tok) {
|
|
|
878
978
|
const h = createHash("sha256").update(tok).digest("hex")
|
|
879
979
|
const had = lists.get(h)
|
|
880
980
|
if (had && Date.now() - had.at < MODELS_KEEP) return had.raw
|
|
881
|
-
const [j,
|
|
981
|
+
const [j, listed] = await Promise.all([
|
|
882
982
|
unary(API, "aiserver.v1.AiService/AvailableModels", tok, { useModelParameters: true, doNotUseMarkdown: true }, 30_000),
|
|
883
983
|
unary(API, "agent.v1.AgentService/GetUsableModels", tok, {}, 8_000).then(
|
|
884
|
-
(u) =>
|
|
885
|
-
|
|
886
|
-
return ids.size ? ids : null
|
|
887
|
-
},
|
|
888
|
-
() => null,
|
|
984
|
+
(u) => (Array.isArray(u?.models) ? u.models : []),
|
|
985
|
+
() => [],
|
|
889
986
|
),
|
|
890
987
|
])
|
|
891
|
-
const
|
|
988
|
+
const ids = new Set(listed.flatMap((m) => [m?.modelId, m?.displayModelId]).filter(Boolean))
|
|
989
|
+
const raw = modelsOf(Array.isArray(j?.models) ? j.models : [], ids.size ? ids : null, listed)
|
|
892
990
|
if (!raw.length) throw new Error("Cursor listed no models")
|
|
893
991
|
lists.set(h, { at: Date.now(), raw })
|
|
894
992
|
return raw
|
|
@@ -921,8 +1019,48 @@ function named(raw, id) {
|
|
|
921
1019
|
}
|
|
922
1020
|
for (const e of models) {
|
|
923
1021
|
const m = e.model
|
|
1022
|
+
if (e.usableOnly) continue
|
|
924
1023
|
if ([m.name, ...(m.legacySlugs ?? []), ...(m.idAliases ?? [])].includes(id)) return { entry: at(e, e.def), seed: undefined, effort: "", fast: false }
|
|
925
1024
|
}
|
|
1025
|
+
// one of the usable list's own ids, or a family of them, that the picker
|
|
1026
|
+
// has nothing of: that family at the id's effort, fast when it is
|
|
1027
|
+
const of = familyOf(id)
|
|
1028
|
+
for (const e of models) {
|
|
1029
|
+
if (!e.usableOnly) continue
|
|
1030
|
+
const u = e.fam.find((x) => x.id === id)
|
|
1031
|
+
if (u) return { entry: e, seed: undefined, effort: u.effort, fast: u.fast }
|
|
1032
|
+
if (e.id === of.family) return { entry: e, seed: undefined, effort: of.effort, fast: of.fast }
|
|
1033
|
+
}
|
|
1034
|
+
// a family neither has, where the account has the same model with
|
|
1035
|
+
// thinking or without it (claude-haiku-5-5-thinking, which 0.1.x listed,
|
|
1036
|
+
// on a Team account whose list has Claude Haiku 5.5 without a thinking
|
|
1037
|
+
// id; ysm119 on Discord): that one, at the id's effort, never the id as
|
|
1038
|
+
// it is, which Cursor answers "AI Model Not Found"
|
|
1039
|
+
const loose = (f) => f.replace(/-thinking$/, "")
|
|
1040
|
+
const key = loose(of.family)
|
|
1041
|
+
for (const e of models) {
|
|
1042
|
+
if (e.usableOnly) {
|
|
1043
|
+
if (loose(e.id) === key) return { entry: e, seed: undefined, effort: of.effort, fast: of.fast }
|
|
1044
|
+
continue
|
|
1045
|
+
}
|
|
1046
|
+
const vs = e.variants.filter((v) => v.legacySlug && loose(familyOf(v.legacySlug).family) === key)
|
|
1047
|
+
if (vs.length) return as(e, vs, of.effort)
|
|
1048
|
+
}
|
|
1049
|
+
// an id at a context size the list no longer has (claude-opus-5-5@1m,
|
|
1050
|
+
// picked from an earlier list, or from another account's): the model at
|
|
1051
|
+
// the nearest size it has; a model listed without sizes, in Max Mode for
|
|
1052
|
+
// a size over its window
|
|
1053
|
+
const sized = /^(.+)@([^@]+)$/.exec(id)
|
|
1054
|
+
if (sized) {
|
|
1055
|
+
const want = tokensOf(sized[2])
|
|
1056
|
+
const same = raw.filter((e) => e.run === sized[1] && e.size)
|
|
1057
|
+
if (same.length) {
|
|
1058
|
+
const near = same.reduce((a, b) => (Math.abs(tokensOf(b.size) - want) < Math.abs(tokensOf(a.size) - want) ? b : a))
|
|
1059
|
+
return { entry: near, seed: undefined, effort: "", fast: false }
|
|
1060
|
+
}
|
|
1061
|
+
const old = named(raw, sized[1])
|
|
1062
|
+
if (old) return { ...old, max: !old.entry.size && want > old.entry.context }
|
|
1063
|
+
}
|
|
926
1064
|
return null
|
|
927
1065
|
}
|
|
928
1066
|
|
|
@@ -933,8 +1071,17 @@ function named(raw, id) {
|
|
|
933
1071
|
function request(raw, model, effort, fast) {
|
|
934
1072
|
const entry = raw.find((m) => m.id === model)
|
|
935
1073
|
if (entry) return pick(entry, effort, fast)
|
|
1074
|
+
const bare = raw.find((m) => m.id === model.replace(/@[^@]*$/, ""))
|
|
1075
|
+
if (bare && model !== bare.id && !bare.size) {
|
|
1076
|
+
// a model now listed without sizes, asked at one
|
|
1077
|
+
const r = pick(bare, effort, fast)
|
|
1078
|
+
return tokensOf(model.split("@").at(-1)) > bare.context ? { ...r, maxMode: true } : r
|
|
1079
|
+
}
|
|
936
1080
|
const old = named(raw, model)
|
|
937
|
-
if (old)
|
|
1081
|
+
if (old) {
|
|
1082
|
+
const r = pick(old.entry, effort || old.effort, fast || old.fast, old.seed)
|
|
1083
|
+
return old.max ? { ...r, maxMode: true } : r
|
|
1084
|
+
}
|
|
938
1085
|
return { id: model === "auto" ? "default" : model, params: [], maxMode: false }
|
|
939
1086
|
}
|
|
940
1087
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@magpie-community/opencode-cursor-auth",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.25",
|
|
4
4
|
"description": "Cursor subscriptions (Pro, Pro+, Ultra, Teams) in OpenCode and magpie, through the API cursor-agent talks to",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.mjs",
|