free-coding-models 0.3.11 β 0.3.12
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 +19 -0
- package/README.md +112 -1134
- package/bin/free-coding-models.js +13 -167
- package/package.json +2 -3
- package/src/cli-help.js +0 -18
- package/src/config.js +5 -117
- package/src/endpoint-installer.js +26 -64
- package/src/key-handler.js +56 -437
- package/src/legacy-proxy-cleanup.js +432 -0
- package/src/openclaw.js +69 -108
- package/src/opencode-config.js +48 -0
- package/src/opencode.js +6 -248
- package/src/overlays.js +23 -517
- package/src/product-flags.js +14 -0
- package/src/render-helpers.js +2 -34
- package/src/render-table.js +10 -18
- package/src/testfcm.js +90 -43
- package/src/token-usage-reader.js +9 -38
- package/src/tool-launchers.js +235 -409
- package/src/tool-metadata.js +0 -7
- package/src/utils.js +3 -68
- package/bin/fcm-proxy-daemon.js +0 -242
- package/src/account-manager.js +0 -634
- package/src/anthropic-translator.js +0 -440
- package/src/daemon-manager.js +0 -527
- package/src/error-classifier.js +0 -157
- package/src/log-reader.js +0 -195
- package/src/opencode-sync.js +0 -200
- package/src/proxy-foreground.js +0 -234
- package/src/proxy-server.js +0 -1506
- package/src/proxy-sync.js +0 -591
- package/src/proxy-topology.js +0 -85
- package/src/request-transformer.js +0 -180
- package/src/responses-translator.js +0 -423
- package/src/token-stats.js +0 -320
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/product-flags.js
|
|
3
|
+
* @description Product-level copy for temporarily unavailable surfaces.
|
|
4
|
+
*
|
|
5
|
+
* @details
|
|
6
|
+
* π The proxy bridge is being rebuilt from scratch. The main TUI still
|
|
7
|
+
* π shows a clear status line so users know the missing integration is
|
|
8
|
+
* π intentional instead of silently broken.
|
|
9
|
+
*
|
|
10
|
+
* @exports PROXY_DISABLED_NOTICE
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// π Public note rendered in the main TUI footer and reused in CLI/runtime guards.
|
|
14
|
+
export const PROXY_DISABLED_NOTICE = 'βΉοΈ Proxy is temporarily disabled while we rebuild it into a much more stable bridge for external tools.'
|
package/src/render-helpers.js
CHANGED
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
* - Overlay viewport management (scrolling, clamping, visibility)
|
|
11
11
|
* - Table viewport calculation
|
|
12
12
|
* - Sorting with pinned favorites and recommendations
|
|
13
|
-
* - Proxy status rendering
|
|
14
13
|
*
|
|
15
14
|
* π― Key features:
|
|
16
15
|
* - Emoji-aware display width calculation without external dependencies
|
|
@@ -19,7 +18,6 @@
|
|
|
19
18
|
* - Overlay viewport helpers (clamp, slice, scroll target visibility)
|
|
20
19
|
* - Table viewport calculation with scroll indicators
|
|
21
20
|
* - Sorting with pinned favorites/recommendations at top
|
|
22
|
-
* - Proxy status formatting
|
|
23
21
|
*
|
|
24
22
|
* β Functions:
|
|
25
23
|
* - `stripAnsi`: Remove ANSI color codes to estimate visible text width
|
|
@@ -32,13 +30,12 @@
|
|
|
32
30
|
* - `sliceOverlayLines`: Slice lines to viewport and pad with blanks
|
|
33
31
|
* - `calculateViewport`: Compute visible slice of model rows
|
|
34
32
|
* - `sortResultsWithPinnedFavorites`: Sort with pinned items at top
|
|
35
|
-
* - `renderProxyStatusLine`: Format proxy status for footer display
|
|
36
33
|
* - `adjustScrollOffset`: Clamp scrollOffset so cursor stays visible
|
|
37
34
|
*
|
|
38
35
|
* π¦ Dependencies:
|
|
39
36
|
* - chalk: Terminal colors and formatting
|
|
40
37
|
* - ../src/constants.js: OVERLAY_PANEL_WIDTH, TABLE_FIXED_LINES
|
|
41
|
-
* - ../src/utils.js: sortResults
|
|
38
|
+
* - ../src/utils.js: sortResults
|
|
42
39
|
*
|
|
43
40
|
* βοΈ Configuration:
|
|
44
41
|
* - OVERLAY_PANEL_WIDTH: Fixed width for overlay panels (from constants.js)
|
|
@@ -50,7 +47,7 @@
|
|
|
50
47
|
|
|
51
48
|
import chalk from 'chalk'
|
|
52
49
|
import { OVERLAY_PANEL_WIDTH, TABLE_FIXED_LINES } from './constants.js'
|
|
53
|
-
import { sortResults
|
|
50
|
+
import { sortResults } from './utils.js'
|
|
54
51
|
|
|
55
52
|
// π stripAnsi: Remove ANSI color/control sequences to estimate visible text width before padding.
|
|
56
53
|
// π Strips CSI sequences (SGR colors) and OSC sequences (hyperlinks).
|
|
@@ -189,35 +186,6 @@ export function sortResultsWithPinnedFavorites(results, sortColumn, sortDirectio
|
|
|
189
186
|
return [...bothRows, ...recommendedRows, ...favoriteRows, ...nonSpecialRows]
|
|
190
187
|
}
|
|
191
188
|
|
|
192
|
-
// βββ Proxy status rendering βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
193
|
-
|
|
194
|
-
// π renderProxyStatusLine: Maps proxyStartupStatus + active proxy into a chalk-coloured footer line.
|
|
195
|
-
// π Always returns a non-empty string (no hidden states) so the footer row is always present.
|
|
196
|
-
// π Delegates state classification to the pure getProxyStatusInfo helper (testable in utils.js).
|
|
197
|
-
export function renderProxyStatusLine(proxyStartupStatus, proxyInstance, proxyEnabled = false) {
|
|
198
|
-
const activeStatus = typeof proxyInstance?.getStatus === 'function' ? proxyInstance.getStatus() : null
|
|
199
|
-
const hasLiveProxy = Boolean(proxyInstance) && activeStatus?.running !== false
|
|
200
|
-
const info = getProxyStatusInfo(proxyStartupStatus, hasLiveProxy, proxyEnabled)
|
|
201
|
-
const neonGreen = chalk.rgb(57, 255, 20)
|
|
202
|
-
switch (info.state) {
|
|
203
|
-
case 'starting':
|
|
204
|
-
return chalk.dim(' ') + chalk.yellow('β³ Proxy') + chalk.dim(' startingβ¦')
|
|
205
|
-
case 'running': {
|
|
206
|
-
const resolvedPort = info.port ?? activeStatus?.port ?? activeStatus?.listeningPort ?? null
|
|
207
|
-
const resolvedAccountCount = info.accountCount ?? activeStatus?.accountCount ?? proxyInstance?._accounts?.length ?? null
|
|
208
|
-
const portPart = resolvedPort ? chalk.dim(` :${resolvedPort}`) : ''
|
|
209
|
-
const acctPart = resolvedAccountCount != null ? chalk.dim(` Β· ${resolvedAccountCount} account${resolvedAccountCount === 1 ? '' : 's'}`) : ''
|
|
210
|
-
return chalk.dim(' ') + neonGreen('π Proxy running') + portPart + acctPart
|
|
211
|
-
}
|
|
212
|
-
case 'failed':
|
|
213
|
-
return chalk.dim(' ') + chalk.red('π Proxy Stopped') + chalk.dim(` β ${info.reason}`)
|
|
214
|
-
case 'configured':
|
|
215
|
-
return chalk.dim(' ') + chalk.cyan('π Proxy configured') + chalk.dim(' β OpenCode rotation')
|
|
216
|
-
default:
|
|
217
|
-
return chalk.dim(' ') + chalk.red('π Proxy Stopped')
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
189
|
// βββ Scroll offset adjustment ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
222
190
|
|
|
223
191
|
// π adjustScrollOffset: Clamp scrollOffset so cursor is always within the visible viewport window.
|
package/src/render-table.js
CHANGED
|
@@ -13,13 +13,11 @@
|
|
|
13
13
|
* - Emoji-aware padding via padEndDisplay for aligned verdict/status cells
|
|
14
14
|
* - Viewport clipping with above/below indicators
|
|
15
15
|
* - Smart badges (mode, tier filter, origin filter)
|
|
16
|
-
* - Footer J badge: green "Proxy On" / red "Proxy Off" indicator with direct overlay access
|
|
17
16
|
* - Install-endpoints shortcut surfaced directly in the footer hints
|
|
18
17
|
* - Full-width red outdated-version banner when a newer npm release is known
|
|
19
18
|
* - Distinct auth-failure vs missing-key health labels so configured providers stay honest
|
|
20
19
|
*
|
|
21
20
|
* β Functions:
|
|
22
|
-
* - `setActiveProxy` β Provide the active proxy instance for footer status rendering
|
|
23
21
|
* - `renderTable` β Render the full TUI table as a string (no side effects)
|
|
24
22
|
*
|
|
25
23
|
* π¦ Dependencies:
|
|
@@ -28,7 +26,7 @@
|
|
|
28
26
|
* - ../src/tier-colors.js: TIER_COLOR
|
|
29
27
|
* - ../src/utils.js: getAvg, getVerdict, getUptime, getStabilityScore
|
|
30
28
|
* - ../src/ping.js: usagePlaceholderForProvider
|
|
31
|
-
* - ../src/render-helpers.js: calculateViewport, sortResultsWithPinnedFavorites,
|
|
29
|
+
* - ../src/render-helpers.js: calculateViewport, sortResultsWithPinnedFavorites, padEndDisplay
|
|
32
30
|
*
|
|
33
31
|
* @see bin/free-coding-models.js β main entry point that calls renderTable
|
|
34
32
|
*/
|
|
@@ -43,6 +41,7 @@ import { usagePlaceholderForProvider } from './ping.js'
|
|
|
43
41
|
import { formatTokenTotalCompact } from './token-usage-reader.js'
|
|
44
42
|
import { calculateViewport, sortResultsWithPinnedFavorites, padEndDisplay, displayWidth } from './render-helpers.js'
|
|
45
43
|
import { getToolMeta } from './tool-metadata.js'
|
|
44
|
+
import { PROXY_DISABLED_NOTICE } from './product-flags.js'
|
|
46
45
|
|
|
47
46
|
const ACTIVE_FILTER_BG_BY_TIER = {
|
|
48
47
|
'S+': [57, 255, 20],
|
|
@@ -84,16 +83,8 @@ export const PROVIDER_COLOR = {
|
|
|
84
83
|
iflow: [220, 231, 117],
|
|
85
84
|
}
|
|
86
85
|
|
|
87
|
-
// π Active proxy reference for footer status line (set by bin/free-coding-models.js).
|
|
88
|
-
let activeProxyRef = null
|
|
89
|
-
|
|
90
|
-
// π setActiveProxy: Store active proxy instance for renderTable footer line.
|
|
91
|
-
export function setActiveProxy(proxyInstance) {
|
|
92
|
-
activeProxyRef = proxyInstance
|
|
93
|
-
}
|
|
94
|
-
|
|
95
86
|
// βββ renderTable: mode param controls footer hint text (opencode vs openclaw) βββββββββ
|
|
96
|
-
export function renderTable(results, pendingPings, frame, cursor = null, sortColumn = 'avg', sortDirection = 'asc', pingInterval = PING_INTERVAL, lastPingTime = Date.now(), mode = 'opencode', tierFilterMode = 0, scrollOffset = 0, terminalRows = 0, terminalCols = 0, originFilterMode = 0,
|
|
87
|
+
export function renderTable(results, pendingPings, frame, cursor = null, sortColumn = 'avg', sortDirection = 'asc', pingInterval = PING_INTERVAL, lastPingTime = Date.now(), mode = 'opencode', tierFilterMode = 0, scrollOffset = 0, terminalRows = 0, terminalCols = 0, originFilterMode = 0, legacyStatus = null, pingMode = 'normal', pingModeSource = 'auto', hideUnconfiguredModels = false, widthWarningStartedAt = null, widthWarningDismissed = false, widthWarningShowCount = 0, settingsUpdateState = 'idle', settingsUpdateLatestVersion = null, legacyFlag = false, startupLatestVersion = null, versionAlertsEnabled = true, disableWidthsWarning = false) {
|
|
97
88
|
// π Filter out hidden models for display
|
|
98
89
|
const visibleResults = results.filter(r => !r.hidden)
|
|
99
90
|
|
|
@@ -561,7 +552,7 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
561
552
|
const usageCell = ''
|
|
562
553
|
|
|
563
554
|
// π Used column β total historical prompt+completion tokens consumed for this
|
|
564
|
-
// π exact provider/model pair, loaded
|
|
555
|
+
// π exact provider/model pair, loaded from the local usage snapshot file at startup.
|
|
565
556
|
const tokenTotal = Number(r.totalTokens) || 0
|
|
566
557
|
const tokensCell = tokenTotal > 0
|
|
567
558
|
? chalk.rgb(120, 210, 255)(formatTokenTotalCompact(tokenTotal).padEnd(W_TOKENS))
|
|
@@ -595,8 +586,6 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
595
586
|
const activeHotkey = (keyLabel, text, bg = [57, 255, 20], fg = [0, 0, 0]) => chalk.bgRgb(...bg).rgb(...fg)(` ${keyLabel}${text} `)
|
|
596
587
|
// π Line 1: core navigation + filtering shortcuts
|
|
597
588
|
lines.push(
|
|
598
|
-
(proxyEnabled ? activeHotkey('J', ' π‘ FCM Proxy V2 On') : activeHotkey('J', ' π‘ FCM Proxy V2 Off', [180, 30, 30], [255, 255, 255])) +
|
|
599
|
-
chalk.dim(` β’ `) +
|
|
600
589
|
hotkey('F', ' Toggle Favorite') +
|
|
601
590
|
chalk.dim(` β’ `) +
|
|
602
591
|
(tierFilterMode > 0
|
|
@@ -609,13 +598,11 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
609
598
|
chalk.dim(` β’ `) +
|
|
610
599
|
(hideUnconfiguredModels ? activeHotkey('E', ' Configured Models Only') : hotkey('E', ' Configured Models Only')) +
|
|
611
600
|
chalk.dim(` β’ `) +
|
|
612
|
-
hotkey('X', ' Token Logs') +
|
|
613
|
-
chalk.dim(` β’ `) +
|
|
614
601
|
hotkey('P', ' Settings') +
|
|
615
602
|
chalk.dim(` β’ `) +
|
|
616
603
|
hotkey('K', ' Help')
|
|
617
604
|
)
|
|
618
|
-
// π Line 2: install flow, recommend,
|
|
605
|
+
// π Line 2: install flow, recommend, feedback, and extended hints.
|
|
619
606
|
lines.push(
|
|
620
607
|
chalk.dim(` `) +
|
|
621
608
|
hotkey('Y', ' Install endpoints') + chalk.dim(` β’ `) +
|
|
@@ -654,6 +641,11 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
654
641
|
lines.push(chalk.bgRed.white.bold(paddedBanner))
|
|
655
642
|
}
|
|
656
643
|
|
|
644
|
+
// π Stable release notice: keep the bridge rebuild status explicit in the main UI
|
|
645
|
+
// π so users do not go hunting for hidden controls that are disabled on purpose.
|
|
646
|
+
const bridgeNotice = chalk.magentaBright.italic(` ${PROXY_DISABLED_NOTICE}`)
|
|
647
|
+
lines.push(bridgeNotice)
|
|
648
|
+
|
|
657
649
|
// π Append \x1b[K (erase to EOL) to each line so leftover chars from previous
|
|
658
650
|
// π frames are cleared. Then pad with blank cleared lines to fill the terminal,
|
|
659
651
|
// π preventing stale content from lingering at the bottom after resize.
|
package/src/testfcm.js
CHANGED
|
@@ -18,13 +18,14 @@
|
|
|
18
18
|
* β `hasConfiguredKey` β decide whether a config entry really contains an API key
|
|
19
19
|
* β `createTestfcmRunId` β build a stable timestamp-based run id for artifacts
|
|
20
20
|
* β `extractJsonPayload` β recover JSON mode output even when logs prefix stdout
|
|
21
|
+
* β `pickTestfcmSelectionIndex` β pick the most promising preflight row before sending Enter
|
|
21
22
|
* β `detectTranscriptFindings` β map raw tool output to actionable failure findings
|
|
22
23
|
* β `classifyToolTranscript` β classify a run as passed, failed, or inconclusive
|
|
23
24
|
* β `buildFixTasks` β convert findings into concrete follow-up work items
|
|
24
25
|
* β `buildTestfcmReport` β render the final Markdown report written under `task/`
|
|
25
26
|
*
|
|
26
27
|
* @exports TESTFCM_TOOL_SPECS, normalizeTestfcmToolName, resolveTestfcmToolSpec
|
|
27
|
-
* @exports hasConfiguredKey, createTestfcmRunId, extractJsonPayload
|
|
28
|
+
* @exports hasConfiguredKey, createTestfcmRunId, extractJsonPayload, pickTestfcmSelectionIndex
|
|
28
29
|
* @exports detectTranscriptFindings, classifyToolTranscript, buildFixTasks
|
|
29
30
|
* @exports buildTestfcmReport
|
|
30
31
|
*/
|
|
@@ -35,39 +36,15 @@ export const TESTFCM_TOOL_SPECS = {
|
|
|
35
36
|
label: 'Crush',
|
|
36
37
|
command: 'crush',
|
|
37
38
|
flag: '--crush',
|
|
38
|
-
prefersProxy:
|
|
39
|
+
prefersProxy: false,
|
|
39
40
|
configPaths: ['.config/crush/crush.json'],
|
|
40
41
|
},
|
|
41
|
-
codex: {
|
|
42
|
-
mode: 'codex',
|
|
43
|
-
label: 'Codex CLI',
|
|
44
|
-
command: 'codex',
|
|
45
|
-
flag: '--codex',
|
|
46
|
-
prefersProxy: true,
|
|
47
|
-
configPaths: [],
|
|
48
|
-
},
|
|
49
|
-
'claude-code': {
|
|
50
|
-
mode: 'claude-code',
|
|
51
|
-
label: 'Claude Code',
|
|
52
|
-
command: 'claude',
|
|
53
|
-
flag: '--claude-code',
|
|
54
|
-
prefersProxy: true,
|
|
55
|
-
configPaths: [],
|
|
56
|
-
},
|
|
57
|
-
gemini: {
|
|
58
|
-
mode: 'gemini',
|
|
59
|
-
label: 'Gemini CLI',
|
|
60
|
-
command: 'gemini',
|
|
61
|
-
flag: '--gemini',
|
|
62
|
-
prefersProxy: true,
|
|
63
|
-
configPaths: ['.gemini/settings.json'],
|
|
64
|
-
},
|
|
65
42
|
goose: {
|
|
66
43
|
mode: 'goose',
|
|
67
44
|
label: 'Goose',
|
|
68
45
|
command: 'goose',
|
|
69
46
|
flag: '--goose',
|
|
70
|
-
prefersProxy:
|
|
47
|
+
prefersProxy: false,
|
|
71
48
|
configPaths: ['.config/goose/config.yaml'],
|
|
72
49
|
},
|
|
73
50
|
aider: {
|
|
@@ -121,13 +98,17 @@ export const TESTFCM_TOOL_SPECS = {
|
|
|
121
98
|
}
|
|
122
99
|
|
|
123
100
|
const TESTFCM_TOOL_ALIASES = {
|
|
124
|
-
claude: 'claude-code',
|
|
125
|
-
claudecode: 'claude-code',
|
|
126
|
-
codexcli: 'codex',
|
|
127
101
|
opencodecli: 'opencode',
|
|
128
102
|
}
|
|
129
103
|
|
|
130
104
|
const TRANSCRIPT_FINDING_RULES = [
|
|
105
|
+
{
|
|
106
|
+
id: 'terminal_too_small',
|
|
107
|
+
title: 'PTY width warning blocked the TUI flow',
|
|
108
|
+
severity: 'high',
|
|
109
|
+
regex: /please maximize your terminal|terminal is too small|reduce font size or maximize width/i,
|
|
110
|
+
task: 'Run `/testfcm` with the width warning disabled in the isolated config or force a wider PTY before sending Enter.',
|
|
111
|
+
},
|
|
131
112
|
{
|
|
132
113
|
id: 'tool_missing',
|
|
133
114
|
title: 'Tool binary missing',
|
|
@@ -140,21 +121,14 @@ const TRANSCRIPT_FINDING_RULES = [
|
|
|
140
121
|
title: 'Invalid or missing API auth',
|
|
141
122
|
severity: 'high',
|
|
142
123
|
regex: /invalid api|bad api key|incorrect api key|authentication failed|unauthorized|forbidden|missing api key|no api key|anthropic_auth_token|401\b|403\b/i,
|
|
143
|
-
task: 'Validate the provider key used by the selected model, then re-run `/testfcm` and inspect the
|
|
124
|
+
task: 'Validate the provider key used by the selected model, then re-run `/testfcm` and inspect the generated tool config and transcript.',
|
|
144
125
|
},
|
|
145
126
|
{
|
|
146
127
|
id: 'rate_limited',
|
|
147
128
|
title: 'Provider rate limited',
|
|
148
129
|
severity: 'medium',
|
|
149
130
|
regex: /rate limit|too many requests|quota exceeded|429\b/i,
|
|
150
|
-
task: 'Retry with another configured provider or inspect
|
|
151
|
-
},
|
|
152
|
-
{
|
|
153
|
-
id: 'proxy_failure',
|
|
154
|
-
title: 'Proxy startup or routing failure',
|
|
155
|
-
severity: 'high',
|
|
156
|
-
regex: /failed to start proxy|proxy mode .* required|selected model may not exist|routing reload is taking longer than expected|proxy launch is blocked/i,
|
|
157
|
-
task: 'Inspect `request-log.jsonl`, proxy startup messages, and the isolated config to verify the tool can reach the local FCM proxy.',
|
|
131
|
+
task: 'Retry with another configured provider or inspect cooldown handling in the direct launcher flow.',
|
|
158
132
|
},
|
|
159
133
|
{
|
|
160
134
|
id: 'tool_launch_failed',
|
|
@@ -211,7 +185,9 @@ export function hasConfiguredKey(value) {
|
|
|
211
185
|
}
|
|
212
186
|
|
|
213
187
|
/**
|
|
214
|
-
* π Build an artifact-friendly run id such as `20260316-184512`.
|
|
188
|
+
* π Build an artifact-friendly run id such as `20260316-184512-123`.
|
|
189
|
+
* π Milliseconds keep concurrent `/testfcm` runs from clobbering each other's
|
|
190
|
+
* π reports and isolated HOME directories when they start in the same second.
|
|
215
191
|
*
|
|
216
192
|
* @param {Date} [date]
|
|
217
193
|
* @returns {string}
|
|
@@ -219,10 +195,11 @@ export function hasConfiguredKey(value) {
|
|
|
219
195
|
export function createTestfcmRunId(date = new Date()) {
|
|
220
196
|
const iso = date.toISOString()
|
|
221
197
|
return iso
|
|
222
|
-
.replace(
|
|
198
|
+
.replace(/Z$/, '')
|
|
223
199
|
.replace(/:/g, '')
|
|
224
200
|
.replace(/-/g, '')
|
|
225
201
|
.replace('T', '-')
|
|
202
|
+
.replace(/\.(\d{3})$/, '-$1')
|
|
226
203
|
}
|
|
227
204
|
|
|
228
205
|
/**
|
|
@@ -246,6 +223,76 @@ export function extractJsonPayload(text) {
|
|
|
246
223
|
return null
|
|
247
224
|
}
|
|
248
225
|
|
|
226
|
+
/**
|
|
227
|
+
* π Pick the best row to highlight before the runner presses Enter.
|
|
228
|
+
* π The TUI and `--json` share the same sorted result order, so picking the
|
|
229
|
+
* π first clearly healthy row from the preflight is a cheap way to avoid
|
|
230
|
+
* π wasting E2E runs on obviously dead or auth-failing models.
|
|
231
|
+
*
|
|
232
|
+
* @param {Array<{ label?: string, status?: string, httpCode?: string }>} results
|
|
233
|
+
* @param {{ preferProxy?: boolean }} [options]
|
|
234
|
+
* @returns {number}
|
|
235
|
+
*/
|
|
236
|
+
export function pickTestfcmSelectionIndex(results, options = {}) {
|
|
237
|
+
if (!Array.isArray(results) || results.length === 0) return 0
|
|
238
|
+
|
|
239
|
+
if (options.preferProxy === true) {
|
|
240
|
+
const groups = new Map()
|
|
241
|
+
|
|
242
|
+
for (let index = 0; index < results.length; index++) {
|
|
243
|
+
const row = results[index]
|
|
244
|
+
const label = typeof row?.label === 'string' ? row.label.trim() : ''
|
|
245
|
+
if (!label) continue
|
|
246
|
+
|
|
247
|
+
if (!groups.has(label)) {
|
|
248
|
+
groups.set(label, {
|
|
249
|
+
rows: [],
|
|
250
|
+
hasUp: false,
|
|
251
|
+
hasAuthFailure: false,
|
|
252
|
+
hasRateLimit: false,
|
|
253
|
+
hasNotFound: false,
|
|
254
|
+
})
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const group = groups.get(label)
|
|
258
|
+
const httpCode = String(row?.httpCode || '')
|
|
259
|
+
group.rows.push({ index, row })
|
|
260
|
+
if (row?.status === 'up') group.hasUp = true
|
|
261
|
+
if (row?.status === 'auth_error' || httpCode === '401' || httpCode === '403') group.hasAuthFailure = true
|
|
262
|
+
if (httpCode === '429') group.hasRateLimit = true
|
|
263
|
+
if (httpCode === '404') group.hasNotFound = true
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
for (const row of results) {
|
|
267
|
+
const label = typeof row?.label === 'string' ? row.label.trim() : ''
|
|
268
|
+
const group = groups.get(label)
|
|
269
|
+
if (!group?.hasUp) continue
|
|
270
|
+
if (group.hasAuthFailure || group.hasRateLimit || group.hasNotFound) continue
|
|
271
|
+
const target = group.rows.find((entry) => entry.row?.status === 'up')
|
|
272
|
+
if (target) return target.index
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
for (const row of results) {
|
|
276
|
+
const label = typeof row?.label === 'string' ? row.label.trim() : ''
|
|
277
|
+
const group = groups.get(label)
|
|
278
|
+
if (!group?.hasUp || group.hasAuthFailure) continue
|
|
279
|
+
const target = group.rows.find((entry) => entry.row?.status === 'up')
|
|
280
|
+
if (target) return target.index
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const exactUpIndex = results.findIndex((row) => row?.status === 'up' && String(row?.httpCode || '') === '200')
|
|
285
|
+
if (exactUpIndex >= 0) return exactUpIndex
|
|
286
|
+
|
|
287
|
+
const upIndex = results.findIndex((row) => row?.status === 'up')
|
|
288
|
+
if (upIndex >= 0) return upIndex
|
|
289
|
+
|
|
290
|
+
const okCodeIndex = results.findIndex((row) => String(row?.httpCode || '') === '200')
|
|
291
|
+
if (okCodeIndex >= 0) return okCodeIndex
|
|
292
|
+
|
|
293
|
+
return 0
|
|
294
|
+
}
|
|
295
|
+
|
|
249
296
|
/**
|
|
250
297
|
* π Detect known failure patterns in the raw tool transcript.
|
|
251
298
|
*
|
|
@@ -409,14 +456,14 @@ export function buildTestfcmReport(input) {
|
|
|
409
456
|
}
|
|
410
457
|
lines.push('')
|
|
411
458
|
|
|
412
|
-
lines.push('##
|
|
459
|
+
lines.push('## Runtime Diagnostics')
|
|
413
460
|
lines.push('')
|
|
414
461
|
if (requestLogSummary.length > 0) {
|
|
415
462
|
for (const entry of requestLogSummary) {
|
|
416
463
|
lines.push(`- ${entry}`)
|
|
417
464
|
}
|
|
418
465
|
} else {
|
|
419
|
-
lines.push('- No
|
|
466
|
+
lines.push('- No extra runtime diagnostic summary was captured for this run.')
|
|
420
467
|
}
|
|
421
468
|
lines.push('')
|
|
422
469
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file token-usage-reader.js
|
|
3
|
-
* @description Reads historical token usage
|
|
3
|
+
* @description Reads historical token usage snapshots and aggregates them by exact provider + model pair.
|
|
4
4
|
*
|
|
5
5
|
* @details
|
|
6
6
|
* The TUI already shows live latency and quota state, but that does not tell
|
|
@@ -9,27 +9,22 @@
|
|
|
9
9
|
* `provider::model -> totalTokens` map for table display.
|
|
10
10
|
*
|
|
11
11
|
* Why this exists:
|
|
12
|
-
* - `token-stats.json`
|
|
13
|
-
*
|
|
14
|
-
* - `request-log.jsonl` is the source of truth because every proxied request
|
|
15
|
-
* records prompt and completion token counts with provider context.
|
|
12
|
+
* - `token-stats.json` is the only remaining source of truth for historical
|
|
13
|
+
* totals now that the older JSONL accounting pipeline has been removed.
|
|
16
14
|
* - Startup-only parsing keeps runtime overhead negligible during TUI redraws.
|
|
17
15
|
*
|
|
18
16
|
* @functions
|
|
19
17
|
* β `buildProviderModelTokenKey` β creates a stable aggregation key
|
|
20
|
-
* β `loadTokenUsageByProviderModel` β reads
|
|
18
|
+
* β `loadTokenUsageByProviderModel` β reads token snapshots and returns total tokens by provider+model
|
|
21
19
|
* β `formatTokenTotalCompact` β renders totals as raw ints or compact K / M strings with 2 decimals
|
|
22
20
|
*
|
|
23
21
|
* @exports buildProviderModelTokenKey, loadTokenUsageByProviderModel, formatTokenTotalCompact
|
|
24
|
-
*
|
|
25
|
-
* @see src/log-reader.js
|
|
26
22
|
* @see src/render-table.js
|
|
27
23
|
*/
|
|
28
24
|
|
|
29
25
|
import { readFileSync, existsSync } from 'node:fs'
|
|
30
26
|
import { join } from 'node:path'
|
|
31
27
|
import { homedir } from 'node:os'
|
|
32
|
-
import { loadRecentLogs } from './log-reader.js'
|
|
33
28
|
|
|
34
29
|
const DEFAULT_DATA_DIR = join(homedir(), '.free-coding-models')
|
|
35
30
|
const STATS_FILE = join(DEFAULT_DATA_DIR, 'token-stats.json')
|
|
@@ -40,23 +35,11 @@ export function buildProviderModelTokenKey(providerKey, modelId) {
|
|
|
40
35
|
return `${providerKey}::${modelId}`
|
|
41
36
|
}
|
|
42
37
|
|
|
43
|
-
// π loadTokenUsageByProviderModel
|
|
44
|
-
// π
|
|
45
|
-
export function loadTokenUsageByProviderModel({
|
|
46
|
-
// π If a custom logFile is provided (Test Mode), ONLY use that file.
|
|
47
|
-
if (logFile) {
|
|
48
|
-
const testTotals = {}
|
|
49
|
-
const rows = loadRecentLogs({ logFile, limit })
|
|
50
|
-
for (const row of rows) {
|
|
51
|
-
const key = buildProviderModelTokenKey(row.provider, row.model)
|
|
52
|
-
testTotals[key] = (testTotals[key] || 0) + (Number(row.tokens) || 0)
|
|
53
|
-
}
|
|
54
|
-
return testTotals
|
|
55
|
-
}
|
|
56
|
-
|
|
38
|
+
// π loadTokenUsageByProviderModel reads the aggregated stats file produced by
|
|
39
|
+
// π the quota/accounting pipeline. Missing or malformed files are treated as empty.
|
|
40
|
+
export function loadTokenUsageByProviderModel({ statsFile = STATS_FILE } = {}) {
|
|
57
41
|
const totals = {}
|
|
58
42
|
|
|
59
|
-
// π Phase 1: Try to load from the aggregated stats file (canonical source for totals)
|
|
60
43
|
try {
|
|
61
44
|
if (existsSync(statsFile)) {
|
|
62
45
|
const stats = JSON.parse(readFileSync(statsFile, 'utf8'))
|
|
@@ -78,24 +61,12 @@ export function loadTokenUsageByProviderModel({ logFile, statsFile = STATS_FILE,
|
|
|
78
61
|
}
|
|
79
62
|
}
|
|
80
63
|
}
|
|
81
|
-
} catch
|
|
82
|
-
// π Silently fall back to log parsing if stats file is corrupt or unreadable
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// π Phase 2: Supplement with recent log entries if totals are still empty
|
|
86
|
-
// π (e.g. fresh install or token-stats.json deleted)
|
|
87
|
-
if (Object.keys(totals).length === 0) {
|
|
88
|
-
const rows = loadRecentLogs({ limit })
|
|
89
|
-
for (const row of rows) {
|
|
90
|
-
const key = buildProviderModelTokenKey(row.provider, row.model)
|
|
91
|
-
totals[key] = (totals[key] || 0) + (Number(row.tokens) || 0)
|
|
92
|
-
}
|
|
93
|
-
}
|
|
64
|
+
} catch {}
|
|
94
65
|
|
|
95
66
|
return totals
|
|
96
67
|
}
|
|
97
68
|
|
|
98
|
-
// π formatTokenTotalCompact keeps token counts readable in
|
|
69
|
+
// π formatTokenTotalCompact keeps token counts readable in the table:
|
|
99
70
|
// π 0-999 => raw integer, 1k-999k => N.NNk, 1m+ => N.NNM.
|
|
100
71
|
export function formatTokenTotalCompact(totalTokens) {
|
|
101
72
|
const safeTotal = Number(totalTokens) || 0
|