{"repo":"NousResearch/hermes-agent","public_id":"61871","display_label":"DeepSeek cache-hit token extraction and pricing fix","summary":"A bug report and several fix attempts addressing that DeepSeek's native prompt_cache_hit_tokens field was not being read, causing cache-hit tokens to go unextracted when using the direct DeepSeek API. Related changes also touch pricing updates and cache token fallback handling.","subsystem":"agent","counts":{"members":5,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":3},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":65678},"resolution":{"state":"resolved","by":"pr-65678"},"freshness":{"state":"verified","last_verified_at":"2026-07-16T14:29:53+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.4},"fix_edges":[{"issue_number":61871,"pr_number":65678,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true}],"members":[{"kind":"issue","number":61871,"title":"[Bug]: direct DeepSeek API cache-hit tokens never extracted — only OpenAI/OpenRouter prompt_tokens_details.cached_tokens is read","url":null,"state":"closed"},{"kind":"pr","number":61878,"title":"fix(transports): extract DeepSeek cache-hit tokens from top-level prompt_cache_hit_tokens","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#65678","number":65678},{"kind":"pr","value":"#61871","number":61871}],"category":"uncategorized","implication":"needs_llm"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":62303,"title":"fix(deepseek): extract prompt_cache_hit_tokens + update Jul 2026 pricing","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#61878","number":61878},{"kind":"pr","value":"#61878","number":61878}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":65676,"title":"usage_pricing: add prompt_cache_hit/miss_tokens as cache token fallback","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"03c0b00f45b0a8fd85ee1192991e839f7c61518a","number":0},{"kind":"pr","value":"#65678","number":65678},{"kind":"pr","value":"#65678","number":65678},{"kind":"pr","value":"#61871","number":61871}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":65678,"title":"fix(usage): read DeepSeek's native prompt_cache_hit_tokens cache field","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["agent/transports/chat_completions.py","agent/usage_pricing.py","tests/agent/test_usage_pricing.py","tests/agent/transports/test_chat_completions.py"],"fix_hunks":[{"file":"agent/transports/chat_completions.py","hunks":["@@ -776,15 +776,18 @@ def validate_response(self, response: Any) -> bool:\n         return True\n \n     def extract_cache_stats(self, response: Any) -> dict[str, int] | None:\n-        \"\"\"Extract OpenRouter/OpenAI cache stats from prompt_tokens_details.\"\"\"\n+        \"\"\"Extract cache stats from prompt_tokens_details (OpenRouter/OpenAI)\n+        or DeepSeek's native top-level prompt_cache_hit_tokens field.\"\"\"\n         usage = getattr(response, \"usage\", None)\n         if usage is None:\n             return None\n         details = getattr(usage, \"prompt_tokens_details\", None)\n-        if details is None:\n-            return None\n-        cached = getattr(details, \"cached_tokens\", 0) or 0\n-        written = getattr(details, \"cache_write_tokens\", 0) or 0\n+        cached = getattr(details, \"cached_tokens\", 0) or 0 if details else 0\n+        written = getattr(details, \"cache_write_tokens\", 0) or 0 if details else 0\n+        if not cached:\n+            # DeepSeek native API shape (api.deepseek.com): top-level\n+            # prompt_cache_hit_tokens / prompt_cache_miss_tokens (#61871).\n+            cached = getattr(usage, \"prompt_cache_hit_tokens\", 0) or 0\n         if cached or written:\n             return {\"cached_tokens\": cached, \"creation_tokens\": written}\n         return None"]},{"file":"agent/usage_pricing.py","hunks":["@@ -1074,6 +1074,15 @@ def normalize_usage(\n         cache_read_tokens = [redacted:secret-assignment] \"cached_tokens\", 0) if details else 0)\n         if not cache_read_tokens:\n             [redacted:secret-assignment] = _to_int(getattr(response_usage, \"cache_read_input_tokens\", 0))\n+        if not cache_read_tokens:\n+            # DeepSeek's native API (api.deepseek.com) reports context-cache\n+            # hits as top-level prompt_cache_hit_tokens (+ the complementary\n+            # prompt_cache_miss_tokens; prompt_tokens = hit + miss), not the\n+            # OpenAI nested shape. Without this, direct DeepSeek sessions\n+            # always showed 0 cache-hit tokens (#61871).\n+            cache_read_tokens = [redacted:secret-assignment]\n+                getattr(response_usage, \"prompt_cache_hit_tokens\", 0)\n+            )\n         cache_write_tokens = [redacted:secret-assignment]\n             getattr(details, \"cache_write_tokens\", 0) if details else 0\n         )"]},{"file":"tests/agent/test_usage_pricing.py","hunks":["@@ -39,6 +39,44 @@ def test_normalize_usage_openai_subtracts_cached_prompt_tokens():\n     assert normalized.output_tokens == 700\n \n \n+def test_normalize_usage_reads_deepseek_native_cache_hit_tokens():\n+    \"\"\"DeepSeek's native API (api.deepseek.com) reports context-cache hits as\n+    top-level prompt_cache_hit_tokens / prompt_cache_miss_tokens (with\n+    prompt_tokens = hit + miss), not OpenAI's nested\n+    prompt_tokens_details.cached_tokens. Before this fix, direct DeepSeek\n+    sessions always normalized to cache_read_tokens=0 — cache hits were\n+    invisible in accounting and billed at the full input rate (#61871).\"\"\"\n+    usage = SimpleNamespace(\n+        prompt_tokens=2000,\n+        completion_tokens=400,\n+        prompt_cache_hit_tokens=1500,\n+        prompt_cache_miss_tokens=500,\n+    )\n+\n+    normalized = normalize_usage(usage, provider=\"deepseek\", api_mode=\"chat_completions\")\n+\n+    assert normalized.cache_read_tokens == 1500\n+    # prompt_tokens includes cached; input = 2000 - 1500 = the miss bucket\n+    assert normalized.input_tokens == 500\n+    assert normalized.output_tokens == 400\n+\n+\n+def test_normalize_usage_nested_details_win_over_deepseek_top_level():\n+    \"\"\"When a proxy forwards both shapes, the OpenAI nested value wins and\n+    the DeepSeek top-level field is not double-read.\"\"\"\n+    usage = SimpleNamespace(\n+        prompt_tokens=2000,\n+        completion_tokens=100,\n+        prompt_tokens_details=[redacted:secret-assignment]\n+        prompt_cache_hit_tokens=1500,\n+    )\n+\n+    normalized = normalize_usage(usage, provider=\"deepseek\", api_mode=\"chat_completions\")\n+\n+    assert normalized.cache_read_tokens == 900\n+    assert normalized.input_tokens == 1100\n+\n+\n def test_normalize_usage_openai_reads_top_level_anthropic_cache_fields():\n     \"\"\"Some OpenAI-compatible proxies (OpenRouter, Cline) expose\n     Anthropic-style cache token counts at the top level of the usage object when"]},{"file":"tests/agent/transports/test_chat_completions.py","hunks":["@@ -1115,6 +1115,33 @@ def test_with_cache(self, transport):\n         result = transport.extract_cache_stats(r)\n         assert result == {\"cached_tokens\": 500, \"creation_tokens\": 100}\n \n+    def test_deepseek_native_top_level_cache_hit_tokens(self, transport):\n+        \"\"\"DeepSeek's native API (api.deepseek.com) reports cache hits as\n+        top-level prompt_cache_hit_tokens, not the OpenAI nested shape —\n+        the extractor must read it or direct DeepSeek sessions show 0%\n+        cache hit rate (#61871).\"\"\"\n+        r = SimpleNamespace(\n+            usage=SimpleNamespace(\n+                prompt_tokens_details=None,\n+                prompt_cache_hit_tokens=1500,\n+                prompt_cache_miss_tokens=500,\n+            )\n+        )\n+        result = transport.extract_cache_stats(r)\n+        assert result == {\"cached_tokens\": 1500, \"creation_tokens\": 0}\n+\n+    def test_nested_details_win_over_deepseek_top_level(self, transport):\n+        \"\"\"When both shapes are present, the OpenAI nested value wins.\"\"\"\n+        details = SimpleNamespace(cached_tokens=800, cache_write_tokens=0)\n+        r = SimpleNamespace(\n+            usage=SimpleNamespace(\n+                prompt_tokens_details=[redacted:secret-assignment]\n+                prompt_cache_hit_tokens=1500,\n+            )\n+        )\n+        result = transport.extract_cache_stats(r)\n+        assert result == {\"cached_tokens\": 800, \"creation_tokens\": 0}\n+\n \n class TestChatCompletionsGeminiNativeExtraBodyStrip:\n     \"\"\"Profile extra_body (e.g. Nous portal tags) must not reach a native\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}