llm-switcher 1.6.0 → 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 +26 -0
- package/README.md +42 -34
- package/README.vi.md +38 -33
- package/docs/session-2026-10-07-compaction.md +224 -0
- package/formats.mjs +85 -25
- package/idlecache.mjs +159 -0
- package/idlecall.mjs +46 -54
- package/idlecompact.mjs +171 -238
- package/package.json +1 -1
- package/proxy.mjs +111 -135
- package/shim.mjs +29 -0
- package/state.mjs +48 -2
- package/switch.mjs +12 -5
- 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 +233 -279
- package/ui.html +449 -81
- package/claudesession.mjs +0 -196
- package/codexsession.mjs +0 -141
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
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
|
+
|
|
3
29
|
## Release 1.6.0 — 2026-10-07
|
|
4
30
|
|
|
5
31
|
- **A Codex thread now keeps the shortening, behind the same opt-in switch.** A Codex compaction is
|
package/README.md
CHANGED
|
@@ -552,43 +552,50 @@ On Linux the unit is a systemd *user* service: it starts when you log in. To sta
|
|
|
552
552
|
|
|
553
553
|
### Idle compaction
|
|
554
554
|
|
|
555
|
-
A
|
|
556
|
-
|
|
557
|
-
a provider keeps its cache is its own business: Anthropic 5 minutes by default and 1 hour when the
|
|
558
|
-
request asks for it, OpenAI 30 minutes, Gemini and DeepSeek publish no lifetime at all.
|
|
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.
|
|
559
557
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
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.
|
|
563
564
|
|
|
564
|
-
| | |
|
|
565
|
+
| Command | Behavior |
|
|
565
566
|
| --- | --- |
|
|
566
|
-
| `switch compact status` | Show
|
|
567
|
-
| `switch compact on` / `off` |
|
|
568
|
-
| `switch compact
|
|
569
|
-
| `switch compact
|
|
570
|
-
| `switch compact
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
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.
|
|
592
599
|
|
|
593
600
|
### Updates
|
|
594
601
|
|
|
@@ -808,6 +815,7 @@ Because of this split, a provider fingerprint is never a rule in this gateway. I
|
|
|
808
815
|
| `LLM_SWITCHER_BIFROST_RETRY_MS` | Time before the gateway asks intact again after a failed Bifrost lookup (default 30000 ms). |
|
|
809
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. |
|
|
810
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. |
|
|
811
819
|
| `profile.endpoints.countTokens` | Override the Anthropic `count_tokens` URL. |
|
|
812
820
|
| `profile.endpoints` | Override upstream URLs per format: `{ "openai-chat": "...", "anthropic": "...", "vertex": "https://.../models/{model}:{action}" }`. |
|
|
813
821
|
| `CLAUDE_CONFIG_DIR` | Respected when locating Claude Code's `settings.json`. |
|
package/README.vi.md
CHANGED
|
@@ -550,43 +550,47 @@ Trên Linux, unit là service systemd *của user*: nó khởi động khi bạn
|
|
|
550
550
|
|
|
551
551
|
### Nén hội thoại khi để lâu
|
|
552
552
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
việc của riêng họ: Anthropic 5 phút mặc định, 1 giờ nếu request yêu cầu; OpenAI 30 phút; Gemini và
|
|
556
|
-
DeepSeek không công bố thời hạn nào.
|
|
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.
|
|
557
555
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
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
561
|
|
|
562
|
-
| | |
|
|
562
|
+
| Lệnh | Hành vi |
|
|
563
563
|
| --- | --- |
|
|
564
564
|
| `switch compact status` | Xem cấu hình |
|
|
565
|
-
| `switch compact on` / `off` | Bật hoặc tắt
|
|
566
|
-
| `switch compact
|
|
567
|
-
| `switch compact
|
|
568
|
-
| `switch compact
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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.
|
|
590
594
|
|
|
591
595
|
### Cập nhật
|
|
592
596
|
|
|
@@ -805,6 +809,7 @@ Nhờ cách chia này, fingerprint của provider không bao giờ là rule tron
|
|
|
805
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). |
|
|
806
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. |
|
|
807
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. |
|
|
808
813
|
| `profile.endpoints.countTokens` | Ghi đè URL `count_tokens` của Anthropic. |
|
|
809
814
|
| `profile.endpoints` | Ghi đè URL upstream theo từng format: `{ "openai-chat": "...", "anthropic": "...", "vertex": "https://.../models/{model}:{action}" }`. |
|
|
810
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.
|