@desplega.ai/agent-swarm 1.56.5 → 1.57.0
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/openapi.json +232 -1
- package/package.json +1 -1
- package/src/be/db.ts +197 -0
- package/src/be/migrations/028_api_key_tracking.sql +38 -0
- package/src/be/migrations/029_task_credential_key_type.sql +2 -0
- package/src/commands/runner.ts +124 -7
- package/src/http/api-keys.ts +200 -0
- package/src/http/index.ts +2 -0
- package/src/tests/api-key-tracking.test.ts +201 -0
- package/src/tests/error-tracker.test.ts +59 -0
- package/src/types.ts +4 -0
- package/src/utils/credentials.test.ts +10 -10
- package/src/utils/credentials.ts +114 -15
- package/src/utils/error-tracker.ts +52 -0
package/openapi.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"openapi": "3.1.0",
|
|
3
3
|
"info": {
|
|
4
4
|
"title": "Agent Swarm API",
|
|
5
|
-
"version": "1.56.
|
|
5
|
+
"version": "1.56.5",
|
|
6
6
|
"description": "Multi-agent orchestration API for Claude Code, Codex, and Gemini CLI. Enables task distribution, agent communication, and service discovery.\n\nMCP tools are documented separately in [MCP.md](./MCP.md)."
|
|
7
7
|
},
|
|
8
8
|
"servers": [
|
|
@@ -1415,6 +1415,237 @@
|
|
|
1415
1415
|
}
|
|
1416
1416
|
}
|
|
1417
1417
|
},
|
|
1418
|
+
"/api/keys/report-usage": {
|
|
1419
|
+
"post": {
|
|
1420
|
+
"summary": "Record which API key was used for a task",
|
|
1421
|
+
"tags": [
|
|
1422
|
+
"API Keys"
|
|
1423
|
+
],
|
|
1424
|
+
"security": [
|
|
1425
|
+
{
|
|
1426
|
+
"bearerAuth": []
|
|
1427
|
+
}
|
|
1428
|
+
],
|
|
1429
|
+
"requestBody": {
|
|
1430
|
+
"content": {
|
|
1431
|
+
"application/json": {
|
|
1432
|
+
"schema": {
|
|
1433
|
+
"type": "object",
|
|
1434
|
+
"properties": {
|
|
1435
|
+
"keyType": {
|
|
1436
|
+
"type": "string"
|
|
1437
|
+
},
|
|
1438
|
+
"keySuffix": {
|
|
1439
|
+
"type": "string",
|
|
1440
|
+
"minLength": 1,
|
|
1441
|
+
"maxLength": 10
|
|
1442
|
+
},
|
|
1443
|
+
"keyIndex": {
|
|
1444
|
+
"type": "integer",
|
|
1445
|
+
"minimum": 0
|
|
1446
|
+
},
|
|
1447
|
+
"taskId": {
|
|
1448
|
+
"type": "string",
|
|
1449
|
+
"format": "uuid"
|
|
1450
|
+
},
|
|
1451
|
+
"scope": {
|
|
1452
|
+
"type": "string"
|
|
1453
|
+
},
|
|
1454
|
+
"scopeId": {
|
|
1455
|
+
"type": "string"
|
|
1456
|
+
}
|
|
1457
|
+
},
|
|
1458
|
+
"required": [
|
|
1459
|
+
"keyType",
|
|
1460
|
+
"keySuffix",
|
|
1461
|
+
"keyIndex"
|
|
1462
|
+
]
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
},
|
|
1467
|
+
"responses": {
|
|
1468
|
+
"200": {
|
|
1469
|
+
"description": "Usage recorded"
|
|
1470
|
+
},
|
|
1471
|
+
"400": {
|
|
1472
|
+
"description": "Validation error"
|
|
1473
|
+
},
|
|
1474
|
+
"401": {
|
|
1475
|
+
"description": "Unauthorized"
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
},
|
|
1480
|
+
"/api/keys/report-rate-limit": {
|
|
1481
|
+
"post": {
|
|
1482
|
+
"summary": "Mark an API key as rate-limited",
|
|
1483
|
+
"tags": [
|
|
1484
|
+
"API Keys"
|
|
1485
|
+
],
|
|
1486
|
+
"security": [
|
|
1487
|
+
{
|
|
1488
|
+
"bearerAuth": []
|
|
1489
|
+
}
|
|
1490
|
+
],
|
|
1491
|
+
"requestBody": {
|
|
1492
|
+
"content": {
|
|
1493
|
+
"application/json": {
|
|
1494
|
+
"schema": {
|
|
1495
|
+
"type": "object",
|
|
1496
|
+
"properties": {
|
|
1497
|
+
"keyType": {
|
|
1498
|
+
"type": "string"
|
|
1499
|
+
},
|
|
1500
|
+
"keySuffix": {
|
|
1501
|
+
"type": "string",
|
|
1502
|
+
"minLength": 1,
|
|
1503
|
+
"maxLength": 10
|
|
1504
|
+
},
|
|
1505
|
+
"keyIndex": {
|
|
1506
|
+
"type": "integer",
|
|
1507
|
+
"minimum": 0
|
|
1508
|
+
},
|
|
1509
|
+
"rateLimitedUntil": {
|
|
1510
|
+
"type": "string",
|
|
1511
|
+
"format": "date-time"
|
|
1512
|
+
},
|
|
1513
|
+
"scope": {
|
|
1514
|
+
"type": "string"
|
|
1515
|
+
},
|
|
1516
|
+
"scopeId": {
|
|
1517
|
+
"type": "string"
|
|
1518
|
+
}
|
|
1519
|
+
},
|
|
1520
|
+
"required": [
|
|
1521
|
+
"keyType",
|
|
1522
|
+
"keySuffix",
|
|
1523
|
+
"keyIndex",
|
|
1524
|
+
"rateLimitedUntil"
|
|
1525
|
+
]
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
},
|
|
1530
|
+
"responses": {
|
|
1531
|
+
"200": {
|
|
1532
|
+
"description": "Key marked as rate-limited"
|
|
1533
|
+
},
|
|
1534
|
+
"400": {
|
|
1535
|
+
"description": "Validation error"
|
|
1536
|
+
},
|
|
1537
|
+
"401": {
|
|
1538
|
+
"description": "Unauthorized"
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
},
|
|
1543
|
+
"/api/keys/available": {
|
|
1544
|
+
"get": {
|
|
1545
|
+
"summary": "Get available (non-rate-limited) key indices for a credential type",
|
|
1546
|
+
"tags": [
|
|
1547
|
+
"API Keys"
|
|
1548
|
+
],
|
|
1549
|
+
"security": [
|
|
1550
|
+
{
|
|
1551
|
+
"bearerAuth": []
|
|
1552
|
+
}
|
|
1553
|
+
],
|
|
1554
|
+
"parameters": [
|
|
1555
|
+
{
|
|
1556
|
+
"schema": {
|
|
1557
|
+
"type": "string"
|
|
1558
|
+
},
|
|
1559
|
+
"required": true,
|
|
1560
|
+
"name": "keyType",
|
|
1561
|
+
"in": "query"
|
|
1562
|
+
},
|
|
1563
|
+
{
|
|
1564
|
+
"schema": {
|
|
1565
|
+
"type": "integer",
|
|
1566
|
+
"minimum": 1
|
|
1567
|
+
},
|
|
1568
|
+
"required": true,
|
|
1569
|
+
"name": "totalKeys",
|
|
1570
|
+
"in": "query"
|
|
1571
|
+
},
|
|
1572
|
+
{
|
|
1573
|
+
"schema": {
|
|
1574
|
+
"type": "string"
|
|
1575
|
+
},
|
|
1576
|
+
"required": false,
|
|
1577
|
+
"name": "scope",
|
|
1578
|
+
"in": "query"
|
|
1579
|
+
},
|
|
1580
|
+
{
|
|
1581
|
+
"schema": {
|
|
1582
|
+
"type": "string"
|
|
1583
|
+
},
|
|
1584
|
+
"required": false,
|
|
1585
|
+
"name": "scopeId",
|
|
1586
|
+
"in": "query"
|
|
1587
|
+
}
|
|
1588
|
+
],
|
|
1589
|
+
"responses": {
|
|
1590
|
+
"200": {
|
|
1591
|
+
"description": "List of available key indices"
|
|
1592
|
+
},
|
|
1593
|
+
"400": {
|
|
1594
|
+
"description": "Validation error"
|
|
1595
|
+
},
|
|
1596
|
+
"401": {
|
|
1597
|
+
"description": "Unauthorized"
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
},
|
|
1602
|
+
"/api/keys/status": {
|
|
1603
|
+
"get": {
|
|
1604
|
+
"summary": "Get all API key status records",
|
|
1605
|
+
"tags": [
|
|
1606
|
+
"API Keys"
|
|
1607
|
+
],
|
|
1608
|
+
"security": [
|
|
1609
|
+
{
|
|
1610
|
+
"bearerAuth": []
|
|
1611
|
+
}
|
|
1612
|
+
],
|
|
1613
|
+
"parameters": [
|
|
1614
|
+
{
|
|
1615
|
+
"schema": {
|
|
1616
|
+
"type": "string"
|
|
1617
|
+
},
|
|
1618
|
+
"required": false,
|
|
1619
|
+
"name": "keyType",
|
|
1620
|
+
"in": "query"
|
|
1621
|
+
},
|
|
1622
|
+
{
|
|
1623
|
+
"schema": {
|
|
1624
|
+
"type": "string"
|
|
1625
|
+
},
|
|
1626
|
+
"required": false,
|
|
1627
|
+
"name": "scope",
|
|
1628
|
+
"in": "query"
|
|
1629
|
+
},
|
|
1630
|
+
{
|
|
1631
|
+
"schema": {
|
|
1632
|
+
"type": "string"
|
|
1633
|
+
},
|
|
1634
|
+
"required": false,
|
|
1635
|
+
"name": "scopeId",
|
|
1636
|
+
"in": "query"
|
|
1637
|
+
}
|
|
1638
|
+
],
|
|
1639
|
+
"responses": {
|
|
1640
|
+
"200": {
|
|
1641
|
+
"description": "List of key status records"
|
|
1642
|
+
},
|
|
1643
|
+
"401": {
|
|
1644
|
+
"description": "Unauthorized"
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
},
|
|
1418
1649
|
"/api/events": {
|
|
1419
1650
|
"post": {
|
|
1420
1651
|
"summary": "Store a single event",
|
package/package.json
CHANGED
package/src/be/db.ts
CHANGED
|
@@ -727,6 +727,8 @@ type AgentTaskRow = {
|
|
|
727
727
|
totalContextTokensUsed: number | null;
|
|
728
728
|
contextWindowSize: number | null;
|
|
729
729
|
was_paused: number;
|
|
730
|
+
credentialKeySuffix: string | null;
|
|
731
|
+
credentialKeyType: string | null;
|
|
730
732
|
};
|
|
731
733
|
|
|
732
734
|
function rowToAgentTask(row: AgentTaskRow): AgentTask {
|
|
@@ -780,6 +782,8 @@ function rowToAgentTask(row: AgentTaskRow): AgentTask {
|
|
|
780
782
|
output: row.output ?? undefined,
|
|
781
783
|
progress: row.progress ?? undefined,
|
|
782
784
|
wasPaused: !!row.was_paused,
|
|
785
|
+
credentialKeySuffix: row.credentialKeySuffix ?? undefined,
|
|
786
|
+
credentialKeyType: row.credentialKeyType ?? undefined,
|
|
783
787
|
};
|
|
784
788
|
}
|
|
785
789
|
|
|
@@ -7487,3 +7491,196 @@ export function getContextSummaryByTaskId(taskId: string): ContextSummary {
|
|
|
7487
7491
|
snapshotCount: countRow?.cnt ?? 0,
|
|
7488
7492
|
};
|
|
7489
7493
|
}
|
|
7494
|
+
|
|
7495
|
+
// ─── API Key Pool Tracking ───────────────────────────────────────────────────
|
|
7496
|
+
|
|
7497
|
+
export interface ApiKeyStatus {
|
|
7498
|
+
id: string;
|
|
7499
|
+
keyType: string;
|
|
7500
|
+
keySuffix: string;
|
|
7501
|
+
keyIndex: number;
|
|
7502
|
+
scope: string;
|
|
7503
|
+
scopeId: string | null;
|
|
7504
|
+
status: string;
|
|
7505
|
+
rateLimitedUntil: string | null;
|
|
7506
|
+
lastUsedAt: string | null;
|
|
7507
|
+
lastRateLimitAt: string | null;
|
|
7508
|
+
totalUsageCount: number;
|
|
7509
|
+
rateLimitCount: number;
|
|
7510
|
+
createdAt: string;
|
|
7511
|
+
updatedAt: string;
|
|
7512
|
+
}
|
|
7513
|
+
|
|
7514
|
+
/**
|
|
7515
|
+
* Get available (non-rate-limited) key indices for a credential type.
|
|
7516
|
+
* Automatically clears expired rate limits before returning.
|
|
7517
|
+
*/
|
|
7518
|
+
export function getAvailableKeyIndices(
|
|
7519
|
+
keyType: string,
|
|
7520
|
+
totalKeys: number,
|
|
7521
|
+
scope = "global",
|
|
7522
|
+
scopeId: string | null = null,
|
|
7523
|
+
): number[] {
|
|
7524
|
+
const now = new Date().toISOString();
|
|
7525
|
+
const db = getDb();
|
|
7526
|
+
const effectiveScopeId = scopeId ?? "";
|
|
7527
|
+
|
|
7528
|
+
// Auto-clear expired rate limits
|
|
7529
|
+
db.prepare(
|
|
7530
|
+
`UPDATE api_key_status
|
|
7531
|
+
SET status = 'available', rateLimitedUntil = NULL, updatedAt = ?
|
|
7532
|
+
WHERE keyType = ? AND scope = ? AND scopeId = ?
|
|
7533
|
+
AND status = 'rate_limited' AND rateLimitedUntil IS NOT NULL AND rateLimitedUntil <= ?`,
|
|
7534
|
+
).run(now, keyType, scope, effectiveScopeId, now);
|
|
7535
|
+
|
|
7536
|
+
// Get currently rate-limited key indices
|
|
7537
|
+
const rateLimited = db
|
|
7538
|
+
.prepare<{ keyIndex: number }, [string, string, string]>(
|
|
7539
|
+
`SELECT keyIndex FROM api_key_status
|
|
7540
|
+
WHERE keyType = ? AND scope = ? AND scopeId = ?
|
|
7541
|
+
AND status = 'rate_limited'`,
|
|
7542
|
+
)
|
|
7543
|
+
.all(keyType, scope, effectiveScopeId);
|
|
7544
|
+
|
|
7545
|
+
const blockedIndices = new Set(rateLimited.map((r) => r.keyIndex));
|
|
7546
|
+
const available: number[] = [];
|
|
7547
|
+
for (let i = 0; i < totalKeys; i++) {
|
|
7548
|
+
if (!blockedIndices.has(i)) available.push(i);
|
|
7549
|
+
}
|
|
7550
|
+
return available;
|
|
7551
|
+
}
|
|
7552
|
+
|
|
7553
|
+
/**
|
|
7554
|
+
* Record that a key was used for a task (upsert key status + update task).
|
|
7555
|
+
*/
|
|
7556
|
+
export function recordKeyUsage(
|
|
7557
|
+
keyType: string,
|
|
7558
|
+
keySuffix: string,
|
|
7559
|
+
keyIndex: number,
|
|
7560
|
+
taskId: string | null,
|
|
7561
|
+
scope = "global",
|
|
7562
|
+
scopeId: string | null = null,
|
|
7563
|
+
): void {
|
|
7564
|
+
const now = new Date().toISOString();
|
|
7565
|
+
const db = getDb();
|
|
7566
|
+
const effectiveScopeId = scopeId ?? "";
|
|
7567
|
+
|
|
7568
|
+
// Upsert key status record
|
|
7569
|
+
db.prepare(
|
|
7570
|
+
`INSERT INTO api_key_status (keyType, keySuffix, keyIndex, scope, scopeId, lastUsedAt, totalUsageCount, updatedAt)
|
|
7571
|
+
VALUES (?, ?, ?, ?, ?, ?, 1, ?)
|
|
7572
|
+
ON CONFLICT(keyType, keySuffix, scope, scopeId)
|
|
7573
|
+
DO UPDATE SET
|
|
7574
|
+
lastUsedAt = excluded.lastUsedAt,
|
|
7575
|
+
totalUsageCount = totalUsageCount + 1,
|
|
7576
|
+
keyIndex = excluded.keyIndex,
|
|
7577
|
+
updatedAt = excluded.updatedAt`,
|
|
7578
|
+
).run(keyType, keySuffix, keyIndex, scope, effectiveScopeId, now, now);
|
|
7579
|
+
|
|
7580
|
+
// Record which key was used on the task
|
|
7581
|
+
if (taskId) {
|
|
7582
|
+
db.prepare(
|
|
7583
|
+
"UPDATE agent_tasks SET credentialKeySuffix = ?, credentialKeyType = ? WHERE id = ?",
|
|
7584
|
+
).run(keySuffix, keyType, taskId);
|
|
7585
|
+
}
|
|
7586
|
+
}
|
|
7587
|
+
|
|
7588
|
+
/**
|
|
7589
|
+
* Mark a key as rate-limited with a retry-after timestamp.
|
|
7590
|
+
*/
|
|
7591
|
+
export function markKeyRateLimited(
|
|
7592
|
+
keyType: string,
|
|
7593
|
+
keySuffix: string,
|
|
7594
|
+
keyIndex: number,
|
|
7595
|
+
rateLimitedUntil: string,
|
|
7596
|
+
scope = "global",
|
|
7597
|
+
scopeId: string | null = null,
|
|
7598
|
+
): void {
|
|
7599
|
+
const now = new Date().toISOString();
|
|
7600
|
+
const effectiveScopeId = scopeId ?? "";
|
|
7601
|
+
getDb()
|
|
7602
|
+
.prepare(
|
|
7603
|
+
`INSERT INTO api_key_status (keyType, keySuffix, keyIndex, scope, scopeId, status, rateLimitedUntil, lastRateLimitAt, rateLimitCount, updatedAt)
|
|
7604
|
+
VALUES (?, ?, ?, ?, ?, 'rate_limited', ?, ?, 1, ?)
|
|
7605
|
+
ON CONFLICT(keyType, keySuffix, scope, scopeId)
|
|
7606
|
+
DO UPDATE SET
|
|
7607
|
+
status = 'rate_limited',
|
|
7608
|
+
rateLimitedUntil = excluded.rateLimitedUntil,
|
|
7609
|
+
lastRateLimitAt = excluded.lastRateLimitAt,
|
|
7610
|
+
rateLimitCount = rateLimitCount + 1,
|
|
7611
|
+
keyIndex = excluded.keyIndex,
|
|
7612
|
+
updatedAt = excluded.updatedAt`,
|
|
7613
|
+
)
|
|
7614
|
+
.run(keyType, keySuffix, keyIndex, scope, effectiveScopeId, rateLimitedUntil, now, now);
|
|
7615
|
+
}
|
|
7616
|
+
|
|
7617
|
+
/**
|
|
7618
|
+
* Get all key status records for a credential type.
|
|
7619
|
+
*/
|
|
7620
|
+
export function getKeyStatuses(
|
|
7621
|
+
keyType?: string,
|
|
7622
|
+
scope?: string,
|
|
7623
|
+
scopeId?: string | null,
|
|
7624
|
+
): ApiKeyStatus[] {
|
|
7625
|
+
const db = getDb();
|
|
7626
|
+
const conditions: string[] = [];
|
|
7627
|
+
const params: string[] = [];
|
|
7628
|
+
|
|
7629
|
+
if (keyType) {
|
|
7630
|
+
conditions.push("keyType = ?");
|
|
7631
|
+
params.push(keyType);
|
|
7632
|
+
}
|
|
7633
|
+
if (scope) {
|
|
7634
|
+
conditions.push("scope = ?");
|
|
7635
|
+
params.push(scope);
|
|
7636
|
+
if (scopeId !== undefined) {
|
|
7637
|
+
conditions.push("scopeId = ?");
|
|
7638
|
+
params.push(scopeId ?? "");
|
|
7639
|
+
}
|
|
7640
|
+
}
|
|
7641
|
+
|
|
7642
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
7643
|
+
return db
|
|
7644
|
+
.prepare<ApiKeyStatus, string[]>(`SELECT * FROM api_key_status ${where} ORDER BY keyIndex`)
|
|
7645
|
+
.all(...params);
|
|
7646
|
+
}
|
|
7647
|
+
|
|
7648
|
+
export interface KeyCostSummary {
|
|
7649
|
+
keyType: string;
|
|
7650
|
+
keySuffix: string;
|
|
7651
|
+
totalCost: number;
|
|
7652
|
+
totalInputTokens: number;
|
|
7653
|
+
totalOutputTokens: number;
|
|
7654
|
+
taskCount: number;
|
|
7655
|
+
}
|
|
7656
|
+
|
|
7657
|
+
/**
|
|
7658
|
+
* Aggregate cost data per API key by joining session_costs through agent_tasks.
|
|
7659
|
+
*/
|
|
7660
|
+
export function getKeyCostSummary(keyType?: string): KeyCostSummary[] {
|
|
7661
|
+
const db = getDb();
|
|
7662
|
+
const conditions = ["t.credentialKeySuffix IS NOT NULL"];
|
|
7663
|
+
const params: string[] = [];
|
|
7664
|
+
|
|
7665
|
+
if (keyType) {
|
|
7666
|
+
conditions.push("t.credentialKeyType = ?");
|
|
7667
|
+
params.push(keyType);
|
|
7668
|
+
}
|
|
7669
|
+
|
|
7670
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
7671
|
+
return db
|
|
7672
|
+
.prepare<KeyCostSummary, string[]>(
|
|
7673
|
+
`SELECT
|
|
7674
|
+
t.credentialKeyType as keyType,
|
|
7675
|
+
t.credentialKeySuffix as keySuffix,
|
|
7676
|
+
COALESCE(SUM(sc.totalCostUsd), 0) as totalCost,
|
|
7677
|
+
COALESCE(SUM(sc.inputTokens), 0) as totalInputTokens,
|
|
7678
|
+
COALESCE(SUM(sc.outputTokens), 0) as totalOutputTokens,
|
|
7679
|
+
COUNT(DISTINCT sc.taskId) as taskCount
|
|
7680
|
+
FROM session_costs sc
|
|
7681
|
+
JOIN agent_tasks t ON sc.taskId = t.id
|
|
7682
|
+
${where}
|
|
7683
|
+
GROUP BY t.credentialKeyType, t.credentialKeySuffix`,
|
|
7684
|
+
)
|
|
7685
|
+
.all(...params);
|
|
7686
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
-- Track API key pool status for rate limit awareness and automatic rotation
|
|
2
|
+
CREATE TABLE IF NOT EXISTS api_key_status (
|
|
3
|
+
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
|
|
4
|
+
-- Which credential type: 'CLAUDE_CODE_OAUTH_TOKEN' or 'ANTHROPIC_API_KEY'
|
|
5
|
+
keyType TEXT NOT NULL,
|
|
6
|
+
-- Last 5 characters of the key (for identification without storing secrets)
|
|
7
|
+
keySuffix TEXT NOT NULL,
|
|
8
|
+
-- Position in the comma-separated credential pool (0-based)
|
|
9
|
+
keyIndex INTEGER NOT NULL,
|
|
10
|
+
-- Scope mirrors swarm_config (global, agent, repo)
|
|
11
|
+
scope TEXT NOT NULL DEFAULT 'global',
|
|
12
|
+
scopeId TEXT NOT NULL DEFAULT '',
|
|
13
|
+
-- Current status
|
|
14
|
+
status TEXT NOT NULL DEFAULT 'available' CHECK(status IN ('available', 'rate_limited')),
|
|
15
|
+
-- When the rate limit expires (ISO timestamp from retry-after)
|
|
16
|
+
rateLimitedUntil TEXT,
|
|
17
|
+
-- Tracking timestamps
|
|
18
|
+
lastUsedAt TEXT,
|
|
19
|
+
lastRateLimitAt TEXT,
|
|
20
|
+
-- Counters
|
|
21
|
+
totalUsageCount INTEGER NOT NULL DEFAULT 0,
|
|
22
|
+
rateLimitCount INTEGER NOT NULL DEFAULT 0,
|
|
23
|
+
-- Metadata
|
|
24
|
+
createdAt TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
25
|
+
updatedAt TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
26
|
+
-- Unique constraint: one entry per key type + suffix + scope.
|
|
27
|
+
-- Known limitation: two different keys sharing the same last 5 chars will collide,
|
|
28
|
+
-- causing the ON CONFLICT upsert to overwrite keyIndex. In practice this is rare
|
|
29
|
+
-- (1 in ~60M for random alphanumeric suffixes) and the impact is minor (wrong index
|
|
30
|
+
-- logged, but rate-limit tracking still works since suffix is the real identifier).
|
|
31
|
+
UNIQUE(keyType, keySuffix, scope, scopeId)
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
CREATE INDEX IF NOT EXISTS idx_api_key_status_lookup
|
|
35
|
+
ON api_key_status(keyType, scope, scopeId, status);
|
|
36
|
+
|
|
37
|
+
-- Track which key was used per task (last 5 chars only)
|
|
38
|
+
ALTER TABLE agent_tasks ADD COLUMN credentialKeySuffix TEXT;
|