Skip to content

fix: DeepSeek usage counts must be numbers - #309

Merged
eric8810 merged 9 commits into
masterfrom
fix/deepseek-usage-counts-must-be-numbers
Oct 27, 2026
Merged

eric8810 merged 9 commits into
masterfrom
fix/deepseek-usage-counts-must-be-numbers

Conversation

@cunninghamcard-bit

Copy link
Copy Markdown
Contributor

Summary

Before: DeepSeek kept usage as a raw JSON value and read each count with a JSON pointer, so a response with "prompt_tokens": "100" (or any other non-number count) was accepted and the count read as 0. Generate and stream both.

After: the response and chunk types check usage when they deserialize, as @ai-sdk/deepseek 3.0.56 does with tokenUsageSchema: usage stays a loose object kept whole as usage.raw, but the seven known counts must be numbers or null, and the two *_details fields objects or null. A bad count fails the generate call; in a stream it becomes an error part and the finish reason error, like any other chunk that fails validation.

Found while porting the upstream DeepSeek tests (#299, case deepseek raw usage › continues validating known usage fields). Part of the #276 list.

How it works today

DeepSeekChatLanguageModel::do_generate                 aimux-providers/src/deepseek/model.rs:364     // upstream: DeepSeekChatLanguageModel.doGenerate  deepseek-chat-language-model.ts
├─ create_json_response_handler::<ChatResponse>        deepseek/model.rs:375                          // upstream: deepSeekChatResponseSchema.usage = tokenUsageSchema
│  └─ ChatResponse: no usage field                     deepseek/types.rs:35                           //   deepseek-chat-api-types.ts:95-113: loose object, counts z.number().nullish()
├─ usage_value = raw.get("usage")                      deepseek/model.rs:389   ← unchecked raw JSON
│  └─ promptCacheHitTokens / MissTokens metadata       :390
└─ convert_deepseek_usage(raw.get("usage"))            deepseek/model.rs:464 → deepseek/usage.rs:16
   └─ count(usage, "/prompt_tokens")                   usage.rs:20             ← a string reads as 0
DeepSeekChatLanguageModel::do_stream                   deepseek/model.rs:481
└─ transform: serde_json::from_value::<ChatChunk>      deepseek/model.rs:588   // upstream: chunk schema validation → error part, finishReason error
   └─ ChatChunk.usage: Option<Value>                   deepseek/types.rs:86    ← any JSON accepted
      └─ convert_deepseek_usage(self.usage)            deepseek/model.rs:756

What this PR changes

ChatResponse.usage / ChatChunk.usage                   deepseek/types.rs:45 / :89
└─ #[serde(default, deserialize_with = "deserialize_usage")]
   deserialize_usage                                   deepseek/types.rs:150   ← new, upstream tokenUsageSchema :95-113
   ├─ null or absent → None
   ├─ usage, prompt_tokens_details, completion_tokens_details must be objects (or null)
   ├─ COUNTS: the 7 known counts (pointer form) must be numbers or null → else "usage/<field> must be a number"
   └─ Some(usage) kept whole, extra keys included
do_generate                                            deepseek/model.rs:389 / :464
└─ usage read from the parsed response (data.usage), not raw.get("usage")
do_stream                                              unchanged: the failed chunk takes the existing error path at model.rs:588

Tests (tests/deepseek_chat_test.rs): generate_rejects_a_usage_count_that_is_not_a_number and stream_rejects_a_usage_count_that_is_not_a_number; both fail on master. The upstream case is ported in #299.

Docs: CHANGELOG Fixed.

Type of change

  • Bug fix (non-breaking)
  • New feature (non-breaking)
  • Breaking change
  • New provider
  • New / updated binding
  • Docs / RFC

Checklist

  • cargo +1.99 fmt -p aimux-providers -- --check passes
  • cargo +1.99 clippy -p aimux-providers --lib --tests -- -D warnings passes
  • cargo +1.99 test -p aimux-providers --test deepseek_chat_test --test deepseek_test passes (95 + 4)
  • scripts/check_provider_boundaries.sh passes
  • Updated relevant docs (CHANGELOG)
  • Commit messages follow Conventional Commits

Notes for reviewers

The usage object itself is still kept as Value, because upstream keeps it loose (.loose()) and returns it whole as usage.raw; only the validation is added.


Generated by Claude Code

tokenUsageSchema (deepseek-chat-api-types.ts:95-113) keeps usage as a loose
object but requires its known counts to be numbers or null, so a response
with "prompt_tokens": "100" is rejected. aimux kept usage as a raw Value
and read such a count as 0. The response and chunk types now validate the
seven counts when they deserialize; the generate path reads usage from the
parsed response.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoxcKU4zBD64Lri3e4Lrq5
cunninghamcard-bit pushed a commit that referenced this pull request Oct 20, 2026
The test branch now stacks on fix PR #309, which adds two aimux-own
tests at the end of tests/deepseek_chat_test.rs. That file is already
merged into src/deepseek/tests/chat/, so the two tests land at the end
of chat_language_model.rs with the other aimux-own cases.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0116HPMoqaLfFr1TyqDq1hrW
@eric8810 eric8810 mentioned this pull request Oct 20, 2026
5 tasks done
@cunninghamcard-bit
cunninghamcard-bit marked this pull request as ready for review October 24, 2026 10:13

@eric8810 eric8810 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DeepSeek usage counts 接受 number-or-null 并保留对象,符合 pinned schema。Approve。

…counts-must-be-numbers

# Conflicts:
#	CHANGELOG.md
…counts-must-be-numbers

# Conflicts:
#	CHANGELOG.md
…counts-must-be-numbers

# Conflicts:
#	CHANGELOG.md
…counts-must-be-numbers

# Conflicts:
#	CHANGELOG.md
…counts-must-be-numbers

# Conflicts:
#	CHANGELOG.md
…counts-must-be-numbers

# Conflicts:
#	CHANGELOG.md
@eric8810
eric8810 merged commit 3c82184 into master Oct 27, 2026
16 checks passed
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