@openlines/opl-pi-sht 0.1.14

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 (100) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +191 -0
  3. package/configs/opl-browser.json.sample +8 -0
  4. package/configs/opl-footer.json +48 -0
  5. package/configs/opl-footer.json.sample +48 -0
  6. package/configs/opl-input.json +22 -0
  7. package/configs/opl-input.json.sample +22 -0
  8. package/configs/opl-modes.json +291 -0
  9. package/configs/opl-modes.json.sample +285 -0
  10. package/configs/opl-simplebench.json +8 -0
  11. package/configs/opl-simplebench.json.sample +8 -0
  12. package/configs/opl-todo.json +12 -0
  13. package/configs/opl-todo.json.sample +12 -0
  14. package/configs/opl-webaccess.json +31 -0
  15. package/configs/opl-webaccess.json.sample +31 -0
  16. package/extensions/opl-browser/README.md +83 -0
  17. package/extensions/opl-browser/browser.ts +185 -0
  18. package/extensions/opl-browser/config.ts +37 -0
  19. package/extensions/opl-browser/index.ts +89 -0
  20. package/extensions/opl-browser/package-lock.json +60 -0
  21. package/extensions/opl-browser/package.json +10 -0
  22. package/extensions/opl-ctxtrim/README.md +78 -0
  23. package/extensions/opl-ctxtrim/index.ts +151 -0
  24. package/extensions/opl-footer/README.md +193 -0
  25. package/extensions/opl-footer/config.ts +91 -0
  26. package/extensions/opl-footer/git-status.ts +170 -0
  27. package/extensions/opl-footer/icons.ts +49 -0
  28. package/extensions/opl-footer/index.ts +368 -0
  29. package/extensions/opl-footer/segments/caveman.ts +25 -0
  30. package/extensions/opl-footer/segments/chat-mode.ts +24 -0
  31. package/extensions/opl-footer/segments/context.ts +92 -0
  32. package/extensions/opl-footer/segments/cost.ts +25 -0
  33. package/extensions/opl-footer/segments/git.ts +47 -0
  34. package/extensions/opl-footer/segments/helpers.ts +18 -0
  35. package/extensions/opl-footer/segments/index.ts +51 -0
  36. package/extensions/opl-footer/segments/mode-switcher.ts +24 -0
  37. package/extensions/opl-footer/segments/model.ts +22 -0
  38. package/extensions/opl-footer/segments/path.ts +34 -0
  39. package/extensions/opl-footer/segments/pi.ts +11 -0
  40. package/extensions/opl-footer/segments/plan-mode.ts +24 -0
  41. package/extensions/opl-footer/segments/separator.ts +9 -0
  42. package/extensions/opl-footer/segments/session-stats.ts +82 -0
  43. package/extensions/opl-footer/segments/thinking.ts +51 -0
  44. package/extensions/opl-footer/segments/tokens.ts +66 -0
  45. package/extensions/opl-footer/theme.ts +113 -0
  46. package/extensions/opl-footer/types.ts +172 -0
  47. package/extensions/opl-init/README.md +23 -0
  48. package/extensions/opl-init/index.ts +451 -0
  49. package/extensions/opl-input/README.md +83 -0
  50. package/extensions/opl-input/config.ts +137 -0
  51. package/extensions/opl-input/index.ts +284 -0
  52. package/extensions/opl-input/mode-style.ts +38 -0
  53. package/extensions/opl-input/utils.ts +378 -0
  54. package/extensions/opl-modes/README.md +132 -0
  55. package/extensions/opl-modes/config.ts +473 -0
  56. package/extensions/opl-modes/index.ts +1081 -0
  57. package/extensions/opl-modes/menus.ts +60 -0
  58. package/extensions/opl-modes/state.ts +152 -0
  59. package/extensions/opl-modes/types.ts +164 -0
  60. package/extensions/opl-modes/utils.ts +138 -0
  61. package/extensions/opl-questionnaire/README.md +26 -0
  62. package/extensions/opl-questionnaire/index.ts +445 -0
  63. package/extensions/opl-questionnaire/types.ts +55 -0
  64. package/extensions/opl-simplebench/README.md +242 -0
  65. package/extensions/opl-simplebench/artifact.ts +41 -0
  66. package/extensions/opl-simplebench/benchmark.ts +873 -0
  67. package/extensions/opl-simplebench/coding.ts +282 -0
  68. package/extensions/opl-simplebench/index.ts +203 -0
  69. package/extensions/opl-simplebench/llama-server.ts +290 -0
  70. package/extensions/opl-simplebench/metrics.ts +73 -0
  71. package/extensions/opl-simplebench/report.ts +46 -0
  72. package/extensions/opl-simplebench/research.ts +200 -0
  73. package/extensions/opl-simplebench/scoring.ts +48 -0
  74. package/extensions/opl-simplebench/tests.ts +39 -0
  75. package/extensions/opl-simplebench/types.ts +6 -0
  76. package/extensions/opl-simplebench/util/config.ts +170 -0
  77. package/extensions/opl-simplebench/util/debug.ts +32 -0
  78. package/extensions/opl-simplebench/util/format.ts +402 -0
  79. package/extensions/opl-simplebench/util/providers.ts +841 -0
  80. package/extensions/opl-todo/README.md +43 -0
  81. package/extensions/opl-todo/config.ts +71 -0
  82. package/extensions/opl-todo/index.ts +415 -0
  83. package/extensions/opl-webaccess/README.md +47 -0
  84. package/extensions/opl-webaccess/config.ts +55 -0
  85. package/extensions/opl-webaccess/extract.ts +83 -0
  86. package/extensions/opl-webaccess/index.ts +232 -0
  87. package/extensions/opl-webaccess/package-lock.json +345 -0
  88. package/extensions/opl-webaccess/package.json +18 -0
  89. package/extensions/opl-webaccess/pdf.ts +8 -0
  90. package/extensions/opl-webaccess/providers/ddgs.ts +72 -0
  91. package/extensions/opl-webaccess/providers/exa.ts +74 -0
  92. package/extensions/opl-webaccess/providers/gemini.ts +60 -0
  93. package/extensions/opl-webaccess/providers/searxng.ts +86 -0
  94. package/extensions/opl-webaccess/providers/tavily.ts +60 -0
  95. package/extensions/opl-webaccess/search.ts +36 -0
  96. package/extensions/opl-webaccess/storage.ts +51 -0
  97. package/extensions/opl-webaccess/turndown.d.ts +1 -0
  98. package/extensions/opl-webaccess/types.ts +26 -0
  99. package/extensions/opl-webaccess/utils.ts +28 -0
  100. package/package.json +55 -0
