@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.
- package/CHANGELOG.md +32 -0
- package/dist/types/auth-storage.d.ts +33 -6
- package/dist/types/index.d.ts +2 -1
- package/dist/types/model-manager.d.ts +2 -0
- package/dist/types/model-pricing.d.ts +3 -0
- package/dist/types/provider-models/special.d.ts +1 -0
- package/dist/types/providers/composer-discipline.d.ts +29 -23
- package/dist/types/providers/openai-opencodex-responses.d.ts +10 -0
- package/dist/types/providers/openai-responses-shared.d.ts +1 -0
- package/dist/types/providers/register-builtins.d.ts +2 -2
- package/dist/types/types.d.ts +15 -7
- package/dist/types/utils/fallback-transport.d.ts +23 -0
- package/dist/types/utils/oauth/anthropic.d.ts +21 -2
- package/dist/types/utils/oauth/callback-server.d.ts +7 -0
- package/dist/types/utils/oauth/types.d.ts +12 -1
- package/package.json +2 -2
- package/src/auth-gateway/server.ts +6 -0
- package/src/auth-storage.ts +325 -39
- package/src/index.ts +2 -0
- package/src/model-manager.ts +9 -3
- package/src/model-pricing.ts +68 -0
- package/src/model-thinking.ts +23 -1
- package/src/models.json +122 -24
- package/src/models.ts +7 -4
- package/src/prompts/composer-bash-policy-recovery.md +1 -0
- package/src/prompts/cursor-composer-bash-policy-recovery.md +1 -0
- package/src/prompts/cursor-composer-edit-discipline.md +7 -0
- package/src/provider-models/descriptors.ts +7 -1
- package/src/provider-models/special.ts +8 -0
- package/src/providers/composer-discipline.ts +54 -0
- package/src/providers/cursor.ts +2 -2
- package/src/providers/openai-codex/response-handler.ts +24 -2
- package/src/providers/openai-codex-responses.ts +5 -1
- package/src/providers/openai-completions.ts +109 -23
- package/src/providers/openai-opencodex-responses.ts +173 -0
- package/src/providers/openai-responses-shared.ts +14 -3
- package/src/providers/openai-responses.ts +91 -13
- package/src/providers/register-builtins.ts +4 -4
- package/src/stream.ts +61 -6
- package/src/types.ts +17 -6
- package/src/utils/discovery/openai-compatible.ts +18 -2
- package/src/utils/fallback-transport.ts +79 -6
- package/src/utils/http-inspector.ts +1 -0
- package/src/utils/idle-iterator.ts +2 -0
- package/src/utils/oauth/anthropic.ts +41 -8
- package/src/utils/oauth/callback-server.ts +64 -16
- package/src/utils/oauth/index.ts +5 -0
- package/src/utils/oauth/types.ts +13 -0
package/src/auth-storage.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
1631
|
-
|
|
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
|
-
|
|
1734
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.#
|
|
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
|
-
|
|
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.#
|
|
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.#
|
|
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 (
|
|
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,
|
package/src/model-manager.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|