@gajae-code/ai 0.12.7 → 0.12.10

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 (48) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/dist/types/auth-storage.d.ts +33 -6
  3. package/dist/types/index.d.ts +2 -1
  4. package/dist/types/model-manager.d.ts +2 -0
  5. package/dist/types/model-pricing.d.ts +3 -0
  6. package/dist/types/provider-models/special.d.ts +1 -0
  7. package/dist/types/providers/composer-discipline.d.ts +29 -23
  8. package/dist/types/providers/openai-opencodex-responses.d.ts +10 -0
  9. package/dist/types/providers/openai-responses-shared.d.ts +1 -0
  10. package/dist/types/providers/register-builtins.d.ts +2 -2
  11. package/dist/types/types.d.ts +15 -7
  12. package/dist/types/utils/fallback-transport.d.ts +23 -0
  13. package/dist/types/utils/oauth/anthropic.d.ts +21 -2
  14. package/dist/types/utils/oauth/callback-server.d.ts +7 -0
  15. package/dist/types/utils/oauth/types.d.ts +12 -1
  16. package/package.json +2 -2
  17. package/src/auth-gateway/server.ts +6 -0
  18. package/src/auth-storage.ts +325 -39
  19. package/src/index.ts +2 -0
  20. package/src/model-manager.ts +9 -3
  21. package/src/model-pricing.ts +68 -0
  22. package/src/model-thinking.ts +23 -1
  23. package/src/models.json +122 -24
  24. package/src/models.ts +7 -4
  25. package/src/prompts/composer-bash-policy-recovery.md +1 -0
  26. package/src/prompts/cursor-composer-bash-policy-recovery.md +1 -0
  27. package/src/prompts/cursor-composer-edit-discipline.md +7 -0
  28. package/src/provider-models/descriptors.ts +7 -1
  29. package/src/provider-models/special.ts +8 -0
  30. package/src/providers/composer-discipline.ts +54 -0
  31. package/src/providers/cursor.ts +2 -2
  32. package/src/providers/openai-codex/response-handler.ts +24 -2
  33. package/src/providers/openai-codex-responses.ts +5 -1
  34. package/src/providers/openai-completions.ts +109 -23
  35. package/src/providers/openai-opencodex-responses.ts +173 -0
  36. package/src/providers/openai-responses-shared.ts +14 -3
  37. package/src/providers/openai-responses.ts +91 -13
  38. package/src/providers/register-builtins.ts +4 -4
  39. package/src/stream.ts +61 -6
  40. package/src/types.ts +17 -6
  41. package/src/utils/discovery/openai-compatible.ts +18 -2
  42. package/src/utils/fallback-transport.ts +79 -6
  43. package/src/utils/http-inspector.ts +1 -0
  44. package/src/utils/idle-iterator.ts +2 -0
  45. package/src/utils/oauth/anthropic.ts +41 -8
  46. package/src/utils/oauth/callback-server.ts +64 -16
  47. package/src/utils/oauth/index.ts +5 -0
  48. package/src/utils/oauth/types.ts +13 -0
@@ -8,9 +8,11 @@
8
8
  * - `SqliteAuthCredentialStore`: concrete SQLite-backed implementation
9
9
  */
10
10
  import { Database, type Statement } from "bun:sqlite";
11
+ import * as crypto from "node:crypto";
11
12
  import * as fs from "node:fs/promises";
12
13
  import * as path from "node:path";
13
14
  import { getAgentDbPath, logger } from "@gajae-code/utils";
15
+ import { checkOpenCodexStatus } from "./providers/openai-opencodex-responses";
14
16
  import { getEnvApiKey } from "./stream";
15
17
  import type { Provider } from "./types";
