@oh-my-pi/pi-ai 17.1.1 → 17.1.3

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 CHANGED
@@ -2,6 +2,29 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [17.1.3] - 2026-07-24
6
+
7
+ ### Fixed
8
+
9
+ - Fixed Cursor sessions exposing `ast_edit` (and other staged-preview `xd://` devices) without a reachable resolver: the built-in `write` tool — which carries the `xd://resolve` / `xd://reject` transport that finalizes a staged preview — was filtered out of Cursor's forwarded catalog, so previews could never be resolved and the session aborted after three forced `write` turns. `write` is now re-included in the forwarded catalog whenever pi-agent devices are advertised ([#6536](https://github.com/can1357/oh-my-pi/issues/6536)).
10
+ - Fixed OpenAI Responses and chat-completions streams honoring per-model first-event watchdog policy, allowing local llama.cpp-style backends to process arbitrarily large prompts without a premature client cancellation ([#6524](https://github.com/can1357/oh-my-pi/issues/6524)).
11
+
12
+ ## [17.1.2] - 2026-07-24
13
+
14
+ ### Added
15
+
16
+ - Added `GET /v1/usage/history` to the auth broker (recorded usage-limit snapshots with `sinceMs`/`provider` filters) and `AuthBrokerClient.fetchUsageHistory` — in broker deployments the broker host performs every upstream usage fetch, so its durable history is the only complete utilization record
17
+ - Added per-client burn tracking to the auth broker: clients batch observed request usage per (provider, model) and flush it to `POST /v1/usage/observed` every 10 seconds (install id as client key, hostname as display name); the broker persists 5-minute buckets in `client_usage`/`clients` and serves aggregates from `GET /v1/usage/clients`. Brokers without the endpoint disable reporting for the process lifetime
18
+
19
+ ### Changed
20
+
21
+ - Renamed the Z.AI feature quota row to `ZAI Zread Quota` (tier/id slug `zread`), replacing the 74-char `ZAI Web Search / Reader / Zread Quota (web-search-reader-zread)` title that wrapped `omp usage` rows
22
+
23
+ ### Fixed
24
+
25
+ - Fixed every Claude (`anthropic-messages`) model on the `opencode-zen` provider failing with `401 Missing API key`: the gateway requires `x-api-key`, so `opencode-zen` now uses X-Api-Key auth like `opencode-go`/`umans` instead of bearer-only, and no longer sends the `context_management` field its Anthropic proxy rejects on thinking requests ([#6510](https://github.com/can1357/oh-my-pi/issues/6510)).
26
+ - Fixed Anthropic native server-tool blocks being dropped from persisted assistant turns, preserving signed web-search continuations in their original response order ([#6495](https://github.com/can1357/oh-my-pi/issues/6495))
27
+
5
28
  ## [17.1.1] - 2026-07-24
6
29
 
7
30
  ### Added
@@ -1,5 +1,5 @@
1
1
  import type { AuthCredential } from "../auth-storage.js";
2
- import type { CredentialBlockRequest, CredentialBlockResponse, CredentialBlocksDeleteResponse, CredentialDisableResponse, CredentialRefreshResponse, CredentialUploadResponse, HealthzResponse, SnapshotResponse, SnapshotStreamEvent, UsageResponse, UsageStaleResponse } from "./types.js";
2
+ import type { ClientUsageReportRequest, ClientUsageReportResponse, ClientUsageSummaryResponse, CredentialBlockRequest, CredentialBlockResponse, CredentialBlocksDeleteResponse, CredentialDisableResponse, CredentialRefreshResponse, CredentialUploadResponse, HealthzResponse, SnapshotResponse, SnapshotStreamEvent, UsageHistoryResponse, UsageResponse, UsageStaleResponse } from "./types.js";
3
3
  export interface AuthBrokerClientOptions {
4
4
  /** Base URL (e.g. `https://broker.tailnet:8765`). Trailing slashes are trimmed. */
5
5
  url: string;
@@ -60,6 +60,17 @@ export declare class AuthBrokerClient {
60
60
  signal?: AbortSignal;
61
61
  }): AsyncGenerator<SnapshotStreamEvent>;
62
62
  fetchUsage(signal?: AbortSignal): Promise<UsageResponse>;
63
+ /** Recorded usage-limit snapshots from the broker host, oldest first. */
64
+ fetchUsageHistory(query?: {
65
+ sinceMs?: number;
66
+ provider?: string;
67
+ }, signal?: AbortSignal): Promise<UsageHistoryResponse>;
68
+ /** Report this client's batched observed request usage for per-install burn tracking. */
69
+ reportClientUsage(report: ClientUsageReportRequest, signal?: AbortSignal): Promise<ClientUsageReportResponse>;
70
+ /** Per-client token burn aggregates recorded by the broker host. */
71
+ fetchClientUsageSummary(query?: {
72
+ sinceMs?: number;
73
+ }, signal?: AbortSignal): Promise<ClientUsageSummaryResponse>;
63
74
  notifyUsageStale(signal?: AbortSignal): Promise<UsageStaleResponse>;
64
75
  refreshCredential(id: number, signal?: AbortSignal): Promise<CredentialRefreshResponse>;
65
76
  disableCredential(id: number, cause: string, signal?: AbortSignal): Promise<CredentialDisableResponse>;
@@ -1,7 +1,7 @@
1
1
  import { type AuthCredential, type AuthCredentialStore, type OAuthCredential, type StoredAuthCredential, type StoredCredentialBlock } from "../auth-storage.js";
2
2
  import type { OAuthCredentials } from "../registry/oauth/types.js";
3
3
  import type { Provider } from "../types.js";
4
- import type { UsageReport } from "../usage.js";
4
+ import type { ObservedUsageEntry, UsageReport } from "../usage.js";
5
5
  import { type AuthBrokerClient } from "./client.js";
6
6
  import type { SnapshotResponse } from "./types.js";
7
7
  /**
@@ -33,6 +33,8 @@ export interface RemoteAuthCredentialStoreOptions {
33
33
  * routing policy, not broker authorization.
34
34
  */
35
35
  accountPool?: AuthBrokerAccountPool;
36
+ /** Flush cadence for batched observed-usage reports. Default 10s. */
37
+ observedUsageFlushMs?: number;
36
38
  }
37
39
  export declare class RemoteAuthCredentialStore implements AuthCredentialStore {
38
40
  #private;
@@ -120,5 +122,12 @@ export declare class RemoteAuthCredentialStore implements AuthCredentialStore {
120
122
  */
121
123
  getUsageReport(provider: Provider, credential: OAuthCredential, signal?: AbortSignal): Promise<UsageReport | null>;
122
124
  ingestUsageReport(provider: Provider, credential: OAuthCredential, report: UsageReport): boolean;
125
+ /**
126
+ * Fold locally observed request usage into the pending report and schedule
127
+ * a flush. One `POST /v1/usage/observed` at most per flush interval; on
128
+ * failure the batch is retained and retried with the next flush. A 404
129
+ * (pre-endpoint broker) disables reporting for the life of this store.
130
+ */
131
+ recordObservedUsage(entries: ObservedUsageEntry[]): void;
123
132
  close(): void;
124
133
  }
@@ -6,7 +6,7 @@
6
6
  * credential expires or a 401 surfaces on a supposedly-fresh credential.
7
7
  */
8
8
  import type { AuthCredential, AuthCredentialSnapshot, AuthCredentialSnapshotEntry, StoredCredentialBlock } from "../auth-storage.js";
9
- import type { UsageReport } from "../usage.js";
9
+ import type { ClientUsageClientSummary, ClientUsageReport, UsageHistoryEntry, UsageReport } from "../usage.js";
10
10
  /** GET /v1/healthz response body. */
11
11
  export interface HealthzResponse {
12
12
  ok: boolean;
@@ -34,6 +34,26 @@ export interface UsageResponse {
34
34
  generatedAt: number;
35
35
  reports: UsageReport[];
36
36
  }
37
+ /**
38
+ * GET /v1/usage/history response body. Entries come from the broker host's
39
+ * durable `usage_history` — the broker performs every upstream usage fetch in
40
+ * broker deployments, so this is the only complete utilization record.
41
+ */
42
+ export interface UsageHistoryResponse {
43
+ generatedAt: number;
44
+ entries: UsageHistoryEntry[];
45
+ }
46
+ /** POST /v1/usage/observed request body — one client's batched observed usage. */
47
+ export type ClientUsageReportRequest = ClientUsageReport;
48
+ /** POST /v1/usage/observed response body. */
49
+ export interface ClientUsageReportResponse {
50
+ ok: boolean;
51
+ }
52
+ /** GET /v1/usage/clients response body — per-client token burn aggregates. */
53
+ export interface ClientUsageSummaryResponse {
54
+ generatedAt: number;
55
+ clients: ClientUsageClientSummary[];
56
+ }
37
57
  /** POST /v1/credential/:id/refresh response body. */
38
58
  export interface CredentialRefreshResponse {
39
59
  entry: AuthCredentialSnapshotEntry;
@@ -418,6 +418,61 @@ export declare const usageResponseSchema: import("arktype/internal/variants/obje
418
418
  raw?: unknown;
419
419
  }[];
420
420
  }, {}>;
421
+ /** Broker `/v1/usage/history` response — recorded usage-limit snapshots, oldest first. */
422
+ export declare const usageHistoryResponseSchema: import("arktype/internal/variants/object.ts").ObjectType<{
423
+ generatedAt: number;
424
+ entries: {
425
+ recordedAt: number;
426
+ provider: string;
427
+ accountKey: string;
428
+ email?: string | undefined;
429
+ accountId?: string | undefined;
430
+ limitId: string;
431
+ label: string;
432
+ windowLabel?: string | undefined;
433
+ usedFraction?: number | undefined;
434
+ status?: "exhausted" | "ok" | "unknown" | "warning" | undefined;
435
+ resetsAt?: number | undefined;
436
+ }[];
437
+ }, {}>;
438
+ /** Broker `POST /v1/usage/observed` request — one client's batched observed usage. */
439
+ export declare const clientUsageReportRequestSchema: import("arktype/internal/variants/object.ts").ObjectType<{
440
+ installId: string;
441
+ hostname?: string | undefined;
442
+ entries: {
443
+ at: number;
444
+ provider: string;
445
+ model: string;
446
+ requests: number;
447
+ inputTokens: number;
448
+ outputTokens: number;
449
+ cacheReadTokens: number;
450
+ cacheWriteTokens: number;
451
+ costUsd: number;
452
+ }[];
453
+ }, {}>;
454
+ export declare const clientUsageReportResponseSchema: import("arktype/internal/variants/object.ts").ObjectType<{
455
+ ok: boolean;
456
+ }, {}>;
457
+ /** Broker `GET /v1/usage/clients` response — per-client token burn aggregates. */
458
+ export declare const clientUsageSummaryResponseSchema: import("arktype/internal/variants/object.ts").ObjectType<{
459
+ generatedAt: number;
460
+ clients: {
461
+ installId: string;
462
+ hostname?: string | undefined;
463
+ firstSeen: number;
464
+ lastSeen: number;
465
+ providers: {
466
+ provider: string;
467
+ requests: number;
468
+ inputTokens: number;
469
+ outputTokens: number;
470
+ cacheReadTokens: number;
471
+ cacheWriteTokens: number;
472
+ costUsd: number;
473
+ }[];
474
+ }[];
475
+ }, {}>;
421
476
  export declare const credentialRefreshResponseSchema: import("arktype/internal/variants/object.ts").ObjectType<{
422
477
  entry: {
423
478
  id: number;
@@ -11,7 +11,7 @@ import { Database } from "bun:sqlite";
11
11
  import type { ApiKeyResolver } from "./auth-retry.js";
12
12
  import type { OAuthAuthInfo, OAuthController, OAuthCredentials, OAuthProviderId } from "./registry/oauth/types.js";
13
13
  import type { Provider } from "./types.js";
14
- import type { CredentialRankingStrategy, UsageCostHistoryEntry, UsageCostHistoryQuery, UsageHistoryEntry, UsageHistoryQuery, UsageLogger, UsageProvider, UsageReport } from "./usage.js";
14
+ import type { ClientUsageReport, ClientUsageSummary, CredentialRankingStrategy, ObservedUsageEntry, UsageCostHistoryEntry, UsageCostHistoryQuery, UsageHistoryEntry, UsageHistoryQuery, UsageLogger, UsageProvider, UsageReport } from "./usage.js";
15
15
  import { type CodexResetConsumeCode, type CodexResetCredit } from "./usage/openai-codex-reset.js";
16
16
  export type ApiKeyCredential = {
17
17
  type: "api_key";
@@ -281,6 +281,16 @@ export interface AuthCredentialStore {
281
281
  listUsageCosts?(query?: UsageCostHistoryQuery): UsageCostHistoryEntry[];
282
282
  /** Read recorded usage-limit snapshots, oldest first. */
283
283
  listUsageHistory?(query?: UsageHistoryQuery): UsageHistoryEntry[];
284
+ /**
285
+ * Client hook: forward locally observed request usage. Remote broker stores
286
+ * batch these to the broker so it can attribute token burn per install;
287
+ * local stores omit it and observation is skipped.
288
+ */
289
+ recordObservedUsage?(entries: ObservedUsageEntry[]): void;
290
+ /** Broker host: persist one client's observed-usage report. */
291
+ recordClientUsage?(report: ClientUsageReport): void;
292
+ /** Broker host: aggregate recorded per-client usage since a timestamp. */
293
+ getClientUsageSummary?(sinceMs: number): ClientUsageSummary;
284
294
  /**
285
295
  * Optional store-supplied OAuth refresh. When present, `AuthStorage` uses
286
296
  * it before the per-provider local refresh path. `RemoteAuthCredentialStore`
@@ -836,6 +846,28 @@ export declare class AuthStorage {
836
846
  recordedAt?: number;
837
847
  baseUrl?: string;
838
848
  }): boolean;
849
+ /**
850
+ * Forward one completed request's usage to the store's observer hook.
851
+ * Broker-backed stores batch these into per-install reports so the broker
852
+ * can track actual token burn per client; local stores have no hook and
853
+ * the call is a no-op.
854
+ */
855
+ recordObservedUsage(entry: {
856
+ provider: Provider;
857
+ model: string;
858
+ usage: {
859
+ input: number;
860
+ output: number;
861
+ cacheRead: number;
862
+ cacheWrite: number;
863
+ };
864
+ costUsd?: number;
865
+ at?: number;
866
+ }): void;
867
+ /** Broker host: persist one client's observed-usage report (per-install token burn). */
868
+ recordClientUsage(report: ClientUsageReport): boolean;
869
+ /** Broker host: aggregate recorded per-client usage since `sinceMs`. */
870
+ getClientUsageSummary(sinceMs: number): ClientUsageSummary;
839
871
  ingestUsageHeaders(provider: Provider, headers: Record<string, string>, options?: {
840
872
  sessionId?: string;
841
873
  baseUrl?: string;
@@ -1200,6 +1232,8 @@ export declare class SqliteAuthCredentialStore implements AuthCredentialStore {
1200
1232
  listUsageHistory(query?: UsageHistoryQuery): UsageHistoryEntry[];
1201
1233
  recordUsageCosts(entries: UsageCostHistoryEntry[]): void;
1202
1234
  listUsageCosts(query?: UsageCostHistoryQuery): UsageCostHistoryEntry[];
1235
+ recordClientUsage(report: ClientUsageReport): void;
1236
+ getClientUsageSummary(sinceMs: number): ClientUsageSummary;
1203
1237
  /**
1204
1238
  * Save OAuth credentials for a provider.
1205
1239
  * Preserves unrelated identities and replaces only the matching credential.
@@ -57,6 +57,33 @@ export type ToolResultBlockParam = {
57
57
  is_error?: boolean;
58
58
  cache_control?: CacheControlEphemeral | null;
59
59
  };
60
+ /** Anthropic-executed server tool call replayed inside an assistant turn. */
61
+ export type ServerToolUseBlockParam = {
62
+ type: "server_tool_use";
63
+ id: string;
64
+ name: string;
65
+ input?: Record<string, unknown> | null;
66
+ [key: string]: unknown;
67
+ };
68
+ /** Web-search server-tool call whose matching result is replayable by omp. */
69
+ export type WebSearchServerToolUseBlockParam = ServerToolUseBlockParam & {
70
+ name: "web_search";
71
+ };
72
+ /** Native web-search result replayed inside an assistant turn. */
73
+ export type WebSearchToolResultBlockParam = {
74
+ type: "web_search_tool_result";
75
+ tool_use_id: string;
76
+ content: unknown;
77
+ [key: string]: unknown;
78
+ };
79
+ /** True for the complete native web-search history variants omp can replay. */
80
+ export declare function isAnthropicWebSearchHistoryBlock(block: {
81
+ type: string;
82
+ name?: unknown;
83
+ id?: unknown;
84
+ tool_use_id?: unknown;
85
+ content?: unknown;
86
+ }): block is WebSearchServerToolUseBlockParam | WebSearchToolResultBlockParam;
60
87
  export type ThinkingBlockParam = {
61
88
  type: "thinking";
62
89
  thinking: string;
@@ -81,7 +108,7 @@ export type FallbackBlockParam = {
81
108
  model: string;
82
109
  };
83
110
  };
84
- export type ContentBlockParam = TextBlockParam | ImageBlockParam | ToolUseBlockParam | ToolResultBlockParam | ThinkingBlockParam | RedactedThinkingBlockParam | FallbackBlockParam;
111
+ export type ContentBlockParam = TextBlockParam | ImageBlockParam | ToolUseBlockParam | ToolResultBlockParam | ServerToolUseBlockParam | WebSearchToolResultBlockParam | ThinkingBlockParam | RedactedThinkingBlockParam | FallbackBlockParam;
85
112
  /**
86
113
  * A single conversation turn.
87
114
  *
@@ -257,7 +284,7 @@ export type ResponseContentBlock = {
257
284
  id: string;
258
285
  name: string;
259
286
  input?: Record<string, unknown> | null;
260
- } | {
287
+ } | ServerToolUseBlockParam | WebSearchToolResultBlockParam | {
261
288
  type: "fallback";
262
289
  from: {
263
290
  model: string;
@@ -1,6 +1,6 @@
1
1
  import type { FetchImpl, Message, Model, ProviderSessionState, ServiceTier, SimpleStreamOptions, StreamFunction, StreamOptions, Usage } from "../types.js";
2
2
  import { type AnthropicFetchOptions, type AnthropicMessagesClientLike } from "./anthropic-client.js";
3
- import type { FallbackParam, MessageParam, TextBlockParam } from "./anthropic-wire.js";
3
+ import { type FallbackParam, type MessageParam, type TextBlockParam } from "./anthropic-wire.js";
4
4
  export type AnthropicHeaderOptions = {
5
5
  apiKey: string;
6
6
  baseUrl?: string;
@@ -2,7 +2,7 @@ import http2 from "node:http2";
2
2
  import { type JsonValue } from "@bufbuild/protobuf";
3
3
  import type { McpToolDefinition } from "@oh-my-pi/pi-catalog/discovery/cursor-gen/agent_pb";
4
4
  import { type AgentServerMessage, type ConversationStateStructure } from "@oh-my-pi/pi-catalog/discovery/cursor-gen/agent_pb";
5
- import type { AssistantMessage, CursorExecHandlerResult, CursorExecHandlers, CursorToolResultHandler, Message, StreamFunction, StreamOptions, TextContent, ThinkingContent, ToolCall, ToolResultMessage } from "../types.js";
5
+ import type { AssistantMessage, CursorExecHandlerResult, CursorExecHandlers, CursorToolResultHandler, Message, StreamFunction, StreamOptions, TextContent, ThinkingContent, Tool, ToolCall, ToolResultMessage } from "../types.js";
6
6
  import { kCursorExecResolved, kStreamingBlockIndex, kStreamingBlockKind, kStreamingLastParseLen, kStreamingPartialJson } from "../utils/block-symbols.js";
7
7
  import { AssistantMessageEventStream } from "../utils/event-stream.js";
8
8
  export declare const CURSOR_API_URL = "https://api2.cursor.sh";
@@ -119,6 +119,7 @@ export declare function mergeCursorMcpToolCallArgs(streamed: Record<string, unkn
119
119
  export declare function synthesizeCursorExecToolCall(output: AssistantMessage, stream: AssistantMessageEventStream, state: BlockState, toolCallId: string, toolName: string, args: Record<string, unknown>): void;
120
120
  /** Exported for tests: drives one Cursor interaction update through the streaming state machine. */
121
121
  export declare function processInteractionUpdate(update: any, output: AssistantMessage, stream: AssistantMessageEventStream, state: BlockState, usageState: UsageState): void;
122
+ export declare function buildMcpToolDefinitions(tools: Tool[] | undefined): McpToolDefinition[];
122
123
  /**
123
124
  * Build `ConversationStateStructure.rootPromptMessagesJson` blob IDs for the
124
125
  * system prompt plus prior conversation history, as JSON blobs matching
@@ -476,6 +476,25 @@ export interface AnthropicFallbackContent {
476
476
  model: string;
477
477
  };
478
478
  }
479
+ /**
480
+ * Verbatim Anthropic web-search call/result retained for same-provider
481
+ * history replay. Other providers discard it in `transformMessages`.
482
+ */
483
+ export interface AnthropicServerToolContent {
484
+ type: "anthropicServerTool";
485
+ block: {
486
+ type: "server_tool_use";
487
+ id: string;
488
+ name: "web_search";
489
+ input?: Record<string, unknown> | null;
490
+ [key: string]: unknown;
491
+ } | {
492
+ type: "web_search_tool_result";
493
+ tool_use_id: string;
494
+ content: unknown;
495
+ [key: string]: unknown;
496
+ };
497
+ }
479
498
  export interface ImageContent {
480
499
  type: "image";
481
500
  data: string;
@@ -631,7 +650,7 @@ export interface ContextSnapshot {
631
650
  }
632
651
  export interface AssistantMessage {
633
652
  role: "assistant";
634
- content: (TextContent | ThinkingContent | RedactedThinkingContent | AnthropicFallbackContent | ImageContent | ToolCall)[];
653
+ content: (TextContent | ThinkingContent | RedactedThinkingContent | AnthropicFallbackContent | AnthropicServerToolContent | ImageContent | ToolCall)[];
635
654
  api: Api;
636
655
  provider: Provider;
637
656
  model: string;
@@ -156,6 +156,56 @@ export interface UsageCostHistoryQuery {
156
156
  /** Inclusive lower bound on {@link UsageCostHistoryEntry.recordedAt} (epoch ms). */
157
157
  sinceMs?: number;
158
158
  }
159
+ /**
160
+ * Aggregated request usage a client observed for one (provider, model) pair.
161
+ * Clients fold every completed request into per-pair buckets and flush them to
162
+ * the auth broker on a short cadence, so the broker can attribute token burn
163
+ * to the install that produced it.
164
+ */
165
+ export interface ObservedUsageEntry {
166
+ /** Epoch ms of the newest request folded into this bucket. */
167
+ at: number;
168
+ provider: Provider;
169
+ model: string;
170
+ /** Completed requests folded into this bucket. */
171
+ requests: number;
172
+ inputTokens: number;
173
+ outputTokens: number;
174
+ cacheReadTokens: number;
175
+ cacheWriteTokens: number;
176
+ /** Estimated USD cost of the folded requests (0 when unknown). */
177
+ costUsd: number;
178
+ }
179
+ /** One client's observed-usage report, keyed by its stable install id. */
180
+ export interface ClientUsageReport {
181
+ /** Stable per-machine install id — the client primary key. */
182
+ installId: string;
183
+ /** Human-readable machine name for display surfaces. */
184
+ hostname?: string;
185
+ entries: ObservedUsageEntry[];
186
+ }
187
+ /** Per-provider aggregate of one client's recorded usage. */
188
+ export interface ClientProviderUsage {
189
+ provider: string;
190
+ requests: number;
191
+ inputTokens: number;
192
+ outputTokens: number;
193
+ cacheReadTokens: number;
194
+ cacheWriteTokens: number;
195
+ costUsd: number;
196
+ }
197
+ /** One known client with its usage aggregates over the queried window. */
198
+ export interface ClientUsageClientSummary {
199
+ installId: string;
200
+ hostname?: string;
201
+ firstSeen: number;
202
+ lastSeen: number;
203
+ providers: ClientProviderUsage[];
204
+ }
205
+ /** Aggregated per-client usage recorded by the broker host. */
206
+ export interface ClientUsageSummary {
207
+ clients: ClientUsageClientSummary[];
208
+ }
159
209
  export declare const usageUnitSchema: import("arktype/internal/variants/string.ts").StringType<"bytes" | "minutes" | "percent" | "requests" | "tokens" | "unknown" | "usd", {}>;
160
210
  export declare const usageStatusSchema: import("arktype/internal/variants/string.ts").StringType<"exhausted" | "ok" | "unknown" | "warning", {}>;
161
211
  export declare const usageWindowSchema: import("arktype/internal/variants/object.ts").ObjectType<{
@@ -39,11 +39,13 @@ export declare function getStreamFirstEventTimeoutMs(idleTimeoutMs?: number, fal
39
39
  * `"0"` disable) wins outright. Otherwise the resolved idle (caller-supplied
40
40
  * `idleTimeoutMs` — which itself already encompasses per-call
41
41
  * `streamIdleTimeoutMs` or `PI_OPENAI_STREAM_IDLE_TIMEOUT_MS` resolved
42
- * upstream) floors the first-event budget so slow local OpenAI-compatible
43
- * servers are not undercut by a shorter `PI_STREAM_FIRST_EVENT_TIMEOUT_MS`
44
- * or the global default during prompt processing.
42
+ * upstream) floors the first-event budget so slow OpenAI-compatible servers
43
+ * are not undercut by a shorter `PI_STREAM_FIRST_EVENT_TIMEOUT_MS` or the
44
+ * global default during prompt processing. A zero per-provider fallback
45
+ * disables the first-event watchdog unless an environment override is set.
45
46
  *
46
- * Returns `undefined` when an explicit env knob disables the watchdog.
47
+ * Returns `0` when an explicit env knob or per-provider fallback disables the
48
+ * watchdog, preserving the sentinel through iterator timeout resolution.
47
49
  */
48
50
  export declare function getOpenAIStreamFirstEventTimeoutMs(idleTimeoutMs?: number, fallbackMs?: number): number | undefined;
49
51
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-ai",
4
- "version": "17.1.1",
4
+ "version": "17.1.3",
5
5
  "description": "Unified LLM API with automatic model discovery and provider configuration",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -38,9 +38,9 @@
38
38
  },
39
39
  "dependencies": {
40
40
  "@bufbuild/protobuf": "^2.12.1",
41
- "@oh-my-pi/pi-catalog": "17.1.1",
42
- "@oh-my-pi/pi-utils": "17.1.1",
43
- "@oh-my-pi/pi-wire": "17.1.1",
41
+ "@oh-my-pi/pi-catalog": "17.1.3",
42
+ "@oh-my-pi/pi-utils": "17.1.3",
43
+ "@oh-my-pi/pi-wire": "17.1.3",
44
44
  "arktype": "2.2.3",
45
45
  "zod": "^4"
46
46
  },
@@ -9,6 +9,9 @@ import { readSseEvents } from "@oh-my-pi/pi-utils";
9
9
  import { type } from "arktype";
10
10
  import type { AuthCredential } from "../auth-storage";
11
11
  import type {
12
+ ClientUsageReportRequest,
13
+ ClientUsageReportResponse,
14
+ ClientUsageSummaryResponse,
12
15
  CredentialBlockRequest,
13
16
  CredentialBlockResponse,
14
17
  CredentialBlocksDeleteResponse,
@@ -20,10 +23,13 @@ import type {
20
23
  HealthzResponse,
21
24
  SnapshotResponse,
22
25
  SnapshotStreamEvent,
26
+ UsageHistoryResponse,
23
27
  UsageResponse,
24
28
  UsageStaleResponse,
25
29
  } from "./types";
26
30
  import {
31
+ clientUsageReportResponseSchema,
32
+ clientUsageSummaryResponseSchema,
27
33
  credentialBlockResponseSchema,
28
34
  credentialBlocksDeleteResponseSchema,
29
35
  credentialDisableResponseSchema,
@@ -32,6 +38,7 @@ import {
32
38
  healthzResponseSchema,
33
39
  snapshotResponseSchema,
34
40
  snapshotStreamEventSchema,
41
+ usageHistoryResponseSchema,
35
42
  usageResponseSchema,
36
43
  usageStaleResponseSchema,
37
44
  } from "./wire-schemas";
@@ -244,6 +251,38 @@ export class AuthBrokerClient {
244
251
  return this.#request<UsageResponse>("GET", "/v1/usage", { schema: usageResponseSchema, signal });
245
252
  }
246
253
 
254
+ /** Recorded usage-limit snapshots from the broker host, oldest first. */
255
+ fetchUsageHistory(
256
+ query?: { sinceMs?: number; provider?: string },
257
+ signal?: AbortSignal,
258
+ ): Promise<UsageHistoryResponse> {
259
+ const params = new URLSearchParams();
260
+ if (query?.sinceMs !== undefined) params.set("sinceMs", String(query.sinceMs));
261
+ if (query?.provider) params.set("provider", query.provider);
262
+ const path = `/v1/usage/history${params.size > 0 ? `?${params.toString()}` : ""}`;
263
+ return this.#request<UsageHistoryResponse>("GET", path, { schema: usageHistoryResponseSchema, signal });
264
+ }
265
+
266
+ /** Report this client's batched observed request usage for per-install burn tracking. */
267
+ reportClientUsage(report: ClientUsageReportRequest, signal?: AbortSignal): Promise<ClientUsageReportResponse> {
268
+ return this.#request<ClientUsageReportResponse>("POST", "/v1/usage/observed", {
269
+ body: report,
270
+ schema: clientUsageReportResponseSchema,
271
+ signal,
272
+ });
273
+ }
274
+
275
+ /** Per-client token burn aggregates recorded by the broker host. */
276
+ fetchClientUsageSummary(query?: { sinceMs?: number }, signal?: AbortSignal): Promise<ClientUsageSummaryResponse> {
277
+ const params = new URLSearchParams();
278
+ if (query?.sinceMs !== undefined) params.set("sinceMs", String(query.sinceMs));
279
+ const path = `/v1/usage/clients${params.size > 0 ? `?${params.toString()}` : ""}`;
280
+ return this.#request<ClientUsageSummaryResponse>("GET", path, {
281
+ schema: clientUsageSummaryResponseSchema,
282
+ signal,
283
+ });
284
+ }
285
+
247
286
  notifyUsageStale(signal?: AbortSignal): Promise<UsageStaleResponse> {
248
287
  return this.#request<UsageStaleResponse>("POST", "/v1/usage/stale", {
249
288
  schema: usageStaleResponseSchema,
@@ -7,8 +7,9 @@
7
7
  * usage reports cache TTL is 5 minutes per credential, so durability across
8
8
  * runs isn't required.
9
9
  */
10
+ import * as os from "node:os";
10
11
  import { scheduler } from "node:timers/promises";
11
- import { logger } from "@oh-my-pi/pi-utils";
12
+ import { getInstallId, logger } from "@oh-my-pi/pi-utils";
12
13
  import {
13
14
  type AuthCredential,
14
15
  type AuthCredentialSnapshotEntry,
@@ -21,8 +22,8 @@ import {
21
22
  import * as AIError from "../error";
22
23
  import type { OAuthCredentials } from "../registry/oauth/types";
23
24
  import type { Provider } from "../types";
24
- import type { UsageReport } from "../usage";
25
- import { type AuthBrokerClient, AuthBrokerStreamUnsupportedError } from "./client";
25
+ import type { ObservedUsageEntry, UsageReport } from "../usage";
26
+ import { type AuthBrokerClient, AuthBrokerError, AuthBrokerStreamUnsupportedError } from "./client";
26
27
  import type {
27
28
  CredentialBlockSnapshot,
28
29
  RefresherSchedule,
@@ -232,6 +233,8 @@ export interface RemoteAuthCredentialStoreOptions {
232
233
  * routing policy, not broker authorization.
233
234
  */
234
235
  accountPool?: AuthBrokerAccountPool;
236
+ /** Flush cadence for batched observed-usage reports. Default 10s. */
237
+ observedUsageFlushMs?: number;
235
238
  }
236
239
 
237
240
  export class RemoteAuthCredentialStore implements AuthCredentialStore {
@@ -259,10 +262,17 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
259
262
  #streamingActive = false;
260
263
  /** Latched once the broker has answered 404 — never try the stream again. */
261
264
  #streamingUnsupported = false;
265
+ /** Pending observed usage keyed by `provider\u0000model`, merged until flush. */
266
+ #observedUsage = new Map<string, ObservedUsageEntry>();
267
+ #observedUsageTimer: Timer | undefined;
268
+ readonly #observedUsageFlushMs: number;
269
+ /** Latched once the broker answered 404 — old broker, never report again. */
270
+ #observedUsageUnsupported = false;
262
271
 
263
272
  constructor(opts: RemoteAuthCredentialStoreOptions) {
264
273
  this.#client = opts.client;
265
274
  this.#streamSnapshots = opts.streamSnapshots ?? true;
275
+ this.#observedUsageFlushMs = opts.observedUsageFlushMs ?? 10_000;
266
276
  this.#accountPool = opts.accountPool
267
277
  ? new Map([...opts.accountPool].map(([provider, identities]) => [provider, new Set(identities)]))
268
278
  : undefined;
@@ -1049,10 +1059,73 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
1049
1059
  return inflight;
1050
1060
  }
1051
1061
 
1062
+ /**
1063
+ * Fold locally observed request usage into the pending report and schedule
1064
+ * a flush. One `POST /v1/usage/observed` at most per flush interval; on
1065
+ * failure the batch is retained and retried with the next flush. A 404
1066
+ * (pre-endpoint broker) disables reporting for the life of this store.
1067
+ */
1068
+ recordObservedUsage(entries: ObservedUsageEntry[]): void {
1069
+ if (this.#closed || this.#observedUsageUnsupported) return;
1070
+ for (const entry of entries) {
1071
+ const key = `${entry.provider}\u0000${entry.model}`;
1072
+ const pending = this.#observedUsage.get(key);
1073
+ if (pending) {
1074
+ pending.at = Math.max(pending.at, entry.at);
1075
+ pending.requests += entry.requests;
1076
+ pending.inputTokens += entry.inputTokens;
1077
+ pending.outputTokens += entry.outputTokens;
1078
+ pending.cacheReadTokens += entry.cacheReadTokens;
1079
+ pending.cacheWriteTokens += entry.cacheWriteTokens;
1080
+ pending.costUsd += entry.costUsd;
1081
+ } else {
1082
+ this.#observedUsage.set(key, { ...entry });
1083
+ }
1084
+ }
1085
+ if (this.#observedUsage.size > 0 && this.#observedUsageTimer === undefined) {
1086
+ this.#observedUsageTimer = setTimeout(() => {
1087
+ this.#observedUsageTimer = undefined;
1088
+ void this.#flushObservedUsage();
1089
+ }, this.#observedUsageFlushMs);
1090
+ this.#observedUsageTimer.unref?.();
1091
+ }
1092
+ }
1093
+
1094
+ async #flushObservedUsage(): Promise<void> {
1095
+ if (this.#observedUsage.size === 0 || this.#observedUsageUnsupported) return;
1096
+ const batch = [...this.#observedUsage.values()];
1097
+ this.#observedUsage.clear();
1098
+ try {
1099
+ await this.#client.reportClientUsage({
1100
+ installId: getInstallId(),
1101
+ hostname: os.hostname(),
1102
+ entries: batch,
1103
+ });
1104
+ } catch (error) {
1105
+ const status = error instanceof AuthBrokerError ? error.status : undefined;
1106
+ if (status === 404 || status === 501) {
1107
+ // Broker predates the endpoint (or store can't persist) — stop trying.
1108
+ this.#observedUsageUnsupported = true;
1109
+ logger.debug("auth-broker does not accept observed usage; reporting disabled", { status });
1110
+ return;
1111
+ }
1112
+ logger.debug("auth-broker observed usage flush failed; retrying next flush", { error: String(error) });
1113
+ // Merge the failed batch back under the (possibly refilled) buffer so
1114
+ // nothing is lost; bounded because entries are keyed per (provider, model).
1115
+ if (!this.#closed) this.recordObservedUsage(batch);
1116
+ }
1117
+ }
1118
+
1052
1119
  close(): void {
1053
1120
  if (this.#closed) return;
1054
1121
  this.#closed = true;
1055
1122
  this.#backgroundAbort.abort();
1123
+ if (this.#observedUsageTimer !== undefined) {
1124
+ clearTimeout(this.#observedUsageTimer);
1125
+ this.#observedUsageTimer = undefined;
1126
+ }
1127
+ // Best-effort final flush; failures are dropped (the process is exiting).
1128
+ if (this.#observedUsage.size > 0) void this.#flushObservedUsage();
1056
1129
  this.#cache.clear();
1057
1130
  this.#usageOverlays.clear();
1058
1131
  }