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.
Files changed (75) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/LICENSE +21 -0
  3. package/README.md +87 -0
  4. package/README.zh-CN.md +87 -0
  5. package/UPSTREAM_CHANGELOG.md +368 -0
  6. package/UPSTREAM_SOURCE.md +23 -0
  7. package/dist/index.js +54 -0
  8. package/package.json +97 -0
  9. package/src/auth/cli-credentials.ts +275 -0
  10. package/src/auth/consent.ts +25 -0
  11. package/src/auth/index.ts +23 -0
  12. package/src/auth/oauth.ts +282 -0
  13. package/src/auth/refresh-guard.ts +93 -0
  14. package/src/client/bridge.ts +673 -0
  15. package/src/client/cursor-wire.ts +213 -0
  16. package/src/client/h2-unary.ts +142 -0
  17. package/src/client/index.ts +18 -0
  18. package/src/config/index.ts +69 -0
  19. package/src/diagnostics/diagnostics.ts +116 -0
  20. package/src/diagnostics/index.ts +1 -0
  21. package/src/extension/auth.ts +99 -0
  22. package/src/extension/commands.ts +163 -0
  23. package/src/extension/compaction-guard.ts +86 -0
  24. package/src/extension/debug-hooks.ts +359 -0
  25. package/src/extension/index.ts +8 -0
  26. package/src/extension/provider.ts +277 -0
  27. package/src/extension/quota-adapter.ts +175 -0
  28. package/src/extension/report-dashboard.ts +133 -0
  29. package/src/identity.ts +16 -0
  30. package/src/index.ts +186 -0
  31. package/src/models/ask-catalog.ts +384 -0
  32. package/src/models/catalog.json +1163 -0
  33. package/src/models/cost.ts +126 -0
  34. package/src/models/index.ts +6 -0
  35. package/src/models/limits.ts +36 -0
  36. package/src/models/parameterized.ts +416 -0
  37. package/src/models/processing.ts +313 -0
  38. package/src/proto/agent_pb.ts +14577 -0
  39. package/src/stream/bridge-session.ts +215 -0
  40. package/src/stream/client-transcript.ts +51 -0
  41. package/src/stream/config.ts +5 -0
  42. package/src/stream/context-normalize.ts +308 -0
  43. package/src/stream/context-usage.ts +168 -0
  44. package/src/stream/debug-log.ts +316 -0
  45. package/src/stream/drift.ts +122 -0
  46. package/src/stream/images.ts +201 -0
  47. package/src/stream/index.ts +68 -0
  48. package/src/stream/interaction-query.ts +369 -0
  49. package/src/stream/message-parsing.ts +402 -0
  50. package/src/stream/model-cache.ts +100 -0
  51. package/src/stream/model-discovery.ts +242 -0
  52. package/src/stream/model-routing.ts +100 -0
  53. package/src/stream/native-core.ts +2121 -0
  54. package/src/stream/pi-adapter.ts +414 -0
  55. package/src/stream/protocol.ts +63 -0
  56. package/src/stream/recovery.ts +494 -0
  57. package/src/stream/request-build.ts +668 -0
  58. package/src/stream/root-prompt.ts +184 -0
  59. package/src/stream/run-journal.ts +474 -0
  60. package/src/stream/run-usage.ts +107 -0
  61. package/src/stream/server-messages.ts +777 -0
  62. package/src/stream/session-state.ts +499 -0
  63. package/src/stream/stream-writer.ts +211 -0
  64. package/src/stream/thinking-filter.ts +63 -0
  65. package/src/stream/tool-schema.ts +185 -0
  66. package/src/stream/transport-errors.ts +150 -0
  67. package/src/stream/tuning.ts +250 -0
  68. package/src/stream/types.ts +330 -0
  69. package/src/types/enums.ts +103 -0
  70. package/src/types/index.ts +4 -0
  71. package/src/usage.ts +262 -0
  72. package/src/utils/cache-dir.ts +39 -0
  73. package/src/utils/index.ts +2 -0
  74. package/src/utils/security.ts +68 -0
  75. package/src/utils/util.ts +43 -0
