Commit Graph
3222 Commits
Author SHA1 Message Date
Jason d9422e7d6a fix(omp): install and update Oh My Pi without assuming Bun
omp also ships as a prebuilt binary (official installer, Homebrew), and
its npm package only runs under Bun. Install now runs the official
installer, which uses Bun when present and the verified binary otherwise.
Updates go through `omp update`, which routes every install source
itself: Homebrew keeps `brew upgrade`, Bun keeps `bun add -g` as the
fallback, Nix is left to the user, and npm is never offered. The
installer's binary is now recognized for uninstall.
2026-10-24 15:30:32 +08:00
Jason 9bacd1d172 fix(omp): allow project images in Oh My Pi session previews 2026-10-24 15:30:32 +08:00
Jason f1d43ebaab docs(readme): list Oh My Pi among the managed tools 2026-10-24 15:30:32 +08:00
Jason b34753b832 feat(omp): manage Oh My Pi global prompts
Oh My Pi reads its user-level instructions from AGENTS.md in the
profile-scoped agent directory, so prompts now project there like Kimi
Code's. Drop the prompt-sync skip and the stale deep-link notes that
assumed there was no prompt panel.
2026-10-24 15:30:32 +08:00
Jason e88a9fa417 refactor(omp): read Oh My Pi sessions through the Pi session reader
Oh My Pi keeps Pi's session tree format, so the Pi reader now takes a
small dialect (provider id, root resolution, resume command, the leading
title slot and the combined provider/model selector). The omp provider
keeps only its root resolution, resume command and artifact cleanup on
delete, and listing no longer renders every transcript to find a title.
2026-10-24 15:30:32 +08:00
Jasonandallenxu09 a03c6c7a3c refactor(omp): reuse the Pi provider form and plumbing for Oh My Pi
Drop the copied OmpProviderForm and the YAML editor; Oh My Pi now uses
PiProviderForm with an omp mode (no provider-level name, no
thinkingLevelMap, plus a discovery type selector). Presets are derived
from the Pi catalog. Pi-family helpers (isPiFamilyApp, parametrized
current-state queries and usage-script command) replace the scattered
pi/omp branches, and omp error keys reuse the Pi error translator.

Co-authored-by: allenxu09 <s23372.xu@stu.scie.com.cn>
2026-10-24 15:30:32 +08:00
Jasonandallenxu09 3d7cf6aa32 refactor(omp): port #8308's text-level models.yml editor and share Pi plumbing
Replace the passthrough/splice YAML layer with a single omp_config.rs:
#6747's verified agent-dir resolution (profiles, PI_CONFIG_DIR, XDG data
dir) plus #8308's block-level editor that rewrites only the one provider
entry, refuses anchors/tags/flow style instead of re-emitting them, and
rolls the file back byte for byte when the database commit fails.

- Validate only the node being written; model-level `api` also accepts
  omp's runner APIs.
- Drop ProviderMeta.ompPassthroughYaml, validation.rs and omp_state; the
  current-state command lives in commands/omp.rs and reuses PiCurrentState.
- Usage scripts go through the Pi command with an `app` argument.
- MCP reuses Pi's transport helpers; mcp.json writes are revision checked.
- Skills follow Pi: no DB column, state derived from <agent>/skills, and
  uninstall keeps an unverifiable same-name directory in either app.
- Move enabled_omp to a new v22 -> v23 migration (v22 shipped in 4.0.19)
  and add it to mcp_servers only; revert the .gitignore additions.

Co-authored-by: allenxu09 <s23372.xu@stu.scie.com.cn>
2026-10-24 15:30:32 +08:00
Jason 2afe8cdbef refactor(omp): rename the internal app id from ohmypi to omp
Match the binary name, the Apps page tool id and the i18n keys that
already used omp. The app is unreleased, so nothing persisted needs a
migration; ohmypi stays accepted as an alias when parsing app ids.
Show the app by default like Pi and Kimi Code.
2026-10-24 15:30:32 +08:00
JackieJKandallenxu09 c8945d2d91 feat(omp): add Oh My Pi support
Squashed from #6747 (head 50c3cec1d) onto current main: providers in
models.yml, MCP in mcp.json, Skills, sessions, app visibility and
directory override for Oh My Pi. Conflicts with the Kimi Code refresh
refactor in App.tsx and the locale files were resolved by keeping both
sides and routing Oh My Pi through refreshAdditiveLiveState.

