@oh-my-pi/pi-catalog 18.4.4 → 18.4.6

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 (61) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/types/compat/auth-ids.d.ts +2 -2
  3. package/dist/types/compat/axes.d.ts +3 -1
  4. package/dist/types/compat/factory-droid.d.ts +65 -0
  5. package/dist/types/compat/provider-ids.d.ts +1 -1
  6. package/dist/types/compat/resolve.d.ts +8 -2
  7. package/dist/types/compat/types.d.ts +26 -1
  8. package/dist/types/discovery/cursor-proto.d.ts +284 -0
  9. package/dist/types/discovery/cursor.d.ts +12 -6
  10. package/dist/types/discovery/factory-droid.d.ts +28 -0
  11. package/dist/types/discovery/index.d.ts +1 -0
  12. package/dist/types/discovery/protobuf.d.ts +3 -3
  13. package/dist/types/model-manager.d.ts +10 -0
  14. package/dist/types/models.d.ts +5 -1
  15. package/dist/types/provider-models/cache-provider-id.d.ts +2 -1
  16. package/dist/types/provider-models/descriptor-types.d.ts +2 -1
  17. package/dist/types/provider-models/openai-compat.d.ts +21 -0
  18. package/dist/types/provider-models/special.d.ts +2 -0
  19. package/dist/types/types.d.ts +100 -3
  20. package/dist/types/wire/cursor.d.ts +20 -0
  21. package/dist/types/wire/factory-droid.d.ts +45 -0
  22. package/package.json +4 -4
  23. package/src/build.ts +21 -0
  24. package/src/compat/auth-ids.ts +4 -0
  25. package/src/compat/axes.ts +113 -2
  26. package/src/compat/cascade.ts +8 -1
  27. package/src/compat/collapse.ts +3 -0
  28. package/src/compat/factory-droid.ts +170 -0
  29. package/src/compat/provider-ids.ts +2 -0
  30. package/src/compat/resolve.ts +47 -5
  31. package/src/compat/rules/README.md +18 -8
  32. package/src/compat/rules/auth/_order.kdl +2 -2
  33. package/src/compat/rules/auth/factory-droid.kdl +28 -0
  34. package/src/compat/rules/auth/helmcode.kdl +9 -0
  35. package/src/compat/rules/classes/anthropic.kdl +195 -80
  36. package/src/compat/rules/providers/commandcode.kdl +21 -5
  37. package/src/compat/rules/providers/cursor.kdl +29 -17
  38. package/src/compat/rules/providers/factory-droid.kdl +1166 -0
  39. package/src/compat/rules/providers/helmcode.kdl +120 -0
  40. package/src/compat/rules/runtime/behavior.kdl +65 -4
  41. package/src/compat/rules/taxonomy/_collapse.kdl +36 -14
  42. package/src/compat/rules.json +1 -1
  43. package/src/compat/types.ts +27 -1
  44. package/src/discovery/cursor-models.proto +261 -0
  45. package/src/discovery/cursor-proto.ts +471 -0
  46. package/src/discovery/cursor.ts +817 -88
  47. package/src/discovery/devin.ts +1 -3
  48. package/src/discovery/factory-droid.ts +296 -0
  49. package/src/discovery/index.ts +1 -0
  50. package/src/discovery/protobuf.ts +16 -7
  51. package/src/model-manager.ts +13 -0
  52. package/src/models.json +1 -1
  53. package/src/models.ts +13 -0
  54. package/src/provider-models/cache-provider-id.ts +37 -6
  55. package/src/provider-models/descriptor-types.ts +2 -1
  56. package/src/provider-models/descriptors.ts +4 -0
  57. package/src/provider-models/openai-compat.ts +83 -17
  58. package/src/provider-models/special.ts +26 -2
  59. package/src/types.ts +112 -1
  60. package/src/wire/cursor.ts +32 -0
  61. package/src/wire/factory-droid.ts +130 -0
