pi-cursor-sdk 0.1.7 → 0.1.9
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 +25 -1
- package/README.md +4 -3
- package/docs/cursor-model-ux-spec.md +22 -38
- package/package.json +3 -3
- package/src/context-window-cache.ts +10 -0
- package/src/cursor-native-tool-display.ts +8 -2
- package/src/cursor-provider.ts +105 -15
- package/src/cursor-state.ts +10 -1
- package/src/cursor-tool-transcript.ts +9 -6
- package/src/model-discovery.ts +95 -22
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 0.1.9 - 2026-05-14
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Clean up recorded native Cursor tool replay outputs when abandoned replay runs are disposed, avoiding retained file or command output in process memory.
|
|
8
|
+
- Restore `/cursor-fast` state when session persistence fails during command handling.
|
|
9
|
+
- Preserve distinct same-payload Cursor tool completions while deduplicating duplicate SDK completion surfaces.
|
|
10
|
+
- Respect exact `model@context` context-window cache overrides before falling back to parsed base-model context values.
|
|
11
|
+
- Emit native replay text block endings with saved content indexes instead of searching by object identity.
|
|
12
|
+
- Redact discovery failure details with the same secret patterns used for stream errors.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- Update fallback Sonnet 4.6 context variants from `300k` to the current `200k` catalog variant.
|
|
17
|
+
- Skip ambiguous Cursor SDK aliases shared by multiple base models or colliding with base model IDs, preventing misleading pi model rows.
|
|
18
|
+
- Reduce context-window cache reloads during model catalog registration.
|
|
19
|
+
- Document image carry-forward as a product decision rather than silently changing current latest-user-message image forwarding behavior.
|
|
20
|
+
|
|
21
|
+
## 0.1.8 - 2026-05-14
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- Update the verified dependency baseline to `@cursor/sdk` 1.0.13 and Vitest 4.1.6.
|
|
26
|
+
- Register latest-style Cursor SDK model aliases returned by `Cursor.models.list()` as pi-selectable Cursor model IDs, including context-qualified alias variants where applicable.
|
|
27
|
+
- Clarify Max Mode behavior against current Cursor SDK docs: Cursor may enable required Max Mode automatically, but the extension still only advertises catalog-exposed context variants.
|
|
4
28
|
|
|
5
29
|
## 0.1.7 - 2026-05-10
|
|
6
30
|
|
package/README.md
CHANGED
|
@@ -137,6 +137,7 @@ How to read model IDs:
|
|
|
137
137
|
- `cursor/...` is the Cursor provider registered by this extension
|
|
138
138
|
- `@1m`, `@272k`, and `@300k` are context-window variants
|
|
139
139
|
- `:medium`, `:high`, and `:xhigh` are pi thinking-level suffixes for models where the Cursor SDK exposes a pi-controllable thinking parameter
|
|
140
|
+
- unambiguous latest-style Cursor aliases returned by `Cursor.models.list()` are registered too, using the same context suffixes when the target model has context variants; aliases shared by multiple base models or colliding with a base model ID are skipped because their SDK resolution and displayed metadata can diverge
|
|
140
141
|
|
|
141
142
|
Examples with pi thinking controls:
|
|
142
143
|
|
|
@@ -146,7 +147,7 @@ pi --model cursor/gpt-5.5@272k:xhigh
|
|
|
146
147
|
pi --model cursor/gpt-5.5@1m --thinking medium
|
|
147
148
|
```
|
|
148
149
|
|
|
149
|
-
Cursor-only parameters are not encoded into pi model IDs. Cursor `context` becomes a pi-visible model variant because it changes pi's native `contextWindow`; Cursor `fast` is extension state, not model identity.
|
|
150
|
+
Cursor-only parameters are not encoded into pi model IDs. Cursor `context` becomes a pi-visible model variant because it changes pi's native `contextWindow`; Cursor `fast` is extension state, not model identity. Alias model IDs still share Cursor-only state, such as fast defaults, with their underlying Cursor base model.
|
|
150
151
|
|
|
151
152
|
## Thinking support
|
|
152
153
|
|
|
@@ -202,7 +203,7 @@ If no key is available from `/login`, `CURSOR_API_KEY`, or `--api-key`, model di
|
|
|
202
203
|
|
|
203
204
|
- `composer-2`
|
|
204
205
|
- `gpt-5.5@1m`, `gpt-5.5@272k`
|
|
205
|
-
- `claude-sonnet-4-6@1m`, `claude-sonnet-4-6@
|
|
206
|
+
- `claude-sonnet-4-6@1m`, `claude-sonnet-4-6@200k`
|
|
206
207
|
- `claude-opus-4-7@1m`, `claude-opus-4-7@300k`
|
|
207
208
|
|
|
208
209
|
Fallback models are a conservative startup model list. Actual Cursor runs still need a key from `/login`, `CURSOR_API_KEY`, or `--api-key`. If you add auth after startup, run `/reload` or restart pi to refresh the full live Cursor model catalog.
|
|
@@ -214,7 +215,7 @@ Fallback models are a conservative startup model list. Actual Cursor runs still
|
|
|
214
215
|
- **Pi tool schemas are not passed through to Cursor.** This extension is a Cursor provider, not a bridge that forwards pi's tool system into Cursor.
|
|
215
216
|
- **One fresh Cursor agent is created per provider call.** Cursor agent state is not reused between pi provider calls.
|
|
216
217
|
- **Ambient Cursor setting/rule layers are not loaded by default.** The current Cursor SDK writes setting/rule loading logs directly to terminal output, which corrupts pi's TUI, so the extension leaves those layers out.
|
|
217
|
-
- **Max Mode is not
|
|
218
|
+
- **Max Mode is not a manual pi variant.** Cursor's SDK may enable Max Mode automatically for models that require it. This extension only advertises exact context-window variants that the SDK catalog exposes and otherwise uses conservative SDK-derived default/non-Max context windows.
|
|
218
219
|
- **Output token limits are conservative.** Cursor SDK model metadata does not currently expose output token limits directly.
|
|
219
220
|
- **Token usage is approximate in pi.** Cursor SDK usage events include internal agent/tool/cache work, so the extension reports an approximate replayable pi prompt/output size for context display and compaction decisions.
|
|
220
221
|
|
|
@@ -13,6 +13,7 @@ Current implementation notes:
|
|
|
13
13
|
- Cursor `fast` is extension state, not model identity.
|
|
14
14
|
- Cursor fast status uses `ctx.ui.setStatus()`; the default pi footer remains intact.
|
|
15
15
|
- Installed `@cursor/sdk` user messages accept images, and Cursor models are treated as image-capable; registered input metadata is `text` plus `image`.
|
|
16
|
+
- Product decision pending: image payload forwarding currently sends images only from the latest user message. If the latest user turn is plain text after an earlier image turn, the transcript keeps an `[image omitted from transcript]` placeholder but no image bytes are sent to Cursor. Changing this to carry images forward across turns requires a deliberate product decision about token cost, privacy, stale visual context, and expected multimodal follow-up behavior.
|
|
16
17
|
- `@cursor/sdk` is a package dependency of this extension; users should not need a global SDK install.
|
|
17
18
|
- Cursor auth uses pi-native API-key resolution for provider `cursor`: CLI `--api-key`, stored `~/.pi/agent/auth.json` API key from `/login`, then `CURSOR_API_KEY`. The extension config file stores only non-secret Cursor-only state such as fast defaults.
|
|
18
19
|
- Local agents do not pass `settingSources` by default because the current Cursor SDK writes setting/rule loading INFO logs directly to terminal output, which corrupts pi's TUI.
|
|
@@ -20,7 +21,8 @@ Current implementation notes:
|
|
|
20
21
|
- Cursor-side thinking remains visible. Cursor internal tool activity is recorded from SDK events and scrubbed. In interactive TTY sessions, supported completed `read`, `bash`, and `ls` activity is replayed through pi's native tool-call rendering path with recorded Cursor results, so the TUI can show native green cards without forcing Cursor to call pi tools or rerunning Cursor's reads/shell commands. Native replay wrappers are registered only for tool names not already owned by another extension; conflicting tools use the bounded scrubbed transcript fallback. `PI_CURSOR_NATIVE_TOOL_DISPLAY=0` disables native replay, and `PI_CURSOR_REGISTER_NATIVE_TOOLS=0` is a registration-only opt-out that keeps the transcript fallback without shadowing pi tool names. When these native cards are emitted, the provider mirrors Codex's turn shape as Cursor SDK completions arrive: assistant `toolUse`, pi `toolResult`s, live post-tool Cursor thinking/text, any later Cursor tool batches as further `toolUse` turns, then Cursor's final assistant answer. Non-interactive runs keep bounded scrubbed transcript output instead, preserving `pi -p` assistant text output. Cursor text deltas stream live when native tool replay is not active.
|
|
21
22
|
- Cursor SDK usage events report cumulative internal agent/tool/cache work, not the replayable pi prompt context. The extension reports approximate prompt/output usage for pi context display and compaction decisions instead of copying raw Cursor SDK usage. When native replay splits one Cursor SDK run into multiple pi turns, prompt input is counted once for the run; later synthetic replay turns report `input: 0` and only their own output estimate.
|
|
22
23
|
- For models without a catalog `context` parameter, context windows are not hardcoded. The extension ships a bundled SDK-derived default/non-Max cache generated from `createAgentPlatform().checkpointStore.loadLatest(agentId).tokenDetails.maxTokens`. Successful runs can update a local override cache, but model discovery does not probe models at startup.
|
|
23
|
-
- Max Mode context windows are distinct from default/non-Max context windows. `@cursor/sdk` 1.0.
|
|
24
|
+
- Max Mode context windows are distinct from default/non-Max context windows. `@cursor/sdk` 1.0.13 documentation says the SDK may enable Max Mode automatically when a selected model requires it, but the public local-agent `ModelSelection` path still does not expose a manual Max Mode selector. Do not advertise Max Mode context windows unless the SDK catalog exposes an exact parameter/variant or the SDK public API adds a Max Mode selector that the extension actually sends.
|
|
25
|
+
- `@cursor/sdk` 1.0.13 adds latest-style `ModelListItem.aliases`. The extension registers only unambiguous aliases as pi model IDs (with the same context suffixes when applicable) and sends the alias back in `ModelSelection.id`, while sharing Cursor-only state such as fast defaults with the underlying catalog `id`. Aliases shared by multiple base models, such as generic family aliases, are skipped because the pi row metadata would otherwise imply one base model while Cursor may resolve the alias to another.
|
|
24
26
|
|
|
25
27
|
## Goal
|
|
26
28
|
|
|
@@ -69,6 +71,7 @@ Users can persist the stored key through `/login` -> `Use an API key` -> `Cursor
|
|
|
69
71
|
For each model, use:
|
|
70
72
|
|
|
71
73
|
- `model.id`
|
|
74
|
+
- `model.aliases`
|
|
72
75
|
- `model.displayName`
|
|
73
76
|
- `model.parameters`
|
|
74
77
|
- `model.variants`
|
|
@@ -135,8 +138,9 @@ Register a `cursor` provider with `pi.registerProvider()`.
|
|
|
135
138
|
|
|
136
139
|
Rules:
|
|
137
140
|
|
|
138
|
-
- Register one pi model for each Cursor base model when there is no Cursor `context` parameter.
|
|
139
|
-
- Register one pi model per Cursor `context` value when the model exposes a `context` parameter.
|
|
141
|
+
- Register one pi model for each Cursor base model and each unambiguous SDK alias when there is no Cursor `context` parameter.
|
|
142
|
+
- Register one pi model per Cursor `context` value for each Cursor base model and each unambiguous SDK alias when the model exposes a `context` parameter.
|
|
143
|
+
- Skip SDK aliases that collide with another base model ID or are shared by multiple base models; those aliases can resolve differently from the pi row metadata.
|
|
140
144
|
- Do not encode `reasoning`, `effort`, `thinking`, or `fast` into pi model IDs.
|
|
141
145
|
- Prefer stable, readable `@<context>` suffixes that do not conflict with pi's final `:<thinking>` suffix parser.
|
|
142
146
|
- Sort Cursor models by base ID, then context value in Cursor SDK order before calling `pi.registerProvider()`. Registration order matters for `/model` display and model cycling; `--list-models` sorts output separately.
|
|
@@ -177,7 +181,7 @@ Reason:
|
|
|
177
181
|
|
|
178
182
|
Each registered model must set:
|
|
179
183
|
|
|
180
|
-
- `id`: context-qualified pi model ID when needed.
|
|
184
|
+
- `id`: context-qualified pi model ID when needed. For SDK aliases, this uses the alias as the pi-visible ID and the alias is sent back to Cursor as `ModelSelection.id`.
|
|
181
185
|
- `name`: human-readable Cursor display name plus context when useful.
|
|
182
186
|
- `reasoning`: `true` only if a Cursor `reasoning`, `effort`, or `thinking` parameter can map to pi thinking. This controls pi's thinking UI and `pi --list-models` `thinking` column; it must not be used to claim whether the Cursor model can think internally. Cursor SDK models are thinking-capable even when this is `false`.
|
|
183
187
|
- `thinkingLevelMap`: model-specific pi-to-Cursor mapping for pi UI, clamping, persistence, and footer display.
|
|
@@ -186,7 +190,7 @@ Each registered model must set:
|
|
|
186
190
|
- `input`: supported input types. The installed Cursor SDK accepts `SDKUserMessage.images`, and Cursor models are expected to support image input, so advertise `["text", "image"]`.
|
|
187
191
|
- `cost`: zeroed unless reliable Cursor costs are available.
|
|
188
192
|
|
|
189
|
-
The extension stores runtime metadata in an internal map keyed by registered pi model ID. That map records the Cursor base model ID, selected context param, default params, and discovered capabilities. `ProviderModelConfig` has no dedicated metadata field, so do not rely on hidden custom fields for this state.
|
|
193
|
+
The extension stores runtime metadata in an internal map keyed by registered pi model ID. That map records the Cursor base catalog model ID, the Cursor selection model ID (base ID or alias), selected context param, default params, and discovered capabilities. `ProviderModelConfig` has no dedicated metadata field, so do not rely on hidden custom fields for this state.
|
|
190
194
|
|
|
191
195
|
## Dynamic Capabilities
|
|
192
196
|
|
|
@@ -484,42 +488,22 @@ Fast flag example:
|
|
|
484
488
|
pi --model cursor/gpt-5.5@1m --cursor-fast -p "Say ok only"
|
|
485
489
|
```
|
|
486
490
|
|
|
487
|
-
##
|
|
491
|
+
## Discovered Model Capability Examples
|
|
488
492
|
|
|
489
|
-
|
|
493
|
+
These examples document the capability shapes the extension handles, not an exhaustive live catalog. The exact Cursor catalog changes over time; use `pi -e . --list-models cursor` or `Cursor.models.list()` for the current model surface. When the SDK reports aliases, only unambiguous aliases are registered; shared generic aliases are skipped.
|
|
490
494
|
|
|
491
|
-
|
|
|
495
|
+
| Example model shape | Cursor controls | Pi representation |
|
|
492
496
|
|---|---|---|
|
|
493
|
-
| `default` | none | plain model |
|
|
494
|
-
| `composer-2
|
|
495
|
-
|
|
|
496
|
-
|
|
|
497
|
-
|
|
|
498
|
-
|
|
|
499
|
-
|
|
|
500
|
-
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
| `gpt-5.2-codex` | reasoning, fast | plain model + native thinking + fast state |
|
|
504
|
-
| `gpt-5.1-codex-max` | reasoning, fast | plain model + native thinking + fast state |
|
|
505
|
-
| `gpt-5.1-codex-mini` | reasoning | plain model + native thinking |
|
|
506
|
-
| `gpt-5.1` | reasoning | plain model + native thinking |
|
|
507
|
-
| `claude-opus-4-7` | thinking, context, effort | context variants + native thinking |
|
|
508
|
-
| `claude-opus-4-6` | thinking, context, effort, fast | context variants + native thinking + fast state |
|
|
509
|
-
| `claude-opus-4-5` | thinking | plain model + native thinking |
|
|
510
|
-
| `claude-sonnet-4-6` | thinking, context, effort | context variants + native thinking |
|
|
511
|
-
| `claude-sonnet-4-5` | thinking, context | context-qualified model + native thinking |
|
|
512
|
-
| `claude-sonnet-4` | thinking, context | context-qualified model + native thinking |
|
|
513
|
-
| `claude-haiku-4-5` | thinking | plain model + native thinking |
|
|
514
|
-
| `grok-4.3` | context | context variants |
|
|
515
|
-
| `grok-4-20` | thinking | plain model + native thinking |
|
|
516
|
-
| `gemini-3.1-pro` | none | plain model |
|
|
517
|
-
| `gemini-3-flash` | none | plain model |
|
|
518
|
-
| `gemini-2.5-flash` | none | plain model |
|
|
519
|
-
| `gpt-5-mini` | none | plain model |
|
|
520
|
-
| `kimi-k2.5` | none | plain model |
|
|
521
|
-
|
|
522
|
-
If Cursor later adds `fast`, `context`, `reasoning`, or `effort` to a model, the extension picks it up dynamically.
|
|
497
|
+
| plain model, such as `default` or models with no exposed controls | none | plain model |
|
|
498
|
+
| `composer-2`-style model | fast | plain model + fast extension state |
|
|
499
|
+
| GPT-style reasoning model with context variants | context, reasoning, fast when exposed | context variants + native thinking + optional fast state |
|
|
500
|
+
| Claude-style thinking model with context variants | thinking, context, effort when exposed | context variants + native thinking + optional fast state |
|
|
501
|
+
| Claude-style thinking model without context variants | thinking and/or effort | plain model + native thinking |
|
|
502
|
+
| context-only model | context | context variants |
|
|
503
|
+
| unique latest alias for any shape | aliases | same pi rows as the base model shape, using the alias as `ModelSelection.id` |
|
|
504
|
+
| shared generic alias across multiple base models | aliases | skipped to avoid misleading pi rows |
|
|
505
|
+
|
|
506
|
+
If Cursor later adds `fast`, `context`, `reasoning`, `effort`, or aliases to a model, the extension picks up unambiguous capability changes dynamically.
|
|
523
507
|
|
|
524
508
|
## Detailed Examples
|
|
525
509
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-cursor-sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "pi provider extension backed by @cursor/sdk local agents",
|
|
5
5
|
"author": "Mitch Fultz (https://github.com/fitchmultz)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"test:watch": "vitest"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@cursor/sdk": "^1.0.
|
|
41
|
+
"@cursor/sdk": "^1.0.13"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
44
|
"@earendil-works/pi-ai": "*",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"@earendil-works/pi-ai": "^0.74.0",
|
|
49
49
|
"@earendil-works/pi-coding-agent": "^0.74.0",
|
|
50
50
|
"typescript": "^6.0.3",
|
|
51
|
-
"vitest": "^4.1.
|
|
51
|
+
"vitest": "^4.1.6"
|
|
52
52
|
},
|
|
53
53
|
"pi": {
|
|
54
54
|
"extensions": [
|
|
@@ -4,6 +4,7 @@ import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
|
4
4
|
import { BUNDLED_CONTEXT_WINDOWS } from "./bundled-context-windows.js";
|
|
5
5
|
|
|
6
6
|
const CONTEXT_WINDOW_CACHE_FILE = "cursor-sdk-context-windows.json";
|
|
7
|
+
let userContextWindowOverrideLoadCount = 0;
|
|
7
8
|
|
|
8
9
|
interface ContextWindowCacheFile {
|
|
9
10
|
contextWindows?: Record<string, number>;
|
|
@@ -18,6 +19,7 @@ function isPositiveInteger(value: unknown): value is number {
|
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
function loadUserContextWindowOverrides(): Map<string, number> {
|
|
22
|
+
userContextWindowOverrideLoadCount += 1;
|
|
21
23
|
const path = getCachePath();
|
|
22
24
|
const overrides = new Map<string, number>();
|
|
23
25
|
if (!existsSync(path)) return overrides;
|
|
@@ -40,6 +42,10 @@ export function loadContextWindowCache(): Map<string, number> {
|
|
|
40
42
|
return cache;
|
|
41
43
|
}
|
|
42
44
|
|
|
45
|
+
export function getCachedContextWindowExact(modelId: string): number | undefined {
|
|
46
|
+
return loadContextWindowCache().get(modelId);
|
|
47
|
+
}
|
|
48
|
+
|
|
43
49
|
export function getCachedContextWindow(modelId: string): number | undefined {
|
|
44
50
|
const cache = loadContextWindowCache();
|
|
45
51
|
return cache.get(modelId) ?? cache.get("default");
|
|
@@ -76,4 +82,8 @@ export function saveCachedContextWindow(modelId: string, contextWindow: number):
|
|
|
76
82
|
|
|
77
83
|
export const __testUtils = {
|
|
78
84
|
getCachePath,
|
|
85
|
+
getUserContextWindowOverrideLoadCount: () => userContextWindowOverrideLoadCount,
|
|
86
|
+
resetUserContextWindowOverrideLoadCount: () => {
|
|
87
|
+
userContextWindowOverrideLoadCount = 0;
|
|
88
|
+
},
|
|
79
89
|
};
|
|
@@ -56,9 +56,14 @@ export function canRenderCursorToolNatively(toolName: string): boolean {
|
|
|
56
56
|
return isNativeCursorToolName(toolName) && registeredNativeToolNames.has(toolName);
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
export function recordCursorNativeToolDisplay(item: CursorNativeToolDisplayItem):
|
|
60
|
-
if (!canRenderCursorToolNatively(item.toolName)) return;
|
|
59
|
+
export function recordCursorNativeToolDisplay(item: CursorNativeToolDisplayItem): boolean {
|
|
60
|
+
if (!canRenderCursorToolNatively(item.toolName)) return false;
|
|
61
61
|
nativeToolResults.set(item.id, item);
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function deleteCursorNativeToolDisplay(id: string): void {
|
|
66
|
+
nativeToolResults.delete(id);
|
|
62
67
|
}
|
|
63
68
|
|
|
64
69
|
function consumeCursorNativeToolDisplay(id: string): CursorNativeToolDisplayItem | undefined {
|
|
@@ -68,6 +73,7 @@ function consumeCursorNativeToolDisplay(id: string): CursorNativeToolDisplayItem
|
|
|
68
73
|
}
|
|
69
74
|
|
|
70
75
|
export const __testUtils = {
|
|
76
|
+
nativeToolResultCount: () => nativeToolResults.size,
|
|
71
77
|
reset(): void {
|
|
72
78
|
registeredNativeToolNames.clear();
|
|
73
79
|
nativeToolResults.clear();
|
package/src/cursor-provider.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { buildCursorPiToolDisplay, formatCursorToolTranscript, mergeCursorToolCa
|
|
|
17
17
|
import {
|
|
18
18
|
canRenderCursorToolNatively,
|
|
19
19
|
isCursorNativeToolDisplayRuntimeEnabled,
|
|
20
|
+
deleteCursorNativeToolDisplay,
|
|
20
21
|
recordCursorNativeToolDisplay,
|
|
21
22
|
type CursorNativeToolDisplayItem,
|
|
22
23
|
} from "./cursor-native-tool-display.js";
|
|
@@ -58,6 +59,7 @@ const AUTH_CURSOR_SDK_ERROR_MESSAGE =
|
|
|
58
59
|
const APPROX_CHARS_PER_TOKEN = 4;
|
|
59
60
|
const IMAGE_TOKEN_ESTIMATE = 1200;
|
|
60
61
|
const CURSOR_ACTIVITY_TRACE_MAX_CHARS = 50000;
|
|
62
|
+
const DEFAULT_CURSOR_NATIVE_REPLAY_IDLE_DISPOSE_MS = 5 * 60 * 1000;
|
|
61
63
|
const CURSOR_NATIVE_REPLAY_TOOL_ID_PATTERN = /^(cursor-replay-\d+-\d+)-tool-\d+$/;
|
|
62
64
|
|
|
63
65
|
type CursorNativeQueuedEvent =
|
|
@@ -73,10 +75,13 @@ interface CursorNativeLiveRun {
|
|
|
73
75
|
promptInputTokensReported: boolean;
|
|
74
76
|
pendingEvents: CursorNativeQueuedEvent[];
|
|
75
77
|
textDeltas: string[];
|
|
78
|
+
recordedToolDisplayIds: string[];
|
|
76
79
|
finalText?: string;
|
|
77
80
|
done: boolean;
|
|
78
81
|
cancelled: boolean;
|
|
82
|
+
disposed: boolean;
|
|
79
83
|
errorMessage?: string;
|
|
84
|
+
idleDisposeTimer?: ReturnType<typeof setTimeout>;
|
|
80
85
|
waiters: Set<() => void>;
|
|
81
86
|
}
|
|
82
87
|
|
|
@@ -88,6 +93,7 @@ interface CursorNativeTurnState {
|
|
|
88
93
|
}
|
|
89
94
|
|
|
90
95
|
let cursorNativeReplayCounter = 0;
|
|
96
|
+
let cursorNativeReplayIdleDisposeMs = DEFAULT_CURSOR_NATIVE_REPLAY_IDLE_DISPOSE_MS;
|
|
91
97
|
const pendingCursorNativeRuns = new Map<string, CursorNativeLiveRun>();
|
|
92
98
|
|
|
93
99
|
function escapeRegExp(value: string): string {
|
|
@@ -264,6 +270,21 @@ function queueCursorNativeEvent(run: CursorNativeLiveRun, event: CursorNativeQue
|
|
|
264
270
|
notifyCursorNativeRun(run);
|
|
265
271
|
}
|
|
266
272
|
|
|
273
|
+
function clearCursorNativeRunIdleDispose(run: CursorNativeLiveRun): void {
|
|
274
|
+
if (!run.idleDisposeTimer) return;
|
|
275
|
+
clearTimeout(run.idleDisposeTimer);
|
|
276
|
+
run.idleDisposeTimer = undefined;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function scheduleCursorNativeRunIdleDispose(run: CursorNativeLiveRun): void {
|
|
280
|
+
if (run.disposed) return;
|
|
281
|
+
clearCursorNativeRunIdleDispose(run);
|
|
282
|
+
run.idleDisposeTimer = setTimeout(() => {
|
|
283
|
+
void disposeCursorNativeRun(run);
|
|
284
|
+
}, cursorNativeReplayIdleDisposeMs);
|
|
285
|
+
run.idleDisposeTimer.unref?.();
|
|
286
|
+
}
|
|
287
|
+
|
|
267
288
|
function isCursorNativeRunReady(run: CursorNativeLiveRun): boolean {
|
|
268
289
|
return run.pendingEvents.length > 0 || run.done || run.cancelled || run.errorMessage !== undefined;
|
|
269
290
|
}
|
|
@@ -310,12 +331,13 @@ function closeCursorNativeThinkingBlock(turn: CursorNativeTurnState): void {
|
|
|
310
331
|
|
|
311
332
|
function closeCursorNativeTextBlock(turn: CursorNativeTurnState): string {
|
|
312
333
|
if (turn.textContentIndex < 0) return "";
|
|
313
|
-
const
|
|
334
|
+
const contentIndex = turn.textContentIndex;
|
|
335
|
+
const block = turn.partial.content[contentIndex];
|
|
314
336
|
turn.textContentIndex = -1;
|
|
315
337
|
if (block.type !== "text") return "";
|
|
316
338
|
turn.stream.push({
|
|
317
339
|
type: "text_end",
|
|
318
|
-
contentIndex
|
|
340
|
+
contentIndex,
|
|
319
341
|
content: block.text,
|
|
320
342
|
partial: turn.partial,
|
|
321
343
|
});
|
|
@@ -402,15 +424,24 @@ function emitCursorNativeToolUseTurn(
|
|
|
402
424
|
stream.push({ type: "toolcall_delta", contentIndex, delta: JSON.stringify(tool.args), partial });
|
|
403
425
|
const block = partial.content[contentIndex];
|
|
404
426
|
if (block.type === "toolCall") stream.push({ type: "toolcall_end", contentIndex, toolCall: block, partial });
|
|
405
|
-
recordCursorNativeToolDisplay({ ...tool, terminate: shouldTerminate })
|
|
427
|
+
if (recordCursorNativeToolDisplay({ ...tool, terminate: shouldTerminate })) {
|
|
428
|
+
run.recordedToolDisplayIds.push(tool.id);
|
|
429
|
+
}
|
|
406
430
|
}
|
|
407
431
|
setApproximateUsage(partial, takeCursorNativePromptInputTokens(run), outputText);
|
|
408
432
|
partial.stopReason = "toolUse";
|
|
409
433
|
stream.push({ type: "done", reason: "toolUse", message: partial });
|
|
434
|
+
scheduleCursorNativeRunIdleDispose(run);
|
|
410
435
|
}
|
|
411
436
|
|
|
412
437
|
async function disposeCursorNativeRun(run: CursorNativeLiveRun): Promise<void> {
|
|
438
|
+
if (run.disposed) return;
|
|
439
|
+
run.disposed = true;
|
|
413
440
|
pendingCursorNativeRuns.delete(run.id);
|
|
441
|
+
clearCursorNativeRunIdleDispose(run);
|
|
442
|
+
for (const toolDisplayId of run.recordedToolDisplayIds) deleteCursorNativeToolDisplay(toolDisplayId);
|
|
443
|
+
run.recordedToolDisplayIds = [];
|
|
444
|
+
run.waiters.clear();
|
|
414
445
|
try {
|
|
415
446
|
await run.agent[Symbol.asyncDispose]();
|
|
416
447
|
} catch {
|
|
@@ -484,7 +515,13 @@ async function replayPendingCursorNativeRun(
|
|
|
484
515
|
if (!replayId) return false;
|
|
485
516
|
const run = pendingCursorNativeRuns.get(replayId);
|
|
486
517
|
if (!run) return false;
|
|
487
|
-
|
|
518
|
+
clearCursorNativeRunIdleDispose(run);
|
|
519
|
+
try {
|
|
520
|
+
await emitCursorNativeRunNextTurn(stream, partial, run, signal);
|
|
521
|
+
} catch (error) {
|
|
522
|
+
if (error instanceof CursorAbortError) await disposeCursorNativeRun(run);
|
|
523
|
+
throw error;
|
|
524
|
+
}
|
|
488
525
|
return true;
|
|
489
526
|
}
|
|
490
527
|
|
|
@@ -498,6 +535,7 @@ export function streamCursor(
|
|
|
498
535
|
(async () => {
|
|
499
536
|
const partial = makeInitialMessage(model);
|
|
500
537
|
let agent: SDKAgent | null = null;
|
|
538
|
+
let activeNativeRun: CursorNativeLiveRun | undefined;
|
|
501
539
|
let resolvedApiKey: string | undefined;
|
|
502
540
|
let abortSignal: AbortSignal | undefined;
|
|
503
541
|
let abortListener: (() => void) | undefined;
|
|
@@ -551,14 +589,21 @@ export function streamCursor(
|
|
|
551
589
|
promptInputTokensReported: false,
|
|
552
590
|
pendingEvents: [],
|
|
553
591
|
textDeltas,
|
|
592
|
+
recordedToolDisplayIds: [],
|
|
554
593
|
done: false,
|
|
555
594
|
cancelled: false,
|
|
595
|
+
disposed: false,
|
|
556
596
|
waiters: new Set(),
|
|
557
597
|
}
|
|
558
598
|
: undefined;
|
|
559
|
-
if (liveRun)
|
|
599
|
+
if (liveRun) {
|
|
600
|
+
pendingCursorNativeRuns.set(liveRun.id, liveRun);
|
|
601
|
+
activeNativeRun = liveRun;
|
|
602
|
+
}
|
|
560
603
|
const startedToolCalls = new Map<string, unknown>();
|
|
561
|
-
const
|
|
604
|
+
const completedToolIdentities = new Set<string>();
|
|
605
|
+
const completedStartedToolFingerprints = new Set<string>();
|
|
606
|
+
const completedFallbackToolFingerprints = new Set<string>();
|
|
562
607
|
|
|
563
608
|
const appendLiveTextDelta = (text: string): void => {
|
|
564
609
|
if (textContentIndex < 0) {
|
|
@@ -652,12 +697,25 @@ export function streamCursor(
|
|
|
652
697
|
}
|
|
653
698
|
};
|
|
654
699
|
|
|
655
|
-
const handleCompletedToolCall = (
|
|
700
|
+
const handleCompletedToolCall = (
|
|
701
|
+
toolCall: unknown,
|
|
702
|
+
options: { identity?: string; source?: "started" | "fallback" } = {},
|
|
703
|
+
): void => {
|
|
656
704
|
const transcript = scrubSensitiveText(formatCursorToolTranscript(toolCall, { cwd }), resolvedApiKey);
|
|
657
705
|
const display = buildCursorPiToolDisplay(toolCall, { cwd });
|
|
658
706
|
const fingerprint = getToolFingerprint({ toolName: display.toolName, args: display.args, result: display.result });
|
|
659
|
-
if (
|
|
660
|
-
|
|
707
|
+
if (options.identity && completedToolIdentities.has(options.identity)) return;
|
|
708
|
+
if (options.source === "started") {
|
|
709
|
+
if (completedFallbackToolFingerprints.has(fingerprint)) return;
|
|
710
|
+
} else if (completedStartedToolFingerprints.has(fingerprint) || completedFallbackToolFingerprints.has(fingerprint)) {
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
if (options.identity) completedToolIdentities.add(options.identity);
|
|
714
|
+
if (options.source === "started") {
|
|
715
|
+
completedStartedToolFingerprints.add(fingerprint);
|
|
716
|
+
} else {
|
|
717
|
+
completedFallbackToolFingerprints.add(fingerprint);
|
|
718
|
+
}
|
|
661
719
|
|
|
662
720
|
if (useNativeToolReplay && canRenderCursorToolNatively(display.toolName) && liveRun) {
|
|
663
721
|
nativeToolReplayStarted = true;
|
|
@@ -704,7 +762,11 @@ export function streamCursor(
|
|
|
704
762
|
} else if (update.type === "tool-call-completed") {
|
|
705
763
|
const mergedToolCall = mergeCursorToolCalls(startedToolCalls.get(update.callId), update.toolCall);
|
|
706
764
|
startedToolCalls.delete(update.callId);
|
|
707
|
-
|
|
765
|
+
const identity = typeof update.callId === "string" ? `cursor-tool:${update.callId}` : undefined;
|
|
766
|
+
handleCompletedToolCall(mergedToolCall, {
|
|
767
|
+
identity,
|
|
768
|
+
source: identity ? "started" : "fallback",
|
|
769
|
+
});
|
|
708
770
|
} else if (update.type === "summary") {
|
|
709
771
|
const summary = `Cursor summary: ${truncateSingleLine(update.summary)}\n`;
|
|
710
772
|
if (liveRun && nativeToolReplayStarted) {
|
|
@@ -723,7 +785,12 @@ export function streamCursor(
|
|
|
723
785
|
const step = getObjectField(args.step, "message") ? args.step : undefined;
|
|
724
786
|
if (getObjectField(args.step, "type") !== "toolCall") return;
|
|
725
787
|
const toolCall = getObjectField(step, "message");
|
|
726
|
-
|
|
788
|
+
const stepId = getObjectField(args.step, "id") ?? getObjectField(toolCall, "id") ?? getObjectField(toolCall, "callId");
|
|
789
|
+
if (toolCall) {
|
|
790
|
+
handleCompletedToolCall(toolCall, {
|
|
791
|
+
identity: typeof stepId === "string" ? `cursor-tool:${stepId}` : undefined,
|
|
792
|
+
});
|
|
793
|
+
}
|
|
727
794
|
};
|
|
728
795
|
|
|
729
796
|
// Handle abort signal
|
|
@@ -750,21 +817,31 @@ export function streamCursor(
|
|
|
750
817
|
void run
|
|
751
818
|
.wait()
|
|
752
819
|
.then(async (result) => {
|
|
820
|
+
if (liveRun.disposed) return;
|
|
753
821
|
await cacheSdkContextWindow(liveRun.agent.agentId, model.id);
|
|
822
|
+
if (liveRun.disposed) return;
|
|
754
823
|
liveRun.cancelled = result.status === "cancelled";
|
|
755
824
|
liveRun.finalText = hasUsableText(result.result) ? result.result : liveRun.textDeltas.join("");
|
|
756
825
|
liveRun.done = true;
|
|
757
826
|
notifyCursorNativeRun(liveRun);
|
|
827
|
+
scheduleCursorNativeRunIdleDispose(liveRun);
|
|
758
828
|
})
|
|
759
829
|
.catch((error: unknown) => {
|
|
830
|
+
if (liveRun.disposed) return;
|
|
760
831
|
liveRun.errorMessage = sanitizeError(error, resolvedApiKey ?? options?.apiKey);
|
|
761
832
|
notifyCursorNativeRun(liveRun);
|
|
833
|
+
scheduleCursorNativeRunIdleDispose(liveRun);
|
|
762
834
|
});
|
|
763
835
|
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
836
|
+
try {
|
|
837
|
+
await waitForCursorNativeRunProgress(liveRun, options?.signal);
|
|
838
|
+
await settleCursorNativeToolBatch(liveRun);
|
|
839
|
+
closeTraceBlock();
|
|
840
|
+
await emitCursorNativeRunNextTurn(stream, partial, liveRun, options?.signal);
|
|
841
|
+
} catch (error) {
|
|
842
|
+
if (error instanceof CursorAbortError) await disposeCursorNativeRun(liveRun);
|
|
843
|
+
throw error;
|
|
844
|
+
}
|
|
768
845
|
agent = null;
|
|
769
846
|
return;
|
|
770
847
|
}
|
|
@@ -794,6 +871,8 @@ export function streamCursor(
|
|
|
794
871
|
stream.push({ type: "error", reason: "error", error: partial });
|
|
795
872
|
}
|
|
796
873
|
} finally {
|
|
874
|
+
if (activeNativeRun?.disposed) agent = null;
|
|
875
|
+
|
|
797
876
|
if (abortSignal && abortListener) {
|
|
798
877
|
abortSignal.removeEventListener("abort", abortListener);
|
|
799
878
|
}
|
|
@@ -813,3 +892,14 @@ export function streamCursor(
|
|
|
813
892
|
|
|
814
893
|
return stream;
|
|
815
894
|
}
|
|
895
|
+
|
|
896
|
+
export const __testUtils = {
|
|
897
|
+
DEFAULT_CURSOR_NATIVE_REPLAY_IDLE_DISPOSE_MS,
|
|
898
|
+
pendingCursorNativeRunCount: () => pendingCursorNativeRuns.size,
|
|
899
|
+
setCursorNativeReplayIdleDisposeMs: (value: number) => {
|
|
900
|
+
cursorNativeReplayIdleDisposeMs = value;
|
|
901
|
+
},
|
|
902
|
+
resetCursorNativeReplayIdleDisposeMs: () => {
|
|
903
|
+
cursorNativeReplayIdleDisposeMs = DEFAULT_CURSOR_NATIVE_REPLAY_IDLE_DISPOSE_MS;
|
|
904
|
+
},
|
|
905
|
+
};
|
package/src/cursor-state.ts
CHANGED
|
@@ -105,16 +105,25 @@ function restoreMapValue(map: Map<string, boolean>, key: string, previous: boole
|
|
|
105
105
|
function persistFastPreference(pi: ExtensionAPI, baseModelId: string, fast: boolean): void {
|
|
106
106
|
const previousSession = sessionFastPreferences.get(baseModelId);
|
|
107
107
|
const previousGlobal = globalFastPreferences.get(baseModelId);
|
|
108
|
+
let savedGlobal = false;
|
|
108
109
|
sessionFastPreferences.set(baseModelId, fast);
|
|
109
110
|
globalFastPreferences.set(baseModelId, fast);
|
|
110
111
|
try {
|
|
111
112
|
saveGlobalFastPreferences();
|
|
113
|
+
savedGlobal = true;
|
|
114
|
+
pi.appendEntry<CursorFastEntryData>(FAST_ENTRY_TYPE, { baseModelId, fast });
|
|
112
115
|
} catch (error) {
|
|
113
116
|
restoreMapValue(sessionFastPreferences, baseModelId, previousSession);
|
|
114
117
|
restoreMapValue(globalFastPreferences, baseModelId, previousGlobal);
|
|
118
|
+
if (savedGlobal) {
|
|
119
|
+
try {
|
|
120
|
+
saveGlobalFastPreferences();
|
|
121
|
+
} catch {
|
|
122
|
+
// Preserve the original append failure reported to the user.
|
|
123
|
+
}
|
|
124
|
+
}
|
|
115
125
|
throw error;
|
|
116
126
|
}
|
|
117
|
-
pi.appendEntry<CursorFastEntryData>(FAST_ENTRY_TYPE, { baseModelId, fast });
|
|
118
127
|
}
|
|
119
128
|
|
|
120
129
|
export function getEffectiveFastForModelId(modelId: string): boolean | undefined {
|
|
@@ -281,15 +281,18 @@ function renderTreeNode(node: unknown, depth = 0, lines: string[] = []): string[
|
|
|
281
281
|
return lines;
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
-
function
|
|
285
|
-
const path = formatPathArg(args, options) ?? ".";
|
|
286
|
-
if (result.status === "error") return joinSections(`ls ${path}`, formatError(result.error));
|
|
287
|
-
|
|
284
|
+
function getLsBody(result: NormalizedResult, options: TranscriptOptions): string {
|
|
288
285
|
const value = asRecord(result.value);
|
|
289
286
|
const root = value?.directoryTreeRoot ?? result.value;
|
|
290
287
|
const treeLines = renderTreeNode(root);
|
|
291
288
|
const body = treeLines.length > 0 ? treeLines.join("\n") : stringifyUnknown(result.value);
|
|
292
|
-
return
|
|
289
|
+
return limitText(body, options);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function formatLs(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
|
|
293
|
+
const path = formatPathArg(args, options) ?? ".";
|
|
294
|
+
if (result.status === "error") return joinSections(`ls ${path}`, formatError(result.error));
|
|
295
|
+
return joinSections(`ls ${path}`, getLsBody(result, options));
|
|
293
296
|
}
|
|
294
297
|
|
|
295
298
|
function formatGlob(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
|
|
@@ -528,7 +531,7 @@ export function buildCursorPiToolDisplay(toolCall: unknown, options: TranscriptO
|
|
|
528
531
|
return {
|
|
529
532
|
toolName: "ls",
|
|
530
533
|
args,
|
|
531
|
-
result: textToolResult(result.status === "error" ? formatError(result.error) :
|
|
534
|
+
result: textToolResult(result.status === "error" ? formatError(result.error) : getLsBody(result, options).trim()),
|
|
532
535
|
isError: result.status === "error",
|
|
533
536
|
};
|
|
534
537
|
}
|
package/src/model-discovery.ts
CHANGED
|
@@ -7,7 +7,7 @@ import type {
|
|
|
7
7
|
} from "@cursor/sdk";
|
|
8
8
|
import { AuthStorage, type ProviderModelConfig } from "@earendil-works/pi-coding-agent";
|
|
9
9
|
import type { ModelThinkingLevel, ThinkingLevelMap } from "@earendil-works/pi-ai";
|
|
10
|
-
import {
|
|
10
|
+
import { loadContextWindowCache } from "./context-window-cache.js";
|
|
11
11
|
|
|
12
12
|
const CURSOR_PROVIDER_ID = "cursor";
|
|
13
13
|
const CURSOR_API_KEY_ENV_VAR = "CURSOR_API_KEY";
|
|
@@ -88,7 +88,7 @@ const FALLBACK_MODEL_ITEMS: ModelListItem[] = [
|
|
|
88
88
|
{
|
|
89
89
|
id: "context",
|
|
90
90
|
displayName: "Context",
|
|
91
|
-
values: [{ value: "1m" }, { value: "
|
|
91
|
+
values: [{ value: "1m" }, { value: "200k" }],
|
|
92
92
|
},
|
|
93
93
|
{
|
|
94
94
|
id: "effort",
|
|
@@ -165,6 +165,7 @@ export type CursorModelFallbackReason = "missing-api-key" | "discovery-failed" |
|
|
|
165
165
|
export interface CursorModelFallbackIssue {
|
|
166
166
|
reason: CursorModelFallbackReason;
|
|
167
167
|
message: string;
|
|
168
|
+
errorMessage?: string;
|
|
168
169
|
}
|
|
169
170
|
|
|
170
171
|
export interface DiscoverModelsOptions {
|
|
@@ -217,6 +218,7 @@ async function getDiscoveryApiKey(): Promise<string | undefined> {
|
|
|
217
218
|
export interface CursorModelMetadata {
|
|
218
219
|
piModelId: string;
|
|
219
220
|
baseModelId: string;
|
|
221
|
+
selectionModelId: string;
|
|
220
222
|
displayName: string;
|
|
221
223
|
defaultParams: ModelParameterValue[];
|
|
222
224
|
context?: string;
|
|
@@ -340,35 +342,44 @@ function getParamValue(params: ModelParameterValue[], id: string): string | unde
|
|
|
340
342
|
return params.find((param) => param.id === id)?.value;
|
|
341
343
|
}
|
|
342
344
|
|
|
343
|
-
function encodePiModelId(
|
|
344
|
-
return context ? `${
|
|
345
|
+
function encodePiModelId(modelId: string, context?: string): string {
|
|
346
|
+
return context ? `${modelId}@${context}` : modelId;
|
|
345
347
|
}
|
|
346
348
|
|
|
347
|
-
function getModelName(item: ModelListItem, context?: string): string {
|
|
349
|
+
function getModelName(item: ModelListItem, context?: string, alias?: string): string {
|
|
348
350
|
const displayName = item.displayName || item.id;
|
|
349
|
-
|
|
351
|
+
const baseName = alias ? `${displayName} (${alias})` : displayName;
|
|
352
|
+
return context ? `${baseName} @ ${context}` : baseName;
|
|
350
353
|
}
|
|
351
354
|
|
|
352
|
-
function getContextWindow(piModelId: string, context?: string): number {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
+
function getContextWindow(contextWindowCache: Map<string, number>, piModelId: string, context?: string, baseModelId?: string): number {
|
|
356
|
+
return (
|
|
357
|
+
contextWindowCache.get(piModelId) ??
|
|
358
|
+
(context ? parseContextWindow(context) : undefined) ??
|
|
359
|
+
(baseModelId ? contextWindowCache.get(baseModelId) : undefined) ??
|
|
360
|
+
contextWindowCache.get("default") ??
|
|
361
|
+
FALLBACK_CONTEXT_WINDOW
|
|
362
|
+
);
|
|
355
363
|
}
|
|
356
364
|
|
|
357
365
|
function toMetadata(
|
|
358
366
|
item: ModelListItem,
|
|
359
367
|
piModelId: string,
|
|
368
|
+
selectionModelId: string,
|
|
360
369
|
defaultParams: ModelParameterValue[],
|
|
361
370
|
context: string | undefined,
|
|
371
|
+
contextWindowCache: Map<string, number>,
|
|
362
372
|
): CursorModelMetadata {
|
|
363
373
|
const thinkingLevelMap = getThinkingLevelMap(item);
|
|
364
374
|
const fastValue = getParamValue(defaultParams, "fast")?.toLowerCase();
|
|
365
375
|
return {
|
|
366
376
|
piModelId,
|
|
367
377
|
baseModelId: item.id,
|
|
378
|
+
selectionModelId,
|
|
368
379
|
displayName: item.displayName || item.id,
|
|
369
380
|
defaultParams: cloneParams(defaultParams),
|
|
370
381
|
...(context ? { context } : {}),
|
|
371
|
-
contextWindow: getContextWindow(piModelId, context),
|
|
382
|
+
contextWindow: getContextWindow(contextWindowCache, piModelId, context, item.id),
|
|
372
383
|
supportsFast: getParameter(item, "fast") !== undefined,
|
|
373
384
|
defaultFast: fastValue === "true",
|
|
374
385
|
supportsReasoning: thinkingLevelMap !== undefined,
|
|
@@ -400,18 +411,56 @@ function getContextValues(item: ModelListItem): string[] {
|
|
|
400
411
|
return getParameter(item, "context")?.values.map((value) => value.value) ?? [];
|
|
401
412
|
}
|
|
402
413
|
|
|
403
|
-
function
|
|
414
|
+
function getAmbiguousAliases(items: ModelListItem[]): Set<string> {
|
|
415
|
+
const aliasOwners = new Map<string, Set<string>>();
|
|
416
|
+
for (const item of items) {
|
|
417
|
+
for (const rawAlias of item.aliases ?? []) {
|
|
418
|
+
const alias = rawAlias.trim();
|
|
419
|
+
if (!alias || alias === item.id) continue;
|
|
420
|
+
const owners = aliasOwners.get(alias) ?? new Set<string>();
|
|
421
|
+
owners.add(item.id);
|
|
422
|
+
aliasOwners.set(alias, owners);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return new Set([...aliasOwners.entries()].filter(([, owners]) => owners.size > 1).map(([alias]) => alias));
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function getModelIds(item: ModelListItem, reservedBaseModelIds: Set<string>, ambiguousAliases: Set<string>): string[] {
|
|
429
|
+
const ids = [item.id];
|
|
430
|
+
for (const rawAlias of item.aliases ?? []) {
|
|
431
|
+
const alias = rawAlias.trim();
|
|
432
|
+
if (!alias || alias === item.id || ids.includes(alias) || reservedBaseModelIds.has(alias) || ambiguousAliases.has(alias)) continue;
|
|
433
|
+
ids.push(alias);
|
|
434
|
+
}
|
|
435
|
+
return ids;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function toModelConfigs(
|
|
439
|
+
item: ModelListItem,
|
|
440
|
+
usedPiModelIds: Set<string>,
|
|
441
|
+
reservedBaseModelIds: Set<string>,
|
|
442
|
+
ambiguousAliases: Set<string>,
|
|
443
|
+
contextWindowCache: Map<string, number>,
|
|
444
|
+
): ProviderModelConfig[] {
|
|
404
445
|
const defaultParams = getDefaultParams(item);
|
|
405
446
|
const contextValues = getContextValues(item);
|
|
406
447
|
const contexts = contextValues.length > 0 ? contextValues : [undefined];
|
|
448
|
+
const configs: ProviderModelConfig[] = [];
|
|
449
|
+
|
|
450
|
+
for (const selectionModelId of getModelIds(item, reservedBaseModelIds, ambiguousAliases)) {
|
|
451
|
+
const alias = selectionModelId === item.id ? undefined : selectionModelId;
|
|
452
|
+
for (const context of contexts) {
|
|
453
|
+
const params = context ? replaceParam(defaultParams, "context", context) : defaultParams;
|
|
454
|
+
const piModelId = encodePiModelId(selectionModelId, context);
|
|
455
|
+
if (usedPiModelIds.has(piModelId)) continue;
|
|
456
|
+
usedPiModelIds.add(piModelId);
|
|
457
|
+
const metadata = toMetadata(item, piModelId, selectionModelId, params, context, contextWindowCache);
|
|
458
|
+
metadataByPiModelId.set(piModelId, metadata);
|
|
459
|
+
configs.push(toModelConfig(metadata, getModelName(item, context, alias)));
|
|
460
|
+
}
|
|
461
|
+
}
|
|
407
462
|
|
|
408
|
-
return
|
|
409
|
-
const params = context ? replaceParam(defaultParams, "context", context) : defaultParams;
|
|
410
|
-
const piModelId = encodePiModelId(item.id, context);
|
|
411
|
-
const metadata = toMetadata(item, piModelId, params, context);
|
|
412
|
-
metadataByPiModelId.set(piModelId, metadata);
|
|
413
|
-
return toModelConfig(metadata, getModelName(item, context));
|
|
414
|
-
});
|
|
463
|
+
return configs;
|
|
415
464
|
}
|
|
416
465
|
|
|
417
466
|
function sortModelsByBaseId(items: ModelListItem[]): ModelListItem[] {
|
|
@@ -420,7 +469,11 @@ function sortModelsByBaseId(items: ModelListItem[]): ModelListItem[] {
|
|
|
420
469
|
|
|
421
470
|
function registerModelItems(items: ModelListItem[]): ProviderModelConfig[] {
|
|
422
471
|
metadataByPiModelId.clear();
|
|
423
|
-
|
|
472
|
+
const usedPiModelIds = new Set<string>();
|
|
473
|
+
const reservedBaseModelIds = new Set(items.map((item) => item.id));
|
|
474
|
+
const ambiguousAliases = getAmbiguousAliases(items);
|
|
475
|
+
const contextWindowCache = loadContextWindowCache();
|
|
476
|
+
return sortModelsByBaseId(items).flatMap((item) => toModelConfigs(item, usedPiModelIds, reservedBaseModelIds, ambiguousAliases, contextWindowCache));
|
|
424
477
|
}
|
|
425
478
|
|
|
426
479
|
export function getCursorModelMetadata(modelId: string): CursorModelMetadata | undefined {
|
|
@@ -501,7 +554,25 @@ export function buildCursorModelSelection(
|
|
|
501
554
|
setParam(params, "fast", fastEnabled ? "true" : "false");
|
|
502
555
|
}
|
|
503
556
|
|
|
504
|
-
return params.length > 0 ? { id: metadata.
|
|
557
|
+
return params.length > 0 ? { id: metadata.selectionModelId, params } : { id: metadata.selectionModelId };
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function scrubDiscoveryErrorText(text: string, apiKey: string): string {
|
|
561
|
+
let scrubbed = text.replace(new RegExp(apiKey.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g"), "[redacted]");
|
|
562
|
+
return scrubbed
|
|
563
|
+
.replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [redacted]")
|
|
564
|
+
.replace(/((?:^|[\s,{])cookie["']?\s*[:=]\s*["']?)[^\n]+/gi, "$1[redacted]")
|
|
565
|
+
.replace(
|
|
566
|
+
/((?:authorization|api[_-]?key|apiKey|token|session(?:[_-]?id)?)["']?\s*[:=]\s*["']?)[^"'\s,;}]+/gi,
|
|
567
|
+
"$1[redacted]",
|
|
568
|
+
)
|
|
569
|
+
.trim();
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function sanitizeDiscoveryError(error: unknown, apiKey: string): string | undefined {
|
|
573
|
+
const message = error instanceof Error ? error.message : typeof error === "string" ? error : "";
|
|
574
|
+
const scrubbed = scrubDiscoveryErrorText(message, apiKey);
|
|
575
|
+
return scrubbed || undefined;
|
|
505
576
|
}
|
|
506
577
|
|
|
507
578
|
function useFallbackModels(options: DiscoverModelsOptions, issue: CursorModelFallbackIssue): ProviderModelConfig[] {
|
|
@@ -527,10 +598,12 @@ export async function discoverModels(options: DiscoverModelsOptions = {}): Promi
|
|
|
527
598
|
reason: "empty-model-list",
|
|
528
599
|
message: `Cursor model discovery returned no models. Using fallback Cursor models; verify ${AUTH_SETUP_HINT}. ${CATALOG_REFRESH_HINT}`,
|
|
529
600
|
});
|
|
530
|
-
} catch {
|
|
601
|
+
} catch (error) {
|
|
602
|
+
const errorMessage = sanitizeDiscoveryError(error, apiKey);
|
|
531
603
|
return useFallbackModels(options, {
|
|
532
604
|
reason: "discovery-failed",
|
|
533
|
-
message: `Cursor model discovery failed. Using fallback Cursor models; verify ${AUTH_SETUP_HINT}. ${CATALOG_REFRESH_HINT}`,
|
|
605
|
+
message: `Cursor model discovery failed${errorMessage ? `: ${errorMessage}` : ""}. Using fallback Cursor models; verify ${AUTH_SETUP_HINT}. ${CATALOG_REFRESH_HINT}`,
|
|
606
|
+
...(errorMessage ? { errorMessage } : {}),
|
|
534
607
|
});
|
|
535
608
|
}
|
|
536
609
|
}
|