shariq-pi-extensions 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 (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +94 -0
  3. package/THIRD_PARTY_NOTICES.md +11 -0
  4. package/docs/ARCHITECTURE.md +54 -0
  5. package/docs/DEVELOPMENT.md +88 -0
  6. package/docs/EXTENSIONS.md +89 -0
  7. package/extensions/antigravity-provider/LICENSE +21 -0
  8. package/extensions/antigravity-provider/README.md +23 -0
  9. package/extensions/antigravity-provider/antigravity/cloud-code-assist.ts +519 -0
  10. package/extensions/antigravity-provider/antigravity/index.ts +67 -0
  11. package/extensions/antigravity-provider/antigravity/models.ts +184 -0
  12. package/extensions/antigravity-provider/antigravity/oauth.ts +499 -0
  13. package/extensions/antigravity-provider/index.ts +3 -0
  14. package/extensions/ask-user/README.md +21 -0
  15. package/extensions/ask-user/index.ts +417 -0
  16. package/extensions/background-terminals/README.md +43 -0
  17. package/extensions/background-terminals/index.ts +400 -0
  18. package/extensions/background-terminals/src/manager.ts +480 -0
  19. package/extensions/background-terminals/src/output-buffer.ts +110 -0
  20. package/extensions/background-terminals/src/presentation.ts +74 -0
  21. package/extensions/background-terminals/src/types.ts +47 -0
  22. package/extensions/background-terminals/src/ui.ts +478 -0
  23. package/extensions/context-usage/LICENSE +21 -0
  24. package/extensions/context-usage/README.md +20 -0
  25. package/extensions/context-usage/index.ts +2006 -0
  26. package/extensions/context-usage/lib/ansi.ts +60 -0
  27. package/extensions/context-usage/lib/boundary.ts +27 -0
  28. package/extensions/context-usage/lib/chat.ts +190 -0
  29. package/extensions/context-usage/lib/config.ts +30 -0
  30. package/extensions/context-usage/lib/fmt.ts +59 -0
  31. package/extensions/context-usage/lib/style.ts +260 -0
  32. package/extensions/copy-all/README.md +15 -0
  33. package/extensions/copy-all/index.ts +72 -0
  34. package/extensions/factory-provider/API_KEYS.md +17 -0
  35. package/extensions/factory-provider/AUTH.md +15 -0
  36. package/extensions/factory-provider/README.md +90 -0
  37. package/extensions/factory-provider/REFRESH_MODELS.md +185 -0
  38. package/extensions/factory-provider/UPDATING.md +97 -0
  39. package/extensions/factory-provider/factory/api-keys.ts +407 -0
  40. package/extensions/factory-provider/factory/auth.ts +132 -0
  41. package/extensions/factory-provider/factory/constants.ts +15 -0
  42. package/extensions/factory-provider/factory/dashboard.ts +363 -0
  43. package/extensions/factory-provider/factory/droid.ts +84 -0
  44. package/extensions/factory-provider/factory/limits.ts +342 -0
  45. package/extensions/factory-provider/factory/models.ts +283 -0
  46. package/extensions/factory-provider/factory/responses.ts +331 -0
  47. package/extensions/factory-provider/factory/warm-theme.ts +53 -0
  48. package/extensions/factory-provider/factory/websocket.ts +130 -0
  49. package/extensions/factory-provider/index.ts +204 -0
  50. package/extensions/factory-provider/scripts/audit-reasoning-efforts.py +84 -0
  51. package/extensions/firecrawl-web/README.md +31 -0
  52. package/extensions/firecrawl-web/auth.ts +92 -0
  53. package/extensions/firecrawl-web/client.ts +238 -0
  54. package/extensions/firecrawl-web/index.ts +96 -0
  55. package/extensions/firecrawl-web/output.ts +125 -0
  56. package/extensions/git-info/README.md +25 -0
  57. package/extensions/git-info/index.ts +162 -0
  58. package/extensions/git-info/src/git.ts +206 -0
  59. package/extensions/git-info/src/ui.ts +291 -0
  60. package/extensions/goal/README.md +44 -0
  61. package/extensions/goal/constants.ts +4 -0
  62. package/extensions/goal/extension.ts +1064 -0
  63. package/extensions/goal/index.ts +2 -0
  64. package/extensions/goal/types.ts +56 -0
  65. package/extensions/goal/ui.ts +334 -0
  66. package/extensions/orchestration/README.md +58 -0
  67. package/extensions/orchestration/dashboard.ts +192 -0
  68. package/extensions/orchestration/engine.ts +706 -0
  69. package/extensions/orchestration/index.ts +243 -0
  70. package/extensions/orchestration/prompts.ts +124 -0
  71. package/extensions/orchestration/protocol.ts +116 -0
  72. package/extensions/orchestration/settings-ui.ts +41 -0
  73. package/extensions/orchestration/settings.ts +59 -0
  74. package/extensions/orchestration/storage.ts +49 -0
  75. package/extensions/orchestration/types.ts +105 -0
  76. package/extensions/performance-status/README.md +5 -0
  77. package/extensions/performance-status/index.ts +171 -0
  78. package/extensions/pi-memory/README.md +53 -0
  79. package/extensions/pi-memory/index.ts +326 -0
  80. package/extensions/pi-memory/src/config.ts +75 -0
  81. package/extensions/pi-memory/src/database.ts +617 -0
  82. package/extensions/pi-memory/src/extraction.ts +178 -0
  83. package/extensions/pi-memory/src/project.ts +80 -0
  84. package/extensions/pi-memory/src/projection.ts +59 -0
  85. package/extensions/pi-memory/src/redaction.ts +60 -0
  86. package/extensions/pi-memory/src/retrieval.ts +30 -0
  87. package/extensions/pi-memory/src/service.ts +194 -0
  88. package/extensions/pi-memory/src/session.ts +146 -0
  89. package/extensions/pi-memory/src/types.ts +82 -0
  90. package/extensions/shared/README.md +18 -0
  91. package/extensions/shared/activity-status.ts +31 -0
  92. package/extensions/shared/child-session.ts +150 -0
  93. package/extensions/shared/context-utilization.ts +47 -0
  94. package/extensions/shared/dashboard-state.ts +99 -0
  95. package/extensions/shared/tool-call-timeout.ts +104 -0
  96. package/extensions/shared/tui-dashboard.ts +139 -0
  97. package/extensions/shell-shortcuts/README.md +17 -0
  98. package/extensions/shell-shortcuts/features/exit-alias.ts +10 -0
  99. package/extensions/shell-shortcuts/index.ts +6 -0
  100. package/extensions/subagents/README.md +89 -0
  101. package/extensions/subagents/index.ts +1386 -0
  102. package/extensions/subagents/src/backend.ts +69 -0
  103. package/extensions/subagents/src/backends/pi.ts +781 -0
  104. package/extensions/subagents/src/catalog.ts +85 -0
  105. package/extensions/subagents/src/config.ts +241 -0
  106. package/extensions/subagents/src/context.ts +57 -0
  107. package/extensions/subagents/src/coordinator.ts +64 -0
  108. package/extensions/subagents/src/domain.ts +294 -0
  109. package/extensions/subagents/src/format.ts +74 -0
  110. package/extensions/subagents/src/manager.ts +813 -0
  111. package/extensions/subagents/src/prompt.ts +94 -0
  112. package/extensions/subagents/src/result-delivery.ts +20 -0
  113. package/extensions/subagents/src/runtime.ts +48 -0
  114. package/extensions/subagents/src/ui/takeover.ts +695 -0
  115. package/extensions/subagents/src/ui/transcript.ts +182 -0
  116. package/extensions/subagents/src/worktree.ts +175 -0
  117. package/extensions/web-fetch/README.md +13 -0
  118. package/extensions/web-fetch/index.ts +441 -0
  119. package/package.json +117 -0
  120. package/skills/background-terminals/SKILL.md +42 -0
  121. package/skills/orchestration/SKILL.md +33 -0
  122. package/skills/subagents/SKILL.md +89 -0
  123. package/themes/ember-warm-dark.json +85 -0
@@ -0,0 +1,342 @@
1
+ import { createHash } from "node:crypto";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import { FACTORY_API_BASE_URL, FACTORY_STATE_DIR } from "./constants.ts";
5
+
6
+ export const FACTORY_LIMITS_CACHE_PATH = path.join(
7
+ FACTORY_STATE_DIR,
8
+ "limits-cache.json",
9
+ );
10
+ export const FACTORY_LIMITS_TTL_MS = 15 * 60 * 1000;
11
+ const CACHE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
12
+ const REQUEST_TIMEOUT_MS = 10_000;
13
+
14
+ export const FACTORY_CORE_MODEL_IDS = new Set([
15
+ "glm-5.2",
16
+ "glm-5.2-fast",
17
+ "kimi-k3",
18
+ "kimi-k2.7-code",
19
+ "kimi-k2.6",
20
+ "nemotron-3-ultra",
21
+ "deepseek-v4-flash-0731",
22
+ "deepseek-v4-pro",
23
+ "minimax-m3",
24
+ "inkling",
25
+ ]);
26
+
27
+ export type FactoryBillingPool = "standard" | "core";
28
+
29
+ export interface FactoryLimitBucket {
30
+ usedPercent: number;
31
+ windowEnd?: string | null;
32
+ secondsRemaining?: number | null;
33
+ }
34
+
35
+ export interface FactoryPoolLimits {
36
+ fiveHour: FactoryLimitBucket;
37
+ weekly: FactoryLimitBucket;
38
+ monthly: FactoryLimitBucket;
39
+ }
40
+
41
+ export interface FactoryLimits {
42
+ standard?: FactoryPoolLimits;
43
+ core?: FactoryPoolLimits;
44
+ }
45
+
46
+ export interface FactoryLimitCredential {
47
+ label: string;
48
+ secret: string;
49
+ headers?: Record<string, string>;
50
+ }
51
+
52
+ export interface FactoryLimitRecord {
53
+ id: string;
54
+ label: string;
55
+ fetchedAt: number;
56
+ attemptedAt?: number;
57
+ limits?: FactoryLimits;
58
+ error?: string;
59
+ }
60
+
61
+ interface FactoryLimitCache {
62
+ version: 1;
63
+ records: FactoryLimitRecord[];
64
+ }
65
+
66
+ function finitePercent(value: unknown) {
67
+ return typeof value === "number" && Number.isFinite(value)
68
+ ? Math.max(0, Math.min(100, value))
69
+ : 0;
70
+ }
71
+
72
+ function bucket(value: unknown): FactoryLimitBucket {
73
+ const raw = value && typeof value === "object"
74
+ ? (value as Record<string, unknown>)
75
+ : {};
76
+ return {
77
+ usedPercent: finitePercent(raw.usedPercent),
78
+ windowEnd: typeof raw.windowEnd === "string" ? raw.windowEnd : null,
79
+ secondsRemaining:
80
+ typeof raw.secondsRemaining === "number" &&
81
+ Number.isFinite(raw.secondsRemaining)
82
+ ? Math.max(0, raw.secondsRemaining)
83
+ : null,
84
+ };
85
+ }
86
+
87
+ function pool(value: unknown): FactoryPoolLimits | undefined {
88
+ if (!value || typeof value !== "object") return undefined;
89
+ const raw = value as Record<string, unknown>;
90
+ return {
91
+ fiveHour: bucket(raw.fiveHour),
92
+ weekly: bucket(raw.weekly),
93
+ monthly: bucket(raw.monthly),
94
+ };
95
+ }
96
+
97
+ function parseLimits(value: unknown): FactoryLimits {
98
+ const raw = value && typeof value === "object"
99
+ ? (value as Record<string, unknown>)
100
+ : {};
101
+ const limits = raw.limits && typeof raw.limits === "object"
102
+ ? (raw.limits as Record<string, unknown>)
103
+ : {};
104
+ return { standard: pool(limits.standard), core: pool(limits.core) };
105
+ }
106
+
107
+ export function factoryCredentialId(secret: string) {
108
+ return createHash("sha256").update(secret).digest("hex").slice(0, 20);
109
+ }
110
+
111
+ export function loadFactoryLimitCache(): FactoryLimitCache {
112
+ try {
113
+ const value = JSON.parse(
114
+ fs.readFileSync(FACTORY_LIMITS_CACHE_PATH, "utf8"),
115
+ ) as Partial<FactoryLimitCache>;
116
+ const records = Array.isArray(value.records)
117
+ ? value.records.filter(
118
+ (record): record is FactoryLimitRecord =>
119
+ !!record &&
120
+ typeof record.id === "string" &&
121
+ typeof record.label === "string" &&
122
+ typeof record.fetchedAt === "number",
123
+ )
124
+ : [];
125
+ return { version: 1, records };
126
+ } catch {
127
+ return { version: 1, records: [] };
128
+ }
129
+ }
130
+
131
+ function saveFactoryLimitCache(cache: FactoryLimitCache) {
132
+ fs.mkdirSync(path.dirname(FACTORY_LIMITS_CACHE_PATH), {
133
+ recursive: true,
134
+ mode: 0o700,
135
+ });
136
+ const temporary = `${FACTORY_LIMITS_CACHE_PATH}.${process.pid}.tmp`;
137
+ fs.writeFileSync(temporary, `${JSON.stringify(cache, null, 2)}\n`, {
138
+ mode: 0o600,
139
+ });
140
+ fs.renameSync(temporary, FACTORY_LIMITS_CACHE_PATH);
141
+ fs.chmodSync(FACTORY_LIMITS_CACHE_PATH, 0o600);
142
+ }
143
+
144
+ function bucketResetAt(
145
+ bucket: FactoryLimitBucket,
146
+ observedAt = Date.now(),
147
+ ) {
148
+ if (bucket.windowEnd) {
149
+ const end = Date.parse(bucket.windowEnd);
150
+ if (Number.isFinite(end)) return end;
151
+ }
152
+ if (typeof bucket.secondsRemaining === "number") {
153
+ return observedAt + bucket.secondsRemaining * 1000;
154
+ }
155
+ return undefined;
156
+ }
157
+
158
+ function active(
159
+ bucket: FactoryLimitBucket,
160
+ now = Date.now(),
161
+ observedAt = now,
162
+ ) {
163
+ const resetAt = bucketResetAt(bucket, observedAt);
164
+ return resetAt !== undefined && resetAt > now;
165
+ }
166
+
167
+ /** Parent windows dominate exhausted child windows: monthly → weekly → 5-hour. */
168
+ export function exhaustedBucket(
169
+ limits: FactoryPoolLimits | undefined,
170
+ now = Date.now(),
171
+ observedAt = now,
172
+ ): "monthly" | "weekly" | "fiveHour" | undefined {
173
+ if (!limits) return undefined;
174
+ if (active(limits.monthly, now, observedAt) && limits.monthly.usedPercent >= 100) {
175
+ return "monthly";
176
+ }
177
+ if (active(limits.weekly, now, observedAt) && limits.weekly.usedPercent >= 100) {
178
+ return "weekly";
179
+ }
180
+ if (active(limits.fiveHour, now, observedAt) && limits.fiveHour.usedPercent >= 100) {
181
+ return "fiveHour";
182
+ }
183
+ return undefined;
184
+ }
185
+
186
+ export function billingPoolForModel(modelId: string): FactoryBillingPool {
187
+ return FACTORY_CORE_MODEL_IDS.has(modelId) ? "core" : "standard";
188
+ }
189
+
190
+ export function cachedLimitDecision(
191
+ secret: string,
192
+ modelId: string,
193
+ now = Date.now(),
194
+ ) {
195
+ const record = loadFactoryLimitCache().records.find(
196
+ (item) => item.id === factoryCredentialId(secret),
197
+ );
198
+ if (!record?.limits) return undefined;
199
+ const poolName = billingPoolForModel(modelId);
200
+ const limits = record.limits[poolName];
201
+ const exhausted = exhaustedBucket(limits, now, record.fetchedAt);
202
+ if (exhausted && limits) {
203
+ return {
204
+ available: false as const,
205
+ pool: poolName,
206
+ exhausted,
207
+ resetAt: bucketResetAt(limits[exhausted], record.fetchedAt),
208
+ label: record.label,
209
+ };
210
+ }
211
+ if (now - record.fetchedAt > FACTORY_LIMITS_TTL_MS * 2) return undefined;
212
+ return { available: true as const, pool: poolName, label: record.label };
213
+ }
214
+
215
+ let refreshPromise: Promise<FactoryLimitRecord[]> | undefined;
216
+
217
+ export function refreshFactoryLimits(
218
+ credentials: ReadonlyArray<FactoryLimitCredential>,
219
+ options: { force?: boolean; version: string; signal?: AbortSignal },
220
+ ) {
221
+ if (refreshPromise) return refreshPromise;
222
+ refreshPromise = (async () => {
223
+ const now = Date.now();
224
+ const cache = loadFactoryLimitCache();
225
+ const unique = new Map(
226
+ credentials
227
+ .filter((credential) => credential.secret.trim())
228
+ .map((credential) => [factoryCredentialId(credential.secret), credential]),
229
+ );
230
+ const records: FactoryLimitRecord[] = [];
231
+ for (const [id, credential] of unique) {
232
+ const existing = cache.records.find((record) => record.id === id);
233
+ if (
234
+ !options.force &&
235
+ existing &&
236
+ now - (existing.attemptedAt ?? existing.fetchedAt) < FACTORY_LIMITS_TTL_MS
237
+ ) {
238
+ records.push({ ...existing, label: credential.label });
239
+ continue;
240
+ }
241
+ const timeout = AbortSignal.timeout(REQUEST_TIMEOUT_MS);
242
+ const signal = options.signal
243
+ ? AbortSignal.any([options.signal, timeout])
244
+ : timeout;
245
+ try {
246
+ const response = await fetch(
247
+ `${FACTORY_API_BASE_URL}/api/billing/limits`,
248
+ {
249
+ headers: {
250
+ Authorization: `Bearer ${credential.secret}`,
251
+ "X-Factory-Client": "cli",
252
+ "X-Client-Version": options.version,
253
+ ...(credential.headers ?? {}),
254
+ },
255
+ signal,
256
+ },
257
+ );
258
+ if (!response.ok) {
259
+ throw new Error(`HTTP ${response.status}`);
260
+ }
261
+ const body = await response.json();
262
+ records.push({
263
+ id,
264
+ label: credential.label,
265
+ fetchedAt: Date.now(),
266
+ attemptedAt: Date.now(),
267
+ limits: parseLimits(body),
268
+ });
269
+ } catch (error) {
270
+ records.push({
271
+ id,
272
+ label: credential.label,
273
+ fetchedAt: existing?.fetchedAt ?? Date.now(),
274
+ attemptedAt: Date.now(),
275
+ limits: existing?.limits,
276
+ error: error instanceof Error ? error.message.slice(0, 300) : String(error).slice(0, 300),
277
+ });
278
+ }
279
+ }
280
+ for (const record of cache.records) {
281
+ if (
282
+ unique.has(record.id) ||
283
+ now - record.fetchedAt > CACHE_MAX_AGE_MS
284
+ ) continue;
285
+ records.push(record);
286
+ }
287
+ saveFactoryLimitCache({ version: 1, records });
288
+ return records.filter((record) => unique.has(record.id));
289
+ })().finally(() => {
290
+ refreshPromise = undefined;
291
+ });
292
+ return refreshPromise;
293
+ }
294
+
295
+ function resetText(
296
+ bucket: FactoryLimitBucket,
297
+ now = Date.now(),
298
+ observedAt = now,
299
+ ) {
300
+ if (!active(bucket, now, observedAt)) return "reset";
301
+ const resetAt = bucketResetAt(bucket, observedAt);
302
+ const seconds = resetAt ? Math.max(0, Math.round((resetAt - now) / 1000)) : 0;
303
+ if (!seconds) return "reset soon";
304
+ const hours = Math.floor(seconds / 3600);
305
+ const minutes = Math.floor((seconds % 3600) / 60);
306
+ return hours ? `${hours}h${minutes ? ` ${minutes}m` : ""}` : `${minutes}m`;
307
+ }
308
+
309
+ export function formatPoolLimits(
310
+ name: "Standard" | "Core",
311
+ limits: FactoryPoolLimits | undefined,
312
+ now = Date.now(),
313
+ observedAt = now,
314
+ ) {
315
+ if (!limits) return `${name}: unavailable`;
316
+ const exhausted = exhaustedBucket(limits, now, observedAt);
317
+ if (exhausted === "monthly") {
318
+ return `${name} used: monthly 100% · resets ${resetText(limits.monthly, now, observedAt)} · weekly/5h inactive`;
319
+ }
320
+ if (exhausted === "weekly") {
321
+ return `${name} used: monthly ${limits.monthly.usedPercent}% · weekly 100% · resets ${resetText(limits.weekly, now, observedAt)} · 5h inactive`;
322
+ }
323
+ if (exhausted === "fiveHour") {
324
+ return `${name} used: monthly ${limits.monthly.usedPercent}% · weekly ${limits.weekly.usedPercent}% · 5h 100% · resets ${resetText(limits.fiveHour, now, observedAt)}`;
325
+ }
326
+ const parts = [
327
+ active(limits.monthly, now, observedAt) ? `M ${limits.monthly.usedPercent}%` : undefined,
328
+ active(limits.weekly, now, observedAt) ? `W ${limits.weekly.usedPercent}%` : undefined,
329
+ active(limits.fiveHour, now, observedAt) ? `5h ${limits.fiveHour.usedPercent}%` : undefined,
330
+ ].filter(Boolean);
331
+ return `${name} used: ${parts.join(" · ") || "fresh window"}`;
332
+ }
333
+
334
+ export function formatLimitRecord(record: FactoryLimitRecord, now = Date.now()) {
335
+ const ageMinutes = Math.max(0, Math.floor((now - record.fetchedAt) / 60_000));
336
+ const lines = [
337
+ `${record.label} · ${ageMinutes ? `${ageMinutes}m old` : "just refreshed"}${record.error ? ` · refresh error: ${record.error}` : ""}`,
338
+ ` ${formatPoolLimits("Standard", record.limits?.standard, now, record.fetchedAt)}`,
339
+ ` ${formatPoolLimits("Core", record.limits?.core, now, record.fetchedAt)}`,
340
+ ];
341
+ return lines;
342
+ }
@@ -0,0 +1,283 @@
1
+ import { FACTORY_API_BASE_URL } from "./constants.ts";
2
+ import { droidExecHelp } from "./droid.ts";
3
+ import {
4
+ billingPoolForModel,
5
+ type FactoryBillingPool,
6
+ } from "./limits.ts";
7
+
8
+ export type FactoryModel = {
9
+ id: string;
10
+ name: string;
11
+ reasoning: boolean;
12
+ supportedReasoningEfforts: string[];
13
+ defaultReasoningEffort?: string;
14
+ contextWindow: number;
15
+ maxTokens: number;
16
+ images: boolean;
17
+ pdf: boolean;
18
+ api: "openai-responses" | "openai-completions" | "anthropic-messages" | "google-generative-ai";
19
+ baseUrl: string;
20
+ apiProvider?: string;
21
+ billingPool: FactoryBillingPool;
22
+ };
23
+
24
+ type BinaryCapability = Omit<FactoryModel, "id" | "name" | "reasoning" | "supportedReasoningEfforts" | "defaultReasoningEffort" | "api" | "baseUrl" | "apiProvider" | "billingPool"> & Partial<Pick<FactoryModel, "api" | "baseUrl" | "apiProvider">>;
25
+
26
+ // Tracks the current Droid embedded registry; re-check the active binary after Droid upgrades.
27
+ // Re-check with UPDATING.md when Droid changes. Keep only the latest/current model per
28
+ // family to avoid cluttering Pi's model picker with older Factory models.
29
+ const BINARY_CAPABILITIES: Record<string, BinaryCapability> = {
30
+ "claude-opus-5": { contextWindow: 867_000, maxTokens: 128_000, images: true, pdf: true },
31
+ "claude-opus-5-fast": { contextWindow: 867_000, maxTokens: 128_000, images: true, pdf: true },
32
+ "claude-fable-5": { contextWindow: 867_000, maxTokens: 128_000, images: true, pdf: true },
33
+ "claude-sonnet-5": { contextWindow: 872_000, maxTokens: 128_000, images: true, pdf: true },
34
+ "claude-haiku-4-5-20251001": { contextWindow: 180_000, maxTokens: 32_000, images: true, pdf: true },
35
+ "gpt-5.6-sol": { contextWindow: 1_050_000, maxTokens: 128_000, images: true, pdf: true },
36
+ "gpt-5.6-terra": { contextWindow: 1_050_000, maxTokens: 128_000, images: true, pdf: true },
37
+ "gpt-5.6-luna": { contextWindow: 1_050_000, maxTokens: 128_000, images: true, pdf: true },
38
+ "gpt-5.5-pro": { contextWindow: 1_050_000, maxTokens: 128_000, images: true, pdf: true },
39
+ "gemini-3.1-pro-preview": { contextWindow: 1_000_000, maxTokens: 65_536, images: true, pdf: true },
40
+ "gemini-3.7-flash": { contextWindow: 1_000_000, maxTokens: 65_536, images: true, pdf: true },
41
+ "grok-4.5": { contextWindow: 200_000, maxTokens: 63_356, images: true, pdf: false },
42
+ "grok-4.6": { contextWindow: 200_000, maxTokens: 63_356, images: true, pdf: false },
43
+ "minimax-m3": { contextWindow: 512_000, maxTokens: 64_000, images: true, pdf: false },
44
+ "glm-5.2": { contextWindow: 1_040_000, maxTokens: 131_072, images: false, pdf: false },
45
+ "glm-5.2-fast": { contextWindow: 524_288, maxTokens: 131_072, images: false, pdf: false },
46
+ // Kimi documents a 1,048,576-token context and a 131,072-token default
47
+ // completion limit. Droid 0.200.0 still embeds a conservative 262,144/65,536
48
+ // proxy entry, so keep this explicit product-capability override.
49
+ "kimi-k3": { contextWindow: 1_048_576, maxTokens: 131_072, images: true, pdf: false },
50
+ "inkling": { contextWindow: 1_040_000, maxTokens: 32_768, images: true, pdf: false },
51
+ "nemotron-3-ultra": { contextWindow: 202_000, maxTokens: 65_536, images: false, pdf: false },
52
+ "deepseek-v4-flash-0731": { contextWindow: 1_040_000, maxTokens: 131_072, images: false, pdf: false },
53
+ "deepseek-v4-pro": { contextWindow: 1_040_000, maxTokens: 65_536, images: false, pdf: false },
54
+ };
55
+
56
+ const HIDDEN_MODEL_IDS = new Set([
57
+ "claude-haiku-4-5-20251001",
58
+ "claude-opus-5",
59
+ "gemini-3.1-pro-preview",
60
+ "glm-5.2-fast",
61
+ "inkling",
62
+ "minimax-m3",
63
+ "nemotron-3-ultra",
64
+ ]);
65
+
66
+ const ACTIVE_MODEL_IDS = new Set(Object.keys(BINARY_CAPABILITIES).filter((id) =>
67
+ !id.endsWith("-fast") && !HIDDEN_MODEL_IDS.has(id),
68
+ ));
69
+
70
+ // OpenRouter provider telemetry checked 2026-07-29. Prefer throughput and
71
+ // latency, with uptime and available context as reliability constraints.
72
+ const FACTORY_CORE_PROVIDER_OVERRIDES: Record<string, "fireworks" | "baseten"> = {
73
+ "kimi-k3": "fireworks",
74
+ "glm-5.2": "baseten",
75
+ "glm-5.2-fast": "fireworks",
76
+ "inkling": "fireworks",
77
+ "deepseek-v4-flash-0731": "fireworks",
78
+ "deepseek-v4-pro": "fireworks",
79
+ "nemotron-3-ultra": "baseten",
80
+ };
81
+
82
+ const FACTORY_OPENAI_BASE_URL = `${FACTORY_API_BASE_URL}/api/llm/o/v1`;
83
+ const FACTORY_ANTHROPIC_BASE_URL = `${FACTORY_API_BASE_URL}/api/llm/a`;
84
+ const FACTORY_GEMINI_BASE_URL = `${FACTORY_API_BASE_URL}/api/llm/g/v1`;
85
+
86
+ const DEFAULT_CAPABILITIES: BinaryCapability = { contextWindow: 200_000, maxTokens: 64_000, images: true, pdf: false, api: "openai-responses", baseUrl: FACTORY_OPENAI_BASE_URL };
87
+
88
+ const FALLBACK_REASONING: Record<string, { supported: string[]; default: string }> = {
89
+ "claude-opus-5": { supported: ["off", "low", "medium", "high", "xhigh", "max"], default: "high" },
90
+ "claude-opus-5-fast": { supported: ["off", "low", "medium", "high", "xhigh", "max"], default: "high" },
91
+ "claude-fable-5": { supported: ["off", "low", "medium", "high", "xhigh", "max"], default: "high" },
92
+ "claude-sonnet-5": { supported: ["off", "low", "medium", "high", "xhigh", "max"], default: "high" },
93
+ "claude-haiku-4-5-20251001": { supported: ["off", "low", "medium", "high"], default: "off" },
94
+ "gpt-5.6-sol": { supported: ["none", "low", "medium", "high", "xhigh", "max"], default: "medium" },
95
+ "gpt-5.6-terra": { supported: ["none", "low", "medium", "high", "xhigh", "max"], default: "medium" },
96
+ "gpt-5.6-luna": { supported: ["none", "low", "medium", "high", "xhigh", "max"], default: "medium" },
97
+ "gpt-5.5-pro": { supported: ["medium", "high", "xhigh"], default: "medium" },
98
+ "gemini-3.7-flash": { supported: ["low", "medium", "high"], default: "high" },
99
+ "grok-4.5": { supported: ["low", "medium", "high"], default: "high" },
100
+ "grok-4.6": { supported: ["low", "medium", "high", "xhigh"], default: "high" },
101
+ "minimax-m3": { supported: ["high"], default: "high" },
102
+ "inkling": { supported: ["off", "minimal", "low", "medium", "high", "xhigh", "max"], default: "high" },
103
+ "deepseek-v4-flash-0731": { supported: ["off", "low", "high", "max"], default: "high" },
104
+ "deepseek-v4-pro": { supported: ["off", "low", "high", "max"], default: "high" },
105
+ "glm-5.2": { supported: ["off", "high", "max"], default: "high" },
106
+ "glm-5.2-fast": { supported: ["off", "high", "max"], default: "high" },
107
+ "kimi-k3": { supported: ["off", "low", "high", "max"], default: "high" },
108
+ // Live Factory streaming rejects reasoning_effort="off" for Nemotron and reports
109
+ // the accepted disable token as `none`; keep this as an API-truth override.
110
+ "nemotron-3-ultra": { supported: ["off", "high"], default: "high" },
111
+ };
112
+
113
+ export const DROID_MODELS_FALLBACK: FactoryModel[] = Object.entries(BINARY_CAPABILITIES).filter(([id]) => ACTIVE_MODEL_IDS.has(id)).map(([id, capability]) => {
114
+ const reasoning = FALLBACK_REASONING[id] || fallbackReasoning(id);
115
+ return {
116
+ id,
117
+ name: fallbackName(id),
118
+ reasoning: reasoning.supported.some((effort) => effort !== "none" && effort !== "off"),
119
+ supportedReasoningEfforts: reasoning.supported,
120
+ defaultReasoningEffort: reasoning.default,
121
+ ...capability,
122
+ ...familyForModel(id),
123
+ billingPool: billingPoolForModel(id),
124
+ } as FactoryModel;
125
+ });
126
+
127
+ function fallbackName(id: string) {
128
+ const fromHelp = id
129
+ .replace(/-/g, " ")
130
+ .replace(/\bgpt\b/i, "GPT")
131
+ .replace(/\bgrok\b/i, "Grok")
132
+ .replace(/\bsol\b/i, "Sol")
133
+ .replace(/\bterra\b/i, "Terra")
134
+ .replace(/\bluna\b/i, "Luna")
135
+ .replace(/\bglm\b/i, "GLM")
136
+ .replace(/\bkimi\b/i, "Kimi")
137
+ .replace(/\bdeepseek\b/i, "DeepSeek")
138
+ .replace(/\binkling\b/i, "Inkling")
139
+ .replace(/\bclaude\b/i, "Claude")
140
+ .replace(/\bopus\b/i, "Opus")
141
+ .replace(/\bfable\b/i, "Fable")
142
+ .replace(/\bsonnet\b/i, "Sonnet")
143
+ .replace(/\bhaiku\b/i, "Haiku")
144
+ .replace(/\bfast\b/i, "Fast Mode")
145
+ .replace(/\bpro\b/i, "Pro")
146
+ .replace(/\bcodex\b/i, "Codex");
147
+ return fromHelp.replace(/\s+/g, " ").trim();
148
+ }
149
+
150
+ function fallbackReasoning(id: string) {
151
+ if (id.startsWith("gpt-5.2")) return { supported: ["off", "low", "medium", "high", "xhigh"], default: "low" };
152
+ if (id.startsWith("gpt-")) return { supported: ["low", "medium", "high", "xhigh"], default: "medium" };
153
+ if (id.startsWith("gemini-3.1")) return { supported: ["low", "medium", "high"], default: "high" };
154
+ if (id.startsWith("gemini-")) return { supported: ["minimal", "low", "medium", "high"], default: "high" };
155
+ if (id.startsWith("glm-5.2")) return { supported: ["off", "high", "max"], default: "high" };
156
+ if (id.startsWith("glm-") || id.startsWith("kimi-") || id.startsWith("nemotron-")) return { supported: ["off", "high"], default: "high" };
157
+ if (id.startsWith("deepseek-")) return { supported: ["off", "low", "high", "max"], default: "high" };
158
+ if (id.startsWith("minimax-m2.5")) return { supported: ["low", "medium", "high"], default: "high" };
159
+ if (id.includes("4-5")) return { supported: ["off", "low", "medium", "high"], default: "off" };
160
+ return { supported: ["off", "low", "medium", "high"], default: "high" };
161
+ }
162
+
163
+ export function factoryApiForModel(id: string) {
164
+ if (id.startsWith("claude-") || id.startsWith("minimax-")) return "anthropic-messages";
165
+ if (id.startsWith("gemini-")) return "google-generative-ai";
166
+ if (id === "inkling" || id.startsWith("kimi-") || id.startsWith("glm-") || id.startsWith("deepseek-") || id.startsWith("nemotron-")) return "openai-completions";
167
+ return "openai-responses";
168
+ }
169
+
170
+ function familyForModel(id: string): Pick<BinaryCapability, "api" | "baseUrl" | "apiProvider"> {
171
+ const api = factoryApiForModel(id);
172
+ if (api === "anthropic-messages") {
173
+ return { api, baseUrl: FACTORY_ANTHROPIC_BASE_URL, apiProvider: id.startsWith("minimax-") ? "fireworks" : "bedrock_anthropic" };
174
+ }
175
+ if (api === "google-generative-ai") return { api, baseUrl: FACTORY_GEMINI_BASE_URL, apiProvider: "google" };
176
+ if (api === "openai-completions") {
177
+ return { api, baseUrl: FACTORY_OPENAI_BASE_URL, apiProvider: FACTORY_CORE_PROVIDER_OVERRIDES[id] || "fireworks" };
178
+ }
179
+ return { api, baseUrl: FACTORY_OPENAI_BASE_URL, apiProvider: id.startsWith("grok-") ? "xai" : "openai" };
180
+ }
181
+
182
+ function capabilityForModel(id: string): Required<Pick<FactoryModel, "api" | "baseUrl">> & BinaryCapability {
183
+ return { ...(BINARY_CAPABILITIES[id] || DEFAULT_CAPABILITIES), ...familyForModel(id) } as Required<Pick<FactoryModel, "api" | "baseUrl">> & BinaryCapability;
184
+ }
185
+
186
+ function normalizeDisplayName(name: string) {
187
+ return name
188
+ .replace(/\s*\(default\)\s*$/, "")
189
+ .replace(/\s+\[Deprecated\]\s*$/, "")
190
+ .replace(/^Droid Core\s*\((.+)\)$/i, "$1")
191
+ .trim();
192
+ }
193
+
194
+ function comparableDisplayName(name: string) {
195
+ return normalizeDisplayName(name).toLowerCase().replace(/[^a-z0-9]+/g, "");
196
+ }
197
+
198
+ export function parseModelsFromDroidHelp(): FactoryModel[] {
199
+ try {
200
+ const help = droidExecHelp();
201
+ const modelSection = help.split("Available Models:")[1]?.split("Model details:")[0] || "";
202
+ const detailSection = help.split("Model details:")[1]?.split("Authentication:")[0] || "";
203
+ const models = new Map<string, FactoryModel>();
204
+ for (const line of modelSection.split(/\r?\n/)) {
205
+ const match = line.match(/^\s{2}([^\s]+)\s{2,}(.+?)\s*(?:\[Deprecated\])?\s*$/);
206
+ if (!match) continue;
207
+ const id = match[1];
208
+ if (!ACTIVE_MODEL_IDS.has(id)) continue;
209
+ const reasoning = FALLBACK_REASONING[id] || fallbackReasoning(id);
210
+ models.set(id, {
211
+ id,
212
+ name: normalizeDisplayName(match[2]),
213
+ reasoning: reasoning.supported.some((effort) => effort !== "none" && effort !== "off"),
214
+ supportedReasoningEfforts: reasoning.supported,
215
+ defaultReasoningEffort: reasoning.default,
216
+ ...capabilityForModel(id),
217
+ billingPool: /\(Droid Core\)/i.test(match[2])
218
+ ? "core"
219
+ : billingPoolForModel(id),
220
+ });
221
+ }
222
+ for (const line of detailSection.split(/\r?\n/)) {
223
+ const match = line.match(/^\s*-\s+(.+?): supports reasoning: (Yes|No); supported: \[([^\]]*)\]; default: ([^\s]+)/);
224
+ if (!match) continue;
225
+ const detailName = comparableDisplayName(match[1]);
226
+ const byName = [...models.values()].find((model) => comparableDisplayName(model.name) === detailName);
227
+ if (!byName) continue;
228
+ byName.supportedReasoningEfforts = match[3].split(",").map((effort) => effort.trim()).filter(Boolean);
229
+ byName.defaultReasoningEffort = match[4];
230
+ byName.reasoning = match[2] === "Yes";
231
+ }
232
+ if (!models.size) return DROID_MODELS_FALLBACK;
233
+ for (const fallback of DROID_MODELS_FALLBACK) {
234
+ if (!models.has(fallback.id)) models.set(fallback.id, fallback);
235
+ }
236
+ return [...models.values()];
237
+ } catch {
238
+ return DROID_MODELS_FALLBACK;
239
+ }
240
+ }
241
+
242
+ function thinkingMap(model: FactoryModel) {
243
+ const supported = model.supportedReasoningEfforts;
244
+ const levels = ["off", "none", "minimal", "low", "medium", "high", "xhigh", "max"];
245
+ // Droid exposes Nemotron's disabled effort as `off`, while Factory's
246
+ // Baseten wire endpoint accepts the equivalent token `none`.
247
+ const disableLevel = model.id === "nemotron-3-ultra"
248
+ ? "none"
249
+ : supported.includes("none")
250
+ ? "none"
251
+ : supported.includes("off")
252
+ ? "off"
253
+ : null;
254
+ return Object.fromEntries(levels.map((level) => {
255
+ if (model.id === "nemotron-3-ultra" && (level === "off" || level === "none")) return [level, "none"];
256
+ const providerLevel = level === "off" || level === "none" ? disableLevel : level;
257
+ return [level, providerLevel && supported.includes(providerLevel) ? providerLevel : null];
258
+ }));
259
+ }
260
+
261
+ export function toPiModel(model: FactoryModel) {
262
+ return {
263
+ id: model.id,
264
+ name:
265
+ model.billingPool === "core" && !/Droid Core/i.test(model.name)
266
+ ? `${model.name} (Droid Core)`
267
+ : model.name,
268
+ baseUrl: model.baseUrl,
269
+ headers: model.apiProvider ? { "x-api-provider": model.apiProvider } : undefined,
270
+ reasoning: model.reasoning,
271
+ input: model.images ? ["text", "image"] : ["text"],
272
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
273
+ contextWindow: model.contextWindow,
274
+ maxTokens: model.maxTokens,
275
+ compat: {
276
+ supportsReasoningEffort: model.reasoning,
277
+ supportsDeveloperRole: false,
278
+ supportsPdf: model.pdf,
279
+ ...(model.api === "anthropic-messages" ? { forceAdaptiveThinking: true } : {}),
280
+ },
281
+ thinkingLevelMap: thinkingMap(model),
282
+ };
283
+ }