The DeepSeek search and Search/Fetch API providers now report readiness
from credential presence alone, on any model route: the account token
first on the deepseek-account route, then a literal apiKey or a
configured apiKeyEnv reference. A conversation on another provider's
model keeps web_search and web_fetch while a DeepSeek key is configured,
as on master, so the route refusal WEB_PROVIDER_ROUTE_DISABLED and its
upgrade guide are removed.
The pi-ai-conversation-updates and deepseek-messages-system-prompt
snapshots replay their master recordings again, the headless keyless
smoke points its search key reference at an unset variable, and the
readiness Agent Note records the rejected route and endpoint rules.
A user who gives DeepSeek search a key of its own has chosen what that key
pays for, so the route check now applies only to the shared
DEEPSEEK_API_KEY. A literal apiKey, or an apiKeyEnv other than
DEEPSEEK_API_KEY, opens every model route for Messages search and for the
Search and Fetch APIs; a configured endpoint does not, since the search
still sends the shared key. The Web search settings page saves its key
under the section's apiKeyEnv, DEEPSEEK_API_KEY by default, so a key saved
there keeps serving only the DeepSeek routes.
The route refusal now names the settings that give search a key of its
own, and the READMEs, the readiness Agent Note, and the upgrade guide
describe the exception.
Review found two faults in the endpoint and configuration checks. The
providers took the official chat route's endpoint from the first active
dsh-llm-deepseek-api-key Loader row, so a composition that isolates several
instances answered for an unrelated one: a correct route lost search, or a
gateway's key reached DeepSeek. And a key saved on the Web search settings
page lands under the default DEEPSEEK_API_KEY reference, so the inference
that search had its own configuration never saw it.
Both DeepSeek web packages now serve only the deepseek-account and
deepseek-official routes, whatever key they are configured with, and no
longer compare the chat endpoint with their own. ready() checks the route
and then credential presence as before; the API-key resolver repeats the
route check and refuses with WEB_PROVIDER_ROUTE_DISABLED. The packages no
longer read Loader rows or depend on dsh-llm-deepseek-api-key, so the
lockfile, the module graph, and the keyless smoke expectations that the
endpoint check had changed return to master's.
An upgrade guide covers conversations on other providers' models, which
lose DeepSeek web search and, with the Search and Fetch APIs selected,
web_fetch.
* feat(webserver): add resource routes and in-process readResource
A webserver route or the fallback seat may now answer through a resource,
a function from a Fetch Request to a Response, besides the existing
node:http handler. register accepts either route form and registerFallback
accepts a handler or { resource }; existing handler callers are unchanged.
HTTP GET and HEAD requests reach a resource; webServer.readResource(request,
peer) follows the same match order and reaches only resources, so handler
routes stay unreachable to in-process readers.
client-modules registers /plugins as the fetchBundle resource, and
frontend-static claims the fallback seat with a resource that renders the
index for a supplied Peer, or runs Connection's existing authorizeIndex for
an HTTP request and renders for the browser Peer.
* fix(webserver): flush resource headers and keep one drain listener per response
Resource responses send their status and headers before the first body
chunk, register one drain/close listener pair per response so the gzip
middleware's zlib stream does not accumulate listeners across backpressure
waits, and readResource drops the body of a HEAD read as HTTP does. HTTP
requests and readResource share one responder lookup.
* chore(scripts): prune the retired client-modules unknown-cast baseline entry
* feat(frontend-static): let browsers revalidate dist files except the index
Every dist file other than the rendered index now carries
cache-control: no-cache and a weak ETag from a SHA-1 of its bytes; a
request whose If-None-Match equals that ETag receives an empty 304. The
index keeps sending no cache headers. In-process reads through
webServer.readResource revalidate the same way.
* fix(webserver): stop reading Host for resource URLs and copy no held body
A resource request's URL is the request target resolved against the same
placeholder origin the route match uses, so a missing or unparsable Host no
longer turns a dist or /plugins request into 400. The webserver ends a
resource response itself and treats any earlier close as an abort, which
also covers the Web Worker tunnel marking an aborted response ended first.
client-modules bundle bodies and frontend-static file bodies are the held
bytes themselves instead of copies, and frontend-static no longer exports
its internal serveStatic helper.
* docs(webserver): document resource route types and the handler-or-resource fallback
Paste WebResource and WebResourceRoute beside WebRoute on the web-server
subsystem page and link the catalog signatures to it, replace the remaining
'fallback handler' wording now that the seat takes a handler or a resource,
and note why the drain listener removal is a no-op under gzip.
The Web search switch selects the DeepSeek Search and Fetch APIs together,
and both reuse the DeepSeek chat key by default. Neither provider answered
readiness, so a step without a usable key was offered web_search and
web_fetch whose every call failed, and a conversation on another provider's
model or behind a gateway sent the chat key to DeepSeek.
dsh-web gives WebFetchProvider an optional ready(llmProvider) and adds
ctx.web.fetchReady(), which asks only the provider fetch() selects, as
searchReady() does for search. tool-web registers web_fetch with a step
activation condition on fetchReady(), so a step that cannot fetch leaves the
tool out of declarations, guidance, and PTC bindings and refuses a call to
it as UNKNOWN_TOOL; without web_fetch the search guidance uses its
search-only text. The shipped HTTP fetch provider has no ready() and stays
admitted.
web-deepseek-api applies the API-key rules of Messages search to both APIs:
without a literal apiKey, an apiKeyEnv other than DEEPSEEK_API_KEY, or a
baseURL other than the default, the chat key goes only to the DeepSeek
routes and only to an API base on the official chat route's origin. The
refusal sits in the key's only resolver, and both providers' ready()
answers presence in the order an operation resolves credentials. The
transport repeats these checks so the package still does not depend on
dsh-web-search-deepseek.
* ci: run native Windows PR jobs in a separate workflow
GitHub re-runs failed jobs only after a whole run finishes, so slow Windows jobs blocked re-running failed Linux jobs. CI Windows now owns the native Windows jobs and the Windows x64 Python runtime target, with its own required 'windows checks passed' verdict.
Refs #6893
* ci: run Python PR jobs in a separate workflow
Python rarely fails while the Windows lanes run long, so the Python SDK suite and the Linux/Windows x64 runtime matrix move into CI Python. The three pull-request workflows can now be re-run without waiting for each other, and branch protection gains "python checks passed" beside the existing verdicts.
Refs #6893
* docs: record the three PR verdicts in the failover runbook
* test: pin the shared environment across the PR workflows
* test: read the python SDK lane from CI Python
* test(sdk): parse session JSONL frames in the keyless smoke
Node 24.19 returned only the first frame of a concatenated zstd log, so the single JSON.parse saw just the header; 24.22 returns every frame and the JSONL body broke the parse. Read frames explicitly and select the main session by its header so both versions agree.
Profile templates and the default, optional, on-demand, retired, and
installation-owned bundle lists move to the new data-only package
@deepseek-ai/dsh-official-bundles, so editing a bundle list no longer
touches app-boot.
Master's step-scoped conditional activation resolves each tool's condition
before a model step assembles, and both the declarations and dispatch read
that admitted set. Prompt assembly no longer carries tool declarations, so
tool-web's system-prompt/assemble listener could not remove web_search any
more, and removing a declaration there never stopped a repeated call from
running.
tool-web now registers web_search with a when condition that asks
ctx.web.searchReady() about the route of the step's model selection, and its
guidance section renders from the step's admitted set. A step that does not
admit the tool leaves it out of declarations, guidance, and PTC program
bindings, and refuses a call to it as UNKNOWN_TOOL without reaching a
provider. Tests that execute web_search directly now run inside a resolved
step, as the executor requires for conditional registrations.
Docs, the readiness Agent Note, and the event producer/consumer table follow
the new mechanism; the tools README gains an explicit conditional-tools
anchor for cross-locale links.
A step's tool set no longer empties when the step is cancelled. The shared
pipeline already settles unstarted calls as cancelled; keeping admission lets
those calls keep their finalizers, and a wrapper result completed at
cancellation now reports cancellation instead of UNKNOWN_TOOL.
exit_plan_mode checks for a missing Agent explicitly instead of asserting it.
Document the activation types on the tools subsystem page, the turn/end code
for a failed tool condition, the replacement and no-set consequences in the
upgrade guide, and the admission rule in the parallel tool-call note.
The tool executor merge added ToolBatchInput.toolSet; the subsystem page's type-equivalence block now matches it. The architecture turn-flow line names step-admitted tool schemas in one line so the page stays within its word budget.
- tool-jobs: inline a completion notice only when the complete text fits inlineOutputBytes and the producer cap; otherwise point at job_output, whose result the spill policy bounds
- subagent: drop toolCallsThisStep; omit the closing text while the child's last logged tool call is its message to the parent