docs(2963): DeepSeek Anthropic-compatible comparison report (decision pending live numbers)

Adds benchmark_results/deepseek-anthropic-comparison-2026-06-24.md for #2963.

The deepseek-anthropic / deepseek-claude route already landed (5b8a5ac0b /
#3449); this is the reporting deliverable, not code. The report:
- documents what landed (route, x-api-key + anthropic-version auth,
  AnthropicMessages wire format, body/SSE/usage parsing) with file:line cites;
- records code-derived findings without live calls: server tools / web search
  are filtered out on encode (anthropic.rs:361) so that capability is not
  exercised via this route today, and reasoning_tokens / server_tool_use are
  always null on the Anthropic usage path vs the Chat-Completions parser;
- gives the comparison methodology and a copy-pasteable live checklist for a
  human with DEEPSEEK_API_KEY to fill in latency/token/correctness numbers;
- states the decision honestly: keep as Experimental, keep-vs-promote PENDING
  the live numbers. No verified verdict is fabricated.
This commit is contained in:
Hunter B
2026-06-24 01:28:23 -07:00
parent 6ab9f72954
commit 35d50878bf

View File

@@ -0,0 +1,524 @@
# DeepSeek Anthropic-Compatible Endpoint — Comparison Report & Decision (#2963)
- **Issue:** [#2963](https://github.com/Hmbown/CodeWhale/issues/2963) — *v0.8.65: DeepSeek Anthropic-compatible endpoint wire-protocol spike*
- **Release lane:** v0.8.65
- **Date:** 2026-06-24
- **Status:** Implementation **landed and Experimental**. Keep-vs-promote decision **PENDING live numbers** (Section 4).
- **Scope of this document:** A *report*. It changes no Rust code and makes no live API calls — no DeepSeek credentials are available in this environment, so all live figures below are left as a checklist for a human operator to fill in.
> Do **not** reimplement the route. It already exists on `main` (commit
> `5b8a5ac0b2c478261740f49756d29c4a7f83d89c`, PR
> [#3449](https://github.com/Hmbown/CodeWhale/pull/3449)). This document
> verifies what landed, derives what can be concluded from the code without a
> network, and specifies the exact live procedure to settle the open question.
All file:line citations below are against the tree at this report's commit
(verified ancestry: `5b8a5ac0b` is an ancestor of `HEAD`).
---
## 1. What's landed
The opt-in DeepSeek route that speaks the **Anthropic Messages** wire protocol
is implemented end to end. **It is already in `main`; do not re-implement it.**
### 1.1 Provider descriptor / route selection
- `crates/config/src/provider.rs:140-178``DeepseekAnthropic` provider:
- id `deepseek-anthropic` (`provider.rs:143-145`)
- display name `DeepSeek (Anthropic-compatible)` (`provider.rs:151-153`)
- aliases `deepseek_anthropic`, `deepseek-claude`, `deepseek_claude`
(`provider.rs:171-173`)
- **wire format `WireFormat::AnthropicMessages`** (`provider.rs:175-177`)
- API-key env var: **`DEEPSEEK_API_KEY` only** (`provider.rs:163-165`) — it
does **not** fall back to `ANTHROPIC_API_KEY`.
- `crates/config/src/provider.rs:31-38``WireFormat` enum
(`ChatCompletions` / `Responses` / `AnthropicMessages`).
- Registry wiring: static entry `provider.rs:544`, registered at
`provider.rs:573`.
- Defaults (`crates/config/src/provider_defaults.rs`):
- base URL `https://api.deepseek.com/anthropic`
(`provider_defaults.rs:14`)
- default model `deepseek-v4-pro``DEFAULT_DEEPSEEK_ANTHROPIC_MODEL`
aliases `DEFAULT_DEEPSEEK_MODEL` (`provider_defaults.rs:8-9`)
- The Chat-Completions DeepSeek route, for contrast, defaults to base URL
`https://api.deepseek.com/beta` (`provider_defaults.rs:13`) with the same
default model `deepseek-v4-pro`.
### 1.2 Dispatch
- `crates/tui/src/client.rs:1331-1339` (`create_message`) and
`client.rs:1341-1352` (`create_message_stream`) route to the Anthropic
adapter when `api_provider_uses_anthropic_messages(self.api_provider)` is
true.
- `client.rs:864-869``api_provider_uses_anthropic_messages` returns true for
`ApiProvider::Anthropic | ApiProvider::DeepseekAnthropic`.
- Request payload mode is selected by route, not prompt:
`crates/tui/src/config.rs:526-530` sets
`RequestPayloadMode::AnthropicMessages` for `DeepseekAnthropic`, else
`ChatCompletions`.
### 1.3 Auth dialect
- `crates/tui/src/client.rs:805-838` builds headers:
- injects `anthropic-version: 2023-06-01` for Anthropic-wire providers
(`client.rs:808-815`)
- uses **`x-api-key`** (never `Authorization: Bearer`) for the API key
(`client.rs:817-819`, applied `client.rs:831-837`)
- `client.rs:846-862` strips any caller-supplied `Authorization` / `api-key` /
`x-api-key` extra headers so a stale OpenAI-style auth header cannot leak onto
the Anthropic wire (`is_auth_dialect_header`, `client.rs:858-862`).
- Tests: `deepseek_anthropic_uses_anthropic_header_dialect`
(`client.rs:2216`+) asserts `x-api-key` + `anthropic-version` are present and
that Bearer / MiMo headers are absent.
### 1.4 Request encoding (Messages body)
- `crates/tui/src/client/anthropic.rs:40-143``build_anthropic_body`:
- `model` / `max_tokens` / `stream` (`anthropic.rs:41-45`)
- `system` as text or cache-aware blocks (`anthropic.rs:47-66`)
- `messages` via `message_to_anthropic` (`anthropic.rs:68-74`,
`anthropic.rs:291-301`)
- `tools` with `strict` + `cache_control` (`anthropic.rs:76-98`)
- `tool_choice` mapped from OpenAI-style string/object to Anthropic object
form (`anthropic.rs:100-102`, `anthropic.rs:279-287`)
- reasoning → `thinking: {type: adaptive}` + `output_config.effort`
(low/medium/high/max), gated on `model_supports_reasoning`
(`anthropic.rs:104-128`)
- sampling-parameter rules: send at most one of temperature/top_p, or neither
for models that reject them (`anthropic.rs:130-139`,
`anthropic.rs:269-275`)
- `cache_control` breakpoint placement, capped at 4
(`anthropic.rs:141`, `anthropic.rs:367-446`)
- Endpoint URL builder tolerates a `/v1` suffix
(`anthropic.rs:259-266`); `https://api.deepseek.com/anthropic`
`…/anthropic/v1/messages`.
### 1.5 Response & stream decoding
- Non-streaming: `anthropic.rs:240-254` (`handle_anthropic_message`) parses the
JSON body and normalizes `usage`.
- Streaming: `anthropic.rs:170-237` (`handle_anthropic_stream`) is an SSE
pass-through; `convert_anthropic_sse_data` (`anthropic.rs:450-494`) decodes
`message_start` / `content_block_*` / `message_delta` / `message_stop` /
`ping` / `error`, tolerates unknown event types, and normalizes usage on
`message_start` / `message_delta`.
- Send/error path: `anthropic.rs:145-167` (`send_anthropic_request`) sets
`Accept: text/event-stream`, maps non-2xx into a typed error via
`parse_anthropic_error_envelope` (`anthropic.rs:528-548`).
### 1.6 Usage / cache normalization (#2961 convention)
- `anthropic.rs:499-523` (`parse_anthropic_usage`):
- `prompt_cache_hit_tokens = cache_read_input_tokens`
- `prompt_cache_miss_tokens = input_tokens + cache_creation_input_tokens`
- normalized `input_tokens = input_tokens + cache_creation + cache_read`
(total prompt — the DeepSeek convention)
### 1.7 Operational guardrails added with the route
- Health check **skips the `/anthropic/v1/models` probe** for this route
(`client.rs:871-873`, `api_provider_skips_models_probe`); test
`deepseek_anthropic_health_check_skips_models_probe` (`client.rs:2301`+).
- **FIM is unsupported** on this route and fails locally with a clear message
(`client.rs:1722-1727`); test `deepseek_anthropic_fim_fails_without_http_request`
(`client.rs:2314`+).
- Base-URL env override is route-aware: `CODEWHALE_BASE_URL` / `DEEPSEEK_BASE_URL`
writes into `providers.deepseek_anthropic.base_url`
(`crates/tui/src/config.rs:3928-3939`).
- Translation helper uses the Messages endpoint for this provider
(`client.rs:974-977`); test
`deepseek_anthropic_translate_uses_messages_endpoint` (`client.rs:2251`+).
### 1.8 Docs framing
- `docs/PROVIDERS.md:48-51`, `:81`, `:111-112`, `:237` document the route as
**Anthropic *wire-protocol* compatibility** (not Anthropic model/provider
semantics), list the aliases, and state "Keep `provider = "deepseek"` for the
default Chat Completions path."
### 1.9 Test coverage already present (no live calls)
In `crates/tui/src/client/anthropic.rs` `#[cfg(test)]` (from `anthropic.rs:550`):
body cache-control placement, reasoning→effort mapping, sampling-param dropping,
signed/unsigned thinking replay, breakpoint cap, full SSE fixture decode
(text + thinking + signature + tool_use + usage), error/unknown-event handling,
usage mapping with missing cache fields, error-envelope parsing, URL `/v1`
tolerance. In `crates/tui/src/client.rs`: the auth-dialect, models-probe-skip,
translate-endpoint, and FIM-unsupported tests cited above.
---
## 2. Code-derived findings (no live calls needed)
These are behavioral facts that can be stated **from the code today**, before
any live comparison. They are the deltas a reviewer most needs to know.
### 2.1 Server tools / web search are NOT exercised via this route today
`content_block_to_anthropic` **drops** the server-tool block types on encode:
```
crates/tui/src/client/anthropic.rs:359-364
// Server-tool block types are DeepSeek/internal concepts with no
// Anthropic client-side wire equivalent.
ContentBlock::ServerToolUse { .. }
| ContentBlock::ToolSearchToolResult { .. }
| ContentBlock::CodeExecutionToolResult { .. } => None,
```
Consequence: any server-tool / web-search content the engine holds is filtered
out before the request is sent on this route. There is also no encode-side path
that *injects* an Anthropic-style server-tool definition (e.g. a `web_search`
tool) into the outbound body — `build_anthropic_body` only forwards
caller-supplied client tools (`anthropic.rs:76-98`). So **server-side web
search / code execution is not exercised through the DeepSeek Anthropic route as
implemented.** Whether DeepSeek's endpoint would *accept* such a tool is a
separate, still-open question that only live testing (Section 4, Test E) can
answer; the code neither offers nor depends on it.
### 2.2 Usage telemetry: two real deltas vs the Chat-Completions path
Compare the two usage parsers:
| Field | Anthropic route (`anthropic.rs:499-523`) | Chat-Completions route (`client.rs:1643-1711`) |
|---|---|---|
| `input_tokens` (normalized) | `input + cache_creation + cache_read` | `input_tokens`/`prompt_tokens` as-is |
| `prompt_cache_hit_tokens` | `cache_read_input_tokens` | `prompt_cache_hit_tokens`, else `prompt_tokens_details.cached_tokens` |
| `prompt_cache_miss_tokens` | `input + cache_creation` | `prompt_cache_miss_tokens`, else `input hit` |
| `reasoning_tokens` | **always `None`** (`anthropic.rs:519`) | parsed from `completion_tokens_details.reasoning_tokens` (`client.rs:1658-1685`) |
| `reasoning_replay_tokens` | `None` (`anthropic.rs:520`) | `None` (`client.rs:1708`) |
| `server_tool_use` | **always `None`** (`anthropic.rs:521`) | parsed from `server_tool_use.{code_execution,tool_search}_requests` (`client.rs:1687-1700`) |
| `output_tokens` | Anthropic `output_tokens` | `output_tokens`/`completion_tokens`, with fallbacks to reasoning or `total input` (`client.rs:1648-1670`) |
Two concrete deltas to record honestly in any telemetry comparison:
1. **`reasoning_tokens` is never populated on the Anthropic route.** Reasoning
*content* still flows (thinking blocks decode and signed blocks replay —
`anthropic.rs:315-330`, `anthropic.rs:822-868` fixture), but the **count**
is dropped. On the Chat-Completions route the count is read from
`completion_tokens_details.reasoning_tokens`. This is per the #2961/#3085
"explicit unknown/null for unsupported fields" rule, but it means
reasoning-token *accounting parity* between the two routes cannot be
expected — confirm in Test C.
2. **`server_tool_use` is never populated on the Anthropic route** (consistent
with §2.1: the route doesn't drive server tools).
### 2.3 Thinking / reasoning request shaping differs by design
The Anthropic route maps `reasoning_effort` tiers to
`thinking: {type: adaptive}` + `output_config.effort`
(`anthropic.rs:104-128`), gated on `model_supports_reasoning`. The
Chat-Completions DeepSeek path uses its own reasoning-split / payload
conventions (`config.rs:526-530` selects the payload mode; DeepSeek-family
reasoning handling lives on the Chat path). Equivalent *output* is the bar to
test (Section 3/4), not byte-identical requests.
### 2.4 Caching model differs in shape
The Anthropic route places explicit `cache_control` breakpoints (max 4) on the
prefix and latest user turn (`anthropic.rs:367-446`) and reports cache
hit/miss from Anthropic's `cache_read` / `cache_creation` fields. The
Chat-Completions route relies on DeepSeek's automatic prefix caching and reads
`prompt_cache_hit_tokens` / `prompt_cache_miss_tokens` (or
`prompt_tokens_details.cached_tokens`). Both normalize into the same #2961
fields, so cache *telemetry* is comparable even though the *mechanism* differs.
### 2.5 Capability/operational deltas (route-level, from code)
- **FIM**: supported on Chat-Completions DeepSeek; **unsupported** on the
Anthropic route (`client.rs:1722-1727`).
- **Models probe**: skipped on the Anthropic route (`client.rs:871-873`); the
Chat path probes `/models`.
- **Auth**: `x-api-key` + `anthropic-version` (Anthropic route) vs
`Authorization: Bearer` (Chat route) — `client.rs:817-827`.
- **Endpoint**: `…/anthropic/v1/messages` vs `…/beta` chat completions.
### 2.6 What is *equivalent* by construction
Tool-call and tool-result mapping, image blocks, system prompt, and stop
reasons all have direct encoders (`anthropic.rs:303-358`) and the SSE decoder
reconstructs tool-use input JSON (fixture `anthropic.rs:816-897`). So for an
ordinary "prompt → text / tool_use" exchange, the two routes are expected to be
functionally equivalent; the open questions are the *quantitative* ones
(latency, token counts) and the *server-tool* one.
---
## 3. Comparison methodology
Compare DeepSeek's **Chat-Completions** route (`provider = "deepseek"`) against
its **Anthropic-Messages** route (`provider = "deepseek-anthropic"`) for the
**same model** (`deepseek-v4-pro`, and `deepseek-v4-flash` if the account has
it). Hold everything else constant (same prompt, same `max_tokens`, same
reasoning effort, same temperature where accepted).
Dimensions:
1. **Correctness / output equivalence** — same prompt → semantically equivalent
answer; same tool selection and arguments for a tool-use prompt; valid JSON
for a structured prompt.
2. **Latency** — wall-clock total and (for streaming) time-to-first-token, over
N≥5 runs each; report median + spread, not a single sample.
3. **Token / usage accounting parity** — compare `input_tokens` (normalized),
`output_tokens`, `prompt_cache_hit_tokens`, `prompt_cache_miss_tokens`,
`reasoning_tokens`. **Expect `reasoning_tokens` to be null on the Anthropic
route** (§2.2) — record it, don't treat it as a bug.
4. **Telemetry fields** — which of the #2961/#3085 normalized fields are
populated vs null on each route; note `server_tool_use` is null on the
Anthropic route by construction.
5. **Server-tool / web-search support** — does DeepSeek's Anthropic endpoint
*accept*, *ignore*, or *reject* an Anthropic-style server tool (e.g.
`web_search`)? Capture the raw request/response. (Recall the engine does not
send such a tool today — §2.1 — so this is an endpoint-capability probe with
a hand-built request, not a test of CodeWhale's encoder.)
6. **Error envelopes & rate limiting** — confirm 4xx/5xx map cleanly
(`anthropic.rs:528-548`) and that the route honors the same retry/backoff.
Pass bar for "comparable" (issue Acceptance Criteria): equivalent correctness on
the smoke tasks, latency within a reasonable band, and usage telemetry that maps
into the normalized fields (with explicit nulls where unsupported).
---
## 4. Runnable live checklist (human, with `DEEPSEEK_API_KEY` set)
All commands are copy-pasteable. They assume the repo root and a DeepSeek key.
**No credentials exist in this environment; these are for a human to run.**
### 4.0 One-time setup
```bash
export DEEPSEEK_API_KEY="sk-..." # your DeepSeek key
MODEL="deepseek-v4-pro" # also repeat with deepseek-v4-flash if available
CHAT_BASE="https://api.deepseek.com" # Chat Completions (OpenAI-compatible)
ANTH_BASE="https://api.deepseek.com/anthropic" # Anthropic Messages
mkdir -p benchmark_results/2963-live && cd "$(git rev-parse --show-toplevel)"
```
### Test A — correctness, single turn (text)
Chat Completions:
```bash
curl -sS -w '\n[http %{http_code} | total %{time_total}s | ttfb %{time_starttransfer}s]\n' \
-X POST "$CHAT_BASE/v1/chat/completions" \
-H "Authorization: Bearer $DEEPSEEK_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"model\":\"$MODEL\",\"max_tokens\":64,\"stream\":false,
\"messages\":[{\"role\":\"user\",\"content\":\"Reply with exactly the word: PONG\"}]}" \
| tee benchmark_results/2963-live/A_chat.json
```
Anthropic Messages (note `x-api-key` + `anthropic-version`, no Bearer):
```bash
curl -sS -w '\n[http %{http_code} | total %{time_total}s | ttfb %{time_starttransfer}s]\n' \
-X POST "$ANTH_BASE/v1/messages" \
-H "x-api-key: $DEEPSEEK_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "Content-Type: application/json" \
-d "{\"model\":\"$MODEL\",\"max_tokens\":64,\"stream\":false,
\"messages\":[{\"role\":\"user\",\"content\":\"Reply with exactly the word: PONG\"}]}" \
| tee benchmark_results/2963-live/A_anthropic.json
```
Record: does each return "PONG"? HTTP status, total time.
### Test B — usage / token accounting (read the `usage` object on both)
```bash
echo "Chat usage:"; jq '.usage' benchmark_results/2963-live/A_chat.json
echo "Anthropic usage:"; jq '.usage' benchmark_results/2963-live/A_anthropic.json
```
Fill in the table:
| Field | Chat Completions | Anthropic Messages |
|---|---|---|
| prompt/input tokens | | |
| completion/output tokens | | |
| cache hit (`prompt_cache_hit_tokens` / `cache_read_input_tokens`) | | |
| cache miss (`prompt_cache_miss_tokens` / `cache_creation_input_tokens`) | | |
| reasoning tokens (`completion_tokens_details.reasoning_tokens`) | | (expected absent) |
### Test C — reasoning / thinking
Chat Completions (DeepSeek reasoner-style):
```bash
curl -sS -X POST "$CHAT_BASE/v1/chat/completions" \
-H "Authorization: Bearer $DEEPSEEK_API_KEY" -H "Content-Type: application/json" \
-d "{\"model\":\"$MODEL\",\"max_tokens\":512,\"stream\":false,
\"messages\":[{\"role\":\"user\",\"content\":\"A bat and ball cost \$1.10. The bat costs \$1 more than the ball. How much is the ball? Think, then answer.\"}]}" \
| tee benchmark_results/2963-live/C_chat.json | jq '{content:.choices[0].message, usage:.usage}'
```
Anthropic Messages with adaptive thinking:
```bash
curl -sS -X POST "$ANTH_BASE/v1/messages" \
-H "x-api-key: $DEEPSEEK_API_KEY" -H "anthropic-version: 2023-06-01" -H "Content-Type: application/json" \
-d "{\"model\":\"$MODEL\",\"max_tokens\":512,\"stream\":false,
\"thinking\":{\"type\":\"adaptive\"},\"output_config\":{\"effort\":\"high\"},
\"messages\":[{\"role\":\"user\",\"content\":\"A bat and ball cost \$1.10. The bat costs \$1 more than the ball. How much is the ball? Think, then answer.\"}]}" \
| tee benchmark_results/2963-live/C_anthropic.json | jq '{content:.content, usage:.usage}'
```
Record: both should answer **\$0.05**. Note whether a `thinking` block is
returned by the Anthropic route and whether reasoning tokens appear anywhere.
### Test D — tool use (same tool both routes)
Chat Completions:
```bash
curl -sS -X POST "$CHAT_BASE/v1/chat/completions" \
-H "Authorization: Bearer $DEEPSEEK_API_KEY" -H "Content-Type: application/json" \
-d "{\"model\":\"$MODEL\",\"max_tokens\":256,
\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"get_weather\",
\"description\":\"Get weather for a city\",
\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"]}}}],
\"messages\":[{\"role\":\"user\",\"content\":\"What's the weather in Paris? Use the tool.\"}]}" \
| tee benchmark_results/2963-live/D_chat.json | jq '.choices[0].message.tool_calls'
```
Anthropic Messages:
```bash
curl -sS -X POST "$ANTH_BASE/v1/messages" \
-H "x-api-key: $DEEPSEEK_API_KEY" -H "anthropic-version: 2023-06-01" -H "Content-Type: application/json" \
-d "{\"model\":\"$MODEL\",\"max_tokens\":256,
\"tools\":[{\"name\":\"get_weather\",\"description\":\"Get weather for a city\",
\"input_schema\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"]}}],
\"messages\":[{\"role\":\"user\",\"content\":\"What's the weather in Paris? Use the tool.\"}]}" \
| tee benchmark_results/2963-live/D_anthropic.json | jq '.content'
```
Record: does each emit a `get_weather` call with `city = "Paris"`?
### Test E — server-tool / web-search capability probe (the open question)
Send an Anthropic-style server tool and **record whether DeepSeek accepts,
ignores, or rejects it** (capture the full body). The engine does not send this
today (§2.1); this is a raw endpoint probe.
```bash
curl -sS -w '\n[http %{http_code}]\n' -X POST "$ANTH_BASE/v1/messages" \
-H "x-api-key: $DEEPSEEK_API_KEY" -H "anthropic-version: 2023-06-01" -H "Content-Type: application/json" \
-d "{\"model\":\"$MODEL\",\"max_tokens\":256,
\"tools\":[{\"type\":\"web_search_20250305\",\"name\":\"web_search\",\"max_uses\":2}],
\"messages\":[{\"role\":\"user\",\"content\":\"Search the web: what is the latest stable Rust version? Cite a source.\"}]}" \
| tee benchmark_results/2963-live/E_websearch.json
```
Classify the outcome:
- **Accepted + worked** — response contains server-tool-use / search results.
- **Ignored** — 200 OK, plain answer, no tool activity.
- **Rejected** — 4xx with an error envelope (record `error.type` / message).
### Test F — streaming smoke (both routes)
```bash
# Anthropic SSE
curl -N -sS -X POST "$ANTH_BASE/v1/messages" \
-H "x-api-key: $DEEPSEEK_API_KEY" -H "anthropic-version: 2023-06-01" \
-H "Accept: text/event-stream" -H "Content-Type: application/json" \
-d "{\"model\":\"$MODEL\",\"max_tokens\":64,\"stream\":true,
\"messages\":[{\"role\":\"user\",\"content\":\"Count: one two three\"}]}" \
| tee benchmark_results/2963-live/F_anthropic.sse | head -40
```
Confirm `message_start``content_block_*``message_delta``message_stop`
arrive (the shapes `convert_anthropic_sse_data` decodes, `anthropic.rs:450-494`).
### Test G — end-to-end through CodeWhale (optional, exercises the real adapter)
```bash
# Anthropic route through the built binary
cargo run -q -p codewhale -- --provider deepseek-anthropic --model "$MODEL" \
--print "Reply with exactly: PONG"
# Chat route for comparison
cargo run -q -p codewhale -- --provider deepseek --model "$MODEL" \
--print "Reply with exactly: PONG"
```
(Adjust the binary/flag names to the project's actual non-interactive entry
point if different; the point is to run one prompt through each resolved route.)
### 4.1 Results table to fill in
| Dimension | Chat Completions | Anthropic Messages | Verdict |
|---|---|---|---|
| Correctness (A/C/D) | | | |
| Latency median (N=…) | | | |
| TTFT (streaming) | | | |
| Token accounting (B) | | | |
| reasoning_tokens present | | (expected no) | |
| Tool use (D) | | | |
| Web search (E) | n/a | accept / ignore / reject | |
| Streaming (F) | | | |
---
## 5. Decision
**Recommendation: KEEP as Experimental. The keep-vs-promote decision is PENDING
the live numbers in Section 4. This report does not assert a "verified" verdict
because no live calls were made.**
Rationale, grounded in code:
- **Keep (not reject):** the route is fully implemented, isolated behind opt-in
provider selection (`deepseek-anthropic` / `deepseek-claude`), guarded
(FIM-unsupported message, models-probe skip, auth-header hygiene), and covered
by unit + SSE-fixture tests. It does not touch or regress the default
Chat-Completions DeepSeek path (separate dispatch at `client.rs:1331-1352`;
docs say keep `provider = "deepseek"` for the default). Nothing in the code
argues for ripping it out.
- **Do not promote yet:** the issue's promotion bar requires the Anthropic route
to be *at least comparable* on a live A/B, plus explicit server-tool evidence.
That evidence does not exist here. Two code-derived caveats that promotion
must weigh: (a) `reasoning_tokens` accounting is dropped on this route
(§2.2 #1), and (b) server tools / web search are not exercised through it
(§2.1) — so if web search is a requirement for "preferred," this route does
not satisfy it today regardless of what Test E shows about the endpoint.
- **Gate to flip the decision:** complete Section 4 (especially Tests AE),
fill the §4.1 table, and confirm equivalent correctness + comparable latency +
clean telemetry mapping. If all green and web search is not a blocker →
candidate to promote to preferred for DeepSeek V4. Otherwise → remain
Experimental, or reject the *promotion* (not the route) if telemetry/latency
regress.
### Suggested issue note (after live numbers are in)
> Implementation verified landed (#3449 / `5b8a5ac0b`); see
> `benchmark_results/deepseek-anthropic-comparison-2026-06-24.md`. Live A/B
> results: [fill in]. Server-tool/web-search probe (Test E): [accept/ignore/
> reject + evidence]. Decision: [keep experimental | promote to preferred].
---
## Appendix — citation index
| Topic | Location |
|---|---|
| `WireFormat` enum | `crates/config/src/provider.rs:31-38` |
| `DeepseekAnthropic` descriptor | `crates/config/src/provider.rs:140-178` |
| Registry entry | `crates/config/src/provider.rs:544`, `:573` |
| Base URL / model defaults | `crates/config/src/provider_defaults.rs:8-9,13-14` |
| Dispatch to Anthropic adapter | `crates/tui/src/client.rs:1331-1352` |
| `api_provider_uses_anthropic_messages` | `crates/tui/src/client.rs:864-869` |
| Auth header build (`x-api-key`/`anthropic-version`) | `crates/tui/src/client.rs:805-862` |
| Models-probe skip | `crates/tui/src/client.rs:871-873` |
| FIM unsupported | `crates/tui/src/client.rs:1722-1727` |
| Chat-Completions usage parser | `crates/tui/src/client.rs:1643-1711` |
| Base-URL env override (route-aware) | `crates/tui/src/config.rs:3928-3939` |
| Payload-mode selection | `crates/tui/src/config.rs:526-530` |
| `build_anthropic_body` | `crates/tui/src/client/anthropic.rs:40-143` |
| Messages URL builder | `crates/tui/src/client/anthropic.rs:259-266` |
| **Server-tool blocks dropped on encode** | `crates/tui/src/client/anthropic.rs:359-364` |
| Anthropic usage normalizer | `crates/tui/src/client/anthropic.rs:499-523` |
| Error-envelope parser | `crates/tui/src/client/anthropic.rs:528-548` |
| Docs framing | `docs/PROVIDERS.md:48-51,81,111-112,237` |
| Landed commit / PR | `5b8a5ac0b2c478261740f49756d29c4a7f83d89c` / [#3449](https://github.com/Hmbown/CodeWhale/pull/3449) |