@@ -0,0 +1,313 @@
1
+ /**
2
+ * Model parsing, effort mapping, and routing lookups.
3
+ */
4
+
5
+ import type { CursorModel } from "../stream/model-discovery.js";
6
+ import type { CursorNativeModelRouting } from "../stream/model-routing.js";
7
+ import { estimateModelCost } from "./cost.js";
8
+ import { ProviderConstant, type PiThinkingLevel } from "../types/enums.js";
9
+
10
+ export type CursorModelRouting = CursorNativeModelRouting;
11
+
12
+ export const CURSOR_EFFORT_SUFFIXES: Array<{ suffix: string; effort: string }> = [
13
+ { suffix: "extra-high", effort: "xhigh" },
14
+ { suffix: "minimal", effort: "minimal" },
15
+ { suffix: "xhigh", effort: "xhigh" },
16
+ { suffix: "medium", effort: "medium" },
17
+ { suffix: "high", effort: "high" },
18
+ { suffix: "low", effort: "low" },
19
+ { suffix: "max", effort: "max" },
20
+ { suffix: "none", effort: "none" },
21
+ ];
22
+
23
+ export type CursorEffortMap = Record<PiThinkingLevel, string | null>;
24
+
25
+ export interface ParsedModelId {
26
+ base: string; // model ID with effort stripped
27
+ effort: string; // effort level, or "" if no effort suffix
28
+ fast: boolean; // has -fast suffix
29
+ thinking: boolean; // has -thinking suffix
30
+ }
31
+
32
+ export function stripEffortSuffix(id: string): { remaining: string; effort: string } {
33
+ for (const { suffix, effort } of CURSOR_EFFORT_SUFFIXES) {
34
+ const marker = `-${suffix}`;
35
+ if (id.endsWith(marker)) {
36
+ return { remaining: id.slice(0, -marker.length), effort };
37
+ }
38
+ }
39
+ return { remaining: id, effort: "" };
40
+ }
41
+
42
+ export function parseModelId(id: string): ParsedModelId {
43
+ let remaining = id;
44
+ let fast = false;
45
+ let thinking = false;
46
+
47
+ if (remaining.endsWith("-fast")) {
48
+ fast = true;
49
+ remaining = remaining.slice(0, -5);
50
+ }
51
+
52
+ // Cursor has used both orders for thinking effort variants:
53
+ // claude-4.6-opus-max-thinking (effort before -thinking)
54
+ // claude-opus-4-7-thinking-max (effort after -thinking)
55
+ let effort: string;
56
+ if (remaining.endsWith("-thinking")) {
57
+ thinking = true;
58
+ remaining = remaining.slice(0, -9);
59
+ const parsed = stripEffortSuffix(remaining);
60
+ remaining = parsed.remaining;
61
+ effort = parsed.effort;
62
+ } else {
63
+ const parsed = stripEffortSuffix(remaining);
64
+ remaining = parsed.remaining;
65
+ effort = parsed.effort;
66
+ if (remaining.endsWith("-thinking")) {
67
+ thinking = true;
68
+ remaining = remaining.slice(0, -9);
69
+ }
70
+ }
71
+
72
+ return { base: remaining, effort, fast, thinking };
73
+ }
74
+
75
+ export interface ProcessedModel extends CursorModel {
76
+ supportsEffort: boolean;
77
+ effortMap?: CursorEffortMap;
78
+ rawModelByEffort?: Record<string, string>;
79
+ rawRoutingByEffort?: Record<string, CursorModelRouting>;
80
+ }
81
+
82
+ export function buildNoReasoningEffortLookup(models: ProcessedModel[]): Map<string, string> {
83
+ const lookup = new Map<string, string>();
84
+ for (const model of models) {
85
+ if (
86
+ model.supportsEffort &&
87
+ model.effortMap &&
88
+ Object.values(model.effortMap).includes("none")
89
+ ) {
90
+ lookup.set(model.id, "none");
91
+ }
92
+ }
93
+ return lookup;
94
+ }
95
+
96
+ function routingForModel(model: CursorModel): CursorModelRouting | undefined {
97
+ if (
98
+ !model.requestedModelId &&
99
+ !model.parameters?.length &&
100
+ !model.requiresMaxMode &&
101
+ typeof model.requestedMaxMode !== "boolean"
102
+ ) {
103
+ return undefined;
104
+ }
105
+ return {
106
+ modelId: model.requestedModelId ?? model.id,
107
+ ...(model.parameters?.length ? { parameters: model.parameters } : {}),
108
+ ...(model.requiresMaxMode ? { requiresMaxMode: true } : {}),
109
+ ...(typeof model.requestedMaxMode === "boolean"
110
+ ? { requestedMaxMode: model.requestedMaxMode }
111
+ : {}),
112
+ };
113
+ }
114
+
115
+ function defaultRoutingEffort(model: ProcessedModel): string | undefined {
116
+ const routes = model.rawRoutingByEffort;
117
+ if (!routes) return undefined;
118
+ const mappedMedium = model.effortMap?.medium;
119
+ for (const effort of [mappedMedium, "medium", "", "low", "high", "none", "xhigh", "max"]) {
120
+ if (typeof effort === "string" && routes[effort]) return effort;
121
+ }
122
+ return Object.keys(routes)[0];
123
+ }
124
+
125
+ export function buildRawModelLookup(
126
+ models: ProcessedModel[],
127
+ ): Map<string, Record<string, CursorModelRouting>> {
128
+ const lookup = new Map<string, Record<string, CursorModelRouting>>();
129
+ for (const model of models) {
130
+ if (model.supportsEffort && model.rawRoutingByEffort) {
131
+ const routes = { ...model.rawRoutingByEffort };
132
+ if (model.effortMap) {
133
+ for (const [piEffort, cursorEffort] of Object.entries(model.effortMap)) {
134
+ if (typeof cursorEffort === "string" && !routes[piEffort] && routes[cursorEffort]) {
135
+ routes[piEffort] = routes[cursorEffort];
136
+ }
137
+ }
138
+ }
139
+ const defaultEffort = defaultRoutingEffort(model);
140
+ if (defaultEffort !== undefined && !routes[""])
141
+ routes[""] = model.rawRoutingByEffort[defaultEffort]!;
142
+ lookup.set(model.id, routes);
143
+ continue;
144
+ }
145
+
146
+ const routing = routingForModel(model);
147
+ if (routing) lookup.set(model.id, { "": routing });
148
+ }
149
+ return lookup;
150
+ }
151
+
152
+ export function applyRawCursorModelId(
153
+ payload: Record<string, unknown>,
154
+ rawRoutingByEffortByModelId: Map<string, Record<string, CursorModelRouting>>,
155
+ ): void {
156
+ if (typeof payload.model !== "string") return;
157
+ const rawRoutingByEffort = rawRoutingByEffortByModelId.get(payload.model);
158
+ const effort = typeof payload.reasoning_effort === "string" ? payload.reasoning_effort : "";
159
+ const routing = rawRoutingByEffort?.[effort];
160
+ if (!routing) return;
161
+ payload.cursor_model_id = routing.modelId;
162
+ if (routing.parameters?.length) payload.cursor_model_parameters = routing.parameters;
163
+ if (routing.requiresMaxMode) payload.cursor_requires_max_mode = true;
164
+ if (typeof routing.requestedMaxMode === "boolean")
165
+ payload.cursor_model_max_mode = routing.requestedMaxMode;
166
+ }
167
+
168
+ export function applyNoReasoningEffort(
169
+ payload: Record<string, unknown>,
170
+ thinkingLevel: string,
171
+ noReasoningEffortByModelId: Map<string, string>,
172
+ ): void {
173
+ if (thinkingLevel !== "off") {
174
+ return;
175
+ }
176
+ if (payload.reasoning_effort !== undefined || typeof payload.model !== "string") {
177
+ return;
178
+ }
179
+ const noReasoningEffort = noReasoningEffortByModelId.get(payload.model);
180
+ if (noReasoningEffort) payload.reasoning_effort = noReasoningEffort;
181
+ }
182
+
183
+ export function supportsReasoningModelId(id: string): boolean {
184
+ const { base, effort, thinking } = parseModelId(id);
185
+ if (effort || thinking) return true;
186
+ if (base === "default" || base === "auto") return true;
187
+ return /^(claude|composer|gemini|gpt|grok|kimi)(-|$)/i.test(base);
188
+ }
189
+
190
+ /**
191
+ * Map only controls Cursor explicitly advertised. Null hides unsupported Pi
192
+ * levels instead of silently routing them to a different Cursor effort.
193
+ */
194
+ export function buildEffortMap(efforts: Set<string>): CursorEffortMap {
195
+ const supported = (effort: string): string | null => (efforts.has(effort) ? effort : null);
196
+ return {
197
+ off: supported("none"),
198
+ minimal: supported("minimal"),
199
+ low: supported("low"),
200
+ // A bare Cursor model ID is the provider's default effort, equivalent to Pi medium.
201
+ medium: efforts.has("medium") ? "medium" : supported(""),
202
+ high: supported("high"),
203
+ xhigh: supported("xhigh"),
204
+ max: supported("max"),
205
+ };
206
+ }
207
+
208
+ /** Dedup raw models: collapse effort variants into one entry with supportsReasoningEffort. */
209
+ export function processModels(raw: CursorModel[]): ProcessedModel[] {
210
+ // Group by (base, fast, thinking)
211
+ const groups = new Map<
212
+ string,
213
+ {
214
+ base: string;
215
+ fast: boolean;
216
+ thinking: boolean;
217
+ efforts: Map<string, CursorModel>;
218
+ }
219
+ >();
220
+
221
+ for (const model of raw) {
222
+ const p = parseModelId(model.id);
223
+ const key = `${p.base}|${p.fast}|${p.thinking}`;
224
+ let g = groups.get(key);
225
+ if (!g) {
226
+ g = { base: p.base, fast: p.fast, thinking: p.thinking, efforts: new Map() };
227
+ groups.set(key, g);
228
+ }
229
+ g.efforts.set(p.effort, model);
230
+ }
231
+
232
+ const result: ProcessedModel[] = [];
233
+
234
+ for (const g of groups.values()) {
235
+ const effortNames = new Set(g.efforts.keys());
236
+
237
+ // Dedup when there are multiple effort variants, OR a single variant
238
+ // whose effort is non-empty (e.g. claude-4.5-opus-high — strip the
239
+ // mandatory effort suffix so the model appears as claude-4.5-opus
240
+ // with effort mapping).
241
+ const hasOnlyEffortVariants = effortNames.size === 1 && ![...effortNames][0]!.trim().length;
242
+ const shouldDedup = effortNames.size >= 2 || !hasOnlyEffortVariants;
243
+ if (shouldDedup && (effortNames.size >= 2 || [...effortNames][0] !== "")) {
244
+ // Pick representative: prefer "medium" or default ("") for name/metadata
245
+ const rep = g.efforts.get("medium") ?? g.efforts.get("") ?? [...g.efforts.values()][0]!;
246
+
247
+ // Build deduped model ID: base + thinking/fast suffix (no effort)
248
+ let id = g.base;
249
+ if (g.thinking) id += "-thinking";
250
+ if (g.fast) id += "-fast";
251
+
252
+ const effortMap = buildEffortMap(effortNames);
253
+ const rawModelByEffort = Object.fromEntries(
254
+ [...g.efforts.entries()].map(([effort, model]) => [effort, model.id]),
255
+ );
256
+ const rawRoutingByEffort = Object.fromEntries(
257
+ [...g.efforts.entries()].map(([effort, model]) => [
258
+ effort,
259
+ {
260
+ modelId: model.requestedModelId ?? model.id,
261
+ ...(model.parameters?.length ? { parameters: model.parameters } : {}),
262
+ ...(model.requiresMaxMode ? { requiresMaxMode: true } : {}),
263
+ ...(typeof model.requestedMaxMode === "boolean"
264
+ ? { requestedMaxMode: model.requestedMaxMode }
265
+ : {}),
266
+ },
267
+ ]),
268
+ );
269
+
270
+ result.push({
271
+ ...rep,
272
+ id,
273
+ supportsEffort: true,
274
+ effortMap,
275
+ rawModelByEffort,
276
+ rawRoutingByEffort,
277
+ });
278
+ } else {
279
+ // Keep single entries as-is (base model without effort variants)
280
+ for (const model of g.efforts.values()) {
281
+ result.push({ ...model, supportsEffort: false });
282
+ }
283
+ }
284
+ }
285
+
286
+ return result.sort((a, b) => a.id.localeCompare(b.id));
287
+ }
288
+
289
+ export function modelConfig(m: ProcessedModel) {
290
+ const input = (m.supportsImages === false ? ["text"] : ["text", "image"]) as ("text" | "image")[];
291
+ return {
292
+ id: m.id,
293
+ name: m.name,
294
+ // Keep api explicit on every model so session restore / models.json merges
295
+ // cannot strand rows on a different transport id.
296
+ api: ProviderConstant.NativeApi,
297
+ // Pi's thinking control must only appear when Cursor exposed selectable
298
+ // effort variants. A model name alone is not evidence of a controllable level.
299
+ reasoning: m.supportsEffort,
300
+ ...(m.supportsEffort &&
301
+ m.effortMap && {
302
+ thinkingLevelMap: m.effortMap,
303
+ }),
304
+ input,
305
+ // Picker ids stay short (`fable-5.1`); price against the Cursor model id
306
+ // behind them, then the picker id so Fast/alias rows still match.
307
+ cost: estimateModelCost(
308
+ [m.requestedModelId, m.id].filter((value): value is string => Boolean(value)).join(" "),
309
+ ),
310
+ contextWindow: m.contextWindow,
311
+ maxTokens: m.maxTokens,
312
+ };
313
+ }