Co-authored-by: allenxu09 <s23372.xu@stu.scie.com.cn>
2026-10-24 15:30:32 +08:00
Jason d8983bb10b docs(release): add v4.0.19 release notes v4.0.19 2026-10-23 20:13:26 +08:00
Jason a043b1f863 chore(release): v4.0.19 2026-10-23 20:13:26 +08:00
Jason bada9a4bb9 fix(models): don't borrow a vendor-hosted entry that points at another vendor
When a relay's model is not recognized by address, the metadata lookup
falls back to the vendor's entry: first the entry that same-name entries
point at via canonical_model_id, and only if none resolves, an entry
listed under a provider that some canonical id points at.

That last step only checked whether the provider counts as a vendor, not
whether the entry claims to be its own. models.dev has no
deepseek/deepseek-v4.1-flash-1017, so the ~40 entries pointing there all
miss, and thinkingmachines (counted as a vendor for its own models) won
with its LoRA fine-tuning entry DeepSeek-V4.1-Flash-1017:peft:262144,
whose canonical id points back at deepseek. It is text-only with a 262144
window, so picking the model filled every app with text-only input and a
quarter of the real window; Codex additionally turned image input off.

Skip entries whose canonical id names another vendor in that fallback.
Against the current models.dev data, 27 ids stop borrowing from hosts of
other vendors' models (thinkingmachines, NVIDIA, vivgrid, alibaba) and
leave the fields empty instead, and laguna-xs-2.1 now resolves to
poolside's own entry rather than being ambiguous. Skipping ':' variants
instead was rejected: it fixes this id but newly resolves nine
thinkingmachines entries carrying 65K training windows (gpt-oss-120b and
others).

Refs #8765, #8682
2026-10-23 20:11:53 +08:00
Jason e53ca7e820 fix(usage): tighten request log column spacing
The request log has more columns than the other usage tables, so at the
default window size the last column (first token) was cut off and the
table had to scroll sideways. Its columns are already sized to their
content, so the model column had no slack to give back.

Narrow the gaps for this table only: 16px between columns (was 24px) and
32px between column groups (was 40px), keeping the 16px outer edges. The
table shrinks by 72px with the same rows; model names are not truncated.
The provider, model and pricing tables keep their spacing.
2026-10-23 17:37:41 +08:00
Jason 50e091b055 fix(usage): step 7/14/30-day presets back by calendar days across DST
getPresetLookbackStart subtracted (N-1) * 24h from the current instant
and then rounded to local midnight. Across a DST offset change that
subtraction can land on the adjacent date, so the 7d/14d/30d presets
included one extra local day after spring-forward and dropped one after
fall-back (e.g. America/New_York, Europe/Berlin). The shifted range fed
the usage summary, model/provider stats and request-log queries.

Build the start date from today's local year/month/day minus (N-1) days
instead, which yields the first instant of that calendar day, including
zones where midnight itself is skipped by DST. The 1d rolling window,
today, all-time and custom ranges are unchanged.

Refs #8719
2026-10-23 17:37:41 +08:00
Jason 9cb6b6d7fa fix(agy): run background agy commands without a console window on Windows
Refreshing the Antigravity official quota on Windows popped up a black
console window. Outside macOS the quota falls back to spawning
`agy --print /usage`, and the model list in the provider form spawns
`agy models`. The release build is a GUI-subsystem app, so a console
child spawned without CREATE_NO_WINDOW gets its own console window.

Both spawn sites now go through agy_config::background_command, which
sets CREATE_NO_WINDOW on Windows like the other background tool probes.
Behaviour on macOS and Linux is unchanged.

Refs #8700
2026-10-23 17:37:41 +08:00
Jason 10e0aa2380 fix(deeplink): don't let a finished import close a newer link's confirmation
While provider link A was still importing (awaiting the backend import or
the follow-up cache refresh), a second link B could arrive, replace the
displayed request and be acknowledged in the backend. When A's import
completed, handleImport unconditionally called setIsOpen(false), closing
B's confirmation even though B was never imported and could no longer be
fetched again.

Track the request currently shown in a ref (set together with setRequest
in showPending) and close the dialog after an import only if it still
shows the request that import started with. Add a regression test that
holds A's import, delivers B, then checks B stays open and still imports
and closes on its own.

Refs #8696
2026-10-23 17:37:41 +08:00
Jason 7ecce7c79e fix(proxy): carry encrypted-only reasoning as empty thinking for Claude clients
Claude Code routed to an OpenAI Responses upstream showed "Unsupported
content type: redacted_thinking" before tool calls in the VS Code
extension. Reasoning items that only carry encrypted_content (summary is
empty) were converted into redacted_thinking blocks, which the extension
cannot render.

