@desplega.ai/agent-swarm 1.56.6 → 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/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
|
|
|
@@ -7575,10 +7579,9 @@ export function recordKeyUsage(
|
|
|
7575
7579
|
|
|
7576
7580
|
// Record which key was used on the task
|
|
7577
7581
|
if (taskId) {
|
|
7578
|
-
db.prepare(
|
|
7579
|
-
|
|
7580
|
-
|
|
7581
|
-
);
|
|
7582
|
+
db.prepare(
|
|
7583
|
+
"UPDATE agent_tasks SET credentialKeySuffix = ?, credentialKeyType = ? WHERE id = ?",
|
|
7584
|
+
).run(keySuffix, keyType, taskId);
|
|
7582
7585
|
}
|
|
7583
7586
|
}
|
|
7584
7587
|
|
|
@@ -7641,3 +7644,43 @@ export function getKeyStatuses(
|
|
|
7641
7644
|
.prepare<ApiKeyStatus, string[]>(`SELECT * FROM api_key_status ${where} ORDER BY keyIndex`)
|
|
7642
7645
|
.all(...params);
|
|
7643
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
|
+
}
|
package/src/http/api-keys.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { IncomingMessage, ServerResponse } from "node:http";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import {
|
|
4
4
|
getAvailableKeyIndices,
|
|
5
|
+
getKeyCostSummary,
|
|
5
6
|
getKeyStatuses,
|
|
6
7
|
markKeyRateLimited,
|
|
7
8
|
recordKeyUsage,
|
|
@@ -93,6 +94,22 @@ const listStatuses = route({
|
|
|
93
94
|
auth: { apiKey: true },
|
|
94
95
|
});
|
|
95
96
|
|
|
97
|
+
const getCosts = route({
|
|
98
|
+
method: "get",
|
|
99
|
+
path: "/api/keys/costs",
|
|
100
|
+
pattern: ["api", "keys", "costs"],
|
|
101
|
+
summary: "Get aggregated cost data per API key",
|
|
102
|
+
tags: ["API Keys"],
|
|
103
|
+
query: z.object({
|
|
104
|
+
keyType: z.string().optional(),
|
|
105
|
+
}),
|
|
106
|
+
responses: {
|
|
107
|
+
200: { description: "Per-key cost aggregation" },
|
|
108
|
+
401: { description: "Unauthorized" },
|
|
109
|
+
},
|
|
110
|
+
auth: { apiKey: true },
|
|
111
|
+
});
|
|
112
|
+
|
|
96
113
|
// ─── Handler ─────────────────────────────────────────────────────────────────
|
|
97
114
|
|
|
98
115
|
export async function handleApiKeys(
|
|
@@ -149,6 +166,21 @@ export async function handleApiKeys(
|
|
|
149
166
|
return true;
|
|
150
167
|
}
|
|
151
168
|
|
|
169
|
+
// GET /api/keys/costs
|
|
170
|
+
if (getCosts.match(req.method, pathSegments)) {
|
|
171
|
+
const parsed = await getCosts.parse(req, res, pathSegments, queryParams);
|
|
172
|
+
if (!parsed) return true;
|
|
173
|
+
|
|
174
|
+
const { keyType } = parsed.query;
|
|
175
|
+
try {
|
|
176
|
+
const costs = getKeyCostSummary(keyType);
|
|
177
|
+
json(res, { success: true, costs });
|
|
178
|
+
} catch (err) {
|
|
179
|
+
jsonError(res, err instanceof Error ? err.message : "Failed to get key costs", 500);
|
|
180
|
+
}
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
|
|
152
184
|
// GET /api/keys/status
|
|
153
185
|
if (listStatuses.match(req.method, pathSegments)) {
|
|
154
186
|
const parsed = await listStatuses.parse(req, res, pathSegments, queryParams);
|
|
@@ -103,12 +103,16 @@ describe("resolveCredentialPools", () => {
|
|
|
103
103
|
expect(env.ANTHROPIC_API_KEY).toBe("key-ccc33");
|
|
104
104
|
});
|
|
105
105
|
|
|
106
|
-
test("
|
|
106
|
+
test("single keys are tracked with index 0", async () => {
|
|
107
107
|
const env: Record<string, string | undefined> = {
|
|
108
108
|
ANTHROPIC_API_KEY: "single-key",
|
|
109
109
|
};
|
|
110
110
|
const selections = await resolveCredentialPools(env);
|
|
111
|
-
expect(selections.length).toBe(
|
|
111
|
+
expect(selections.length).toBe(1);
|
|
112
|
+
expect(selections[0]!.index).toBe(0);
|
|
113
|
+
expect(selections[0]!.total).toBe(1);
|
|
114
|
+
expect(selections[0]!.keySuffix).toBe("e-key");
|
|
115
|
+
expect(selections[0]!.keyType).toBe("ANTHROPIC_API_KEY");
|
|
112
116
|
expect(env.ANTHROPIC_API_KEY).toBe("single-key");
|
|
113
117
|
});
|
|
114
118
|
});
|
package/src/types.ts
CHANGED
|
@@ -149,6 +149,10 @@ export const AgentTaskSchema = z.object({
|
|
|
149
149
|
peakContextPercent: z.number().min(0).max(100).optional(),
|
|
150
150
|
totalContextTokensUsed: z.number().int().min(0).optional(),
|
|
151
151
|
contextWindowSize: z.number().int().min(0).optional(),
|
|
152
|
+
|
|
153
|
+
// Credential tracking
|
|
154
|
+
credentialKeySuffix: z.string().optional(),
|
|
155
|
+
credentialKeyType: z.string().optional(),
|
|
152
156
|
});
|
|
153
157
|
|
|
154
158
|
export const AgentStatusSchema = z.enum(["idle", "busy", "offline"]);
|
package/src/utils/credentials.ts
CHANGED
|
@@ -91,8 +91,8 @@ async function fetchAvailableIndices(
|
|
|
91
91
|
const availableIndicesMap: Record<string, number[]> = {};
|
|
92
92
|
for (const envVar of CREDENTIAL_POOL_VARS) {
|
|
93
93
|
const val = env[envVar];
|
|
94
|
-
if (val
|
|
95
|
-
const totalKeys = val.split(",").filter((s) => s.trim()).length;
|
|
94
|
+
if (val) {
|
|
95
|
+
const totalKeys = val.includes(",") ? val.split(",").filter((s) => s.trim()).length : 1;
|
|
96
96
|
try {
|
|
97
97
|
const resp = await fetch(
|
|
98
98
|
`${apiUrl}/api/keys/available?keyType=${encodeURIComponent(envVar)}&totalKeys=${totalKeys}`,
|
|
@@ -134,7 +134,7 @@ export async function resolveCredentialPools(
|
|
|
134
134
|
const selections: CredentialSelection[] = [];
|
|
135
135
|
for (const envVar of CREDENTIAL_POOL_VARS) {
|
|
136
136
|
const val = env[envVar];
|
|
137
|
-
if (val
|
|
137
|
+
if (val) {
|
|
138
138
|
const available = availableIndicesMap?.[envVar];
|
|
139
139
|
const result = selectCredential(val, available, envVar);
|
|
140
140
|
env[envVar] = result.selected;
|