@oh-my-pi/pi-coding-agent 16.4.5 → 16.4.8

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.
Files changed (45) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/cli.js +3271 -3224
  3. package/dist/types/cli/bench-cli.d.ts +1 -7
  4. package/dist/types/cli/usage-cli.d.ts +1 -0
  5. package/dist/types/commands/usage.d.ts +7 -0
  6. package/dist/types/config/settings-schema.d.ts +1 -1
  7. package/dist/types/modes/components/custom-editor.d.ts +3 -8
  8. package/dist/types/modes/components/model-browser.d.ts +14 -1
  9. package/dist/types/modes/components/model-hub.d.ts +4 -3
  10. package/dist/types/modes/components/plan-review-overlay.d.ts +2 -0
  11. package/dist/types/modes/components/welcome.d.ts +4 -0
  12. package/dist/types/modes/controllers/input-controller.d.ts +2 -0
  13. package/dist/types/modes/interactive-mode.d.ts +2 -0
  14. package/dist/types/modes/queue-input.d.ts +8 -0
  15. package/dist/types/modes/types.d.ts +2 -0
  16. package/dist/types/session/agent-storage.d.ts +57 -0
  17. package/package.json +12 -12
  18. package/scripts/build-binary.ts +0 -1
  19. package/scripts/compile-binary.ts +4 -3
  20. package/src/cli/bench-cli.ts +7 -26
  21. package/src/cli/usage-cli.ts +11 -0
  22. package/src/commands/usage.ts +13 -2
  23. package/src/config/settings-schema.ts +1 -1
  24. package/src/eval/js/shared/rewrite-imports.ts +31 -13
  25. package/src/modes/components/advisor-config.ts +3 -1
  26. package/src/modes/components/custom-editor.test.ts +58 -1
  27. package/src/modes/components/custom-editor.ts +42 -11
  28. package/src/modes/components/model-browser.ts +154 -60
  29. package/src/modes/components/model-hub.ts +475 -122
  30. package/src/modes/components/plan-review-overlay.ts +7 -0
  31. package/src/modes/components/tips.txt +2 -1
  32. package/src/modes/components/usage-row.ts +5 -6
  33. package/src/modes/components/welcome.ts +13 -14
  34. package/src/modes/controllers/input-controller.ts +140 -6
  35. package/src/modes/controllers/selector-controller.ts +20 -13
  36. package/src/modes/controllers/todo-command-controller.ts +1 -2
  37. package/src/modes/interactive-mode.ts +18 -0
  38. package/src/modes/queue-input.ts +132 -0
  39. package/src/modes/types.ts +2 -0
  40. package/src/modes/utils/ui-helpers.ts +19 -20
  41. package/src/session/agent-session.ts +184 -48
  42. package/src/session/agent-storage.ts +330 -3
  43. package/src/session/history-storage.ts +1 -34
  44. package/src/slash-commands/builtin-registry.ts +9 -0
  45. package/src/web/search/providers/perplexity.ts +18 -2
@@ -30,7 +30,7 @@ export interface BenchRunSuccess {
30
30
  ttftMs: number;
31
31
  durationMs: number;
32
32
  outputTokens: number;
33
- /** Generation throughput measured over the post-first-token window. */
33
+ /** Output tokens/sec over the total request duration. */
34
34
  tokensPerSecond: number;
35
35
  }
