llm-switcher 1.4.1 → 1.6.2
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 +97 -0
- package/README.md +48 -0
- package/README.vi.md +45 -0
- package/docs/session-2026-10-07-compaction.md +224 -0
- package/formats.mjs +85 -25
- package/idlecache.mjs +159 -0
- package/idlecall.mjs +59 -0
- package/idlecompact.mjs +217 -0
- package/package.json +1 -1
- package/proxy.mjs +160 -7
- package/shim.mjs +29 -0
- package/state.mjs +48 -2
- package/switch.mjs +61 -0
- package/tests/certs.test.mjs +7 -1
- package/tests/codex-daemon.test.mjs +57 -3
- package/tests/compact-cli.test.mjs +34 -0
- package/tests/dashboard.e2e.test.mjs +7 -1
- package/tests/formats.test.mjs +44 -1
- package/tests/idlecompact.e2e.test.mjs +203 -0
- package/tests/idlecompact.test.mjs +282 -0
- package/ui.html +481 -18
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,102 @@
|
|
|
1
1
|
# Changelog — LLM Switcher
|
|
2
2
|
|
|
3
|
+
## Release 1.6.2 — 2026-10-08
|
|
4
|
+
|
|
5
|
+
- **Per-Model Slot Target Thinking Effort:** Support granular reasoning effort configuration per model slot (`profile.modelEfforts`). Individual slot selectors in the dashboard allow setting target effort levels (`auto`, `none`, `low`, `medium`, `high`, `xhigh`, `max`) independently for each model tier (e.g. Sonnet, Opus, Haiku, Codex roles) with automatic fallback to profile-level defaults.
|
|
6
|
+
- **Dedicated Idle Compaction Navigation & Collapsible View:** Separate Idle Compaction into its own sidebar menu (`#/compact`) with dedicated overview telemetry cards. Automatically collapse detailed configuration cards when inactive, and replace button-like unit badges with clean native typography and tooltips.
|
|
7
|
+
- **Accurate Bifrost Passthrough Detection:** Refine Bifrost passthrough indicators to activate only for genuine intact-routed native prefix models, keeping reasoning effort selectors fully accessible for normal OpenAI/Anthropic/Vertex/Gemini endpoints.
|
|
8
|
+
|
|
9
|
+
## Release 1.6.1 — 2026-10-08
|
|
10
|
+
|
|
11
|
+
- **Durable Gateway Prefix Cache:** Replace external Claude Code and Codex transcript/database
|
|
12
|
+
writers with a private, durable gateway prefix cache under `STATE_DIR/idle-compact/`. Exact source-prefix,
|
|
13
|
+
caller/session/profile/context and generation guards preserve every newly appended turn and reject
|
|
14
|
+
stale summaries. Native client files (`~/.claude/projects/` and `thread_history_1.sqlite`) remain untouched.
|
|
15
|
+
- **Reasoning and Tool Pairing Preservation:** Retain assistant decision and reasoning text
|
|
16
|
+
accompanying tool calls; preserve tool dependencies throughout the recent tail and forward
|
|
17
|
+
malformed or unsupported tool histories unchanged.
|
|
18
|
+
- **Effort Mapping Ladder & Dynamic UI:** Introduce an 8-level effort ladder (`none`, `minimal`,
|
|
19
|
+
`low`, `medium`, `high`, `xhigh`, `max`, `ultra`) with automatic nearest-level equivalence mapping
|
|
20
|
+
(`mapEquivalentEffort`) when targeting upstreams with fewer levels (strict OpenAI, Anthropic, Gemini).
|
|
21
|
+
Add `profile.thinkingEffort` override setting with Web UI dropdown sync and automatic Bifrost passthrough hint.
|
|
22
|
+
- **Pure CA Validation & Fail-safe TLS:** Introduce strict file validation for custom CA certificates
|
|
23
|
+
(`usableCACertificate`); automatically unsets loopback proxy when CA is missing, corrupt, or unreadable
|
|
24
|
+
so Codex connects directly via system root certificates instead of failing TLS during daemon startup.
|
|
25
|
+
- **Consistent Idle-compaction Settings:** Validate all idle-compaction settings and budgets consistently.
|
|
26
|
+
An empty/null summary model makes no provider call; a configured summary model routes independently
|
|
27
|
+
with bounded timeout and visible diagnostics. Update dashboard and CLI controls accordingly.
|
|
28
|
+
|
|
29
|
+
## Release 1.6.0 — 2026-10-07
|
|
30
|
+
|
|
31
|
+
- **A Codex thread now keeps the shortening, behind the same opt-in switch.** A Codex compaction is
|
|
32
|
+
three writes that have to agree, all read off a thread that had compacted seven times:
|
|
33
|
+
|
|
34
|
+
1. a `compacted` entry in the rollout file, carrying the summary and the history that replaces
|
|
35
|
+
the old one;
|
|
36
|
+
2. a row of type `contextCompaction` in `thread_items`, whose `item_json` is nothing but a type
|
|
37
|
+
and an id -- a marker, not a summary;
|
|
38
|
+
3. `thread_history_projection_state` moved forward to the byte offset and ordinal of that entry.
|
|
39
|
+
|
|
40
|
+
The third is the one that is easy to leave out, and leaving it out looks exactly like success.
|
|
41
|
+
That table records how far the store has consumed the rollout file: `next_rollout_byte_offset` is
|
|
42
|
+
the size of the file, exactly, and `next_rollout_ordinal` is the ordinal after the last entry
|
|
43
|
+
read. The rollout file is the record and the store is a projection of it, so an entry appended to
|
|
44
|
+
the file without moving the projection is simply never read. With all three written, Codex
|
|
45
|
+
resumes and answers from the summary; with the first two alone it resumes and says it has none.
|
|
46
|
+
|
|
47
|
+
`idleCompact.codex` remains off by default, because this opens Codex's database. With no summary
|
|
48
|
+
model named, no model is called: the request still goes out shortened, which is the whole of the
|
|
49
|
+
saving, and nothing is spent to earn it.
|
|
50
|
+
|
|
51
|
+
## Release 1.5.3 — 2026-10-07
|
|
52
|
+
|
|
53
|
+
- **Idle compaction no longer calls a model unless one was named for it.** With no summary model set,
|
|
54
|
+
a summary was written with the conversation's own model — the expensive one, on an official
|
|
55
|
+
account, and the one with limits. That spends the costly model to save the tokens of a single
|
|
56
|
+
turn, and on a plan with limits it is worse than doing nothing.
|
|
57
|
+
|
|
58
|
+
There was no point paying for it either: the summary is used only to write the session file, so
|
|
59
|
+
with no model named there was nothing to store and the call bought nothing. Now no model is called
|
|
60
|
+
at all. The saving is unchanged — the request still leaves shortened, which is the whole of what
|
|
61
|
+
this feature costs and all of what it saves without a model. Set one with
|
|
62
|
+
`switch compact model <id>` to also make the shortening last.
|
|
63
|
+
|
|
64
|
+
## Release 1.5.2 — 2026-10-07
|
|
65
|
+
|
|
66
|
+
- **Codex conversations can be shortened on an idle return too, behind its own switch.**
|
|
67
|
+
`idleCompact.codex`, off by default. The history is shortened in the request, which is
|
|
68
|
+
where a Responses client keeps it: a Responses client reaches a Responses provider as the
|
|
69
|
+
bytes it sent, so there is nothing to render through. Measured on a 25 KB conversation,
|
|
70
|
+
6 KB goes out.
|
|
71
|
+
|
|
72
|
+
The thread itself is left to Codex. Its history is a SQLite store of its own, and a
|
|
73
|
+
compaction there needs two halves: a `contextCompaction` row in `thread_items`, whose
|
|
74
|
+
`item_json` is nothing but a type and an id, and a `compacted` entry in the rollout file
|
|
75
|
+
carrying the summary. Both were written, in the shape a real compaction takes, and Codex
|
|
76
|
+
resumed as if neither existed. Something else in that store says where the summary begins;
|
|
77
|
+
it was not found. So the summary is produced and the request goes out short, which saves
|
|
78
|
+
the tokens of this turn, and no claim is made that the thread stays short.
|
|
79
|
+
|
|
80
|
+
## Release 1.5.1 — 2026-10-07
|
|
81
|
+
|
|
82
|
+
- **Idle compaction is now in the dashboard.** A card on the Routes page carries the switch, the pause that counts as losing the cache, the size it must exceed, and the model that writes the summary. It reads and writes `/api/idle-compact`, the same endpoint and the same key `switch compact` uses, so the page and the terminal cannot show two different settings.
|
|
83
|
+
|
|
84
|
+
One bug came out of writing the endpoint: the field types were checked as groups, so `{"enabled":"yes"}` was accepted and then read as `false` — a typo switched the feature off without a word. Each field is now checked as the type it actually is, and a wrong type is refused rather than quietly turned into the default.
|
|
85
|
+
|
|
86
|
+
## Release 1.5.0 — 2026-10-07
|
|
87
|
+
|
|
88
|
+
- **A conversation that comes back after a pause no longer pays full price for a cache nobody holds.** When a Claude Code session resumes after being idle long enough, and the context is large enough to be worth it, the gateway shortens the history and writes the compaction into the session file. Off by default. `switch compact on`, and `switch compact model <id>` for the model that writes the summary (empty means the conversation's own model).
|
|
89
|
+
|
|
90
|
+
Two decisions worth naming, because the obvious alternatives are worse.
|
|
91
|
+
|
|
92
|
+
Compacting once, not every turn. Changing the prefix on every turn means no turn ever hits the cache and the conversation is summarized over and over. Compacting when a pause says the cache is gone keeps the summary stable for the turns that follow.
|
|
93
|
+
|
|
94
|
+
The session file, not the API. Claude Code builds every request from the entry chain in `~/.claude/projects/<project>/<session>.jsonl`, so a compaction written there shortens every request after it rather than only the turn that triggered it. The two entries are the ones Claude Code writes itself, read off a real session: a `compact_boundary` and a `user` entry marked `isCompactSummary`. Old entries stay in the file, as they do after Claude Code's own compaction, because a transcript a person can still read is worth more than a small file.
|
|
95
|
+
|
|
96
|
+
Codex and the Antigravity CLI are deliberately untouched. Codex compaction is a protocol item the provider gateway answers and Codex stores in its own history, so shortening a Codex request here would replace that with a summary no client records. The CLI has no session file to write.
|
|
97
|
+
|
|
98
|
+
Tests: 14, including one that runs the real Claude Code binary against a local server and reads the request it sends. With twelve synthetic turns behind a boundary, that request carries the summary and none of the twelve.
|
|
99
|
+
|
|
3
100
|
## Release 1.4.1 — 2026-10-07
|
|
4
101
|
|
|
5
102
|
- **The update comes from npm, because that is where a release is published.** The gateway asked npm what the newest version is and ran `npm install -g llm-switcher@<version>` for exactly that version. Before, the install decided: a directory with a `.git` pulled and never asked npm, and the dashboard's Update now button read as a git operation on a project that ships to npm.
|
package/README.md
CHANGED
|
@@ -550,6 +550,53 @@ The service runs without your shell. `switch service install` therefore copies `
|
|
|
550
550
|
|
|
551
551
|
On Linux the unit is a systemd *user* service: it starts when you log in. To start it at boot without a login (a server reached over SSH), run `loginctl enable-linger $USER` once. The unit records the absolute path of the current `node`; if you manage Node with nvm and remove that version, run `switch service install` again.
|
|
552
552
|
|
|
553
|
+
### Idle compaction
|
|
554
|
+
|
|
555
|
+
A configurable pause can indicate that the provider's prompt cache is cold; it is a heuristic,
|
|
556
|
+
not a guarantee about any provider's cache lifetime. This feature is off by default.
|
|
557
|
+
|
|
558
|
+
For an identified Claude Code session, the gateway shortens an old prefix once after a sufficiently
|
|
559
|
+
long pause. It stores the exact original prefix hash and its replacement in a private cache under
|
|
560
|
+
`STATE_DIR/idle-compact`. Later requests reuse that prefix only while the original raw history still
|
|
561
|
+
matches, and preserve every recent and newly appended turn unchanged. The cache survives gateway
|
|
562
|
+
restarts. A native client compaction or edited prefix invalidates it safely. Client JSONL files and
|
|
563
|
+
Codex SQLite databases are never read or modified by this feature.
|
|
564
|
+
|
|
565
|
+
| Command | Behavior |
|
|
566
|
+
| --- | --- |
|
|
567
|
+
| `switch compact status` | Show settings |
|
|
568
|
+
| `switch compact on` / `off` | Enable or disable Claude request compaction |
|
|
569
|
+
| `switch compact codex on` / `off` | Opt Codex Responses requests into the same wire prefix cache; off by default |
|
|
570
|
+
| `switch compact model <id>` | Explicitly choose a summary model |
|
|
571
|
+
| `switch compact model none` / `default` | Disable summary calls; no fallback to the conversation model |
|
|
572
|
+
| `switch compact idle <minutes>` | Pause threshold, default 15; allowed 1–1440 |
|
|
573
|
+
| `switch compact min <KB>` | Minimum request size, default 64; allowed 8–4096 |
|
|
574
|
+
|
|
575
|
+
An unset summary model (empty string or `null`) makes no model call. The deterministic replacement
|
|
576
|
+
keeps earlier user and assistant text within the configured budgets, removes older tool payloads,
|
|
577
|
+
and retains the recent tail with complete call/result pairs. With an explicit summary model, one
|
|
578
|
+
background call summarizes only the replaced prefix and upgrades its cache generation. A late
|
|
579
|
+
summary cannot overwrite a newer generation or remove turns that arrived while it was running.
|
|
580
|
+
The summary model is mapped and its upstream format resolved independently of the main model.
|
|
581
|
+
|
|
582
|
+
Caching requires an explicit session identifier: Claude session headers/metadata, or Codex session,
|
|
583
|
+
thread or prompt-cache identity. Anonymous requests stay unchanged; shared opening prompts are not
|
|
584
|
+
used as session identity. Cache scope includes the client, caller credential digest, profile,
|
|
585
|
+
model, system/tools and compaction policy. Unsupported or malformed tool histories are forwarded
|
|
586
|
+
unchanged. Cache failure or corruption also forwards the original request.
|
|
587
|
+
|
|
588
|
+
The dashboard and `/api/idle-compact` use the same settings as the CLI. Supported fields are
|
|
589
|
+
`enabled`, `codex`, `model`, `idleMinutes`, `minBytes`, `keepRecent`, `userChars`, `summaryMaxChars`,
|
|
590
|
+
and `sessionLookbackHours`. The API rejects unknown fields and invalid budgets with HTTP 400.
|
|
591
|
+
Additional bounds/defaults: `keepRecent` 1–100 (6), `userChars` 128–24000 (3000),
|
|
592
|
+
`summaryMaxChars` 256–128000 (24000), and `sessionLookbackHours` 1–168 (72).
|
|
593
|
+
The private cache keeps at most 256 records and expires idle clocks and prefix mappings after the
|
|
594
|
+
configured lifetime. A later idle window may compact a larger prefix; ordinary appended turns
|
|
595
|
+
reuse a stable prefix. Native Codex compaction remains Codex's own responsibility.
|
|
596
|
+
|
|
597
|
+
When using intact, configure the profile with an authorized intact API key. Client User-Agent
|
|
598
|
+
matching chooses the native protocol route; it does not replace upstream authentication.
|
|
599
|
+
|
|
553
600
|
### Updates
|
|
554
601
|
|
|
555
602
|
When npm has a newer release, the dashboard shows a notice. Click **Update now** in this notice. The gateway installs the release and starts the new code. Then the dashboard reloads. `switch update` does the same from a terminal.
|
|
@@ -768,6 +815,7 @@ Because of this split, a provider fingerprint is never a rule in this gateway. I
|
|
|
768
815
|
| `LLM_SWITCHER_BIFROST_RETRY_MS` | Time before the gateway asks intact again after a failed Bifrost lookup (default 30000 ms). |
|
|
769
816
|
| `x-llm-profile: <key>` header (alias `x-profile`) or `?profile=<key>` | Route a single request through a specific profile. An unknown key returns HTTP 400 instead of silently falling back. |
|
|
770
817
|
| `profile.thinkingMode` | `auto` (default, for gateways like intact or 9Router): restore stripped thinking, inject a `<think>` guide for non-reasoning models, send `thinking` + `reasoning_effort`. `native` (strict OpenAI APIs): send only `reasoning_effort` when the client asks, never touch the prompt, use `max_completion_tokens`. `off`: never send reasoning parameters. |
|
|
818
|
+
| `profile.thinkingEffort` | `auto` (default, caller/client controls effort), or an explicit override level: `none`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`, `ultra`. Overrides client effort before routing upstream. Automatically maps to the nearest equivalent level accepted by the target upstream (e.g. OpenAI ceiling `high`, Anthropic `max`, Gemini budget bounds). In the Web UI, this selector dynamically hides with a guidance hint when Bifrost passthrough is active. |
|
|
771
819
|
| `profile.endpoints.countTokens` | Override the Anthropic `count_tokens` URL. |
|
|
772
820
|
| `profile.endpoints` | Override upstream URLs per format: `{ "openai-chat": "...", "anthropic": "...", "vertex": "https://.../models/{model}:{action}" }`. |
|
|
773
821
|
| `CLAUDE_CONFIG_DIR` | Respected when locating Claude Code's `settings.json`. |
|
package/README.vi.md
CHANGED
|
@@ -548,6 +548,50 @@ Service không chạy trong shell của bạn. Vì vậy `switch service install
|
|
|
548
548
|
|
|
549
549
|
Trên Linux, unit là service systemd *của user*: nó khởi động khi bạn đăng nhập. Muốn nó chạy ngay khi máy boot mà không cần đăng nhập (ví dụ server truy cập qua SSH), chạy một lần `loginctl enable-linger $USER`. Unit ghi đường dẫn tuyệt đối của `node` hiện tại; nếu bạn quản lý Node bằng nvm và gỡ phiên bản đó, hãy chạy lại `switch service install`.
|
|
550
550
|
|
|
551
|
+
### Nén hội thoại khi để lâu
|
|
552
|
+
|
|
553
|
+
Khoảng nghỉ đủ dài có thể là dấu hiệu cache của provider đã nguội. Đây là ngưỡng do bạn chọn,
|
|
554
|
+
không phải bảo đảm cache đã hết hạn. Tính năng mặc định tắt.
|
|
555
|
+
|
|
556
|
+
Với phiên Claude Code có định danh rõ ràng, gateway rút gọn một prefix cũ sau khoảng nghỉ và lưu
|
|
557
|
+
ánh xạ giữa hash của prefix gốc với bản thay thế trong cache riêng `STATE_DIR/idle-compact`.
|
|
558
|
+
Các request sau chỉ dùng lại khi prefix gốc khớp hoàn toàn, giữ nguyên mọi turn gần đây và turn
|
|
559
|
+
mới thêm. Cache còn hiệu lực sau khi gateway khởi động lại. Nếu client tự compact hoặc sửa lịch
|
|
560
|
+
sử cũ, ánh xạ hết hiệu lực. Tính năng không đọc hay sửa JSONL của client hoặc SQLite của Codex.
|
|
561
|
+
|
|
562
|
+
| Lệnh | Hành vi |
|
|
563
|
+
| --- | --- |
|
|
564
|
+
| `switch compact status` | Xem cấu hình |
|
|
565
|
+
| `switch compact on` / `off` | Bật hoặc tắt nén request Claude |
|
|
566
|
+
| `switch compact codex on` / `off` | Bật riêng cache prefix cho request Responses của Codex; mặc định tắt |
|
|
567
|
+
| `switch compact model <id>` | Chọn rõ model viết summary |
|
|
568
|
+
| `switch compact model none` / `default` | Không gọi model summary; không dùng model của hội thoại làm dự phòng |
|
|
569
|
+
| `switch compact idle <phút>` | Ngưỡng nghỉ, mặc định 15; phạm vi 1–1440 |
|
|
570
|
+
| `switch compact min <KB>` | Kích thước request tối thiểu, mặc định 64; phạm vi 8–4096 |
|
|
571
|
+
|
|
572
|
+
Không đặt model summary (chuỗi rỗng hoặc `null`) thì không gọi model nào. Bản rút gọn xác định
|
|
573
|
+
sẵn giữ chữ của user và assistant trong ngân sách đã chọn, bỏ payload tool cũ và giữ tail gần
|
|
574
|
+
nhất cùng đủ cặp call/result. Nếu có model summary, một lượt gọi nền chỉ tóm tắt prefix được
|
|
575
|
+
thay thế rồi cập nhật generation cache. Summary đến muộn không ghi đè generation mới hơn và
|
|
576
|
+
không mất các turn xuất hiện trong lúc chờ. Model summary được map và chọn format upstream riêng.
|
|
577
|
+
|
|
578
|
+
Phải có định danh phiên rõ ràng: header/metadata phiên Claude, hoặc session, thread hay prompt
|
|
579
|
+
cache key của Codex. Request không có định danh giữ nguyên; cùng câu mở đầu không được coi là
|
|
580
|
+
cùng phiên. Phạm vi cache gồm client, digest credential của caller, profile, model, system/tool
|
|
581
|
+
và policy nén. Lịch sử tool không được hỗ trợ hoặc sai cặp giữ nguyên; cache lỗi cũng gửi nguyên
|
|
582
|
+
request gốc.
|
|
583
|
+
|
|
584
|
+
Dashboard và `/api/idle-compact` dùng cùng cấu hình với CLI. Các field được nhận: `enabled`,
|
|
585
|
+
`codex`, `model`, `idleMinutes`, `minBytes`, `keepRecent`, `userChars`, `summaryMaxChars` và
|
|
586
|
+
`sessionLookbackHours`. API trả HTTP 400 cho field lạ hoặc ngân sách không hợp lệ.
|
|
587
|
+
Các phạm vi/mặc định còn lại: `keepRecent` 1–100 (6), `userChars` 128–24000 (3000),
|
|
588
|
+
`summaryMaxChars` 256–128000 (24000), `sessionLookbackHours` 1–168 (72).
|
|
589
|
+
Cache riêng giữ tối đa 256 record, hết hạn theo thời gian đã cấu hình. Một khoảng nghỉ mới có
|
|
590
|
+
thể nén prefix lớn hơn; các turn liên tiếp dùng lại prefix ổn định. Codex tự quản lý native compaction.
|
|
591
|
+
|
|
592
|
+
Khi dùng intact, profile cần API key intact được cấp quyền. User-Agent chỉ chọn đường protocol
|
|
593
|
+
gốc, không thay cho xác thực ở upstream.
|
|
594
|
+
|
|
551
595
|
### Cập nhật
|
|
552
596
|
|
|
553
597
|
Khi npm có bản mới, dashboard hiện một thông báo. Bấm **Update now** trong thông báo này. Gateway cài bản mới và chạy code mới. Sau đó dashboard tự tải lại. `switch update` làm việc tương tự từ terminal.
|
|
@@ -765,6 +809,7 @@ Nhờ cách chia này, fingerprint của provider không bao giờ là rule tron
|
|
|
765
809
|
| `LLM_SWITCHER_BIFROST_RETRY_MS` | Thời gian trước khi gateway hỏi lại intact sau một lần tra cứu Bifrost thất bại (mặc định 30000 ms). |
|
|
766
810
|
| Header `x-llm-profile: <key>` (tên khác `x-profile`) hoặc `?profile=<key>` | Định tuyến riêng 1 request qua profile chỉ định. Key không tồn tại trả HTTP 400 thay vì âm thầm dùng profile khác. |
|
|
767
811
|
| `profile.thinkingMode` | `auto` (mặc định, cho gateway như intact hoặc 9Router): phục hồi thinking bị xoá, inject hướng dẫn `<think>` cho model không có reasoning, gửi `thinking` + `reasoning_effort`. `native` (API OpenAI nghiêm ngặt): chỉ gửi `reasoning_effort` khi client yêu cầu, không sửa prompt, dùng `max_completion_tokens`. `off`: không bao giờ gửi tham số reasoning. |
|
|
812
|
+
| `profile.thinkingEffort` | `auto` (mặc định, client tự quyết định), hoặc đặt cố định mức override: `none`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`, `ultra`. Ghi đè mức reasoning effort từ công cụ trước khi gửi lên upstream. Tự động ánh xạ về mức tương đương gần nhất mà upstream đích hỗ trợ (OpenAI trần `high`, Anthropic trần `max`, Gemini điều chỉnh `thinkingBudget`). Trên Web UI, tùy chọn này tự động ẩn cùng thông báo gợi ý khi profile bật chế độ Bifrost passthrough. |
|
|
768
813
|
| `profile.endpoints.countTokens` | Ghi đè URL `count_tokens` của Anthropic. |
|
|
769
814
|
| `profile.endpoints` | Ghi đè URL upstream theo từng format: `{ "openai-chat": "...", "anthropic": "...", "vertex": "https://.../models/{model}:{action}" }`. |
|
|
770
815
|
| `LLM_SWITCHER_STATE_DIR` | Chuyển file launcher và log ra khỏi thư mục dữ liệu. Test dùng biến này; shim đọc thư mục đã đặt lúc cài shim. |
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# Báo cáo phiên làm việc — 2026-10-07
|
|
2
|
+
|
|
3
|
+
> Bản ghi của một phiên làm việc, để đọc lại được về sau. Nó nói cả những gì đã kiểm chứng và
|
|
4
|
+
> những gì **chưa**, và các mục trong bảng mục lục là các phần.
|
|
5
|
+
|
|
6
|
+
Một phiên làm việc trên hai repo: **llm-switcher** (gateway ở phía client) và **intact** (gateway
|
|
7
|
+
ở phía provider). Chủ đề xuyên suốt: **một hội thoại dài đáng tiền khi cache đã mất, và cách làm nó
|
|
8
|
+
rẻ đi mà không phá client nào.**
|
|
9
|
+
|
|
10
|
+
Trang này là bản ghi của những gì đã kiểm chứng, những gì đo được, và những gì **chưa** chứng
|
|
11
|
+
minh được. Phần cuối là danh sách việc dở — có một mục đã biết là chưa xong và nói rõ ở đó.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 1. Kết quả, theo thứ tự đã giao
|
|
16
|
+
|
|
17
|
+
### intact — `5dddbbe`, `ba19457` · deploy `intact`, `intact-af`, `intact-ops`
|
|
18
|
+
|
|
19
|
+
**a. 429 → retry với context đã rút gọn.**
|
|
20
|
+
|
|
21
|
+
Giới hạn của provider tính theo token/phút, nên account kế tiếp gặp đúng cuộc hội thoại đó trước
|
|
22
|
+
cùng bức tường. Tệ hơn: account kế tiếp **không có cache** cho prefix này, vì nó chưa từng thấy lượt
|
|
23
|
+
trả lời đầu — lịch sử dài tốn tiền đầy đủ và không đổi lấy gì.
|
|
24
|
+
|
|
25
|
+
Đo trên một hội thoại agent 32 KB: retry đi ra **10 KB, bớt 68%**.
|
|
26
|
+
|
|
27
|
+
Hai cổng, và không có gì ngoài chúng:
|
|
28
|
+
|
|
29
|
+
| Cổng | Vì sao |
|
|
30
|
+
|---|---|
|
|
31
|
+
| Provider ở chế độ round-robin **và** còn account sẵn sàng | Provider ghim một account thì không có chỗ nào nhận bản rút gọn; rút gọn chỉ là vứt lịch sử đi. Một account standby không tính là sẵn sàng — nó là phương án cuối. |
|
|
32
|
+
| **429 thật** | `500` cũng được retry nhưng rút gọn không giúp: provider **hỏng**, không phải **đầy**. |
|
|
33
|
+
|
|
34
|
+
**b. Codex compaction — trả đúng 1 `compaction` item.**
|
|
35
|
+
|
|
36
|
+
Codex 0.160 gửi một `compaction_trigger` và chỉ nhận đúng một output item loại `compaction`. Provider
|
|
37
|
+
trả lời như một completion thường thì cho 0 item, và Codex coi đó là lỗi chết **trước khi** nó thay
|
|
38
|
+
lịch sử — thread giữ nguyên độ dài, mọi lần `resume` lại compact và chết y hệt. Tính năng báo
|
|
39
|
+
`removed`, không tắt được.
|
|
40
|
+
|
|
41
|
+
Kiểm chứng trên provider codex thật: **HTTP 200, đúng 1 compaction item**, cả stream lẫn non-stream.
|
|
42
|
+
|
|
43
|
+
Ba điều không dễ thấy:
|
|
44
|
+
- Mang theo setting của client. Responses của OpenAI từ chối request không có `store:false`, và từ
|
|
45
|
+
chối không có `stream:true` — nó sẽ từ chối đúng những account đang phục vụ mọi turn khác.
|
|
46
|
+
- Account chỉ nói dạng stream thì một lần retry bật flag.
|
|
47
|
+
- **Không bao giờ lưu summary rỗng.** Provider trả tool call sẽ nhận lỗi, vì summary rỗng thay cả
|
|
48
|
+
lịch sử bằng hư không.
|
|
49
|
+
|
|
50
|
+
**c. Cổng xác thực nhận client chính thức của provider.**
|
|
51
|
+
|
|
52
|
+
Codex nói chuyện với ChatGPT bằng credential của ChatGPT. Trỏ sang intact thì nó vẫn gửi credential
|
|
53
|
+
đó, mà intact không kiểm được — intact chỉ biết key mình phát. Request mà intact sinh ra để phục
|
|
54
|
+
vụ bị chặn ngay cửa.
|
|
55
|
+
|
|
56
|
+
Miễn trừ hẹp **hai chiều**: chỉ route proxy `/v1/*`, không phải `/api/*` (nếu không, một
|
|
57
|
+
User-Agent bịa sẽ mở usage, quota, error log, drift); và chỉ đúng provider khai báo tiền tố — client
|
|
58
|
+
codex hỏi model claude thì **403**. Principal đó không có key, không admin, không model nào riêng;
|
|
59
|
+
credential lấy từ kho intact đã lưu, không lấy từ caller.
|
|
60
|
+
|
|
61
|
+
Hai lỗi test bắt được: `codex_cli_rs/` là của **TUI tương tác**, còn `codex exec` gửi
|
|
62
|
+
`codex_exec/` — nên `BifrostUA` phải nhận nhiều tiền tố. Và một test đang **đòi từ chối** chính Claude
|
|
63
|
+
Code thật — cái sai là test.
|
|
64
|
+
|
|
65
|
+
### llm-switcher — `281f180` → `4423205`, 1.5.0 → 1.6.0
|
|
66
|
+
|
|
67
|
+
**a. Nén hội thoại khi idle quá lâu (Claude Code).**
|
|
68
|
+
|
|
69
|
+
Hội thoại để lâu thì mất cache. Thời gian giữ cache là việc riêng của từng provider:
|
|
70
|
+
|
|
71
|
+
| Provider | Thời hạn |
|
|
72
|
+
|---|---|
|
|
73
|
+
| Anthropic | 5 phút mặc định, 1 giờ nếu request yêu cầu (`cache_control.ttl`) |
|
|
74
|
+
| OpenAI | `prompt_cache_options.ttl` chỉ có `"30m"`; model cũ dùng `prompt_cache_retention`, `in_memory` ~5–10 phút |
|
|
75
|
+
| Gemini | implicit caching không công bố thời hạn; explicit mặc định 1 giờ |
|
|
76
|
+
| DeepSeek | tự động trên đĩa, không công bố thời hạn |
|
|
77
|
+
|
|
78
|
+
Nên thời gian là **setting**, mặc định 15 phút.
|
|
79
|
+
|
|
80
|
+
Hai quyết định:
|
|
81
|
+
|
|
82
|
+
- **Nén một lần, không nén mọi lượt.** Đổi prefix mỗi lượt nghĩa là không lượt nào trúng cache,
|
|
83
|
+
và hội thoại bị tóm tắt đi đi lại.
|
|
84
|
+
- **Ghi vào session file, không qua API.** Claude Code dựng mọi request từ chuỗi entry trong
|
|
85
|
+
`~/.claude/projects/<project>/<session>.jsonl`, nên một lần nén ghi vào đó rút gọn **mọi** request
|
|
86
|
+
sau, không chỉ lượt kích hoạt.
|
|
87
|
+
|
|
88
|
+
Entry ghi ra đúng hai loại Claude Code tự ghi: `compact_boundary` và một entry `user` mang
|
|
89
|
+
`isCompactSummary`. Entry cũ **vẫn còn** trong file — Claude Code cũng vậy sau khi nén 949k token —
|
|
90
|
+
vì một transcript người ta vẫn đọc được đáng giá hơn một file nhỏ.
|
|
91
|
+
|
|
92
|
+
**Kiểm chứng trên Claude Code thật:** 12 turn giả (~200 KB) + một boundary → `claude --resume` gửi
|
|
93
|
+
**4 message, 80.567 byte** thay vì 13; có marker summary; **0** còn 4000 ký tự filler.
|
|
94
|
+
|
|
95
|
+
**b. Dashboard.** Card trên trang Routes, đọc/ghi `/api/idle-compact` — **cùng key với `switch
|
|
96
|
+
compact`**, nên trang và terminal không thể lệch nhau.
|
|
97
|
+
|
|
98
|
+
**c. Nén cho Codex (1.6.0).** Xem mục 4.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## 2. Cách kiểm chứng
|
|
103
|
+
|
|
104
|
+
| Cách | Kết quả |
|
|
105
|
+
|---|---|
|
|
106
|
+
| Test end-to-end có 429 thật, store thật, account thật | retry 32.532 → 10.473 byte |
|
|
107
|
+
| Test Claude Code thật, đọc request nó gửi | 4 message, không còn turn cũ |
|
|
108
|
+
| Gọi thẳng intact với `compaction_trigger`, provider codex thật | đúng 1 compaction item |
|
|
109
|
+
| Cổng xác thực, 5 hành vi trên bản đang chạy | key thật 200 · UA lạ 401 · UA codex 200 · `/api/*` 401 · codex→claude 403 |
|
|
110
|
+
| **Claude Code thật qua gateway đang chạy** | `idle compact h:bbd52533-4 [p1]: 9681 -> 3832 bytes` · `summary stored` · session file có `compact_boundary` + `isCompactSummary` · `compactMetadata` preTokens 2420 → postTokens 958 |
|
|
111
|
+
| Suite | llm-switcher 704 pass · intact toàn bộ xanh · zencore 301 pass |
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## 3. Những điều đoán sai rồi đính chính
|
|
116
|
+
|
|
117
|
+
Ghi lại vì chúng là phần tốn thời gian nhất và có thể sai lại.
|
|
118
|
+
|
|
119
|
+
1. **"Không có cách nào báo cho tool biết phải compact."** Sai. llm-switcher ngồi ở phía client, có
|
|
120
|
+
filesystem. Tôi đã nghĩ trong khuôn protocol rồi kết luận là không thể.
|
|
121
|
+
2. **"Sao 1.4.0 chưa tải được?"** Đúng là registry-side propagation, không phải publish hỏng.
|
|
122
|
+
3. **Remote compaction của Codex gắn với auth mode.** Sai: nó compact cục bộ cả khi auth là
|
|
123
|
+
ChatGPT và endpoint trỏ về intact. Nó gắn với **endpoint có phải backend ChatGPT thật không**.
|
|
124
|
+
4. **Ghi vào file jsonl của Codex là đủ.** Sai: SQLite mới là nơi Codex đọc.
|
|
125
|
+
5. **Viết hai nửa vào store là xong.** Sai: còn phải đẩy projection.
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## 4. Cơ chế compaction của Codex — đã tìm ra đủ
|
|
130
|
+
|
|
131
|
+
Một compaction của Codex là **ba ghi phải khớp nhau**, đọc từ một thread thật đã compact 7 lần:
|
|
132
|
+
|
|
133
|
+
1. **rollout jsonl**: entry `{type:"compacted", payload:{message, replacement_history, …}}` — mang
|
|
134
|
+
nội dung tóm tắt.
|
|
135
|
+
2. **`thread_history_1.sqlite` → `thread_items`**: hàng `item_type='contextCompaction'`, mà
|
|
136
|
+
`item_json` chỉ có `{"type":"contextCompaction","id":…}` — **chỉ là mốc, không có chữ summary**.
|
|
137
|
+
3. **`thread_history_projection_state`**: `next_rollout_byte_offset` và `next_rollout_ordinal`.
|
|
138
|
+
|
|
139
|
+
Mảnh thứ ba là cái dễ bỏ, và bỏ nó thì **trông y hệt thành công**. Bảng đó ghi store đã đọc tới đâu
|
|
140
|
+
trong file: `next_rollout_byte_offset` **bằng đúng kích thước file** (đã đo: 3.722.618 byte =
|
|
141
|
+
`os.path.getsize`), `next_rollout_ordinal` là ordinal sau entry cuối. **File là bản ghi, store là
|
|
142
|
+
projection của nó.** Ghi thêm vào file mà không đẩy projection thì entry đó không bao giờ được đọc.
|
|
143
|
+
|
|
144
|
+
Kiểm chứng: cài một compaction chứa `CON_VIET_4711` vào thread Codex thật → `resume` → Codex trả
|
|
145
|
+
đúng `CON_VIET_4711`. Qua gateway: `contextCompaction 1` trong store, projection đã đẩy, log ghi
|
|
146
|
+
`summary stored`.
|
|
147
|
+
|
|
148
|
+
Ba lỗi chỉ chạy mới thấy:
|
|
149
|
+
- Ordinal lấy từ **projection**, không phải `MAX(rollout_ordinal)` — hai số khác nhau.
|
|
150
|
+
- Hàng marker cần `started_at_ms` và `completed_at_ms`; hàng của Codex có, để NULL thì không đọc được.
|
|
151
|
+
- Hội thoại **không có message người dùng** thì không có conversation key → không làm gì, trông
|
|
152
|
+
như tính năng tắt chứ không phải input sai.
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## 5. Bốn quy tắc đã đặt, và trạng thái
|
|
157
|
+
|
|
158
|
+
| Quy tắc | Trạng thái |
|
|
159
|
+
|---|---|
|
|
160
|
+
| Chỉ dùng intact và llm-switcher, không đụng config Codex | **OK** — `~/.codex` không có file config nào; mọi test Codex chạy trong sandbox `CODEX_HOME` riêng |
|
|
161
|
+
| Không tốn token account chính thức | **OK, sau khi sửa** — xem dưới |
|
|
162
|
+
| 429 → compact | **OK** — intact, 4 test e2e |
|
|
163
|
+
| Hết TTL cache → tự compact | **OK** — llm-switcher, test Claude Code thật |
|
|
164
|
+
|
|
165
|
+
**Quy tắc 2 đã bị vi phạm và đã sửa.** Khi không đặt model summary, code gọi model của chính thread —
|
|
166
|
+
với Codex đó là `gpt-5.6-sol` trên tài khoản ChatGPT chính thức, đúng thứ bị cấm. Tệ hơn: lượt
|
|
167
|
+
summary chỉ dùng để ghi session file, nên không có gì để lưu — gọi model chỉ tốn tiền mà không đổi
|
|
168
|
+
lấy gì. Nay **không gọi model nào trừ khi có đặt**; phần tiết kiệm không đổi vì request vẫn ra
|
|
169
|
+
ngắn.
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## 6. Lỗi của tôi trong phiên này
|
|
174
|
+
|
|
175
|
+
- **Giết nhầm zencore.** Chọn port 8793 cho công cụ thử mà không kiểm tra port đó đã có dịch vụ
|
|
176
|
+
chạy. Đã khởi động lại và xác nhận khỏe, nhưng đó là lỗi của tôi.
|
|
177
|
+
- **Trộn hai kiểu dữ liệu.** Đưa shape của IR vào body Messages, khiến body ra upstream sai định
|
|
178
|
+
dạng. Chỉ thấy khi đọc body thật ra ngoài.
|
|
179
|
+
- **Ghi session file bằng rename.** Sẽ mất entry: Claude Code giữ file mở, append tiếp vào inode cũ.
|
|
180
|
+
- **Bỏ qua kiểm tra ngược.** Viết xong rồi chỉ chạy test, không đọc lại code sạch sau những lần sửa.
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## 7. Việc dở
|
|
185
|
+
|
|
186
|
+
**Không còn mục nào trong bốn quy tắc.**
|
|
187
|
+
|
|
188
|
+
Đã đóng trong phiên: validate Claude Code qua gateway thật. Còn ba lỗi của *công cụ thử*, không
|
|
189
|
+
phải của tính năng — model thử không tồn tại ở intact; sandbox đặt `CLAUDE_CONFIG_DIR` chỉ cho
|
|
190
|
+
Claude Code nên gateway tìm nhầm `~/.claude` thật; và hàm thử dọn đồ trước khi lượt summary xong.
|
|
191
|
+
Cả ba đã sửa và chạy lại thành công.
|
|
192
|
+
|
|
193
|
+
Còn một việc **không tự làm được**, vì nó nằm trên tài khoản của bạn: thu hồi token npm trong
|
|
194
|
+
`/opt/hermes/.npmrc`. `id_token` OpenAI từng dán trong chat đã hết hạn từ lâu, không cần làm gì.
|
|
195
|
+
|
|
196
|
+
Còn một điều **không xong**: Codex **không** dùng compaction của provider. `remote_compaction_v2` chỉ
|
|
197
|
+
chạy khi Codex nói chuyện với backend ChatGPT thật; `CHATGPT_BASE_URL` bị bỏ qua, và các khoá tắt
|
|
198
|
+
WebSocket không ăn. Để đi hết đường đó phải chặn TLS (`HTTPS_PROXY` + `SSL_CERT_FILE`), mà tôi
|
|
199
|
+
không tự làm vì nó đổi niềm tin TLS của máy.
|
|
200
|
+
|
|
201
|
+
Hệ quả đã ghi rõ trong tài liệu: cờ `idleCompact.codex` tắt mặc định, tiết kiệm đúng **một lượt**,
|
|
202
|
+
và không có lời hứa nào rằng thread sẽ giữ ngắn.
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## 8. Bàn giao
|
|
207
|
+
|
|
208
|
+
| | |
|
|
209
|
+
|---|---|
|
|
210
|
+
| llm-switcher | `4423205`, 1.6.0, `main` |
|
|
211
|
+
| intact | `ba19457`, `master` |
|
|
212
|
+
| zencore | không đổi |
|
|
213
|
+
| Service | `intact`, `intact-af`, `intact-ops`, `zencore` — đều `active` |
|
|
214
|
+
| Gateway | llm-switcher, Claude Code → `claude-intact` |
|
|
215
|
+
| Tính năng | `switch compact` — **tắt** |
|
|
216
|
+
|
|
217
|
+
```
|
|
218
|
+
switch compact on # Claude Code
|
|
219
|
+
switch compact on && switch compact model infron/deepseek/deepseek-v4-flash:free
|
|
220
|
+
switch compact idle 30 # phút
|
|
221
|
+
switch compact min 128 # KB
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
**Còn nợ bạn:** token npm và `id_token` từng dán trong chat — hai thứ đều nên thu hồi.
|