Skip to content
Merged
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
34 changes: 21 additions & 13 deletions src/Responses/Chat/CreateResponseUsageCompletionTokensDetails.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,34 +8,42 @@ final class CreateResponseUsageCompletionTokensDetails
{
private function __construct(
public readonly ?int $audioTokens,
public readonly int $reasoningTokens,
public readonly int $acceptedPredictionTokens,
public readonly int $rejectedPredictionTokens
public readonly ?int $reasoningTokens,
public readonly ?int $acceptedPredictionTokens,
public readonly ?int $rejectedPredictionTokens
) {}

/**
* @param array{audio_tokens?:int, reasoning_tokens:int, accepted_prediction_tokens:int, rejected_prediction_tokens:int} $attributes
* @param array{audio_tokens?:int|null, reasoning_tokens?:int|null, accepted_prediction_tokens?:int|null, rejected_prediction_tokens?:int|null} $attributes
*/
public static function from(array $attributes): self
{
return new self(
$attributes['audio_tokens'] ?? null,
$attributes['reasoning_tokens'],
$attributes['accepted_prediction_tokens'],
$attributes['rejected_prediction_tokens'],
$attributes['reasoning_tokens'] ?? null,
$attributes['accepted_prediction_tokens'] ?? null,
$attributes['rejected_prediction_tokens'] ?? null,
);
}

/**
* @return array{audio_tokens?:int, reasoning_tokens:int, accepted_prediction_tokens:int, rejected_prediction_tokens:int}
* @return array{audio_tokens?:int, reasoning_tokens?:int, accepted_prediction_tokens?:int, rejected_prediction_tokens?:int}
*/
public function toArray(): array
{
$result = [
'reasoning_tokens' => $this->reasoningTokens,
'accepted_prediction_tokens' => $this->acceptedPredictionTokens,
'rejected_prediction_tokens' => $this->rejectedPredictionTokens,
];
$result = [];

if (! is_null($this->reasoningTokens)) {
$result['reasoning_tokens'] = $this->reasoningTokens;
}

if (! is_null($this->acceptedPredictionTokens)) {
$result['accepted_prediction_tokens'] = $this->acceptedPredictionTokens;
}

if (! is_null($this->rejectedPredictionTokens)) {
$result['rejected_prediction_tokens'] = $this->rejectedPredictionTokens;
}

if (! is_null($this->audioTokens)) {
$result['audio_tokens'] = $this->audioTokens;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ private function __construct(
) {}

/**
* @param array{audio_tokens?:int, cached_tokens?: int} $attributes
* @param array{audio_tokens?:int|null, cached_tokens?:int} $attributes
*/
public static function from(array $attributes): self
{
Expand Down
142 changes: 142 additions & 0 deletions tests/Fixtures/Chat.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,148 @@ function chatCompletion(): array
];
}

/**
* @return array<string, mixed>
*/
function chatCompletionOpenRouter(): array
{
return [
'id' => 'gen-123',
'object' => 'chat.completion',
'created' => 1744873707,
'model' => 'mistral/ministral-8b',
'choices' => [
[
'index' => 0,
'message' => [
'role' => 'assistant',
'content' => 'Hello! How can I assist you today?',
],
'logprobs' => null,
'finish_reason' => 'stop',
],
],
'usage' => [
'prompt_tokens' => 13,
'completion_tokens' => 20,
'total_tokens' => 33,
],
];
}

/**
* @return array<string, mixed>
*/
function chatCompletionOpenRouterOpenAI(): array
{
return [
'id' => 'gen-123',
'provider' => 'OpenAI',
'model' => 'openai/gpt-4o-mini',
'object' => 'chat.completion',
'created' => 1744900650,
'system_fingerprint' => 'fp_0392822090',
'choices' => [
[
'index' => 0,
'message' => [
'role' => 'assistant',
'content' => 'Hello! How can I assist you today?',
'refusal' => null,
'reasoning' => null,
],
'logprobs' => null,
'finish_reason' => 'stop',
'native_finish_reason' => 'stop',
],
],
'usage' => [
'prompt_tokens' => 21,
'completion_tokens' => 21,
'total_tokens' => 42,
'prompt_tokens_details' => [
'cached_tokens' => 0,
],
'completion_tokens_details' => [
'reasoning_tokens' => 0,
],
],
];
}

/**
* @return array<string, mixed>
*/
function chatCompletionOpenRouterGoogle(): array
{
return [
'id' => 'gen-123',
'provider' => 'Google',
'model' => 'google/gemini-2.5-pro-preview-03-25',
'object' => 'chat.completion',
'created' => 1744910839,
'choices' => [
[
'index' => 0,
'message' => [
'role' => 'assistant',
'content' => 'Hello there! I\'m a large language model, trained by Google.',
'refusal' => null,
'reasoning' => null,
],
'logprobs' => null,
'finish_reason' => 'stop',
'native_finish_reason' => 'STOP',
],
],
'usage' => [
'prompt_tokens' => 10,
'completion_tokens' => 138,
'total_tokens' => 148,
],
];
}

/**
* @return array<string, mixed>
*/
function chatCompletionOpenRouterXAI(): array
{
return [
'id' => 'gen-123',
'provider' => 'xAI',
'model' => 'x-ai/grok-3-mini-beta',
'object' => 'chat.completion',
'created' => 1744911228,
'system_fingerprint' => 'fp_d133ae3397',
'choices' => [
[
'index' => 0,
'message' => [
'role' => 'assistant',
'content' => 'Hello! I\'m Grok, an AI model created by xAI.',
'refusal' => null,
'reasoning' => 'First, the user is asking "Hello! what model are you?"',
],
'logprobs' => null,
'finish_reason' => 'stop',
'native_finish_reason' => 'stop',
],
],
'usage' => [
'prompt_tokens' => 21,
'completion_tokens' => 36,
'total_tokens' => 392,
'prompt_tokens_details' => [
'cached_tokens' => 0,
],
'completion_tokens_details' => [
'reasoning_tokens' => 335,
],
],
];
}

/**
* @return array<string, mixed>
*/
Expand Down
64 changes: 64 additions & 0 deletions tests/Responses/Chat/CreateResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -203,3 +203,67 @@
->function->name->toBe('get_current_weather')
->function->arguments->toBe("{\n \"location\": \"Boston, MA\"\n}");
});