16
18
  import type {
@@ -35,7 +37,13 @@ import { getOAuthApiKey, getOAuthProvider, refreshOAuthToken, resolveOAuthStorag
35
37
  import { loginDeepInfra } from "./utils/oauth/deepinfra";
36
38
  import { loginDeepSeek } from "./utils/oauth/deepseek";
37
39
  import { loginOpenAICodexDevice } from "./utils/oauth/openai-codex";
38
- import type { OAuthController, OAuthCredentials, OAuthProvider, OAuthProviderId } from "./utils/oauth/types";
40
+ import type {
41
+ OAuthController,
42
+ OAuthCredentials,
43
+ OAuthLoginOptions,
44
+ OAuthProvider,
45
+ OAuthProviderId,
46
+ } from "./utils/oauth/types";
39
47
 
40
48
  // ─────────────────────────────────────────────────────────────────────────────
41
49
  // Credential Types
@@ -455,8 +463,9 @@ export type AuthStorageOptions = {
455
463
  * Resolve a config value (API key, header value, etc.) to an actual value.
456
464
  * - coding-agent injects its resolveConfigValue (supports "!command" syntax via pi-natives)
457
465
  * - Default: checks environment variable first, then treats as literal
466
+ * `cacheScope` changes whenever the provider credential configuration changes.
458
467
  */
459
- configValueResolver?: (config: string) => Promise<string | undefined>;
468
+ configValueResolver?: (config: string, cacheScope?: string) => Promise<string | undefined>;
460
469
  /**
461
470
  * Optional callback fired when AuthStorage automatically disables a
462
471
  * credential because something detected it as no longer usable — today
@@ -843,7 +852,9 @@ export class AuthStorage {
843
852
  #usageLogger?: UsageLogger;
844
853
  #fallbackResolver?: (provider: string) => string | undefined;
845
854
  #store: AuthCredentialStore;
846
- #configValueResolver: (config: string) => Promise<string | undefined>;
855
+ #configValueResolver: (config: string, cacheScope?: string) => Promise<string | undefined>;
856
+ #resolvedStoredApiKeyValues: Map<string, Map<string, { fingerprint: string; usable: boolean }>> = new Map();
857
+ #storedApiKeyResolutionInFlight: Map<string, Map<string, Promise<string | undefined>>> = new Map();
847
858
  #refreshOAuthCredentialOverride?: AuthStorageOptions["refreshOAuthCredential"];
848
859
  #fetchUsageReportsOverride?: AuthStorageOptions["fetchUsageReports"];
849
860
  #sourceLabel?: string;
@@ -858,6 +869,9 @@ export class AuthStorage {
858
869
  */
859
870
  #pendingDisabledEvents: CredentialDisabledEvent[] = [];
860
871
  #generation = 1;
872
+ #providerGenerations = new Map<string, number>();
873
+ #providerConfigurationGenerations = new Map<string, number>();
874
+ #providerOAuthRefreshGenerations = new Map<string, number>();
861
875
  #generationListeners: Set<(generation: number) => void> = new Set();
862
876
  #oauthRefreshInFlight: Map<number, Promise<AuthCredentialSnapshotEntry>> = new Map();
863
877
  #oauthCredentialRefreshInFlight: Map<number, Promise<OAuthCredentials>> = new Map();
@@ -914,7 +928,66 @@ export class AuthStorage {
914
928
  getGeneration(): number {
915
929
  return this.#generation;
916
930
  }
917
-
931
+ getProviderConfigurationGeneration(provider: string): number {
932
+ return this.#getProviderConfigurationGeneration(provider);
933
+ }
934
+ getProviderOAuthRefreshGeneration(provider: string): number {
935
+ return this.#providerOAuthRefreshGenerations.get(resolveOAuthStorageProvider(provider)) ?? 0;
936
+ }
937
+ #getProviderGeneration(provider: string): number {
938
+ return this.#providerGenerations.get(resolveOAuthStorageProvider(provider)) ?? 1;
939
+ }
940
+ #getProviderConfigurationGeneration(provider: string): number {
941
+ return this.#providerConfigurationGenerations.get(resolveOAuthStorageProvider(provider)) ?? 1;
942
+ }
943
+ getProviderEvidenceGeneration(provider: string, resolvedApiKey?: string): string {
944
+ const storageProvider = resolveOAuthStorageProvider(provider);
945
+ const evidenceApiKey = resolvedApiKey;
946
+ let selectedCredential: ({ index: number } & StoredCredential) | undefined;
947
+ try {
948
+ selectedCredential = this.#resolveSelectedStoredCredential(provider);
949
+ } catch {
950
+ return crypto
951
+ .createHash("sha256")
952
+ .update(`${this.#getProviderGeneration(storageProvider)}\u0000unavailable-selector`)
953
+ .digest("hex");
954
+ }
955
+ const credentials = selectedCredential
956
+ ? [selectedCredential.credential]
957
+ : this.#getCredentialsForProvider(provider);
958
+ const hasApiKey = credentials.some(credential => credential.type === "api_key");
959
+ const hasUsableOAuth = credentials.some(
960
+ credential =>
961
+ credential.type === "oauth" && Number.isFinite(credential.expires) && credential.expires > Date.now(),
962
+ );
963
+ const effectiveEnvKey =
964
+ this.#runtimeOverrides.get(provider) || this.#configOverrides.get(provider) || hasApiKey || hasUsableOAuth
965
+ ? undefined
966
+ : getEnvApiKey(provider);
967
+ const storedApiKeyFingerprint = credentials
968
+ .filter(
969
+ (credential): credential is Extract<AuthCredential, { type: "api_key" }> => credential.type === "api_key",
970
+ )
971
+ .map(credential => {
972
+ const resolved = this.#resolvedStoredApiKeyValues.get(storageProvider)?.get(credential.key);
973
+ return `${credential.key}\u0000${
974
+ credential.key.startsWith("!")
975
+ ? (resolved?.fingerprint ?? "")
976
+ : (process.env[credential.key] ?? credential.key)
977
+ }`;
978
+ })
979
+ .join("\u0001");
980
+ const storedOAuthFingerprint = credentials
981
+ .filter((credential): credential is Extract<AuthCredential, { type: "oauth" }> => credential.type === "oauth")
982
+ .map(credential => `${credential.expires}\u0000${credential.expires > Date.now() ? "usable" : "expired"}`)
983
+ .join("\u0001");
984
+ return crypto
985
+ .createHash("sha256")
986
+ .update(
987
+ `${this.#getProviderGeneration(storageProvider)}\u0000${effectiveEnvKey ?? ""}\u0000${storedApiKeyFingerprint}\u0000${storedOAuthFingerprint}\u0000${evidenceApiKey ?? ""}`,
988
+ )
989
+ .digest("hex");
990
+ }
918
991
  onGenerationChanged(listener: (generation: number) => void): () => void {
919
992
  this.#generationListeners.add(listener);
920
993
  return () => {
@@ -926,8 +999,18 @@ export class AuthStorage {
926
999
  this.#generationListeners.delete(listener);
927
1000
  }
928
1001
 
929
- #bumpGeneration(reason: string): void {
1002
+ #bumpGeneration(reason: string, provider?: string): void {
930
1003
  this.#generation += 1;
1004
+ if (provider) {
1005
+ const storageProvider = resolveOAuthStorageProvider(provider);
1006
+ this.#providerGenerations.set(storageProvider, this.#getProviderGeneration(storageProvider) + 1);
1007
+ if (reason !== "stored-api-key-usability") {
1008
+ this.#providerConfigurationGenerations.set(
1009
+ storageProvider,
1010
+ this.#getProviderConfigurationGeneration(storageProvider) + 1,
1011
+ );
1012
+ }
1013
+ }
931
1014
  for (const listener of [...this.#generationListeners]) {
932
1015
  try {
933
1016
  listener(this.#generation);
@@ -976,7 +1059,7 @@ export class AuthStorage {
976
1059
  */
977
1060
  setRuntimeApiKey(provider: string, apiKey: string): void {
978
1061
  this.#runtimeOverrides.set(provider, apiKey);
979
- this.#bumpGeneration("set-runtime-api-key");
1062
+ this.#bumpGeneration("set-runtime-api-key", provider);
980
1063
  }
981
1064
 
982
1065
  /**
@@ -987,20 +1070,24 @@ export class AuthStorage {
987
1070
  const storageProvider = resolveOAuthStorageProvider(provider);
988
1071
  this.#assertCredentialSelectorUsable(storageProvider, selector);
989
1072
  this.#runtimeCredentialSelectors.set(storageProvider, selector);
1073
+ this.#bumpGeneration("set-runtime-credential-selector", provider);
990
1074
  }
991
1075
 
992
1076
  /**
993
1077
  * Remove a runtime credential selector.
994
1078
  */
995
1079
  removeRuntimeCredentialSelector(provider: string): void {
996
- this.#runtimeCredentialSelectors.delete(resolveOAuthStorageProvider(provider));
1080
+ const storageProvider = resolveOAuthStorageProvider(provider);
1081
+ if (this.#runtimeCredentialSelectors.delete(storageProvider)) {
1082
+ this.#bumpGeneration("remove-runtime-credential-selector", provider);
1083
+ }
997
1084
  }
998
1085
 
999
1086
  /**
1000
1087
  * Remove a runtime API key override.
1001
1088
  */
1002
1089
  removeRuntimeApiKey(provider: string): void {
1003
- if (this.#runtimeOverrides.delete(provider)) this.#bumpGeneration("remove-runtime-api-key");
1090
+ if (this.#runtimeOverrides.delete(provider)) this.#bumpGeneration("remove-runtime-api-key", provider);
1004
1091
  }
1005
1092
 
1006
1093
  /** Whether a provider is currently authenticated by a runtime API-key override. */
@@ -1008,6 +1095,37 @@ export class AuthStorage {
1008
1095
  return Boolean(this.#runtimeOverrides.get(provider));
1009
1096
  }
1010
1097
 
1098
+ /**
1099
+ * Whether credential selection for a provider is pinned to one stored row by
1100
+ * a runtime selector (`--credential`).
1101
+ *
1102
+ * Distinct from {@link AuthStorage.hasRuntimeApiKey}: that reports the
1103
+ * `--api-key` override, which lives in a different map and is mutually
1104
+ * exclusive with a selector. Callers that must not rotate away from a pinned
1105
+ * credential have to consult BOTH.
1106
+ */
1107
+ hasRuntimeCredentialSelector(provider: string): boolean {
1108
+ return this.#runtimeCredentialSelectors.has(resolveOAuthStorageProvider(provider));
1109
+ }
1110
+
1111
+ /**
1112
+ * Opaque stored row id of the credential this session is currently using.
1113
+ *
1114
+ * Deliberately non-identifying: the persisted primary key, never an email,
1115
+ * account id, project id, or key material. Callers that need to correlate a
1116
+ * credential across a session boundary use this instead of projecting
1117
+ * personal metadata.
1118
+ *
1119
+ * Returns `undefined` when the session has not been routed to a stored
1120
+ * credential yet, or when it authenticated through an env key or fallback
1121
+ * resolver rather than a stored row.
1122
+ */
1123
+ getSessionCredentialRowId(provider: string, sessionId?: string): number | undefined {
1124
+ const session = this.#getSessionCredential(provider, sessionId);
1125
+ if (!session) return undefined;
1126
+ return this.#getStoredCredentials(provider)[session.index]?.id;
1127
+ }
1128
+
1011
1129
  /**
1012
1130
  * Register a per-provider API key sourced from user configuration
1013
1131
  * (e.g. `models.yml` `providers.<name>.apiKey`). Higher priority than
@@ -1020,14 +1138,14 @@ export class AuthStorage {
1020
1138
  */
1021
1139
  setConfigApiKey(provider: string, apiKey: string): void {
1022
1140
  this.#configOverrides.set(provider, apiKey);
1023
- this.#bumpGeneration("set-config-api-key");
1141
+ this.#bumpGeneration("set-config-api-key", provider);
1024
1142
  }
1025
1143
 
1026
1144
  /**
1027
1145
  * Remove a single config-sourced API key override.
1028
1146
  */
1029
1147
  removeConfigApiKey(provider: string): void {
1030
- if (this.#configOverrides.delete(provider)) this.#bumpGeneration("remove-config-api-key");
1148
+ if (this.#configOverrides.delete(provider)) this.#bumpGeneration("remove-config-api-key", provider);
1031
1149
  }
1032
1150
 
1033
1151
  /**
@@ -1035,9 +1153,10 @@ export class AuthStorage {
1035
1153
  * re-parsing `models.yml` so removed entries actually disappear.
1036
1154
  */
1037
1155
  clearConfigApiKeys(): void {
1038
- if (this.#configOverrides.size === 0) return;
1156
+ const providers = [...this.#configOverrides.keys()];
1157
+ if (providers.length === 0) return;
1039
1158
  this.#configOverrides.clear();
1040
- this.#bumpGeneration("clear-config-api-keys");
1159
+ for (const provider of providers) this.#bumpGeneration("clear-config-api-keys", provider);
1041
1160
  }
1042
1161
 
1043
1162
  /**
@@ -1097,12 +1216,14 @@ export class AuthStorage {
1097
1216
  #setStoredCredentials(provider: string, credentials: StoredCredential[]): void {
1098
1217
  const current = this.#data.get(provider) ?? [];
1099
1218
  if (storedCredentialArraysEqual(current, credentials)) return;
1219
+ this.#resolvedStoredApiKeyValues.delete(provider);
1220
+ this.#storedApiKeyResolutionInFlight.delete(provider);
1100
1221
  if (credentials.length === 0) {
1101
1222
  this.#data.delete(provider);
1102
1223
  } else {
1103
1224
  this.#data.set(provider, credentials);
1104
1225
  }
1105
- this.#bumpGeneration("credentials");
1226
+ this.#bumpGeneration("credentials", provider);
1106
1227
  }
1107
1228
 
1108
1229
  #resolveOAuthDedupeIdentityKey(provider: string, credential: OAuthCredential): string | null {
@@ -1355,6 +1476,7 @@ export class AuthStorage {
1355
1476
  provider: string,
1356
1477
  type: T,
1357
1478
  sessionId?: string,
1479
+ isUsable?: (credential: Extract<AuthCredential, { type: T }>, index: number) => boolean,
1358
1480
  ): { credential: Extract<AuthCredential, { type: T }>; index: number } | undefined {
1359
1481
  const credentials = this.#getCredentialsForProvider(provider)
1360
1482
  .map((credential, index) => ({ credential, index }))
@@ -1372,7 +1494,10 @@ export class AuthStorage {
1372
1494
 
1373
1495
  for (const idx of order) {
1374
1496
  const candidate = credentials[idx];
1375
- if (!this.#isCredentialBlocked(providerKey, candidate.index)) {
1497
+ if (
1498
+ !this.#isCredentialBlocked(providerKey, candidate.index) &&
1499
+ (isUsable === undefined || isUsable(candidate.credential, candidate.index))
1500
+ ) {
1376
1501
  return candidate;
1377
1502
  }
1378
1503
  }
@@ -1407,6 +1532,19 @@ export class AuthStorage {
1407
1532
  const updated = [...entries];
1408
1533
  updated[index] = { id: target.id, credential };
1409
1534
  this.#setStoredCredentials(provider, updated);
1535
+ if (
1536
+ credential.type === "oauth" &&
1537
+ target.credential.type === "oauth" &&
1538
+ (credential.access !== target.credential.access ||
1539
+ credential.refresh !== target.credential.refresh ||
1540
+ credential.expires !== target.credential.expires)
1541
+ ) {
1542
+ const storageProvider = resolveOAuthStorageProvider(provider);
1543
+ this.#providerOAuthRefreshGenerations.set(
1544
+ storageProvider,
1545
+ this.getProviderOAuthRefreshGeneration(storageProvider) + 1,
1546
+ );
1547
+ }
1410
1548
  }
1411
1549
 
1412
1550
  /**
@@ -1627,9 +1765,8 @@ export class AuthStorage {
1627
1765
  * Check if any form of auth is configured for a provider.
1628
1766
  * Unlike getApiKey(), this doesn't refresh OAuth tokens.
1629
1767
  */
1630
- hasAuth(provider: string): boolean {
1631
- const storageProvider = resolveOAuthStorageProvider(provider);
1632
- if (this.#runtimeOverrides.has(storageProvider)) return true;
1768
+ #hasConfiguredAuth(storageProvider: string): boolean {
1769
+ if (this.hasRuntimeApiKey(storageProvider)) return true;
1633
1770
  if (this.#configOverrides.has(storageProvider)) return true;
1634
1771
  if (this.#getCredentialsForProvider(storageProvider).length > 0) return true;
1635
1772
  if (getEnvApiKey(storageProvider)) return true;
@@ -1637,6 +1774,67 @@ export class AuthStorage {
1637
1774
  return false;
1638
1775
  }
1639
1776
 
1777
+ hasAuth(provider: string): boolean {
1778
+ const storageProvider = resolveOAuthStorageProvider(provider);
1779
+ try {
1780
+ this.#resolveSelectedStoredCredential(storageProvider);
1781
+ } catch {
1782
+ return false;
1783
+ }
1784
+ return this.#hasConfiguredAuth(storageProvider);
1785
+ }
1786
+
1787
+ /**
1788
+ * Check whether configured auth is currently usable without resolving credentials.
1789
+ */
1790
+ hasUsableAuth(provider: string): boolean {
1791
+ const storageProvider = resolveOAuthStorageProvider(provider);
1792
+ try {
1793
+ const selectedCredential = this.#resolveSelectedStoredCredential(storageProvider);
1794
+ if (this.hasRuntimeApiKey(storageProvider)) return true;
1795
+ if (this.#configOverrides.has(storageProvider)) return true;
1796
+ if (selectedCredential) {
1797
+ if (selectedCredential.credential.type === "api_key") {
1798
+ return (
1799
+ !this.#isCredentialBlocked(
1800
+ this.#getProviderTypeKey(storageProvider, selectedCredential.credential.type),
1801
+ selectedCredential.index,
1802
+ ) && this.#hasUsableResolvedStoredApiKey(storageProvider, selectedCredential.credential.key)
1803
+ );
1804
+ }
1805
+ return !this.#isCredentialBlocked(
1806
+ this.#getProviderTypeKey(storageProvider, selectedCredential.credential.type),
1807
+ selectedCredential.index,
1808
+ );
1809
+ }
1810
+
1811
+ const credentials = this.#getCredentialsForProvider(storageProvider);
1812
+ let hasStoredApiKey = false;
1813
+ let hasSelectableApiKey = false;
1814
+ let hasUsableApiKey = false;
1815
+ for (const [index, credential] of credentials.entries()) {
1816
+ if (credential.type !== "api_key") continue;
1817
+ hasStoredApiKey = true;
1818
+ if (this.#isCredentialBlocked(this.#getProviderTypeKey(storageProvider, credential.type), index)) continue;
1819
+ hasSelectableApiKey = true;
1820
+ hasUsableApiKey ||= this.#hasUsableResolvedStoredApiKey(storageProvider, credential.key);
1821
+ }
1822
+ if (hasStoredApiKey) return hasSelectableApiKey && hasUsableApiKey;
1823
+ if (
1824
+ this.#getCredentialsForProvider(storageProvider).some(
1825
+ (credential, index) =>
1826
+ credential.type === "oauth" &&
1827
+ !this.#isCredentialBlocked(this.#getProviderTypeKey(storageProvider, credential.type), index),
1828
+ )
1829
+ ) {
1830
+ return true;
1831
+ }
1832
+ } catch {
1833
+ return false;
1834
+ }
1835
+ return Boolean(getEnvApiKey(storageProvider) || this.#fallbackResolver?.(storageProvider));
1836
+ }
1837
+
1640
1838
  /**
1641
1839
  * Check if OAuth credentials are configured for a provider.
1642
1840
  */
@@ -1719,6 +1917,7 @@ export class AuthStorage {
1719
1917
  /** onPrompt is required for some providers (github-copilot, OpenAI code provider) */
1720
1918
  onPrompt: (prompt: { message: string; placeholder?: string }) => Promise<string>;
1721
1919
  },
1920
+ options: OAuthLoginOptions = {},
1722
1921
  ): Promise<void> {
1723
1922
  let credentials: OAuthCredentials;
1724
1923
  const saveApiKeyCredential = async (apiKey: string): Promise<void> => {
@@ -1726,13 +1925,21 @@ export class AuthStorage {
1726
1925
  await this.set(provider, newCredential);
1727
1926
  };
1728
1927
  const manualCodeInput = () => ctrl.onPrompt({ message: "Paste the authorization code (or full redirect URL):" });
1928
+
1729
1929
  switch (provider) {
1930
+ case "opencodex": {
1931
+ await checkOpenCodexStatus(ctrl.onProgress);
1932
+ return;
1933
+ }
1730
1934
  case "anthropic": {
1731
1935
  const { loginAnthropic } = await import("./utils/oauth/anthropic");
1732
- credentials = await loginAnthropic({
1733
- ...ctrl,
1734
- onManualCodeInput: ctrl.onManualCodeInput ?? manualCodeInput,
1735
- });
1936
+ credentials = await loginAnthropic(
1937
+ {
1938
+ ...ctrl,
1939
+ onManualCodeInput: ctrl.onManualCodeInput ?? manualCodeInput,
1940
+ },
1941
+ { manualCode: options.manualCode },
1942
+ );
1736
1943
  break;
1737
1944
  }
1738
1945
  case "alibaba-token-plan": {
@@ -3498,6 +3705,64 @@ export class AuthStorage {
3498
3705
  return undefined;
3499
3706
  }
3500
3707
 
3708
+ async #resolveStoredApiKey(provider: string, key: string): Promise<string | undefined> {
3709
+ const storageProvider = resolveOAuthStorageProvider(provider);
3710
+ const configurationGeneration = this.#getProviderConfigurationGeneration(storageProvider);
3711
+ const resolutions =
3712
+ this.#storedApiKeyResolutionInFlight.get(storageProvider) ?? new Map<string, Promise<string | undefined>>();
3713
+ this.#storedApiKeyResolutionInFlight.set(storageProvider, resolutions);
3714
+ const existing = resolutions.get(key);
3715
+ if (existing) return existing;
3716
+
3717
+ const { promise, resolve, reject } = Promise.withResolvers<string | undefined>();
3718
+ resolutions.set(key, promise);
3719
+ const publish = (value: string | undefined) => {
3720
+ if (
3721
+ configurationGeneration !== this.#getProviderConfigurationGeneration(storageProvider) ||
3722
+ this.#storedApiKeyResolutionInFlight.get(storageProvider) !== resolutions ||
3723
+ resolutions.get(key) !== promise
3724
+ ) {
3725
+ return;
3726
+ }
3727
+ const values =
3728
+ this.#resolvedStoredApiKeyValues.get(storageProvider) ??
3729
+ new Map<string, { fingerprint: string; usable: boolean }>();
3730
+ const wasUsable = !values.has(key) || values.get(key)?.usable === true;
3731
+ const isUsable = (value?.length ?? 0) > 0;
3732
+ values.set(key, {
3733
+ fingerprint: value ? crypto.createHash("sha256").update(value).digest("hex") : "",
3734
+ usable: isUsable,
3735
+ });
3736
+ this.#resolvedStoredApiKeyValues.set(storageProvider, values);
3737
+ if (key.startsWith("!") && wasUsable !== isUsable) {
3738
+ this.#bumpGeneration("stored-api-key-usability", storageProvider);
3739
+ }
3740
+ };
3741
+ void (async () => {
3742
+ try {
3743
+ const value = await this.#configValueResolver(key, String(configurationGeneration));
3744
+ publish(value);
3745
+ resolve(value);
3746
+ } catch (error) {
3747
+ publish(undefined);
3748
+ reject(error);
3749
+ } finally {
3750
+ if (
3751
+ this.#storedApiKeyResolutionInFlight.get(storageProvider) === resolutions &&
3752
+ resolutions.get(key) === promise
3753
+ ) {
3754
+ resolutions.delete(key);
3755
+ if (resolutions.size === 0) this.#storedApiKeyResolutionInFlight.delete(storageProvider);
3756
+ }
3757
+ }
3758
+ })();
3759
+ return promise;
3760
+ }
3761
+ #hasUsableResolvedStoredApiKey(provider: string, key: string): boolean {
3762
+ const resolved = this.#resolvedStoredApiKeyValues.get(resolveOAuthStorageProvider(provider))?.get(key);
3763
+ return key.startsWith("!") ? resolved?.usable === true : true;
3764
+ }
3765
+
3501
3766
  /**
3502
3767
  * Peek at API key for a provider without refreshing OAuth tokens.
3503
3768
  * Used for model discovery where we only need to know if credentials exist
@@ -3506,21 +3771,38 @@ export class AuthStorage {
3506
3771
  */
3507
3772
  async peekApiKey(provider: string): Promise<string | undefined> {
3508
3773
  const runtimeKey = this.#runtimeOverrides.get(provider);
3509
- if (runtimeKey) {
3510
- return runtimeKey;
3511
- }
3774
+ if (runtimeKey) return runtimeKey;
3512
3775
 
3513
3776
  const configKey = this.#configOverrides.get(provider);
3514
- if (configKey) {
3515
- return configKey;
3777
+ if (configKey) return configKey;
3778
+
3779
+ const selectedCredential = this.#resolveSelectedStoredCredential(provider);
3780
+ if (selectedCredential?.credential.type === "api_key") {
3781
+ return this.#resolveStoredApiKey(provider, selectedCredential.credential.key);
3516
3782
  }
3517
3783
 
3518
- const apiKeySelection = this.#selectCredentialByType(provider, "api_key");
3784
+ // Return current OAuth access token only if it is not already expired.
3785
+ if (selectedCredential?.credential.type === "oauth") {
3786
+ const expiresAt = selectedCredential.credential.expires;
3787
+ if (Number.isFinite(expiresAt) && expiresAt > Date.now()) {
3788
+ if (provider === "github-copilot") {
3789
+ return JSON.stringify({
3790
+ token: selectedCredential.credential.access,
3791
+ enterpriseUrl: selectedCredential.credential.enterpriseUrl,
3792
+ });
3793
+ }
3794
+ return selectedCredential.credential.access;
3795
+ }
3796
+ return undefined;
3797
+ }
3798
+
3799
+ const apiKeySelection = this.#selectCredentialByType(provider, "api_key", undefined, credential =>
3800
+ this.#hasUsableResolvedStoredApiKey(provider, credential.key),
3801
+ );
3519
3802
  if (apiKeySelection) {
3520
- return this.#configValueResolver(apiKeySelection.credential.key);
3803
+ return this.#resolveStoredApiKey(provider, apiKeySelection.credential.key);
3521
3804
  }
3522
3805
 
3523
- // Return current OAuth access token only if it is not already expired.
3524
3806
  const oauthSelection = this.#selectCredentialByType(provider, "oauth");
3525
3807
  if (oauthSelection) {
3526
3808
  const expiresAt = oauthSelection.credential.expires;
@@ -3535,10 +3817,7 @@ export class AuthStorage {
3535
3817
  }
3536
3818
  }
3537
3819
 
3538
- const envKey = getEnvApiKey(provider);
3539
- if (envKey) return envKey;
3540
-
3541
- return this.#fallbackResolver?.(provider) ?? undefined;
3820
+ return getEnvApiKey(provider) || this.#fallbackResolver?.(provider);
3542
3821
  }
3543
3822
 
3544
3823
  /**
@@ -3572,14 +3851,16 @@ export class AuthStorage {
3572
3851
 
3573
3852
  if (selectedCredential?.credential.type === "api_key") {
3574
3853
  this.#recordSessionCredential(provider, sessionId, "api_key", selectedCredential.index);
3575
- return this.#configValueResolver(selectedCredential.credential.key);
3854
+ return this.#resolveStoredApiKey(provider, selectedCredential.credential.key);
3576
3855
  }
3577
3856
 
3578
3857
  if (!selectedCredential) {
3579
- const apiKeySelection = this.#selectCredentialByType(provider, "api_key", sessionId);
3858
+ const apiKeySelection = this.#selectCredentialByType(provider, "api_key", sessionId, credential =>
3859
+ this.#hasUsableResolvedStoredApiKey(provider, credential.key),
3860
+ );
3580
3861
  if (apiKeySelection) {
3581
3862
  this.#recordSessionCredential(provider, sessionId, "api_key", apiKeySelection.index);
3582
- return this.#configValueResolver(apiKeySelection.credential.key);
3863
+ return this.#resolveStoredApiKey(provider, apiKeySelection.credential.key);
3583
3864
  }
3584
3865
  }
3585
3866
 
@@ -3648,9 +3929,14 @@ export class AuthStorage {
3648
3929
  }
3649
3930
  }
3650
3931
 
3651
- async #credentialMatchesApiKey(credential: AuthCredential, apiKey: string): Promise<boolean> {
3932
+ async #credentialMatchesApiKey(provider: string, credential: AuthCredential, apiKey: string): Promise<boolean> {
3652
3933
  if (credential.type === "api_key") {
3653
- return (await this.#configValueResolver(credential.key)) === apiKey;
3934
+ return (
3935
+ (await this.#configValueResolver(
3936
+ credential.key,
3937
+ String(this.#getProviderConfigurationGeneration(provider)),
3938
+ )) === apiKey
3939
+ );
3654
3940
  }
3655
3941
  if (credential.access === apiKey) return true;
3656
3942
  return this.#extractStructuredApiKeyToken(apiKey) === credential.access;
@@ -3673,7 +3959,7 @@ export class AuthStorage {
3673
3959
  let matched: { id: number; type: AuthCredential["type"]; index: number } | undefined;
3674
3960
  for (let index = 0; index < stored.length; index++) {
3675
3961
  const entry = stored[index];
3676
- if (entry && (await this.#credentialMatchesApiKey(entry.credential, apiKey))) {
3962
+ if (entry && (await this.#credentialMatchesApiKey(provider, entry.credential, apiKey))) {
3677
3963
  matched = { id: entry.id, type: entry.credential.type, index };
3678
3964
  break;
3679
3965
  }
package/src/index.ts CHANGED
@@ -24,6 +24,7 @@ export * from "./providers/mock";
24
24
  export * from "./providers/ollama";
25
25
  export * from "./providers/openai-codex-responses";
26
26
  export * from "./providers/openai-completions";
27
+ export * from "./providers/openai-opencodex-responses";
27
28
  export * from "./providers/openai-responses";
28
29
  export * from "./providers/synthetic";
29
30
  export * from "./rate-limit-utils";
@@ -47,6 +48,7 @@ export * from "./utils/h2-fetch";
47
48
  export * from "./utils/oauth";
48
49
  export type {
49
50
  OAuthCredentials,
51
+ OAuthLoginOptions,
50
52
  OAuthProvider,
51
53
  OAuthProviderId,
52
54
  OAuthProviderInfo,
@@ -56,6 +56,8 @@ export interface ModelManagerOptions<TApi extends Api = Api, TModelsDevPayload =
56
56
  export interface ModelResolutionResult<TApi extends Api = Api> {
57
57
  models: Model<TApi>[];
58
58
  stale: boolean;
59
+ /** Whether this resolution successfully fetched dynamic models. */
60
+ fetched: boolean;
59
61
  }
60
62
 
61
63
  /**
@@ -147,13 +149,13 @@ export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPa
147
149
  ) {
148
150
  const cachedModels = passModelList<TApi>(cache.models);
149
151
  if (!hasStaticTransportDrift(staticModels, cachedModels)) {
150
- return { models: cachedModels, stale: false };
152
+ return { models: cachedModels, stale: false, fetched: false };
151
153
  }
152
154
  const repairedModels = mergeDynamicModels(staticModels, cachedModels);
153
155
  if (options.canPublishCache?.() ?? true) {
154
156
  writeModelCache(options.providerId, now(), repairedModels, true, staticFingerprint, dbPath);
155
157
  }
156
- return { models: repairedModels, stale: false };
158
+ return { models: repairedModels, stale: false, fetched: false };
157
159
  }
158
160
 
159
161
  const [fetchedModelsDevModels, fetchedDynamicModels] = shouldFetchFromNetwork
@@ -200,6 +202,7 @@ export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPa
200
202
  return {
201
203
  models,
202
204
  stale: !dynamicAuthoritative,
205
+ fetched: shouldFetchFromNetwork && dynamicFetchSucceeded,
203
206
  };
204
207
  }
205
208
 
@@ -403,9 +406,12 @@ function normalizeModelList<TApi extends Api>(value: unknown): Model<TApi>[] {
403
406
  const models: Model<TApi>[] = [];
404
407
  for (const item of value) {
405
408
  if (isModelLike(item) && !isRetiredModel(item)) {
406
- models.push(enrichModelThinking(item as Model<TApi>));
409
+ const model = enrichModelThinking(item as Model<TApi>);
410
+ model.longContextPricing = undefined;
411
+ models.push(model);
407
412
  }
408
413
  }
414
+ applyGeneratedModelPolicies(models as Model<Api>[]);
409
415
  return applyFinalCodexGpt56ContextCap(models);
410
416
  }
411
417