36
36
  export interface BenchRunFailure {
@@ -74,12 +74,6 @@ export interface BenchDependencies {
74
74
  now?: () => number;
75
75
  stdoutIsTTY?: boolean;
76
76
  }
77
- /**
78
- * Tokens/s over the generation window (duration minus TTFT) so queue/prefill
79
- * latency does not dilute throughput. Falls back to total duration when the
80
- * response arrived as a single chunk (TTFT ~ duration).
81
- */
82
- export declare function computeTokensPerSecond(outputTokens: number, durationMs: number, ttftMs: number, deltaChunkCount: number): number;
83
77
  export declare function formatBenchTable(summary: BenchSummary): string;
84
78
  export declare function runBenchCommand(command: BenchCommandArgs, deps?: BenchDependencies): Promise<BenchSummary>;
85
79
  export {};
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import { type UsageHistoryEntry, type UsageReport } from "@oh-my-pi/pi-ai";
11
11
  export interface UsageCommandArgs {
12
+ action?: string;
12
13
  json?: boolean;
13
14
  provider?: string;
14
15
  redact?: boolean;
@@ -4,6 +4,13 @@
4
4
  import { Command } from "@oh-my-pi/pi-utils/cli";
5
5
  export default class Usage extends Command {
6
6
  static description: string;
7
+ static args: {
8
+ action: import("@oh-my-pi/pi-utils/cli").ArgDescriptor & {
9
+ description: string;
10
+ required: false;
11
+ options: string[];
12
+ };
13
+ };
7
14
  static flags: {
8
15
  json: import("@oh-my-pi/pi-utils/cli").FlagDescriptor<"boolean"> & {
9
16
  char: string;
@@ -1384,7 +1384,7 @@ export declare const SETTINGS_SCHEMA: {
1384
1384
  readonly tab: "model";
1385
1385
  readonly group: "Retry & Fallback";
1386
1386
  readonly label: "Retry Fallback Chains";
1387
- readonly description: 'JSON object mapping model roles to ordered fallback model selectors, e.g. {"default":["openai/gpt-4o-mini"]}.';
1387
+ readonly description: 'JSON object mapping model roles, model selectors ("provider/model-id"), or provider wildcards ("provider/*") to ordered fallback selectors, e.g. {"default":["openai/gpt-4o-mini"],"google-antigravity/*":["google/*","google-vertex/*"]}. Model-oriented keys apply whenever that model/provider is active, regardless of role; a "provider/*" entry keeps the failing model\'s id and swaps the provider.';
1388
1388
  };
1389
1389
  };
1390
1390
  readonly "retry.fallbackRevertPolicy": {
@@ -86,14 +86,9 @@ export declare class CustomEditor extends Editor {
86
86
  static readonly SHIMMER_FRAME_MS = 70;
87
87
  /** Time for the gradient to sweep one full cycle across each keyword. */
88
88
  static readonly SHIMMER_PERIOD_MS = 1800;
89
- /** Gradient-highlight the "ultrathink" / "orchestrate" / "workflowz" keywords as the user types
90
- * them, skipping any occurrence inside code spans, fenced blocks, or XML sections. Also make
91
- * pasted image placeholders visually distinct and hyperlink them once their blob file exists.
92
- * When the editor is focused, the buffer contains a magic keyword, and `magicKeywords.enabled`
93
- * is on, the gradient shifts every frame to produce a Claude-Code-style shimmer; each render
94
- * schedules the next frame, so losing focus, deleting the keyword, or flipping the setting
95
- * stops the animation on its own. The static glow itself runs even when shimmering is gated
96
- * off, matching existing behavior for the editor and sent bubbles. */
89
+ /** Decorate magic keywords, attachments, and the queue-composer header/list markers.
90
+ * Queue shorthand reserves its first logical line as a dim `Queueing` label; sequential
91
+ * item markers use the accent color so separate follow-ups remain visible while composing. */
97
92
  decorateText: (text: string) => string;
98
93
  /** Optional test/host override for the magic-keyword shimmer gate. When
99
94
  * defined, takes precedence over the global `magicKeywords.enabled` setting,
@@ -1,6 +1,7 @@
1
1
  import type { Model } from "@oh-my-pi/pi-ai";
2
2
  import { type Component, type SgrMouseEvent } from "@oh-my-pi/pi-tui";
3
3
  import type { Settings } from "../../config/settings.js";
4
+ import type { ModelPerfStats } from "../../session/agent-storage.js";
4
5
  import { type ConfiguredThinkingLevel } from "../../thinking.js";
5
6
  /** One selectable model row. `selector` is the canonical `provider/id` key. */
6
7
  export interface ModelBrowserItem {
@@ -73,8 +74,12 @@ export declare class ModelBrowser implements Component {
73
74
  setItems(items: ModelBrowserItem[]): void;
74
75
  setRoles(roles: RoleAssignments): void;
75
76
  setMruOrder(order: ReadonlyArray<string>): void;
77
+ /** Measured TPS/TTFT averages keyed by `provider/id` selector (see AgentStorage.getModelPerf). */
78
+ setPerfStats(perf: ReadonlyMap<string, ModelPerfStats>): void;
76
79
  setMaxVisible(rows: number): void;
77
80
  setShowProvider(show: boolean): void;
81
+ /** Focused: accent cursor + selected-row background band. Unfocused: dim cursor, no band. */
82
+ setFocused(focused: boolean): void;
78
83
  /** Total rendered height for the current `maxVisible` (host layout budgeting). */
79
84
  get renderedRows(): number;
80
85
  get query(): string;
@@ -83,7 +88,13 @@ export declare class ModelBrowser implements Component {
83
88
  get visibleCount(): number;
84
89
  /** Move selection to `selector`; false when it is not in the current view. */
85
90
  selectSelector(selector: string): boolean;
86
- moveSelection(delta: number): void;
91
+ /**
92
+ * Move the selection by `delta` rows, skipping disabled rows. Single steps
93
+ * wrap at the ends; `wrap: false` (page/home/end jumps) clamps instead.
94
+ */
95
+ moveSelection(delta: number, options?: {
96
+ wrap?: boolean;
97
+ }): void;
87
98
  handleInput(data: string): void;
88
99
  /** Cancel-key ladder: clear a non-empty query first, then bubble to the host. */
89
100
  handleCancel(): void;
@@ -92,6 +103,8 @@ export declare class ModelBrowser implements Component {
92
103
  * row (the search row).
93
104
  */
94
105
  routeMouse(event: SgrMouseEvent, line: number): void;
106
+ /** Drop the hover band. Hosts call this when the pointer leaves the browser pane. */
107
+ clearHover(): void;
95
108
  render(width: number): string[];
96
109
  invalidate(): void;
97
110
  }
@@ -5,17 +5,18 @@ import type { Settings } from "../../config/settings.js";
5
5
  import { type ConfiguredThinkingLevel } from "../../thinking.js";
6
6
  /** `roles` is the full /models hub; `pick` is a one-shot session/embedded picker. */
7
7
  export type ModelHubMode = "roles" | "pick";
8
- export type ModelHubAction = "modelRole" | "retryFallback";
9
8
  /** A `--models` scope entry (mirrors the session's scoped model list). */
10
9
  export interface ScopedModelItem {
11
10
  model: Model;
12
11
  thinkingLevel?: string;
13
12
  }
14
13
  export interface ModelHubCallbacks {
15
- /** Persist a role assignment (or a retry-fallback registration). */
16
- onAssign: (model: Model, role: string, thinkingLevel: ConfiguredThinkingLevel | undefined, selector: string, action: ModelHubAction) => void;
14
+ /** Persist a role assignment. */
15
+ onAssign: (model: Model, role: string, thinkingLevel: ConfiguredThinkingLevel | undefined, selector: string) => void;
17
16
  /** Clear a configured role back to auto-selection. */
18
17
  onUnassign: (role: string) => void;
18
+ /** Persist a `retry.fallbackChains` entry — keyed by a role, `provider/model-id`, or `provider/*`; an empty chain clears the key. */
19
+ onFallbackChainChange?: (role: string, chain: string[]) => void;
19
20
  /** Pick-mode activation: session-only switch or embedded pick. */
20
21
  onPick?: (model: Model, selector: string) => void;
21
22
  /** Locked provider activation: forward to the /login flow. */
@@ -23,6 +23,8 @@ export interface PlanReviewOverlayCallbacks {
23
23
  onPick: (label: string) => void;
24
24
  /** Invoked on Esc / cancel. */
25
25
  onCancel: () => void;
26
+ /** Invoked with the current full plan text when the copy hotkey is pressed. */
27
+ onCopyPlan?: (content: string) => void | Promise<void>;
26
28
  /** Invoked when the external-editor key is pressed (overlay stays open). */
27
29
  onExternalEditor?: () => void;
28
30
  /** Invoked when the external-editor key edits the active annotation draft. */
@@ -9,6 +9,10 @@ export declare const WELCOME_SESSION_SLOTS = 4;
9
9
  * the box height is constant regardless of how many servers a project has.
10
10
  */
11
11
  export declare const WELCOME_LSP_SLOTS = 4;
12
+ /** Pick a tip from `tips`, biased toward "[NEW]" tips by {@link NEW_TIP_WEIGHT};
13
+ * `r` is a uniform sample in [0, 1). Returns "" when `tips` is empty.
14
+ * Exported for tests. */
15
+ export declare function pickWeightedTip(tips: readonly string[], r: number): string;
12
16
  export declare function renderWelcomeTip(tip: string, boxWidth: number, phase?: number): string[];
13
17
  export interface RecentSession {
14
18
  name: string;
@@ -33,6 +33,8 @@ export declare class InputController {
33
33
  handleCtrlZ(): void;
34
34
  handleDequeue(): void;
35
35
  handleRetry(): Promise<void>;
36
+ /** Queue `/queue` input behind an active turn, or start it immediately when idle. */
37
+ handleQueueCommand(text: string): Promise<void>;
36
38
  /** Send editor text as a follow-up message (queued behind current stream). */
37
39
  handleFollowUp(): Promise<void>;
38
40
  restoreQueuedMessagesToEditor(options?: {
@@ -365,6 +365,8 @@ export declare class InteractiveMode implements InteractiveModeContext {
365
365
  handleCtrlZ(): void;
366
366
  handleDequeue(): void;
367
367
  handleImagePaste(): Promise<boolean>;
368
+ /** Queue slash-command input behind the active turn. */
369
+ handleQueueCommand(message: string): Promise<void>;
368
370
  handleBtwCommand(question: string): Promise<void>;
369
371
  handleTanCommand(work: string): Promise<void>;
370
372
  hasActiveBtw(): boolean;
@@ -0,0 +1,8 @@
1
+ /** Prefix matcher shared by queue-list parsing and editor highlighting. */
2
+ export declare const QUEUE_LIST_MARKER_RE: RegExp;
3
+ /** Extract the message body from the `->` / `=>` yield-queue shorthand. */
4
+ export declare function parseQueueShorthand(text: string): string | undefined;
5
+ /** Whether text currently forms a sequential queue list, including an unfinished trailing item. */
6
+ export declare function isQueuedMessageList(text: string): boolean;
7
+ /** Split a sequential numeric, Roman-numeral, or alphabetic list into queue entries. */
8
+ export declare function splitQueuedMessages(text: string): string[];
@@ -352,6 +352,8 @@ export interface InteractiveModeContext {
352
352
  handleCtrlZ(): void;
353
353
  handleDequeue(): void;
354
354
  handleImagePaste(): Promise<boolean>;
355
+ /** Queue a message for delivery only after the active agent turn would stop. */
356
+ handleQueueCommand(message: string): Promise<void>;
355
357
  handleBtwCommand(question: string): Promise<void>;
356
358
  handleTanCommand(work: string): Promise<void>;
357
359
  hasActiveBtw(): boolean;
@@ -1,5 +1,25 @@
1
1
  import { type AuthCredential, type AuthCredentialStore, type StoredAuthCredential } from "@oh-my-pi/pi-ai";
2
2
  import type { RawSettings as Settings } from "../config/settings.js";
3
+ /** One completed request's timing, folded into the per-model aggregates. */
4
+ export interface ModelPerfSample {
5
+ /** Output tokens the provider reported for the turn. */
6
+ outputTokens: number;
7
+ /** Total request duration in milliseconds. */
8
+ durationMs: number;
9
+ /** Time to first token in milliseconds; omit when the provider did not report one. */
10
+ ttftMs?: number;
11
+ }
12
+ /** Recency-weighted per-model performance averages. */
13
+ export interface ModelPerfStats {
14
+ /** Decayed sample count backing the averages. */
15
+ samples: number;
16
+ /** Average output tokens/sec over the total request duration. */
17
+ tps: number;
18
+ /** Average time-to-first-token in milliseconds; null when no sample reported one. */
19
+ ttftMs: number | null;
20
+ }
21
+ /** Current agent.db schema version; bump when schema changes require migration. */
22
+ export declare const SCHEMA_VERSION = 6;
3
23
  /**
4
24
  * Unified SQLite storage for agent settings, model usage, and auth credentials.
5
25
  * Delegates auth credential operations to AuthCredentialStore from @oh-my-pi/pi-ai.
@@ -36,6 +56,43 @@ export declare class AgentStorage {
36
56
  * @returns Array of model keys ("provider/modelId") in MRU order
37
57
  */
38
58
  getModelUsageOrder(): string[];
59
+ /**
60
+ * Folds one completed request's timing into the model's perf aggregates.
61
+ * TPS is measured over the total request duration — not the post-TTFT
62
+ * decode window, which undercounts generation time (and so inflates the
63
+ * rate) when reasoning tokens are generated before the first visible
64
+ * token. Invalid samples (no tokens, no duration) are dropped.
65
+ *
66
+ * Deferred like prompt history: samples are batched and written in one
67
+ * transaction after {@link MODEL_PERF_FLUSH_DELAY_MS}, keeping SQLite off
68
+ * the turn-completion hot path. Fire-and-forget safe — flush failures are
69
+ * logged, never thrown; await the returned promise only to observe the flush.
70
+ * @param modelKey - Model key in "provider/modelId" format
71
+ */
72
+ recordModelPerf(modelKey: string, sample: ModelPerfSample): Promise<void>;
73
+ /**
74
+ * Returns recency-weighted TPS/TTFT averages for every model with recorded
75
+ * requests, keyed by "provider/modelId". Read by the /models browser.
76
+ * Also kicks the one-time background stats.db import; until it completes,
77
+ * models without live samples are simply absent.
78
+ */
79
+ getModelPerf(): Map<string, ModelPerfStats>;
80
+ /**
81
+ * Imports recent measurable request rows from an `omp stats` database
82
+ * (`messages` table) into the model_perf aggregates. Walks newest-first
83
+ * over the timestamp index in {@link MODEL_PERF_BACKFILL_CHUNK}-row chunks,
84
+ * yielding to the event loop between chunks, and keeps at most
85
+ * {@link MODEL_PERF_DECAY_AT} rows per model within the
86
+ * {@link MODEL_PERF_BACKFILL_MAX_AGE_MS} window — beyond either bound the
87
+ * live decay would erase the contribution anyway. Errored turns are
88
+ * excluded; aborted turns with reported usage count, matching live capture.
89
+ * Sums land in one additive transaction at the end, so concurrent live
90
+ * samples merge correctly regardless of order.
91
+ * @param statsDbPath - Path to a stats.db file; opened read-only
92
+ * @returns Number of rows folded in
93
+ * @throws When the stats db cannot be opened or queried
94
+ */
95
+ backfillModelPerfFromStats(statsDbPath: string): Promise<number>;
39
96
  /**
40
97
  * Checks if any auth credentials exist in storage.
41
98
  * @returns True if at least one credential is stored
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-coding-agent",
4
- "version": "16.4.5",
4
+ "version": "16.4.8",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -52,17 +52,17 @@
52
52
  "@agentclientprotocol/sdk": "0.25.0",
53
53
  "@babel/parser": "^7.29.7",
54
54
  "@mozilla/readability": "^0.6.0",
55
- "@oh-my-pi/hashline": "16.4.5",
56
- "@oh-my-pi/omp-stats": "16.4.5",
57
- "@oh-my-pi/pi-agent-core": "16.4.5",
58
- "@oh-my-pi/pi-ai": "16.4.5",
59
- "@oh-my-pi/pi-catalog": "16.4.5",
60
- "@oh-my-pi/pi-mnemopi": "16.4.5",
61
- "@oh-my-pi/pi-natives": "16.4.5",
62
- "@oh-my-pi/pi-tui": "16.4.5",
63
- "@oh-my-pi/pi-utils": "16.4.5",
64
- "@oh-my-pi/pi-wire": "16.4.5",
65
- "@oh-my-pi/snapcompact": "16.4.5",
55
+ "@oh-my-pi/hashline": "16.4.8",
56
+ "@oh-my-pi/omp-stats": "16.4.8",
57
+ "@oh-my-pi/pi-agent-core": "16.4.8",
58
+ "@oh-my-pi/pi-ai": "16.4.8",
59
+ "@oh-my-pi/pi-catalog": "16.4.8",
60
+ "@oh-my-pi/pi-mnemopi": "16.4.8",
61
+ "@oh-my-pi/pi-natives": "16.4.8",
62
+ "@oh-my-pi/pi-tui": "16.4.8",
63
+ "@oh-my-pi/pi-utils": "16.4.8",
64
+ "@oh-my-pi/pi-wire": "16.4.8",
65
+ "@oh-my-pi/snapcompact": "16.4.8",
66
66
  "@opentelemetry/api": "^1.9.1",
67
67
  "@opentelemetry/context-async-hooks": "^2.7.1",
68
68
  "@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",
@@ -99,7 +99,6 @@ async function main(): Promise<void> {
99
99
  outfile: outputPath,
100
100
  transformersVersion,
101
101
  target: crossBuild?.target,
102
- external: ["fastembed", "onnxruntime-node"],
103
102
  skipBuiltinCodesign: shouldAdhocSignDarwinBinary(crossBuild),
104
103
  });
105
104
 
@@ -1,6 +1,9 @@
1
1
  import { buildDocsIndexPayload } from "./generate-docs-index";
2
2
  import { createLegacyPiVirtualModulePlugin } from "./legacy-pi-virtual-module";
3
3
 
4
+ /** Native runtime dependencies always resolved from the on-demand install instead of embedded into compiled binaries. */
5
+ export const COMPILED_EXTERNAL_DEPENDENCIES: readonly string[] = Object.freeze(["fastembed", "onnxruntime-node"]);
6
+
4
7
  /** Inputs shared by local and release coding-agent binary builds. */
5
8
  export interface CodingAgentCompileOptions {
6
9
  /** Absolute repository root used for package resolution. */
@@ -13,8 +16,6 @@ export interface CodingAgentCompileOptions {
13
16
  readonly transformersVersion: string;
14
17
  /** Optional cross-compilation runtime target. */
15
18
  readonly target?: Bun.Build.CompileTarget;
16
- /** Dependencies intentionally resolved from the runtime filesystem. */
17
- readonly external?: readonly string[];
18
19
  /** Match release builds that minify identifiers while retaining names. */
19
20
  readonly minifyIdentifiers?: boolean;
20
21
  /** Disable Bun's built-in Darwin signing before the caller re-signs. */
@@ -34,7 +35,7 @@ export async function compileCodingAgent(options: CodingAgentCompileOptions): Pr
34
35
  const output = await Bun.build({
35
36
  entrypoints: [options.entrypoint],
36
37
  root: options.repoRoot,
37
- external: options.external ? [...options.external] : undefined,
38
+ external: [...COMPILED_EXTERNAL_DEPENDENCIES],
38
39
  define: {
39
40
  "process.env.PI_COMPILED": JSON.stringify("true"),
40
41
  "process.env.PI_TINY_TRANSFORMERS_VERSION": JSON.stringify(options.transformersVersion),
@@ -74,7 +74,7 @@ export interface BenchRunSuccess {
74
74
  ttftMs: number;
75
75
  durationMs: number;
76
76
  outputTokens: number;
77
- /** Generation throughput measured over the post-first-token window. */
77
+ /** Output tokens/sec over the total request duration. */
78
78
  tokensPerSecond: number;
79
79
  }
80
80
 
@@ -181,23 +181,6 @@ function hasVisibleFinalContent(message: AssistantMessage): boolean {
181
181
  });
182
182
  }
183
183
 
184
- /**
185
- * Tokens/s over the generation window (duration minus TTFT) so queue/prefill
186
- * latency does not dilute throughput. Falls back to total duration when the
187
- * response arrived as a single chunk (TTFT ~ duration).
188
- */
189
- export function computeTokensPerSecond(
190
- outputTokens: number,
191
- durationMs: number,
192
- ttftMs: number,
193
- deltaChunkCount: number,
194
- ): number {
195
- const decodeMs = durationMs - ttftMs;
196
- // Fall back to total duration when the response arrived as a single chunk/non-streaming.
197
- const windowMs = decodeMs > 0 && deltaChunkCount >= 2 ? decodeMs : durationMs;
198
- return windowMs > 0 ? (outputTokens * 1000) / windowMs : 0;
199
- }
200
-
201
184
  interface BenchRequestOptions {
202
185
  apiKey: ApiKeyResolver;
203
186
  sessionId: string;
@@ -247,17 +230,10 @@ async function runBenchRequest(
247
230
  headers: model.provider === "openrouter" ? { "X-OpenRouter-Cache": "false" } : undefined,
248
231
  });
249
232
  let message: AssistantMessage | undefined;
250
- let deltaChunkCount = 0;
251
233
  for await (const event of stream) {
252
234
  if (firstTokenAt === undefined && isFirstTokenEvent(event)) {
253
235
  firstTokenAt = now();
254
236
  }
255
- if (
256
- (event.type === "text_delta" || event.type === "thinking_delta" || event.type === "toolcall_delta") &&
257
- event.delta.length > 0
258
- ) {
259
- deltaChunkCount++;
260
- }
261
237
  if (event.type === "error") {
262
238
  return { ok: false, error: event.error.errorMessage ?? "request failed" };
263
239
  }
@@ -291,7 +267,12 @@ async function runBenchRequest(
291
267
  ttftMs,
292
268
  durationMs,
293
269
  outputTokens,
294
- tokensPerSecond: computeTokensPerSecond(outputTokens, durationMs, ttftMs, deltaChunkCount),
270
+ // TPS over the TOTAL request duration, deliberately not the post-TTFT
271
+ // decode window: reasoning models can spend seconds generating hidden
272
+ // thinking tokens (counted in usage.output) before the first visible
273
+ // byte, so "duration - TTFT" inflates TPS several-fold on providers
274
+ // that buffer or hide reasoning (e.g. google vs google-vertex).
275
+ tokensPerSecond: durationMs > 0 ? (outputTokens * 1000) / durationMs : 0,
295
276
  };
296
277
  } catch (error) {
297
278
  return { ok: false, error: getErrorMessage(error) };
@@ -23,6 +23,7 @@ import { discoverAuthStorage } from "../sdk";
23
23
  const BAR_WIDTH = 28;
24
24
 
25
25
  export interface UsageCommandArgs {
26
+ action?: string;
26
27
  json?: boolean;
27
28
  provider?: string;
28
29
  redact?: boolean;
@@ -755,6 +756,16 @@ function redactReportForJson(
755
756
  export async function runUsageCommand(cmd: UsageCommandArgs): Promise<void> {
756
757
  const authStorage = await discoverAuthStorage();
757
758
  try {
759
+ if (cmd.action === "invalidate") {
760
+ const provider = cmd.provider?.toLowerCase();
761
+ await authStorage.invalidateUsageCache(provider);
762
+ if (provider) {
763
+ process.stdout.write(`Invalidated cached usage reports for provider "${provider}".\n`);
764
+ } else {
765
+ process.stdout.write("Invalidated cached usage reports for all providers.\n");
766
+ }
767
+ return;
768
+ }
758
769
  if (cmd.history) {
759
770
  const days = cmd.days !== undefined && Number.isFinite(cmd.days) && cmd.days > 0 ? cmd.days : 7;
760
771
  const nowMs = Date.now();
@@ -1,12 +1,20 @@
1
1
  /**
2
2
  * Show provider usage limits for every authenticated account.
3
3
  */
4
- import { Command, Flags } from "@oh-my-pi/pi-utils/cli";
4
+ import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli";
5
5
  import { runUsageCommand } from "../cli/usage-cli";
6
6
 
7
7
  export default class Usage extends Command {
8
8
  static description = "Show provider usage limits for every authenticated account";
9
9
 
10
+ static args = {
11
+ action: Args.string({
12
+ description: "Optional subcommand to execute",
13
+ required: false,
14
+ options: ["invalidate"],
15
+ }),
16
+ };
17
+
10
18
  static flags = {
11
19
  json: Flags.boolean({ char: "j", description: "Output usage reports as JSON", default: false }),
12
20
  provider: Flags.string({ char: "p", description: "Only show usage for this provider id (e.g. anthropic)" }),
@@ -28,11 +36,14 @@ export default class Usage extends Command {
28
36
  "# Redact account identifiers for screenshots\n omp usage --redact",
29
37
  "# Machine-readable output\n omp usage --json",
30
38
  "# Usage-limit trend over the last 30 days\n omp usage --history --days 30",
39
+ "# Invalidate cached usage reports for all providers\n omp usage invalidate",
40
+ "# Invalidate cached usage reports for a specific provider\n omp usage invalidate --provider anthropic",
31
41
  ];
32
42
 
33
43
  async run(): Promise<void> {
34
- const { flags } = await this.parse(Usage);
44
+ const { args, flags } = await this.parse(Usage);
35
45
  await runUsageCommand({
46
+ action: args.action,
36
47
  json: flags.json,
37
48
  provider: flags.provider,
38
49
  redact: flags.redact,
@@ -1375,7 +1375,7 @@ export const SETTINGS_SCHEMA = {
1375
1375
  group: "Retry & Fallback",
1376
1376
  label: "Retry Fallback Chains",
1377
1377
  description:
1378
- 'JSON object mapping model roles to ordered fallback model selectors, e.g. {"default":["openai/gpt-4o-mini"]}.',
1378
+ 'JSON object mapping model roles, model selectors ("provider/model-id"), or provider wildcards ("provider/*") to ordered fallback selectors, e.g. {"default":["openai/gpt-4o-mini"],"google-antigravity/*":["google/*","google-vertex/*"]}. Model-oriented keys apply whenever that model/provider is active, regardless of role; a "provider/*" entry keeps the failing model\'s id and swaps the provider.',
1379
1379
  },
1380
1380
  },
1381
1381
  "retry.fallbackRevertPolicy": {
@@ -48,6 +48,15 @@ type BabelClassDeclaration = {
48
48
  };
49
49
 
50
50
  type BabelLexicalDecl = BabelVariableDeclaration | BabelClassDeclaration;
51
+ type BabelFunctionDeclaration = {
52
+ type: "FunctionDeclaration";
53
+ start: number;
54
+ end: number;
55
+ id: { start: number; end: number; name: string } | null;
56
+ };
57
+
58
+ /** Top-level declarations whose bindings must survive the cell (demoted and/or published). */
59
+ type BabelPublishableDecl = BabelLexicalDecl | BabelFunctionDeclaration;
51
60
 
52
61
  type BabelExpressionStatement = {
53
62
  type: "ExpressionStatement";
@@ -322,7 +331,7 @@ function collectBindingNames(pattern: unknown, names: string[]): void {
322
331
  }
323
332
  }
324
333
 
325
- function getLexicalBindingNames(node: BabelLexicalDecl): string[] {
334
+ function getLexicalBindingNames(node: BabelPublishableDecl): string[] {
326
335
  const names: string[] = [];
327
336
  if (node.type === "VariableDeclaration") {
328
337
  for (const declaration of node.declarations ?? []) collectBindingNames(declaration.id, names);
@@ -348,40 +357,49 @@ function appendGlobalBindingPublish(source: string, names: readonly string[]): s
348
357
  * let { a, b } = obj; -> var { a, b } = obj;
349
358
  * class Foo extends Bar {} -> var Foo = class extends Bar {};
350
359
  *
351
- * When the source must run inside the async wrapper, demoted `var`s would normally become
352
- * function-scoped. In that mode we publish each top-level binding back to the wrapper's
353
- * lexical `this`, which is the worker global object.
360
+ * When the source must run inside the async wrapper (top-level `await`), demoted `var`s
361
+ * and the user's own top-level `var` and `function` declarations would be scoped to the
362
+ * wrapper function and die with the cell. In that mode we publish every top-level binding
363
+ * back to the wrapper's lexical `this`, which is the worker global object.
354
364
  *
355
- * Nested declarations (inside functions, blocks, classes) are left alone \u2014 they're
365
+ * Nested declarations (inside functions, blocks, classes) are left alone they're
356
366
  * scoped to their enclosing function/block regardless of `var` vs `let`/`const`.
357
367
  */
358
368
  async function demoteTopLevelLexicals(code: string, options: { publishGlobals?: boolean } = {}): Promise<string> {
359
- if (!/\b(?:const|let|class)\b/.test(code)) return code;
369
+ const publishGlobals = options.publishGlobals === true;
370
+ const fastPath = publishGlobals ? /\b(?:const|let|class|var|function)\b/ : /\b(?:const|let|class)\b/;
371
+ if (!fastPath.test(code)) return code;
360
372
 
361
373
  const ast = await parseProgram(code);
362
374
  if (!ast) {
363
375
  return code;
364
376
  }
365
377
 
366
- const targets: BabelLexicalDecl[] = [];
378
+ const targets: Array<{ node: BabelPublishableDecl; demote: boolean }> = [];
367
379
  for (const node of ast.program.body) {
368
380
  if (node.type === "VariableDeclaration") {
369
381
  const decl = node as unknown as BabelVariableDeclaration;
370
- if (decl.kind === "const" || decl.kind === "let") targets.push(decl);
382
+ if (decl.kind === "const" || decl.kind === "let") targets.push({ node: decl, demote: true });
383
+ else if (publishGlobals) targets.push({ node: decl, demote: false });
371
384
  } else if (node.type === "ClassDeclaration") {
372
385
  const decl = node as unknown as BabelClassDeclaration;
373
- if (decl.id) targets.push(decl);
386
+ if (decl.id) targets.push({ node: decl, demote: true });
387
+ } else if (publishGlobals && node.type === "FunctionDeclaration") {
388
+ const decl = node as unknown as BabelFunctionDeclaration;
389
+ if (decl.id) targets.push({ node: decl, demote: false });
374
390
  }
375
391
  }
376
392
  if (targets.length === 0) return code;
377
393
 
378
- targets.sort((a, b) => b.start - a.start);
394
+ targets.sort((a, b) => b.node.start - a.node.start);
379
395
  let result = code;
380
- for (const node of targets) {
396
+ for (const { node, demote } of targets) {
381
397
  const segment = result.slice(node.start, node.end);
382
- const bindingNames = options.publishGlobals ? getLexicalBindingNames(node) : [];
398
+ const bindingNames = publishGlobals ? getLexicalBindingNames(node) : [];
383
399
  let replacement: string;
384
- if (node.type === "VariableDeclaration") {
400
+ if (!demote) {
401
+ replacement = segment;
402
+ } else if (node.type === "VariableDeclaration") {
385
403
  replacement = `var${segment.slice(node.kind.length)}`;
386
404
  } else {
387
405
  const id = node.id;
@@ -461,7 +461,8 @@ export class AdvisorConfigOverlayComponent implements Component {
461
461
  }
462
462
 
463
463
  #showModelPicker(index: number): void {
464
- const mruOrder = this.#settings.getStorage()?.getModelUsageOrder() ?? [];
464
+ const storage = this.#settings.getStorage();
465
+ const mruOrder = storage?.getModelUsageOrder() ?? [];
465
466
  let models: ReadonlyArray<Model>;
466
467
  if (this.#scopedModels.length > 0) {
467
468
  models = this.#scopedModels.map(scoped => scoped.model);
@@ -477,6 +478,7 @@ export class AdvisorConfigOverlayComponent implements Component {
477
478
 
478
479
  const picker = new ModelBrowser(this.#settings, {});
479
480
  picker.setMruOrder(mruOrder);
481
+ picker.setPerfStats(storage?.getModelPerf() ?? new Map());
480
482
  picker.setItems(items);
481
483
  picker.onActivate = item => {
482
484
  const efforts = getSupportedEfforts(item.model);