@gajae-code/ai 0.8.1 → 0.8.2

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
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.8.2] - 2026-07-06
6
+
7
+ ### Fixed
8
+
9
+ - Refreshed matching existing OAuth credentials during `importCredentialIfAbsent` and cleared provider usage caches after the write, so external credential import no longer keeps stale tokens or stale usage-limit reports for the same account.
10
+
5
11
  ## [0.7.9] - 2026-07-01
6
12
 
7
13
  ### Fixed
@@ -70,6 +70,7 @@ export declare class RemoteAuthCredentialStore implements AuthCredentialStore {
70
70
  getCache(key: string): string | null;
71
71
  setCache(key: string, value: string, expiresAtSec: number): void;
72
72
  cleanExpiredCache(): void;
73
+ deleteCachePrefix(prefix: string): void;
73
74
  /**
74
75
  * Store-level hook consumed by `AuthStorage` — routes refresh through the
75
76
  * broker so the actual refresh token never leaves the broker host. Returns
@@ -420,6 +420,7 @@ export declare const credentialIfAbsentUploadResponseSchema: z.ZodObject<{
420
420
  "skipped-existing-fallback": "skipped-existing-fallback";
421
421
  "skipped-existing-runtime": "skipped-existing-runtime";
422
422
  "skipped-invalid": "skipped-invalid";
423
+ "updated-existing": "updated-existing";
423
424
  }>;
424
425
  entries: z.ZodArray<z.ZodObject<{
425
426
  id: z.ZodNumber;
@@ -102,7 +102,7 @@ export interface AuthCredentialSnapshotEntry {
102
102
  credential: SnapshotCredential;
103
103
  identityKey: string | null;
104
104
  }
105
- export type AuthCredentialIfAbsentReason = "inserted" | "skipped-existing" | "skipped-existing-runtime" | "skipped-existing-config" | "skipped-existing-env" | "skipped-existing-fallback" | "skipped-invalid";
105
+ export type AuthCredentialIfAbsentReason = "inserted" | "updated-existing" | "skipped-existing" | "skipped-existing-runtime" | "skipped-existing-config" | "skipped-existing-env" | "skipped-existing-fallback" | "skipped-invalid";
106
106
  export interface AuthCredentialIfAbsentResult {
107
107
  inserted: boolean;
108
108
  reason: AuthCredentialIfAbsentReason;
@@ -147,6 +147,7 @@ export interface AuthCredentialStore {
147
147
  includeExpired?: boolean;
148
148
  }): string | null;
149
149
  setCache(key: string, value: string, expiresAtSec: number): void;
150
+ deleteCachePrefix?(prefix: string): void;
150
151
  cleanExpiredCache(): void;
151
152
  /**
152
153
  * Optional store-supplied OAuth refresh. When present, `AuthStorage` uses
@@ -646,6 +647,7 @@ export declare class SqliteAuthCredentialStore implements AuthCredentialStore {
646
647
  includeExpired?: boolean;
647
648
  }): string | null;
648
649
  setCache(key: string, value: string, expiresAtSec: number): void;
650
+ deleteCachePrefix(prefix: string): void;
649
651
  cleanExpiredCache(): void;
650
652
  /**
651
653
  * Save OAuth credentials for a provider.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/ai",
4
- "version": "0.8.1",
4
+ "version": "0.8.2",
5
5
  "description": "Unified LLM API with automatic model discovery and provider configuration",
6
6
  "homepage": "https://gajae-code.com",
7
7
  "author": "Yeachan-Heo",
@@ -43,7 +43,7 @@
43
43
  "dependencies": {
44
44
  "@anthropic-ai/sdk": "^0.94.0",
45
45
  "@bufbuild/protobuf": "^2.12.0",
46
- "@gajae-code/utils": "0.8.1",
46
+ "@gajae-code/utils": "0.8.2",
47
47
  "openai": "^6.36.0",
48
48
  "partial-json": "^0.1.7",
49
49
  "zod": "4.4.3"
@@ -86,6 +86,7 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
86
86
  #cache: Map<string, CacheEntry> = new Map();
87
87
  #usageCache?: UsageCacheEntry;
88
88
  #usageInflight?: Promise<UsageReport[] | null>;
89
+ #usageCacheEpoch = 0;
89
90
  #closed = false;
90
91
  /**
91
92
  * `true` once the SSE consumer received its first frame and hasn't dropped
@@ -465,6 +466,19 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
465
466
  }
466
467
  }
467
468
 
469
+ deleteCachePrefix(prefix: string): void {
470
+ for (const key of this.#cache.keys()) {
471
+ if (key.startsWith(prefix)) this.#cache.delete(key);
472
+ }
473
+ if (prefix.startsWith("usage_cache:")) this.#invalidateUsageCache();
474
+ }
475
+
476
+ #invalidateUsageCache(): void {
477
+ this.#usageCache = undefined;
478
+ this.#usageInflight = undefined;
479
+ this.#usageCacheEpoch += 1;
480
+ }
481
+
468
482
  /**
469
483
  * Store-level hook consumed by `AuthStorage` — routes refresh through the
470
484
  * broker so the actual refresh token never leaves the broker host. Returns
@@ -561,10 +575,11 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
561
575
  return Promise.resolve(cached.reports);
562
576
  }
563
577
  if (this.#usageInflight) return this.#usageInflight;
578
+ const epoch = this.#usageCacheEpoch;
564
579
  const inflight = this.#client
565
580
  .fetchUsage()
566
581
  .then(body => {
567
- this.#usageCache = { reports: body.reports, fetchedAt: Date.now() };
582
+ if (this.#usageCacheEpoch === epoch) this.#usageCache = { reports: body.reports, fetchedAt: Date.now() };
568
583
  return body.reports;
569
584
  })
570
585
  .catch(error => {
@@ -572,7 +587,7 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
572
587
  return null;
573
588
  })
574
589
  .finally(() => {
575
- this.#usageInflight = undefined;
590
+ if (this.#usageCacheEpoch === epoch) this.#usageInflight = undefined;
576
591
  });
577
592
  this.#usageInflight = inflight;
578
593
  return inflight;
@@ -187,6 +187,7 @@ export const credentialDisableResponseSchema = z
187
187
  // ─── Upload ────────────────────────────────────────────────────────────────
188
188
  const credentialIfAbsentReasonSchema = z.enum([
189
189
  "inserted",
190
+ "updated-existing",
190
191
  "skipped-existing",
191
192
  "skipped-existing-runtime",
192
193
  "skipped-existing-config",
@@ -154,6 +154,7 @@ export interface AuthCredentialSnapshotEntry {
154
154
 
155
155
  export type AuthCredentialIfAbsentReason =
156
156
  | "inserted"
157
+ | "updated-existing"
157
158
  | "skipped-existing"
158
159
  | "skipped-existing-runtime"
159
160
  | "skipped-existing-config"
@@ -210,6 +211,7 @@ export interface AuthCredentialStore {
210
211
  deleteAuthCredentialsForProvider(provider: string, disabledCause: string): void;
211
212
  getCache(key: string, options?: { includeExpired?: boolean }): string | null;
212
213
  setCache(key: string, value: string, expiresAtSec: number): void;
214
+ deleteCachePrefix?(prefix: string): void;
213
215
  cleanExpiredCache(): void;
214
216
  /**
215
217
  * Optional store-supplied OAuth refresh. When present, `AuthStorage` uses
@@ -472,6 +474,7 @@ interface UsageCache {
472
474
  get<T>(key: string): UsageCacheEntry<T> | undefined;
473
475
  getStale<T>(key: string): UsageCacheEntry<T> | undefined;
474
476
  set<T>(key: string, entry: UsageCacheEntry<T>): void;
477
+ deletePrefix?(prefix: string): void;
475
478
  cleanup?(): void;
476
479
  }
477
480
 
@@ -662,6 +665,10 @@ class AuthStorageUsageCache implements UsageCache {
662
665
  this.store.setCache(`${USAGE_CACHE_PREFIX}${key}`, payload, Math.floor(durableExpiresAt / 1000));
663
666
  }
664
667
 
668
+ deletePrefix(prefix: string): void {
669
+ this.store.deleteCachePrefix?.(`${USAGE_CACHE_PREFIX}${prefix}`);
670
+ }
671
+
665
672
  cleanup(): void {
666
673
  this.store.cleanExpiredCache();
667
674
  }
@@ -1297,8 +1304,6 @@ export class AuthStorage {
1297
1304
  return this.#snapshotSkipResult(storageProvider, "skipped-existing-runtime");
1298
1305
  if (this.#configOverrides.has(storageProvider))
1299
1306
  return this.#snapshotSkipResult(storageProvider, "skipped-existing-config");
1300
- if (this.#getCredentialsForProvider(storageProvider).length > 0)
1301
- return this.#snapshotSkipResult(storageProvider, "skipped-existing");
1302
1307
  if (getEnvApiKey(storageProvider)) return this.#snapshotSkipResult(storageProvider, "skipped-existing-env");
1303
1308
  if (this.#fallbackResolver?.(storageProvider))
1304
1309
  return this.#snapshotSkipResult(storageProvider, "skipped-existing-fallback");
@@ -1311,6 +1316,7 @@ export class AuthStorage {
1311
1316
  result.entries.map(entry => ({ id: entry.id, credential: entry.credential })),
1312
1317
  );
1313
1318
  this.#resetProviderAssignments(storageProvider);
1319
+ if (result.inserted) this.#invalidateUsageCacheForProvider(storageProvider);
1314
1320
  return {
1315
1321
  inserted: result.inserted,
1316
1322
  reason: result.reason,
@@ -1328,6 +1334,13 @@ export class AuthStorage {
1328
1334
  stored.map(record => ({ id: record.id, credential: record.credential })),
1329
1335
  );
1330
1336
  this.#resetProviderAssignments(provider);
1337
+ this.#invalidateUsageCacheForProvider(provider);
1338
+ }
1339
+
1340
+ #invalidateUsageCacheForProvider(provider: string): void {
1341
+ this.#usageRequestInFlight.clear();
1342
+ this.#usageReportsInFlight.clear();
1343
+ this.#usageCache.deletePrefix?.(`report:${provider}:`);
1331
1344
  }
1332
1345
 
1333
1346
  /**
@@ -3705,6 +3718,7 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
3705
3718
  #getCacheStmt: Statement;
3706
3719
  #getCacheIncludingExpiredStmt: Statement;
3707
3720
  #upsertCacheStmt: Statement;
3721
+ #deleteCachePrefixStmt: Statement;
3708
3722
  #deleteExpiredCacheStmt: Statement;
3709
3723
  #closed = false;
3710
3724
 
@@ -3744,6 +3758,7 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
3744
3758
  this.#upsertCacheStmt = this.#db.prepare(
3745
3759
  "INSERT INTO cache (key, value, expires_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value, expires_at = excluded.expires_at",
3746
3760
  );
3761
+ this.#deleteCachePrefixStmt = this.#db.prepare("DELETE FROM cache WHERE substr(key, 1, ?) = ?");
3747
3762
  this.#deleteExpiredCacheStmt = this.#db.prepare(`DELETE FROM cache WHERE expires_at <= ${SQLITE_NOW_EPOCH}`);
3748
3763
  }
3749
3764
 
@@ -4102,16 +4117,58 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
4102
4117
  }
4103
4118
 
4104
4119
  const writeIfAbsent = this.#db.transaction(
4105
- (providerName: string, record: SerializedCredentialRecord): AuthCredentialIfAbsentResult => {
4120
+ (
4121
+ providerName: string,
4122
+ item: AuthCredential,
4123
+ record: SerializedCredentialRecord,
4124
+ ): AuthCredentialIfAbsentResult => {
4106
4125
  const existingRows = this.#listActiveByProviderStmt.all(providerName) as AuthRow[];
4107
- const existing: StoredAuthCredential[] = [];
4126
+ const existing: Array<{
4127
+ id: number;
4128
+ credential: AuthCredential;
4129
+ identityKey: string | null;
4130
+ }> = [];
4108
4131
  for (const row of existingRows) {
4109
4132
  const activeCredential = deserializeCredential(row);
4110
4133
  if (!activeCredential) continue;
4111
- existing.push(toStoredAuthCredential(row, activeCredential));
4134
+ existing.push({
4135
+ id: row.id,
4136
+ credential: activeCredential,
4137
+ identityKey: resolveRowCredentialIdentityKey(providerName, row),
4138
+ });
4112
4139
  }
4113
4140
  if (existing.length > 0) {
4114
- return { inserted: false, reason: "skipped-existing", provider: providerName, entries: existing };
4141
+ let targetId: number | null = null;
4142
+ for (const row of existing) {
4143
+ if (!matchesReplacementCredential(providerName, row.credential, row.identityKey, item)) continue;
4144
+ if (targetId === null) {
4145
+ targetId = row.id;
4146
+ this.#updateStmt.run(record.credentialType, record.data, record.identityKey, row.id);
4147
+ } else {
4148
+ this.#deleteStmt.run("replaced by newer credential", row.id);
4149
+ }
4150
+ }
4151
+
4152
+ if (targetId !== null) {
4153
+ return {
4154
+ inserted: true,
4155
+ reason: "updated-existing",
4156
+ provider: providerName,
4157
+ entries: this.listAuthCredentials(providerName),
4158
+ };
4159
+ }
4160
+
4161
+ return {
4162
+ inserted: false,
4163
+ reason: "skipped-existing",
4164
+ provider: providerName,
4165
+ entries: existing.map(row => ({
4166
+ id: row.id,
4167
+ provider: providerName,
4168
+ credential: row.credential,
4169
+ disabledCause: null,
4170
+ })),
4171
+ };
4115
4172
  }
4116
4173
 
4117
4174
  this.#insertStmt.get(providerName, record.credentialType, record.data, record.identityKey);
@@ -4124,7 +4181,9 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
4124
4181
  },
4125
4182
  );
4126
4183
 
4127
- return writeIfAbsent.immediate(provider, serialized);
4184
+ const result = writeIfAbsent.immediate(provider, credential, serialized);
4185
+ if (result.inserted) this.#purgeSupersededDisabledRows(provider, result.entries);
4186
+ return result;
4128
4187
  }
4129
4188
 
4130
4189
  /**
@@ -4221,6 +4280,13 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
4221
4280
  }
4222
4281
  }
4223
4282
 
4283
+ deleteCachePrefix(prefix: string): void {
4284
+ if (prefix.length === 0) return;
4285
+ try {
4286
+ this.#deleteCachePrefixStmt.run(prefix.length, prefix);
4287
+ } catch {}
4288
+ }
4289
+
4224
4290
  cleanExpiredCache(): void {
4225
4291
  try {
4226
4292
  this.#deleteExpiredCacheStmt.run();
@@ -4311,6 +4377,7 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
4311
4377
  this.#getCacheStmt.finalize();
4312
4378
  this.#getCacheIncludingExpiredStmt.finalize();
4313
4379
  this.#upsertCacheStmt.finalize();
4380
+ this.#deleteCachePrefixStmt.finalize();
4314
4381
  this.#deleteExpiredCacheStmt.finalize();
4315
4382
  this.#db.close();
4316
4383
  }
@@ -2281,8 +2281,6 @@ function buildParams(
2281
2281
  params.output_config = { effort } as typeof params.output_config;
2282
2282
  }
2283
2283
  }
2284
- } else if (options?.thinkingEnabled === false) {
2285
- params.thinking = { type: "disabled" };
2286
2284
  }
2287
2285
  }
2288
2286