diff --git a/internal/types/types.go b/internal/types/types.go index d166965..34fda89 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -107,6 +107,14 @@ type PromptTokensDetails struct { // standard keys (prompt_tokens/completion_tokens/total_tokens). Standard // clients such as DSH and DevEco Code read the *_tokens fields. func (t TokenUsage) MarshalJSON() ([]byte, error) { + // Auto-generate prompt_tokens_details from legacy DeepSeek fields when + // the upstream adapter only set the standalone hit count (openai.lua + // does this in Lua, but other adapters or the standardSSEChunk fallback + // may not). dsh reads prompt_tokens_details.cached_tokens first. + pdetails := t.PromptTokensDetails + if pdetails == nil && t.PromptCacheHit > 0 { + pdetails = &PromptTokensDetails{CachedTokens: t.PromptCacheHit} + } return json.Marshal(struct { PromptTokens int `json:"prompt_tokens"` CompletionTokens int `json:"completion_tokens"` @@ -124,7 +132,7 @@ func (t TokenUsage) MarshalJSON() ([]byte, error) { Prompt: t.Prompt, Completion: t.Completion, Total: t.Total, - PromptTokensDetails: t.PromptTokensDetails, + PromptTokensDetails: pdetails, PromptCacheHitTokens: t.PromptCacheHit, PromptCacheMissTokens: t.PromptCacheMiss, }) diff --git a/internal/types/types_test.go b/internal/types/types_test.go new file mode 100644 index 0000000..bb4e8a1 --- /dev/null +++ b/internal/types/types_test.go @@ -0,0 +1,70 @@ +package types + +import ( + "encoding/json" + "testing" +) + +func TestTokenUsageMarshalCacheFields(t *testing.T) { + // DeepSeek-style: prompt_cache_hit_tokens + prompt_tokens_details auto-generated + u := TokenUsage{ + Prompt: 100, + Completion: 50, + Total: 150, + PromptCacheHit: 80, + PromptCacheMiss: 20, + } + b, _ := json.Marshal(u) + var out map[string]interface{} + json.Unmarshal(b, &out) + check := func(name string, want interface{}) { + got := out[name] + if got != want { + t.Errorf("%s = %v (want %v)", name, got, want) + } + } + check("prompt_tokens", 100.0) + check("completion_tokens", 50.0) + check("prompt_cache_hit_tokens", 80.0) + check("prompt_cache_miss_tokens", 20.0) + details := out["prompt_tokens_details"] + if details == nil { + t.Error("prompt_tokens_details is missing (should be set from prompt_cache_hit_tokens?)") + } else { + d := details.(map[string]interface{}) + if d["cached_tokens"] != 80.0 { + t.Errorf("cached_tokens = %v (want 80)", d["cached_tokens"]) + } + } + + // OpenAI-style: prompt_tokens_details.cached_tokens + u2 := TokenUsage{ + Prompt: 200, + Completion: 100, + Total: 300, + PromptTokensDetails: &PromptTokensDetails{ + CachedTokens: 150, + }, + } + b2, _ := json.Marshal(u2) + json.Unmarshal(b2, &out) + check2 := func(name string, want interface{}) { + got := out[name] + if got != want { + t.Errorf("OpenAI %s = %v (want %v)", name, got, want) + } + } + check2("prompt_tokens", 200.0) + details2 := out["prompt_tokens_details"] + if details2 == nil { + t.Error("OpenAI prompt_tokens_details is missing") + } else { + d := details2.(map[string]interface{}) + if d["cached_tokens"] != 150.0 { + t.Errorf("OpenAI cached_tokens = %v (want 150)", d["cached_tokens"]) + } + } + + t.Logf("DeepSeek JSON: %s", string(b)) + t.Logf("OpenAI JSON: %s", string(b2)) +}