aisubs 0.3.7 → 0.3.8

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 CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.8 - 2026-09-19
4
+
5
+ - Breaking: `createSubscriptionAuth` now requires an explicit credential store.
6
+ - Breaking: provider factories use the `aisubs/providers/*` entrypoints so the core `aisubs` import stays runtime-portable.
7
+ - Add `aisubs/node` SQLite credential and API-key stores with shared-connection support and cross-process refresh ownership.
8
+ - Persist standalone AISubs records in `~/.aisubs/aisubs.db` without retaining the legacy file-store implementation.
9
+
3
10
  ## 0.3.7 - 2026-09-16
4
11
 
5
12
  - Improve provider compatibility, proxy handling, and request normalization.
package/README.md CHANGED
@@ -438,9 +438,14 @@ bun add aisubs
438
438
  ```
439
439
 
440
440
  ```js
441
- import { chatGptProvider, createSubscriptionAuth } from "aisubs";
441
+ import { createSubscriptionAuth } from "aisubs";
442
+ import { SqliteCredentialStore } from "aisubs/node";
443
+ import { chatGptProvider } from "aisubs/providers/chatgpt";
442
444
 
443
- const subscriptions = createSubscriptionAuth({ providers: [chatGptProvider()] });
445
+ const subscriptions = createSubscriptionAuth({
446
+ store: new SqliteCredentialStore("./data/aisubs.db"),
447
+ providers: [chatGptProvider()],
448
+ });
444
449
  const account = subscriptions.account("chatgpt", "personal");
445
450
 
