@oh-my-pi/pi-coding-agent 16.4.5 → 16.4.6
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 +29 -0
- package/dist/cli.js +3052 -3005
- package/dist/types/cli/bench-cli.d.ts +1 -7
- package/dist/types/cli/usage-cli.d.ts +1 -0
- package/dist/types/commands/usage.d.ts +7 -0
- package/dist/types/config/settings-schema.d.ts +1 -1
- package/dist/types/modes/components/custom-editor.d.ts +3 -8
- package/dist/types/modes/components/model-browser.d.ts +3 -0
- package/dist/types/modes/components/model-hub.d.ts +4 -3
- package/dist/types/modes/controllers/input-controller.d.ts +2 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/queue-input.d.ts +8 -0
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/session/agent-storage.d.ts +57 -0
- package/package.json +12 -12
- package/scripts/build-binary.ts +0 -1
- package/scripts/compile-binary.ts +4 -3
- package/src/cli/bench-cli.ts +7 -26
- package/src/cli/usage-cli.ts +11 -0
- package/src/commands/usage.ts +13 -2
- package/src/config/settings-schema.ts +1 -1
- package/src/modes/components/advisor-config.ts +3 -1
- package/src/modes/components/custom-editor.test.ts +58 -1
- package/src/modes/components/custom-editor.ts +42 -11
- package/src/modes/components/model-browser.ts +114 -33
- package/src/modes/components/model-hub.ts +455 -108
- package/src/modes/components/usage-row.ts +5 -6
- package/src/modes/controllers/input-controller.ts +140 -6
- package/src/modes/controllers/selector-controller.ts +20 -13
- package/src/modes/controllers/todo-command-controller.ts +1 -2
- package/src/modes/interactive-mode.ts +5 -0
- package/src/modes/queue-input.ts +132 -0
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/ui-helpers.ts +19 -20
- package/src/session/agent-session.ts +184 -48
- package/src/session/agent-storage.ts +330 -3
- package/src/session/history-storage.ts +1 -34
- package/src/slash-commands/builtin-registry.ts +9 -0
|
@@ -30,7 +30,7 @@ export interface BenchRunSuccess {
|
|
|
30
30
|
ttftMs: number;
|
|
31
31
|
durationMs: number;
|
|
32
32
|
outputTokens: number;
|
|
33
|
-
/**
|
|
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 {};
|
|
@@ -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
|
|
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
|
-
/**
|
|
90
|
-
*
|
|
91
|
-
*
|
|
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,6 +74,8 @@ 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;
|
|
78
81
|
/** Total rendered height for the current `maxVisible` (host layout budgeting). */
|
|
@@ -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
|
|
16
|
-
onAssign: (model: Model, role: string, thinkingLevel: ConfiguredThinkingLevel | undefined, selector: string
|
|
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. */
|
|
@@ -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.
|
|
4
|
+
"version": "16.4.6",
|
|
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.
|
|
56
|
-
"@oh-my-pi/omp-stats": "16.4.
|
|
57
|
-
"@oh-my-pi/pi-agent-core": "16.4.
|
|
58
|
-
"@oh-my-pi/pi-ai": "16.4.
|
|
59
|
-
"@oh-my-pi/pi-catalog": "16.4.
|
|
60
|
-
"@oh-my-pi/pi-mnemopi": "16.4.
|
|
61
|
-
"@oh-my-pi/pi-natives": "16.4.
|
|
62
|
-
"@oh-my-pi/pi-tui": "16.4.
|
|
63
|
-
"@oh-my-pi/pi-utils": "16.4.
|
|
64
|
-
"@oh-my-pi/pi-wire": "16.4.
|
|
65
|
-
"@oh-my-pi/snapcompact": "16.4.
|
|
55
|
+
"@oh-my-pi/hashline": "16.4.6",
|
|
56
|
+
"@oh-my-pi/omp-stats": "16.4.6",
|
|
57
|
+
"@oh-my-pi/pi-agent-core": "16.4.6",
|
|
58
|
+
"@oh-my-pi/pi-ai": "16.4.6",
|
|
59
|
+
"@oh-my-pi/pi-catalog": "16.4.6",
|
|
60
|
+
"@oh-my-pi/pi-mnemopi": "16.4.6",
|
|
61
|
+
"@oh-my-pi/pi-natives": "16.4.6",
|
|
62
|
+
"@oh-my-pi/pi-tui": "16.4.6",
|
|
63
|
+
"@oh-my-pi/pi-utils": "16.4.6",
|
|
64
|
+
"@oh-my-pi/pi-wire": "16.4.6",
|
|
65
|
+
"@oh-my-pi/snapcompact": "16.4.6",
|
|
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",
|
package/scripts/build-binary.ts
CHANGED
|
@@ -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:
|
|
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),
|
package/src/cli/bench-cli.ts
CHANGED
|
@@ -74,7 +74,7 @@ export interface BenchRunSuccess {
|
|
|
74
74
|
ttftMs: number;
|
|
75
75
|
durationMs: number;
|
|
76
76
|
outputTokens: number;
|
|
77
|
-
/**
|
|
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
|
-
|
|
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) };
|
package/src/cli/usage-cli.ts
CHANGED
|
@@ -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();
|
package/src/commands/usage.ts
CHANGED
|
@@ -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
|
|
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": {
|
|
@@ -461,7 +461,8 @@ export class AdvisorConfigOverlayComponent implements Component {
|
|
|
461
461
|
}
|
|
462
462
|
|
|
463
463
|
#showModelPicker(index: number): void {
|
|
464
|
-
const
|
|
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);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "bun:test";
|
|
2
|
+
import { CURSOR_MARKER } from "@oh-my-pi/pi-tui";
|
|
2
3
|
import { setKittyProtocolActive } from "@oh-my-pi/pi-tui/keys";
|
|
3
4
|
import { $ } from "bun";
|
|
4
5
|
import { getDefaultPasteImageKeys } from "../../config/keybindings";
|
|
5
|
-
import { getEditorTheme, initTheme } from "../theme/theme";
|
|
6
|
+
import { getEditorTheme, initTheme, theme } from "../theme/theme";
|
|
6
7
|
import {
|
|
7
8
|
CustomEditor,
|
|
8
9
|
extractBracketedImagePastePaths,
|
|
@@ -78,6 +79,62 @@ describe("CustomEditor placeholder decoration", () => {
|
|
|
78
79
|
});
|
|
79
80
|
});
|
|
80
81
|
|
|
82
|
+
describe("CustomEditor queue shorthand decoration", () => {
|
|
83
|
+
beforeAll(async () => {
|
|
84
|
+
await initTheme();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("reserves the first line as soon as either queue prefix is completed", () => {
|
|
88
|
+
for (const prefix of ["->", "=>"]) {
|
|
89
|
+
const editor = new CustomEditor(getEditorTheme());
|
|
90
|
+
editor.handleInput(prefix[0] ?? "");
|
|
91
|
+
expect(editor.getText()).toBe(prefix[0]);
|
|
92
|
+
|
|
93
|
+
editor.handleInput(prefix[1] ?? "");
|
|
94
|
+
expect(editor.getText()).toBe(`${prefix}\n`);
|
|
95
|
+
expect(editor.getCursor()).toEqual({ line: 1, col: 0 });
|
|
96
|
+
|
|
97
|
+
editor.handleInput("\x7f");
|
|
98
|
+
expect(editor.getText()).toBe(`${prefix}\n`);
|
|
99
|
+
expect(editor.getCursor()).toEqual({ line: 1, col: 0 });
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("renders the reserved line as a dim Queueing header", () => {
|
|
104
|
+
for (const prefix of ["->", "=>"]) {
|
|
105
|
+
const editor = new CustomEditor(getEditorTheme());
|
|
106
|
+
editor.setText(`${prefix}\nqueue this`);
|
|
107
|
+
|
|
108
|
+
expect(editor.decorateText(prefix)).toBe(theme.fg("dim", `Queueing ${theme.nav.selected}`));
|
|
109
|
+
editor.focused = true;
|
|
110
|
+
const rendered = editor.render(40).map(line => Bun.stripANSI(line.replace(CURSOR_MARKER, "")));
|
|
111
|
+
expect(rendered.some(line => line.includes(`Queueing ${theme.nav.selected}`))).toBe(true);
|
|
112
|
+
expect(rendered.every(line => Bun.stringWidth(line) === 40)).toBe(true);
|
|
113
|
+
expect(rendered.some(line => line.includes("queue this"))).toBe(true);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("highlights dot and parenthesis markers only for detected queue lists", () => {
|
|
118
|
+
for (const [input, marker] of [
|
|
119
|
+
["=>\n1. first\n2. second", "1."],
|
|
120
|
+
["=>\n1) first\n2) second", "1)"],
|
|
121
|
+
]) {
|
|
122
|
+
const editor = new CustomEditor(getEditorTheme());
|
|
123
|
+
editor.setText(input);
|
|
124
|
+
expect(editor.decorateText(`${marker} first`).startsWith(theme.fg("accent", marker))).toBe(true);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const unfinished = new CustomEditor(getEditorTheme());
|
|
128
|
+
unfinished.setText("=>\n1. first\n2. second\n3. third\n4.");
|
|
129
|
+
expect(unfinished.decorateText("1. first").startsWith(theme.fg("accent", "1."))).toBe(true);
|
|
130
|
+
expect(unfinished.decorateText("4.").startsWith(theme.fg("accent", "4."))).toBe(true);
|
|
131
|
+
|
|
132
|
+
const editor = new CustomEditor(getEditorTheme());
|
|
133
|
+
editor.setText("=>\n1. first\n3. third");
|
|
134
|
+
expect(editor.decorateText("1. first")).toBe("1. first");
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
81
138
|
describe("CustomEditor bracketed path paste", () => {
|
|
82
139
|
it("leaves a pasted bare .png filename on the normal text path", () => {
|
|
83
140
|
expect(extractBracketedImagePastePaths(bracketedPaste("icon-photo-default.png"))).toBeUndefined();
|
|
@@ -6,7 +6,8 @@ import type { AppKeybinding } from "../../config/keybindings";
|
|
|
6
6
|
import { isSettingsInitialized, settings } from "../../config/settings";
|
|
7
7
|
import { imageReferenceHyperlink, PLACEHOLDER_REGEX, renderPlaceholders } from "../image-references";
|
|
8
8
|
import { hasMagicKeyword, highlightMagicKeywords } from "../magic-keywords";
|
|
9
|
-
import {
|
|
9
|
+
import { isQueuedMessageList, parseQueueShorthand, QUEUE_LIST_MARKER_RE } from "../queue-input";
|
|
10
|
+
import { fgOrPlain, theme } from "../theme/theme";
|
|
10
11
|
|
|
11
12
|
type ConfigurableEditorAction = Extract<
|
|
12
13
|
AppKeybinding,
|
|
@@ -326,21 +327,41 @@ export class CustomEditor extends Editor {
|
|
|
326
327
|
* timer to request the next animation frame. Undefined when nobody is
|
|
327
328
|
* listening (tests, headless callers); the timer chain still self-cleans. */
|
|
328
329
|
#requestShimmerRepaint: (() => void) | undefined;
|
|
330
|
+
#queueDecorationText: string | undefined;
|
|
331
|
+
#queueShorthandActive = false;
|
|
332
|
+
#queueListActive = false;
|
|
329
333
|
|
|
330
|
-
/**
|
|
331
|
-
*
|
|
332
|
-
*
|
|
333
|
-
* When the editor is focused, the buffer contains a magic keyword, and `magicKeywords.enabled`
|
|
334
|
-
* is on, the gradient shifts every frame to produce a Claude-Code-style shimmer; each render
|
|
335
|
-
* schedules the next frame, so losing focus, deleting the keyword, or flipping the setting
|
|
336
|
-
* stops the animation on its own. The static glow itself runs even when shimmering is gated
|
|
337
|
-
* off, matching existing behavior for the editor and sent bubbles. */
|
|
334
|
+
/** Decorate magic keywords, attachments, and the queue-composer header/list markers.
|
|
335
|
+
* Queue shorthand reserves its first logical line as a dim `Queueing` label; sequential
|
|
336
|
+
* item markers use the accent color so separate follow-ups remain visible while composing. */
|
|
338
337
|
decorateText = (text: string): string => {
|
|
339
|
-
const
|
|
338
|
+
const editorText = this.getText();
|
|
339
|
+
const animated = this.focused && this.#shimmerEnabled() && hasMagicKeyword(editorText);
|
|
340
340
|
const phase = animated ? (Date.now() % CustomEditor.SHIMMER_PERIOD_MS) / CustomEditor.SHIMMER_PERIOD_MS : 0;
|
|
341
341
|
if (animated) this.#scheduleShimmerFrame();
|
|
342
|
+
if (this.#queueDecorationText !== editorText) {
|
|
343
|
+
this.#queueDecorationText = editorText;
|
|
344
|
+
const queueBody = parseQueueShorthand(editorText);
|
|
345
|
+
this.#queueShorthandActive = queueBody !== undefined;
|
|
346
|
+
this.#queueListActive = queueBody !== undefined && isQueuedMessageList(queueBody);
|
|
347
|
+
}
|
|
342
348
|
return renderPlaceholders(text, {
|
|
343
|
-
renderText: value =>
|
|
349
|
+
renderText: value => {
|
|
350
|
+
const highlighted = highlightMagicKeywords(value, undefined, phase);
|
|
351
|
+
if (this.#queueShorthandActive && (value.startsWith("->") || value.startsWith("=>"))) {
|
|
352
|
+
const icon = typeof theme === "undefined" ? "➤" : theme.nav.selected;
|
|
353
|
+
return `${fgOrPlain("dim", `Queueing ${icon}`)}${highlighted.slice(2)}`;
|
|
354
|
+
}
|
|
355
|
+
if (this.#queueListActive) {
|
|
356
|
+
const markerMatch = QUEUE_LIST_MARKER_RE.exec(value);
|
|
357
|
+
if (markerMatch) {
|
|
358
|
+
const indent = markerMatch[1] ?? "";
|
|
359
|
+
const markerEnd = markerMatch[0].length;
|
|
360
|
+
return `${indent}${fgOrPlain("accent", value.slice(indent.length, markerEnd))}${highlighted.slice(markerEnd)}`;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return highlighted;
|
|
364
|
+
},
|
|
344
365
|
renderReference: (value, kind, index) =>
|
|
345
366
|
kind === "image"
|
|
346
367
|
? imageReferenceHyperlink(value, index, this.imageLinks, label =>
|
|
@@ -628,6 +649,7 @@ export class CustomEditor extends Editor {
|
|
|
628
649
|
this.#pendingInput.push(data);
|
|
629
650
|
return;
|
|
630
651
|
}
|
|
652
|
+
const hadBareQueuePrefix = this.getText() === "->" || this.getText() === "=>";
|
|
631
653
|
const kittyParsed = parseKittySequence(data);
|
|
632
654
|
if (kittyParsed && (kittyParsed.modifier & 64) !== 0 && this.onCapsLock) {
|
|
633
655
|
// Caps Lock is modifier bit 64
|
|
@@ -831,5 +853,14 @@ export class CustomEditor extends Editor {
|
|
|
831
853
|
|
|
832
854
|
// Pass to parent for normal handling
|
|
833
855
|
super.handleInput(data);
|
|
856
|
+
const cursor = this.getCursor();
|
|
857
|
+
if (
|
|
858
|
+
!hadBareQueuePrefix &&
|
|
859
|
+
(this.getText() === "->" || this.getText() === "=>") &&
|
|
860
|
+
cursor.line === 0 &&
|
|
861
|
+
cursor.col === 2
|
|
862
|
+
) {
|
|
863
|
+
this.insertText("\n");
|
|
864
|
+
}
|
|
834
865
|
}
|
|
835
866
|
}
|