test('from (OpenRouter)', function () {
$completion = CreateResponse::from(chatCompletionOpenRouter(), meta());

expect($completion)
->toBeInstanceOf(CreateResponse::class)
->id->toBe('gen-123')
->object->toBe('chat.completion')
->created->toBe(1744873707)
->model->toBe('mistral/ministral-8b')
->systemFingerprint->toBeNull()
->choices->toBeArray()->toHaveCount(1)
->choices->each->toBeInstanceOf(CreateResponseChoice::class)
->usage->toBeInstanceOf(CreateResponseUsage::class)
->meta()->toBeInstanceOf(MetaInformation::class);
});

test('from (OpenRouter OpenAI)', function () {
$completion = CreateResponse::from(chatCompletionOpenRouterOpenAI(), meta());

expect($completion)
->toBeInstanceOf(CreateResponse::class)
->id->toBe('gen-123')
->object->toBe('chat.completion')
->created->toBe(1744900650)
->model->toBe('openai/gpt-4o-mini')
->systemFingerprint->toBe('fp_0392822090')
->choices->toBeArray()->toHaveCount(1)
->choices->each->toBeInstanceOf(CreateResponseChoice::class)
->usage->toBeInstanceOf(CreateResponseUsage::class)
->meta()->toBeInstanceOf(MetaInformation::class);
});

test('from (OpenRouter Google)', function () {
$completion = CreateResponse::from(chatCompletionOpenRouterGoogle(), meta());

expect($completion)
->toBeInstanceOf(CreateResponse::class)
->id->toBe('gen-123')
->object->toBe('chat.completion')
->created->toBe(1744910839)
->model->toBe('google/gemini-2.5-pro-preview-03-25')
->systemFingerprint->toBeNull()
->choices->toBeArray()->toHaveCount(1)
->choices->each->toBeInstanceOf(CreateResponseChoice::class)
->usage->toBeInstanceOf(CreateResponseUsage::class)
->meta()->toBeInstanceOf(MetaInformation::class);
});