Emit them as an empty thinking block whose signature holds the reasoning
envelope instead, the same shape Anthropic returns for omitted thinking.
Replay already decodes thinking.signature and redacted_thinking.data the
same way, so replayed upstream requests are byte-identical and histories
written by older versions keep working.

Also recover summary text that never arrived as deltas: when
output_item.done carries a summary that strictly extends what was
streamed (e.g. a later summary part had no deltas), emit only the
missing tail before the signature. Snapshots that do not extend the
streamed text are ignored, so nothing is re-emitted.

The upstream request is unchanged; reasoning.summary is not injected.

Refs #8691
2026-10-23 17:37:41 +08:00
Jason 2fbd9c70f4 fix(hermes): launch the dashboard inside WSL when Hermes lives there
On Windows, pointing the Hermes config directory at a WSL path
(\\wsl$\<distro>\... or \\wsl.localhost\<distro>\...) only redirected
config, sessions and the version probe. When the Web UI was offline, the
"launch dashboard" action still opened a Windows terminal running
`hermes dashboard` natively, so it started the Windows hermes (or failed
with command not found) instead of the instance inside the distribution.

Build the dashboard command line from the existing per-tool WSL distro
detection: for a WSL override run it as
`wsl.exe -d <distro> -- sh -c "\"${SHELL:-sh}\" -lic 'hermes dashboard'"`
so the user's login shell PATH (e.g. ~/.local/bin) applies. Only one
shell flag is used, since a Ctrl+C exit is non-zero and a fallback retry
would relaunch the dashboard. The Web UI probe on 127.0.0.1 is unchanged;
WSL2 forwards localhost to the distribution. Other platforms keep the
plain `hermes dashboard`.

Refs #8689
2026-10-23 17:37:41 +08:00
Jason b5aa7fbbcb fix(agy): show Gemini quota when querying Google directly
Official Antigravity cards on macOS often showed only the Claude and GPT
quota. Google's retrieveUserQuotaSummary decides what to return from the
User-Agent: "cc-switch/<version> (antigravity)" passes the license check
but gets only the Claude and GPT group, and the Gemini group is returned
only to "antigravity/cli/<version>" from 1.2.0 on. agy's own /usage sends
that, which is why the fallback path always showed all four tiers.

