From bda1232d5cc585ab68af7653ffefbc88ca20b088 Mon Sep 17 00:00:00 2001 From: eliot lauger Date: Tue, 15 Sep 2026 11:00:28 +0200 Subject: [PATCH 1/4] feat: add missing fields in usage response --- src/Usage.php | 68 ++++++++++++++++++++++++++++++++++-- src/UsageProduct.php | 53 ++++++++++++++++++++++++++++ tests/UsageTest.php | 83 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 201 insertions(+), 3 deletions(-) create mode 100644 src/UsageProduct.php create mode 100644 tests/UsageTest.php diff --git a/src/Usage.php b/src/Usage.php index 74b7230..b5a0095 100644 --- a/src/Usage.php +++ b/src/Usage.php @@ -23,6 +23,11 @@ class Usage */ public $character; + /** + * @var UsageDetail|null Usage details for characters used by this API key only. + */ + public $apiKeyCharacter; + /** * @var UsageDetail|null Usage details for documents. */ @@ -33,22 +38,67 @@ class Usage */ public $teamDocument; + /** + * @var UsageDetail|null Usage details for speech-to-text, in minutes. + */ + public $speechToTextMinutes; + + /** + * @var UsageDetail|null Usage details for speech-to-text, in milliseconds. + */ + public $speechToTextMilliseconds; + + /** + * @var UsageDetail|null Usage details for speech-to-speech, in minutes. + */ + public $speechToSpeechMinutes; + + /** + * @var UsageProduct[] Usage broken down per product, empty if the account does not report it. + */ + public $products; + + /** + * @var string|null Start of the billing period, as an ISO 8601 timestamp. + */ + public $startTime; + + /** + * @var string|null End of the billing period, as an ISO 8601 timestamp. + */ + public $endTime; + /** * @return bool True if any usage type limit has been reached or passed, otherwise false. */ public function anyLimitReached(): bool { - return ($this->character !== null && $this->character->limitReached()) || - ($this->document !== null && $this->document->limitReached()) || - ($this->teamDocument !== null && $this->teamDocument->limitReached()); + $details = [ + $this->character, + $this->apiKeyCharacter, + $this->document, + $this->teamDocument, + $this->speechToTextMinutes, + $this->speechToSpeechMinutes, + ]; + /** @var UsageDetail|null $detail */ + foreach ($details as $detail) { + if ($detail !== null && $detail->limitReached()) { + return true; + } + } + return false; } public function __toString(): string { $list = [ 'Characters' => $this->character, + 'API key characters' => $this->apiKeyCharacter, 'Documents' => $this->document, 'Team documents' => $this->teamDocument, + 'Speech-to-text minutes' => $this->speechToTextMinutes, + 'Speech-to-speech minutes' => $this->speechToSpeechMinutes, ]; $result = 'Usage this billing period:'; foreach ($list as $label => $detail) { @@ -71,8 +121,20 @@ public function __construct(string $content) } $this->character = $this->buildUsageDetail('character', $json); + $this->apiKeyCharacter = $this->buildUsageDetail('api_key_character', $json); $this->document = $this->buildUsageDetail('document', $json); $this->teamDocument = $this->buildUsageDetail('team_document', $json); + $this->speechToTextMinutes = $this->buildUsageDetail('speech_to_text_minutes', $json); + $this->speechToTextMilliseconds = $this->buildUsageDetail('speech_to_text_milliseconds', $json); + $this->speechToSpeechMinutes = $this->buildUsageDetail('speech_to_speech_minutes', $json); + $this->products = array_map( + function (array $product): UsageProduct { + return new UsageProduct($product); + }, + $json['products'] ?? [] + ); + $this->startTime = $json['start_time'] ?? null; + $this->endTime = $json['end_time'] ?? null; } private function buildUsageDetail(string $prefix, array $json): ?UsageDetail diff --git a/src/UsageProduct.php b/src/UsageProduct.php new file mode 100644 index 0000000..39ac3b3 --- /dev/null +++ b/src/UsageProduct.php @@ -0,0 +1,53 @@ +productType = $json['product_type'] ?? ''; + $this->billingUnit = $json['billing_unit'] ?? ''; + $this->apiKeyUnitCount = $json['api_key_unit_count'] ?? 0; + $this->accountUnitCount = $json['account_unit_count'] ?? 0; + $this->apiKeyCharacterCount = $json['api_key_character_count'] ?? 0; + $this->characterCount = $json['character_count'] ?? 0; + } +} diff --git a/tests/UsageTest.php b/tests/UsageTest.php new file mode 100644 index 0000000..3eee793 --- /dev/null +++ b/tests/UsageTest.php @@ -0,0 +1,83 @@ +assertEquals(636, $usage->apiKeyCharacter->count); + $this->assertEquals(30, $usage->speechToTextMinutes->count); + $this->assertEquals(600, $usage->speechToSpeechMinutes->limit); + $this->assertEquals(0, $usage->speechToTextMilliseconds->limit); + $this->assertNull($usage->document); + $this->assertEquals('2025-05-13T09:18:42Z', $usage->startTime); + $this->assertEquals('speechToText', $usage->products[1]->productType); + $this->assertEquals('minutes', $usage->products[1]->billingUnit); + $this->assertEquals(30, $usage->products[1]->apiKeyUnitCount); + // Milliseconds are reported with a limit of 0, they must not count as a reached limit. + $this->assertFalse($usage->anyLimitReached()); + $this->assertStringContainsString('Speech-to-text minutes: 30 of 600', strval($usage)); + } + + public function testSpeechLimitReached() + { + $json = str_replace('"speech_to_speech_minutes_count":12', '"speech_to_speech_minutes_count":600', self::PRO_RESPONSE); + $this->assertTrue((new Usage($json))->anyLimitReached()); + } + + public function testFreeAccountResponseUnchanged() + { + $usage = new Usage('{"character_count":180,"character_limit":500000}'); + + $this->assertEquals(180, $usage->character->count); + $this->assertNull($usage->apiKeyCharacter); + $this->assertNull($usage->speechToTextMinutes); + $this->assertSame([], $usage->products); + $this->assertNull($usage->startTime); + $this->assertFalse($usage->anyLimitReached()); + } +} From a5079eb64edff1f02202ee456678cf5cbfc482a1 Mon Sep 17 00:00:00 2001 From: eliot lauger Date: Tue, 15 Sep 2026 11:11:38 +0200 Subject: [PATCH 2/4] test: fix speech limit fixture replacement --- tests/UsageTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/UsageTest.php b/tests/UsageTest.php index 3eee793..f4b8eb1 100644 --- a/tests/UsageTest.php +++ b/tests/UsageTest.php @@ -65,7 +65,8 @@ public function testProAccountFields() public function testSpeechLimitReached() { - $json = str_replace('"speech_to_speech_minutes_count":12', '"speech_to_speech_minutes_count":600', self::PRO_RESPONSE); + $json = str_replace('"speech_to_speech_minutes_count": 12', '"speech_to_speech_minutes_count": 600', self::PRO_RESPONSE); + $this->assertNotEquals($json, self::PRO_RESPONSE); $this->assertTrue((new Usage($json))->anyLimitReached()); } From 58152654af7d0832922923ba6195db68ce131ca1 Mon Sep 17 00:00:00 2001 From: eliot lauger Date: Tue, 15 Sep 2026 11:50:44 +0200 Subject: [PATCH 3/4] refactor: change order to fallback for have limit reached --- src/Usage.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Usage.php b/src/Usage.php index b5a0095..27745c7 100644 --- a/src/Usage.php +++ b/src/Usage.php @@ -74,8 +74,8 @@ class Usage public function anyLimitReached(): bool { $details = [ - $this->character, $this->apiKeyCharacter, + $this->character, $this->document, $this->teamDocument, $this->speechToTextMinutes, @@ -93,8 +93,8 @@ public function anyLimitReached(): bool public function __toString(): string { $list = [ - 'Characters' => $this->character, 'API key characters' => $this->apiKeyCharacter, + 'Characters' => $this->character, 'Documents' => $this->document, 'Team documents' => $this->teamDocument, 'Speech-to-text minutes' => $this->speechToTextMinutes, From 775f8e6a720357d6cda2422ecbbb30bd8d492dcf Mon Sep 17 00:00:00 2001 From: eliot lauger Date: Tue, 15 Sep 2026 14:03:28 +0200 Subject: [PATCH 4/4] test: add new test with limit reached --- tests/UsageTest.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/UsageTest.php b/tests/UsageTest.php index f4b8eb1..3442c61 100644 --- a/tests/UsageTest.php +++ b/tests/UsageTest.php @@ -70,6 +70,17 @@ public function testSpeechLimitReached() $this->assertTrue((new Usage($json))->anyLimitReached()); } + public function testApiKeyAndAccountLimitReached() + { + $usage = new Usage( + '{"character_count":500001,"character_limit":500000,"api_key_character_count":500001,"api_key_character_limit":500000}' + ); + + $this->assertTrue($usage->character->limitReached()); + $this->assertTrue($usage->apiKeyCharacter->limitReached()); + $this->assertTrue($usage->anyLimitReached()); + } + public function testFreeAccountResponseUnchanged() { $usage = new Usage('{"character_count":180,"character_limit":500000}');