Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions src/Providers/DeepSeek/Handlers/Stream.php
Original file line number Diff line number Diff line change
Expand Up @@ -357,9 +357,15 @@ protected function extractUsage(array $data): ?Usage
return null;
}

$totalPrompt = (int) data_get($usage, 'prompt_tokens', 0);
$cacheHit = (int) data_get($usage, 'prompt_cache_hit_tokens', 0);
$reasoning = (int) data_get($usage, 'completion_tokens_details.reasoning_tokens', 0);

return new Usage(
promptTokens: (int) data_get($usage, 'prompt_tokens', 0),
completionTokens: (int) data_get($usage, 'completion_tokens', 0)
promptTokens: max(0, $totalPrompt - $cacheHit),
completionTokens: (int) data_get($usage, 'completion_tokens', 0),
cacheReadInputTokens: $cacheHit > 0 ? $cacheHit : null,
thoughtTokens: $reasoning > 0 ? $reasoning : null,
);
}

Expand Down
10 changes: 8 additions & 2 deletions src/Providers/DeepSeek/Handlers/Text.php
Original file line number Diff line number Diff line change
Expand Up @@ -122,15 +122,21 @@ protected function sendRequest(Request $request): array
*/
protected function addStep(array $data, Request $request, array $toolResults = []): void
{
$totalPrompt = (int) (data_get($data, 'usage.prompt_tokens') ?? 0);
$cacheHit = (int) (data_get($data, 'usage.prompt_cache_hit_tokens') ?? 0);
$reasoning = (int) (data_get($data, 'usage.completion_tokens_details.reasoning_tokens') ?? 0);

$this->responseBuilder->addStep(new Step(
text: data_get($data, 'choices.0.message.content') ?? '',
finishReason: $this->mapFinishReason($data),
toolCalls: ToolCallMap::map(data_get($data, 'choices.0.message.tool_calls', [])),
toolResults: $toolResults,
providerToolCalls: [],
usage: new Usage(
data_get($data, 'usage.prompt_tokens'),
data_get($data, 'usage.completion_tokens'),
promptTokens: max(0, $totalPrompt - $cacheHit),
completionTokens: (int) (data_get($data, 'usage.completion_tokens') ?? 0),
cacheReadInputTokens: $cacheHit > 0 ? $cacheHit : null,
thoughtTokens: $reasoning > 0 ? $reasoning : null,
),
meta: new Meta(
id: data_get($data, 'id'),
Expand Down
9 changes: 7 additions & 2 deletions tests/Providers/DeepSeek/TextTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,13 @@
expect($secondStep->messages[1]->toolCalls[1]->name)->toBe('weather');
expect($secondStep->messages[2])->toBeInstanceOf(ToolResultMessage::class);

// Assert usage
expect($response->usage->promptTokens)->toBe(507);
// Assert usage. promptTokens is now the FRESH portion (prompt_tokens minus
// prompt_cache_hit_tokens) so cost trackers can apply the cached rate to the
// hit portion separately. Aggregated across both steps:
// step 1 fixture: prompt_tokens=220, prompt_cache_hit_tokens=192 → fresh 28, cached 192
// step 2 fixture: prompt_tokens=287, prompt_cache_hit_tokens=256 → fresh 31, cached 256
expect($response->usage->promptTokens)->toBe(59);
expect($response->usage->cacheReadInputTokens)->toBe(448);
expect($response->usage->completionTokens)->toBe(76);

// Assert response
Expand Down