446
451
  if (!(await account.status()).authenticated) {
@@ -493,19 +498,16 @@ Useful account methods:
493
498
  <summary><strong>Configure every provider and custom credential storage</strong></summary>
494
499
 
495
500
  ```js
496
- import {
497
- FileCredentialStore,
498
- chatGptProvider,
499
- claudeProvider,
500
- copilotProvider,
501
- createSubscriptionAuth,
502
- grokProvider,
503
- openCodeGoProvider,
504
- openCodeZenProvider,
505
- } from "aisubs";
501
+ import { createSubscriptionAuth } from "aisubs";
502
+ import { SqliteCredentialStore } from "aisubs/node";
503
+ import { chatGptProvider } from "aisubs/providers/chatgpt";
504
+ import { claudeProvider } from "aisubs/providers/claude";
505
+ import { copilotProvider } from "aisubs/providers/copilot";
506
+ import { grokProvider } from "aisubs/providers/grok";
507
+ import { openCodeGoProvider, openCodeZenProvider } from "aisubs/providers/opencode";
506
508
 
507
509
  const subscriptions = createSubscriptionAuth({
508
- store: new FileCredentialStore("./data/aisubs-credentials.json"),
510
+ store: new SqliteCredentialStore("./data/aisubs.db"),
509
511
  providers: [
510
512
  chatGptProvider(),
511
513
  claudeProvider(),
@@ -517,9 +519,10 @@ const subscriptions = createSubscriptionAuth({
517
519
  });
518
520
  ```
519
521
 
520
- Without a custom store, credentials are saved to
521
- `~/.aisubs/credentials.json`. Select an account with its provider ID and a
522
- local account name:
522
+ Storage is always explicit. The core SDK never discovers a home directory,
523
+ creates files, starts a server, or opens a browser. The standalone CLI composes
524
+ the SQLite adapter at `~/.aisubs/aisubs.db`. Select an account with its provider
525
+ ID and a local account name:
523
526
 
524
527
  ```js
525
528
  const chatgpt = subscriptions.account("chatgpt", "personal");
@@ -596,25 +599,23 @@ const response = await selected.proxy("responses", requestOptions);
596
599
  <summary><strong>Run the local HTTP server from Node.js</strong></summary>
597
600
 
598
601
  This is the programmatic equivalent of `aisubs dashboard`. The API key is
599
- created once and reused across restarts; delete or regenerate the key file only
600
- when clients should receive a new key.
602
+ created once and reused across restarts; regenerate it when clients should
603
+ receive a new key.
601
604
 
602
605
  ```js
603
606
  import { homedir } from "node:os";
604
607
  import { join } from "node:path";
605
- import {
606
- FileApiKeyStore,
607
- FileCredentialStore,
608
- chatGptProvider,
609
- claudeProvider,
610
- createSubscriptionAuth,
611
- } from "aisubs";
608
+ import { createSubscriptionAuth } from "aisubs";
612
609
  import { createSubscriptionAuthServer } from "aisubs/http";
610
+ import { SqliteApiKeyStore, SqliteCredentialStore } from "aisubs/node";
611
+ import { chatGptProvider } from "aisubs/providers/chatgpt";
612
+ import { claudeProvider } from "aisubs/providers/claude";
613
613
 
614
614
  const directory = join(homedir(), ".aisubs");
615
- const apiKey = await new FileApiKeyStore(join(directory, "api-key")).readOrCreate();
615
+ const database = join(directory, "aisubs.db");
616
+ const apiKey = await new SqliteApiKeyStore(database).readOrCreate();
616
617
  const auth = createSubscriptionAuth({
617
- store: new FileCredentialStore(join(directory, "credentials.json")),
618
+ store: new SqliteCredentialStore(database),
618
619
  providers: [chatGptProvider(), claudeProvider()],
619
620
  });
620
621
 
@@ -674,8 +675,7 @@ the old key.
674
675
 
675
676
  ## Storage and security
676
677
 
677
- - Credentials: `~/.aisubs/credentials.json`.
678
- - Persistent local API key: `~/.aisubs/api-key`.
678
+ - Credentials and persistent local API key: `~/.aisubs/aisubs.db`.
679
679
  - Optional Codex catalog: `~/.codex/aisubs-catalog.json`.
680
680
  - Codex integration stores the local AISubs key in the user-private Codex config.
681
681
  - State directories and files use private permissions where the platform supports them.
package/dist/auth.d.ts CHANGED
@@ -29,6 +29,7 @@ export declare class SubscriptionAuth {
29
29
  private readonly attempts;
30
30
  private readonly generations;
31
31
  private readonly refreshes;
32
+ private readonly coordinatedRefreshes;
32
33
  private readonly usageCache;
33
34
  private readonly modelsCache;
34
35
  private readonly usageInflight;
@@ -62,6 +63,7 @@ export declare class SubscriptionAuth {
62
63
  listAccounts(provider: ProviderId): Promise<Session[]>;
63
64
  signOut(provider: ProviderId, account?: string): Promise<void>;
64
65
  private credential;
66
+ private coordinatedCredential;
65
67
  getAccessToken(provider: ProviderId, account?: string): Promise<string>;
66
68
  credentialSummary(provider: ProviderId, account?: string): Promise<CredentialSummary>;
67
69
  details(provider: ProviderId, account?: string, signal?: AbortSignal): Promise<SubscriptionAccountDetails>;
@@ -74,6 +76,6 @@ export declare class SubscriptionAuth {
74
76
  account(provider: ProviderId, account: string): SubscriptionAccount;
75
77
  }
76
78
  export declare function createSubscriptionAuth(options: {
77
- store?: CredentialStore;
79
+ store: CredentialStore;
78
80
  providers: readonly ProviderAdapter[];
79
81
  } & SubscriptionAuthOptions): SubscriptionAuth;
package/dist/auth.js CHANGED
@@ -1,9 +1,13 @@
1
- import { defaultAiSubsDataDir, FileCredentialStore } from "./store.js";
2
1
  import { errorMessage } from "./utils.js";
3
- import { join } from "node:path";
4
2
  export const DEFAULT_ACCOUNT = "default";
5
3
  const ACCOUNT_STORAGE_PREFIX = "$subscription-account$";
6
4
  const LOGIN_ATTEMPT_RETENTION_MS = 5 * 60_000;
5
+ function isCoordinatedStore(store) {
6
+ return ("readVersioned" in store &&
7
+ "replaceCredential" in store &&
8
+ "claimRefresh" in store &&
9
+ "commitRefresh" in store);
10
+ }
7
11
  function normalizeAccountKey(value) {
8
12
  if (value == null)
9
13
  return DEFAULT_ACCOUNT;
@@ -63,6 +67,7 @@ export class SubscriptionAuth {
63
67
  attempts = new Map();
64
68
  generations = new Map();
65
69
  refreshes = new Map();
70
+ coordinatedRefreshes = new Map();
66
71
  usageCache = new Map();
67
72
  modelsCache = new Map();
68
73
  usageInflight = new Map();
@@ -157,7 +162,10 @@ export class SubscriptionAuth {
157
162
  const accountKey = normalizeAccountKey(options?.account);
158
163
  const scope = credentialKey(provider, accountKey);
159
164
  const replace = options?.replace !== false;
160
- if (!replace && (await this.store.read(scope))) {
165
+ const durable = isCoordinatedStore(this.store)
166
+ ? await this.store.readVersioned(scope)
167
+ : undefined;
168
+ if (!replace && (durable?.credential ?? (await this.store.read(scope)))) {
161
169
  throw new Error(`Account name ${accountKey} is already connected for ${provider}`);
162
170
  }
163
171
  const epoch = this.advance(scope);
@@ -166,6 +174,7 @@ export class SubscriptionAuth {
166
174
  this.cancelAttempt(attempt);
167
175
  }
168
176
  const abort = new AbortController();
177
+ const id = crypto.randomUUID();
169
178
  const providerOptions = { ...options };
170
179
  delete providerOptions.account;
171
180
  delete providerOptions.replace;
@@ -175,18 +184,31 @@ export class SubscriptionAuth {
175
184
  void login.complete.catch(() => { });
176
185
  throw new Error("Login cancelled");
177
186
  }
178
- const id = crypto.randomUUID();
179
187
  let record;
180
188
  const promise = login.complete
181
189
  .then(async (credential) => {
182
- const saved = await this.store.modify(scope, (current) => {
183
- if (this.generation(scope) !== epoch)
184
- return current;
185
- if (current && !replace) {
186
- throw new Error(`Account name ${accountKey} is already connected for ${provider}`);
187
- }
188
- return credential;
189
- });
190
+ let saved;
191
+ if (isCoordinatedStore(this.store)) {
192
+ const replacement = await this.store.replaceCredential({
193
+ provider: scope,
194
+ credential,
195
+ expectedGeneration: durable?.generation ?? 0,
196
+ operationId: `login:${id}`,
197
+ });
198
+ if (!replacement.applied)
199
+ throw new Error("Login cancelled");
200
+ saved = replacement.record.credential;
201
+ }
202
+ else {
203
+ saved = await this.store.modify(scope, (current) => {
204
+ if (this.generation(scope) !== epoch)
205
+ return current;
206
+ if (current && !replace) {
207
+ throw new Error(`Account name ${accountKey} is already connected for ${provider}`);
208
+ }
209
+ return credential;
210
+ });
211
+ }
190
212
  if (this.generation(scope) !== epoch || !saved)
191
213
  throw new Error("Login cancelled");
192
214
  this.clearMetadata(scope);
@@ -290,12 +312,31 @@ export class SubscriptionAuth {
290
312
  if (attempt.scope === scope)
291
313
  this.cancelAttempt(attempt);
292
314
  }
293
- await this.store.delete(scope);
315
+ if (isCoordinatedStore(this.store)) {
316
+ await this.store.deleteCredential({
317
+ provider: scope,
318
+ operationId: `logout:${crypto.randomUUID()}`,
319
+ });
320
+ }
321
+ else {
322
+ await this.store.delete(scope);
323
+ }
294
324
  }
295
325
  async credential(provider, account, forceRefresh = false) {
296
326
  const adapter = this.adapter(provider);
297
327
  const accountKey = normalizeAccountKey(account);
298
328
  const scope = credentialKey(provider, accountKey);
329
+ if (isCoordinatedStore(this.store)) {
330
+ const pending = this.coordinatedRefreshes.get(scope);
331
+ if (pending)
332
+ return pending;
333
+ const operation = this.coordinatedCredential(this.store, adapter, provider, accountKey, scope, forceRefresh).finally(() => {
334
+ if (this.coordinatedRefreshes.get(scope) === operation)
335
+ this.coordinatedRefreshes.delete(scope);
336
+ });
337
+ this.coordinatedRefreshes.set(scope, operation);
338
+ return operation;
339
+ }
299
340
  const observed = await this.store.read(scope);
300
341
  if (!observed)
301
342
  throw new Error(`Not authenticated with ${provider} account ${accountKey}`);
@@ -350,6 +391,74 @@ export class SubscriptionAuth {
350
391
  this.clearMetadata(scope);
351
392
  return refreshed;
352
393
  }
394
+ async coordinatedCredential(store, adapter, provider, accountKey, scope, forceRefresh) {
395
+ const observed = await store.readVersioned(scope);
396
+ const credential = observed.credential;
397
+ if (!credential)
398
+ throw new Error(`Not authenticated with ${provider} account ${accountKey}`);
399
+ if (credential.metadata?.reauthRequired === true) {
400
+ throw new Error(`Session expired for ${provider} account ${accountKey}; sign in again`);
401
+ }
402
+ if (!forceRefresh && credential.expiresAt > Date.now())
403
+ return credential;
404
+ const claimId = crypto.randomUUID();
405
+ const claim = await store.claimRefresh({
406
+ provider: scope,
407
+ expectedVersion: observed.version,
408
+ expectedGeneration: observed.generation,
409
+ claimId,
410
+ operationId: `refresh-claim:${claimId}`,
411
+ });
412
+ if (claim === "missing") {
413
+ throw new Error(`Not authenticated with ${provider} account ${accountKey}`);
414
+ }
415
+ if (claim !== "claimed") {
416
+ const deadline = Date.now() + this.refreshTimeoutMs;
417
+ while (Date.now() < deadline) {
418
+ await new Promise((resolve) => setTimeout(resolve, 50));
419
+ const latest = await store.readVersioned(scope);
420
+ if (latest.version === observed.version && latest.generation === observed.generation)
421
+ continue;
422
+ if (!latest.credential || latest.credential.metadata?.reauthRequired === true)
423
+ break;
424
+ if (latest.credential.expiresAt > Date.now())
425
+ return latest.credential;
426
+ break;
427
+ }
428
+ throw new Error(`Refresh outcome is uncertain for ${provider} account ${accountKey}; sign in again`);
429
+ }
430
+ let next;
431
+ try {
432
+ next = await adapter.refresh(credential, AbortSignal.timeout(this.refreshTimeoutMs));
433
+ }
434
+ catch (error) {
435
+ if (!adapter.isPermanentRefreshError?.(error))
436
+ throw error;
437
+ next = {
438
+ accessToken: "",
439
+ expiresAt: 0,
440
+ account: credential.account,
441
+ metadata: { ...credential.metadata, reauthRequired: true },
442
+ };
443
+ }
444
+ const committed = await store.commitRefresh({
445
+ provider: scope,
446
+ claimId,
447
+ expectedGeneration: observed.generation,
448
+ credential: next,
449
+ operationId: `refresh-commit:${claimId}`,
450
+ });
451
+ if (!committed.applied) {
452
+ throw new Error(`Session changed while refreshing ${provider} account ${accountKey}`);
453
+ }
454
+ const saved = committed.record.credential;
455
+ if (!saved || saved.metadata?.reauthRequired === true) {
456
+ this.clearMetadata(scope);
457
+ throw new Error(`Session expired for ${provider} account ${accountKey}; sign in again`);
458
+ }
459
+ this.clearMetadata(scope);
460
+ return saved;
461
+ }
353
462
  async getAccessToken(provider, account = DEFAULT_ACCOUNT) {
354
463
  const credential = await this.credential(provider, account);
355
464
  if (credential.metadata?.delegatedCli === true) {
@@ -512,6 +621,5 @@ export class SubscriptionAuth {
512
621
  }
513
622
  }
514
623
  export function createSubscriptionAuth(options) {
515
- const store = options.store ?? new FileCredentialStore(join(defaultAiSubsDataDir(), "credentials.json"));
516
- return new SubscriptionAuth(store, options.providers, options);
624
+ return new SubscriptionAuth(options.store, options.providers, options);
517
625
  }
package/dist/cli.js CHANGED
@@ -8,7 +8,8 @@ import { claudeProvider } from "./providers/claude.js";
8
8
  import { copilotProvider } from "./providers/copilot.js";
9
9
  import { grokProvider } from "./providers/grok.js";
10
10
  import { openCodeGoProvider, openCodeZenProvider } from "./providers/opencode.js";
11
- import { defaultAiSubsDataDir, FileApiKeyStore, FileCredentialStore } from "./store.js";
11
+ import { defaultAiSubsDataDir } from "./store.js";
12
+ import { SqliteApiKeyStore, SqliteCredentialStore } from "./sqlite-store.js";
12
13
  const DEFAULT_DASHBOARD_PORT = 4319;
13
14
  function usage() {
14
15
  console.log(`AI Subs
@@ -61,8 +62,9 @@ async function main() {
61
62
  }
62
63
  if (!Number.isInteger(port) || port < 0 || port > 65535)
63
64
  throw new Error("Invalid port");
64
- const store = new FileCredentialStore(join(dataDirectory, "credentials.json"));
65
- const apiKeys = new FileApiKeyStore(join(dataDirectory, "api-key"));
65
+ const database = join(dataDirectory, "aisubs.db");
66
+ const store = new SqliteCredentialStore(database);
67
+ const apiKeys = new SqliteApiKeyStore(database);
66
68
  const auth = createSubscriptionAuth({
67
69
  store,
68
70
  providers: [
package/dist/http.js CHANGED
@@ -112,6 +112,7 @@ function openAiModel(provider, model) {
112
112
  capabilities: {
113
113
  endpoints: model.endpoints ?? [],
114
114
  input_modalities: model.inputModalities ?? ["text"],
115
+ output_modalities: model.outputModalities ?? ["text"],
115
116
  reasoning_efforts: model.reasoningEfforts ?? [],
116
117
  tools: model.supportsToolCall ?? false,
117
118
  },
package/dist/index.d.ts CHANGED
@@ -1,9 +1,4 @@
1
1
  export { createSubscriptionAuth, DEFAULT_ACCOUNT, SubscriptionAuth, type SubscriptionAccount, type SubscriptionAuthOptions, } from "./auth.js";
2
- export { defaultAiSubsDataDir, FileApiKeyStore, FileCredentialStore, MemoryCredentialStore, } from "./store.js";
3
- export { chatGptProvider, type ChatGptProviderOptions } from "./providers/chatgpt.js";
4
- export { claudeProvider, type ClaudeProviderOptions } from "./providers/claude.js";
5
- export { copilotProvider, type CopilotProviderOptions } from "./providers/copilot.js";
6
- export { grokProvider, type GrokProviderOptions } from "./providers/grok.js";
7
- export { openCodeGoProvider, openCodeZenProvider } from "./providers/opencode.js";
2
+ export { MemoryCredentialStore } from "./memory-store.js";
8
3
  export { parseChatGptUsage, parseCopilotUsage, parseGrokUsage } from "./usage.js";
9
- export type { CredentialStore, CredentialSummary, BrowserLoginPrompt, DeviceLoginPrompt, ImmediateLoginPrompt, LoginAttempt, LoginMode, LoginPrompt, LoginState, OAuthCredential, ProviderAdapter, ProviderId, ProviderLogin, ProviderLoginField, ProviderModel, ProviderModels, ProviderSummary, ProviderUsage, ProviderUsageContext, ProviderUsageData, Session, SubscriptionAccountDetails, UsageFact, UsageMeter, UsageResetCredit, UsageResetCredits, } from "./types.js";
4
+ export type { CredentialStore, CoordinatedCredentialStore, CredentialSummary, BrowserLoginPrompt, DeviceLoginPrompt, ImmediateLoginPrompt, LoginAttempt, LoginMode, LoginPrompt, LoginState, OAuthCredential, VersionedCredential, ProviderAdapter, ProviderId, ProviderLogin, ProviderLoginField, ProviderModel, ProviderModels, ProviderSummary, ProviderUsage, ProviderUsageContext, ProviderUsageData, Session, SubscriptionAccountDetails, UsageFact, UsageMeter, UsageResetCredit, UsageResetCredits, } from "./types.js";
package/dist/index.js CHANGED
@@ -1,8 +1,3 @@
1
1
  export { createSubscriptionAuth, DEFAULT_ACCOUNT, SubscriptionAuth, } from "./auth.js";
2
- export { defaultAiSubsDataDir, FileApiKeyStore, FileCredentialStore, MemoryCredentialStore, } from "./store.js";
3
- export { chatGptProvider } from "./providers/chatgpt.js";
4
- export { claudeProvider } from "./providers/claude.js";
5
- export { copilotProvider } from "./providers/copilot.js";
6
- export { grokProvider } from "./providers/grok.js";
7
- export { openCodeGoProvider, openCodeZenProvider } from "./providers/opencode.js";
2
+ export { MemoryCredentialStore } from "./memory-store.js";
8
3
  export { parseChatGptUsage, parseCopilotUsage, parseGrokUsage } from "./usage.js";
@@ -0,0 +1,9 @@
1
+ import type { CredentialStore, OAuthCredential, ProviderId } from "./types.js";
2
+ export declare class MemoryCredentialStore implements CredentialStore {
3
+ private readonly values;
4
+ private readonly queues;
5
+ read(provider: ProviderId): Promise<OAuthCredential | null>;
6
+ listKeys(): Promise<string[]>;
7
+ modify(provider: ProviderId, update: (current: OAuthCredential | null) => OAuthCredential | null | Promise<OAuthCredential | null>): Promise<OAuthCredential | null>;
8
+ delete(provider: ProviderId): Promise<void>;
9
+ }
@@ -0,0 +1,34 @@
1
+ export class MemoryCredentialStore {
2
+ values = new Map();
3
+ queues = new Map();
4
+ async read(provider) {
5
+ return this.values.get(provider) ?? null;
6
+ }
7
+ async listKeys() {
8
+ return [...this.values.keys()];
9
+ }
10
+ async modify(provider, update) {
11
+ const previous = this.queues.get(provider) ?? Promise.resolve();
12
+ let result = null;
13
+ const current = previous.then(async () => {
14
+ result = await update(this.values.get(provider) ?? null);
15
+ if (result)
16
+ this.values.set(provider, result);
17
+ else
18
+ this.values.delete(provider);
19
+ });
20
+ const settled = current.catch(() => { });
21
+ this.queues.set(provider, settled);
22
+ try {
23
+ await current;
24
+ }
25
+ finally {
26
+ if (this.queues.get(provider) === settled)
27
+ this.queues.delete(provider);
28
+ }
29
+ return result;
30
+ }
31
+ async delete(provider) {
32
+ await this.modify(provider, () => null);
33
+ }
34
+ }
package/dist/node.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { defaultAiSubsDataDir } from "./store.js";
2
+ export { SqliteApiKeyStore, SqliteCredentialStore } from "./sqlite-store.js";
package/dist/node.js ADDED
@@ -0,0 +1,2 @@
1
+ export { defaultAiSubsDataDir } from "./store.js";
2
+ export { SqliteApiKeyStore, SqliteCredentialStore } from "./sqlite-store.js";
@@ -1,4 +1,5 @@
1
- import type { ProviderAdapter } from "../types.js";
1
+ import type { ProviderAdapter, ProviderModel } from "../types.js";
2
+ export declare function parseOpenAiImageModels(html: string): ProviderModel[];
2
3
  export interface ChatGptProviderOptions {
3
4
  clientId?: string;
4
5
  compatibilityVersion?: string;
@@ -12,6 +12,7 @@ const VERIFICATION_URL = `${ISSUER}/codex/device`;
12
12
  const USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
13
13
  const RESET_CREDITS_URL = "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits";
14
14
  const MODELS_URL = "https://chatgpt.com/backend-api/codex/models";
15
+ const OPENAI_MODELS_URL = "https://developers.openai.com/api/docs/models";
15
16
  const EXPIRY_SKEW_MS = 5 * 60_000;
16
17
  const BROWSER_LOGIN_TIMEOUT_MS = 10 * 60_000;
17
18
  const BROWSER_CALLBACK_PORTS = [1455, 1457];
@@ -84,6 +85,7 @@ function normalizeModel(value) {
84
85
  })
85
86
  : [];
86
87
  const visibility = stringValue(value.visibility);
88
+ const outputModalities = stringArray(value.output_modalities);
87
89
  return {
88
90
  id,
89
91
  name: stringValue(value.display_name) ?? stringValue(value.name),
@@ -92,12 +94,65 @@ function normalizeModel(value) {
92
94
  maxOutputTokens: numberValue(value.max_output_tokens),
93
95
  reasoningEfforts: levels.length ? levels : stringArray(value.supported_reasoning_efforts),
94
96
  inputModalities: stringArray(value.input_modalities),
95
- endpoints: ["responses"],
97
+ outputModalities,
98
+ endpoints: outputModalities?.includes("image")
99
+ ? ["images/generations", "images/edits"]
100
+ : ["responses"],
96
101
  supportsToolCall: value.supports_tool_calls === false || value.supports_tools === false ? false : true,
97
102
  available: visibility !== "hide" && value.supported_in_api !== false,
98
103
  priority: numberValue(value.priority) ?? Number.MAX_SAFE_INTEGER,
99
104
  };
100
105
  }
106
+ function imageModelName(id) {
107
+ const [prefix, suffix] = id.startsWith("chatgpt-image-")
108
+ ? ["ChatGPT Image", id.slice("chatgpt-image-".length)]
109
+ : id.startsWith("dall-e-")
110
+ ? ["DALL-E", id.slice("dall-e-".length)]
111
+ : ["GPT Image", id.slice("gpt-image-".length)];
112
+ const label = suffix
113
+ .split("-")
114
+ .map((part) => (/^[a-z]/.test(part) ? part[0].toUpperCase() + part.slice(1) : part))
115
+ .join(" ");
116
+ return `${prefix} ${label}`.trim();
117
+ }
118
+ export function parseOpenAiImageModels(html) {
119
+ const ids = new Set();
120
+ const links = html.matchAll(/href=["'](?:https:\/\/developers\.openai\.com)?\/api\/docs\/models\/([^"'/?#]+)[^"']*["']/gi);
121
+ for (const link of links) {
122
+ const id = decodeURIComponent(link[1] ?? "").toLowerCase();
123
+ if (/^(?:gpt-image-|chatgpt-image-|dall-e-)/.test(id))
124
+ ids.add(id);
125
+ }
126
+ return [...ids].map(openAiImageModel);
127
+ }
128
+ function openAiImageModel(id) {
129
+ return {
130
+ id,
131
+ name: imageModelName(id),
132
+ inputModalities: ["text", "image"],
133
+ outputModalities: ["image"],
134
+ endpoints: ["images/generations", "images/edits"],
135
+ supportsToolCall: false,
136
+ available: true,
137
+ selectable: true,
138
+ };
139
+ }
140
+ async function discoverOpenAiImageModels(fetcher, signal) {
141
+ try {
142
+ const response = await fetcher(OPENAI_MODELS_URL, {
143
+ headers: { accept: "text/html" },
144
+ signal,
145
+ });
146
+ if (!response.ok)
147
+ return [];
148
+ return parseOpenAiImageModels(await response.text());
149
+ }
150
+ catch (error) {
151
+ if (signal.aborted)
152
+ throw error;
153
+ return [];
154
+ }
155
+ }
101
156
  async function normalizeChatGptRequest(request) {
102
157
  if (request.method !== "POST" || !new URL(request.url).pathname.endsWith("/responses")) {
103
158
  return request;
@@ -115,6 +170,7 @@ async function normalizeChatGptRequest(request) {
115
170
  headers.set("session-id", sessionId);
116
171
  // Preserve instruction placement and message boundaries. The native Codex
117
172
  // client sends top-level instructions too; moving them does not enable caching.
173
+ delete body.max_output_tokens;
118
174
  delete body.prompt_cache_options;
119
175
  delete body.prompt_cache_retention;
120
176
  const stripBreakpoints = (value) => Array.isArray(value)
@@ -459,12 +515,18 @@ export function chatGptProvider(options = {}) {
459
515
  const response = await fetch(url, { headers: { accept: "application/json" }, signal });
460
516
  const raw = await responseJson(response, "ChatGPT models");
461
517
  const models = Array.isArray(raw.models) ? raw.models : [];
462
- return models
518
+ const languageModels = models
463
519
  .map(normalizeModel)
464
520
  .filter((model) => Boolean(model))
465
521
  .filter((model) => model.available !== false)
466
522
  .sort((left, right) => left.priority - right.priority)
467
523
  .map(({ priority: _priority, ...model }) => model);
524
+ const listed = new Map(languageModels.map((model) => [model.id, model]));
525
+ for (const model of await discoverOpenAiImageModels(fetcher, signal)) {
526
+ if (!listed.has(model.id))
527
+ listed.set(model.id, model);
528
+ }
529
+ return [...listed.values()];
468
530
  },
469
531
  isPermanentRefreshError(error) {
470
532
  return (error instanceof ChatGptTokenError &&
@@ -1,4 +1,8 @@
1
1
  import type { ProviderAdapter, ProviderUsageData } from "../types.js";
2
+ export interface OpenCodeProviderOptions {
3
+ compatibilityVersion?: string;
4
+ fetch?: typeof globalThis.fetch;
5
+ }
2
6
  export declare function parseOpenCodeGoUsage(raw: unknown): ProviderUsageData | null;
3
- export declare function openCodeGoProvider(): ProviderAdapter;
4
- export declare function openCodeZenProvider(): ProviderAdapter;
7
+ export declare function openCodeGoProvider(options?: OpenCodeProviderOptions): ProviderAdapter;
8
+ export declare function openCodeZenProvider(options?: OpenCodeProviderOptions): ProviderAdapter;