@magpie-community/opencode-cursor-auth 0.2.23 → 0.2.24

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.
Files changed (3) hide show
  1. package/README.md +11 -0
  2. package/index.mjs +125 -10
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -197,6 +197,17 @@ 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.
207
+ - An `@<size>` id the list no longer has (`claude-opus-5-5@1m` when only
208
+ 300k is listed, or the model is listed unsized) runs at the model's
209
+ nearest size, in Max Mode when the size asked for is over the model's
210
+ window, never as the literal `@<size>` id.
200
211
  - An id from before 0.2.0 still runs. A variant's own id
201
212
  (`claude-opus-5-5-high`) is that variant; a family 0.1.x listed
202
213
  (`claude-opus-5-5`, `claude-opus-5-5-fast`, `claude-haiku-5-5-thinking`,
package/index.mjs CHANGED
@@ -735,12 +735,72 @@ 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: the
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).
779
+ function usableTitle(name) {
780
+ const drop = new Set(["low", "medium", "high", "xhigh", "extra", "max", "minimal", "none", "fast", "1m", "(1m)"])
781
+ const ws = name.split(" ")
782
+ while (ws.length > 1 && drop.has(ws.at(-1).toLowerCase())) ws.pop()
783
+ return ws.join(" ")
784
+ }
785
+
738
786
  // modelsOf is the picker's models as offered: one entry for each context
739
787
  // size a model has, or one with Cursor's limit when it has none. A model
740
788
  // served in part is offered at the sizes and efforts it is served at.
