feat(gateway): clean DeepSeek Harness requests for non-DeepSeek upstreams - #205
Merged
Merged
Conversation
fylorn
force-pushed
the
claude/core-deepseek-harness-7gev3m
branch
from
September 25, 2026 16:38
a8e8fd6 to
be32c88
Compare
…eams DeepSeek Harness (dsh) sends DeepSeek's wire extensions to whatever base URL it is given: top-level `dsh_*` fields (including a session log of the whole conversation, up to 8 MiB per request), `role: system` entries in `messages`, `tool_addition` / `tool_removal` blocks with `defer_loading` tools, `thinking.type: enabled` without a budget, and its own `x-deepseek-harness-*` headers. Only api.deepseek.com understands them; other upstreams reject unknown fields or blocks, and the session log should not be handed to an upstream that never asked for it. A request is recognised by its User-Agent or a `dsh_*` field. When the upstream is DeepSeek's official endpoint it goes through byte for byte, and a conversion to DeepSeek carries the `dsh_*` fields along. Otherwise: - same-format passthrough runs `tw_dialect::harness::clean`: `dsh_*` fields go, system entries are appended to `system` in order, tool change blocks and `defer_loading` are dropped and listed on the hop like any conversion drop, and a budget-less `thinking` is rewritten the way the model takes it (adaptive, or a budget from the effort) - conversions already lose the extension fields; the Anthropic decoder now lists the tool change blocks inside system entries and `defer_loading`, and the Chat decoder reads `thinking.type` - `x-deepseek-harness-*` headers are not forwarded, and the tool changes beta is taken out of `anthropic-beta` `RequestStarted` and `HistoryRow` gain `session_log_bytes`, so the app can show that a request carried a session log and how large it was (CONTROL_API_VERSION 23, store schema 21). `/v1/files` and `/files` now answer 404 instead of being forwarded: a file uploaded to whichever upstream was picked at that moment cannot be referenced by a later request routed elsewhere, and a 404 is what makes dsh fall back to inline images. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PQef3Cg2FVuc3u8w9qa18j
fylorn
force-pushed
the
claude/core-deepseek-harness-7gev3m
branch
from
September 25, 2026 16:56
be32c88 to
e5882cf
Compare
This was referenced Sep 25, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Requested by F · project thread
What this changes
Before: a DeepSeek Harness (dsh) request was forwarded as-is to any upstream, carrying
dsh_*fields (including the whole session log),role: systementries inmessages,tool_addition/tool_removalblocks, a budget-lessthinking, andx-deepseek-harness-*headers./v1/fileswas forwarded to whichever upstream was picked.After: dsh requests (recognised by
User-Agent: deepseek-harness/…or anydsh_*field) go toapi.deepseek.combyte for byte; to any other upstream they are cleaned first. Requests record the session log size, and/v1/filesanswers 404.Cleaning rules for a non-DeepSeek upstream:
tw_dialect::harness::clean)dsh_*role: systemin Anthropicmessagessystemin ordertool_addition/tool_removalmessages.content.tool_*defer_loadingon toolstools.defer_loadingthinking.type: enabledwithout budget (Anthropic)adaptivefor adaptive Claude models, otherwise abudget_tokensfromoutput_config.effortthinking.typex-deepseek-harness-*headersanthropic-beta: mid-conversation-tool-changes-*Drops are reported through the existing
Translatedevent (from = to on passthrough, as the ChatGPT passthrough already does). When the upstream is DeepSeek and a conversion is needed anyway, thedsh_*fields are copied into the converted body.RequestStartedandHistoryRowgainsession_log_bytes(size ofdsh_session_log).CONTROL_API_VERSION22 → 23, store schema 20 → 21 (rebuilt, no migration). The gateway also reportsclient_hint: deepseek-harnessfor dsh's User-Agent. New msg codegw.files.unsupported.Why
Only DeepSeek's official endpoint understands dsh's wire extensions (see
docs/deepseek-llm-api-wire-extensions.mdandpackages/llm/llm-deepseek/src/serialize.tsupstream). Other upstreams reject unknown fields/blocks, Anthropic rejectsenabledwithout a budget, and the session log (whole conversation, up to 8 MiB) should not reach an upstream that never asked for it. dsh uploads images to/v1/filesfirst and falls back to inline base64 when that fails; the 404 is what triggers the fallback.Why
/filesis 404 for every client, not only dshThe task asked for 404 on the path itself. It is safe beyond dsh because the gateway could not serve the Files API correctly for anyone before this change:
/filescall has no model, so routing sends it to the first candidate of the client's route: whichever upstream happens to be first, not the one that will later serve the request that references the file. With more than one upstream, failover, or a rule that routes by model, the returnedfile_idis unknown to the upstream that receives the generation request.file_idacross vendors; the decoders already dropsource.type: fileandfileparts as unconvertible.A clear 404 with a
[ThinkWatch]message ("the gateway does not host files; send them inline") is better than a silent upload to an arbitrary upstream. If a real need for proxying Files appears, it needs upstream pinning by file id, which is a separate feature.How it was verified
crates/tw-gateway/tests/harness.rs, 9 end-to-end tests: dsh 0.1.7 Anthropic request to an Anthropic relay, to an OpenAI Chat upstream, and to DeepSeek; dsh 0.1.5 Chat request to an OpenAI upstream, to an Anthropic upstream, and to DeepSeek; conversion to DeepSeek carryingdsh_*; a non-dsh request is untouched;/v1/files,/files,/v1/files/{id}give 404 and never reach an upstream. The DeepSeek cases point the provider athttp://api.deepseek.comthrough an HTTP proxy that is the fake upstream, so the host check is exercised for real and the body is compared byte for byte.tw_dialect::harnessandofficial::is_deepseek_host(host spoofing), plus a client-hint test.cargo fmt --check,cargo clippy --workspace --all-targets -D warnings(locally on 1.94 with the pre-existingnonminimal_boolhit inconvert.rs:276allowed; it isn't from this change),cargo test --workspaceall green,./scripts/smoke.sh: 60 passed, 0 failed.Notes for review
tw-dialectchanges are additive (newharnessmodule, newofficial::is_deepseek_host); decoders now listtools.defer_loadingfor any client that sets it (e.g. Claude Code's tool search) when converting, which is accurate but new.thinkingin place: GLM, Kimi and others accept it.CONTROL_API_VERSIONto 23; both ship in the same release, so whichever merges second keeps 23 and merges both notes.🤖 Generated with Claude Code
https://claude.ai/code/session_01PQef3Cg2FVuc3u8w9qa18j