sprag-cli 3.55.0 → 3.57.0
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/README.ko.md +5 -1
- package/README.md +5 -1
- package/bin/cli.js +60 -19
- package/bin/postinstall.js +22 -0
- package/docs/CODEX.ko.md +44 -5
- package/docs/CODEX.md +74 -12
- package/package.json +2 -2
- package/src/agents.js +16 -2
- package/src/brief.js +17 -8
- package/src/claude-price.js +49 -0
- package/src/codex-brief.js +55 -2
- package/src/codex-cache-policy.js +26 -41
- package/src/codex-delegation.js +56 -8
- package/src/codex-doctor.js +1 -1
- package/src/codex-harness.js +63 -18
- package/src/codex-hooks.js +129 -20
- package/src/codex-ledger.js +150 -16
- package/src/codex-panel.js +51 -26
- package/src/codex-parser.js +41 -4
- package/src/codex-route-scan.js +14 -7
- package/src/codex-rule-health.js +41 -0
- package/src/commands/brief.js +14 -3
- package/src/commands/codex-delegate.js +17 -5
- package/src/commands/codex.js +20 -10
- package/src/commands/harness.js +6 -0
- package/src/commands/route-scan.js +4 -3
- package/src/compact-window.js +12 -4
- package/src/config.js +3 -3
- package/src/cost.js +62 -5
- package/src/ctx-window-cache.js +61 -0
- package/src/delegation-guard.js +15 -9
- package/src/doc2md.cjs +22 -8
- package/src/fig2md.cjs +7 -1
- package/src/formatters/statusline.js +22 -11
- package/src/formatters/table.js +19 -10
- package/src/gateway-auth.js +31 -3
- package/src/gateway-prices.js +39 -0
- package/src/harness.js +43 -12
- package/src/history.js +6 -2
- package/src/hook-manager.js +54 -32
- package/src/installer.js +286 -153
- package/src/korean-lint.cjs +40 -7
- package/src/litellm-budget.js +10 -1
- package/src/litellm-models.js +72 -10
- package/src/model-rules.js +90 -42
- package/src/money.js +9 -0
- package/src/month-spend.js +23 -12
- package/src/openai-prices.js +33 -0
- package/src/parser.js +232 -56
- package/src/postinstall.js +32 -0
- package/src/route-caps.js +74 -0
- package/src/route-inject.js +31 -7
- package/src/route-scan.js +93 -33
- package/src/savings-ledger.js +43 -11
- package/src/seed-rules.js +3 -4
- package/src/session-cache.js +35 -1
- package/src/state-file.js +43 -0
- package/src/stats.js +20 -5
- package/src/stdin-payload.js +18 -4
package/README.ko.md
CHANGED
|
@@ -185,7 +185,7 @@ JetBrains IDE 내장 터미널에서는 이모지 대신 한 칸 글리프로
|
|
|
185
185
|
|
|
186
186
|
| 명령 | 하는 일 |
|
|
187
187
|
|---|---|
|
|
188
|
-
| `sprag` | 최근
|
|
188
|
+
| `sprag` | 최근 30일 진단 리포트 |
|
|
189
189
|
| `sprag handoff` | 한도에 막히기 전에 작업 상태를 백업 |
|
|
190
190
|
| `sprag route-scan` | 내 기록에서 위임 후보 찾기 (LLM 호출 0회) |
|
|
191
191
|
| `sprag route-scan savings` | 라우팅 절감 원장 조회 |
|
|
@@ -209,6 +209,10 @@ JetBrains IDE 내장 터미널에서는 이모지 대신 한 칸 글리프로
|
|
|
209
209
|
| [한국어 문체 지침](https://github.com/rootstudioyaml/sprag/blob/main/docs/KOREAN-STYLE.ko.md) | 주입 내용, 쓰기 시점 검사, 적용 전후 비교 |
|
|
210
210
|
| [라우터가 아닌 이유](https://github.com/rootstudioyaml/sprag/blob/main/docs/NOT-A-ROUTER.ko.md) | 실시간 라우팅이 캐시를 깨서 비용을 키우는 구조 |
|
|
211
211
|
| [게이트웨이와 환경](https://github.com/rootstudioyaml/sprag/blob/main/docs/GATEWAYS.ko.md) | Bedrock·Vertex·LiteLLM, 가격표, FAQ, 동작 원리 |
|
|
212
|
+
| [클로드 코드 토큰 줄이는 법](https://sprag.io/docs/ko/reduce-claude-code-tokens/) | 압축·초기화 타이밍, 서브에이전트 탐색, 문서 변환, 캐시 TTL |
|
|
213
|
+
| [클로드 코드 사용 한도](https://sprag.io/docs/ko/claude-code-usage-limits/) | 5시간·7일 윈도우, 함께 지켜보는 법, 턴당 비용 줄이기 |
|
|
214
|
+
| [코덱스 토큰 사용량](https://sprag.io/docs/ko/codex-token-usage/) | 세션 범위 나누기, 네이티브 서브에이전트 위임, 코덱스 문서 변환 |
|
|
215
|
+
| [도구 비교](https://sprag.io/docs/ko/compare/) | ccusage·claude-usage·CCometixLine 같은 스테이터스라인 도구와 Sprag의 차이 |
|
|
212
216
|
|
|
213
217
|
LiteLLM 게이트웨이 환경에서는 `sprag profile-map --refresh` 가 게이트웨이의 `GET /model/info` 에서 모델 별칭을 바로 읽어 오므로, route-scan 이 위임 투표가 쌓이길 기다리거나 `profile-map.json` 을 손으로 편집할 필요가 없어졌습니다. 이 갱신은 LiteLLM 예산 게이지와 같은 5분 점검·24시간 캐시 주기를 씁니다. 인증 토큰은 저장하지 않으며, 호출 시점마다 `apiKeyHelper` 에서 읽어 그 헬퍼의 수명 그대로 만료되게 둡니다. 자세한 내용은 [route-scan](https://github.com/rootstudioyaml/sprag/blob/main/docs/ROUTE_SCAN.md) 문서에 있습니다.
|
|
214
218
|
|
package/README.md
CHANGED
|
@@ -229,7 +229,7 @@ work types, and once each has a rule there is nothing left to promote.
|
|
|
229
229
|
|
|
230
230
|
| Command | What it does |
|
|
231
231
|
|---|---|
|
|
232
|
-
| `sprag` | Last-
|
|
232
|
+
| `sprag` | Last-30-day diagnostic report |
|
|
233
233
|
| `sprag handoff` | Back work up to markdown before a cap blocks you |
|
|
234
234
|
| `sprag route-scan` | Find delegation candidates in your own history (0 LLM calls) |
|
|
235
235
|
| `sprag route-scan savings` | The routing-savings ledger |
|
|
@@ -254,6 +254,10 @@ Every subcommand, flag and the output-language setting: [command reference](http
|
|
|
254
254
|
| [Korean style](https://github.com/rootstudioyaml/sprag/blob/main/docs/KOREAN-STYLE.md) | What gets injected, the write-time check, before/after |
|
|
255
255
|
| [Not a router](https://github.com/rootstudioyaml/sprag/blob/main/docs/NOT-A-ROUTER.md) | Why realtime routing breaks the cache and costs more |
|
|
256
256
|
| [Gateways & environment](https://github.com/rootstudioyaml/sprag/blob/main/docs/GATEWAYS.md) | Bedrock, Vertex, LiteLLM budgets, pricing table, FAQ, how it works |
|
|
257
|
+
| [Reduce Claude Code tokens](https://sprag.io/docs/reduce-claude-code-tokens/) | Compact/clear timing, subagent exploration, document conversion, cache TTL |
|
|
258
|
+
| [Claude Code usage limits](https://sprag.io/docs/claude-code-usage-limits/) | The 5-hour and 7-day windows, watching both, cutting per-turn cost |
|
|
259
|
+
| [Codex token usage](https://sprag.io/docs/codex-token-usage/) | Session scoping, native subagent delegation, document conversion for Codex |
|
|
260
|
+
| [Compare](https://sprag.io/docs/compare/) | Sprag vs ccusage, claude-usage, and statusline tools like CCometixLine |
|
|
257
261
|
|
|
258
262
|
Behind a LiteLLM gateway, `sprag profile-map --refresh` now reads model aliases straight from the gateway's own `GET /model/info`, so route-scan no longer needs to wait on learned votes or a hand-edited `profile-map.json`. The refresh uses the same 5-minute check and 24-hour cache as the LiteLLM budget gauge. It never stores your auth token: each refresh reads it from `apiKeyHelper` at call time and lets it expire on the helper's own schedule. Details: [route-scan](https://github.com/rootstudioyaml/sprag/blob/main/docs/ROUTE_SCAN.md).
|
|
259
263
|
|
package/bin/cli.js
CHANGED
|
@@ -36,12 +36,13 @@ import {
|
|
|
36
36
|
extractContextUsage,
|
|
37
37
|
extractEffort,
|
|
38
38
|
extractModel,
|
|
39
|
+
extractSessionId,
|
|
39
40
|
extractTranscriptPath,
|
|
40
41
|
} from '../src/stdin-payload.js';
|
|
41
42
|
|
|
42
43
|
import { effortChipLevel } from '../src/ultracode.js';
|
|
43
44
|
|
|
44
|
-
import { parseAllSessions, getLastUserMessageTime } from '../src/parser.js';
|
|
45
|
+
import { parseAllSessions, sessionsSince, getLastUserMessageTime } from '../src/parser.js';
|
|
45
46
|
import {
|
|
46
47
|
dailyTrend,
|
|
47
48
|
ttlBreakdown,
|
|
@@ -52,7 +53,7 @@ import {
|
|
|
52
53
|
sessionMetrics,
|
|
53
54
|
diagnoseSession,
|
|
54
55
|
} from '../src/stats.js';
|
|
55
|
-
import {
|
|
56
|
+
import { sessionCostAcross } from '../src/claude-price.js';
|
|
56
57
|
import { chipForIssues } from '../src/advice.js';
|
|
57
58
|
import { debug } from '../src/debug.js';
|
|
58
59
|
import { createArgs } from '../src/cli-args.js';
|
|
@@ -92,6 +93,15 @@ function readUpdateChip() {
|
|
|
92
93
|
}
|
|
93
94
|
}
|
|
94
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Whether the table report may use ANSI codes. The formatter looked at NO_COLOR
|
|
98
|
+
* alone, so `--no-color` did nothing there and a piped report (`sprag | less`,
|
|
99
|
+
* a redirect to a file) carried escape codes into its output.
|
|
100
|
+
*/
|
|
101
|
+
function tableColor() {
|
|
102
|
+
return !hasFlag('--no-color') && !process.env.NO_COLOR && !!process.stdout.isTTY;
|
|
103
|
+
}
|
|
104
|
+
|
|
95
105
|
// Subcommands this build knows how to run. Used only by the guard below.
|
|
96
106
|
const KNOWN_SUBCOMMANDS = new Set([
|
|
97
107
|
'last', 'brief', 'history', 'handoff', 'install', 'uninstall', 'mode', 'korean', 'cohesion',
|
|
@@ -131,6 +141,16 @@ Usage:
|
|
|
131
141
|
sprag last most recent warning + how to handle it
|
|
132
142
|
sprag history recent warning transitions
|
|
133
143
|
sprag handoff write a session handoff file
|
|
144
|
+
sprag mode [keywords] statusline preferences (icon/text, en/ko, window, ttl)
|
|
145
|
+
sprag seed register the bundled starter rules, one answer at a time
|
|
146
|
+
sprag korean on|off|status Korean writing guidance + write-time check
|
|
147
|
+
sprag cohesion on|off|status English sentence-connection guidance
|
|
148
|
+
sprag doc2md on|off|<file> convert documents to Markdown before the model reads them
|
|
149
|
+
sprag delegate on|off|status add bounds and context to Task/Agent delegation prompts
|
|
150
|
+
sprag compact-window audit or pin autoCompactWindow for 1M sessions
|
|
151
|
+
sprag litellm-budget LiteLLM gateway key budget and spend
|
|
152
|
+
sprag update-check is a newer version out?
|
|
153
|
+
sprag brief --hook internal: the UserPromptSubmit hook entry point
|
|
134
154
|
sprag feedback "<msg>" file a bug report / feature request (no browser needed)
|
|
135
155
|
sprag upgrade install the latest release
|
|
136
156
|
sprag --install-hook install cache-monitor PostToolUse hook
|
|
@@ -434,7 +454,7 @@ async function main() {
|
|
|
434
454
|
const { buildTableDemoData } = await import('../src/demo.js');
|
|
435
455
|
const { formatReport } = await import('../src/formatters/table.js');
|
|
436
456
|
const data = buildTableDemoData({ version: PKG_VERSION });
|
|
437
|
-
console.log(formatReport(data));
|
|
457
|
+
console.log(formatReport(data, { color: tableColor() }));
|
|
438
458
|
return;
|
|
439
459
|
}
|
|
440
460
|
|
|
@@ -529,7 +549,10 @@ async function main() {
|
|
|
529
549
|
const excludeSessionPath =
|
|
530
550
|
getArg('--exclude-session') || process.env.CACHE_MONITOR_EXCLUDE_SESSION || undefined;
|
|
531
551
|
|
|
532
|
-
|
|
552
|
+
// The statusline also reports this month's spend. Naming the month start here
|
|
553
|
+
// lets the one pass over the transcripts trim boundary sessions to it too.
|
|
554
|
+
const monthSince = isStatusline ? (await import('../src/month-spend.js')).monthStartMs() : undefined;
|
|
555
|
+
const sessions = await parseAllSessions({ days, projectFilter, sinceMs: monthSince });
|
|
533
556
|
|
|
534
557
|
if (sessions.length === 0) {
|
|
535
558
|
// Statusline must always emit a single line (no multi-line help spam every
|
|
@@ -552,6 +575,15 @@ async function main() {
|
|
|
552
575
|
// Resolved the same way as on the populated line below, so the chip does
|
|
553
576
|
// not change its mind about the level when the analysis window empties.
|
|
554
577
|
const effort = effortChipLevel(extractEffort(stdinJson), extractTranscriptPath(stdinJson));
|
|
578
|
+
// Recorded here too: a fresh session can render before it has any request
|
|
579
|
+
// in the analysis window, and its first prompt is briefed from this.
|
|
580
|
+
const ctxLive = extractContextUsage(stdinJson);
|
|
581
|
+
if (ctxLive?.size) {
|
|
582
|
+
try {
|
|
583
|
+
const { recordLiveWindow } = await import('../src/ctx-window-cache.js');
|
|
584
|
+
recordLiveWindow(extractSessionId(stdinJson), ctxLive.size);
|
|
585
|
+
} catch (e) { debug('ctx-window-cache:record', e); }
|
|
586
|
+
}
|
|
555
587
|
if (caps || model) {
|
|
556
588
|
try {
|
|
557
589
|
const { persistSnapshot } = await import('../src/caps-cache.js');
|
|
@@ -592,7 +624,7 @@ async function main() {
|
|
|
592
624
|
const ttl = ttlBreakdown(sessions);
|
|
593
625
|
const sum = summary(sessions);
|
|
594
626
|
const anomalies = detectAnomalies(trend);
|
|
595
|
-
const cost =
|
|
627
|
+
const cost = sessionCostAcross(sessions);
|
|
596
628
|
const spikeReport = detectSpikes(sessions, { recentHours: 24, multiplier: 3 });
|
|
597
629
|
const contextWindow = detectContextWindow(sessions, { recentHours: 24 });
|
|
598
630
|
|
|
@@ -618,6 +650,16 @@ async function main() {
|
|
|
618
650
|
? effortChipLevel(extractEffort(stdinJson), extractTranscriptPath(stdinJson))
|
|
619
651
|
: extractEffort(stdinJson);
|
|
620
652
|
const ctxLive = extractContextUsage(stdinJson);
|
|
653
|
+
// The prompt-submit briefing has no payload field for the window, so it reads
|
|
654
|
+
// back what this render was told (see src/ctx-window-cache.js).
|
|
655
|
+
if (isStatusline && ctxLive?.size) {
|
|
656
|
+
try {
|
|
657
|
+
const { recordLiveWindow } = await import('../src/ctx-window-cache.js');
|
|
658
|
+
recordLiveWindow(extractSessionId(stdinJson), ctxLive.size);
|
|
659
|
+
} catch (e) {
|
|
660
|
+
debug('ctx-window-cache:record', e);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
621
663
|
if (isStatusline && (caps || model)) {
|
|
622
664
|
try {
|
|
623
665
|
const { persistSnapshot } = await import('../src/caps-cache.js');
|
|
@@ -730,28 +772,23 @@ async function main() {
|
|
|
730
772
|
if (isStatusline) {
|
|
731
773
|
try {
|
|
732
774
|
const { monthSpend, monthStartMs } = await import('../src/month-spend.js');
|
|
733
|
-
const
|
|
775
|
+
const since = monthStartMs();
|
|
776
|
+
const daysNeeded = (Date.now() - since) / 86400000;
|
|
734
777
|
const monthSessions = days >= daysNeeded
|
|
735
778
|
? sessions
|
|
736
|
-
: await parseAllSessions({ days: Math.max(1, Math.ceil(daysNeeded)), projectFilter });
|
|
737
|
-
|
|
779
|
+
: await parseAllSessions({ days: Math.max(1, Math.ceil(daysNeeded)), projectFilter, sinceMs: since });
|
|
780
|
+
// 월 경계에 걸친 세션은 월초 이후 요청만 남긴다. 그러지 않으면 1일 자정을
|
|
781
|
+
// 넘긴 세션의 전월분이 이번 달 지출에 그대로 들어온다.
|
|
782
|
+
monthSpendInfo = monthSpend(await sessionsSince(monthSessions, since));
|
|
738
783
|
} catch (e) {
|
|
739
784
|
debug('month-spend', e);
|
|
740
785
|
}
|
|
741
786
|
}
|
|
742
787
|
|
|
743
|
-
// What delegation has measurably saved, read straight from the registry
|
|
744
|
-
// route-scan maintains. A lookup, never a scan: the statusline re-renders
|
|
745
|
-
// every few seconds and a scan parses tens of MB of transcripts.
|
|
746
|
-
let delegationSaved = 0;
|
|
747
|
-
try {
|
|
748
|
-
const { delegationSavedUsd } = await import('../src/model-rules.js');
|
|
749
|
-
delegationSaved = delegationSavedUsd();
|
|
750
|
-
} catch (e) {
|
|
751
|
-
debug('model-rules:saved', e); // an unreadable registry just hides the chip
|
|
752
|
-
}
|
|
753
788
|
// Rolling week/month/lifetime totals from the delegation ledger — the
|
|
754
789
|
// statusline's headline line. Falls back to null (chip hidden) on any error.
|
|
790
|
+
// A lookup, never a scan: the statusline re-renders every few seconds and a
|
|
791
|
+
// scan parses tens of MB of transcripts.
|
|
755
792
|
let delegationTotals = null;
|
|
756
793
|
try {
|
|
757
794
|
const { delegationSavedTotals } = await import('../src/savings-ledger.js');
|
|
@@ -759,6 +796,10 @@ async function main() {
|
|
|
759
796
|
} catch (e) {
|
|
760
797
|
debug('savings-ledger:totals', e);
|
|
761
798
|
}
|
|
799
|
+
// The single-line layout shows the same lifetime figure as the headline
|
|
800
|
+
// line. It used to sum the rule registry instead, a scan-window snapshot
|
|
801
|
+
// priced against another baseline, so one label carried two amounts.
|
|
802
|
+
const delegationSaved = Number(delegationTotals?.total) || 0;
|
|
762
803
|
// Document conversions, same shape as the delegation totals: a lifetime sum
|
|
763
804
|
// plus a document count. A lookup of a small JSON file, never a scan.
|
|
764
805
|
let doc2mdTotals = null;
|
|
@@ -857,7 +898,7 @@ async function main() {
|
|
|
857
898
|
});
|
|
858
899
|
} else {
|
|
859
900
|
const { formatReport } = await import('../src/formatters/table.js');
|
|
860
|
-
output = formatReport(data);
|
|
901
|
+
output = formatReport(data, { color: tableColor() });
|
|
861
902
|
}
|
|
862
903
|
|
|
863
904
|
console.log(output);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* npm `postinstall` entry point: runs `sprag install` for a global install and
|
|
5
|
+
* says why it did not for anything else. See src/postinstall.js.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { spawnSync } from 'node:child_process';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import { dirname, join } from 'node:path';
|
|
11
|
+
import { postinstallDecision } from '../src/postinstall.js';
|
|
12
|
+
|
|
13
|
+
const decision = postinstallDecision(process.env);
|
|
14
|
+
if (!decision.run) {
|
|
15
|
+
console.log(`sprag: setup skipped (${decision.reason}). Run \`sprag install\` to register the hooks and the statusline.`);
|
|
16
|
+
process.exit(0);
|
|
17
|
+
}
|
|
18
|
+
const cli = join(dirname(fileURLToPath(import.meta.url)), 'cli.js');
|
|
19
|
+
spawnSync(process.execPath, [cli, 'install'], { stdio: 'inherit' });
|
|
20
|
+
// A failed setup must not fail the package install: the CLI is still usable
|
|
21
|
+
// and `sprag install` can be run again by hand.
|
|
22
|
+
process.exit(0);
|
package/docs/CODEX.ko.md
CHANGED
|
@@ -51,7 +51,7 @@ macOS zsh 환경에서는 설치가 tmux 설치 여부를 먼저 확인합니다
|
|
|
51
51
|
| 한국어 검사 | 파일 쓰기 후 문체 위반을 검사하며 block·warn·off를 지원합니다 |
|
|
52
52
|
| 상태 표시 | 별도 패널, tmux 패널, 일회성 상태줄을 제공합니다 |
|
|
53
53
|
| 브리핑·이력 | 최신 컨텍스트와 사용량 경고를 알리고 세션별로 저장합니다 |
|
|
54
|
-
| 위임 원장 | Sprag가 위임한 하위
|
|
54
|
+
| 위임 원장 | Sprag가 위임한 하위 실행의 토큰에 호출 경로의 단가를 적용해 예상 차액을 계산합니다. 직접 호출은 OpenAI 정가를, LiteLLM 공급자는 그 게이트웨이의 단가를 씁니다 |
|
|
55
55
|
|
|
56
56
|
## 패널과 보고서
|
|
57
57
|
|
|
@@ -108,8 +108,38 @@ sprag seed reset --agent codex
|
|
|
108
108
|
등록할 때 `--model ID`를 요구합니다. 모델 사용 가능 여부는 실제 공급자 설정에 달려 있습니다.
|
|
109
109
|
명시한 모델과 사용자 정의 역할은 유지하며, 사용할 수 없는 하위 에이전트 도구를 만들어 내지 않습니다.
|
|
110
110
|
|
|
111
|
+
Codex 0.159.2에서는 `spawn_agent`가 `PreToolUse`를 거치지 않습니다(기록용 훅을 붙여 직접
|
|
112
|
+
확인한 결과입니다). 그래서 실제로 동작하는 경로는 프롬프트 시점입니다. 규칙에 맞는 프롬프트가
|
|
113
|
+
오면 `[Sprag model routing]` 안내가 붙고, `harness init`이 넣는 AGENTS.md 블록이 이 안내를
|
|
114
|
+
"사용자가 위임을 요청했다"는 뜻으로 읽게 합니다. 자식은 `fork_turns "none"`으로 뜨고, 부모는
|
|
115
|
+
같은 일을 직접 하지 않고 `wait_agent`(제한 시간 120초 이상)로 자식이 끝나기를 기다립니다. 귀속은
|
|
116
|
+
자식이 route id를 되받아 적는 것에 기대지 않습니다. 대신 `SubagentStart`가 대기 중인 기록(같은
|
|
117
|
+
부모 세션, 같은 목표 모델, 30분 이내)에 새로 뜬 자식을 매칭해 묶습니다. 어떤 자식도 가져가지
|
|
118
|
+
않은 안내는 부모 세션에 다음 프롬프트가 들어올 때 닫히므로, 나중에 다른 이유로 뜬 자식이 그
|
|
119
|
+
안내의 절감으로 잡히지 않습니다. 같은 프롬프트 시점에 아직 장부에 반영되지 않은 위임 실행도
|
|
120
|
+
별도 프로세스로 계산하므로, `Routing saved`는 다음 route-scan을 기다리지 않고 위임 뒤 첫
|
|
121
|
+
프롬프트에서 갱신됩니다.
|
|
122
|
+
|
|
123
|
+
이 안내는 부모 세션의 컨텍스트가 `codex.delegateMinContext`(기본 60000 토큰) 아래면 붙지
|
|
124
|
+
않습니다. 2026-09-30 실측에서 작은 세션은 스폰·대기·검증이라는 조율 비용이 자식이 아끼는
|
|
125
|
+
비용보다 커서, 문턱 아래에서는 안내를 붙이는 쪽이 오히려 손해였습니다. 아직 전용 CLI 명령은
|
|
126
|
+
없고, `sprag mode` 실행 시 "Stored config file"로 나오는 설정 파일(맥은
|
|
127
|
+
`~/Library/Application Support/claude-token-saver/config.json`)에서 `codex.delegateMinContext`
|
|
128
|
+
값을 직접 고치면 됩니다. `PreToolUse` 재작성은 spawn을 훅으로 보내는 Codex 버전을 위해 남아
|
|
129
|
+
있을 뿐, 0.159.2에서는 호출되지 않습니다. 위임 원장은 자식이 쓴 토큰을 부모 모델 단가로 환산한
|
|
130
|
+
추정치이며, 부모가 조율에 쓴 턴은 빼지 않습니다. 끝나기 전에 중단된 자식은 부모의 일을 대신하지
|
|
131
|
+
못했으므로, 그 비용 전액을 손실로 기록하고 항목에 `aborted`를 표시합니다.
|
|
132
|
+
|
|
111
133
|
SessionStart는 캐시된 후보를 읽고 필요할 때 별도 프로세스에서 갱신합니다. 명시적인 `--refresh`는
|
|
112
|
-
대기 주기를 건너뜁니다.
|
|
134
|
+
대기 주기를 건너뜁니다.
|
|
135
|
+
|
|
136
|
+
Codex 0.159.2는 훅 하나가 넘긴 컨텍스트를 약 2,450토큰까지만 보존하고, 그보다 길면 가운데를
|
|
137
|
+
잘라 냅니다. 그래서 SessionStart는 추정치 2,000토큰 안에 들어가는 항목만 잘리지 않은 상태로
|
|
138
|
+
넣습니다. 우선순위는 ratchet 룰, 한국어 지침, 응집성 지침, 문서 변환 안내, 제안 순입니다. 들어가지
|
|
139
|
+
못한 룰과 지침은 Sprag 데이터 디렉터리의 프로젝트별 파일에 쓰고, 컨텍스트 첫머리에서 그 파일을
|
|
140
|
+
먼저 읽으라고 안내합니다. rollout의 `originator`로 `codex exec` 실행을 알아보며, 답할 사람이
|
|
141
|
+
없으므로 seed 제안과 route 안내를 넣지 않습니다.
|
|
142
|
+
`seed`는 Claude의 Haiku·Sonnet 프리셋을 가져오지 않으며,
|
|
113
143
|
Codex의 수락·거절 기록도 Claude 기록과 분리합니다.
|
|
114
144
|
|
|
115
145
|
## 래칫 규칙
|
|
@@ -158,9 +188,18 @@ sprag capabilities --agent codex
|
|
|
158
188
|
하위 실행의 토큰을 고정하고 부모·자식 단가를 각각 적용한 차이이므로, 실제 청구서의 절감액은
|
|
159
189
|
아닙니다. 손해는 음수로 남기며 단가나 사용량이 없으면 0 대신 미상으로 표시합니다.
|
|
160
190
|
|
|
161
|
-
단가는
|
|
162
|
-
|
|
163
|
-
|
|
191
|
+
단가는 호출이 실제로 거친 경로로 정하며, 두 출처는 서로를 대신하지 않습니다.
|
|
192
|
+
|
|
193
|
+
- ChatGPT·OpenAI 로그인이나 `api.openai.com`으로 바로 가는 API 키는 내장된 OpenAI 정가를
|
|
194
|
+
씁니다. 정가는 Standard 등급의 짧은 컨텍스트 단가이고 2026-10-01에 확인했습니다. 긴 컨텍스트
|
|
195
|
+
할증은 반영하지 않으므로, 그 기준을 넘는 요청은 실제보다 낮게 계산됩니다.
|
|
196
|
+
- LiteLLM 공급자는 그 게이트웨이의 `/model/info` 단가를 씁니다. 같은 별칭 아래 배포 단가가
|
|
197
|
+
다르면 계산하지 않고, 단가 캐시는 7일 동안 사용합니다. 게이트웨이가 단가를 주지 않는 모델은
|
|
198
|
+
정가로 채우지 않고 미상으로 둡니다.
|
|
199
|
+
|
|
200
|
+
공급자가 다른 동명 모델은 섞지 않으며, 이미 계산한 실행은 단가 사본을 보존합니다.
|
|
201
|
+
`sprag delegate rules --agent codex`는 룰마다 실측 위임 건수와 실패율, 절감액을 보여 줍니다.
|
|
202
|
+
실패율의 95% 신뢰 하한이 20%를 넘으면(실측 5건 이상) 조건을 좁히거나 룰을 지우라고 알립니다.
|
|
164
203
|
위임 후 `--refresh`로 원장을 갱신할 수 있고, 아직 일치하지 않은 기록은 7일 동안 찾습니다.
|
|
165
204
|
|
|
166
205
|
캐시 타이머는 확인된 공급자 정책과 기록된 캐시 사용 시각을 보여 줍니다. 정확한 만료 시각이나
|
package/docs/CODEX.md
CHANGED
|
@@ -76,14 +76,14 @@ shared hooks needed by other features.
|
|
|
76
76
|
| Token report | Table, JSON, or CSV from local Codex rollout logs, with time and project filters |
|
|
77
77
|
| Statusline | `--statusline` or `--format statusline` renders the compact panel once, with wrapping and label/color preferences |
|
|
78
78
|
| Briefing | UserPromptSubmit reports fresh context/limit threshold crossings once per session and threshold cycle |
|
|
79
|
-
| Warning history | `last` and `history` read Codex-only, per-session
|
|
79
|
+
| Warning history | `last` and `history` read Codex-only, per-session records of context (80%/95%) and rate-limit (90%) warnings and handoffs; cache and TTL chips are not recorded |
|
|
80
80
|
| Handoff | Git state, session/model, recorded context/tokens/limits, and fillable remaining-work sections |
|
|
81
81
|
| Subagents | Opt-in native spawn model routing, scoped category rules, and SubagentStart guidance |
|
|
82
82
|
| Cache timer | Resolved provider retention policy after recorded cache usage; exact expiry is unknown |
|
|
83
83
|
| Route discovery | Repeated simple turns grouped by project, provider, and parent model; no automatic registration |
|
|
84
84
|
| Ratchet detection | Recent repeated command/patch failures produce `ratchet?` candidates |
|
|
85
85
|
| Seed presets | Compatible ratchet rules, separate decisions, explicit registration scope |
|
|
86
|
-
| Routing saved | Sprag-attributed child tokens priced
|
|
86
|
+
| Routing saved | Sprag-attributed child tokens priced by the path the call took (OpenAI list prices for direct calls, the gateway's `/model/info` prices for a LiteLLM provider); signed, so a pricier child is a loss; an estimate, not billed savings |
|
|
87
87
|
| Diagnostics | `doctor` checks user config, hook registration, harness, logs, panel activity, and preferences |
|
|
88
88
|
| Capability matrix | `capabilities` lists supported, partial, native, and unsupported areas without invoking a model |
|
|
89
89
|
|
|
@@ -151,12 +151,44 @@ warning records. A handoff file is never overwritten and identifies quota
|
|
|
151
151
|
values as recorded snapshots, not live responses. Fill in its remaining-work
|
|
152
152
|
sections before using it to continue in another session.
|
|
153
153
|
|
|
154
|
-
Delegation is independently opt-in via `delegate on`.
|
|
155
|
-
`spawn_agent`
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
154
|
+
Delegation is independently opt-in via `delegate on`. As of Codex 0.159.2,
|
|
155
|
+
`spawn_agent` never reaches `PreToolUse` (measured directly: a recording hook
|
|
156
|
+
saw only `Bash` calls while a spawn happened), so the rewrite below cannot see
|
|
157
|
+
or steer it. The live path instead runs at prompt time: `UserPromptSubmit`
|
|
158
|
+
checks the prompt against your category rules and, on a match, appends a
|
|
159
|
+
`[Sprag model routing]` note naming the target model, effort, and a route id.
|
|
160
|
+
That note by itself changes nothing, because Codex's own instructions tell it not to
|
|
161
|
+
spawn subagents unless something explicitly asked for it. So `harness init`'s
|
|
162
|
+
AGENTS.md block adds the missing half: a `[Sprag model routing]` note *is* the
|
|
163
|
+
user's delegation request, and the agent should spawn exactly what it describes
|
|
164
|
+
with `fork_turns "none"`, then wait for it (`wait_agent` with a timeout of at least
|
|
165
|
+
120 seconds) instead of doing the same task itself. Attribution does not depend on the child echoing the
|
|
166
|
+
route id back: `SubagentStart` binds the id to whichever spawned child matches
|
|
167
|
+
the pending route (same parent thread, same target model, within 30 minutes),
|
|
168
|
+
so a route is credited even when the child's own first message never repeats it.
|
|
169
|
+
A hint that no child claimed is closed when the parent's next prompt arrives, so
|
|
170
|
+
a child spawned later for some other reason is not credited to it. That same
|
|
171
|
+
prompt also prices any delegated run the ledger has not joined yet, in a
|
|
172
|
+
detached process, so `Routing saved` follows a delegation by one prompt instead
|
|
173
|
+
of waiting for the next route scan.
|
|
174
|
+
|
|
175
|
+
The hint is withheld below `codex.delegateMinContext` (default 60000 input
|
|
176
|
+
tokens, read from the parent's own rollout). Measured 2026-09-30: a small
|
|
177
|
+
parent session spends more on the coordinating turn (spawning, waiting,
|
|
178
|
+
verifying) than a delegated child saves, so under the floor Sprag leaves the
|
|
179
|
+
request on the main agent instead of proposing a loss. There is no CLI flag for
|
|
180
|
+
it yet; set it by editing `codex.delegateMinContext` (an integer) in the config
|
|
181
|
+
file `sprag mode` prints under "Stored config file" (`~/Library/Application
|
|
182
|
+
Support/claude-token-saver/config.json` on macOS, `$XDG_CONFIG_HOME/claude-token-saver/config.json`
|
|
183
|
+
or `~/.config/claude-token-saver/config.json` on Linux).
|
|
184
|
+
|
|
185
|
+
The `PreToolUse` rewrite (native `spawn_agent` arguments using Codex's
|
|
186
|
+
`message`, `model`, and `reasoning_effort` fields, preserving explicit model
|
|
187
|
+
choices and custom roles) is kept for Codex versions that do route spawns
|
|
188
|
+
through hooks; on 0.159.2 it simply never fires. `SubagentStart` injects scoped
|
|
189
|
+
ratchets and enabled writing guidance regardless of which path routed the
|
|
190
|
+
child. Bounds are instructions, not an enforced tool-call budget. Normal Codex
|
|
191
|
+
sandbox and approval rules still apply. Review new hooks in `/hooks`.
|
|
160
192
|
|
|
161
193
|
```sh
|
|
162
194
|
sprag delegate on --agent codex
|
|
@@ -174,6 +206,13 @@ exact parent model, and prefer project rules over global rules. Matching prompts
|
|
|
174
206
|
request native delegation; unavailable subagent tools leave work on the main
|
|
175
207
|
agent. Claude tier presets and history are not imported.
|
|
176
208
|
|
|
209
|
+
The routing ledger (`Routing Saved` below) prices a child's own tokens at the
|
|
210
|
+
parent model's rate as the counterfactual; it does not charge or subtract the
|
|
211
|
+
parent's coordination turn, so a delegation that barely clears the context
|
|
212
|
+
floor can still show a positive but overstated saving. A child that was
|
|
213
|
+
interrupted before it finished replaced no parent work, so its whole cost is
|
|
214
|
+
booked as a loss and the entry is marked `aborted`.
|
|
215
|
+
|
|
177
216
|
## Harness Commands
|
|
178
217
|
|
|
179
218
|
```sh
|
|
@@ -231,6 +270,15 @@ SessionStart reads cached candidates and schedules an eligible refresh outside
|
|
|
231
270
|
the hook. Small log changes refresh at most daily; a 5 MiB increase can refresh
|
|
232
271
|
after an hour. `--refresh` bypasses that schedule.
|
|
233
272
|
|
|
273
|
+
Codex 0.159.2 keeps about 2,450 tokens of one hook's context and cuts the
|
|
274
|
+
middle of anything longer. SessionStart therefore fits its parts whole within a
|
|
275
|
+
2,000-token estimate, in priority order: ratchet rules, Korean guidance,
|
|
276
|
+
cohesion guidance, the document note, then offers. Rules and guidance that do
|
|
277
|
+
not fit are written to a per-project file under the Sprag data directory, and
|
|
278
|
+
the context opens with an instruction to read it. `codex exec` runs, recognized
|
|
279
|
+
from the rollout's `originator`, get no seed offer or route notice, since no one
|
|
280
|
+
can answer them.
|
|
281
|
+
|
|
234
282
|
`seed` offers only compatible ratchet presets, not Claude model-tier presets.
|
|
235
283
|
Accept and skip decisions are separate from Claude. Both seed acceptance and
|
|
236
284
|
route approval require the user's choice of global or project scope. A project
|
|
@@ -306,10 +354,24 @@ subagent earns no credit. The estimate holds the child's measured tokens
|
|
|
306
354
|
constant and prices them at the parent and child rates. Negative differences
|
|
307
355
|
remain negative; this is not a comparison of actual billed totals.
|
|
308
356
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
357
|
+
Prices follow the path each call took, and the two sources never fall back to
|
|
358
|
+
each other:
|
|
359
|
+
|
|
360
|
+
- ChatGPT/OpenAI login or an API key straight to `api.openai.com` uses the
|
|
361
|
+
built-in OpenAI list prices (Standard tier, short-context rates, checked
|
|
362
|
+
2026-10-01). Long-context surcharges are not modeled, so requests past the
|
|
363
|
+
long-context threshold are under-priced.
|
|
364
|
+
- A LiteLLM provider uses that gateway's `/model/info` prices, with agreement
|
|
365
|
+
required across deployments behind an alias. Price snapshots last seven days.
|
|
366
|
+
A model the gateway does not price stays unpriced; it is not filled in from
|
|
367
|
+
the list prices.
|
|
368
|
+
|
|
369
|
+
`sprag delegate rules --agent codex` shows each rule's measured runs, failure
|
|
370
|
+
rate and signed saving. A rule is flagged for review when the 95% lower bound
|
|
371
|
+
of its failure rate exceeds 20% over at least five runs, as on the Claude side.
|
|
372
|
+
|
|
373
|
+
Providers are kept separate. Negative values are losses: the child ran on a
|
|
374
|
+
pricier model than the parent, and the loss reduces the totals. Missing rates or usage produce unpriced entries, never invented zeroes. A priced
|
|
313
375
|
entry keeps its rate snapshot across later outages. Pending route records are
|
|
314
376
|
matched for seven days; saved ledger entries remain afterward. Run `--refresh`
|
|
315
377
|
after delegation to update the ledger without waiting for the next route scan.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sprag-cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.57.0",
|
|
4
4
|
"description": "Cut the tokens Claude Code and Codex spend: routes simple work to cheaper models, converts documents to compact Markdown, and shows spend and savings live. Ratchet rules and Korean writing checks included.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"scripts": {
|
|
11
11
|
"deploy": "node scripts/deploy.mjs",
|
|
12
12
|
"docs:statusline": "node scripts/docs-statusline.mjs",
|
|
13
|
-
"postinstall": "node bin/
|
|
13
|
+
"postinstall": "node bin/postinstall.js || true",
|
|
14
14
|
"publish:legacy": "node scripts/publish-legacy.mjs",
|
|
15
15
|
"release:notes": "node scripts/release-notes.mjs",
|
|
16
16
|
"site:docs": "node scripts/build-site-docs.mjs",
|
package/src/agents.js
CHANGED
|
@@ -20,12 +20,26 @@ const AGENT_MODEL = {
|
|
|
20
20
|
'haiku-runner': 'haiku',
|
|
21
21
|
'haiku-translate': 'haiku',
|
|
22
22
|
'sonnet-worker': 'sonnet',
|
|
23
|
+
// A T1 rule names its target by tier alone (`agent: 'sonnet'`), with no
|
|
24
|
+
// preset agent behind it. Without these the phrase fell through to the
|
|
25
|
+
// haiku default, and a sonnet rule told the session to delegate to haiku.
|
|
26
|
+
sonnet: 'sonnet',
|
|
27
|
+
haiku: 'haiku',
|
|
23
28
|
};
|
|
24
29
|
|
|
25
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* True when `<name>.md` exists in the project or user agents directory.
|
|
32
|
+
*
|
|
33
|
+
* `root: null` asks about the user directory alone. Text that every project
|
|
34
|
+
* reads (the global ratchet-model.md) has to be worded from what every project
|
|
35
|
+
* can see: checked against the current directory, the same rule read
|
|
36
|
+
* `haiku-explore(model: haiku)` when the scan ran in a project that defines
|
|
37
|
+
* the agent and `model: haiku` when it ran anywhere else, and each flip
|
|
38
|
+
* rewrote a file sitting in the prompt-cache prefix of every session.
|
|
39
|
+
*/
|
|
26
40
|
export function agentExists(name, root = process.cwd()) {
|
|
27
41
|
if (!name) return false;
|
|
28
|
-
return existsSync(join(root, '.claude', 'agents', `${name}.md`)) ||
|
|
42
|
+
return (root !== null && existsSync(join(root, '.claude', 'agents', `${name}.md`))) ||
|
|
29
43
|
existsSync(join(claudeUserDir(), 'agents', `${name}.md`));
|
|
30
44
|
}
|
|
31
45
|
|
package/src/brief.js
CHANGED
|
@@ -29,6 +29,7 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync, openSync,
|
|
|
29
29
|
import { join } from 'node:path';
|
|
30
30
|
import { userDataDir } from './paths.js';
|
|
31
31
|
import { resolveModelId, isOneMillionModel, effectiveWindow } from './compact-window.js';
|
|
32
|
+
import { liveWindowFor } from './ctx-window-cache.js';
|
|
32
33
|
|
|
33
34
|
// Context tiers as a fraction of the session's context window. Tier 1 warns
|
|
34
35
|
// (compaction/cost territory ahead), tier 2 urges wrapping up. A session only
|
|
@@ -83,14 +84,21 @@ function saveState(state, now) {
|
|
|
83
84
|
*
|
|
84
85
|
* `observedMax` stays as a floor: it proves a 1M window even when the model id
|
|
85
86
|
* is unreadable (env override, settings we do not resolve).
|
|
87
|
+
*
|
|
88
|
+
* `liveWindow` outranks both. It is the size Claude Code reported to the
|
|
89
|
+
* statusline for this session, so the briefing and the statusline count against
|
|
90
|
+
* the same window. The model id cannot stand in for it: a model that is 1M
|
|
91
|
+
* without a `[1m]` suffix reads as 200k there.
|
|
86
92
|
*/
|
|
87
|
-
export function ctxWindowFor(observedMax = 0, root = process.cwd()) {
|
|
93
|
+
export function ctxWindowFor(observedMax = 0, root = process.cwd(), { liveWindow = null } = {}) {
|
|
88
94
|
let modelWindow = observedMax > WINDOW_1M_MIN_INPUT ? 1_000_000 : 200_000;
|
|
95
|
+
const reported = Number.isFinite(liveWindow) && liveWindow > 0;
|
|
96
|
+
if (reported) modelWindow = liveWindow;
|
|
89
97
|
let window = modelWindow;
|
|
90
98
|
let compactCapped = false;
|
|
91
99
|
try {
|
|
92
100
|
const { model } = resolveModelId(root);
|
|
93
|
-
if (isOneMillionModel(model)) modelWindow = window = 1_000_000;
|
|
101
|
+
if (!reported && isOneMillionModel(model)) modelWindow = window = 1_000_000;
|
|
94
102
|
const cap = effectiveWindow(root).value;
|
|
95
103
|
if (cap !== null && cap < window) {
|
|
96
104
|
window = cap;
|
|
@@ -104,7 +112,7 @@ export function ctxWindowFor(observedMax = 0, root = process.cwd()) {
|
|
|
104
112
|
* Last request's input size for THIS session, from the transcript tail.
|
|
105
113
|
* Reads at most TAIL_BYTES — prompt-submit hooks must stay fast.
|
|
106
114
|
*/
|
|
107
|
-
export function sessionCtx(transcriptPath, { root = process.cwd() } = {}) {
|
|
115
|
+
export function sessionCtx(transcriptPath, { root = process.cwd(), liveWindow = null } = {}) {
|
|
108
116
|
let size;
|
|
109
117
|
try { size = statSync(transcriptPath).size; } catch { return null; }
|
|
110
118
|
const start = Math.max(0, size - TAIL_BYTES);
|
|
@@ -130,7 +138,7 @@ export function sessionCtx(transcriptPath, { root = process.cwd() } = {}) {
|
|
|
130
138
|
if (total > 0) { input = total; maxInput = Math.max(maxInput, total); }
|
|
131
139
|
}
|
|
132
140
|
if (input == null) return null;
|
|
133
|
-
const { window, modelWindow, compactCapped } = ctxWindowFor(maxInput, root);
|
|
141
|
+
const { window, modelWindow, compactCapped } = ctxWindowFor(maxInput, root, { liveWindow });
|
|
134
142
|
return { input, window, modelWindow, compactCapped, pct: input / window };
|
|
135
143
|
}
|
|
136
144
|
|
|
@@ -166,14 +174,15 @@ export function seedSessionBriefed(sessionId, signatures, now = Date.now()) {
|
|
|
166
174
|
* (recorded via seedSessionBriefed) are skipped; every other fresh signature
|
|
167
175
|
* is emitted, including on the session's first event.
|
|
168
176
|
*/
|
|
169
|
-
export async function runBrief({ sessionId, transcriptPath, now = Date.now() }) {
|
|
177
|
+
export async function runBrief({ sessionId, transcriptPath, root = process.cwd(), now = Date.now() }) {
|
|
170
178
|
if (!sessionId) return null;
|
|
179
|
+
const liveWindow = liveWindowFor(sessionId);
|
|
171
180
|
const state = loadState();
|
|
172
181
|
const s = state.sessions[sessionId] || { ctxTier: 0, briefed: [] };
|
|
173
182
|
const items = [];
|
|
174
183
|
|
|
175
184
|
// ── context tier crossing (per-session) ──
|
|
176
|
-
const ctx = transcriptPath ? sessionCtx(transcriptPath) : null;
|
|
185
|
+
const ctx = transcriptPath ? sessionCtx(transcriptPath, { root, liveWindow }) : null;
|
|
177
186
|
if (ctx) {
|
|
178
187
|
const tier = ctxTierOf(ctx.pct);
|
|
179
188
|
// The tier is not monotonic: auto-compaction drops the live context back to
|
|
@@ -238,7 +247,7 @@ export async function runBrief({ sessionId, transcriptPath, now = Date.now() })
|
|
|
238
247
|
// never needed. 200k sessions are exempt (their window is already <= 200k).
|
|
239
248
|
try {
|
|
240
249
|
const cw = await import('./compact-window.js');
|
|
241
|
-
const st = cw.compactWindowStatus({ root
|
|
250
|
+
const st = cw.compactWindowStatus({ root, liveWindow });
|
|
242
251
|
if (!st.ok) {
|
|
243
252
|
const sig = `compact-window|${st.reason}`;
|
|
244
253
|
const briefed = new Set(s.briefed || []);
|
|
@@ -246,7 +255,7 @@ export async function runBrief({ sessionId, transcriptPath, now = Date.now() })
|
|
|
246
255
|
briefed.add(sig);
|
|
247
256
|
s.briefed = [...briefed];
|
|
248
257
|
const now = st.window ? `현재 ${fmtK(st.window)}` : '현재 미설정';
|
|
249
|
-
items.push(`1M 컨텍스트 모델(${st.model})인데 autoCompactWindow가 ${now}입니다 — 자동 압축이 80만 토큰 근처에서야 걸려 그전까지 모든 요청이 전체 컨텍스트를 재과금합니다. 1M은 너무 크니 ${fmtK(st.recommendedMin)}~${fmtK(st.recommendedMax)} 범위를 권장합니다(그 범위 안이면 경고하지 않습니다). 1M 창 자체는 그대로 두고 압축 시점만 앞당깁니다. 등록: sprag compact-window set --global|--project [--value ${fmtK(st.recommendedMax)}] (기본 ${fmtK(st.recommended)}, 적용 범위는 사용자에게 확인) / 끄기: compact-window off`);
|
|
258
|
+
items.push(`1M 컨텍스트 모델(${st.model || 'Claude Code 기본 모델'})인데 autoCompactWindow가 ${now}입니다 — 자동 압축이 80만 토큰 근처에서야 걸려 그전까지 모든 요청이 전체 컨텍스트를 재과금합니다. 1M은 너무 크니 ${fmtK(st.recommendedMin)}~${fmtK(st.recommendedMax)} 범위를 권장합니다(그 범위 안이면 경고하지 않습니다). 1M 창 자체는 그대로 두고 압축 시점만 앞당깁니다. 등록: sprag compact-window set --global|--project [--value ${fmtK(st.recommendedMax)}] (기본 ${fmtK(st.recommended)}, 적용 범위는 사용자에게 확인) / 끄기: compact-window off`);
|
|
250
259
|
}
|
|
251
260
|
}
|
|
252
261
|
} catch { /* settings unreadable — other briefings above still apply */ }
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude-side cost by the path the call took. A session on a LiteLLM gateway
|
|
3
|
+
* (gatewayBase(env) set) is billed by the gateway, so it is priced from its
|
|
4
|
+
* /model/info table and stays unpriced (null) when the table has no entry;
|
|
5
|
+
* the Anthropic list table is never a fallback there. Every other session is a
|
|
6
|
+
* direct call and uses the list table. Kept apart from cost.js because
|
|
7
|
+
* litellm-models.js imports cost.js, and this needs both.
|
|
8
|
+
*/
|
|
9
|
+
import { estimateCost, estimateCostAcross } from './cost.js';
|
|
10
|
+
import { gatewayPriceFor } from './litellm-models.js';
|
|
11
|
+
import { gatewayBase } from './gateway-auth.js';
|
|
12
|
+
|
|
13
|
+
export function sessionCost(totals, model, { env = process.env, now } = {}) {
|
|
14
|
+
if (!gatewayBase(env)) return estimateCost(totals, model);
|
|
15
|
+
const price = gatewayPriceFor(model, env, now);
|
|
16
|
+
return price ? estimateCost(totals, model, { price }) : null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** One priced part per model that spent tokens, or the whole at its one model. */
|
|
20
|
+
function partsByModel(x) {
|
|
21
|
+
if (!x.modelTotals) return [{ totals: x.totals, model: x.model }];
|
|
22
|
+
return Object.entries(x.modelTotals).map(([model, totals]) => ({ totals, model }));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* What a session's cost is made of: the session itself and each subagent run
|
|
27
|
+
* it spawned (parser.js attaches them as `subagentRuns`). A run has its own
|
|
28
|
+
* model, so it is priced on its own instead of at the session's rate, and a
|
|
29
|
+
* session or run that switched model is split again so each model's tokens
|
|
30
|
+
* meet that model's rate. `own` marks the parts that are the session itself.
|
|
31
|
+
*/
|
|
32
|
+
export function pricedParts(session) {
|
|
33
|
+
if (!session) return [];
|
|
34
|
+
return [
|
|
35
|
+
...partsByModel(session).map((part) => ({ ...part, own: true })),
|
|
36
|
+
...(session.subagentRuns || []).flatMap(partsByModel),
|
|
37
|
+
];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Like estimateCostAcross, plus how many priced parts (a session, a subagent run, or one model's share of either) had no price and were left out. */
|
|
41
|
+
export function sessionCostAcross(sessions, opts = {}) {
|
|
42
|
+
let unpriced = 0;
|
|
43
|
+
const total = estimateCostAcross((sessions || []).flatMap(pricedParts), (totals, model) => {
|
|
44
|
+
const c = sessionCost(totals, model, opts);
|
|
45
|
+
if (!c) unpriced++;
|
|
46
|
+
return c;
|
|
47
|
+
});
|
|
48
|
+
return { ...total, unpriced };
|
|
49
|
+
}
|