test('from (OpenRouter xAI)', function () {
$completion = CreateResponse::from(chatCompletionOpenRouterXAI(), meta());

expect($completion)
->toBeInstanceOf(CreateResponse::class)
->id->toBe('gen-123')
->object->toBe('chat.completion')
->created->toBe(1744911228)
->model->toBe('x-ai/grok-3-mini-beta')
->systemFingerprint->toBe('fp_d133ae3397')
->choices->toBeArray()->toHaveCount(1)
->choices->each->toBeInstanceOf(CreateResponseChoice::class)
->usage->toBeInstanceOf(CreateResponseUsage::class)
->meta()->toBeInstanceOf(MetaInformation::class);
});
30 changes: 30 additions & 0 deletions tests/Responses/Chat/CreateResponseChoice.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,36 @@
->finishReason->toBeNull();
});

test('from OpenRouter OpenAI response', function () {
$result = CreateResponseChoice::from(chatCompletionOpenRouterOpenAI()['choices'][0]);

expect($result)
->index->toBe(0)
->message->toBeInstanceOf(CreateResponseMessage::class)
->logprobs->toBeNull()
->finishReason->toBe('stop');
});

test('from OpenRouter Google response', function () {
$result = CreateResponseChoice::from(chatCompletionOpenRouterGoogle()['choices'][0]);

expect($result)
->index->toBe(0)
->message->toBeInstanceOf(CreateResponseMessage::class)
->logprobs->toBeNull()
->finishReason->toBe('stop');
});

test('from OpenRouter xAI response', function () {
$result = CreateResponseChoice::from(chatCompletionOpenRouterXAI()['choices'][0]);

expect($result)
->index->toBe(0)
->message->toBeInstanceOf(CreateResponseMessage::class)
->logprobs->toBeNull()
->finishReason->toBe('stop');
});

test('to array', function () {
$result = CreateResponseChoice::from(chatCompletion()['choices'][0]);

Expand Down
44 changes: 44 additions & 0 deletions tests/Responses/Chat/CreateResponseUsage.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,50 @@
->completionTokensDetails->toBeInstanceOf(CreateResponseUsageCompletionTokensDetails::class);
});

test('from (OpenRouter)', function () {
$result = CreateResponseUsage::from(chatCompletionOpenRouter()['usage']);

expect($result)
->promptTokens->toBe(13)
->completionTokens->toBe(20)
->totalTokens->toBe(33)
->promptTokensDetails->toBeNull()
->completionTokensDetails->toBeNull();
});

test('from (OpenRouter OpenAI)', function () {
$result = CreateResponseUsage::from(chatCompletionOpenRouterOpenAI()['usage']);

expect($result)
->promptTokens->toBe(21)
->completionTokens->toBe(21)
->totalTokens->toBe(42)
->promptTokensDetails->toBeInstanceOf(CreateResponseUsagePromptTokensDetails::class)
->completionTokensDetails->toBeInstanceOf(CreateResponseUsageCompletionTokensDetails::class);
});

test('from (OpenRouter Google)', function () {
$result = CreateResponseUsage::from(chatCompletionOpenRouterGoogle()['usage']);

expect($result)
->promptTokens->toBe(10)
->completionTokens->toBe(138)
->totalTokens->toBe(148)
->promptTokensDetails->toBeNull()
->completionTokensDetails->toBeNull();
});

test('from (OpenRouter xAI)', function () {
$result = CreateResponseUsage::from(chatCompletionOpenRouterXAI()['usage']);

expect($result)
->promptTokens->toBe(21)
->completionTokens->toBe(36)
->totalTokens->toBe(392)
->promptTokensDetails->toBeInstanceOf(CreateResponseUsagePromptTokensDetails::class)
->completionTokensDetails->toBeInstanceOf(CreateResponseUsageCompletionTokensDetails::class);
});

test('to array', function () {
$result = CreateResponseUsage::from(chatCompletion()['usage']);

Expand Down