gsd-pi 2.10.2 → 2.10.5
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/README.md +2 -0
- package/dist/cli.js +7 -0
- package/dist/loader.js +1 -0
- package/dist/onboarding.js +104 -59
- package/dist/update-cmd.d.ts +1 -0
- package/dist/update-cmd.js +40 -0
- package/node_modules/@gsd/native/dist/hasher/index.d.ts +32 -0
- package/node_modules/@gsd/native/dist/hasher/index.js +37 -0
- package/node_modules/@gsd/native/dist/native.d.ts +4 -1
- package/node_modules/@gsd/native/dist/native.js +39 -9
- package/node_modules/@gsd/native/dist/xxhash/index.d.ts +14 -0
- package/node_modules/@gsd/native/dist/xxhash/index.js +17 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/agent-session.d.ts +6 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/agent-session.js +58 -9
- package/node_modules/@gsd/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/auth-storage.d.ts +72 -12
- package/node_modules/@gsd/pi-coding-agent/dist/core/auth-storage.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/auth-storage.js +254 -43
- package/node_modules/@gsd/pi-coding-agent/dist/core/auth-storage.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/auth-storage.test.d.ts +2 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/auth-storage.test.d.ts.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/auth-storage.test.js +159 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/auth-storage.test.js.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/model-registry.d.ts +4 -2
- package/node_modules/@gsd/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/model-registry.js +6 -4
- package/node_modules/@gsd/pi-coding-agent/dist/core/model-registry.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/settings-manager.d.ts +15 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/settings-manager.js +12 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash-interceptor.d.ts +40 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash-interceptor.d.ts.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash-interceptor.js +92 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash-interceptor.js.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash-interceptor.test.d.ts +2 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash-interceptor.test.d.ts.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash-interceptor.test.js +156 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash-interceptor.test.js.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.d.ts +8 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.js +18 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/index.d.ts +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/index.js +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/index.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/index.d.ts +2 -2
- package/node_modules/@gsd/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/index.js +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/index.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/src/core/agent-session.ts +65 -9
- package/node_modules/@gsd/pi-coding-agent/src/core/auth-storage.test.ts +194 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/auth-storage.ts +283 -53
- package/node_modules/@gsd/pi-coding-agent/src/core/model-registry.ts +6 -4
- package/node_modules/@gsd/pi-coding-agent/src/core/settings-manager.ts +29 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/tools/bash-interceptor.test.ts +198 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/tools/bash-interceptor.ts +115 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/tools/bash.ts +29 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/tools/index.ts +8 -0
- package/node_modules/@gsd/pi-coding-agent/src/index.ts +6 -0
- package/node_modules/@gsd/pi-coding-agent/src/modes/interactive/interactive-mode.ts +1 -1
- package/package.json +8 -2
- package/packages/native/dist/hasher/index.d.ts +32 -0
- package/packages/native/dist/hasher/index.js +37 -0
- package/packages/native/dist/native.d.ts +4 -1
- package/packages/native/dist/native.js +39 -9
- package/packages/native/dist/xxhash/index.d.ts +14 -0
- package/packages/native/dist/xxhash/index.js +17 -0
- package/packages/native/src/native.ts +39 -9
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts +6 -0
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +58 -9
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.d.ts +72 -12
- package/packages/pi-coding-agent/dist/core/auth-storage.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.js +254 -43
- package/packages/pi-coding-agent/dist/core/auth-storage.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/auth-storage.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/auth-storage.test.js +159 -0
- package/packages/pi-coding-agent/dist/core/auth-storage.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts +4 -2
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry.js +6 -4
- package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +15 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js +12 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/bash-interceptor.d.ts +40 -0
- package/packages/pi-coding-agent/dist/core/tools/bash-interceptor.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/tools/bash-interceptor.js +92 -0
- package/packages/pi-coding-agent/dist/core/tools/bash-interceptor.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/tools/bash-interceptor.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/tools/bash-interceptor.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/tools/bash-interceptor.test.js +156 -0
- package/packages/pi-coding-agent/dist/core/tools/bash-interceptor.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/tools/bash.d.ts +8 -0
- package/packages/pi-coding-agent/dist/core/tools/bash.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/bash.js +18 -0
- package/packages/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/index.d.ts +1 -0
- package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/index.js +1 -0
- package/packages/pi-coding-agent/dist/core/tools/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts +2 -2
- package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/index.js +1 -1
- package/packages/pi-coding-agent/dist/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/src/core/agent-session.ts +65 -9
- package/packages/pi-coding-agent/src/core/auth-storage.test.ts +194 -0
- package/packages/pi-coding-agent/src/core/auth-storage.ts +283 -53
- package/packages/pi-coding-agent/src/core/model-registry.ts +6 -4
- package/packages/pi-coding-agent/src/core/settings-manager.ts +29 -0
- package/packages/pi-coding-agent/src/core/tools/bash-interceptor.test.ts +198 -0
- package/packages/pi-coding-agent/src/core/tools/bash-interceptor.ts +115 -0
- package/packages/pi-coding-agent/src/core/tools/bash.ts +29 -0
- package/packages/pi-coding-agent/src/core/tools/index.ts +8 -0
- package/packages/pi-coding-agent/src/index.ts +6 -0
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +1 -1
- package/src/resources/extensions/async-jobs/async-bash-tool.ts +211 -0
- package/src/resources/extensions/async-jobs/await-tool.ts +101 -0
- package/src/resources/extensions/async-jobs/cancel-job-tool.ts +34 -0
- package/src/resources/extensions/async-jobs/index.ts +133 -0
- package/src/resources/extensions/async-jobs/job-manager.ts +250 -0
- package/src/resources/extensions/gsd/git-service.ts +13 -3
- package/src/resources/extensions/gsd/prompts/system.md +5 -2
- package/src/resources/extensions/gsd/tests/git-service.test.ts +36 -0
|
@@ -869,7 +869,7 @@ export class AgentSession {
|
|
|
869
869
|
}
|
|
870
870
|
|
|
871
871
|
// Validate API key
|
|
872
|
-
const apiKey = await this._modelRegistry.getApiKey(this.model);
|
|
872
|
+
const apiKey = await this._modelRegistry.getApiKey(this.model, this.sessionId);
|
|
873
873
|
if (!apiKey) {
|
|
874
874
|
const isOAuth = this._modelRegistry.isUsingOAuth(this.model);
|
|
875
875
|
if (isOAuth) {
|
|
@@ -1309,7 +1309,7 @@ export class AgentSession {
|
|
|
1309
1309
|
* @throws Error if no API key available for the model
|
|
1310
1310
|
*/
|
|
1311
1311
|
async setModel(model: Model<any>, options?: { persist?: boolean }): Promise<void> {
|
|
1312
|
-
const apiKey = await this._modelRegistry.getApiKey(model);
|
|
1312
|
+
const apiKey = await this._modelRegistry.getApiKey(model, this.sessionId);
|
|
1313
1313
|
if (!apiKey) {
|
|
1314
1314
|
throw new Error(`No API key for ${model.provider}/${model.id}`);
|
|
1315
1315
|
}
|
|
@@ -1351,7 +1351,7 @@ export class AgentSession {
|
|
|
1351
1351
|
if (apiKeysByProvider.has(provider)) {
|
|
1352
1352
|
apiKey = apiKeysByProvider.get(provider);
|
|
1353
1353
|
} else {
|
|
1354
|
-
apiKey = await this._modelRegistry.getApiKeyForProvider(provider);
|
|
1354
|
+
apiKey = await this._modelRegistry.getApiKeyForProvider(provider, this.sessionId);
|
|
1355
1355
|
apiKeysByProvider.set(provider, apiKey);
|
|
1356
1356
|
}
|
|
1357
1357
|
|
|
@@ -1406,7 +1406,7 @@ export class AgentSession {
|
|
|
1406
1406
|
const nextIndex = direction === "forward" ? (currentIndex + 1) % len : (currentIndex - 1 + len) % len;
|
|
1407
1407
|
const nextModel = availableModels[nextIndex];
|
|
1408
1408
|
|
|
1409
|
-
const apiKey = await this._modelRegistry.getApiKey(nextModel);
|
|
1409
|
+
const apiKey = await this._modelRegistry.getApiKey(nextModel, this.sessionId);
|
|
1410
1410
|
if (!apiKey) {
|
|
1411
1411
|
throw new Error(`No API key for ${nextModel.provider}/${nextModel.id}`);
|
|
1412
1412
|
}
|
|
@@ -1560,7 +1560,7 @@ export class AgentSession {
|
|
|
1560
1560
|
throw new Error("No model selected");
|
|
1561
1561
|
}
|
|
1562
1562
|
|
|
1563
|
-
const apiKey = await this._modelRegistry.getApiKey(this.model);
|
|
1563
|
+
const apiKey = await this._modelRegistry.getApiKey(this.model, this.sessionId);
|
|
1564
1564
|
if (!apiKey) {
|
|
1565
1565
|
throw new Error(`No API key for ${this.model.provider}`);
|
|
1566
1566
|
}
|
|
@@ -1780,7 +1780,7 @@ export class AgentSession {
|
|
|
1780
1780
|
return;
|
|
1781
1781
|
}
|
|
1782
1782
|
|
|
1783
|
-
const apiKey = await this._modelRegistry.getApiKey(this.model);
|
|
1783
|
+
const apiKey = await this._modelRegistry.getApiKey(this.model, this.sessionId);
|
|
1784
1784
|
if (!apiKey) {
|
|
1785
1785
|
this._emit({ type: "auto_compaction_end", result: undefined, aborted: false, willRetry: false });
|
|
1786
1786
|
return;
|
|
@@ -2082,7 +2082,7 @@ export class AgentSession {
|
|
|
2082
2082
|
refreshTools: () => this._refreshToolRegistry(),
|
|
2083
2083
|
getCommands,
|
|
2084
2084
|
setModel: async (model, options) => {
|
|
2085
|
-
const key = await this.modelRegistry.getApiKey(model);
|
|
2085
|
+
const key = await this.modelRegistry.getApiKey(model, this.sessionId);
|
|
2086
2086
|
if (!key) return false;
|
|
2087
2087
|
await this.setModel(model, options);
|
|
2088
2088
|
return true;
|
|
@@ -2188,7 +2188,14 @@ export class AgentSession {
|
|
|
2188
2188
|
? this._baseToolsOverride
|
|
2189
2189
|
: createAllTools(this._cwd, {
|
|
2190
2190
|
read: { autoResizeImages },
|
|
2191
|
-
bash: {
|
|
2191
|
+
bash: {
|
|
2192
|
+
commandPrefix: shellCommandPrefix,
|
|
2193
|
+
interceptor: {
|
|
2194
|
+
enabled: this.settingsManager.getBashInterceptorEnabled(),
|
|
2195
|
+
rules: this.settingsManager.getBashInterceptorRules(),
|
|
2196
|
+
},
|
|
2197
|
+
availableToolNames: () => this.getActiveToolNames(),
|
|
2198
|
+
},
|
|
2192
2199
|
});
|
|
2193
2200
|
|
|
2194
2201
|
this._baseToolRegistry = new Map(Object.entries(baseTools).map(([name, tool]) => [name, tool as AgentTool]));
|
|
@@ -2275,8 +2282,21 @@ export class AgentSession {
|
|
|
2275
2282
|
);
|
|
2276
2283
|
}
|
|
2277
2284
|
|
|
2285
|
+
/**
|
|
2286
|
+
* Classify an error message into a usage-limit error type for credential backoff.
|
|
2287
|
+
*/
|
|
2288
|
+
private _classifyErrorType(errorMessage: string): import("./auth-storage.js").UsageLimitErrorType {
|
|
2289
|
+
const err = errorMessage.toLowerCase();
|
|
2290
|
+
if (/quota|billing|exceeded.*limit|usage.*limit/i.test(err)) return "quota_exhausted";
|
|
2291
|
+
if (/rate.?limit|too many requests|429/i.test(err)) return "rate_limit";
|
|
2292
|
+
if (/500|502|503|504|server.?error|internal.?error|service.?unavailable/i.test(err)) return "server_error";
|
|
2293
|
+
return "unknown";
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2278
2296
|
/**
|
|
2279
2297
|
* Handle retryable errors with exponential backoff.
|
|
2298
|
+
* When multiple credentials are available, marks the failing credential
|
|
2299
|
+
* as backed off and retries immediately with the next one.
|
|
2280
2300
|
* @returns true if retry was initiated, false if max retries exceeded or disabled
|
|
2281
2301
|
*/
|
|
2282
2302
|
private async _handleRetryableError(message: AssistantMessage): Promise<boolean> {
|
|
@@ -2294,6 +2314,42 @@ export class AgentSession {
|
|
|
2294
2314
|
});
|
|
2295
2315
|
}
|
|
2296
2316
|
|
|
2317
|
+
// Try credential fallback before counting against retry budget.
|
|
2318
|
+
// If another credential is available, switch to it and retry immediately.
|
|
2319
|
+
if (this.model && message.errorMessage) {
|
|
2320
|
+
const errorType = this._classifyErrorType(message.errorMessage);
|
|
2321
|
+
const hasAlternate = this._modelRegistry.authStorage.markUsageLimitReached(
|
|
2322
|
+
this.model.provider,
|
|
2323
|
+
this.sessionId,
|
|
2324
|
+
{ errorType },
|
|
2325
|
+
);
|
|
2326
|
+
|
|
2327
|
+
if (hasAlternate) {
|
|
2328
|
+
// Remove error message from agent state
|
|
2329
|
+
const messages = this.agent.state.messages;
|
|
2330
|
+
if (messages.length > 0 && messages[messages.length - 1].role === "assistant") {
|
|
2331
|
+
this.agent.replaceMessages(messages.slice(0, -1));
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
this._emit({
|
|
2335
|
+
type: "auto_retry_start",
|
|
2336
|
+
attempt: this._retryAttempt + 1,
|
|
2337
|
+
maxAttempts: settings.maxRetries,
|
|
2338
|
+
delayMs: 0,
|
|
2339
|
+
errorMessage: `${message.errorMessage} (switching credential)`,
|
|
2340
|
+
});
|
|
2341
|
+
|
|
2342
|
+
// Retry immediately with the next credential - don't increment _retryAttempt
|
|
2343
|
+
setTimeout(() => {
|
|
2344
|
+
this.agent.continue().catch(() => {
|
|
2345
|
+
// Retry failed - will be caught by next agent_end
|
|
2346
|
+
});
|
|
2347
|
+
}, 0);
|
|
2348
|
+
|
|
2349
|
+
return true;
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
|
|
2297
2353
|
this._retryAttempt++;
|
|
2298
2354
|
|
|
2299
2355
|
if (this._retryAttempt > settings.maxRetries) {
|
|
@@ -2750,7 +2806,7 @@ export class AgentSession {
|
|
|
2750
2806
|
let summaryDetails: unknown;
|
|
2751
2807
|
if (options.summarize && entriesToSummarize.length > 0 && !extensionSummary) {
|
|
2752
2808
|
const model = this.model!;
|
|
2753
|
-
const apiKey = await this._modelRegistry.getApiKey(model);
|
|
2809
|
+
const apiKey = await this._modelRegistry.getApiKey(model, this.sessionId);
|
|
2754
2810
|
if (!apiKey) {
|
|
2755
2811
|
throw new Error(`No API key for ${model.provider}`);
|
|
2756
2812
|
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { AuthStorage } from "./auth-storage.js";
|
|
4
|
+
|
|
5
|
+
// ─── helpers ──────────────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
function makeKey(key: string) {
|
|
8
|
+
return { type: "api_key" as const, key };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function inMemory(data: Record<string, unknown> = {}) {
|
|
12
|
+
return AuthStorage.inMemory(data as any);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// ─── single credential (backward compat) ─────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
describe("AuthStorage — single credential (backward compat)", () => {
|
|
18
|
+
it("returns the api key for a provider with one key", async () => {
|
|
19
|
+
const storage = inMemory({ anthropic: makeKey("sk-abc") });
|
|
20
|
+
const key = await storage.getApiKey("anthropic");
|
|
21
|
+
assert.equal(key, "sk-abc");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("returns undefined for unknown provider", async () => {
|
|
25
|
+
const storage = inMemory({});
|
|
26
|
+
const key = await storage.getApiKey("unknown");
|
|
27
|
+
assert.equal(key, undefined);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("runtime override takes precedence over stored key", async () => {
|
|
31
|
+
const storage = inMemory({ anthropic: makeKey("sk-stored") });
|
|
32
|
+
storage.setRuntimeApiKey("anthropic", "sk-runtime");
|
|
33
|
+
const key = await storage.getApiKey("anthropic");
|
|
34
|
+
assert.equal(key, "sk-runtime");
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// ─── multiple credentials ─────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
describe("AuthStorage — multiple credentials", () => {
|
|
41
|
+
it("round-robins across multiple api keys without sessionId", async () => {
|
|
42
|
+
const storage = inMemory({
|
|
43
|
+
anthropic: [makeKey("sk-1"), makeKey("sk-2"), makeKey("sk-3")],
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const keys = new Set<string>();
|
|
47
|
+
for (let i = 0; i < 6; i++) {
|
|
48
|
+
const k = await storage.getApiKey("anthropic");
|
|
49
|
+
assert.ok(k, `call ${i} should return a key`);
|
|
50
|
+
keys.add(k);
|
|
51
|
+
}
|
|
52
|
+
// All three keys should have been selected across 6 calls
|
|
53
|
+
assert.deepEqual(keys, new Set(["sk-1", "sk-2", "sk-3"]));
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("session-sticky: same sessionId always picks the same key", async () => {
|
|
57
|
+
const storage = inMemory({
|
|
58
|
+
anthropic: [makeKey("sk-1"), makeKey("sk-2"), makeKey("sk-3")],
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const sessionId = "sess-abc";
|
|
62
|
+
const first = await storage.getApiKey("anthropic", sessionId);
|
|
63
|
+
for (let i = 0; i < 5; i++) {
|
|
64
|
+
const k = await storage.getApiKey("anthropic", sessionId);
|
|
65
|
+
assert.equal(k, first, `call ${i} should be sticky to first selection`);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("different sessionIds may select different keys", async () => {
|
|
70
|
+
const storage = inMemory({
|
|
71
|
+
anthropic: [makeKey("sk-1"), makeKey("sk-2"), makeKey("sk-3")],
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const results = new Set<string>();
|
|
75
|
+
for (let i = 0; i < 20; i++) {
|
|
76
|
+
const k = await storage.getApiKey("anthropic", `sess-${i}`);
|
|
77
|
+
if (k) results.add(k);
|
|
78
|
+
}
|
|
79
|
+
// With 20 different sessions and 3 keys, we should see more than one key
|
|
80
|
+
assert.ok(results.size > 1, "multiple sessions should hash to different keys");
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// ─── login accumulation ───────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
describe("AuthStorage — login accumulation", () => {
|
|
87
|
+
it("accumulates api keys on repeated set()", () => {
|
|
88
|
+
const storage = inMemory({});
|
|
89
|
+
storage.set("anthropic", makeKey("sk-1"));
|
|
90
|
+
storage.set("anthropic", makeKey("sk-2"));
|
|
91
|
+
const creds = storage.getCredentialsForProvider("anthropic");
|
|
92
|
+
assert.equal(creds.length, 2);
|
|
93
|
+
assert.deepEqual(
|
|
94
|
+
creds.map((c) => (c.type === "api_key" ? c.key : null)),
|
|
95
|
+
["sk-1", "sk-2"],
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("deduplicates identical api keys", () => {
|
|
100
|
+
const storage = inMemory({});
|
|
101
|
+
storage.set("anthropic", makeKey("sk-1"));
|
|
102
|
+
storage.set("anthropic", makeKey("sk-1"));
|
|
103
|
+
const creds = storage.getCredentialsForProvider("anthropic");
|
|
104
|
+
assert.equal(creds.length, 1);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// ─── backoff / markUsageLimitReached ─────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
describe("AuthStorage — rate-limit backoff", () => {
|
|
111
|
+
it("returns true when a backed-off credential has an alternate", async () => {
|
|
112
|
+
const storage = inMemory({
|
|
113
|
+
anthropic: [makeKey("sk-1"), makeKey("sk-2")],
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Use sk-1 via round-robin (first call, index 0)
|
|
117
|
+
await storage.getApiKey("anthropic");
|
|
118
|
+
|
|
119
|
+
// Mark it as rate-limited; sk-2 should still be available
|
|
120
|
+
const hasAlternate = storage.markUsageLimitReached("anthropic");
|
|
121
|
+
assert.equal(hasAlternate, true);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("returns false when all credentials are backed off", async () => {
|
|
125
|
+
const storage = inMemory({
|
|
126
|
+
anthropic: [makeKey("sk-1"), makeKey("sk-2")],
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Back off both keys
|
|
130
|
+
await storage.getApiKey("anthropic"); // uses index 0
|
|
131
|
+
storage.markUsageLimitReached("anthropic"); // backs off index 0
|
|
132
|
+
await storage.getApiKey("anthropic"); // uses index 1
|
|
133
|
+
const hasAlternate = storage.markUsageLimitReached("anthropic"); // backs off index 1
|
|
134
|
+
assert.equal(hasAlternate, false);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("backed-off credential is skipped; next available key is returned", async () => {
|
|
138
|
+
const storage = inMemory({
|
|
139
|
+
anthropic: [makeKey("sk-1"), makeKey("sk-2")],
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// First call → sk-1 (round-robin index 0)
|
|
143
|
+
const first = await storage.getApiKey("anthropic");
|
|
144
|
+
assert.equal(first, "sk-1");
|
|
145
|
+
|
|
146
|
+
// Back off sk-1
|
|
147
|
+
storage.markUsageLimitReached("anthropic");
|
|
148
|
+
|
|
149
|
+
// Next call should skip backed-off sk-1 and return sk-2
|
|
150
|
+
const second = await storage.getApiKey("anthropic");
|
|
151
|
+
assert.equal(second, "sk-2");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("single credential: markUsageLimitReached returns false", async () => {
|
|
155
|
+
const storage = inMemory({ anthropic: makeKey("sk-only") });
|
|
156
|
+
await storage.getApiKey("anthropic");
|
|
157
|
+
const hasAlternate = storage.markUsageLimitReached("anthropic");
|
|
158
|
+
assert.equal(hasAlternate, false);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("session-sticky: marks the correct credential as backed off", async () => {
|
|
162
|
+
const storage = inMemory({
|
|
163
|
+
anthropic: [makeKey("sk-1"), makeKey("sk-2")],
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const sessionId = "sess-xyz";
|
|
167
|
+
const chosen = await storage.getApiKey("anthropic", sessionId);
|
|
168
|
+
assert.ok(chosen);
|
|
169
|
+
|
|
170
|
+
// Back off the chosen credential for this session
|
|
171
|
+
const hasAlternate = storage.markUsageLimitReached("anthropic", sessionId);
|
|
172
|
+
assert.equal(hasAlternate, true);
|
|
173
|
+
|
|
174
|
+
// Next call with same session should return the other key
|
|
175
|
+
const next = await storage.getApiKey("anthropic", sessionId);
|
|
176
|
+
assert.ok(next);
|
|
177
|
+
assert.notEqual(next, chosen);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// ─── getAll truncation ────────────────────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
describe("AuthStorage — getAll()", () => {
|
|
184
|
+
it("returns first credential only for providers with multiple keys", () => {
|
|
185
|
+
const storage = inMemory({
|
|
186
|
+
anthropic: [makeKey("sk-1"), makeKey("sk-2")],
|
|
187
|
+
openai: makeKey("sk-openai"),
|
|
188
|
+
});
|
|
189
|
+
const all = storage.getAll();
|
|
190
|
+
assert.ok(all["anthropic"]?.type === "api_key");
|
|
191
|
+
assert.equal((all["anthropic"] as any).key, "sk-1");
|
|
192
|
+
assert.equal((all["openai"] as any).key, "sk-openai");
|
|
193
|
+
});
|
|
194
|
+
});
|