package/src/models.ts CHANGED
@@ -6,6 +6,7 @@ import type {
6
6
  KnownProvider,
7
7
  Model,
8
8
  ModelCost,
9
+ ModelPricingStatus,
9
10
  TimeBasedCost,
10
11
  TokenCost,
11
12
  Usage,
@@ -44,6 +45,11 @@ function getProviderModels(provider: string): Map<string, Model<Api>> | undefine
44
45
 
45
46
  export type GeneratedProvider = keyof typeof MODELS;
46
47
 
48
+ /** Whether `provider` has bundled rows in models.json (e.g. a provider id authored in KDL). */
49
+ export function isGeneratedProvider(provider: string): provider is GeneratedProvider {
50
+ return Object.hasOwn(MODELS, provider);
51
+ }
52
+
47
53
  export function getBundledModel<TApi extends Api = Api>(provider: GeneratedProvider, modelId: string): Model<TApi> {
48
54
  const providerModels = getProviderModels(provider);
49
55
  return providerModels?.get(modelId) as Model<TApi>;
@@ -57,6 +63,13 @@ export function getBundledModels(provider: GeneratedProvider): Model<Api>[] {
57
63
  const models = getProviderModels(provider);
58
64
  return models ? (Array.from(models.values()) as Model<Api>[]) : [];
59
65
  }
66
+ /** Resolve display semantics without mistaking an absent rate card for a free model. */
67
+ export function getModelPricingStatus(model: Pick<Model<Api>, "cost" | "pricingStatus">): ModelPricingStatus {
68
+ const cost = model.cost;
69
+ if (cost.input !== 0 || cost.output !== 0 || cost.cacheRead !== 0 || cost.cacheWrite !== 0) return "fixed";
70
+ return model.pricingStatus ?? "unknown";
71
+ }
72
+
60
73
  function resolveTokenCost(cost: ModelCost, promptInputTokens: number, timestamp: number | undefined): TokenCost {
61
74
  let rates: ModelCost | EffectiveTokenCost = cost;
62
75
  let effectiveFrom = -Infinity;
@@ -1,5 +1,7 @@
1
1
  import { CHARM_HYPER_API_BASE_URL, normalizeCharmHyperBaseUrl } from "../wire/charm-hyper";
2
2
  import { CODEX_CLIENT_VERSION } from "../wire/codex";
3
+ import { CURSOR_DEFAULT_BASE_URL } from "../wire/cursor";
4
+ import { type AccountScope, factoryDroidModelCacheProviderId } from "../wire/factory-droid";
3
5
  import { PERSONAL_GITHUB_COPILOT_BASE_URL } from "../wire/github-copilot";
4
6
  import {
5
7
  SINGULARITYAPI_DEV_API_BASE_URL,
@@ -7,7 +9,7 @@ import {
7
9
  normalizeSingularityApiBaseUrl,
8
10
  } from "../wire/singularityapi";
9
11
 
10
- export interface ModelCacheProviderIdOptions {
12
+ export interface ModelCacheProviderIdOptions extends AccountScope {
11
13
  apiKey?: string;
12
14
  baseUrl?: string;
13
15
  }
@@ -17,6 +19,8 @@ const CREDENTIAL_SCOPED_MODEL_CACHE_PROVIDERS: Readonly<Record<string, true>> =
17
19
  "opencode-zen": true,
18
20
  "github-copilot": true,
19
21
  "muse-code": true,
22
+ cursor: true,
23
+ "factory-droid": true,
20
24
  // Both SingularityAPI rosters are issued per key, so the namespace must be
21
25
  // resolved with the credential (`hydrateCredentialScopedModelCaches`) rather
22
26
  // than from the synchronous, credential-less startup read.
@@ -66,6 +70,24 @@ export function resolveOllamaModelCacheProviderId(providerId: string, baseUrl?:
66
70
  return `${providerId}:ollama-models-v1:${Bun.hash(endpoint).toString(36)}`;
67
71
  }
68
72
 
73
+ /**
74
+ * The `sub` claim of a Cursor access token. Cursor access tokens are JWTs
75
+ * that `refreshCursorToken` rotates (new `exp`/`iat`) for the same account,
76
+ * so the subject is the stable account scope. Non-JWT keys return undefined.
77
+ */
78
+ function cursorCredentialSubject(apiKey: string): string | undefined {
79
+ const parts = apiKey.split(".");
80
+ if (parts.length !== 3 || !parts[1]) return undefined;
81
+ try {
82
+ const payload: unknown = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf-8"));
83
+ if (typeof payload !== "object" || payload === null) return undefined;
84
+ const subject = Reflect.get(payload, "sub");
85
+ return typeof subject === "string" && subject.length > 0 ? `sub:${subject}` : undefined;
86
+ } catch {
87
+ return undefined;
88
+ }
89
+ }
90
+
69
91
  /** Resolve the cache namespace used by a provider's model-manager options without constructing those options. */
70
92
  export function resolveModelCacheProviderId(providerId: string, options: ModelCacheProviderIdOptions = {}): string {
71
93
  switch (providerId) {
@@ -74,11 +96,18 @@ export function resolveModelCacheProviderId(providerId: string, options: ModelCa
74
96
  return `${providerId}:${CODEX_CLIENT_VERSION}`;
75
97
  case "ollama":
76
98
  return resolveOllamaModelCacheProviderId(providerId, options.baseUrl);
77
- case "cursor":
78
- // v4: Grok 4.5/4.6 rows cached before the effort-less default-tier fix
79
- // carry `requestModelId: *-low`, which the Start plan refuses; refetch
80
- // so the collapsed default is re-pointed to `-medium` (issue #9478).
81
- return "cursor:default-effort-v4";
99
+ case "cursor": {
100
+ // Cursor catalogs are entitlement-, admin-policy-, and privacy-mode
101
+ // scoped. A credential switch must never reuse another account's
102
+ // authoritative model rows.
103
+ // v3 invalidates zero-price rows written before rich lanes were priced
104
+ // from the KDL rate card; v4 keys the scope on the token's stable
105
+ // account subject instead of the rotating JWT.
106
+ const baseUrl = (options.baseUrl ?? CURSOR_DEFAULT_BASE_URL).replace(/\/+$/, "");
107
+ const apiKey = options.apiKey ?? "";
108
+ const scope = `${cursorCredentialSubject(apiKey) ?? apiKey}\u0000${baseUrl}`;
109
+ return `cursor:rich-models-v4:${Bun.hash(scope).toString(36)}`;
110
+ }
82
111
  case "charm-hyper": {
83
112
  // Discovery is authoritative for this gateway, so a warm cache is served
84
113
  // for its full TTL without re-probing: the namespace must follow the
@@ -167,6 +196,8 @@ export function resolveModelCacheProviderId(providerId: string, options: ModelCa
167
196
  const scope = `${options.apiKey ?? ""}\u0000${baseUrl}`;
168
197
  return `github-copilot:models-v2:${Bun.hash(scope).toString(36)}`;
169
198
  }
199
+ case "factory-droid":
200
+ return factoryDroidModelCacheProviderId(options);
170
201
  case "openrouter":
171
202
  return "openrouter:pseudo-api";
172
203
  case "vllm": {
@@ -1,9 +1,10 @@
1
1
  import type { CompiledProviderDiscovery } from "../compat/types";
2
2
  import type { ModelManagerOptions } from "../model-manager";
3
3
  import type { Api, FetchImpl } from "../types";
4
+ import type { AccountScope } from "../wire/factory-droid";
4
5
 
5
6
  /** Config passed to a provider's runtime model-manager factory. */
6
- export type ModelManagerConfig = {
7
+ export type ModelManagerConfig = AccountScope & {
7
8
  apiKey?: string;
8
9
  baseUrl?: string;
9
10
  fetch?: FetchImpl;
@@ -37,6 +37,7 @@ import {
37
37
  githubCopilotModelManagerOptions,
38
38
  gmiCloudModelManagerOptions,
39
39
  groqModelManagerOptions,
40
+ helmcodeModelManagerOptions,
40
41
  huggingfaceModelManagerOptions,
41
42
  kiloModelManagerOptions,
42
43
  kimiCodeModelManagerOptions,
@@ -79,6 +80,7 @@ import {
79
80
  import {
80
81
  cursorModelManagerOptions,
81
82
  devinModelManagerOptions,
83
+ factoryDroidModelManagerOptions,
82
84
  gitLabDuoWorkflowModelManagerOptions,
83
85
  localModelManagerOptions,
84
86
  typesafeModelManagerOptions,
@@ -107,6 +109,7 @@ const MODEL_MANAGER_FACTORIES: Readonly<Partial<Record<KnownProvider, ModelManag
107
109
  deepinfra: config => deepinfraModelManagerOptions(config),
108
110
  deepseek: config => deepseekModelManagerOptions(config),
109
111
  devin: config => devinModelManagerOptions(config),
112
+ "factory-droid": config => factoryDroidModelManagerOptions(config),
110
113
  "cline-pass": config => clinePassModelManagerOptions(config),
111
114
  firepass: config => firepassModelManagerOptions(config),
112
115
  fireworks: config => fireworksModelManagerOptions(config),
@@ -116,6 +119,7 @@ const MODEL_MANAGER_FACTORIES: Readonly<Partial<Record<KnownProvider, ModelManag
116
119
  google: config => googleModelManagerOptions(config),
117
120
  "google-vertex": config => googleVertexModelManagerOptions(config),
118
121
  groq: config => groqModelManagerOptions(config),
122
+ helmcode: config => helmcodeModelManagerOptions(config),
119
123
  huggingface: config => huggingfaceModelManagerOptions(config),
120
124
  kilo: config => kiloModelManagerOptions(config),
121
125
  "kimi-code": config => kimiCodeModelManagerOptions(config),
@@ -1257,6 +1257,81 @@ export function huggingfaceModelManagerOptions(
1257
1257
  return createSimpleOpenAICompletionsOptions("huggingface", "https://router.huggingface.co/v1", config);
1258
1258
  }
1259
1259
 
1260
+ // ---------------------------------------------------------------------------
1261
+ // 4.5 Helmcode
1262
+ // ---------------------------------------------------------------------------
1263
+
1264
+ export interface HelmcodeModelManagerConfig {
1265
+ apiKey?: string;
1266
+ baseUrl?: string;
1267
+ fetch?: FetchImpl;
1268
+ }
1269
+
1270
+ /**
1271
+ * First-party hosts of the models Helmcode resells (helmcode.com/docs/models,
1272
+ * "Frontier models"). Resold ids resolve only against these rows: the global
1273
+ * bare-id index picks whichever gateway row wins a context/output tie, which
1274
+ * can carry a zero or marked-up price instead of the vendor list price.
1275
+ */
1276
+ const HELMCODE_RESOLD_VENDORS = ["anthropic", "openai", "google"] as const satisfies readonly GeneratedProvider[];
1277
+
1278
+ function createHelmcodeVendorReferenceMap(): Map<string, ModelSpec<"openai-completions">> {
1279
+ const references = new Map<string, ModelSpec<"openai-completions">>();
1280
+ for (const vendor of HELMCODE_RESOLD_VENDORS) {
1281
+ for (const [id, reference] of createBundledReferenceMap<"openai-completions">(vendor)) {
1282
+ if (!references.has(id)) references.set(id, reference);
1283
+ }
1284
+ }
1285
+ return references;
1286
+ }
1287
+
1288
+ /**
1289
+ * Helmcode model manager: OpenAI-compatible chat completions at
1290
+ * `api.helmcode.com/v1`. `/v1/models` also lists embedding, rerank, TTS, and
1291
+ * STT models; the exclusion policy lives in `runtime/behavior.kdl`
1292
+ * (`exclude-models provider="helmcode"`).
1293
+ *
1294
+ * `/v1/models` carries no capability data. Resold frontier ids (Claude, GPT,
1295
+ * Gemini) take only capability facts from the first-party vendor's bundled
1296
+ * row: reasoning, modalities, context window, output cap, and list price. The
1297
+ * rest of that row (thinking shape, compat, native web search, tool dialects,
1298
+ * cache semantics) describes the vendor's own API, not this chat-completions
1299
+ * proxy; the host's `reasoning_effort` ladders and cache-write pricing live in
1300
+ * `providers/helmcode.kdl`. Ids with no Helmcode or vendor row (e.g. a new
1301
+ * open-weight model) inherit nothing from other gateways.
1302
+ */
1303
+ export function helmcodeModelManagerOptions(
1304
+ config?: HelmcodeModelManagerConfig,
1305
+ ): ModelManagerOptions<"openai-completions"> {
1306
+ let vendorReferences: Map<string, ModelSpec<"openai-completions">> | undefined;
1307
+ const resolveVendorReference = (id: string) => (vendorReferences ??= createHelmcodeVendorReferenceMap()).get(id);
1308
+ return createOpenAICompatibleModelManagerOptions({
1309
+ api: "openai-completions",
1310
+ providerId: "helmcode",
1311
+ defaultBaseUrl: "https://api.helmcode.com/v1",
1312
+ config,
1313
+ requireApiKey: true,
1314
+ filterModel: (_entry, model) => !isExcludedModel("helmcode", model.id),
1315
+ mapModel: (entry, defaults, helmcodeReference) => {
1316
+ if (helmcodeReference) return mapWithBundledReference(entry, defaults, helmcodeReference);
1317
+ const vendor = resolveVendorReference(defaults.id);
1318
+ if (!vendor) return mapWithBundledReference(entry, defaults, undefined);
1319
+ return {
1320
+ ...defaults,
1321
+ name: toModelName(entry.name, vendor.name),
1322
+ reasoning: vendor.reasoning,
1323
+ input: vendor.input,
1324
+ cost: vendor.cost,
1325
+ contextWindow: toPositiveNumber(entry.context_length, vendor.contextWindow),
1326
+ maxTokens: toPositiveNumber(entry.max_completion_tokens, vendor.maxTokens),
1327
+ };
1328
+ },
1329
+ // Must live on the manager options, not only the KDL descriptor:
1330
+ // `createModelManager()` prunes the bundled slice from this flag.
1331
+ dynamicModelsAuthoritative: true,
1332
+ });
1333
+ }
1334
+
1260
1335
  // ---------------------------------------------------------------------------
1261
1336
  // 5. NVIDIA
1262
1337
  // ---------------------------------------------------------------------------
@@ -3277,12 +3352,8 @@ export function openrouterModelManagerOptions(config?: OpenRouterModelManagerCon
3277
3352
  cacheRead: parseFloat(String(pricing?.input_cache_read ?? "0")) * 1_000_000,
3278
3353
  cacheWrite: parseFloat(String(pricing?.input_cache_write ?? "0")) * 1_000_000,
3279
3354
  },
3280
- contextWindow:
3281
- typeof entry.context_length === "number" ? entry.context_length : baseModel.contextWindow,
3282
- maxTokens:
3283
- typeof topProvider?.max_completion_tokens === "number"
3284
- ? topProvider.max_completion_tokens
3285
- : baseModel.maxTokens,
3355
+ contextWindow: toPositiveNumber(entry.context_length, baseModel.contextWindow),
3356
+ maxTokens: toPositiveNumber(topProvider?.max_completion_tokens, baseModel.maxTokens),
3286
3357
  ...(!supportsToolChoice && {
3287
3358
  compat: { ...baseModel.compat, supportsToolChoice: false },
3288
3359
  }),
@@ -3346,11 +3417,9 @@ export function openrouterModelManagerOptions(config?: OpenRouterModelManagerCon
3346
3417
  cacheRead: 0,
3347
3418
  cacheWrite: 0,
3348
3419
  },
3349
- contextWindow: typeof entry.context_length === "number" ? entry.context_length : null,
3350
- maxTokens:
3351
- typeof topProvider?.max_completion_tokens === "number"
3352
- ? topProvider.max_completion_tokens
3353
- : null,
3420
+ // Some rows (e.g. respan/span-01) advertise `0` for unknown limits.
3421
+ contextWindow: toPositiveNumber(entry.context_length, null),
3422
+ maxTokens: toPositiveNumber(topProvider?.max_completion_tokens, null),
3354
3423
  };
3355
3424
  },
3356
3425
  fetch: config?.fetch,
@@ -3382,11 +3451,8 @@ export function openrouterModelManagerOptions(config?: OpenRouterModelManagerCon
3382
3451
  supportsTools: false,
3383
3452
  // OpenRouter bills reranking per search; ModelCost has no search-unit axis.
3384
3453
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
3385
- contextWindow: typeof entry.context_length === "number" ? entry.context_length : null,
3386
- maxTokens:
3387
- typeof topProvider?.max_completion_tokens === "number"
3388
- ? topProvider.max_completion_tokens
3389
- : null,
3454
+ contextWindow: toPositiveNumber(entry.context_length, null),
3455
+ maxTokens: toPositiveNumber(topProvider?.max_completion_tokens, null),
3390
3456
  };
3391
3457
  },
3392
3458
  fetch: config?.fetch,
@@ -3430,7 +3496,7 @@ export function openrouterModelManagerOptions(config?: OpenRouterModelManagerCon
3430
3496
  cacheRead: 0,
3431
3497
  cacheWrite: 0,
3432
3498
  },
3433
- contextWindow: typeof entry.context_length === "number" ? entry.context_length : null,
3499
+ contextWindow: toPositiveNumber(entry.context_length, null),
3434
3500
  maxTokens: null,
3435
3501
  };
3436
3502
  },
@@ -4,6 +4,11 @@ import { apiRouteFor } from "../compat/behavior";
4
4
  import { seedModels } from "../compat/providers";
5
5
  import { type CodexModelDiscoveryResult, fetchCodexModels } from "../discovery/codex";
6
6
  import type { DevinModelDiscoveryOptions } from "../discovery/devin";
7
+ import {
8
+ type FactoryDroidModelDiscoveryOptions,
9
+ factoryDroidSeedModels,
10
+ fetchFactoryDroidModels,
11
+ } from "../discovery/factory-droid";
7
12
  import { fetchTypeSafeModels, TYPESAFE_DEFAULT_BASE_URL } from "../discovery/typesafe";
8
13
  import { buildGitLabDuoWorkflowFallbackModel, fetchGitLabDuoWorkflowModels } from "../discovery/gitlab-duo-workflow";
9
14
  import type { ModelManagerOptions } from "../model-manager";
@@ -131,7 +136,8 @@ export function cursorModelManagerOptions(config: CursorModelManagerConfig = {})
131
136
  const { apiKey, baseUrl, clientVersion } = config;
132
137
  return {
133
138
  providerId: "cursor",
134
- cacheProviderId: resolveModelCacheProviderId("cursor"),
139
+ dynamicModelsAuthoritative: true,
140
+ cacheProviderId: resolveModelCacheProviderId("cursor", { apiKey, baseUrl }),
135
141
  ...(apiKey
136
142
  ? {
137
143
  fetchDynamicModels: async () => {
@@ -365,6 +371,25 @@ export function devinModelManagerOptions(config: DevinModelManagerConfig = {}):
365
371
 
366
372
  const devinDiscovery = once(() => import("../discovery/devin"));
367
373
 
374
+ // ---------------------------------------------------------------------------
375
+ // Factory Droid
376
+ // ---------------------------------------------------------------------------
377
+
378
+ export function factoryDroidModelManagerOptions(
379
+ config: FactoryDroidModelDiscoveryOptions = {},
380
+ ): ModelManagerOptions<"factory-droid-agent"> {
381
+ return {
382
+ providerId: "factory-droid",
383
+ cacheProviderId: resolveModelCacheProviderId("factory-droid", config),
384
+ // No model-listing endpoint exists; the registry narrowed offline is the seed.
385
+ staticModels: factoryDroidSeedModels(config),
386
+ dynamicModelsAuthoritative: true,
387
+ fetchDynamicModels: () => fetchFactoryDroidModels(config),
388
+ // Refresh current policy online; cached eligibility is not current entitlement.
389
+ alwaysRefetchDynamicModels: true,
390
+ };
391
+ }
392
+
368
393
  // ---------------------------------------------------------------------------
369
394
  // Synthetic role providers
370
395
  // ---------------------------------------------------------------------------
@@ -415,7 +440,6 @@ export function typesafeModelManagerOptions(config: TypeSafeModelManagerConfig =
415
440
  : undefined),
416
441
  };
417
442
  }
418
-
419
443
  // ---------------------------------------------------------------------------
420
444
  // Zai
421
445
  // ---------------------------------------------------------------------------
package/src/types.ts CHANGED
@@ -19,6 +19,7 @@ export type KnownApi =
19
19
  | "google-vertex"
20
20
  | "ollama-chat"
21
21
  | "cursor-agent"
22
+ | "factory-droid-agent"
22
23
  | "gitlab-duo-agent"
23
24
  | "devin-agent"
24
25
  | "apple-foundation-models";
@@ -284,12 +285,21 @@ export interface OpenAICompat {
284
285
  thinkingKeep?: "all" | false;
285
286
  /** Which reasoning content field to emit on assistant messages. Default: auto-detected. */
286
287
  reasoningContentField?: "reasoning_content" | "reasoning" | "reasoning_text";
288
+ /** Decode Mistral thinking parts in streamed content and replay them as typed assistant content. */
289
+ mistralReasoningContentParts?: boolean;
287
290
  /** Whether assistant tool-call messages must include reasoning content. Default: false. */
288
291
  requiresReasoningContentForToolCalls?: boolean;
289
292
  /** Whether all assistant messages must include reasoning content. Default: false. */
290
293
  requiresReasoningContentForAllAssistantTurns?: boolean;
291
294
  /** Whether the provider accepts a synthetic placeholder (e.g. ".") for missing reasoning_content on tool-call turns. Default: true. Set to false for providers like DeepSeek that validate the exact reasoning_content value. */
292
295
  allowsSyntheticReasoningContentForToolCalls?: boolean;
296
+ /**
297
+ * Value emitted for the reasoning field on tool-call turns when the provider
298
+ * requires it (`requiresReasoningContentForToolCalls`) but no reasoning was
299
+ * captured. Default: "". Some upstreams validate the exact value — the droid
300
+ * proxy's DeepSeek family requires a single space.
301
+ */
302
+ syntheticReasoningContentFallback?: string;
293
303
  /**
294
304
  * Replay preserved thinking blocks as `reasoning_content` (or the configured
295
305
  * `reasoningContentField`) on EVERY assistant turn that carried reasoning,
@@ -673,6 +683,25 @@ export interface AnthropicCompat {
673
683
  stripImageInput?: boolean;
674
684
  /** Thinking-loop watchdog guard family applied to streamed reasoning. */
675
685
  thinkingLoopGuard?: "gemini" | "deepseek" | "xai";
686
+ /**
687
+ * Drop the enabled `thinking` config and replayed thinking blocks when the
688
+ * conversation is not thinking-led (a proxy contract for non-interleaved
689
+ * budget models). Default: false.
690
+ */
691
+ stripThinkingHistory?: boolean;
692
+ /**
693
+ * Send the `effort-2025-11-24` beta exactly when `output_config.effort`
694
+ * rides the request. Unset keeps the transport's legacy heuristic.
695
+ */
696
+ effortBeta?: boolean;
697
+ /**
698
+ * Wire form of a disabled-thinking turn: omit the field, send
699
+ * `{ type: "disabled" }`, or keep adaptive thinking. Unset keeps the
700
+ * direct-provider behavior.
701
+ */
702
+ disabledThinking?: "omit" | "disabled" | "adaptive";
703
+ /** The model is a fast-mode SKU: send `speed: "fast"` with the fast-mode beta. Default: false. */
704
+ fastMode?: boolean;
676
705
  }
677
706
 
678
707
  /**
@@ -703,6 +732,12 @@ export interface BedrockCompat {
703
732
  * `PI_OPENAI_STREAM_IDLE_TIMEOUT_MS` alias, then the 300s default.
704
733
  */
705
734
  streamIdleTimeoutMs?: number;
735
+ /**
736
+ * Whether the model accepts a forced `toolChoice` (`any` / `tool`). Claude
737
+ * Opus/Sonnet 5.5 reject it outright; the request builder downgrades forced
738
+ * choices to `auto` when this is false. Default: true.
739
+ */
740
+ supportsForcedToolChoice?: boolean;
706
741
  }
707
742
 
708
743
  /** Fully-resolved Bedrock Converse prompt-cache capabilities, materialized once by `buildModel`. */
@@ -713,6 +748,7 @@ export interface ResolvedBedrockCompat {
713
748
  supportsLongPromptCacheRetention: boolean;
714
749
  promptCacheMinimumTokens: number;
715
750
  promptCacheMaximumCheckpoints: number;
751
+ supportsForcedToolChoice: boolean;
716
752
  /**
717
753
  * Stream-watchdog idle-timeout fallback in ms for hosts with no keepalive
718
754
  * events; 0 disables the idle watchdog. Undefined defers to
@@ -780,9 +816,12 @@ export interface ResolvedOpenAISharedCompat {
780
816
  supportsForcedToolChoice: boolean;
781
817
  supportsNamedToolChoice: boolean;
782
818
  reasoningContentField?: OpenAICompat["reasoningContentField"];
819
+ mistralReasoningContentParts?: boolean;
783
820
  requiresReasoningContentForToolCalls: boolean;
784
821
  requiresReasoningContentForAllAssistantTurns: boolean;
785
822
  allowsSyntheticReasoningContentForToolCalls: boolean;
823
+ /** See {@link OpenAICompat.syntheticReasoningContentFallback}. */
824
+ syntheticReasoningContentFallback?: string;
786
825
  replayReasoningContent: boolean;
787
826
  qwenPreserveThinking: boolean;
788
827
  qwenTemplateReasoningEffort: boolean;
@@ -860,9 +899,11 @@ export type ResolvedOpenAICompat = ResolvedOpenAISharedCompat &
860
899
  | "supportsForcedToolChoice"
861
900
  | "supportsNamedToolChoice"
862
901
  | "reasoningContentField"
902
+ | "mistralReasoningContentParts"
863
903
  | "requiresReasoningContentForToolCalls"
864
904
  | "requiresReasoningContentForAllAssistantTurns"
865
905
  | "allowsSyntheticReasoningContentForToolCalls"
906
+ | "syntheticReasoningContentFallback"
866
907
  | "replayReasoningContent"
867
908
  | "qwenPreserveThinking"
868
909
  | "qwenTemplateReasoningEffort"
@@ -977,8 +1018,25 @@ export type ResolvedOpenRouterCompat = ResolvedOpenAICompat & ResolvedOpenAIResp
977
1018
 
978
1019
  /** Fully-resolved anthropic-messages compat view (same contract as `ResolvedOpenAICompat`). */
979
1020
  export type ResolvedAnthropicCompat = Required<
980
- Omit<AnthropicCompat, "streamIdleTimeoutMs" | "thinkingLoopGuard" | "bedrockMessagesApi">
1021
+ Omit<
1022
+ AnthropicCompat,
1023
+ | "streamIdleTimeoutMs"
1024
+ | "thinkingLoopGuard"
1025
+ | "bedrockMessagesApi"
1026
+ | "effortBeta"
1027
+ | "disabledThinking"
1028
+ | "stripThinkingHistory"
1029
+ | "fastMode"
1030
+ >
981
1031
  > & {
1032
+ /** Effort-beta override; undefined keeps the transport's legacy heuristic. */
1033
+ effortBeta?: AnthropicCompat["effortBeta"];
1034
+ /** Disabled-thinking wire form; undefined keeps the direct-provider behavior. */
1035
+ disabledThinking?: AnthropicCompat["disabledThinking"];
1036
+ /** Strip thinking history on non-thinking-led turns; undefined behaves as false. */
1037
+ stripThinkingHistory?: AnthropicCompat["stripThinkingHistory"];
1038
+ /** Fast-mode SKU; undefined behaves as false. */
1039
+ fastMode?: AnthropicCompat["fastMode"];
982
1040
  /** Thinking-loop watchdog guard family applied to streamed reasoning. */
983
1041
  thinkingLoopGuard?: AnthropicCompat["thinkingLoopGuard"];
984
1042
  /**
@@ -1145,6 +1203,9 @@ export interface TokenCost {
1145
1203
  cacheWrite: number;
1146
1204
  }
1147
1205
 
1206
+ /** User-facing interpretation of a model's token-rate card. */
1207
+ export type ModelPricingStatus = "fixed" | "free" | "included" | "variable" | "unknown";
1208
+
1148
1209
  /**
1149
1210
  * Rates applied to the full request when its prompt exceeds `inputThreshold`,
1150
1211
  * or reaches it when `inputThresholdInclusive` is true. Prompt input is the
@@ -1218,6 +1279,18 @@ export interface ModelAccountAccess {
1218
1279
  cyberPrograms?: readonly string[];
1219
1280
  }
1220
1281
 
1282
+ /** One Cursor `RequestedModel.parameters` entry recovered from rich discovery. */
1283
+ export interface CursorModelParameter {
1284
+ id: string;
1285
+ value: string;
1286
+ }
1287
+
1288
+ /** Cursor wire target selected after local effort/variant routing. */
1289
+ export interface CursorModelRoute {
1290
+ modelId: string;
1291
+ parameters: readonly CursorModelParameter[];
1292
+ maxMode?: boolean;
1293
+ }
1221
1294
  // Model interface for the unified model system
1222
1295
  export interface Model<TApi extends Api = Api> {
1223
1296
  id: string;
@@ -1327,6 +1400,29 @@ export interface Model<TApi extends Api = Api> {
1327
1400
  * bundled/config rows and on single-account discovery.
1328
1401
  */
1329
1402
  accountAccess?: Readonly<Record<string, ModelAccountAccess>>;
1403
+ /** Cursor `RequestedModel.parameters` for this model's default variant. */
1404
+ cursorModelParameters?: readonly CursorModelParameter[];
1405
+ /**
1406
+ * Per-wire-id Cursor routes recovered from `AvailableModels`. Generic effort
1407
+ * collapse retains these while routing selects one key at request time.
1408
+ */
1409
+ cursorModelRoutes?: Readonly<Record<string, CursorModelRoute>>;
1410
+ /** Cursor's account-relative model price/multiplier; not a per-token USD rate. */
1411
+ cursorPrice?: number;
1412
+ /** Cursor requires data retention to invoke this model. */
1413
+ cursorRequiresDataRetention?: boolean;
1414
+ /** Cursor explicitly reports support for its Agent surface. */
1415
+ cursorSupportsAgent?: boolean;
1416
+ /** Cursor explicitly reports support for sandboxed execution. */
1417
+ cursorSupportsSandboxing?: boolean;
1418
+ /** Factory Droid: account-resolved upstream rotation (first entry is the default `x-api-provider`). */
1419
+ factoryDroidApiProviders?: string[];
1420
+ /** Factory Droid: `configured_order` when live routing chose the rotation; absent means registry order. */
1421
+ factoryDroidRoutingSource?: "configured_order";
1422
+ /** Factory Droid Standard Credits base rate (relative per-token weight, not dollars). */
1423
+ factoryDroidCredits?: number;
1424
+ /** Canonical organization resolved during Factory discovery. */
1425
+ factoryDroidOrgId?: string;
1330
1426
  cost: ModelCost;
1331
1427
  /**
1332
1428
  * Prompt-cache entry lifetime per retention tier, in seconds. Populated only
@@ -1336,9 +1432,22 @@ export interface Model<TApi extends Api = Api> {
1336
1432
  * Custom models and provider overrides opt in via models.yml `promptCache`.
1337
1433
  */
1338
1434
  promptCache?: ModelPromptCache;
1435
+ /**
1436
+ * Verbatim configured lifetimes (models.yml, `modelOverrides`, runtime
1437
+ * registrations). `buildModel` applies them over catalog `prompt-cache`
1438
+ * rules on every rebuild; `{}` keeps warming disabled.
1439
+ */
1440
+ promptCacheConfig?: ModelPromptCache;
1441
+ /**
1442
+ * Interpretation of an all-zero token-rate card. Omitted zero-rate cards
1443
+ * are unknown; any non-zero rate is always treated as fixed pricing.
1444
+ */
1445
+ pricingStatus?: Exclude<ModelPricingStatus, "fixed">;
1339
1446
  /** Premium Copilot requests charged per user-initiated request (defaults to 1). */
1340
1447
  premiumMultiplier?: number;
1341
1448
  contextWindow: number | null;
1449
+ /** Preserve the host's supplied window instead of applying inferred expansion or reference-price caps. */
1450
+ contextWindowAuthoritative?: boolean;
1342
1451
  /** Optional larger prompt window available when extended context is enabled. */
1343
1452
  maxContextWindow?: number;
1344
1453
  maxTokens: number | null;
@@ -1411,6 +1520,8 @@ export interface Model<TApi extends Api = Api> {
1411
1520
  isNew?: boolean;
1412
1521
  /** Upstream marks this model as beta / preview quality. */
1413
1522
  isBeta?: boolean;
1523
+ /** Authenticated provider catalog marks this as the account's default model. */
1524
+ isProviderDefault?: boolean;
1414
1525
  /** Upstream marks this model as one of its recommended picks. */
1415
1526
  isRecommended?: boolean;
1416
1527
  /** Canonical thinking capability metadata for this model. */
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Cursor wire constants shared by authenticated catalog discovery, the pi-ai
3
+ * provider, and account usage. Keep the announced client build aligned with the
4
+ * released Cursor agent CLI because the service gates protocol features on it.
5
+ */
6
+
7
+ /** Default host for Cursor's Connect RPCs and account API. */
8
+ export const CURSOR_DEFAULT_BASE_URL = "https://api2.cursor.sh";
9
+
10
+ /** Released Cursor agent CLI build whose protocol surface this client mirrors. */
11
+ export const CURSOR_CLIENT_VERSION = "cli-2026.09.02-c22c1a3";
12
+
13
+ export const CURSOR_RUN_PATH = "/agent.v1.AgentService/Run";
14
+ export const CURSOR_RUN_SSE_PATH = "/agent.v1.AgentService/RunSSE";
15
+ export const CURSOR_BIDI_APPEND_PATH = "/aiserver.v1.BidiService/BidiAppend";
16
+ export const CURSOR_GET_USABLE_MODELS_PATH = "/agent.v1.AgentService/GetUsableModels";
17
+ export const CURSOR_GET_DEFAULT_MODEL_PATH = "/agent.v1.AgentService/GetDefaultModelForCli";
18
+ export const CURSOR_AVAILABLE_MODELS_PATH = "/aiserver.v1.AiService/AvailableModels";
19
+
20
+ /** Headers shared by Cursor's authenticated CLI RPCs. */
21
+ export function cursorClientHeaders(
22
+ apiKey: string,
23
+ options: { clientVersion?: string; contentType?: "application/proto" | "application/connect+proto" } = {},
24
+ ): Record<string, string> {
25
+ return {
26
+ "content-type": options.contentType ?? "application/proto",
27
+ authorization: `Bearer ${apiKey}`,
28
+ "x-cursor-client-type": "cli",
29
+ "x-cursor-client-version": options.clientVersion ?? CURSOR_CLIENT_VERSION,
30
+ "x-ghost-mode": "true",
31
+ };
32
+ }