The User-Agent is now "cc-switch/<version> (antigravity/cli/<agy
version>)". The version comes from the local "agy --version", probed once
per process, and falls back to 1.4.6 when agy cannot be found or read.
2026-10-23 17:37:41 +08:00
Jason 3494d0d100 chore(presets): add V-API backup endpoints
List V-API's mirror lines (api.vveai.com, run.v3.cm, api1.v3.cm,
api2.v3.cm, us.gpt.ge) as endpoint candidates after the main
api.gpt.ge in the Claude Code, Claude Desktop, Gemini CLI, Codex and
Grok Build presets, with /v1 on the OpenAI-format apps.
2026-10-23 17:37:41 +08:00
Allen Xu e5b04221ff fix(subscription): select Claude Keychain credentials by account (#8764) 2026-10-23 17:35:05 +08:00
ClarkChenandClaude Code c3ad9bb0bd fix(model-fetch): try compatible-mode models endpoint for /apps/anthropic base URLs (#8755)
Qianwen / Bailian Token Plan Anthropic endpoints (e.g.
https://token-plan.cn-beijing.maas.aliyuncs.com/apps/anthropic) return 404 for
/v1/models and /models; the model list is only served under the
OpenAI-compatible path {root}/compatible-mode/v1/models. Append it as the last
candidate when the base URL ends with /apps/anthropic.

Co-authored-by: Claude Code <noreply@anthropic.com>
2026-10-23 17:34:54 +08:00
streng during 2e1e29795a fix(proxy): recognize Codex session header aliases (#8718) 2026-10-23 17:34:44 +08:00
微尘 52652a02da fix(sessions): make the project group header clickable across the row (#8709) 2026-10-23 17:34:34 +08:00
秋澪Akimio c8f85674f4 fix(mcp): render server display name in unified MCP panel (#8692) 2026-10-23 17:34:24 +08:00
Jason e85ade714a chore(presets): swap the FluxA TokenPlan and Shengsuanyun sponsor positions
Move FluxA TokenPlan up to Shengsuanyun's former slot after V-API and
Shengsuanyun down to FluxA's former slot after SubRouter, in the four
README sponsor tables and the seven preset files that carry both.
2026-10-23 17:05:55 +08:00
Jason 91d3ed101d feat(presets): add V-API sponsor presets across nine apps
Add V-API (gpt.ge) as a full sponsor placed after BuyToken: README rows
and banner in all four languages, the partner promotion copy, an inline
tile icon, and presets for Claude Code, Claude Desktop, Codex, Gemini
CLI, Grok Build, OpenCode, OpenClaw, Hermes and Pi. The gateway prices
and gates models per token group, so each preset notes which group it
needs. It has no undated claude-haiku-4-5, so the Desktop Haiku tier is
mapped to claude-haiku-5-5.
2026-10-23 16:57:28 +08:00
Jason 069c7fac19 feat(presets): add BuyToken sponsor presets across eight apps
Add BuyToken as a full sponsor placed after DIT.ai: README rows and
banner in all four languages, the partner promotion copy, an inline tile
icon, and presets for Claude Code, Claude Desktop, Codex, Grok Build,
OpenCode, OpenClaw, Hermes and Pi. Keys are bound to a model group, so
each preset notes which group's key it needs. The gateway only accepts
the dated claude-haiku-4-5-20251001 ID, so the Haiku tier is mapped to
it. There is no Gemini preset because /v1beta is not served.
2026-10-23 16:28:46 +08:00
Jason 717172bd6b chore(presets): point DIT.ai links at the ccswitch referral URL
Replace the placeholder /register link with
https://dit.ai?utm_source=ccswitch in the nine DIT.ai presets and the
four README sponsor rows.
2026-10-23 16:11:02 +08:00
Jason 03707c400d style(pi): use the shared required-field asterisk
The provider key, the model ID and display name column headings, and
the context window and max output labels now use REQUIRED_LABEL like
the rest of the provider forms, instead of a hand-written "*" span.
The asterisk is drawn by CSS, so it no longer becomes part of the
label text that screen readers and tests read.
2026-10-23 15:50:15 +08:00
Jason 1e2559ef9e feat(pi): always show the provider key and flag duplicates
The provider key names the provider node in models.json, so it is now
shown for presets too (prefilled, editable) instead of only for custom
and edited providers, matching Kimi Code. A key that is already used
by another Pi card is flagged under the field and blocks saving, so
adding the same preset twice no longer fails only at the backend.
2026-10-23 15:49:22 +08:00
Jason 0f76829001 feat(kimi): lay out the provider editor like Pi and OpenCode
Models are now one row each (expand arrow, model ID, display name,
delete) with capabilities and the context window in the expanded
details, instead of one large card per model. Fields follow the same
order as Pi and OpenCode (API format, API key, base URL), the API
format menu shows readable protocol names, request headers are shown
inline, and required fields use the shared asterisk style.

The new display name column writes the model's display_name, which
Kimi Code shows in /model; clearing it removes the field. The model
selection hint moves into a "?" beside the models heading, and the
Kimi-only labels that duplicated OpenCode's are removed from all
locales.
2026-10-23 15:48:09 +08:00
Jason d6db0c7570 fix(providers): stop the advanced options toggle from jumping when pressed
Small buttons shrink slightly while pressed. On the full-width,
left-aligned "Advanced options" toggle the shrink is centered, so the
label visibly shifts. Keep these toggles at full size in the Claude,
Codex and Kimi Code forms.
2026-10-23 15:15:19 +08:00
Jason b96ff61821 feat(kimi): move request headers into collapsible advanced options
Custom headers are rarely needed for Kimi Code providers, so keep them
under an "Advanced options" section like the Claude and Codex forms.
It opens by itself when headers are already set, from an existing card
or a preset that carries them.
2026-10-23 15:15:19 +08:00
Jason 8969264b96 feat(kimi): explain the less obvious model capabilities
"Always thinking" and "Load tools on demand" don't say what they do.
Add a "?" help tip next to each in all four locales: the first is for
models whose thinking can't be turned off, the second only works with
Kimi's own models and needs KIMI_CODE_EXPERIMENTAL_TOOL_SELECT. Move
"Always thinking" next to the last one so the common three come first.
2026-10-23 15:15:18 +08:00
Jason 535d12831f fix(models): match old OpenCode Zen/Go addresses to the moved models.dev entries
models.dev moved OpenCode Zen and Go to /inference/... endpoints, while
presets and existing providers still use /zen/... (both work and bill
the same). The endpoint lookup no longer found them, so models only
listed there, such as deepseek-v4.1-flash, filled nothing. Map the old
normalized addresses to the new ones; this applies to every app's fill.
2026-10-23 15:15:18 +08:00
Jason b61e2767c0 feat(kimi): fetch the model list and fill parameters from models.dev
The Kimi Code form only let users type model IDs and context windows by
hand, unlike the other apps' forms. Add the same "Fetch models" picker
and per-row dropdown: picking models adds them with tool use enabled,
then fills the context window, thinking and image input from the
matching preset or models.dev entry (the endpoint decides which). A
window the user typed is never overwritten; rows still on the
placeholder window take the looked-up value. Anthropic and Gemini
providers fetch with their own auth header.
2026-10-23 15:15:18 +08:00
Jason 93e33d110a fix(apps): drop the redundant not-installed error line under uninstalled tools
The version column already says the tool is not installed, so the extra
"not installed or not executable" caption repeated it in English. A plain
not-found probe now reports no error; WSL failures with real diagnostics
(invalid shell, exec failed) still surface.
2026-10-23 11:46:49 +08:00
Jason 1c52fa918d fix(providers): reread additive client state when a removal is refused
Kimi Code saves every /model pick as default_model, so the default shown
in CC Switch can go stale while the page stays open. The backend rereads
config.toml and refuses to remove the provider Kimi Code is using, but
the page kept showing the old default and an untranslated English reason.

Reread the additive app's live state after a failed removal as well as a
successful one (now including the OpenCode, OpenClaw and Hermes default
model queries), reread Kimi providers after failed edits and deletions,
and explain the two Kimi "model in use" refusals in all four locales.
2026-10-23 11:42:12 +08:00
Jason c6f85464e7 style(usage): restore the original trend chart and heatmap colors
Point --chart-usage back at --chart-1 and restore the heatmap's darkest
light-mode step (#0e7490), so the usage charts look as they did before
#8711. The separate token stays, so a later color change does not touch
the quota bars that also use --chart-1.
2026-10-23 11:21:04 +08:00
Jason 29f8bef378 fix(usage): clamp the request log page when the total shrinks
When the user sat on page 2+ and the total dropped (the "today" window
rolled over, Codex usage was rebuilt), the page index stayed out of
range. With a single page the pagination bar is hidden, leaving an
empty table with no way back. Pull the page back to the last valid one
once real (non-placeholder) data arrives.
2026-10-23 11:15:39 +08:00
Jason f4e9211a11 fix(panel): let Escape close only the topmost full-screen panel
Each FullScreenPanel adds its own keydown listener on window, and
stopPropagation does not stop other listeners on the same target, so
stacked panels (pricing -> pricing editor, edit provider -> endpoint
speed test) all closed on a single Escape. Track open panels in a
module-level stack and only let the top one react.
2026-10-23 11:15:39 +08:00
Jason 7cc3141cae style(providers): mark additive default providers in blue
The default provider in OpenCode, OpenClaw, Hermes and Kimi used a gray
card and dot. Use the same blue as an in-use switch-mode card instead,
and drop the now-unused neutral card tone.
2026-10-23 11:15:35 +08:00
Jason d6a2324196 feat(kimi): give provider cards real add/remove actions
Kimi is additive, but both card groups only offered "Switch", which
wrote the provider into config.toml and changed default_model at once,
so the Added / Available split did nothing.

- Available cards get "Add": a new add_provider_to_live_config command
  writes the provider and its models into config.toml without changing
  default_model, so they show up in Kimi's /model list. A node already in
  config.toml belongs to Kimi and is never overwritten from the archive.
- Added cards get a "Remove" button; "Set as default" (select the
  provider's model) moves into the more menu, as for OpenCode/OpenClaw.
- The provider owning default_model is marked with the gray "Default"
  status like OpenCode, and cannot be removed.
2026-10-23 11:15:35 +08:00
Jason 44e0fc9630 feat(kimi): name separate official accounts "Kimi 订阅 <nickname>"
Kimi Code shows a provider's key as its name in the model picker, so
separate accounts appeared as "kimi-code-ccs-<hash>". New separate
accounts are now keyed "Kimi 订阅 <nickname>" ("Kimi Subscription" when
CC Switch is not in Chinese), matching Kimi's own label for its default
login and limited to the characters Kimi accepts, numbered on clashes.

The credential slot and model aliases keep the stable
"kimi-code-ccs-<hash>" name, so default_model stays an ASCII key and
ownership checks are unchanged. A returning account is also recognised
by its credential slot, not only by the local identity cache. Card names
fall back to the readable key when no nickname is cached.
2026-10-23 11:15:35 +08:00
Jason c49c2be94b refactor(kimi): render Auth Center accounts with the shared account group
Kimi's account list drew its own layout (inline description, a permanent
region picker, highlighted current row, extra badges). It now uses the
same ManagedAccountsGroup as Copilot, ChatGPT, xAI and Antigravity:
help behind "?", region chosen when adding an account, "In use by Kimi"
badge, nickname and avatar, shared pending/error blocks, and a
"needs re-login" state for signed-out accounts.

The shared group gains optional per-row remove label and disabled
reason, an optional "remove all" action, and a looser device-code type.
Account action failures now toast instead of reusing the login error
block. Unused Kimi strings are removed from all locales.
2026-10-23 11:15:35 +08:00
Jason e12415b5ba refactor(providers): move OpenCode/OpenClaw set-default into the more menu
The icon-only star on added OpenCode/OpenClaw cards was hard to read and
had no selected state. "Set as default" now sits at the top of the card's
more menu, like the aggregation page; providers with several models open
a submenu to pick the default model. The card keeps only the remove
button, and the primary button no longer needs its own dropdown.
2026-10-23 11:15:35 +08:00
Jason cdd2190df2 feat(kimi): identify, remove and show quota for official accounts
Follow-up to #8726, which lets the Authorization Center hold several Kimi
official subscriptions, each with its own provider and credential slot.

Identity: after sign-in (and for logins found on disk) read /me to show
the account's nickname on its row and card. Signing in to an account that
is already listed writes back to its slot instead of adding a duplicate,
and the slot is chosen when the login completes, so a native Kimi login
made meanwhile is never overwritten. New separate slots are named from a
hash of region and user id, stable across devices. Opening the Kimi page
reads identities too, so card names carry the nickname without a visit to
the Authorization Center first.

Remove instead of sign out: a separate account is deleted together with
its provider node, models, credential and card (refused while in use);
web search/fetch pointing at it moves to Kimi's own default login. Kimi's
own login is only hidden from the list - CC Switch never logs Kimi out -
and stays hidden across Kimi's token rotations until the user signs in to
it again here. An empty list caused by that says so instead of "signed
out".

Quota: each account row and official card shows its quota. Tokens close
to expiry are refreshed under the same proper-lockfile-compatible lock
Kimi uses (mkdir <slot>.lock, 5 s stale takeover), with Kimi's revoked
token recovery. Windows, where Kimi takes no lock, only reads.

Also: re-login keeps custom fields such as custom_headers on the node;
deleting a card whose node was removed elsewhere deletes the leftover
CC Switch-owned credential; the card's switch button works for official
accounts after checking they are signed in on this machine; account rows
use the same compact layout as the other Authorization Center groups.
2026-10-23 11:15:35 +08:00
Jason e88ff5404d test(usage): scope Codex parent-cache count assertions to each test's dir
The parent_rollout_* tests asserted on the total size of the process-wide
CODEX_REPLAY_CACHES.parent_timelines map. #[serial_test::serial] only
excludes other serial tests, while non-serial tests such as
test_parse_codex_file_recognizes_branch_thread_paginated_continuation
reach load_parent_timeline (via resolve_continuation_baseline) and insert
their own entries concurrently. Under multi-threaded `cargo test` that
occasionally turned an expected count of 1 into 2; nextest's
process-per-test model hid it in CI.

Count only entries under the test's temp dir instead. Production code is
unchanged.
2026-10-23 11:15:35 +08:00
Jason 0640c6fcc7 fix(codex): never turn image input off when filling fetched model metadata
Picking a model from the fetched list fills its empty catalog fields from
known metadata (same-endpoint preset, models.dev). For a model the lookup
reported as text-only, the Codex fill wrote inputModalities ["text"].

For Codex an absent inputModalities is not "unknown = text": the backend
checks the exact text-only registry and otherwise fails open to
["text", "image"]. Writing ["text"] therefore switched image input off,
against the fill rule that modalities are only ever turned toward
"supported", and the Codex form has no control to switch it back, so
Codex kept refusing image attachments for that model.

Only fill inputModalities when the metadata includes image; otherwise
leave it absent and let the registry and fail-open default decide.

Refs #8682
2026-10-23 11:15:35 +08:00