@oh-my-pi/pi-ai 17.1.1 → 17.1.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 +16 -0
- package/dist/types/auth-broker/client.d.ts +12 -1
- package/dist/types/auth-broker/remote-store.d.ts +10 -1
- package/dist/types/auth-broker/types.d.ts +21 -1
- package/dist/types/auth-broker/wire-schemas.d.ts +55 -0
- package/dist/types/auth-storage.d.ts +35 -1
- package/dist/types/providers/anthropic-wire.d.ts +29 -2
- package/dist/types/providers/anthropic.d.ts +1 -1
- package/dist/types/types.d.ts +20 -1
- package/dist/types/usage.d.ts +50 -0
- package/package.json +4 -4
- package/src/auth-broker/client.ts +39 -0
- package/src/auth-broker/remote-store.ts +76 -3
- package/src/auth-broker/server.ts +40 -0
- package/src/auth-broker/types.ts +25 -1
- package/src/auth-broker/wire-schemas.ts +71 -0
- package/src/auth-storage.ts +198 -0
- package/src/providers/anthropic-messages-server.ts +47 -3
- package/src/providers/anthropic-wire.ts +41 -0
- package/src/providers/anthropic.ts +81 -33
- package/src/providers/transform-messages.ts +8 -0
- package/src/types.ts +23 -0
- package/src/usage/zai.ts +3 -3
- package/src/usage.ts +55 -0
- package/src/utils/leaked-thinking-stream.ts +38 -1
|
@@ -15,6 +15,7 @@ import type { AuthStorage, StoredCredentialBlock } from "../auth-storage";
|
|
|
15
15
|
import { parseBind } from "../utils/parse-bind";
|
|
16
16
|
import { AuthBrokerRefresher, type AuthBrokerRefresherSchedule } from "./refresher";
|
|
17
17
|
import type {
|
|
18
|
+
ClientUsageReportRequest,
|
|
18
19
|
CredentialBlockResponse,
|
|
19
20
|
CredentialBlockSnapshot,
|
|
20
21
|
CredentialBlocksDeleteResponse,
|
|
@@ -37,6 +38,7 @@ import {
|
|
|
37
38
|
DEFAULT_STREAM_KEEPALIVE_MS,
|
|
38
39
|
} from "./types";
|
|
39
40
|
import {
|
|
41
|
+
clientUsageReportRequestSchema,
|
|
40
42
|
credentialBlockRequestSchema,
|
|
41
43
|
credentialDisableRequestSchema,
|
|
42
44
|
credentialUploadRequestSchema,
|
|
@@ -608,6 +610,44 @@ export function startAuthBroker(opts: AuthBrokerServerOptions): AuthBrokerServer
|
|
|
608
610
|
return json(502, { error: message });
|
|
609
611
|
}
|
|
610
612
|
}
|
|
613
|
+
if (req.method === "GET" && pathname === "/v1/usage/history") {
|
|
614
|
+
const sinceMsRaw = url.searchParams.get("sinceMs");
|
|
615
|
+
const sinceMsParsed = sinceMsRaw === null ? undefined : Number.parseInt(sinceMsRaw, 10);
|
|
616
|
+
const sinceMs =
|
|
617
|
+
sinceMsParsed !== undefined && Number.isFinite(sinceMsParsed) ? sinceMsParsed : undefined;
|
|
618
|
+
const provider = url.searchParams.get("provider") ?? undefined;
|
|
619
|
+
const entries = opts.storage.listUsageHistory({ sinceMs, provider });
|
|
620
|
+
logger.info("auth-broker usage history served", { peer, entries: entries.length, sinceMs, provider });
|
|
621
|
+
return json(200, { generatedAt: Date.now(), entries });
|
|
622
|
+
}
|
|
623
|
+
if (req.method === "POST" && pathname === "/v1/usage/observed") {
|
|
624
|
+
const parsed = await parseBody(req, clientUsageReportRequestSchema);
|
|
625
|
+
if (!parsed.ok) return parsed.response;
|
|
626
|
+
// Arktype's inferred union collides the `entries` field with
|
|
627
|
+
// Array.prototype.entries; the schema already validated the shape.
|
|
628
|
+
const report = parsed.data as ClientUsageReportRequest;
|
|
629
|
+
try {
|
|
630
|
+
const recorded = opts.storage.recordClientUsage(report);
|
|
631
|
+
if (!recorded) return json(501, { error: "broker store does not persist client usage" });
|
|
632
|
+
logger.debug("auth-broker client usage recorded", {
|
|
633
|
+
peer,
|
|
634
|
+
installId: report.installId,
|
|
635
|
+
hostname: report.hostname,
|
|
636
|
+
entries: report.entries.length,
|
|
637
|
+
});
|
|
638
|
+
return json(200, { ok: true });
|
|
639
|
+
} catch (error) {
|
|
640
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
641
|
+
logger.warn("auth-broker client usage record failed", { peer, error: message });
|
|
642
|
+
return json(500, { error: message });
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
if (req.method === "GET" && pathname === "/v1/usage/clients") {
|
|
646
|
+
const sinceMsRaw = url.searchParams.get("sinceMs");
|
|
647
|
+
const sinceMsParsed = sinceMsRaw === null ? Number.NaN : Number.parseInt(sinceMsRaw, 10);
|
|
648
|
+
const summary = opts.storage.getClientUsageSummary(Number.isFinite(sinceMsParsed) ? sinceMsParsed : 0);
|
|
649
|
+
return json(200, { generatedAt: Date.now(), clients: summary.clients });
|
|
650
|
+
}
|
|
611
651
|
if (req.method === "POST" && pathname === "/v1/usage/stale") {
|
|
612
652
|
try {
|
|
613
653
|
opts.storage.invalidateUsageCache?.();
|
package/src/auth-broker/types.ts
CHANGED
|
@@ -12,7 +12,7 @@ import type {
|
|
|
12
12
|
AuthCredentialSnapshotEntry,
|
|
13
13
|
StoredCredentialBlock,
|
|
14
14
|
} from "../auth-storage";
|
|
15
|
-
import type { UsageReport } from "../usage";
|
|
15
|
+
import type { ClientUsageClientSummary, ClientUsageReport, UsageHistoryEntry, UsageReport } from "../usage";
|
|
16
16
|
|
|
17
17
|
/** GET /v1/healthz response body. */
|
|
18
18
|
export interface HealthzResponse {
|
|
@@ -47,6 +47,30 @@ export interface UsageResponse {
|
|
|
47
47
|
reports: UsageReport[];
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* GET /v1/usage/history response body. Entries come from the broker host's
|
|
52
|
+
* durable `usage_history` — the broker performs every upstream usage fetch in
|
|
53
|
+
* broker deployments, so this is the only complete utilization record.
|
|
54
|
+
*/
|
|
55
|
+
export interface UsageHistoryResponse {
|
|
56
|
+
generatedAt: number;
|
|
57
|
+
entries: UsageHistoryEntry[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** POST /v1/usage/observed request body — one client's batched observed usage. */
|
|
61
|
+
export type ClientUsageReportRequest = ClientUsageReport;
|
|
62
|
+
|
|
63
|
+
/** POST /v1/usage/observed response body. */
|
|
64
|
+
export interface ClientUsageReportResponse {
|
|
65
|
+
ok: boolean;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** GET /v1/usage/clients response body — per-client token burn aggregates. */
|
|
69
|
+
export interface ClientUsageSummaryResponse {
|
|
70
|
+
generatedAt: number;
|
|
71
|
+
clients: ClientUsageClientSummary[];
|
|
72
|
+
}
|
|
73
|
+
|
|
50
74
|
/** POST /v1/credential/:id/refresh response body. */
|
|
51
75
|
export interface CredentialRefreshResponse {
|
|
52
76
|
entry: AuthCredentialSnapshotEntry;
|
|
@@ -230,6 +230,77 @@ export const usageResponseSchema = type({
|
|
|
230
230
|
reports: arkUsageReportSchema.array(),
|
|
231
231
|
});
|
|
232
232
|
|
|
233
|
+
const usageHistoryEntrySchema = type({
|
|
234
|
+
recordedAt: "number",
|
|
235
|
+
provider: "string",
|
|
236
|
+
accountKey: "string",
|
|
237
|
+
"email?": "string",
|
|
238
|
+
"accountId?": "string",
|
|
239
|
+
limitId: "string",
|
|
240
|
+
label: "string",
|
|
241
|
+
"windowLabel?": "string",
|
|
242
|
+
"usedFraction?": "number",
|
|
243
|
+
"status?": "'ok' | 'warning' | 'exhausted' | 'unknown'",
|
|
244
|
+
"resetsAt?": "number",
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
/** Broker `/v1/usage/history` response — recorded usage-limit snapshots, oldest first. */
|
|
248
|
+
export const usageHistoryResponseSchema = type({
|
|
249
|
+
"+": "reject",
|
|
250
|
+
generatedAt: "number",
|
|
251
|
+
entries: usageHistoryEntrySchema.array(),
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const observedUsageEntrySchema = type({
|
|
255
|
+
at: "number",
|
|
256
|
+
provider: "string",
|
|
257
|
+
model: "string",
|
|
258
|
+
requests: "number",
|
|
259
|
+
inputTokens: "number",
|
|
260
|
+
outputTokens: "number",
|
|
261
|
+
cacheReadTokens: "number",
|
|
262
|
+
cacheWriteTokens: "number",
|
|
263
|
+
costUsd: "number",
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
/** Broker `POST /v1/usage/observed` request — one client's batched observed usage. */
|
|
267
|
+
export const clientUsageReportRequestSchema = type({
|
|
268
|
+
"+": "reject",
|
|
269
|
+
installId: "string",
|
|
270
|
+
"hostname?": "string",
|
|
271
|
+
entries: observedUsageEntrySchema.array(),
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
export const clientUsageReportResponseSchema = type({
|
|
275
|
+
"+": "reject",
|
|
276
|
+
ok: "boolean",
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
const clientProviderUsageSchema = type({
|
|
280
|
+
provider: "string",
|
|
281
|
+
requests: "number",
|
|
282
|
+
inputTokens: "number",
|
|
283
|
+
outputTokens: "number",
|
|
284
|
+
cacheReadTokens: "number",
|
|
285
|
+
cacheWriteTokens: "number",
|
|
286
|
+
costUsd: "number",
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
const clientUsageClientSummarySchema = type({
|
|
290
|
+
installId: "string",
|
|
291
|
+
"hostname?": "string",
|
|
292
|
+
firstSeen: "number",
|
|
293
|
+
lastSeen: "number",
|
|
294
|
+
providers: clientProviderUsageSchema.array(),
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
/** Broker `GET /v1/usage/clients` response — per-client token burn aggregates. */
|
|
298
|
+
export const clientUsageSummaryResponseSchema = type({
|
|
299
|
+
"+": "reject",
|
|
300
|
+
generatedAt: "number",
|
|
301
|
+
clients: clientUsageClientSummarySchema.array(),
|
|
302
|
+
});
|
|
303
|
+
|
|
233
304
|
// ─── Refresh ───────────────────────────────────────────────────────────────
|
|
234
305
|
|
|
235
306
|
export const credentialRefreshResponseSchema = type({
|
package/src/auth-storage.ts
CHANGED
|
@@ -28,8 +28,12 @@ import type {
|
|
|
28
28
|
import { getEnvApiKey, getEnvApiKeyName } from "./stream";
|
|
29
29
|
import type { Provider } from "./types";
|
|
30
30
|
import type {
|
|
31
|
+
ClientProviderUsage,
|
|
32
|
+
ClientUsageReport,
|
|
33
|
+
ClientUsageSummary,
|
|
31
34
|
CredentialRankingContext,
|
|
32
35
|
CredentialRankingStrategy,
|
|
36
|
+
ObservedUsageEntry,
|
|
33
37
|
UsageCostHistoryEntry,
|
|
34
38
|
UsageCostHistoryQuery,
|
|
35
39
|
UsageCredential,
|
|
@@ -399,6 +403,16 @@ export interface AuthCredentialStore {
|
|
|
399
403
|
listUsageCosts?(query?: UsageCostHistoryQuery): UsageCostHistoryEntry[];
|
|
400
404
|
/** Read recorded usage-limit snapshots, oldest first. */
|
|
401
405
|
listUsageHistory?(query?: UsageHistoryQuery): UsageHistoryEntry[];
|
|
406
|
+
/**
|
|
407
|
+
* Client hook: forward locally observed request usage. Remote broker stores
|
|
408
|
+
* batch these to the broker so it can attribute token burn per install;
|
|
409
|
+
* local stores omit it and observation is skipped.
|
|
410
|
+
*/
|
|
411
|
+
recordObservedUsage?(entries: ObservedUsageEntry[]): void;
|
|
412
|
+
/** Broker host: persist one client's observed-usage report. */
|
|
413
|
+
recordClientUsage?(report: ClientUsageReport): void;
|
|
414
|
+
/** Broker host: aggregate recorded per-client usage since a timestamp. */
|
|
415
|
+
getClientUsageSummary?(sinceMs: number): ClientUsageSummary;
|
|
402
416
|
/**
|
|
403
417
|
* Optional store-supplied OAuth refresh. When present, `AuthStorage` uses
|
|
404
418
|
* it before the per-provider local refresh path. `RemoteAuthCredentialStore`
|
|
@@ -624,6 +638,12 @@ const USAGE_LAST_GOOD_RETENTION_MS = 24 * 60 * 60_000;
|
|
|
624
638
|
* unnecessary — 1 row/hour is ~9k rows per account window per year.
|
|
625
639
|
*/
|
|
626
640
|
const USAGE_HISTORY_BUCKET_MS = 60 * 60_000;
|
|
641
|
+
/**
|
|
642
|
+
* Merge client observed-usage flushes into at most one row per 5 minutes per
|
|
643
|
+
* (install, provider, model): ~300 rows/day per active model per client
|
|
644
|
+
* instead of one row per 10s flush.
|
|
645
|
+
*/
|
|
646
|
+
const CLIENT_USAGE_BUCKET_MS = 5 * 60_000;
|
|
627
647
|
/**
|
|
628
648
|
* Per-credential cool-down after a usage fetch fails. While this window is
|
|
629
649
|
* active we serve the last successful value to avoid dropping the credential
|
|
@@ -3142,6 +3162,56 @@ export class AuthStorage {
|
|
|
3142
3162
|
}
|
|
3143
3163
|
}
|
|
3144
3164
|
|
|
3165
|
+
/**
|
|
3166
|
+
* Forward one completed request's usage to the store's observer hook.
|
|
3167
|
+
* Broker-backed stores batch these into per-install reports so the broker
|
|
3168
|
+
* can track actual token burn per client; local stores have no hook and
|
|
3169
|
+
* the call is a no-op.
|
|
3170
|
+
*/
|
|
3171
|
+
recordObservedUsage(entry: {
|
|
3172
|
+
provider: Provider;
|
|
3173
|
+
model: string;
|
|
3174
|
+
usage: { input: number; output: number; cacheRead: number; cacheWrite: number };
|
|
3175
|
+
costUsd?: number;
|
|
3176
|
+
at?: number;
|
|
3177
|
+
}): void {
|
|
3178
|
+
const record = this.#store.recordObservedUsage;
|
|
3179
|
+
if (!record) return;
|
|
3180
|
+
try {
|
|
3181
|
+
record.call(this.#store, [
|
|
3182
|
+
{
|
|
3183
|
+
at: entry.at ?? Date.now(),
|
|
3184
|
+
provider: entry.provider,
|
|
3185
|
+
model: entry.model,
|
|
3186
|
+
requests: 1,
|
|
3187
|
+
inputTokens: entry.usage.input,
|
|
3188
|
+
outputTokens: entry.usage.output,
|
|
3189
|
+
cacheReadTokens: entry.usage.cacheRead,
|
|
3190
|
+
cacheWriteTokens: entry.usage.cacheWrite,
|
|
3191
|
+
costUsd: Number.isFinite(entry.costUsd) ? (entry.costUsd ?? 0) : 0,
|
|
3192
|
+
},
|
|
3193
|
+
]);
|
|
3194
|
+
} catch (error) {
|
|
3195
|
+
this.#usageLogger?.debug("observed usage record failed", {
|
|
3196
|
+
provider: entry.provider,
|
|
3197
|
+
error: String(error),
|
|
3198
|
+
});
|
|
3199
|
+
}
|
|
3200
|
+
}
|
|
3201
|
+
|
|
3202
|
+
/** Broker host: persist one client's observed-usage report (per-install token burn). */
|
|
3203
|
+
recordClientUsage(report: ClientUsageReport): boolean {
|
|
3204
|
+
const record = this.#store.recordClientUsage;
|
|
3205
|
+
if (!record) return false;
|
|
3206
|
+
record.call(this.#store, report);
|
|
3207
|
+
return true;
|
|
3208
|
+
}
|
|
3209
|
+
|
|
3210
|
+
/** Broker host: aggregate recorded per-client usage since `sinceMs`. */
|
|
3211
|
+
getClientUsageSummary(sinceMs: number): ClientUsageSummary {
|
|
3212
|
+
return this.#store.getClientUsageSummary?.(sinceMs) ?? { clients: [] };
|
|
3213
|
+
}
|
|
3214
|
+
|
|
3145
3215
|
#resolveObservedUsageCredential(provider: Provider, sessionId?: string): UsageCredential | undefined {
|
|
3146
3216
|
const entries = this.#getStoredCredentials(provider);
|
|
3147
3217
|
const sessionCredential = this.#getSessionCredential(provider, sessionId);
|
|
@@ -6603,6 +6673,27 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
6603
6673
|
);
|
|
6604
6674
|
CREATE INDEX IF NOT EXISTS idx_usage_cost_history_lookup ON usage_cost_history(provider, account_key, recorded_at);
|
|
6605
6675
|
CREATE INDEX IF NOT EXISTS idx_usage_history_recorded ON usage_history(recorded_at);
|
|
6676
|
+
CREATE TABLE IF NOT EXISTS clients (
|
|
6677
|
+
install_id TEXT PRIMARY KEY,
|
|
6678
|
+
hostname TEXT,
|
|
6679
|
+
first_seen INTEGER NOT NULL,
|
|
6680
|
+
last_seen INTEGER NOT NULL
|
|
6681
|
+
);
|
|
6682
|
+
CREATE TABLE IF NOT EXISTS client_usage (
|
|
6683
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
6684
|
+
recorded_at INTEGER NOT NULL,
|
|
6685
|
+
install_id TEXT NOT NULL,
|
|
6686
|
+
provider TEXT NOT NULL,
|
|
6687
|
+
model TEXT NOT NULL,
|
|
6688
|
+
requests INTEGER NOT NULL,
|
|
6689
|
+
input_tokens INTEGER NOT NULL,
|
|
6690
|
+
output_tokens INTEGER NOT NULL,
|
|
6691
|
+
cache_read_tokens INTEGER NOT NULL,
|
|
6692
|
+
cache_write_tokens INTEGER NOT NULL,
|
|
6693
|
+
cost_usd REAL NOT NULL DEFAULT 0
|
|
6694
|
+
);
|
|
6695
|
+
CREATE INDEX IF NOT EXISTS idx_client_usage_series ON client_usage(install_id, provider, model, recorded_at);
|
|
6696
|
+
CREATE INDEX IF NOT EXISTS idx_client_usage_recorded ON client_usage(recorded_at);
|
|
6606
6697
|
`);
|
|
6607
6698
|
|
|
6608
6699
|
if (!this.#authCredentialsTableExists()) {
|
|
@@ -7381,6 +7472,113 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
7381
7472
|
}
|
|
7382
7473
|
}
|
|
7383
7474
|
|
|
7475
|
+
recordClientUsage(report: ClientUsageReport): void {
|
|
7476
|
+
const now = Date.now();
|
|
7477
|
+
this.#db
|
|
7478
|
+
.query(
|
|
7479
|
+
`INSERT INTO clients (install_id, hostname, first_seen, last_seen) VALUES (?, ?, ?, ?)
|
|
7480
|
+
ON CONFLICT(install_id) DO UPDATE SET hostname = COALESCE(excluded.hostname, hostname), last_seen = excluded.last_seen`,
|
|
7481
|
+
)
|
|
7482
|
+
.run(report.installId, report.hostname ?? null, now, now);
|
|
7483
|
+
const findBucket = this.#db.query(
|
|
7484
|
+
`SELECT id FROM client_usage
|
|
7485
|
+
WHERE install_id = ? AND provider = ? AND model = ? AND recorded_at >= ?
|
|
7486
|
+
ORDER BY recorded_at DESC LIMIT 1`,
|
|
7487
|
+
);
|
|
7488
|
+
const merge = this.#db.query(
|
|
7489
|
+
`UPDATE client_usage SET recorded_at = ?, requests = requests + ?, input_tokens = input_tokens + ?,
|
|
7490
|
+
output_tokens = output_tokens + ?, cache_read_tokens = cache_read_tokens + ?,
|
|
7491
|
+
cache_write_tokens = cache_write_tokens + ?, cost_usd = cost_usd + ? WHERE id = ?`,
|
|
7492
|
+
);
|
|
7493
|
+
const insert = this.#db.query(
|
|
7494
|
+
`INSERT INTO client_usage (recorded_at, install_id, provider, model, requests, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, cost_usd)
|
|
7495
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
7496
|
+
);
|
|
7497
|
+
for (const entry of report.entries) {
|
|
7498
|
+
// Merge into the newest row of the same (install, provider, model)
|
|
7499
|
+
// bucket so 10s client flushes don't accrete one row apiece forever.
|
|
7500
|
+
const bucketFloor = entry.at - CLIENT_USAGE_BUCKET_MS;
|
|
7501
|
+
const existing = findBucket.get(report.installId, entry.provider, entry.model, bucketFloor) as {
|
|
7502
|
+
id: number;
|
|
7503
|
+
} | null;
|
|
7504
|
+
if (existing) {
|
|
7505
|
+
merge.run(
|
|
7506
|
+
entry.at,
|
|
7507
|
+
entry.requests,
|
|
7508
|
+
entry.inputTokens,
|
|
7509
|
+
entry.outputTokens,
|
|
7510
|
+
entry.cacheReadTokens,
|
|
7511
|
+
entry.cacheWriteTokens,
|
|
7512
|
+
entry.costUsd,
|
|
7513
|
+
existing.id,
|
|
7514
|
+
);
|
|
7515
|
+
continue;
|
|
7516
|
+
}
|
|
7517
|
+
insert.run(
|
|
7518
|
+
entry.at,
|
|
7519
|
+
report.installId,
|
|
7520
|
+
entry.provider,
|
|
7521
|
+
entry.model,
|
|
7522
|
+
entry.requests,
|
|
7523
|
+
entry.inputTokens,
|
|
7524
|
+
entry.outputTokens,
|
|
7525
|
+
entry.cacheReadTokens,
|
|
7526
|
+
entry.cacheWriteTokens,
|
|
7527
|
+
entry.costUsd,
|
|
7528
|
+
);
|
|
7529
|
+
}
|
|
7530
|
+
}
|
|
7531
|
+
|
|
7532
|
+
getClientUsageSummary(sinceMs: number): ClientUsageSummary {
|
|
7533
|
+
const clients = this.#db
|
|
7534
|
+
.query("SELECT install_id, hostname, first_seen, last_seen FROM clients ORDER BY last_seen DESC")
|
|
7535
|
+
.all() as Array<{ install_id: string; hostname: string | null; first_seen: number; last_seen: number }>;
|
|
7536
|
+
const aggregates = this.#db
|
|
7537
|
+
.query(
|
|
7538
|
+
`SELECT install_id, provider, SUM(requests) requests, SUM(input_tokens) input_tokens,
|
|
7539
|
+
SUM(output_tokens) output_tokens, SUM(cache_read_tokens) cache_read_tokens,
|
|
7540
|
+
SUM(cache_write_tokens) cache_write_tokens, SUM(cost_usd) cost_usd
|
|
7541
|
+
FROM client_usage WHERE recorded_at >= ? GROUP BY install_id, provider
|
|
7542
|
+
ORDER BY install_id, SUM(input_tokens + output_tokens + cache_read_tokens + cache_write_tokens) DESC`,
|
|
7543
|
+
)
|
|
7544
|
+
.all(sinceMs) as Array<{
|
|
7545
|
+
install_id: string;
|
|
7546
|
+
provider: string;
|
|
7547
|
+
requests: number;
|
|
7548
|
+
input_tokens: number;
|
|
7549
|
+
output_tokens: number;
|
|
7550
|
+
cache_read_tokens: number;
|
|
7551
|
+
cache_write_tokens: number;
|
|
7552
|
+
cost_usd: number;
|
|
7553
|
+
}>;
|
|
7554
|
+
const providersByInstall = new Map<string, ClientProviderUsage[]>();
|
|
7555
|
+
for (const row of aggregates) {
|
|
7556
|
+
let list = providersByInstall.get(row.install_id);
|
|
7557
|
+
if (!list) {
|
|
7558
|
+
list = [];
|
|
7559
|
+
providersByInstall.set(row.install_id, list);
|
|
7560
|
+
}
|
|
7561
|
+
list.push({
|
|
7562
|
+
provider: row.provider,
|
|
7563
|
+
requests: row.requests,
|
|
7564
|
+
inputTokens: row.input_tokens,
|
|
7565
|
+
outputTokens: row.output_tokens,
|
|
7566
|
+
cacheReadTokens: row.cache_read_tokens,
|
|
7567
|
+
cacheWriteTokens: row.cache_write_tokens,
|
|
7568
|
+
costUsd: row.cost_usd,
|
|
7569
|
+
});
|
|
7570
|
+
}
|
|
7571
|
+
return {
|
|
7572
|
+
clients: clients.map(client => ({
|
|
7573
|
+
installId: client.install_id,
|
|
7574
|
+
hostname: client.hostname ?? undefined,
|
|
7575
|
+
firstSeen: client.first_seen,
|
|
7576
|
+
lastSeen: client.last_seen,
|
|
7577
|
+
providers: providersByInstall.get(client.install_id) ?? [],
|
|
7578
|
+
})),
|
|
7579
|
+
};
|
|
7580
|
+
}
|
|
7581
|
+
|
|
7384
7582
|
// ─── Convenience methods for CLI ────────────────────────────────────────
|
|
7385
7583
|
|
|
7386
7584
|
/**
|
|
@@ -4,6 +4,7 @@ import { type } from "arktype";
|
|
|
4
4
|
import { captureRequestHeaders, resolvePromptCacheKey } from "../auth-gateway/http";
|
|
5
5
|
import * as AIError from "../error";
|
|
6
6
|
import type {
|
|
7
|
+
AnthropicServerToolContent,
|
|
7
8
|
AssistantMessage,
|
|
8
9
|
AssistantMessageEventStream,
|
|
9
10
|
Message,
|
|
@@ -26,6 +27,7 @@ import {
|
|
|
26
27
|
type AnthropicUserContentBlock,
|
|
27
28
|
anthropicMessagesRequestSchema,
|
|
28
29
|
} from "./anthropic-messages-server-schema";
|
|
30
|
+
import { isAnthropicWebSearchHistoryBlock } from "./anthropic-wire";
|
|
29
31
|
|
|
30
32
|
/**
|
|
31
33
|
* Anthropic Messages API (https://docs.anthropic.com/en/api/messages) ↔ pi-ai
|
|
@@ -181,8 +183,8 @@ function walkUserContent(
|
|
|
181
183
|
|
|
182
184
|
function walkAssistantContent(
|
|
183
185
|
blocks: string | AnthropicAssistantContentBlock[],
|
|
184
|
-
): (TextContent | ThinkingContent | RedactedThinkingContent | ToolCall)[] {
|
|
185
|
-
const out: (TextContent | ThinkingContent | RedactedThinkingContent | ToolCall)[] = [];
|
|
186
|
+
): (TextContent | ThinkingContent | RedactedThinkingContent | AnthropicServerToolContent | ToolCall)[] {
|
|
187
|
+
const out: (TextContent | ThinkingContent | RedactedThinkingContent | AnthropicServerToolContent | ToolCall)[] = [];
|
|
186
188
|
if (typeof blocks === "string") {
|
|
187
189
|
if (blocks.length > 0) out.push({ type: "text", text: blocks });
|
|
188
190
|
return out;
|
|
@@ -212,8 +214,24 @@ function walkAssistantContent(
|
|
|
212
214
|
: {},
|
|
213
215
|
});
|
|
214
216
|
break;
|
|
217
|
+
case "server_tool_use":
|
|
218
|
+
case "web_search_tool_result":
|
|
219
|
+
if (isAnthropicWebSearchHistoryBlock(block)) {
|
|
220
|
+
// Native web-search call/result. Anthropic requires these
|
|
221
|
+
// replayed verbatim (encrypted_content included), so retain
|
|
222
|
+
// the block instead of flattening it to text.
|
|
223
|
+
out.push({ type: "anthropicServerTool", block: { ...block } });
|
|
224
|
+
} else {
|
|
225
|
+
// Other server tools use distinct result block types that omp
|
|
226
|
+
// cannot yet replay atomically. Flatten both sides rather than
|
|
227
|
+
// persisting a lone server_tool_use without its matching result.
|
|
228
|
+
const unknown = block as { type: string };
|
|
229
|
+
warnUnknownBlockType("assistant", unknown.type);
|
|
230
|
+
out.push({ type: "text", text: describeUnknownBlock(unknown) });
|
|
231
|
+
}
|
|
232
|
+
break;
|
|
215
233
|
default: {
|
|
216
|
-
// Unknown assistant variant (
|
|
234
|
+
// Unknown assistant variant (mcp_tool_use, code_execution_*, …).
|
|
217
235
|
// Flatten to a text placeholder; warn once per unknown type.
|
|
218
236
|
const unknown = block as { type: string };
|
|
219
237
|
warnUnknownBlockType("assistant", unknown.type);
|
|
@@ -457,6 +475,9 @@ function encodeContentBlocks(message: AssistantMessage): Record<string, unknown>
|
|
|
457
475
|
case "redactedThinking":
|
|
458
476
|
blocks.push({ type: "redacted_thinking", data: c.data });
|
|
459
477
|
break;
|
|
478
|
+
case "anthropicServerTool":
|
|
479
|
+
blocks.push(c.block);
|
|
480
|
+
break;
|
|
460
481
|
case "toolCall":
|
|
461
482
|
blocks.push({ type: "tool_use", id: c.id, name: c.name, input: c.arguments ?? {} });
|
|
462
483
|
break;
|
|
@@ -575,6 +596,25 @@ export function encodeStream(
|
|
|
575
596
|
);
|
|
576
597
|
};
|
|
577
598
|
|
|
599
|
+
let nextContentIndexToInspect = 0;
|
|
600
|
+
const emitServerToolBlocksBefore = (message: AssistantMessage, beforeIndex: number) => {
|
|
601
|
+
const limit = Math.min(beforeIndex, message.content.length);
|
|
602
|
+
while (nextContentIndexToInspect < limit) {
|
|
603
|
+
const index = nextContentIndexToInspect++;
|
|
604
|
+
const content = message.content[index];
|
|
605
|
+
if (content?.type !== "anthropicServerTool") continue;
|
|
606
|
+
ensureStart(message);
|
|
607
|
+
controller.enqueue(
|
|
608
|
+
sseFrame("content_block_start", {
|
|
609
|
+
type: "content_block_start",
|
|
610
|
+
index,
|
|
611
|
+
content_block: content.block,
|
|
612
|
+
}),
|
|
613
|
+
);
|
|
614
|
+
controller.enqueue(sseFrame("content_block_stop", { type: "content_block_stop", index }));
|
|
615
|
+
}
|
|
616
|
+
};
|
|
617
|
+
|
|
578
618
|
const closeBlock = (index: number) => {
|
|
579
619
|
if (!open.has(index)) return;
|
|
580
620
|
controller.enqueue(sseFrame("content_block_stop", { type: "content_block_stop", index }));
|
|
@@ -606,6 +646,7 @@ export function encodeStream(
|
|
|
606
646
|
ensureStart(ev.partial);
|
|
607
647
|
break;
|
|
608
648
|
case "text_start": {
|
|
649
|
+
emitServerToolBlocksBefore(ev.partial, ev.contentIndex);
|
|
609
650
|
ensureStart(ev.partial);
|
|
610
651
|
open.set(ev.contentIndex, { index: ev.contentIndex, kind: "text" });
|
|
611
652
|
controller.enqueue(
|
|
@@ -630,6 +671,7 @@ export function encodeStream(
|
|
|
630
671
|
closeBlock(ev.contentIndex);
|
|
631
672
|
break;
|
|
632
673
|
case "thinking_start": {
|
|
674
|
+
emitServerToolBlocksBefore(ev.partial, ev.contentIndex);
|
|
633
675
|
ensureStart(ev.partial);
|
|
634
676
|
open.set(ev.contentIndex, { index: ev.contentIndex, kind: "thinking" });
|
|
635
677
|
controller.enqueue(
|
|
@@ -665,6 +707,7 @@ export function encodeStream(
|
|
|
665
707
|
break;
|
|
666
708
|
}
|
|
667
709
|
case "toolcall_start": {
|
|
710
|
+
emitServerToolBlocksBefore(ev.partial, ev.contentIndex);
|
|
668
711
|
ensureStart(ev.partial);
|
|
669
712
|
const tc = ev.partial.content[ev.contentIndex] as ToolCall | undefined;
|
|
670
713
|
open.set(ev.contentIndex, { index: ev.contentIndex, kind: "tool_use" });
|
|
@@ -696,6 +739,7 @@ export function encodeStream(
|
|
|
696
739
|
break;
|
|
697
740
|
case "done": {
|
|
698
741
|
for (const idx of [...open.keys()]) closeBlock(idx);
|
|
742
|
+
emitServerToolBlocksBefore(ev.message, ev.message.content.length);
|
|
699
743
|
controller.enqueue(
|
|
700
744
|
sseFrame("message_delta", {
|
|
701
745
|
type: "message_delta",
|
|
@@ -65,6 +65,43 @@ export type ToolResultBlockParam = {
|
|
|
65
65
|
cache_control?: CacheControlEphemeral | null;
|
|
66
66
|
};
|
|
67
67
|
|
|
68
|
+
/** Anthropic-executed server tool call replayed inside an assistant turn. */
|
|
69
|
+
export type ServerToolUseBlockParam = {
|
|
70
|
+
type: "server_tool_use";
|
|
71
|
+
id: string;
|
|
72
|
+
name: string;
|
|
73
|
+
input?: Record<string, unknown> | null;
|
|
74
|
+
[key: string]: unknown;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/** Web-search server-tool call whose matching result is replayable by omp. */
|
|
78
|
+
export type WebSearchServerToolUseBlockParam = ServerToolUseBlockParam & { name: "web_search" };
|
|
79
|
+
|
|
80
|
+
/** Native web-search result replayed inside an assistant turn. */
|
|
81
|
+
export type WebSearchToolResultBlockParam = {
|
|
82
|
+
type: "web_search_tool_result";
|
|
83
|
+
tool_use_id: string;
|
|
84
|
+
content: unknown;
|
|
85
|
+
[key: string]: unknown;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/** True for the complete native web-search history variants omp can replay. */
|
|
89
|
+
export function isAnthropicWebSearchHistoryBlock(block: {
|
|
90
|
+
type: string;
|
|
91
|
+
name?: unknown;
|
|
92
|
+
id?: unknown;
|
|
93
|
+
tool_use_id?: unknown;
|
|
94
|
+
content?: unknown;
|
|
95
|
+
}): block is WebSearchServerToolUseBlockParam | WebSearchToolResultBlockParam {
|
|
96
|
+
if (block.type === "server_tool_use") {
|
|
97
|
+
return block.name === "web_search" && typeof block.id === "string" && block.id.length > 0;
|
|
98
|
+
}
|
|
99
|
+
if (block.type === "web_search_tool_result") {
|
|
100
|
+
return typeof block.tool_use_id === "string" && block.tool_use_id.length > 0 && Object.hasOwn(block, "content");
|
|
101
|
+
}
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
|
|
68
105
|
export type ThinkingBlockParam = {
|
|
69
106
|
type: "thinking";
|
|
70
107
|
thinking: string;
|
|
@@ -93,6 +130,8 @@ export type ContentBlockParam =
|
|
|
93
130
|
| ImageBlockParam
|
|
94
131
|
| ToolUseBlockParam
|
|
95
132
|
| ToolResultBlockParam
|
|
133
|
+
| ServerToolUseBlockParam
|
|
134
|
+
| WebSearchToolResultBlockParam
|
|
96
135
|
| ThinkingBlockParam
|
|
97
136
|
| RedactedThinkingBlockParam
|
|
98
137
|
| FallbackBlockParam;
|
|
@@ -278,6 +317,8 @@ export type ResponseContentBlock =
|
|
|
278
317
|
| { type: "thinking"; thinking: string; signature?: string }
|
|
279
318
|
| { type: "redacted_thinking"; data: string }
|
|
280
319
|
| { type: "tool_use"; id: string; name: string; input?: Record<string, unknown> | null }
|
|
320
|
+
| ServerToolUseBlockParam
|
|
321
|
+
| WebSearchToolResultBlockParam
|
|
281
322
|
| { type: "fallback"; from: { model: string }; to: { model: string } };
|
|
282
323
|
|
|
283
324
|
export type ContentBlockDelta =
|