@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@desplega.ai/agent-swarm",
3
- "version": "1.56.6",
3
+ "version": "1.57.0",
4
4
  "description": "Multi-agent orchestration for Claude Code, Codex, Gemini CLI, and other AI coding assistants",
5
5
  "license": "MIT",
6
6
  "author": "desplega.sh <contact@desplega.sh>",
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("UPDATE agent_tasks SET credentialKeySuffix = ? WHERE id = ?").run(
7579
- keySuffix,
7580
- taskId,
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
+ }
@@ -0,0 +1,2 @@
1
+ -- Store which credential type (env var name) was used per task
2
+ ALTER TABLE agent_tasks ADD COLUMN credentialKeyType TEXT;
@@ -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("non-pool vars are unchanged", async () => {
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(0);
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"]);
@@ -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?.includes(",")) {
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?.includes(",")) {
137
+ if (val) {
138
138
  const available = availableIndicesMap?.[envVar];
139
139
  const result = selectCredential(val, available, envVar);
140
140
  env[envVar] = result.selected;