translatePiProviderMutationError returns pi.form.providerKeyDuplicate for the Pi backend's duplicate-key error, but the key existed in none of the four locale files, so the toast showed the raw key. common.collapse (aria-label of the thinking-map toggle) was missing the same way. Add both keys to zh/en/ja/zh-TW and lock them in the locale coverage test.
mask_url's fallback branch (used when the proxy URL fails Url::parse) sliced the raw string at byte 20 and panicked when a multi-byte UTF-8 character straddled that offset, e.g. a CJK string pasted into the global proxy address. Snap the cut to the nearest char boundary at or before 20 and add a regression test.
On Windows, Claude Desktop config paths resolve from LOCALAPPDATA, which the test home override in tests/support.rs did not redirect, so profile_roundtrip wrote the https://desktop.test fixture into the developer's real Claude Desktop config. Point LOCALAPPDATA at the isolated test home alongside HOME and USERPROFILE.
Fixes#6077
The pricing model source SelectTrigger in the usage cost settings table was w-28, which leaves about 70px for the label; the English and Japanese labels need about 107px and overflowed the trigger. Widen it to w-40 and drop the h-7 overrides so the trigger and the adjacent Input use the shared h-9 height.
Add eight Tencent presets for Pi, covering the TokenHub pay-as-you-go line
(domestic and international) and the six Token Plan subscription products —
Pi was the only managed app still missing them. Model IDs, ordering and
endpoints follow Tencent's own model lists verbatim, and the tests assert each
product's line-up in order so future drift is caught.
Declare thinking control alongside the capabilities rather than leaving it to
defaults: the 31 DeepSeek entries carry a Tencent-specific compat so selecting
"off" actually disables thinking instead of silently billing reasoning tokens,
the eight Kimi K2.7 Code entries no longer offer an "off" level the model
cannot honour, and kimi-k2.7-code-highspeed is marked image-capable so
tool-returned screenshots are no longer dropped.
Drop minimax-m2.5 from every app: it is gone from Tencent's model tables and
the platform is retiring it.
Codex's built-in ImageGen calls the legacy OpenAI Images API, but the local
proxy only registered Responses / Chat Completions / Compact / Alpha Search
routes, so `/v1/images/generations` returned an empty 404 under Codex routing.
- Register `/images/generations` aliases (bare, `/v1`, `/v1/v1`, `/codex/v1`)
and forward them as a Codex standalone passthrough shared with Alpha Search.
- Generalize the Alpha Search full-URL rewrite into
`rewrite_codex_standalone_full_url`: derive the sibling
`/images/generations` from full Responses / Compact / Chat Completions URLs,
keep an existing Images URL as-is, and fail closed on opaque full URLs.
- Record `usage.input_tokens` / `output_tokens` through the Codex usage parser.
Fixes#5429Fixes#6745
Seeds the built-in pricing row for Gemini 3.8 Flash at 0.75 / 3.75 input/output per million tokens, 0.075 cache read, no cache-write charge, plus a regression test locking all four fields.
Values verified against Google's own pricing page (introductory price through 2026-12-31; list price 1.50 / 7.50 / 0.15 from 2027-01-01) and models.dev.
Add a PPIO provider preset for Pi using the OpenAI-compatible endpoint,
and pin an explicit models URL on the Claude Code PPIO preset so model
discovery resolves against https://api.ppio.com/openai/v1/models instead
of the derived candidates, which all return 404.
Co-authored-by: Jason <farion1231@gmail.com>
Codex OAuth requests defaulted parallel_tool_calls to false when the incoming
Anthropic request carried no explicit value, forcing Claude Code into one tool
call per model turn. Default it to true (matching codex-rs), map Anthropic
tool_choice.disable_parallel_tool_use to the inverse Responses value so explicit
serial execution is preserved, and add regression coverage for both directions.
Carries #5722 forward on top of current main.
Fixes#5719
Co-authored-by: 太一 <taiyi@douyu.tv>
When a model slug is not in the bundled DeepSeek official catalog, the vendor path clones the flagship entry and previously inherited its text-only input_modalities. Resolve modalities for unmatched slugs through the same registry/fail-open logic the non-vendor path uses, so a vision variant such as deepseek-v4-flash-vision-exp is no longer declared text-only. Explicit user inputModalities still win; matched models keep the vendor's declaration verbatim.
Fixes#6725
Bump the stable client-facing role aliases that takeover mode writes to
Claude Code: Opus claude-opus-4-8 -> claude-opus-5 and Sonnet
claude-sonnet-4-6 -> claude-sonnet-5, so the /model menu matches the
Claude Desktop default routes and preset defaults that already moved to
Opus 5 / Sonnet 5. Add "opus-5" to the adaptive-thinking classifier so
the Bedrock thinking optimizer, which runs on the client alias before
model mapping, keeps sending adaptive thinking instead of the removed
budget_tokens.
Routing is unaffected: the proxy maps client aliases by role keyword, and
the aliases are rewritten automatically the next time the proxy starts.
Fixes#5876
Co-authored-by: Jason <farion1231@gmail.com>
Seed glm-5.3 at $1.4/$4.4 per MTok with $0.26 cache read, matching the
official Z.ai list price (identical to the GLM-5.2 tier):
https://docs.z.ai/guides/overview/pricing
New model id, so seed only; no repair entry and no SCHEMA_VERSION bump.
Existing installs pick the row up on next launch via INSERT OR IGNORE, and
the startup cost backfill re-prices historical glm-5.3 rows recorded at $0.
Requests for gpt-6-astra through the Claude-to-Codex OAuth takeover route advertised Codex 0.144.1, below the model's 0.153.0 minimal_client_version, so ChatGPT's backend rejected them with HTTP 400 "requires a newer version of Codex". Bump the advertised client version to 0.153.4 and share the originator/version constants between generation and model discovery, which previously sent cc-switch's own package version and originator.
Two regression tests pin the generation headers and the model-list request to a version at or above the gpt-6-astra floor.
Refs #7129
DeepSeek's Responses API has no tool_search support, but the bundled
official catalog declared supports_search_tool=true for deepseek-v4-pro
and deepseek-v4-flash. Codex gates deferred/namespaced MCP tools on
this flag (search_tool_enabled = supports_search_tool &&
namespace_tools), so MCP tools were hidden behind a tool_search the
gateway cannot serve — no MCP tool could ever be called.
Set the flag to false for both models so Codex lists MCP tools inline.
Hosted web_search is unaffected (gated by provider capabilities, not
this flag) and DeepSeek's server-side web_search still works.
Fixes#6647
Moonshot's Chat Completions validator (api.moonshot.cn, api.moonshot.ai and the Kimi For Coding endpoint api.kimi.com) rejects a `$ref` that carries sibling keywords. Codex Desktop's built-in tool schemas carry exactly that shape, so every desktop turn routed through the local proxy to Kimi failed with HTTP 400.
When the resolved upstream host is Moonshot/Kimi, each `$ref` with siblings is moved into `allOf` after the Responses -> Chat conversion and the siblings stay in place. The traversal only enters schema-valued keywords and the rewrite is idempotent. Every other provider keeps byte-identical tool schemas, and the Codex -> Anthropic path is left alone because Moonshot's Anthropic-compatible endpoint accepts the original shape.
Fixes#6867
Co-authored-by: Jason <farion1231@gmail.com>
Zhipu documents three separate base URLs per site (Anthropic /api/anthropic,
Chat /api/coding/paas/v4, Responses /api/v1) and warns that the wrong one
cannot use Coding Plan quota. Codex direct-connect speaks Responses, so the
presets must target /api/v1; the 400 in #6944 (`unknown variant custom`)
came from the Chat endpoint's strict legacy gateway.
- Switch the Zhipu GLM / Zhipu GLM en presets to apiFormat openai_responses
(NativeResponses catalog: shell_command edits, no freeform apply_patch)
- Point base_url/endpointCandidates at /api/v1 and mirror the official
models.json: glm-5.3 (1M context, low/high/max, default max) plus
glm-5-turbo on the CN site; drop glm-5.2 and the `none` level
- Host fallback: rows saved with a stale openai_chat apiFormat but a native
Responses base_url get the NativeResponses catalog without a re-save;
Chat-endpoint paths (/paas/v4, /chat/completions) are excluded
- Match vendor hosts on DNS label boundaries via a shared helper, so `z.ai`
no longer captures xyz.ai; add bigmodel.cn / z.ai and the glm prefix to
the Codex web_search reject list
Existing Zhipu providers still point at the Chat endpoint; re-import the
preset to pick up /api/v1.
Fixes#6944
Co-authored-by: Jason <farion1231@gmail.com>
Codex 0.144.5 through 0.148.0-alpha.15 declare `supports_parallel_tool_calls`
as a required catalog field (no serde default), while openai/codex#38499
(2026-08-14) removed the field from ModelInfo and models.json. Any
models_cache.json refreshed by a newer build therefore lacks the field, and
the ProxyChat catalog cloned from it is rejected by an older Codex on the
same machine with "missing field `supports_parallel_tool_calls`".
Add the field to CODEX_CATALOG_PARSER_REQUIRED_FIELDS so it is backfilled
from the bundled gpt-5.5 template (true); existing values are never
overwritten. Newer builds ignore unknown fields, so the extra key is
harmless there.
Verified with real codex binaries 0.147.0 / 0.148.0-alpha.15 / 0.148.0:
the catalog generated on main is rejected by the first two, the catalog
generated with this change loads on all three.
Fixes#6661Fixes#6709
Reject an ordinary Codex managed-account login when both the ChatGPT workspace and the stable user identity (id_token sub) already match an existing account. The check runs under the storage lock so concurrent login completions cannot create duplicate records, and the refresh lock is now taken only for targeted re-auth so a rejected add leaves no lock entry behind. Distinct users in the same workspace remain supported. The duplicate-account error code is mapped to a localized message in the managed-auth UI.
Give icon-only back buttons, the header local-routing switch, the project
switcher popover, the Claude JSON editors, and the existing-Skills import
checkboxes programmatic names so screen readers announce their purpose.
Reuses existing i18n strings where possible and adds two short localized
labels across zh/en/ja/zh-TW.
Fixes#7048
- Add claude-fable-5-1 / claude-mythos-5-1 pricing rows ($10/$50, cache read $0.25 = 0.025x, cache write $12.50). Without a row these requests were stored at $0: the prefix rule only looks for longer dated ids and cannot fall back to claude-fable-5.
- Sonnet 5: Anthropic's pricing page now states the $2/$10 introductory price is the standard price and the September 1 increase to $3/$15 will not occur. Update the seed and add a repair_current_model_pricing guard so installs seeded at 3/15/0.30/3.75 are corrected; user-customized rows are left alone.
- Tests: seed values for the three rows; the repair guard fixes the old list price and keeps a custom price.
Fixes#7050
The tools panel asked PyPI for Hermes' latest version, but upstream
stopped publishing there after 0.19.0 (2026-07-20) and now ships only
through GitHub Releases. Users saw "latest 0.19.0" forever, often below
their installed version, and the upgrade button never appeared because
it only shows when latest is strictly newer than current.
PyPI was never the channel cc-switch installs or upgrades Hermes
through: install runs the official install.sh (git clone of main) and
update runs `hermes update` (git pull). Probe the same channel.
- Query GitHub Releases first and fall back to PyPI only when GitHub is
unreachable or rate-limited.
- Parse the semver from the release name ("Hermes Agent v0.21.0
(v2026.8.31)") since the tag is calendar-versioned; reject calendar
numbers on both paths so the frontend never treats 2026.8.31 as a
version and reports a permanent update.
- Hide the PyPI fallback value when the local version already leads it,
so a degraded probe shows "unknown" rather than "latest < current".
- Add a 15s request-level timeout to the GitHub and PyPI probes; the
shared proxy client allows 600s, which would stall the Hermes card
and the refresh/upgrade-all buttons when api.github.com hangs.
Refs: #6475, #6618, #7033
Supersedes: #6621
The nine 9527CODE presets pinned iconColor to #4F46E5, which came from
the docs site favicon rather than the logo itself. The vendor mark is
two-tone black (#000000 over #2D2D2D).
The fix is not to swap in #000000. ProviderIcon resolves colour as
preset iconColor -> metadata defaultColor -> undefined, and only the
undefined case leaves style.color unset, letting the inline SVG's
fill="currentColor" inherit the surrounding text colour. Any hex opts
out of theming, and a black one disappears in dark mode: the icon sits
in a bg-muted container at 18% lightness there, against a --foreground
of 98%, and nothing in the stylesheet inverts icons for dark mode.
Dropping iconColor and marking the metadata defaultColor currentColor
renders near-black on light, matching the official logo, and near-white
on dark. The traced SVG already suits this — its two tones are a
0.82-opacity base layer plus an opaque overlay, so the depth
relationship is relative and survives the inversion. Hard-coding both
tones would have foreclosed it.
This follows packycode and the 26 other currentColor icons.
9527CODE (9527.codes) is a sponsored enterprise-grade relay running New
API, fronting Claude, GPT, Gemini, Grok and Chinese models behind a
single key. It lands across Claude Code, Claude Desktop, Codex, Gemini
CLI, Grok Build, OpenCode, OpenClaw, Hermes and Pi, and is inserted
after APIKEY.FUN in both the preset files and the sponsor table so the
README order stays the ordering SSOT.
The gateway accepts every protocol with or without the /v1 prefix, but
the presets are written to match what each client actually appends
rather than relying on that fallback:
- Claude Code / Desktop / Gemini CLI: 9527.codes (client adds /v1)
- Codex / Grok Build: 9527.codes/v1 (responses)
- OpenCode: 9527.codes/v1 (@ai-sdk/anthropic)
- OpenClaw / Hermes / Pi: 9527.codes (anthropic-messages)
OpenCode and the other Anthropic-protocol clients look inconsistent but
are not: the AI SDK appends /messages to its baseURL while OpenClaw,
Hermes and Pi append /v1/messages themselves.
Three non-obvious details:
- endpointCandidates carry only the two live backups, api.9527.codes
(overseas) and cdn.9527.codes (Asia-Pacific). The vendor docs list a
fourth route, txcdn.9527.codes, which does not resolve; including it
would offer users an endpoint that cannot connect.
- No cost is recorded for the OpenClaw and Hermes models. What a user
actually pays is the official price times a token-group multiplier
that varies per group and plan, so quoting list prices would mislead.
- The icon is an inline SVG traced from the vendor logo, drawn as a
full silhouette at 0.82 opacity with the black regions layered on top.
Stroking the two tones as separate adjacent paths leaves a hairline
seam between them; the base layer covers it.
AICodeWith (aicodewith.ai) is an aggregator exposing 30 models — Claude,
GPT, Gemini, GLM, Kimi, DeepSeek and Grok — behind a single key and
balance. It lands across Claude Code, Claude Desktop, Codex, Gemini CLI,
OpenCode, OpenClaw, Hermes and Pi.
The vendor exposes one endpoint family per protocol and its own docs warn
against reusing the generic /v1. All four are transcribed from the
official client guides:
- Claude Code / Desktop: api.aicodewith.ai (Anthropic, no /v1)
- Codex: api.aicodewith.ai/chatgpt/v1 (responses)
- Gemini CLI: api.aicodewith.ai/gemini_cli
- OpenCode: api.aicodewith.ai/v1 (@ai-sdk/anthropic)
The first and last look inconsistent but are not: Claude Code appends
/v1/messages itself while the AI SDK appends /messages to its baseURL,
so both land on the same route.
Three non-obvious details:
- Claude Desktop uses mappedRoutes rather than passthroughRoutes. The
upstream Haiku id is claude-haiku-4-5-20251001, which differs from the
claude-haiku-4-5 role route id, so passthrough would point the Haiku
route at a model that does not exist.
- OpenClaw, Hermes and Pi have no official integration guide. They reuse
the Codex endpoint with their respective Responses protocol instead of
guessing at /v1/chat/completions, for which the vendor publishes no
example anywhere. Endpoint probing cannot settle this either: the auth
middleware runs before route matching, so every path returns 401
unauthenticated.
- Model ids come from GET /v1/models, which is unauthenticated and more
current than the pricing page; the OpenCode guide still advertises
kimi-k2 and glm-4.6, both retired.
The only logo the vendor ships is a 128KB PNG whose 2664 colours are
almost entirely compression noise over a two-tone image. It is vectorised
to a 4.9KB inline SVG using currentColor, so the icon follows the theme
rather than carrying an opaque white background.
* fix(usage): stop deferring resumed Codex rollouts on filename/meta ID mismatch
Codex resume creates rollout files named rollout-<ts>-<threadId>_<sessionId>.jsonl
(dual UUID). The root session_meta inside keeps payload.id = the ORIGINAL thread
id (the UUID before the underscore; continuation info lives in history_base).
The consistency check in parse_codex_file compared meta id against only the
filename's trailing UUID (the new session id), so every resumed rollout was
permanently deferred with:
文件名线程 ID (<sessionId>) 与 root meta ID (<threadId>) 不一致
Usage statistics then silently stop: once the user resumes an old thread, all
subsequent turns append to the dual-ID file which is skipped every sync cycle.
Reproduced live: stats stopped at 08/27 17:33, file had 152 unimported
token_count events from that day, deferral warning repeated every 60s cycle.
Fix: also accept meta id matching the pre-underscore UUID of dual-segment
filenames (leading_thread_id_from_filename). Single-UUID filenames keep the
strict check. thread_id_from_filename still returns the trailing UUID - it is
the request_id prefix (codex_thread:<id>:<event_index>), and event_index is
per-file, so the trailing session id is what keeps dedup keys from colliding
across segments of the same thread.
Tests: resumed rollout with meta id = leading UUID no longer defers (asserts
parent != Deferred and root_thread_id = trailing UUID); single-UUID mismatch
still defers. 2576 pass, clippy clean.
* fix(usage): store session_id as the leading thread id for dual-segment rollouts
Dual-segment rollout filenames carry <threadId>_<rolloutId>: the leading
UUID is the stable logical thread id (what session_meta.payload.id holds),
the trailing one is the rollout id of the replacement physical file. Such
files are minted by thread/revert (see openai/codex#38127); subsequent
resumes append to them. The sync path was writing the trailing rollout id
into proxy_request_logs.session_id, while the session manager keys threads
by the meta id - so usage rows of a reverted thread attach to an id that
never shows up in the session list.
No query consumes session_id today, so nothing is visibly wrong yet; but
the misattributed rows keep accumulating, and correcting them later would
require rescanning rollout files to rebuild the mapping - unrecoverable
once users delete those files.
Fix: parse_codex_file captures the normalized root meta thread id and the
insert path uses it as session_id. The trailing rollout id keeps its only
remaining role as the request_id prefix (codex_session:thread-v1:<id>:<n>;
event_index counts per file, so the trailing id is what prevents dedup-key
collisions across segments of one thread). Single-segment files are
unaffected: meta id equals the filename uuid there.
Tests: full-sync regression on a dual-segment rollout asserting request_id
contains the trailing rollout id while the stored session_id equals the
leading thread id.
QwenCloud is DashScope's international site — a separate deployment from
the existing Bailian presets, with its own domain, console and API keys.
It lands as new entries rather than a rename, so existing Bailian users
keep working unchanged.
Each of the three plans gets its own preset because their base URLs and
keys are not interchangeable; the vendor docs state that a mismatched
key/base_url pair fails with 401:
- Pay-as-you-go: dashscope-intl.aliyuncs.com
- Coding Plan: coding-intl.dashscope.aliyuncs.com
- Token Plan: token-plan.ap-southeast-1.maas.aliyuncs.com
Endpoints and model metadata are transcribed from the official client
docs. Two non-obvious details:
- The Coding Plan's OpenAI endpoint is a bare /v1 with no compatible-mode
segment, and only supports Chat Completions, so its Codex preset is
marked openai_chat rather than inheriting the Responses default.
- qwen3.8 models expose a 983616-token context window and accept only
low/medium/xhigh reasoning levels, so the Claude Desktop Token Plan
routes are deliberately not flagged 1M-capable.
c08040e92 declared xhigh for the grok-4.5 xAI presets but missed the
test expectations, leaving main's frontend checks red and dragging
every PR's Frontend Checks down with the same two failures.
* feat(presets): add Tencent Token Plan presets across six apps
Tencent Token Plan is the subscription line (product 1300/1823), separate
from the TokenHub pay-as-you-go marketplace already covered by the Tencent
Hunyuan preset: keys are scoped per site and the lines do not interoperate,
so domestic and intl endpoints are never mixed as candidates. Six products
land across six apps (Claude, Claude Desktop, Codex, OpenCode, Hermes,
OpenClaw):
- personal / enterprise pro / enterprise lite, each in domestic and
international (Singapore) variants, named with an "(Intl)" suffix —
clearer than the existing "en" suffix convention since these differ by
site rather than by language
- endpoints per the plan docs: api.lkeap.cloud.tencent.com/plan
(domestic personal), tokenhub.tencentmaas.com/plan (domestic
enterprise), tokenhub-intl.tencentcloudmaas.com/plan (intl)
- the Auto routing id is asymmetric: the domestic personal plan calls it
tc-code-latest, every other plan calls it auto (verified live — each
endpoint rejects the other's id)
Lineups, thinking switches and endpoints were verified with real API keys
against all four endpoints (2026-08-31, 124 live calls):
- cross-site calls all fail 401/403, confirming per-site key isolation
- thinking + reasoning_effort both take effect on /plan endpoints, so all
six Codex presets declare codexChatReasoning with per-model
reasoningLevels matching observed behavior: glm-5.3 always thinks and
accepts a strict low/high/max enum (medium/xhigh → 400),
kimi-k2.7-code(-highspeed) only accepts enabled, minimax-m2.5/m2.7 and
the domestic auto silently ignore thinking-off, the intl auto honors it
- /plan/v3/models exists only on the domestic personal endpoint, so the
modelsUrl override is set on that Claude preset alone
- minimax-m2.5 is gone from the plan doc tables but still listed by
/plan/v3/models and callable under the plans that cover it → included
there; kimi-k2.5 retired on 8/31 → excluded
OpenCode/Hermes/OpenClaw presets ride the tool-agnostic /plan/v3
OpenAI-compatible endpoint (no subscription-line integration pages exist;
the 1300/80642-80644 guides cover the marketplace line). OpenClaw entries
carry contextWindow from the platform model list and omit cost —
subscription plans have no per-token price. Reasoning levels are
registered in the codexReasoningLevelPresets whitelist (11 rows); the new
tokenPlanProviderPresets test locks endpoints, lineups, key URLs and the
OpenClaw key rebase across all six apps (43 tests).
* fix(presets): address review — OpenClaw official pages, Auto window, glm-5.3 default, region candidates
P1: OpenClaw entries now copy the five fields verbatim from the official
subscription-line integration pages (personal 1823/130062, enterprise
1300/81503) — all-zero cost, official contextWindow/maxTokens (Auto
196608/32768, glm-5 202752/16384, deepseek 1000000/384000 personal /
1048576/393216 enterprise, hy3-preview 262144/131072 reasoning:true),
replacing the previous platform-list derivations; maxTokens was the
consequential omission — OpenClaw defaults it to 8192, truncating long
completions. Models beyond those pages (minimax-m2.7, glm-5.1, glm-5.2,
hy3, kimi-k2.6, glm-5.3, glm-5-turbo, deepseek GA ids) get maxTokens
from the platform model list (1300/78934) with the boundary noted
in comments.
P2: the six Auto routing rows in the Codex catalogs now declare
contextWindow 196608 (the only official figure, from the OpenClaw
pages) — undeclared, the backend falls back to the 128K default and
every preset's default model ships with half its window.
P2: glm-5.3 rows declare defaultReasoningLevel "high" — its strict
low/high/max enum drops the template's medium default, which resolved
to canonical.last() = max, the slowest and most quota-hungry setting
on a fixed-budget plan.
P2: enterprise presets carry a second endpointCandidates entry for
their plan's other region — domestic Guangzhou primary + Singapore
(tokenhub-intl.tencentmaas.com), intl Singapore primary + Guangzhou
(tokenhub.tencentcloudmaas.com) — since both region tables state
cross-region calls are not supported, a customer on the other region
otherwise has no usable preset. Personal plans are single-region and
stay single-candidate.
Also widens the cross-site isolation test to distinguish site domains
(tencentmaas vs tencentcloudmaas) from the tokenhub-intl prefix, since
the same-site Singapore candidate now matches that prefix.
---------
Co-authored-by: zmq1121 <183985311+zmq1121@users.noreply.github.com>
u64::MAX as f64 rounds up to 2^64, so the > guard let exactly 2^64
through; the saturating cast then produced u64::MAX, which rounds back
to 2^64 and passes the round-trip check - silently rewriting the value
off by one. Guard with >= instead.
create_namespace_restore_sse_stream and restore_sse_block lost their
last caller when the xAI response path moved to the sanitize rewrite
stream; delete them instead of shipping #[allow(dead_code)].
- Remap unknown request models before sanitizing, so a subagent SKU
that lands on grok-4.5 gets its unsupported sampling fields stripped.
- Intersect union-branch required lists instead of unioning them, so a
flattened oneOf does not force fields the chosen branch lacks.
- Let grok-prefixed request models bypass the catalog remap: real Grok
SKUs the catalog has not caught up with must pass through.
Keep Grok sanitizers, agent_message rewrite, and unknown-model
remap behind one forwarder call so upstream rebases conflict
at a single gate instead of three inlined rewrites.
xAI 404s Codex role SKUs such as gpt-5.6-sol. Rewrite those
request models onto the provider's configured model, keep
catalog slugs like grok-4.5, and walk the full body for
agent_message items so collaboration 422s log at info.
Codex multi-agent v2 injects agent_message mailbox items that
xAI Responses cannot deserialize, so child spawns 422 before
any tool runs. Rewrite those items to ordinary user messages
on the native Responses path, flattening encrypted_content
parts to input_text and leaving field-deletion sanitizers
untouched.
xAI still rejects Codex Desktop tool schemas before sampling, and
Grok still emits whole-float JSON numbers that Codex serde refuses
as i32/u64. Collapse root oneOf/anyOf function parameters on the
request path (#6815) and rewrite integral floats on completed
function_call arguments.
Keep both transforms in transform_codex_responses_xai_sanitize.rs
with a thin native-Responses gate so the patch can be rebased or
cherry-picked until upstream covers the same cases.
Probed 2026-08-30 against native /v1/responses: grok-4.5 accepts
low/medium/high/xhigh and rejects max with HTTP 400 "Invalid reasoning
effort". Codex does not clamp out-of-catalog efforts, so the preset
list must match what the upstream actually accepts.
The v3.20.1 config-only switch refactor extended the keyless safety gate
to all write paths, which broke switching to proxy-managed OAuth cards
(xAI Grok OAuth): they are keyless by design — the local proxy injects
the real token per request — yet their preset snapshot inherited the
pre-0.149 template's requires_openai_auth = true, so the gate refused
the switch with provider.codex.config.official_auth_fallback.
Fix the snapshot instead of exempting the gate: force the flag to false
on the active custom table for proxy-injected OAuth providers
(uses_proxy_injected_oauth: xai_oauth, github_copilot — codex_oauth is
deliberately excluded, the official login IS its credential). The
neutralization hooks into build_effective_provider_for_live so
preflight, normal writes, and proxy backup/takeover projections all see
the same safe shape, and 0.149 resolves the provider as unauthenticated
without ever reading auth.json. The OAuth preset now emits
requires_openai_auth = false at the source; stored cards self-heal via
backfill on the next switch.
* fix(proxy): preserve mid-conversation system messages for prefix cache
Remove normalize_openai_system_messages which merged all system messages
to the head, breaking radix prefix cache on every turn when Claude Code
injects <total_tokens> metadata as a mid-conversation system message.
Now system messages from the top-level field are placed first,
and all messages (including mid-conversation system) are extended in
their original order without any merging or reordering.
Also add custom-protocol feature for standalone release builds.
* fix(proxy): restore top-level system merge, add regression test
Restore merging of top-level system array blocks into a single system
message (cross-turn stable, does not affect prefix cache). Mid-conversation
system messages from the messages array remain in place without merging
or reordering.
Add regression test to lock the new behavior: mid-conversation system
messages stay at their original position.
The session sync card sat in the page-level space-y-8 stack while the
pricing/maintenance cards below use space-y-4 inside the Accordion,
leaving an inconsistent 32px vs 16px gap. Group the sync card with the
Accordion under a shared space-y-4 wrapper so all three cards align.
Also show the Sync Now button only when auto-scan is off: in auto mode
the background timer already covers scanning, so the manual entry point
is redundant there.
When an external truncation or rewrite pins the cursor to EOF, the
skipped range is permanently dropped (replaying it would double-count
entries whose detail rows were already pruned into the rollups). That
was only logged; manual sync showed plain success. Surface it through
the result's errors instead — deliberately not deferred_files, whose
semantics promise a retry next round that will never happen.
Follow-up hardening on the byte-cursor scan:
- session_log_sync gains last_tail_fingerprint (added to the still
unreleased v17->v18 migration): a hash of the bytes just before the
cursor boundary. On each incremental read the stored fingerprint is
verified against the file, catching same-size or larger rewrites
that a size check cannot see. The verification read doubles as the
seek to the cursor and seeds the rolling tail buffer, so the append
fast path costs no extra IO.
- truncation and fingerprint-mismatch rewrites now pin the cursor to
the current EOF instead of rescanning from byte 0. A rescan would
re-import entries whose detail rows were already pruned into the
daily rollups, double-counting them forever; skipping the rewritten
range loses at most externally-modified lines, which is the lesser
harm. Appends after the pin resume normal incremental reads.
- mid-file IO errors are now reported through the sync result
(errors + deferred count) instead of silently returning success;
manual sync has no next round to quietly retry in.
Replace the line-number cursor with a byte offset so unchanged file
prefixes are skipped via seek instead of re-reading whole files each
round (12MB active file: 6.04s full scan -> 9.3ms incremental), and
prefetch all sync cursors in one query per round across the five
file-based importers instead of one row lookup per file.
Details:
- session_log_sync gains last_byte_offset via a standalone v17->v18
migration (v17 already ran on live databases; appending the column
to it would leave them permanently missing the column)
- cursor only advances past complete (newline-terminated) lines; a
parseable unterminated tail is imported but deferred, fixing the
legacy bug where a half-written line was counted and then skipped
forever once completed
- legacy line cursors are converted by skipping N lines byte-wise,
never by full rescan: rollup_and_prune deletes detail rows that
request_id dedup relies on, so a rescan would double-count pruned
history into the rollups
- load_sync_cursors now returns Result and aborts the round on
failure instead of falling back to an empty map (same rescan
double-count hazard); Pi reports the error through its result
- on mid-file read errors the cursor keeps the old mtime so the next
round actually resumes from the committed offset
- imports and cursor updates commit atomically in one transaction
Verified with a frozen snapshot replay (1017 files / 409MB): old and
new code produce identical aggregates.