Skip to content

feat(gateway): clean DeepSeek Harness requests for non-DeepSeek upstreams - #205

Merged
fylorn merged 1 commit into
mainfrom
claude/core-deepseek-harness-7gev3m
Sep 25, 2026
Merged

fylorn merged 1 commit into
mainfrom
claude/core-deepseek-harness-7gev3m

Conversation

@fylorn

@fylorn fylorn commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

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: system entries in messages, tool_addition/tool_removal blocks, a budget-less thinking, and x-deepseek-harness-* headers. /v1/files was forwarded to whichever upstream was picked.

After: dsh requests (recognised by User-Agent: deepseek-harness/… or any dsh_* field) go to api.deepseek.com byte for byte; to any other upstream they are cleaned first. Requests record the session log size, and /v1/files answers 404.

Cleaning rules for a non-DeepSeek upstream:

Same-format passthrough (tw_dialect::harness::clean) Format conversion
top-level dsh_* removed not carried by the IR
role: system in Anthropic messages appended to system in order merged into system by the decoder (already)
tool_addition / tool_removal dropped, listed as messages.content.tool_* dropped, now listed
defer_loading on tools removed, listed as tools.defer_loading now listed
thinking.type: enabled without budget (Anthropic) adaptive for adaptive Claude models, otherwise a budget_tokens from output_config.effort handled by the encoder; Chat decoder now reads thinking.type
x-deepseek-harness-* headers not forwarded not forwarded
anthropic-beta: mid-conversation-tool-changes-* that item removed, others kept header not forwarded (already)

Drops are reported through the existing Translated event (from = to on passthrough, as the ChatGPT passthrough already does). When the upstream is DeepSeek and a conversion is needed anyway, the dsh_* fields are copied into the converted body.

RequestStarted and HistoryRow gain session_log_bytes (size of dsh_session_log). CONTROL_API_VERSION 22 → 23, store schema 20 → 21 (rebuilt, no migration). The gateway also reports client_hint: deepseek-harness for dsh's User-Agent. New msg code gw.files.unsupported.

Why

Only DeepSeek's official endpoint understands dsh's wire extensions (see docs/deepseek-llm-api-wire-extensions.md and packages/llm/llm-deepseek/src/serialize.ts upstream). Other upstreams reject unknown fields/blocks, Anthropic rejects enabled without 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/files first and falls back to inline base64 when that fails; the 404 is what triggers the fallback.

Why /files is 404 for every client, not only dsh

The 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:

  • A /files call 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 returned file_id is unknown to the upstream that receives the generation request.
  • Even with a single upstream, a later format conversion (e.g. Anthropic client → OpenAI upstream) cannot carry a file_id across vendors; the decoders already drop source.type: file and file parts as unconvertible.
  • Every client in scope (Claude Code, Codex, OpenCode, dsh) sends images and documents inline; dsh is the only one that tries Files first, and it falls back on any non-2xx.

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 carrying dsh_*; 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 at http://api.deepseek.com through an HTTP proxy that is the fake upstream, so the host check is exercised for real and the body is compared byte for byte.
  • Unit tests in tw_dialect::harness and official::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-existing nonminimal_bool hit in convert.rs:276 allowed; it isn't from this change), cargo test --workspace all green, ./scripts/smoke.sh: 60 passed, 0 failed.
  • No credential handling touched; stripped headers are dsh's anonymous user/session ids only.

Notes for review

  • tw-dialect changes are additive (new harness module, new official::is_deepseek_host); decoders now list tools.defer_loading for any client that sets it (e.g. Claude Code's tool search) when converting, which is accurate but new.
  • Chat passthrough leaves thinking in place: GLM, Kimi and others accept it.
  • feat(quota): read the GLM Coding Plan quota for Z.ai and BigModel upstreams #204 also bumps CONTROL_API_VERSION to 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

@fylorn fylorn self-assigned this Sep 25, 2026
@fylorn
fylorn force-pushed the claude/core-deepseek-harness-7gev3m branch from a8e8fd6 to be32c88 Compare September 25, 2026 16:38
…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
fylorn force-pushed the claude/core-deepseek-harness-7gev3m branch from be32c88 to e5882cf Compare September 25, 2026 16:56
@fylorn
fylorn merged commit f9c9b66 into main Sep 25, 2026
4 checks passed
@fylorn
fylorn deleted the claude/core-deepseek-harness-7gev3m branch September 25, 2026 17:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants