* refactor(llm-deepseek): share the /models listing machinery
Move the account route's `/models` request and decoding, its
credential-scoped listing cache, and listed-catalog resolution with the
version gate into the shared DeepSeek Messages library, so a second
route can follow the same listing. Names drop the account prefix, the
cache takes each access's endpoint settings with the call, and it gains
`invalidate()` for a forced reload. `isBuiltInCatalog()` recognizes the
catalog an uncustomized `models` setting resolves to.
The account route keeps its behavior; only its imports and the
override diagnostic wording ("model listing") change.
* feat(llm-deepseek-api-key): follow the official /models listing
While `models` equals the built-in catalog and the Messages origin is
https://api.deepseek.com, `deepseek-official` serves the official
`GET /models` listing, authenticated with the API key in `x-api-key`.
Loading, refresh, fallback, merging, and the client-version gate match
the account route: the first access waits up to `modelsTimeoutMs`, a
listing older than `modelsMaxAgeMs` refreshes in the background on the
next access, a failure keeps the last good listing or the built-in
catalog, and listed models this build cannot serve are refused before
network I/O.
A customized `models` list replaces the catalog and requests nothing.
Switching between a customized catalog and the listing discards the
cached listing, so Restore defaults reloads it on the next access.
Suites that boot the route on the official endpoint now answer the
listing themselves instead of reaching the network.
* docs: describe the official route model listing
Document when `deepseek-official` follows the official `/models`
listing, its timing fields, customization precedence, and the forced
reload after Restore defaults in the API-key README, the shared
transport README, and the provider guide. Record the decision in a new
Agent Note, keep the account note's statement of where the shared
listing code lives current, and regenerate the configuration catalog.
* test(llm-deepseek-api-key): pin the once-per-listing hidden-entry diagnostic
* docs: regenerate module and event graphs for the shared listing
* fix(llm-deepseek-api-key): report credential failures on the model request
On the official endpoint, `connect` waited for the listing view, whose
credential lookup rethrew a malformed key or a credential-store failure
before the agent loop recorded the prompt. A failed lookup now resolves
the connection from the current listing view, so `resolveAuth` reports
the failure on the model request and the turn keeps the prompt and the
failed attempt. A caller abort still rejects.
* docs(llm-deepseek-api-key): name which hidden listing entries are logged
* test: keep unit suites off the official model listing
Suites that boot the DeepSeek routes with a dummy or inherited key now
request `GET https://api.deepseek.com/models` on the official endpoint.
A unit-lane setup file rejects that request at once, so such a suite
serves the built-in catalog instead of reaching production or waiting
out the listing deadline on a network that never answers; suites that
exercise the listing answer `fetch` themselves. Other requests pass
through unchanged.
* feat(llm-deepseek): read tool_update from the model listing
Both DeepSeek routes now take a listed model's Messages tool update mode
from `api_capabilities.anthropic_messages.tool_update`, alongside the
system prompt mode. `in-history` and `addition-only` replace the
bundled mode; any other value reads as `leading-only` and removes it.
An absent or `null` mode keeps the bundled one, so a listing that does
not declare the field leaves `deepseek-flash` on `addition-only`.
`system_prompt_update: null` likewise keeps the bundled mode now; it
previously read as `leading-only`, which a serializer writing empty
values as null would have turned into a cache-unfriendly downgrade.
* fix(llm-deepseek): sync the built-in context window with the official listing
The official `/models` listing declares a 1,048,576-token context window
for `deepseek-flash` and `deepseek-v4-pro`; the built-in catalog said
1,000,000, a decimal-versus-binary mismatch. Both built-in entries now
carry the listed value, so offline starts and listing failures size the
context the same way the listing does. The Models page shows the exact
count for these rows, since 1,048,576 has no decimal K/M spelling.
`defaultContextWindow`, the fallback for model ids the catalog does not
describe, stays 1,000,000.
* test(llm-deepseek-api-key): reject the stubbed listing request with an Error
* test(cli): keep the built SDK smoke off the official model listing
The SDK profile's initialize now resolves the deepseek-official catalog,
which requests GET https://api.deepseek.com/models on the official
endpoint. With the smoke's fixture key that request answers 401 and the
route warns on stderr, so the test's empty-stderr assertion failed in the
e2e and snapshots lanes. Point the run at a loopback mock and assert it
received no model call.
* feat(llm): carry /models badges into the model catalog
The shared DeepSeek listing decoder reads an optional per-entry
`badge: { text, tone }`. Text is one literal or translations keyed by
lowercase language id with a required English entry, each trimmed to 1
to 12 characters on one line; tone is neutral, info, success, or
warning, and an absent or unrecognized tone reads as neutral. An absent
or null badge declares none; an unreadable one is dropped with a listing
problem and never hides its model.
resolveListedCatalog keeps the badges of offered models, both DeepSeek
routes attach them through catalogModelInfo, LlmRuntime.listModels
detaches them, and the Host model catalog forwards them as
ModelCatalogModel.badge. resolveModelInfo omits the badge, so no model
request, Session event, or model-facing tool carries it.
* feat(client): render catalog badges in the model selector
The composer model seat shows a catalog model's badge after its name in
the trigger, the root-menu value, and each option: a Tag in the badge's
tone whose text is resolved for the active locale through
ctx.locale.resolveText. A conversation.input.model.label contribution
replaces the name and badge together and receives the badge in its
owner props.
Remove ui-deepseek-model-label-1017, which hard-coded `1017` beside
DeepSeek-V4.1-Flash for two providers, and its Web bundle row. The badge
now comes only from the model listing, so the build ships none of its
own; the upgrade guide covers profile patches that still target the
removed row. The Web e2e replaces the fixed-badge case with replay
catalogs that declare literal and translated badges, and the replay
adapter validates and advertises a configured badge.
* feat(llm-deepseek): read model badges from client_metadata
Each /models entry now declares its selector badge under
client_metadata["deepseek-harness"].badge, keyed by the User-Agent product
token like client_requirements, so the public model fields stay unchanged
and later DeepSeek Harness fields join the same object. Every field under
client_metadata is ignorable: null at any level shows no badge, and a level
that is not an object drops only the badge with a diagnostic naming it.
* docs: record why the listing badge container is client_metadata
* fix(models): refresh listed badges in open selectors
* refactor(models): share listing cache lifecycle wiring
* docs(models): refresh listing event producer catalog
* fix(models): retain badges when listing refresh fails
* fix(models): validate badge text and preserve tags beside long names
* test(models): cover account listing automatic badge removal
* fix(forge): make reference and worktree tests portable
* test(worktree): cover error and text cancellation reasons
* test(forge): brace void arrow bodies in the reference lookup spec
* test(ci): expect the joined version script path in the pre-merge check
---------
Co-authored-by: imccyu <[email protected]>
The scan now reads exactly the manifests the release tooling rewrites, names
every directory and file it cannot read, and refuses an empty corpus; the
command takes the root it checks so the exit code is tested, and the workflow
reports a crashed check as a warning in report mode.
Each step is a spawn whose stdio reaches the job log, the first failure stops
the list, and the script decides from CHECK_MODE whether to annotate as an
error or a warning, so the workflow only calls it.
The missing-script branch now fails in block mode instead of passing, the shell
block parses, the version walk refuses a directory it cannot read or one that
holds no manifest, the install is watched before anything else can throw, and a
spec runs `bash -n` over every Policy shell block.
`Policy / Pre-Merge Check` now runs `scripts/pre-merge-check.ts` under plain
Node and knows nothing else about the merged tree. That script installs the
dependencies, checks that every released manifest carries the root version
while the install runs, and builds the Host and Client contracts, reporting
which step failed.
* fix(ci): keep the pre-merge check off the report-mode failures
* fix(ci): fail the pre-merge check in block mode without a base commit
* fix(ci): report every pre-merge failure the way its mode asks
The merge step now separates a base commit it cannot fetch from a merge it
cannot do, both failures print an annotation, and a spec pins the mode
wiring of the three Policy workflows.
* test(ci): type the Policy workflow assertions
---------
Co-authored-by: imccyu <[email protected]>
Move the release verifier's npm platform check into scripts/npm-platform.ts and use it when pruning pnpm's bundled packages, so negations, 'any' and invalid fields follow npm. Skip non-directories and name the manifest on invalid fields.
pnpm's npm package carries reflink bindings for darwin-arm64, darwin-x64, win32-arm64 and win32-x64. The primary runtime copied all of them, so the Apple silicon app contained an x86_64 Mach-O module that triggers macOS's Intel app warning. Prune packages whose os/cpu fields exclude the target and bump the payload format.
Brings the Session list branch up to date with the forge panel, whose
held messages now wait at inbox insertion.
- client-ui-forge keeps the Session list's remote-access import and the
panel's messageOf; the agent-controller import is gone.
- The README and the forge panel Agent Note combine both sides: the
forgeWorktree projection keeps its client view next to the inbox hold,
and the Session list paragraphs stay.
- The config catalog is regenerated, and the README, forge subsystem
page, and Agent Note pairings are recorded again.
Master replaced the creation and plugin recommendation bundles with one
personalized recommendations bundle.
- The CLI manifest keeps master's bundle and the forge bundle.
- The plugin page expectation ranks the forge bundle before the held-back
personalized recommendations bundle.
- The experimental README pairing is recorded again.