741
- function modelsOf(picker, ids) {
789
+ // listed is the usable list: a family of its ids the picker has nothing of
790
+ // (a Team's picker can leave out what cursor-agent models lists, magpie
791
+ // #3150 ysm119 on Discord) is offered too, once, with its efforts, and each
792
+ // entry keeps its family's usable ids, so a request goes as one of those
793
+ // and never as a name Cursor would answer "AI Model Not Found".
794
+ function modelsOf(picker, ids, listed = []) {
742
795
  const out = []
743
796
  const used = new Set()
797
+ const fams = familiesOf(listed)
798
+ // every id the picker names at all, shown or left out
799
+ const named = new Set()
800
+ for (const m of picker) {
801
+ if (!m?.name) continue
802
+ for (const x of [m.name, ...(m.legacySlugs ?? []), ...(m.idAliases ?? []), ...(m.variants ?? []).map((v) => v.legacySlug)]) if (x) named.add(x)
803
+ }
744
804
  for (const m of picker) {
745
805
  if (!m?.name || m.isHidden || m.isChatOnly || m.onlySupportsCmdK || m.supportsAgent === false || PICKER_SKIP.has(m.name)) continue
746
806
  const defs = paramDefs(m)
@@ -750,7 +810,8 @@ function modelsOf(picker, ids) {
750
810
  const def = defaultVariant({ variants })
751
811
  const { pid, levels: all } = effortsOf(defs)
752
812
  const levels = Object.fromEntries(Object.entries(all).filter(([, value]) => has(pid, value)))
753
- const base = { run: m.name, title: clean(m.clientDisplayName || m.name), pid, levels, fast: !!defs.fast?.length, variants, def, partial, model: m }
813
+ const fam = fams.get(familyOf(def?.legacySlug || m.name).family) ?? []
814
+ 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
815
  const contexts = (defs.context ?? []).filter((c) => has("context", c))
755
816
  if (contexts.length) {
756
817
  for (const c of contexts) {
@@ -765,6 +826,15 @@ function modelsOf(picker, ids) {
765
826
  out.push({ ...base, id: m.name === "default" ? "auto" : m.name, name: base.title, context: limit > 0 ? limit : DEFAULT_CONTEXT, size: "", offersFast: fastAt(variants, "") })
766
827
  }
767
828
  }
829
+ for (const [family, fam] of fams) {
830
+ if (fam.some((u) => named.has(u.id)) || named.has(family) || used.has(family)) continue
831
+ used.add(family)
832
+ const levels = Object.fromEntries(fam.filter((u) => LEVEL_RANK.includes(u.effort)).map((u) => [u.effort, u.effort]))
833
+ const def = fromUsable(fam, "", false)
834
+ // Cursor's context for it isn't known: its default's, as for a model
835
+ // the picker gives no limit for
836
+ out.push({ id: family, run: family, name: usableTitle(def.name), title: usableTitle(def.name), 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) })
837
+ }
768
838
  return out
769
839
  }
770
840
 
@@ -797,6 +867,11 @@ function nearest(vs, want) {
797
867
  // variant the account has. seed is the variant an id of Cursor's own named
798
868
  // (request), in the default's place.
799
869
  function pick(entry, effort, fast, seed) {
870
+ // a family the usable list has and the picker doesn't: its own id
871
+ if (entry.usableOnly) {
872
+ const u = fromUsable(entry.fam, effort, fast)
873
+ return { id: u.id, params: [], maxMode: u.maxMode }
874
+ }
800
875
  const def = entry.def
801
876
  const want = { ...valuesOf(seed ?? def) }
802
877
  if (entry.size) want.context = entry.size
@@ -824,6 +899,14 @@ function pick(entry, effort, fast, seed) {
824
899
  // cursor-agent models lists it: claude-opus-5-5-high), not the picker's
825
900
  // model name; the default's when no variant matched
826
901
  const id = v?.legacySlug || def?.legacySlug || entry.run
902
+ // an id the usable list doesn't have, when it has the family's (a
903
+ // picker model without variants goes by its name, claude-opus-5-5,
904
+ // which Cursor doesn't serve): the family's usable id at the effort
905
+ if (entry.ids && !entry.ids.has(id) && entry.fam?.length) {
906
+ const level = entry.pid && entry.pid !== "thinking" ? String(want[entry.pid] ?? "").replace("extra-high", "xhigh") : ""
907
+ const u = fromUsable(entry.fam, effort || level, fast)
908
+ return { id: u.id, params: [], maxMode: maxMode || u.maxMode }
909
+ }
827
910
  return { id, params, maxMode }
828
911
  }
829
912
 
@@ -878,17 +961,15 @@ async function usable(tok) {
878
961
  const h = createHash("sha256").update(tok).digest("hex")
879
962
  const had = lists.get(h)
880
963
  if (had && Date.now() - had.at < MODELS_KEEP) return had.raw
881
- const [j, ids] = await Promise.all([
964
+ const [j, listed] = await Promise.all([
882
965
  unary(API, "aiserver.v1.AiService/AvailableModels", tok, { useModelParameters: true, doNotUseMarkdown: true }, 30_000),
883
966
  unary(API, "agent.v1.AgentService/GetUsableModels", tok, {}, 8_000).then(
884
- (u) => {
885
- const ids = new Set((u?.models ?? []).flatMap((m) => [m.modelId, m.displayModelId]).filter(Boolean))
886
- return ids.size ? ids : null
887
- },
888
- () => null,
967
+ (u) => (Array.isArray(u?.models) ? u.models : []),
968
+ () => [],
889
969
  ),
890
970
  ])
891
- const raw = modelsOf(Array.isArray(j?.models) ? j.models : [], ids)
971
+ const ids = new Set(listed.flatMap((m) => [m?.modelId, m?.displayModelId]).filter(Boolean))
972
+ const raw = modelsOf(Array.isArray(j?.models) ? j.models : [], ids.size ? ids : null, listed)
892
973
  if (!raw.length) throw new Error("Cursor listed no models")
893
974
  lists.set(h, { at: Date.now(), raw })
894
975
  return raw
@@ -921,8 +1002,33 @@ function named(raw, id) {
921
1002
  }
922
1003
  for (const e of models) {
923
1004
  const m = e.model
1005
+ if (e.usableOnly) continue
924
1006
  if ([m.name, ...(m.legacySlugs ?? []), ...(m.idAliases ?? [])].includes(id)) return { entry: at(e, e.def), seed: undefined, effort: "", fast: false }
925
1007
  }
1008
+ // one of the usable list's own ids, or a family of them, that the picker
1009
+ // has nothing of: that family at the id's effort, fast when it is
1010
+ const of = familyOf(id)
1011
+ for (const e of models) {
1012
+ if (!e.usableOnly) continue
1013
+ const u = e.fam.find((x) => x.id === id)
1014
+ if (u) return { entry: e, seed: undefined, effort: u.effort, fast: u.fast }
1015
+ if (e.id === of.family) return { entry: e, seed: undefined, effort: of.effort, fast: of.fast }
1016
+ }
1017
+ // an id at a context size the list no longer has (claude-opus-5-5@1m,
1018
+ // picked from an earlier list, or from another account's): the model at
1019
+ // the nearest size it has; a model listed without sizes, in Max Mode for
1020
+ // a size over its window
1021
+ const sized = /^(.+)@([^@]+)$/.exec(id)
1022
+ if (sized) {
1023
+ const want = tokensOf(sized[2])
1024
+ const same = raw.filter((e) => e.run === sized[1] && e.size)
1025
+ if (same.length) {
1026
+ const near = same.reduce((a, b) => (Math.abs(tokensOf(b.size) - want) < Math.abs(tokensOf(a.size) - want) ? b : a))
1027
+ return { entry: near, seed: undefined, effort: "", fast: false }
1028
+ }
1029
+ const old = named(raw, sized[1])
1030
+ if (old) return { ...old, max: !old.entry.size && want > old.entry.context }
1031
+ }
926
1032
  return null
927
1033
  }
928
1034
 
@@ -933,8 +1039,17 @@ function named(raw, id) {
933
1039
  function request(raw, model, effort, fast) {
934
1040
  const entry = raw.find((m) => m.id === model)
935
1041
  if (entry) return pick(entry, effort, fast)
1042
+ const bare = raw.find((m) => m.id === model.replace(/@[^@]*$/, ""))
1043
+ if (bare && model !== bare.id && !bare.size) {
1044
+ // a model now listed without sizes, asked at one
1045
+ const r = pick(bare, effort, fast)
1046
+ return tokensOf(model.split("@").at(-1)) > bare.context ? { ...r, maxMode: true } : r
1047
+ }
936
1048
  const old = named(raw, model)
937
- if (old) return pick(old.entry, effort || old.effort, fast || old.fast, old.seed)
1049
+ if (old) {
1050
+ const r = pick(old.entry, effort || old.effort, fast || old.fast, old.seed)
1051
+ return old.max ? { ...r, maxMode: true } : r
1052
+ }
938
1053
  return { id: model === "auto" ? "default" : model, params: [], maxMode: false }
939
1054
  }
940
1055
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magpie-community/opencode-cursor-auth",
3
- "version": "0.2.23",
3
+ "version": "0.2.24",
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",