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
package/src/index.ts
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor Provider Extension for pi
|
|
3
|
+
*
|
|
4
|
+
* Provides access to Cursor models (Claude, GPT, Gemini, etc.) via:
|
|
5
|
+
* 1. Browser-based PKCE OAuth login to Cursor
|
|
6
|
+
* 2. Native Pi streamSimple provider translating Pi context → Cursor gRPC protocol
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* /login cursor — authenticate via browser
|
|
10
|
+
* /model — select any Cursor model
|
|
11
|
+
*
|
|
12
|
+
* Based on https://github.com/ephraimduncan/opencode-cursor by Ephraim Duncan.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
16
|
+
import { CURSOR_ASK_LOGIN_COMMAND } from "./identity.js";
|
|
17
|
+
import { resolveSystemCredentialPolicy, systemCredentialsAllowed } from "./auth/consent.js";
|
|
18
|
+
import {
|
|
19
|
+
setLastClientVersion,
|
|
20
|
+
setLastTokenSource,
|
|
21
|
+
setSystemCredentialsPolicy,
|
|
22
|
+
} from "./diagnostics/index.js";
|
|
23
|
+
import { getCursorClientVersion } from "./stream/config.js";
|
|
24
|
+
import { CredentialSource, ProviderConstant } from "./types/enums.js";
|
|
25
|
+
import type { ProcessedModel } from "./models/processing.js";
|
|
26
|
+
import {
|
|
27
|
+
debugExtensionLog,
|
|
28
|
+
getExtensionDebugLogFilePath,
|
|
29
|
+
isExtensionDebugEnabled,
|
|
30
|
+
registerCursorNotifySink,
|
|
31
|
+
registerExtensionDebugHooks,
|
|
32
|
+
registerSessionLifecycleCleanup,
|
|
33
|
+
} from "./extension/debug-hooks.js";
|
|
34
|
+
import { getStartupCursorAccessToken, isTokenNearExpiry } from "./extension/auth.js";
|
|
35
|
+
import { createProviderManager, loadStartupCatalog } from "./extension/provider.js";
|
|
36
|
+
import { registerCursorCommands } from "./extension/commands.js";
|
|
37
|
+
import { registerCursorQuotaAdapters } from "./extension/quota-adapter.js";
|
|
38
|
+
import { registerCursorCompactionGuard } from "./extension/compaction-guard.js";
|
|
39
|
+
|
|
40
|
+
// ── Re-exports for public API, tests, and external scripts ──
|
|
41
|
+
|
|
42
|
+
export {
|
|
43
|
+
extractToolResultImagePayloads,
|
|
44
|
+
type CursorToolResultImagePayload,
|
|
45
|
+
} from "./extension/debug-hooks.js";
|
|
46
|
+
|
|
47
|
+
export {
|
|
48
|
+
applyNoReasoningEffort,
|
|
49
|
+
applyRawCursorModelId,
|
|
50
|
+
buildEffortMap,
|
|
51
|
+
buildNoReasoningEffortLookup,
|
|
52
|
+
buildRawModelLookup,
|
|
53
|
+
modelConfig,
|
|
54
|
+
parseModelId,
|
|
55
|
+
processModels,
|
|
56
|
+
supportsReasoningModelId,
|
|
57
|
+
type CursorEffortMap,
|
|
58
|
+
type CursorModelRouting,
|
|
59
|
+
type ProcessedModel,
|
|
60
|
+
} from "./models/processing.js";
|
|
61
|
+
|
|
62
|
+
export {
|
|
63
|
+
augmentCursorModels,
|
|
64
|
+
FALLBACK_MODELS,
|
|
65
|
+
modelsFromParameterizedMetadata,
|
|
66
|
+
} from "./models/parameterized.js";
|
|
67
|
+
|
|
68
|
+
export {
|
|
69
|
+
ASK_MODEL_SPECS,
|
|
70
|
+
PASSTHROUGH_ASK_SPECS,
|
|
71
|
+
buildAskCatalog,
|
|
72
|
+
supportedAskThinkingLevels,
|
|
73
|
+
} from "./models/ask-catalog.js";
|
|
74
|
+
|
|
75
|
+
export { registerSessionLifecycleCleanup } from "./extension/debug-hooks.js";
|
|
76
|
+
|
|
77
|
+
/** API id used by Cursor models and the native streamSimple implementation. */
|
|
78
|
+
export const CURSOR_NATIVE_API = ProviderConstant.NativeApi;
|
|
79
|
+
|
|
80
|
+
// ── Extension entry point ──
|
|
81
|
+
|
|
82
|
+
export default async function (pi: ExtensionAPI): Promise<void> {
|
|
83
|
+
let currentToken = "";
|
|
84
|
+
let currentTokenSource: CredentialSource = CredentialSource.None;
|
|
85
|
+
let lastRegisteredModels: ProcessedModel[] = [];
|
|
86
|
+
|
|
87
|
+
setSystemCredentialsPolicy(resolveSystemCredentialPolicy());
|
|
88
|
+
setLastClientVersion(getCursorClientVersion());
|
|
89
|
+
|
|
90
|
+
// Proactive token pre-refresh: background-refresh when the token is within 5 minutes
|
|
91
|
+
// of expiry so a fresh token is ready before the stream starts (not mid-retry).
|
|
92
|
+
const TOKEN_PREREFRESH_SKEW_MS = 5 * 60 * 1000;
|
|
93
|
+
let preRefreshInFlight = false;
|
|
94
|
+
|
|
95
|
+
const scheduleProactiveTokenRefresh = (): void => {
|
|
96
|
+
if (preRefreshInFlight || !currentToken || currentTokenSource === CredentialSource.Env) return;
|
|
97
|
+
if (!isTokenNearExpiry(currentToken, TOKEN_PREREFRESH_SKEW_MS)) return;
|
|
98
|
+
preRefreshInFlight = true;
|
|
99
|
+
void getStartupCursorAccessToken({ forceRefresh: true })
|
|
100
|
+
.then((resolved) => {
|
|
101
|
+
if (resolved) {
|
|
102
|
+
currentToken = resolved.accessToken;
|
|
103
|
+
currentTokenSource = resolved.source;
|
|
104
|
+
setLastTokenSource(resolved.source);
|
|
105
|
+
debugExtensionLog("token.proactive_refresh", { source: resolved.source });
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
.catch((err) => {
|
|
109
|
+
debugExtensionLog("token.proactive_refresh_failed", {
|
|
110
|
+
message: err instanceof Error ? err.message : String(err),
|
|
111
|
+
});
|
|
112
|
+
})
|
|
113
|
+
.finally(() => {
|
|
114
|
+
preRefreshInFlight = false;
|
|
115
|
+
});
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const getAccessToken = async (options?: { forceRefresh?: boolean }): Promise<string> => {
|
|
119
|
+
// Kick off a non-blocking pre-refresh if we're close to expiry but not yet expired.
|
|
120
|
+
scheduleProactiveTokenRefresh();
|
|
121
|
+
|
|
122
|
+
const needsRefresh =
|
|
123
|
+
options?.forceRefresh ||
|
|
124
|
+
!currentToken ||
|
|
125
|
+
(currentTokenSource !== CredentialSource.Env && isTokenNearExpiry(currentToken));
|
|
126
|
+
|
|
127
|
+
if (needsRefresh) {
|
|
128
|
+
const resolved = await getStartupCursorAccessToken({
|
|
129
|
+
forceRefresh: options?.forceRefresh || isTokenNearExpiry(currentToken || "x"),
|
|
130
|
+
});
|
|
131
|
+
if (resolved) {
|
|
132
|
+
currentToken = resolved.accessToken;
|
|
133
|
+
currentTokenSource = resolved.source;
|
|
134
|
+
setLastTokenSource(resolved.source);
|
|
135
|
+
} else if (options?.forceRefresh) {
|
|
136
|
+
// Keep previous token only if force refresh failed and we still have one.
|
|
137
|
+
setLastTokenSource(currentTokenSource || CredentialSource.None);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (!currentToken) {
|
|
142
|
+
const consentHint = systemCredentialsAllowed()
|
|
143
|
+
? ""
|
|
144
|
+
: " System credential reuse is disabled (PI_CURSOR_SYSTEM_CREDENTIALS=0).";
|
|
145
|
+
throw new Error(
|
|
146
|
+
`Not logged in to Cursor. Run ${CURSOR_ASK_LOGIN_COMMAND} or log in via Cursor CLI.${consentHint}`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
setLastTokenSource(currentTokenSource);
|
|
150
|
+
return currentToken;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
registerSessionLifecycleCleanup(pi);
|
|
154
|
+
registerCursorCompactionGuard(pi);
|
|
155
|
+
registerCursorNotifySink(pi);
|
|
156
|
+
registerExtensionDebugHooks(pi);
|
|
157
|
+
debugExtensionLog("extension.start", {
|
|
158
|
+
mode: "native-streamSimple",
|
|
159
|
+
debugLogFile: isExtensionDebugEnabled() ? getExtensionDebugLogFilePath() : undefined,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const providerManager = createProviderManager(pi, {
|
|
163
|
+
getAccessToken,
|
|
164
|
+
setCurrentToken: (token, source) => {
|
|
165
|
+
currentToken = token;
|
|
166
|
+
currentTokenSource = source;
|
|
167
|
+
setLastTokenSource(source);
|
|
168
|
+
},
|
|
169
|
+
onRegisteredModelsUpdated: (models) => {
|
|
170
|
+
lastRegisteredModels = models;
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// Activation must not block on the network. Register the last-known-good
|
|
175
|
+
// catalog (or the bundled fallback) synchronously; pi calls refreshModels in
|
|
176
|
+
// the background and updates an open /model selector with the live list.
|
|
177
|
+
const startupCatalog = loadStartupCatalog();
|
|
178
|
+
providerManager.registerModels(startupCatalog.rawModels, startupCatalog.parameterizedModels);
|
|
179
|
+
|
|
180
|
+
registerCursorCommands(pi, {
|
|
181
|
+
getAccessToken,
|
|
182
|
+
getLastRegisteredModels: () => lastRegisteredModels,
|
|
183
|
+
getCurrentTokenSource: () => currentTokenSource,
|
|
184
|
+
});
|
|
185
|
+
registerCursorQuotaAdapters(getAccessToken);
|
|
186
|
+
}
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stable, advisor-oriented Cursor Ask catalog.
|
|
3
|
+
*
|
|
4
|
+
* Upstream Cursor catalogs expose backend model ids and parameter variants.
|
|
5
|
+
* This adapter runs after upstream processing, keeps the curated 1M Claude rows
|
|
6
|
+
* plus Composer 2.5 / Composer 2.5 Fast, and retains an explicit route back to
|
|
7
|
+
* Cursor's requestedModelId and parameters for every supported Pi thinking level.
|
|
8
|
+
* Composer has no Cursor effort parameter; its Pi thinking map is an explicit
|
|
9
|
+
* Max Mode switch (off = non-max, max = Max Mode).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { CursorEffortMap, CursorModelRouting, ProcessedModel } from "./processing.js";
|
|
13
|
+
import type { PiThinkingLevel } from "../types/enums.js";
|
|
14
|
+
|
|
15
|
+
const SELECTABLE_LEVELS = ["low", "medium", "high", "xhigh", "max"] as const;
|
|
16
|
+
type SelectableLevel = (typeof SELECTABLE_LEVELS)[number];
|
|
17
|
+
|
|
18
|
+
export interface AskModelSpec {
|
|
19
|
+
id: string;
|
|
20
|
+
name: string;
|
|
21
|
+
requestedModelId: string;
|
|
22
|
+
context: "1m";
|
|
23
|
+
contextWindow: number;
|
|
24
|
+
candidates: readonly string[];
|
|
25
|
+
familyCandidates: readonly string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const families = {
|
|
29
|
+
fable51: {
|
|
30
|
+
requestedModelId: "claude-fable-5-1",
|
|
31
|
+
defaultContext: "300k" as const,
|
|
32
|
+
candidates: [
|
|
33
|
+
"claude-fable-5-1-thinking",
|
|
34
|
+
"claude-fable-5.1-thinking",
|
|
35
|
+
"claude-5-1-fable-thinking",
|
|
36
|
+
],
|
|
37
|
+
// Not `claude-fable-5-1m-thinking`: that is Fable 5's 1M row.
|
|
38
|
+
oneMillionCandidates: [
|
|
39
|
+
"claude-fable-5-1-1m-thinking",
|
|
40
|
+
"claude-fable-5.1-1m-thinking",
|
|
41
|
+
"claude-5-1-fable-1m-thinking",
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
fable5: {
|
|
45
|
+
requestedModelId: "claude-fable-5",
|
|
46
|
+
defaultContext: "300k" as const,
|
|
47
|
+
candidates: ["claude-fable-5-thinking", "claude-5-fable-thinking"],
|
|
48
|
+
oneMillionCandidates: ["claude-fable-5-1m-thinking", "claude-5-fable-1m-thinking"],
|
|
49
|
+
},
|
|
50
|
+
opus5: {
|
|
51
|
+
requestedModelId: "claude-opus-5",
|
|
52
|
+
defaultContext: "300k" as const,
|
|
53
|
+
candidates: ["claude-opus-5-thinking", "claude-5-opus-thinking"],
|
|
54
|
+
oneMillionCandidates: ["claude-opus-5-1m-thinking", "claude-5-opus-1m-thinking"],
|
|
55
|
+
},
|
|
56
|
+
opus46: {
|
|
57
|
+
requestedModelId: "claude-opus-4-6",
|
|
58
|
+
defaultContext: "200k" as const,
|
|
59
|
+
candidates: [
|
|
60
|
+
"claude-opus-4-6-thinking",
|
|
61
|
+
"claude-4-6-opus-thinking",
|
|
62
|
+
"claude-4.6-opus-thinking",
|
|
63
|
+
],
|
|
64
|
+
oneMillionCandidates: [
|
|
65
|
+
"claude-opus-4-6-1m-thinking",
|
|
66
|
+
"claude-4-6-opus-1m-thinking",
|
|
67
|
+
"claude-4.6-opus-1m-thinking",
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
sonnet5: {
|
|
71
|
+
requestedModelId: "claude-sonnet-5",
|
|
72
|
+
defaultContext: "300k" as const,
|
|
73
|
+
candidates: ["claude-sonnet-5-thinking", "claude-5-sonnet-thinking"],
|
|
74
|
+
oneMillionCandidates: ["claude-sonnet-5-1m-thinking", "claude-5-sonnet-1m-thinking"],
|
|
75
|
+
},
|
|
76
|
+
} as const;
|
|
77
|
+
|
|
78
|
+
function specForFamily(options: {
|
|
79
|
+
id: string;
|
|
80
|
+
name: string;
|
|
81
|
+
requestedModelId: string;
|
|
82
|
+
defaultContext: "200k" | "300k";
|
|
83
|
+
candidates: readonly string[];
|
|
84
|
+
oneMillionCandidates: readonly string[];
|
|
85
|
+
}): AskModelSpec {
|
|
86
|
+
return {
|
|
87
|
+
id: options.id,
|
|
88
|
+
name: options.name,
|
|
89
|
+
requestedModelId: options.requestedModelId,
|
|
90
|
+
context: "1m",
|
|
91
|
+
contextWindow: 1_000_000,
|
|
92
|
+
candidates: options.oneMillionCandidates,
|
|
93
|
+
familyCandidates: [...options.candidates, ...options.oneMillionCandidates],
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export const ASK_MODEL_SPECS: readonly AskModelSpec[] = [
|
|
98
|
+
specForFamily({ id: "fable-5.1", name: "Fable 5.1", ...families.fable51 }),
|
|
99
|
+
specForFamily({ id: "fable-5", name: "Fable 5", ...families.fable5 }),
|
|
100
|
+
specForFamily({ id: "opus-5", name: "Opus 5", ...families.opus5 }),
|
|
101
|
+
specForFamily({ id: "opus-4.6", name: "Opus 4.6", ...families.opus46 }),
|
|
102
|
+
specForFamily({ id: "sonnet-5", name: "Sonnet 5", ...families.sonnet5 }),
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
function normalizedId(id: string): string {
|
|
106
|
+
return id.trim().toLowerCase();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function findSourceModel(modelsById: Map<string, ProcessedModel>, spec: AskModelSpec) {
|
|
110
|
+
for (const candidate of spec.candidates) {
|
|
111
|
+
const model = modelsById.get(normalizedId(candidate));
|
|
112
|
+
if (model) return model;
|
|
113
|
+
}
|
|
114
|
+
// Old bundled catalogs may have only one family row (not separate default
|
|
115
|
+
// and 1M rows). Reuse it for capability metadata, then rebuild the context
|
|
116
|
+
// route explicitly below.
|
|
117
|
+
for (const candidate of spec.familyCandidates) {
|
|
118
|
+
const model = modelsById.get(normalizedId(candidate));
|
|
119
|
+
if (model) return model;
|
|
120
|
+
}
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function mappedSourceEffort(source: ProcessedModel | undefined, level: SelectableLevel) {
|
|
125
|
+
if (!source?.supportsEffort || !source.effortMap) return level;
|
|
126
|
+
const mapped = source.effortMap[level];
|
|
127
|
+
return typeof mapped === "string" ? mapped : undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function sourceRouting(
|
|
131
|
+
source: ProcessedModel | undefined,
|
|
132
|
+
level: SelectableLevel,
|
|
133
|
+
mappedEffort: string,
|
|
134
|
+
): CursorModelRouting | undefined {
|
|
135
|
+
const routes = source?.rawRoutingByEffort;
|
|
136
|
+
if (!routes) return undefined;
|
|
137
|
+
return routes[level] ?? routes[mappedEffort] ?? (level === "medium" ? routes[""] : undefined);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function sourceEffortParameter(
|
|
141
|
+
route: CursorModelRouting | undefined,
|
|
142
|
+
fallback: SelectableLevel,
|
|
143
|
+
): string {
|
|
144
|
+
const parameter = route?.parameters?.find(
|
|
145
|
+
({ id }) => id.toLowerCase() === "effort" || id.toLowerCase() === "reasoning",
|
|
146
|
+
);
|
|
147
|
+
return parameter?.value || fallback;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function routeForLevel(
|
|
151
|
+
spec: AskModelSpec,
|
|
152
|
+
source: ProcessedModel | undefined,
|
|
153
|
+
level: SelectableLevel,
|
|
154
|
+
mappedEffort: string,
|
|
155
|
+
): CursorModelRouting {
|
|
156
|
+
const sourceRoute = sourceRouting(source, level, mappedEffort);
|
|
157
|
+
const passthrough =
|
|
158
|
+
sourceRoute?.parameters?.filter(
|
|
159
|
+
({ id }) =>
|
|
160
|
+
!["thinking", "context", "effort", "reasoning", "fast"].includes(id.toLowerCase()),
|
|
161
|
+
) ?? [];
|
|
162
|
+
const parameters = [
|
|
163
|
+
...passthrough,
|
|
164
|
+
{ id: "thinking", value: "true" },
|
|
165
|
+
{ id: "context", value: spec.context },
|
|
166
|
+
{ id: "effort", value: sourceEffortParameter(sourceRoute, level) },
|
|
167
|
+
{ id: "fast", value: "false" },
|
|
168
|
+
];
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
// Parameterized live metadata already carries the authoritative requested
|
|
172
|
+
// model id. Legacy raw fallback rows do not, so use the current canonical
|
|
173
|
+
// id instead of sending an obsolete effort-suffixed picker id.
|
|
174
|
+
modelId: sourceRoute?.parameters?.length ? sourceRoute.modelId : spec.requestedModelId,
|
|
175
|
+
parameters,
|
|
176
|
+
requiresMaxMode: spec.context === "1m",
|
|
177
|
+
requestedMaxMode: spec.context === "1m",
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function buildEffortRouting(spec: AskModelSpec, source: ProcessedModel | undefined) {
|
|
182
|
+
const effortMap: CursorEffortMap = {
|
|
183
|
+
off: null,
|
|
184
|
+
minimal: null,
|
|
185
|
+
low: null,
|
|
186
|
+
medium: null,
|
|
187
|
+
high: null,
|
|
188
|
+
xhigh: null,
|
|
189
|
+
max: null,
|
|
190
|
+
};
|
|
191
|
+
const routes: Record<string, CursorModelRouting> = {};
|
|
192
|
+
|
|
193
|
+
for (const level of SELECTABLE_LEVELS) {
|
|
194
|
+
const mappedEffort = mappedSourceEffort(source, level);
|
|
195
|
+
if (mappedEffort === undefined) continue;
|
|
196
|
+
effortMap[level] = level;
|
|
197
|
+
routes[level] = routeForLevel(spec, source, level, mappedEffort);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return { effortMap, routes };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function representativeRoute(routes: Record<string, CursorModelRouting>): CursorModelRouting {
|
|
204
|
+
const route = routes.medium ?? routes.high ?? routes.low ?? routes.xhigh ?? routes.max;
|
|
205
|
+
if (!route) throw new Error("Cursor Ask model has no thinking route");
|
|
206
|
+
return route;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export interface ComposerAskSpec {
|
|
210
|
+
id: string;
|
|
211
|
+
name: string;
|
|
212
|
+
requestedModelId: string;
|
|
213
|
+
fast: boolean;
|
|
214
|
+
candidates: readonly string[];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export const COMPOSER_ASK_SPECS: readonly ComposerAskSpec[] = [
|
|
218
|
+
{
|
|
219
|
+
id: "composer-2.5",
|
|
220
|
+
name: "Composer 2.5",
|
|
221
|
+
requestedModelId: "composer-2.5",
|
|
222
|
+
fast: false,
|
|
223
|
+
candidates: ["composer-2.5", "composer-2.5-max-mode"],
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
id: "composer-2.5-fast",
|
|
227
|
+
name: "Composer 2.5 Fast",
|
|
228
|
+
requestedModelId: "composer-2.5",
|
|
229
|
+
fast: true,
|
|
230
|
+
candidates: ["composer-2.5-fast", "composer-2.5-max-mode-fast"],
|
|
231
|
+
},
|
|
232
|
+
];
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Non-Claude/Composer families registered as-is from upstream processing.
|
|
236
|
+
*
|
|
237
|
+
* Unlike the Claude specs these keep whatever effort routing `processModels`
|
|
238
|
+
* derived (no forced 1M/thinking parameter rebuild), and unlike every other
|
|
239
|
+
* spec they are only emitted when the account catalog actually contains the
|
|
240
|
+
* model — never advertised from defaults alone.
|
|
241
|
+
*/
|
|
242
|
+
export interface PassthroughAskSpec {
|
|
243
|
+
id: string;
|
|
244
|
+
name: string;
|
|
245
|
+
/** Processed ids in priority order; raw-mode fallbacks (effort-suffixed) may follow. */
|
|
246
|
+
candidates: readonly string[];
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export const PASSTHROUGH_ASK_SPECS: readonly PassthroughAskSpec[] = [
|
|
250
|
+
{
|
|
251
|
+
id: "grok-4.6",
|
|
252
|
+
name: "Grok 4.6",
|
|
253
|
+
candidates: ["cursor-grok-4.6", "cursor-grok-4.6-medium"],
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
id: "grok-4.6-fast",
|
|
257
|
+
name: "Grok 4.6 Fast",
|
|
258
|
+
candidates: ["cursor-grok-4.6-fast", "cursor-grok-4.6-medium-fast"],
|
|
259
|
+
},
|
|
260
|
+
];
|
|
261
|
+
|
|
262
|
+
function buildPassthroughCatalog(processedModels: ProcessedModel[]): ProcessedModel[] {
|
|
263
|
+
const modelsById = new Map(processedModels.map((model) => [normalizedId(model.id), model]));
|
|
264
|
+
const rows: ProcessedModel[] = [];
|
|
265
|
+
for (const spec of PASSTHROUGH_ASK_SPECS) {
|
|
266
|
+
const source = spec.candidates
|
|
267
|
+
.map((candidate) => modelsById.get(normalizedId(candidate)))
|
|
268
|
+
.find((model) => model !== undefined);
|
|
269
|
+
if (!source) continue;
|
|
270
|
+
rows.push({ ...source, id: spec.id, name: spec.name });
|
|
271
|
+
}
|
|
272
|
+
return rows;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const COMPOSER_MAX_MODE_EFFORT_MAP: CursorEffortMap = {
|
|
276
|
+
off: "none",
|
|
277
|
+
minimal: null,
|
|
278
|
+
low: null,
|
|
279
|
+
medium: null,
|
|
280
|
+
high: null,
|
|
281
|
+
xhigh: null,
|
|
282
|
+
max: "max",
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
export function isComposerModel(model: ProcessedModel): boolean {
|
|
286
|
+
return /^composer(?:-|$)/i.test(model.id);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function findComposerSource(
|
|
290
|
+
modelsById: Map<string, ProcessedModel>,
|
|
291
|
+
spec: ComposerAskSpec,
|
|
292
|
+
): ProcessedModel | undefined {
|
|
293
|
+
for (const candidate of spec.candidates) {
|
|
294
|
+
const model = modelsById.get(normalizedId(candidate));
|
|
295
|
+
if (model) return model;
|
|
296
|
+
}
|
|
297
|
+
return undefined;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function composerRoute(spec: ComposerAskSpec, maxMode: boolean): CursorModelRouting {
|
|
301
|
+
return {
|
|
302
|
+
modelId: spec.requestedModelId,
|
|
303
|
+
parameters: [{ id: "fast", value: String(spec.fast) }],
|
|
304
|
+
requiresMaxMode: false,
|
|
305
|
+
requestedMaxMode: maxMode,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function buildComposerCatalog(processedModels: ProcessedModel[]): ProcessedModel[] {
|
|
310
|
+
const modelsById = new Map(processedModels.map((model) => [normalizedId(model.id), model]));
|
|
311
|
+
return COMPOSER_ASK_SPECS.map((spec) => {
|
|
312
|
+
const source = findComposerSource(modelsById, spec);
|
|
313
|
+
const routes = {
|
|
314
|
+
none: composerRoute(spec, false),
|
|
315
|
+
max: composerRoute(spec, true),
|
|
316
|
+
};
|
|
317
|
+
const representative = routes.none;
|
|
318
|
+
return {
|
|
319
|
+
...(source ?? {
|
|
320
|
+
reasoning: true,
|
|
321
|
+
maxTokens: 64_000,
|
|
322
|
+
supportsImages: false,
|
|
323
|
+
contextWindow: 200_000,
|
|
324
|
+
}),
|
|
325
|
+
id: spec.id,
|
|
326
|
+
name: spec.name,
|
|
327
|
+
reasoning: true,
|
|
328
|
+
requestedModelId: representative.modelId,
|
|
329
|
+
parameters: representative.parameters,
|
|
330
|
+
requiresMaxMode: false,
|
|
331
|
+
requestedMaxMode: false,
|
|
332
|
+
supportsEffort: true,
|
|
333
|
+
effortMap: { ...COMPOSER_MAX_MODE_EFFORT_MAP },
|
|
334
|
+
rawModelByEffort: {
|
|
335
|
+
none: representative.modelId,
|
|
336
|
+
max: representative.modelId,
|
|
337
|
+
},
|
|
338
|
+
rawRoutingByEffort: routes,
|
|
339
|
+
} satisfies ProcessedModel;
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** Return the curated 1M Claude rows followed by Composer 2.5 / Fast. */
|
|
344
|
+
export function buildAskCatalog(processedModels: ProcessedModel[]): ProcessedModel[] {
|
|
345
|
+
const modelsById = new Map(processedModels.map((model) => [normalizedId(model.id), model]));
|
|
346
|
+
const claudeModels = ASK_MODEL_SPECS.map((spec) => {
|
|
347
|
+
const source = findSourceModel(modelsById, spec);
|
|
348
|
+
const { effortMap, routes } = buildEffortRouting(spec, source);
|
|
349
|
+
const representative = representativeRoute(routes);
|
|
350
|
+
|
|
351
|
+
return {
|
|
352
|
+
...(source ?? {
|
|
353
|
+
reasoning: true,
|
|
354
|
+
maxTokens: 64_000,
|
|
355
|
+
supportsImages: true,
|
|
356
|
+
}),
|
|
357
|
+
id: spec.id,
|
|
358
|
+
name: spec.name,
|
|
359
|
+
reasoning: true,
|
|
360
|
+
contextWindow: spec.contextWindow,
|
|
361
|
+
requestedModelId: representative.modelId,
|
|
362
|
+
parameters: representative.parameters,
|
|
363
|
+
requiresMaxMode: spec.context === "1m",
|
|
364
|
+
requestedMaxMode: spec.context === "1m",
|
|
365
|
+
supportsEffort: true,
|
|
366
|
+
effortMap,
|
|
367
|
+
rawModelByEffort: Object.fromEntries(
|
|
368
|
+
Object.entries(routes).map(([level, route]) => [level, route.modelId]),
|
|
369
|
+
),
|
|
370
|
+
rawRoutingByEffort: routes,
|
|
371
|
+
} satisfies ProcessedModel;
|
|
372
|
+
});
|
|
373
|
+
return [
|
|
374
|
+
...claudeModels,
|
|
375
|
+
...buildComposerCatalog(processedModels),
|
|
376
|
+
...buildPassthroughCatalog(processedModels),
|
|
377
|
+
];
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export function supportedAskThinkingLevels(model: ProcessedModel): PiThinkingLevel[] {
|
|
381
|
+
return (Object.entries(model.effortMap ?? {}) as Array<[PiThinkingLevel, string | null]>)
|
|
382
|
+
.filter(([, effort]) => typeof effort === "string")
|
|
383
|
+
.map(([level]) => level);
|
|
384
|
+
}
|