@@ -0,0 +1,39 @@
1
+ export interface ReasoningFixture {
2
+ name: string;
3
+ prompt: string;
4
+ expectedAnswer: string;
5
+ category: string;
6
+ }
7
+
8
+ const finalAnswer = "Reason as needed. Put only the exact answer on your final line.";
9
+
10
+ export const REASONING_TESTS: ReasoningFixture[] = [
11
+ { name: "snail_wall", prompt: `A snail climbs 3 feet each day and slides 2 feet each night. The wall is 10 feet tall. How many days until it first reaches the top? ${finalAnswer}`, expectedAnswer: "8", category: "logic" },
12
+ { name: "math_sequence", prompt: `What is the next number: 2, 6, 18, 54, ? ${finalAnswer}`, expectedAnswer: "162", category: "math" },
13
+ { name: "spatial_directions", prompt: `Face north. Turn 90 degrees clockwise, then 180 degrees counter-clockwise. Which direction are you facing? ${finalAnswer}`, expectedAnswer: "west", category: "spatial" },
14
+ { name: "commonsense", prompt: `Can a rooster lay an egg? Answer with a yes or a no only. ${finalAnswer}`, expectedAnswer: "no", category: "commonsense" },
15
+ { name: "code_simplify", prompt: `What is x after: let x = 0; for (let i = 1; i <= 5; i++) x += i; ${finalAnswer}`, expectedAnswer: "15", category: "code" },
16
+ { name: "bat_and_ball", prompt: `A bat and ball cost 110 cents. The bat costs 100 cents more than the ball. How many cents does the ball cost? Answer with digits only. ${finalAnswer}`, expectedAnswer: "5", category: "counterint" },
17
+ { name: "scale_weight", prompt: `One weight is 100 grams. Another is four times as heavy. How many grams does the second weigh? Answer with digits only. ${finalAnswer}`, expectedAnswer: "400", category: "counterint" },
18
+ { name: "syllogism", prompt: `All mammals are warm-blooded. All dogs are mammals. What property do all dogs have? Answer exactly: warm-blooded. ${finalAnswer}`, expectedAnswer: "warm-blooded", category: "logic" },
19
+ { name: "if_then_chain", prompt: `If it rains, the ground gets wet. If the ground gets wet, grass grows. It is raining. What happens to the grass? Answer exactly: grass grows. ${finalAnswer}`, expectedAnswer: "grass grows", category: "logic" },
20
+ { name: "cause_effect", prompt: `A tomato plant produces 4 tomatoes each day for 3 days. How many tomatoes does it produce? Answer with digits only. ${finalAnswer}`, expectedAnswer: "12", category: "causal" },
21
+ { name: "relative_quantities", prompt: `Tom has three times as many apples as Sara. Sara has five apples. How many apples does Tom have? Answer with digits only. ${finalAnswer}`, expectedAnswer: "15", category: "comparative" },
22
+ { name: "analogy_1", prompt: `Use the explicitly stated relationship “is the capital city of”: Paris is to France as Tokyo is to which country? Answer exactly one word: Japan or China. Reason as needed. Put only the exact answer on your final line. ${finalAnswer}`, expectedAnswer: "japan", category: "analogy" },
23
+ { name: "analogy_2", prompt: `3 is to 6 as 5 is to what number? ${finalAnswer}`, expectedAnswer: "10", category: "analogy" },
24
+ { name: "physics_1", prompt: `Which has more mass? Answer exactly: bowling ball or tennis ball. ${finalAnswer}`, expectedAnswer: "bowling ball", category: "commonsense" },
25
+ { name: "physics_2", prompt: `Compared with before heating, is a metal spoon hotter or colder after it is heated? ${finalAnswer}`, expectedAnswer: "hotter", category: "commonsense" },
26
+ { name: "objects_1", prompt: `What tool cuts paper? ${finalAnswer}`, expectedAnswer: "scissors", category: "commonsense" },
27
+ { name: "social_1", prompt: `Someone says please and thank you. Are they polite or rude? Answer polite or rude. ${finalAnswer}`, expectedAnswer: "polite", category: "commonsense" },
28
+ { name: "animals_1", prompt: `Dolphins live in water, not on land. Answer exactly: water or land. ${finalAnswer}`, expectedAnswer: "water", category: "commonsense" },
29
+ { name: "gk_1", prompt: `Which planet is known as the Red Planet? ${finalAnswer}`, expectedAnswer: "mars", category: "commonsense" },
30
+ { name: "gk_2", prompt: `How many days are in a leap year? ${finalAnswer}`, expectedAnswer: "366", category: "commonsense" },
31
+ ];
32
+
33
+ export const MULTISTEP_INSTRUCTION = `Respond with only this JSON object, with no Markdown or explanation:
34
+ {"operation":"status","requestId":"bench-42","ok":true}`;
35
+
36
+ export const CALC_TOOL_DEFINITION = {
37
+ type: "function" as const,
38
+ function: { name: "calculate", description: "Perform a mathematical calculation", parameters: { type: "object", properties: { expression: { type: "string" } }, required: ["expression"] } },
39
+ };
@@ -0,0 +1,6 @@
1
+ import type { ServerStats } from "./llama-server";
2
+
3
+ export interface SimplebenchOptions { model?: string; allModels: boolean; writeArtifact: boolean; thinkingMax?: boolean; codingLite?: boolean; testAll?: boolean; researchLive?: boolean; llamaServer?: boolean; llamagputop?: boolean; tag?: string; }
4
+ export interface RequestMetrics { requestCount: number; retryCount: number; wallTimeMs: number; timeToAnswerMs: number; timeToFirstTokenMs: number | null; startedAt: string | null; finishedAt: string | null; inputTokens: number | null; outputTokens: number | null; totalTokens: number | null; outputTokensPerSecond: number | null; toolCalls: Array<{ name: string; arguments?: unknown }>; }
5
+ export interface TestRecord { id: string; kind: "reasoning" | "instructions" | "tools" | "coding" | "research-grounded" | "research-live"; category?: string; prompt: string; response: string | null; expectedAnswer?: string | string[]; extractedAnswer?: string; score: string; passed: boolean; error: string | null; metrics: RequestMetrics; coding?: { publicPassed: boolean; hiddenPassed: boolean; verifiedAfterEdit: boolean; unrelatedFiles: string[]; toolCalls: number; turns: number; efficiency?: "STRONG" | "MODERATE" | "WEAK" | "FAIL"; }; research?: { toolCalls: string[]; files: string[]; }; }
6
+ export interface RunArtifact { schemaVersion: 1; benchmark: { name: "opl-simplebench"; suite?: "baseline" | "coding-lite" | "test-all"; tag?: string; model: string; provider: string; providerKind: string; thinking: { requested: "default" | "max"; effective: "provider-default" | "openai-reasoning-effort" | "pi-bedrock-reasoning"; level: "max" | null; modelMetadataSource: "active-context" | "scoped-model" | null; }; startedAt: string; finishedAt: string; wallTimeMs: number; artifactEnabled: boolean; }; tests: TestRecord[]; summary: Record<string, unknown> & { serverStats?: ServerStats }; }
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Shared test utilities for simplebench extension.
3
+ * Extracted from extensions/simplebench.ts to reduce duplication between
4
+ * Ollama-specific and Provider-specific test variants.
5
+ *
6
+ * @module shared/simplebench-utils
7
+ * @writtenby VTSTech — https://www.vts-tech.org
8
+ */
9
+
10
+ import * as fs from "node:fs";
11
+ import * as os from "node:os";
12
+ import * as path from "node:path";
13
+
14
+ // ============================================================================
15
+ // Configuration Constants
16
+ // ============================================================================
17
+
18
+ /**
19
+ * Configuration constants for model testing.
20
+ * Centralized to make tuning and maintenance easier.
21
+ *
22
+ * @property DEFAULT_TIMEOUT_MS - Default timeout for Ollama API calls (~16.7 min)
23
+ * @property CONNECT_TIMEOUT_S - Connection timeout for fetch (seconds)
24
+ * @property MAX_RETRIES - Number of retry attempts for transient failures
25
+ * @property RETRY_DELAY_MS - Delay between retry attempts (milliseconds)
26
+ * @property MIN_THINKING_LENGTH - Minimum characters to consider thinking tokens valid
27
+ * @property TOOL_TEST_TIMEOUT_MS - Timeout for tool usage tests
28
+ * @property TOOL_SUPPORT_TIMEOUT_MS - Timeout for tool support detection
29
+ * @property TAGS_TIMEOUT_MS - Timeout for /api/tags requests
30
+ */
31
+ export const CONFIG = {
32
+ // General API settings - standardized across all providers
33
+ DEFAULT_TIMEOUT_MS: 300000, // 5 minutes - reasonable timeout for all providers
34
+ CONNECT_TIMEOUT_S: 60, // 60 seconds to establish connection
35
+ MAX_RETRIES: 2, // Two retries for transient failures (standardized)
36
+ RETRY_DELAY_MS: 15000, // 15 seconds between retries (standardized)
37
+
38
+ // Test-specific settings - standardized across all providers
39
+ MIN_THINKING_LENGTH: 10, // Minimum chars to consider thinking tokens valid
40
+ TOOL_TEST_TIMEOUT_MS: 300000, // 5 minutes - consistent timeout for tool usage tests
41
+ TOOL_SUPPORT_TIMEOUT_MS: 300000, // 5 minutes - consistent timeout for tool support detection
42
+
43
+ // Metadata retrieval
44
+ TAGS_TIMEOUT_MS: 15000, // 15 seconds for /api/tags
45
+ MODEL_INFO_TIMEOUT_MS: 30000, // 30 seconds for model info lookup
46
+
47
+ // Provider API settings
48
+ PROVIDER_TIMEOUT_MS: 300000, // 5 minutes - consistent with Ollama
49
+ PROVIDER_TOOL_TIMEOUT_MS: 300000, // 5 minutes - consistent with Ollama tool tests
50
+
51
+ // Context length fetching
52
+ CONTEXT_BATCH_SIZE: 3, // Concurrent requests when fetching model context lengths
53
+
54
+ // Rate limiting
55
+ TEST_DELAY_MS: 4000, // 4 seconds between tests to avoid rate limiting
56
+ } as const;
57
+
58
+ // ============================================================================
59
+ // User Configuration Overrides
60
+ // ============================================================================
61
+
62
+ const TEST_CONFIG_DIR = process.env.PI_AGENT_DIR || path.join(os.homedir(), ".pi", "agent");
63
+ export const TEST_CONFIG_PATH = path.join(TEST_CONFIG_DIR, "configs", "opl-simplebench.json");
64
+ const LEGACY_TEST_CONFIG_PATH = path.join(TEST_CONFIG_DIR, "simplebench-config.json");
65
+
66
+ /** Shape of the user configuration file. */
67
+ export interface ModelTestUserConfig {
68
+ defaultTimeoutMs?: number;
69
+ connectTimeoutS?: number;
70
+ maxRetries?: number;
71
+ retryDelayMs?: number;
72
+ testDelayMs?: number;
73
+ toolTestTimeoutMs?: number;
74
+ providerTimeoutMs?: number;
75
+ providerToolTimeoutMs?: number;
76
+ contextBatchSize?: number;
77
+ researchSearchProvider?: "ddgs" | "searxng";
78
+ researchSearchUrl?: string;
79
+ researchMaxResults?: number;
80
+ llamaServerUrl?: string;
81
+ llamagputopUrl?: string;
82
+ }
83
+
84
+ /**
85
+ * Read user configuration from ~/.pi/agent/simplebench-config.json.
86
+ * Returns an empty object if the file doesn't exist or is invalid.
87
+ */
88
+ export function readTestConfig(): ModelTestUserConfig {
89
+ try {
90
+ const configPath = fs.existsSync(TEST_CONFIG_PATH) ? TEST_CONFIG_PATH : LEGACY_TEST_CONFIG_PATH;
91
+ if (fs.existsSync(configPath)) return JSON.parse(fs.readFileSync(configPath, "utf-8")) as ModelTestUserConfig;
92
+ } catch { /* config read/parse failure is non-critical — defaults are used */ }
93
+ return {};
94
+ }
95
+
96
+ /**
97
+ * Get effective test configuration by merging user overrides with defaults.
98
+ * User values take precedence over CONFIG defaults.
99
+ */
100
+ export function getEffectiveConfig(): typeof CONFIG {
101
+ const userConfig = readTestConfig();
102
+ return {
103
+ ...CONFIG,
104
+ DEFAULT_TIMEOUT_MS: (userConfig.defaultTimeoutMs ?? CONFIG.DEFAULT_TIMEOUT_MS) as typeof CONFIG.DEFAULT_TIMEOUT_MS,
105
+ CONNECT_TIMEOUT_S: (userConfig.connectTimeoutS ?? CONFIG.CONNECT_TIMEOUT_S) as typeof CONFIG.CONNECT_TIMEOUT_S,
106
+ MAX_RETRIES: (userConfig.maxRetries ?? CONFIG.MAX_RETRIES) as typeof CONFIG.MAX_RETRIES,
107
+ RETRY_DELAY_MS: (userConfig.retryDelayMs ?? CONFIG.RETRY_DELAY_MS) as typeof CONFIG.RETRY_DELAY_MS,
108
+ TEST_DELAY_MS: (userConfig.testDelayMs ?? CONFIG.TEST_DELAY_MS) as typeof CONFIG.TEST_DELAY_MS,
109
+ TOOL_TEST_TIMEOUT_MS: (userConfig.toolTestTimeoutMs ?? CONFIG.TOOL_TEST_TIMEOUT_MS) as typeof CONFIG.TOOL_TEST_TIMEOUT_MS,
110
+ PROVIDER_TIMEOUT_MS: (userConfig.providerTimeoutMs ?? CONFIG.PROVIDER_TIMEOUT_MS) as typeof CONFIG.PROVIDER_TIMEOUT_MS,
111
+ PROVIDER_TOOL_TIMEOUT_MS: (userConfig.providerToolTimeoutMs ?? CONFIG.PROVIDER_TOOL_TIMEOUT_MS) as typeof CONFIG.PROVIDER_TOOL_TIMEOUT_MS,
112
+ CONTEXT_BATCH_SIZE: (userConfig.contextBatchSize ?? CONFIG.CONTEXT_BATCH_SIZE) as typeof CONFIG.CONTEXT_BATCH_SIZE,
113
+ };
114
+ }
115
+
116
+ // ============================================================================
117
+ // Weather Tool Definition (shared across all tool tests)
118
+ // ============================================================================
119
+
120
+ /**
121
+ * Standard get_weather tool schema used by all tool usage tests.
122
+ * Previously copy-pasted in 3+ places — now a single shared constant.
123
+ */
124
+ export const WEATHER_TOOL_DEFINITION = {
125
+ type: "function" as const,
126
+ function: {
127
+ name: "get_weather",
128
+ description: "Get the current weather for a location",
129
+ parameters: {
130
+ type: "object",
131
+ properties: {
132
+ location: { type: "string", description: "City name" },
133
+ unit: { type: "string", enum: ["celsius", "fahrenheit"] },
134
+ },
135
+ required: ["location"],
136
+ },
137
+ },
138
+ };
139
+
140
+ // ============================================================================
141
+ // Tool Support Cache (path only — backing file is no longer written)
142
+ // ============================================================================
143
+
144
+ const TOOL_SUPPORT_CACHE_DIR = path.join(os.homedir(), ".pi", "agent", "cache");
145
+ export const TOOL_SUPPORT_CACHE_PATH = path.join(TOOL_SUPPORT_CACHE_DIR, "tool_support.json");
146
+
147
+ // ============================================================================
148
+ // ChatFn Abstraction
149
+ // ============================================================================
150
+
151
+ /**
152
+ * Abstraction over Ollama and Provider chat APIs.
153
+ * Callers wrap their specific chat implementation into this shape.
154
+ */
155
+ export type ChatMessage = { role: string; content: string; tool_calls?: any[]; tool_call_id?: string };
156
+
157
+ export function buildToolResultMessages(assistantContent: string, calls: any[], results: string[]): ChatMessage[] {
158
+ const toolCalls = calls.map((call, index) => ({ ...call, id: call.id || `toolcall${index + 1}` }));
159
+ return [
160
+ { role: "assistant", content: assistantContent || "", tool_calls: toolCalls },
161
+ ...toolCalls.map((call, index) => ({ role: "tool", tool_call_id: call.id, content: results[index] || "INVALID_ARGUMENTS" })),
162
+ ];
163
+ }
164
+
165
+ export type ChatFn = (
166
+ model: string,
167
+ messages: ChatMessage[],
168
+ options?: Record<string, unknown>,
169
+ ) => Promise<{ content: string; toolCalls?: any[]; elapsedMs: number; raw?: any; requestCount?: number; retryCount?: number; startedAt?: string; finishedAt?: string; timeToFirstTokenMs?: number | null }>;
170
+
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Debug logging utility for Pi Coding Agent extensions.
3
+ *
4
+ * Only emits output when the PI_EXTENSIONS_DEBUG environment variable is set to "1".
5
+ * This allows developers to enable verbose diagnostic logging without impacting
6
+ * production performance or cluttering normal output.
7
+ *
8
+ * @module shared/debug
9
+ */
10
+
11
+ const DEBUG_ENABLED = process?.env?.PI_EXTENSIONS_DEBUG === "1";
12
+
13
+ /**
14
+ * Log a debug message to stderr (via console.debug).
15
+ *
16
+ * @param module - Short identifier for the emitting module (e.g., "ollama", "security")
17
+ * @param message - Human-readable message describing the event
18
+ * @param args - Additional values to log alongside the message
19
+ *
20
+ * @example
21
+ * ```typescript
22
+ * import { debugLog } from "../shared/debug";
23
+ * try { … } catch (err) {
24
+ * debugLog("ollama", "failed to read models.json", err);
25
+ * }
26
+ * ```
27
+ */
28
+ export function debugLog(module: string, message: string, ...args: unknown[]): void {
29
+ if (!DEBUG_ENABLED) return;
30
+ const timestamp = new Date().toISOString();
31
+ console.debug(`[pi-ext:${module}] ${timestamp} ${message}`, ...args);
32
+ }
@@ -0,0 +1,402 @@
1
+ /**
2
+ * Shared formatting utilities for Pi Coding Agent extensions.
3
+ * Extracted from diag.ts and simplebench.ts to eliminate duplication.
4
+ *
5
+ * @module shared/format
6
+ * @writtenby VTSTech — https://www.vts-tech.org
7
+ */
8
+
9
+ // ============================================================================
10
+ // Section & Indicator Helpers
11
+ // ============================================================================
12
+
13
+ /**
14
+ * Render a section header with a horizontal rule.
15
+ *
16
+ * Creates a visually distinct section header in the format:
17
+ * `── TITLE ──────────────────────────────────────────────`
18
+ *
19
+ * The horizontal rule automatically fills the remaining space
20
+ * to create a consistent 60-character wide header.
21
+ *
22
+ * @param title - The section title to display
23
+ * @returns A formatted section header string
24
+ *
25
+ * @example
26
+ * ```typescript
27
+ * section("REASONING TEST");
28
+ * // Returns: "\n── REASONING TEST ───────────────────────────────────────"
29
+ * ```
30
+ */
31
+ export function section(title: string): string {
32
+ return `\n── ${title} ${"─".repeat(Math.max(1, 60 - title.length - 4))}`;
33
+ }
34
+
35
+ /**
36
+ * Format a success message with a checkmark indicator.
37
+ *
38
+ * @param msg - The message to format
39
+ * @returns The formatted message with ✅ prefix
40
+ *
41
+ * @example
42
+ * ```typescript
43
+ * ok("Tool call successful");
44
+ * // Returns: " ✅ Tool call successful"
45
+ * ```
46
+ */
47
+ export function ok(msg: string): string { return ` ✅ ${msg}`; }
48
+
49
+ /**
50
+ * Format a failure message with an X indicator.
51
+ *
52
+ * @param msg - The message to format
53
+ * @returns The formatted message with ❌ prefix
54
+ *
55
+ * @example
56
+ * ```typescript
57
+ * fail("Connection timeout");
58
+ * // Returns: " ❌ Connection timeout"
59
+ * ```
60
+ */
61
+ export function fail(msg: string): string { return ` ❌ ${msg}`; }
62
+
63
+ /**
64
+ * Format a warning message with a warning indicator.
65
+ *
66
+ * @param msg - The message to format
67
+ * @returns The formatted message with ⚠️ prefix
68
+ *
69
+ * @example
70
+ * ```typescript
71
+ * warn("Model may be too slow for production use");
72
+ * // Returns: " ⚠️ Model may be too slow for production use"
73
+ * ```
74
+ */
75
+ export function warn(msg: string): string { return ` ⚠️ ${msg}`; }
76
+
77
+ /**
78
+ * Format an informational message with an info indicator.
79
+ *
80
+ * @param msg - The message to format
81
+ * @returns The formatted message with ℹ️ prefix
82
+ *
83
+ * @example
84
+ * ```typescript
85
+ * info("Testing reasoning capability...");
86
+ * // Returns: " ℹ️ Testing reasoning capability..."
87
+ * ```
88
+ */
89
+ export function info(msg: string): string { return ` ℹ️ ${msg}`; }
90
+
91
+ // ============================================================================
92
+ // Numeric Formatters
93
+ // ============================================================================
94
+
95
+ /**
96
+ * Format bytes as a human-readable string with appropriate units.
97
+ *
98
+ * Automatically selects the most appropriate unit (B, KB, MB, GB, TB)
99
+ * and formats the number with one decimal place.
100
+ *
101
+ * @param bytes - The number of bytes to format
102
+ * @returns A human-readable string representation
103
+ *
104
+ * @example
105
+ * ```typescript
106
+ * bytesHuman(512); // "512.0B"
107
+ * bytesHuman(1536); // "1.5KB"
108
+ * bytesHuman(1048576); // "1.0MB"
109
+ * bytesHuman(1073741824); // "1.0GB"
110
+ * ```
111
+ */
112
+ export function bytesHuman(bytes: number): string {
113
+ const units = ["B", "KB", "MB", "GB", "TB"];
114
+ let i = 0;
115
+ let b = bytes;
116
+ while (b >= 1024 && i < units.length - 1) { b /= 1024; i++; }
117
+ return `${b.toFixed(1)}${units[i]}`;
118
+ }
119
+
120
+ /**
121
+ * Format milliseconds as a human-readable duration string.
122
+ *
123
+ * Automatically selects the most appropriate unit (ms, s, m)
124
+ * based on the magnitude of the input.
125
+ *
126
+ * @param ms - The duration in milliseconds
127
+ * @returns A human-readable string representation
128
+ *
129
+ * @example
130
+ * ```typescript
131
+ * msHuman(500); // "500ms"
132
+ * msHuman(1500); // "1.5s"
133
+ * msHuman(90000); // "1.5m"
134
+ * ```
135
+ */
136
+ export function msHuman(ms: number): string {
137
+ if (ms < 1000) return `${ms.toFixed(0)}ms`;
138
+ if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
139
+ return `${(ms / 60000).toFixed(1)}m`;
140
+ }
141
+
142
+ /**
143
+ * Format bytes as a compact string for status bar display.
144
+ *
145
+ * Uses single-letter suffixes (K, M, G) without spaces,
146
+ * optimized for narrow display contexts like status bars.
147
+ *
148
+ * @param b - The number of bytes to format
149
+ * @returns A compact string representation
150
+ *
151
+ * @example
152
+ * ```typescript
153
+ * fmtBytes(512); // "512K"
154
+ * fmtBytes(1048576); // "1.0M"
155
+ * fmtBytes(1073741824); // "1.0G"
156
+ * ```
157
+ */
158
+ export function fmtBytes(b: number): string {
159
+ if (b === 0) return "0B";
160
+ if (b < 1024) return `${b}B`;
161
+ if (b >= 1073741824) return `${(b / 1073741824).toFixed(1)}G`;
162
+ if (b >= 1048576) return `${(b / 1048576).toFixed(0)}M`;
163
+ return `${(b / 1024).toFixed(0)}K`;
164
+ }
165
+
166
+ /**
167
+ * Format milliseconds as a compact duration for status bar display.
168
+ *
169
+ * For durations over a minute, displays as `Xm Ys` format.
170
+ * For shorter durations, displays as seconds with one decimal.
171
+ *
172
+ * @param ms - The duration in milliseconds
173
+ * @returns A compact string representation
174
+ *
175
+ * @example
176
+ * ```typescript
177
+ * fmtDur(500); // "500ms"
178
+ * fmtDur(1500); // "1.5s"
179
+ * fmtDur(90000); // "1m30s"
180
+ * ```
181
+ */
182
+ export function fmtDur(ms: number): string {
183
+ if (ms < 1000) return `${Math.round(ms)}ms`;
184
+ if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
185
+ return `${Math.floor(ms / 60000)}m${Math.round((ms % 60000) / 1000)}s`;
186
+ }
187
+
188
+ /**
189
+ * Calculate and format a percentage.
190
+ *
191
+ * @param used - The used/occupied amount
192
+ * @param total - The total capacity
193
+ * @returns A percentage string with one decimal place
194
+ *
195
+ * @example
196
+ * ```typescript
197
+ * pct(512, 1024); // "50.0%"
198
+ * pct(75, 100); // "75.0%"
199
+ * ```
200
+ */
201
+ export function pct(used: number, total: number): string {
202
+ if (total === 0) return "0.0%";
203
+ return `${((used / total) * 100).toFixed(1)}%`;
204
+ }
205
+
206
+ // ============================================================================
207
+ // String Utilities
208
+ // ============================================================================
209
+
210
+ /**
211
+ * Truncate a string to a maximum length with an ellipsis suffix.
212
+ *
213
+ * If the string exceeds the maximum length, it is truncated
214
+ * and "..." is appended to indicate truncation.
215
+ *
216
+ * @param s - The string to truncate
217
+ * @param max - Maximum length before truncation
218
+ * @returns The original string or truncated version
219
+ *
220
+ * @example
221
+ * ```typescript
222
+ * truncate("Hello, World!", 5); // "He..."
223
+ * truncate("Short", 10); // "Short"
224
+ * ```
225
+ */
226
+ export function truncate(s: string, max: number): string {
227
+ return s.length > max ? s.slice(0, max) + "..." : s;
228
+ }
229
+
230
+ /**
231
+ * Strip markdown code fences and truncate large content for clean report output.
232
+ *
233
+ * Performs the following transformations:
234
+ * 1. Removes markdown code fence markers (``` and ```lang)
235
+ * 2. Collapses excessive blank lines
236
+ * 3. Detects and truncates HTML content (error pages)
237
+ * 4. Caps output at a maximum number of lines
238
+ *
239
+ * @param s - The string to sanitize
240
+ * @param maxLines - Maximum number of lines to output (default: 40)
241
+ * @returns The sanitized and potentially truncated string
242
+ *
243
+ * @example
244
+ * ```typescript
245
+ * sanitizeForReport("```json\n{\"key\": \"value\"}\n```");
246
+ * // Returns: "{\"key\": \"value\"}"
247
+ *
248
+ * sanitizeForReport(largeHtmlContent);
249
+ * // Returns: "<!DOCTYPE html>...\n ℹ️ (HTML response truncated)"
250
+ * ```
251
+ */
252
+ export function sanitizeForReport(s: string, maxLines = 40): string {
253
+ let cleaned = s.replace(/^\s*```[a-zA-Z]*[ \t]*\n?/gm, "");
254
+ cleaned = cleaned.replace(/^\s*```[ \t]*\n?/gm, "");
255
+ cleaned = cleaned.replace(/\n{3,}/g, "\n\n").trim();
256
+
257
+ // Detect HTML content (error pages, curl failures) and truncate to first few lines
258
+ // Use strict detection: require a closing tag AND a common HTML opening tag to avoid
259
+ // false positives on code that mentions HTML-like syntax (e.g., "use <Item> from 'react'")
260
+ if (/<!DOCTYPE\b|<html[\s>]/i.test(cleaned) || (/<[a-z][\s\S]*>/i.test(cleaned) && cleaned.includes("</") && /<(?:div|span|p|head|body|html|table|form|script)\b/i.test(cleaned))) {
261
+ const firstLine = cleaned.split("\n")[0];
262
+ return truncate(firstLine, 200) + "\n ℹ️ (HTML response truncated)";
263
+ }
264
+
265
+ // Cap line count for any large output
266
+ const lines = cleaned.split("\n");
267
+ if (lines.length > maxLines) {
268
+ cleaned = lines.slice(0, maxLines).join("\n") + `\n ℹ️ (truncated, ${lines.length - maxLines} more lines)`;
269
+ }
270
+
271
+ return cleaned;
272
+ }
273
+
274
+ /**
275
+ * Right-pad a string to a specified length with spaces.
276
+ *
277
+ * If the string is already longer than the target length,
278
+ * it is returned unchanged.
279
+ *
280
+ * @param s - The string to pad
281
+ * @param n - The target length
282
+ * @returns The padded string
283
+ *
284
+ * @example
285
+ * ```typescript
286
+ * padRight("test", 10); // "test "
287
+ * padRight("toolong", 5); // "toolong"
288
+ * ```
289
+ */
290
+ export function padRight(s: string, n: number): string {
291
+ return s + " ".repeat(Math.max(0, n - s.length));
292
+ }
293
+
294
+ // ============================================================================
295
+ // Model Estimation
296
+ // ============================================================================
297
+
298
+ /**
299
+ * Estimate memory usage (in bytes) from parameter size and quantization level.
300
+ *
301
+ * Uses bits-per-parameter based on the quantization format:
302
+ * - FP32/BF16/F16: 16 bits (full precision)
303
+ * - Q8_0: 8 bits
304
+ * - Q5_K_M/Q5_M/Q5_K_S/Q5_0/Q5_1: 5.5 bits (average)
305
+ * - Q4_K_M/Q4_K_S/Q4_0/Q4_1/Q4_M: 4.5 bits (average)
306
+ * - Q3_K_M/Q3_K_S/Q3_K_L: 3.5 bits (average)
307
+ * - Q2_K: 2.5 bits (average)
308
+ * - IQ variants (IQ3_XS, IQ4_XS, etc.): mapped to nearest Q equivalent
309
+ * - Fallback: 5 bits
310
+ *
311
+ * Returns both GPU (VRAM) and CPU (RAM) estimates:
312
+ * - GPU: base size + 10% overhead (KV cache, runtime allocs — weights dominate)
313
+ * - CPU: context-aware overhead — KV cache is pre-allocated at model load
314
+ * and scales linearly with context window. Formula calibrated against
315
+ * real-world Colab CPU observations:
316
+ * 1.5 + (contextLength / 100_000)
317
+ * Without context, falls back to 2.5× (typical mid-range CPU deployment).
318
+ *
319
+ * @param parameterSize - Human-readable parameter count (e.g., "7B", "1.5B", "350M")
320
+ * @param quantizationLevel - Quantization format string (e.g., "Q4_K_M", "BF16")
321
+ * @param contextLength - Model context window in tokens (e.g., 32768, 131072)
322
+ * @returns Object with GPU and CPU estimates in bytes, or undefined if unparseable
323
+ *
324
+ * @example
325
+ * ```typescript
326
+ * estimateMemory("7B", "Q4_K_M", 32768); // { gpu: ~3.5GB, cpu: ~5.6GB }
327
+ * estimateMemory("7B", "Q4_K_M", 131072); // { gpu: ~3.5GB, cpu: ~15.8GB }
328
+ * estimateMemory("350M", "BF16", 32768); // { gpu: ~0.7GB, cpu: ~1.1GB }
329
+ * ```
330
+ */
331
+ export function estimateMemory(parameterSize: string, quantizationLevel: string, contextLength?: number): { gpu: number; cpu: number } | undefined {
332
+ const params = parseParamCount(parameterSize);
333
+ if (params === undefined) return undefined;
334
+
335
+ const bitsPerParam = bitsPerParamForQuant(quantizationLevel);
336
+ // Base model size in bytes
337
+ const modelBytes = (params * bitsPerParam) / 8;
338
+
339
+ // CPU overhead scales with context window — KV cache is pre-allocated at load
340
+ // Calibrated: nemotron 4B Q4, 131k ctx → 2.8× observed (formula gives 2.82×)
341
+ const cpuMultiplier = contextLength != null
342
+ ? 1.5 + (contextLength / 100_000)
343
+ : 2.5; // flat fallback when context unknown
344
+
345
+ return {
346
+ gpu: Math.ceil(modelBytes * 1.1), // 10% overhead — GPU: weights dominate
347
+ cpu: Math.ceil(modelBytes * cpuMultiplier), // context-aware — CPU: KV cache dominates
348
+ };
349
+ }
350
+
351
+ /**
352
+ * Parse a human-readable parameter count string to a numeric value.
353
+ *
354
+ * Handles formats like "7B", "1.5B", "350M", "13a" (Apple-style),
355
+ * "70B", "0.6B", "494.03M", etc.
356
+ *
357
+ * @param s - The parameter size string
358
+ * @returns Parameter count as a number, or undefined if unparseable
359
+ */
360
+ function parseParamCount(s: string): number | undefined {
361
+ if (!s || typeof s !== "string") return undefined;
362
+ const str = s.trim().toLowerCase();
363
+
364
+ // Match: number + suffix (B/b, M/m, T/t, a/apple)
365
+ const match = str.match(/^([\d.]+)\s*([bmt]?|a(?:pple)?)$/);
366
+ if (!match) return undefined;
367
+
368
+ const num = parseFloat(match[1]);
369
+ if (isNaN(num) || num <= 0) return undefined;
370
+
371
+ const suffix = match[2];
372
+ switch (suffix) {
373
+ case "b": return num * 1e9;
374
+ case "m": return num * 1e6;
375
+ case "t": return num * 1e12;
376
+ case "a": return num * 1e9; // Apple-style (e.g., "3a" = 3B parameters)
377
+ case "": return num * 1e9; // Bare number assumed to be billions
378
+ default: return undefined;
379
+ }
380
+ }
381
+
382
+ /**
383
+ * Determine bits per parameter based on quantization format string.
384
+ */
385
+ function bitsPerParamForQuant(quant: string): number {
386
+ const q = quant.toUpperCase().replace(/[-_.]/g, "");
387
+
388
+ if (q.startsWith("FP32") || q === "F32" || q === "TF32") return 32;
389
+ if (q.startsWith("F16") || q === "BF16") return 16;
390
+ if (q.startsWith("Q8")) return 8;
391
+ if (q.startsWith("IQ4")) return 4.5;
392
+ if (q.startsWith("IQ3")) return 3.5;
393
+ if (q.startsWith("IQ2")) return 2.5;
394
+ if (q.startsWith("IQ1")) return 1.75;
395
+ if (q.startsWith("Q5") || q.startsWith("Q6")) return 5.5;
396
+ if (q.startsWith("Q4")) return 4.5;
397
+ if (q.startsWith("Q3")) return 3.5;
398
+ if (q.startsWith("Q2")) return 2.5;
399
+ if (q.startsWith("Q1")) return 1.75;
400
+
401
+ return 5; // conservative fallback
402
+ }