pi-provider-cursor-ask 0.1.0
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/CHANGELOG.md +9 -0
- package/LICENSE +21 -0
- package/README.md +87 -0
- package/README.zh-CN.md +87 -0
- package/UPSTREAM_CHANGELOG.md +368 -0
- package/UPSTREAM_SOURCE.md +23 -0
- package/dist/index.js +54 -0
- package/package.json +97 -0
- package/src/auth/cli-credentials.ts +275 -0
- package/src/auth/consent.ts +25 -0
- package/src/auth/index.ts +23 -0
- package/src/auth/oauth.ts +282 -0
- package/src/auth/refresh-guard.ts +93 -0
- package/src/client/bridge.ts +673 -0
- package/src/client/cursor-wire.ts +213 -0
- package/src/client/h2-unary.ts +142 -0
- package/src/client/index.ts +18 -0
- package/src/config/index.ts +69 -0
- package/src/diagnostics/diagnostics.ts +116 -0
- package/src/diagnostics/index.ts +1 -0
- package/src/extension/auth.ts +99 -0
- package/src/extension/commands.ts +163 -0
- package/src/extension/compaction-guard.ts +86 -0
- package/src/extension/debug-hooks.ts +359 -0
- package/src/extension/index.ts +8 -0
- package/src/extension/provider.ts +277 -0
- package/src/extension/quota-adapter.ts +175 -0
- package/src/extension/report-dashboard.ts +133 -0
- package/src/identity.ts +16 -0
- package/src/index.ts +186 -0
- package/src/models/ask-catalog.ts +384 -0
- package/src/models/catalog.json +1163 -0
- package/src/models/cost.ts +126 -0
- package/src/models/index.ts +6 -0
- package/src/models/limits.ts +36 -0
- package/src/models/parameterized.ts +416 -0
- package/src/models/processing.ts +313 -0
- package/src/proto/agent_pb.ts +14577 -0
- package/src/stream/bridge-session.ts +215 -0
- package/src/stream/client-transcript.ts +51 -0
- package/src/stream/config.ts +5 -0
- package/src/stream/context-normalize.ts +308 -0
- package/src/stream/context-usage.ts +168 -0
- package/src/stream/debug-log.ts +316 -0
- package/src/stream/drift.ts +122 -0
- package/src/stream/images.ts +201 -0
- package/src/stream/index.ts +68 -0
- package/src/stream/interaction-query.ts +369 -0
- package/src/stream/message-parsing.ts +402 -0
- package/src/stream/model-cache.ts +100 -0
- package/src/stream/model-discovery.ts +242 -0
- package/src/stream/model-routing.ts +100 -0
- package/src/stream/native-core.ts +2121 -0
- package/src/stream/pi-adapter.ts +414 -0
- package/src/stream/protocol.ts +63 -0
- package/src/stream/recovery.ts +494 -0
- package/src/stream/request-build.ts +668 -0
- package/src/stream/root-prompt.ts +184 -0
- package/src/stream/run-journal.ts +474 -0
- package/src/stream/run-usage.ts +107 -0
- package/src/stream/server-messages.ts +777 -0
- package/src/stream/session-state.ts +499 -0
- package/src/stream/stream-writer.ts +211 -0
- package/src/stream/thinking-filter.ts +63 -0
- package/src/stream/tool-schema.ts +185 -0
- package/src/stream/transport-errors.ts +150 -0
- package/src/stream/tuning.ts +250 -0
- package/src/stream/types.ts +330 -0
- package/src/types/enums.ts +103 -0
- package/src/types/index.ts +4 -0
- package/src/usage.ts +262 -0
- package/src/utils/cache-dir.ts +39 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/security.ts +68 -0
- package/src/utils/util.ts +43 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model token pricing and cost estimation table.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface ModelCost {
|
|
6
|
+
input: number;
|
|
7
|
+
output: number;
|
|
8
|
+
cacheRead: number;
|
|
9
|
+
cacheWrite: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const MODEL_COST_TABLE: Record<string, ModelCost> = {
|
|
13
|
+
"claude-4-sonnet": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
|
|
14
|
+
"claude-4.5-haiku": { input: 1, output: 5, cacheRead: 0.1, cacheWrite: 1.25 },
|
|
15
|
+
"claude-4.5-opus": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
|
|
16
|
+
"claude-4.5-sonnet": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
|
|
17
|
+
"claude-4.6-opus": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
|
|
18
|
+
"claude-4.6-sonnet": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
|
|
19
|
+
"claude-fable-5": { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 },
|
|
20
|
+
"claude-fable-5-1": { input: 10, output: 50, cacheRead: 0.25, cacheWrite: 12.5 },
|
|
21
|
+
"claude-opus-4-6": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
|
|
22
|
+
"claude-opus-5": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
|
|
23
|
+
"claude-sonnet-5": { input: 2, output: 10, cacheRead: 0.2, cacheWrite: 2.5 },
|
|
24
|
+
"composer-1": { input: 1.25, output: 10, cacheRead: 0.125, cacheWrite: 0 },
|
|
25
|
+
"composer-1.5": { input: 3.5, output: 17.5, cacheRead: 0.35, cacheWrite: 0 },
|
|
26
|
+
"composer-2": { input: 0.5, output: 2.5, cacheRead: 0.2, cacheWrite: 0 },
|
|
27
|
+
"composer-2.5": { input: 0.5, output: 2.5, cacheRead: 0.2, cacheWrite: 0 },
|
|
28
|
+
"composer-2.5-fast": { input: 3, output: 15, cacheRead: 0.5, cacheWrite: 0 },
|
|
29
|
+
"gemini-2.5-flash": { input: 0.3, output: 2.5, cacheRead: 0.03, cacheWrite: 0 },
|
|
30
|
+
"gemini-3-flash": { input: 0.5, output: 3, cacheRead: 0.05, cacheWrite: 0 },
|
|
31
|
+
"gemini-3-pro": { input: 2, output: 12, cacheRead: 0.2, cacheWrite: 0 },
|
|
32
|
+
"gemini-3.1-pro": { input: 2, output: 12, cacheRead: 0.2, cacheWrite: 0 },
|
|
33
|
+
"gpt-5": { input: 1.25, output: 10, cacheRead: 0.125, cacheWrite: 0 },
|
|
34
|
+
"gpt-5-mini": { input: 0.25, output: 2, cacheRead: 0.025, cacheWrite: 0 },
|
|
35
|
+
"gpt-5.2": { input: 1.75, output: 14, cacheRead: 0.175, cacheWrite: 0 },
|
|
36
|
+
"gpt-5.2-codex": { input: 1.75, output: 14, cacheRead: 0.175, cacheWrite: 0 },
|
|
37
|
+
"gpt-5.3-codex": { input: 1.75, output: 14, cacheRead: 0.175, cacheWrite: 0 },
|
|
38
|
+
"gpt-5.4": { input: 2.5, output: 15, cacheRead: 0.25, cacheWrite: 0 },
|
|
39
|
+
"gpt-5.4-mini": { input: 0.75, output: 4.5, cacheRead: 0.075, cacheWrite: 0 },
|
|
40
|
+
"gpt-5.5": { input: 2.5, output: 15, cacheRead: 0.25, cacheWrite: 0 },
|
|
41
|
+
"grok-4.20": { input: 2, output: 6, cacheRead: 0.2, cacheWrite: 0 },
|
|
42
|
+
// Mirrors pi core's built-in xai/grok-4.6 row (models.dev): 2 / 6 / 0.5 / 0.
|
|
43
|
+
// Cursor-side billing may differ; users can override via models.json.
|
|
44
|
+
"grok-4.6": { input: 2, output: 6, cacheRead: 0.5, cacheWrite: 0 },
|
|
45
|
+
"kimi-k2.5": { input: 0.6, output: 3, cacheRead: 0.1, cacheWrite: 0 },
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const DEFAULT_COST: ModelCost = { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 0 };
|
|
49
|
+
|
|
50
|
+
export const MODEL_COST_PATTERNS: Array<{ match: (id: string) => boolean; cost: ModelCost }> = [
|
|
51
|
+
{
|
|
52
|
+
match: (id) => /fable-5\.1|fable-5-1(?!m)/i.test(id),
|
|
53
|
+
cost: MODEL_COST_TABLE["claude-fable-5-1"] ?? DEFAULT_COST,
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
match: (id) => /fable/i.test(id),
|
|
57
|
+
cost: MODEL_COST_TABLE["claude-fable-5"] ?? DEFAULT_COST,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
match: (id) => /sonnet-5/i.test(id),
|
|
61
|
+
cost: MODEL_COST_TABLE["claude-sonnet-5"] ?? DEFAULT_COST,
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
match: (id) => /claude.*opus.*fast/i.test(id),
|
|
65
|
+
cost: { input: 30, output: 150, cacheRead: 3, cacheWrite: 37.5 },
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
match: (id) => /claude.*opus|opus-5|opus-4/i.test(id),
|
|
69
|
+
cost: MODEL_COST_TABLE["claude-4.6-opus"] ?? DEFAULT_COST,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
match: (id) => /claude.*haiku/i.test(id),
|
|
73
|
+
cost: MODEL_COST_TABLE["claude-4.5-haiku"] ?? DEFAULT_COST,
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
match: (id) => /claude.*sonnet/i.test(id),
|
|
77
|
+
cost: MODEL_COST_TABLE["claude-4.6-sonnet"] ?? DEFAULT_COST,
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
match: (id) => /composer-2\.5.*fast|composer-2\.5-fast/i.test(id),
|
|
81
|
+
cost: MODEL_COST_TABLE["composer-2.5-fast"] ?? DEFAULT_COST,
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
match: (id) => /composer-2\.5/i.test(id),
|
|
85
|
+
cost: MODEL_COST_TABLE["composer-2.5"] ?? DEFAULT_COST,
|
|
86
|
+
},
|
|
87
|
+
{ match: (id) => /composer/i.test(id), cost: MODEL_COST_TABLE["composer-1"] ?? DEFAULT_COST },
|
|
88
|
+
{ match: (id) => /gpt-5\.5/i.test(id), cost: MODEL_COST_TABLE["gpt-5.5"] ?? DEFAULT_COST },
|
|
89
|
+
{
|
|
90
|
+
match: (id) => /gpt-5\.4.*mini/i.test(id),
|
|
91
|
+
cost: MODEL_COST_TABLE["gpt-5.4-mini"] ?? DEFAULT_COST,
|
|
92
|
+
},
|
|
93
|
+
{ match: (id) => /gpt-5\.4/i.test(id), cost: MODEL_COST_TABLE["gpt-5.4"] ?? DEFAULT_COST },
|
|
94
|
+
{ match: (id) => /gpt-5\.3/i.test(id), cost: MODEL_COST_TABLE["gpt-5.3-codex"] ?? DEFAULT_COST },
|
|
95
|
+
{ match: (id) => /gpt-5\.2/i.test(id), cost: MODEL_COST_TABLE["gpt-5.2"] ?? DEFAULT_COST },
|
|
96
|
+
{ match: (id) => /gpt-5.*mini/i.test(id), cost: MODEL_COST_TABLE["gpt-5-mini"] ?? DEFAULT_COST },
|
|
97
|
+
{ match: (id) => /gpt-5/i.test(id), cost: MODEL_COST_TABLE["gpt-5"] ?? DEFAULT_COST },
|
|
98
|
+
{
|
|
99
|
+
match: (id) => /gemini.*3\.1/i.test(id),
|
|
100
|
+
cost: MODEL_COST_TABLE["gemini-3.1-pro"] ?? DEFAULT_COST,
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
match: (id) => /gemini.*flash/i.test(id),
|
|
104
|
+
cost: MODEL_COST_TABLE["gemini-2.5-flash"] ?? DEFAULT_COST,
|
|
105
|
+
},
|
|
106
|
+
{ match: (id) => /gemini/i.test(id), cost: MODEL_COST_TABLE["gemini-3-pro"] ?? DEFAULT_COST },
|
|
107
|
+
{
|
|
108
|
+
match: (id) => /grok-4\.6/i.test(id),
|
|
109
|
+
cost: MODEL_COST_TABLE["grok-4.6"] ?? DEFAULT_COST,
|
|
110
|
+
},
|
|
111
|
+
{ match: (id) => /grok/i.test(id), cost: MODEL_COST_TABLE["grok-4.20"] ?? DEFAULT_COST },
|
|
112
|
+
{ match: (id) => /kimi/i.test(id), cost: MODEL_COST_TABLE["kimi-k2.5"] ?? DEFAULT_COST },
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
export function estimateModelCost(modelId: string): ModelCost {
|
|
116
|
+
const normalized = modelId.toLowerCase();
|
|
117
|
+
const exact = MODEL_COST_TABLE[normalized];
|
|
118
|
+
if (exact) return exact;
|
|
119
|
+
const stripped = normalized.replace(
|
|
120
|
+
/-(high|medium|low|preview|thinking|spark-preview|fast)$/g,
|
|
121
|
+
"",
|
|
122
|
+
);
|
|
123
|
+
const strippedMatch = MODEL_COST_TABLE[stripped];
|
|
124
|
+
if (strippedMatch) return strippedMatch;
|
|
125
|
+
return MODEL_COST_PATTERNS.find((p) => p.match(normalized))?.cost ?? DEFAULT_COST;
|
|
126
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Re-export catalog JSON for external tooling. Prefer provider registration via src/index.ts. */
|
|
2
|
+
export { default as catalog } from "./catalog.json" with { type: "json" };
|
|
3
|
+
export * from "./cost.js";
|
|
4
|
+
export * from "./processing.js";
|
|
5
|
+
export * from "./parameterized.js";
|
|
6
|
+
export * from "./ask-catalog.js";
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context-window and output-token ceilings for Cursor models.
|
|
3
|
+
*
|
|
4
|
+
* Cursor's `ModelDetails` carries neither number, so both are inferred from the
|
|
5
|
+
* model id and display name. Kept in a dependency-free module because the model
|
|
6
|
+
* catalog needs it at startup and must not drag the transport stack in with it.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const DEFAULT_CONTEXT_WINDOW = 200_000;
|
|
10
|
+
export const DEFAULT_MAX_OUTPUT_TOKENS = 64_000;
|
|
11
|
+
|
|
12
|
+
export function inferCursorContextWindow(id: string, name: string): number {
|
|
13
|
+
const text = `${id} ${name}`.toLowerCase();
|
|
14
|
+
if (/\b1\s*m\b|(?:^|-)1m(?:-|$)/.test(text)) return 1_000_000;
|
|
15
|
+
if (/\b272\s*k\b|(?:^|-)272k(?:-|$)/.test(text)) return 272_000;
|
|
16
|
+
return DEFAULT_CONTEXT_WINDOW;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Pi-side budgeting metadata only: the Cursor run request has no max-output
|
|
21
|
+
* field, so a wrong value here cannot fail a request upstream — it only
|
|
22
|
+
* mis-sizes Pi's output allowance.
|
|
23
|
+
*
|
|
24
|
+
* Conservative by design. Only families whose provider documents a ceiling above
|
|
25
|
+
* 64K are raised; everything else keeps the 64K floor Cursor's older models use.
|
|
26
|
+
*/
|
|
27
|
+
export function inferCursorMaxOutputTokens(id: string, name: string): number {
|
|
28
|
+
const text = `${id} ${name}`.toLowerCase();
|
|
29
|
+
// Claude 4.6 and newer (Opus/Sonnet) document a 128K output ceiling. Claude 4.5
|
|
30
|
+
// and earlier — Haiku 4.5 included — stay at 64K.
|
|
31
|
+
if (/claude-(?:[5-9]|4\.(?:[6-9]|\d{2,}))/.test(text)) return 128_000;
|
|
32
|
+
// Cursor labels these "Opus 4.6" / "Sonnet 4.6" rather than "claude-4.6-*".
|
|
33
|
+
if (/\b(?:sonnet|opus)\s*(?:[5-9]|4\.(?:[6-9]|\d{2,}))/.test(text)) return 128_000;
|
|
34
|
+
if (/\bgpt-5/.test(text)) return 128_000;
|
|
35
|
+
return DEFAULT_MAX_OUTPUT_TOKENS;
|
|
36
|
+
}
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parameterized Cursor models metadata conversion and catalog augmentation.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import rawFallbackModels from "./catalog.json" with { type: "json" };
|
|
6
|
+
import type {
|
|
7
|
+
CursorModelParameter,
|
|
8
|
+
CursorParameterizedModel,
|
|
9
|
+
CursorParameterizedVariant,
|
|
10
|
+
} from "../client/cursor-wire.js";
|
|
11
|
+
import type { CursorModel } from "../stream/model-discovery.js";
|
|
12
|
+
import { inferCursorContextWindow, inferCursorMaxOutputTokens } from "./limits.js";
|
|
13
|
+
import { supportsReasoningModelId } from "./processing.js";
|
|
14
|
+
|
|
15
|
+
export const GPT55_VARIANTS = [
|
|
16
|
+
{
|
|
17
|
+
idPart: "",
|
|
18
|
+
label: "272K",
|
|
19
|
+
context: "272k",
|
|
20
|
+
contextWindow: 272_000,
|
|
21
|
+
requestedMaxMode: false,
|
|
22
|
+
fastOptions: [false, true],
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
idPart: "-max",
|
|
26
|
+
label: "272K Max",
|
|
27
|
+
context: "272k",
|
|
28
|
+
contextWindow: 272_000,
|
|
29
|
+
requestedMaxMode: true,
|
|
30
|
+
fastOptions: [false, true],
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
idPart: "-1m",
|
|
34
|
+
label: "1M",
|
|
35
|
+
context: "1m",
|
|
36
|
+
contextWindow: 1_000_000,
|
|
37
|
+
requestedMaxMode: true,
|
|
38
|
+
fastOptions: [false],
|
|
39
|
+
},
|
|
40
|
+
] as const;
|
|
41
|
+
|
|
42
|
+
export const GPT55_REASONING_LEVELS = [
|
|
43
|
+
{ suffix: "none", label: "None", value: "none" },
|
|
44
|
+
{ suffix: "low", label: "Low", value: "low" },
|
|
45
|
+
{ suffix: "medium", label: "", value: "medium" },
|
|
46
|
+
{ suffix: "high", label: "High", value: "high" },
|
|
47
|
+
{ suffix: "extra-high", label: "Extra High", value: "extra-high" },
|
|
48
|
+
] as const;
|
|
49
|
+
|
|
50
|
+
export function gpt55ParameterizedModels(): CursorModel[] {
|
|
51
|
+
const models: CursorModel[] = [];
|
|
52
|
+
for (const variant of GPT55_VARIANTS) {
|
|
53
|
+
// Cursor treats maxMode as an orthogonal request flag. The model picker
|
|
54
|
+
// cannot toggle Cursor-specific flags, so expose useful maxMode states as
|
|
55
|
+
// explicit rows. Cursor's metadata does not include context=1m + fast=true,
|
|
56
|
+
// so the 1M variant intentionally has fast=false only.
|
|
57
|
+
for (const fast of variant.fastOptions) {
|
|
58
|
+
for (const reasoning of GPT55_REASONING_LEVELS) {
|
|
59
|
+
const id = `gpt-5.5${variant.idPart}-${reasoning.suffix}${fast ? "-fast" : ""}`;
|
|
60
|
+
const nameParts = ["GPT-5.5", variant.label, reasoning.label, fast ? "Fast" : ""].filter(
|
|
61
|
+
Boolean,
|
|
62
|
+
);
|
|
63
|
+
models.push({
|
|
64
|
+
id,
|
|
65
|
+
name: nameParts.join(" "),
|
|
66
|
+
reasoning: true,
|
|
67
|
+
contextWindow: variant.contextWindow,
|
|
68
|
+
maxTokens: inferCursorMaxOutputTokens(id, nameParts.join(" ")),
|
|
69
|
+
requestedModelId: "gpt-5.5",
|
|
70
|
+
requiresMaxMode: variant.context === "1m",
|
|
71
|
+
requestedMaxMode: variant.requestedMaxMode,
|
|
72
|
+
parameters: [
|
|
73
|
+
{ id: "context", value: variant.context },
|
|
74
|
+
{ id: "reasoning", value: reasoning.value },
|
|
75
|
+
{ id: "fast", value: String(fast) },
|
|
76
|
+
],
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return models;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function parameterValue(parameters: CursorModelParameter[], id: string): string | undefined {
|
|
85
|
+
return parameters.find((parameter) => parameter.id === id)?.value;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function contextWindowFromParameter(
|
|
89
|
+
context: string | undefined,
|
|
90
|
+
fallback = 200_000,
|
|
91
|
+
): number {
|
|
92
|
+
if (context === "272k") return 272_000;
|
|
93
|
+
if (context === "1m") return 1_000_000;
|
|
94
|
+
const k = context?.match(/^(\d+)k$/i)?.[1];
|
|
95
|
+
if (k) return Number(k) * 1_000;
|
|
96
|
+
const m = context?.match(/^(\d+)m$/i)?.[1];
|
|
97
|
+
if (m) return Number(m) * 1_000_000;
|
|
98
|
+
return fallback;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function cursorEffortSuffix(value: string): string {
|
|
102
|
+
return value;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function cursorEffortLabel(value: string): string {
|
|
106
|
+
return (
|
|
107
|
+
GPT55_REASONING_LEVELS.find((level) => level.value === value)?.label ||
|
|
108
|
+
({ xhigh: "Extra High", max: "Max", none: "None" } as Record<string, string>)[value] ||
|
|
109
|
+
value.replace(/-/g, " ").replace(/\b\w/g, (char) => char.toUpperCase())
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function metadataEffortParameterId(
|
|
114
|
+
variant: CursorParameterizedVariant,
|
|
115
|
+
): "reasoning" | "effort" | undefined {
|
|
116
|
+
if (variant.parameters.some((parameter) => parameter.id === "reasoning")) return "reasoning";
|
|
117
|
+
if (variant.parameters.some((parameter) => parameter.id === "effort")) return "effort";
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function isDefaultContext(context: string | undefined): boolean {
|
|
122
|
+
if (!context) return true;
|
|
123
|
+
return context === "200k" || context === "272k" || context === "300k";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function contextIdPart(context: string | undefined): string {
|
|
127
|
+
return context && !isDefaultContext(context) ? `-${context.toLowerCase()}` : "";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function contextLabel(context: string | undefined): string | undefined {
|
|
131
|
+
if (!context || isDefaultContext(context)) return undefined;
|
|
132
|
+
return context.toUpperCase();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function maxModeIdPart(
|
|
136
|
+
modelName: string,
|
|
137
|
+
context: string | undefined,
|
|
138
|
+
requestedMaxMode: boolean,
|
|
139
|
+
hasEffortParameter: boolean,
|
|
140
|
+
): string {
|
|
141
|
+
// 1M context already names the Max/extended-context selection. For default
|
|
142
|
+
// context windows, expose maxMode as an explicit row suffix. If the Cursor
|
|
143
|
+
// model ID already contains "max" (for example gpt-5.1-codex-max), or if
|
|
144
|
+
// this row has no effort parameter, use a clearer suffix so the model parser
|
|
145
|
+
// does not confuse Max Mode with a Cursor effort value.
|
|
146
|
+
if (!requestedMaxMode || context === "1m") return "";
|
|
147
|
+
return !hasEffortParameter || /(^|-)max($|-)/i.test(modelName) ? "-max-mode" : "-max";
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function maxModeLabel(
|
|
151
|
+
modelName: string,
|
|
152
|
+
context: string | undefined,
|
|
153
|
+
requestedMaxMode: boolean,
|
|
154
|
+
hasEffortParameter: boolean,
|
|
155
|
+
): string | undefined {
|
|
156
|
+
const idPart = maxModeIdPart(modelName, context, requestedMaxMode, hasEffortParameter);
|
|
157
|
+
if (!idPart) return undefined;
|
|
158
|
+
return idPart === "-max-mode" ? "Max Mode" : "Max";
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function parameterizedBaseId(
|
|
162
|
+
modelName: string,
|
|
163
|
+
variant: CursorParameterizedVariant,
|
|
164
|
+
requestedMaxMode: boolean,
|
|
165
|
+
hasEffortParameter: boolean,
|
|
166
|
+
): string {
|
|
167
|
+
const context = parameterValue(variant.parameters, "context");
|
|
168
|
+
return `${modelName}${contextIdPart(context)}${maxModeIdPart(modelName, context, requestedMaxMode, hasEffortParameter)}`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function parameterizedBaseLabel(
|
|
172
|
+
model: CursorParameterizedModel,
|
|
173
|
+
variant: CursorParameterizedVariant,
|
|
174
|
+
requestedMaxMode: boolean,
|
|
175
|
+
hasEffortParameter: boolean,
|
|
176
|
+
): string[] {
|
|
177
|
+
const context = parameterValue(variant.parameters, "context");
|
|
178
|
+
return [
|
|
179
|
+
model.clientDisplayName || model.name,
|
|
180
|
+
contextLabel(context),
|
|
181
|
+
maxModeLabel(model.name, context, requestedMaxMode, hasEffortParameter),
|
|
182
|
+
].filter(Boolean) as string[];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Every variant of a model is checked against every variant of that same model, so normalizing
|
|
186
|
+
// the model's variant sets on each call made catalog building O(variants^2) in string sorts —
|
|
187
|
+
// the dominant cost of activation with a live catalog. The advertised sets are fixed for a given
|
|
188
|
+
// model object, so derive them once and keep them keyed by model identity.
|
|
189
|
+
const variantParameterSetCache = new WeakMap<CursorParameterizedModel, Set<string>>();
|
|
190
|
+
|
|
191
|
+
function advertisedParameterSets(model: CursorParameterizedModel): Set<string> {
|
|
192
|
+
let sets = variantParameterSetCache.get(model);
|
|
193
|
+
if (!sets) {
|
|
194
|
+
sets = new Set(model.variants.map((variant) => normalizeParameterValues(variant.parameters)));
|
|
195
|
+
variantParameterSetCache.set(model, sets);
|
|
196
|
+
}
|
|
197
|
+
return sets;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function hasVariantParameterSet(
|
|
201
|
+
model: CursorParameterizedModel,
|
|
202
|
+
parameters: CursorModelParameter[],
|
|
203
|
+
): boolean {
|
|
204
|
+
return advertisedParameterSets(model).has(normalizeParameterValues(parameters));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Codepoint order rather than `localeCompare`: this string is only ever compared against other
|
|
208
|
+
// outputs of this function, so the order just has to be deterministic and total. `localeCompare`
|
|
209
|
+
// is both far slower and can rank distinct strings as equal, which would leave the key dependent
|
|
210
|
+
// on the input array's order.
|
|
211
|
+
function compareCodepoint(a: string, b: string): number {
|
|
212
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function normalizeParameterValues(parameters: CursorModelParameter[]): string {
|
|
216
|
+
return parameters
|
|
217
|
+
.map((parameter) => `${parameter.id}=${parameter.value}`)
|
|
218
|
+
.sort(compareCodepoint)
|
|
219
|
+
.join(";");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function buildParameterizedRowsFromGroup(options: {
|
|
223
|
+
model: CursorParameterizedModel;
|
|
224
|
+
variants: CursorParameterizedVariant[];
|
|
225
|
+
requestedMaxMode: boolean;
|
|
226
|
+
effortParameterId?: "reasoning" | "effort";
|
|
227
|
+
}): CursorModel[] {
|
|
228
|
+
const first = options.variants[0];
|
|
229
|
+
if (!first) return [];
|
|
230
|
+
if (options.requestedMaxMode && !first.isMaxMode && !options.model.supportsMaxMode) return [];
|
|
231
|
+
|
|
232
|
+
const context = parameterValue(first.parameters, "context");
|
|
233
|
+
const fast = parameterValue(first.parameters, "fast") === "true";
|
|
234
|
+
const thinking = parameterValue(first.parameters, "thinking") === "true";
|
|
235
|
+
const hasEffortParameter = Boolean(options.effortParameterId);
|
|
236
|
+
const baseId = parameterizedBaseId(
|
|
237
|
+
options.model.name,
|
|
238
|
+
first,
|
|
239
|
+
options.requestedMaxMode,
|
|
240
|
+
hasEffortParameter,
|
|
241
|
+
);
|
|
242
|
+
const baseLabelParts = parameterizedBaseLabel(
|
|
243
|
+
options.model,
|
|
244
|
+
first,
|
|
245
|
+
options.requestedMaxMode,
|
|
246
|
+
hasEffortParameter,
|
|
247
|
+
);
|
|
248
|
+
const contextWindow = contextWindowFromParameter(
|
|
249
|
+
context,
|
|
250
|
+
options.requestedMaxMode
|
|
251
|
+
? (options.model.contextTokenLimitForMaxMode ?? options.model.contextTokenLimit ?? 200_000)
|
|
252
|
+
: (options.model.contextTokenLimit ?? 200_000),
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
return options.variants.flatMap((variant) => {
|
|
256
|
+
const parameters = variant.parameters.map((parameter) => ({
|
|
257
|
+
id: parameter.id,
|
|
258
|
+
value: parameter.value,
|
|
259
|
+
}));
|
|
260
|
+
if (!hasVariantParameterSet(options.model, parameters)) return [];
|
|
261
|
+
|
|
262
|
+
const effort = options.effortParameterId
|
|
263
|
+
? parameterValue(variant.parameters, options.effortParameterId)
|
|
264
|
+
: undefined;
|
|
265
|
+
const id = options.effortParameterId
|
|
266
|
+
? `${baseId}-${cursorEffortSuffix(effort ?? "")}${thinking ? "-thinking" : ""}${fast ? "-fast" : ""}`
|
|
267
|
+
: `${baseId}${thinking ? "-thinking" : ""}${fast ? "-fast" : ""}`;
|
|
268
|
+
const name = [
|
|
269
|
+
...baseLabelParts,
|
|
270
|
+
effort ? cursorEffortLabel(effort) : undefined,
|
|
271
|
+
thinking ? "Thinking" : undefined,
|
|
272
|
+
fast ? "Fast" : undefined,
|
|
273
|
+
]
|
|
274
|
+
.filter(Boolean)
|
|
275
|
+
.join(" ");
|
|
276
|
+
|
|
277
|
+
return [
|
|
278
|
+
{
|
|
279
|
+
id,
|
|
280
|
+
name,
|
|
281
|
+
reasoning: Boolean(options.effortParameterId) || thinking,
|
|
282
|
+
contextWindow,
|
|
283
|
+
maxTokens: inferCursorMaxOutputTokens(id, name),
|
|
284
|
+
requestedModelId: options.model.name,
|
|
285
|
+
requiresMaxMode: variant.isMaxMode,
|
|
286
|
+
requestedMaxMode: options.requestedMaxMode,
|
|
287
|
+
supportsImages: options.model.supportsImages,
|
|
288
|
+
parameters,
|
|
289
|
+
} satisfies CursorModel,
|
|
290
|
+
];
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export function parameterGroupKey(
|
|
295
|
+
variant: CursorParameterizedVariant,
|
|
296
|
+
effortParameterId?: string,
|
|
297
|
+
): string {
|
|
298
|
+
const params = variant.parameters
|
|
299
|
+
.filter((parameter) => parameter.id !== effortParameterId)
|
|
300
|
+
.map((parameter) => `${parameter.id}=${parameter.value}`)
|
|
301
|
+
.sort(compareCodepoint)
|
|
302
|
+
.join(";");
|
|
303
|
+
return `${variant.isMaxMode ? "max" : "nonmax"}|${params}`;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export function shouldGenerateSyntheticMaxRows(
|
|
307
|
+
model: CursorParameterizedModel,
|
|
308
|
+
variant: CursorParameterizedVariant,
|
|
309
|
+
): boolean {
|
|
310
|
+
// Cursor's metadata has both per-variant isMaxMode and model-level
|
|
311
|
+
// supportsMaxMode. Some supported Max Mode combinations are represented only
|
|
312
|
+
// by supportsMaxMode=true over a non-Max parameter set, so expose explicit
|
|
313
|
+
// max-mode rows for every such advertised parameter set.
|
|
314
|
+
return model.supportsMaxMode === true && !variant.isMaxMode;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export function modelsFromParameterizedMetadata(
|
|
318
|
+
parameterizedModels: CursorParameterizedModel[],
|
|
319
|
+
): CursorModel[] {
|
|
320
|
+
const rows: CursorModel[] = [];
|
|
321
|
+
for (const model of parameterizedModels) {
|
|
322
|
+
const groups = new Map<
|
|
323
|
+
string,
|
|
324
|
+
{ effortParameterId?: "reasoning" | "effort"; variants: CursorParameterizedVariant[] }
|
|
325
|
+
>();
|
|
326
|
+
for (const variant of model.variants) {
|
|
327
|
+
if (variant.parameters.length === 0) continue;
|
|
328
|
+
const effortParameterId = metadataEffortParameterId(variant);
|
|
329
|
+
const key = parameterGroupKey(variant, effortParameterId);
|
|
330
|
+
const group = groups.get(key) ?? { effortParameterId, variants: [] };
|
|
331
|
+
group.variants.push(variant);
|
|
332
|
+
groups.set(key, group);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
for (const group of groups.values()) {
|
|
336
|
+
const first = group.variants[0];
|
|
337
|
+
if (!first) continue;
|
|
338
|
+
rows.push(
|
|
339
|
+
...buildParameterizedRowsFromGroup({
|
|
340
|
+
model,
|
|
341
|
+
variants: group.variants,
|
|
342
|
+
requestedMaxMode: first.isMaxMode,
|
|
343
|
+
effortParameterId: group.effortParameterId,
|
|
344
|
+
}),
|
|
345
|
+
);
|
|
346
|
+
if (shouldGenerateSyntheticMaxRows(model, first)) {
|
|
347
|
+
rows.push(
|
|
348
|
+
...buildParameterizedRowsFromGroup({
|
|
349
|
+
model,
|
|
350
|
+
variants: group.variants,
|
|
351
|
+
requestedMaxMode: true,
|
|
352
|
+
effortParameterId: group.effortParameterId,
|
|
353
|
+
}),
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return rows;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export function normalizeDisplayModel(model: CursorModel): CursorModel {
|
|
362
|
+
if (model.id !== "default") return model;
|
|
363
|
+
return {
|
|
364
|
+
...model,
|
|
365
|
+
id: "auto",
|
|
366
|
+
name: model.name && model.name !== "default" ? model.name : "Auto",
|
|
367
|
+
requestedModelId: model.requestedModelId ?? "default",
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export function augmentCursorModels(
|
|
372
|
+
raw: CursorModel[],
|
|
373
|
+
parameterizedModels: CursorParameterizedModel[] = [],
|
|
374
|
+
): CursorModel[] {
|
|
375
|
+
const byId = new Map<string, CursorModel>();
|
|
376
|
+
const imageSupportByModelId = new Map(
|
|
377
|
+
parameterizedModels
|
|
378
|
+
.filter((model) => typeof model.supportsImages === "boolean")
|
|
379
|
+
.map((model) => [model.name, model.supportsImages!]),
|
|
380
|
+
);
|
|
381
|
+
for (const model of raw.map(normalizeDisplayModel)) {
|
|
382
|
+
const lookupId = model.requestedModelId ?? model.id;
|
|
383
|
+
const metadataSupportsImages = imageSupportByModelId.get(lookupId);
|
|
384
|
+
byId.set(model.id, {
|
|
385
|
+
...model,
|
|
386
|
+
...(model.supportsImages === undefined && metadataSupportsImages !== undefined
|
|
387
|
+
? { supportsImages: metadataSupportsImages }
|
|
388
|
+
: {}),
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const metadataRows =
|
|
393
|
+
modelsFromParameterizedMetadata(parameterizedModels).map(normalizeDisplayModel);
|
|
394
|
+
for (const model of metadataRows) byId.set(model.id, model);
|
|
395
|
+
|
|
396
|
+
// Fallback for static/offline discovery. Cursor exposes GPT-5.5 context as
|
|
397
|
+
// parameters (272K vs 1M), not distinct backend model IDs.
|
|
398
|
+
if (metadataRows.length === 0 && raw.some((model) => /^gpt-5\.5(?:-|$)/.test(model.id))) {
|
|
399
|
+
for (const model of gpt55ParameterizedModels()) byId.set(model.id, model);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return [...byId.values()];
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// The bundled catalog is a snapshot of a live discovery response, so its derived
|
|
406
|
+
// columns are recomputed here rather than trusted. Ten rows had already drifted:
|
|
407
|
+
// every "1M" Claude row claimed a 200K window because the file was hand-edited
|
|
408
|
+
// after `inferCursorContextWindow` learned to read the "1M" suffix.
|
|
409
|
+
export const FALLBACK_MODELS: CursorModel[] = augmentCursorModels(
|
|
410
|
+
rawFallbackModels as CursorModel[],
|
|
411
|
+
).map((model) => ({
|
|
412
|
+
...model,
|
|
413
|
+
reasoning: supportsReasoningModelId(model.id),
|
|
414
|
+
contextWindow: inferCursorContextWindow(model.id, model.name),
|
|
415
|
+
maxTokens: inferCursorMaxOutputTokens(model.id, model.name),
|
|
416
|
+
}));
|