assertEquals('openai', $service->name()); } public function test_default_model_returns_configured_model(): void { $service = new OpenAIService('test-api-key', 'gpt-4o'); $this->assertEquals('gpt-4o', $service->defaultModel()); } public function test_default_model_uses_gpt4o_mini_when_not_specified(): void { $service = new OpenAIService('test-api-key'); $this->assertEquals('gpt-4o-mini', $service->defaultModel()); } public function test_is_available_returns_true_with_api_key(): void { $service = new OpenAIService('test-api-key'); $this->assertTrue($service->isAvailable()); } public function test_is_available_returns_false_without_api_key(): void { $service = new OpenAIService(''); $this->assertFalse($service->isAvailable()); } // ========================================================================= // Generate Tests // ========================================================================= public function test_generate_returns_agentic_response(): void { Http::fake([ self::API_URL => Http::response([ 'id' => 'chatcmpl-123', 'object' => 'chat.completion', 'model' => 'gpt-4o-mini', 'choices' => [ [ 'index' => 0, 'message' => [ 'role' => 'assistant', 'content' => 'Hello, world!', ], 'finish_reason' => 'stop', ], ], 'usage' => [ 'prompt_tokens' => 10, 'completion_tokens' => 5, 'total_tokens' => 15, ], ], 200), ]); $service = new OpenAIService('test-api-key'); $response = $service->generate('You are helpful.', 'Say hello'); $this->assertInstanceOf(AgenticResponse::class, $response); $this->assertEquals('Hello, world!', $response->content); $this->assertEquals('gpt-4o-mini', $response->model); $this->assertEquals(10, $response->inputTokens); $this->assertEquals(5, $response->outputTokens); $this->assertEquals('stop', $response->stopReason); } public function test_generate_sends_correct_request_body(): void { Http::fake([ self::API_URL => Http::response([ 'model' => 'gpt-4o-mini', 'choices' => [ ['message' => ['content' => 'Response'], 'finish_reason' => 'stop'], ], 'usage' => ['prompt_tokens' => 10, 'completion_tokens' => 5], ], 200), ]); $service = new OpenAIService('test-api-key'); $service->generate('System prompt', 'User prompt'); Http::assertSent(function ($request) { $body = $request->data(); return $body['messages'][0]['role'] === 'system' && $body['messages'][0]['content'] === 'System prompt' && $body['messages'][1]['role'] === 'user' && $body['messages'][1]['content'] === 'User prompt' && $body['model'] === 'gpt-4o-mini' && $body['max_tokens'] === 4096 && $body['temperature'] === 1.0; }); } public function test_generate_uses_custom_config(): void { Http::fake([ self::API_URL => Http::response([ 'model' => 'gpt-4o', 'choices' => [ ['message' => ['content' => 'Response'], 'finish_reason' => 'stop'], ], 'usage' => ['prompt_tokens' => 10, 'completion_tokens' => 5], ], 200), ]); $service = new OpenAIService('test-api-key'); $service->generate('System', 'User', [ 'model' => 'gpt-4o', 'max_tokens' => 8192, 'temperature' => 0.5, ]); Http::assertSent(function ($request) { $body = $request->data(); return $body['model'] === 'gpt-4o' && $body['max_tokens'] === 8192 && $body['temperature'] === 0.5; }); } public function test_generate_sends_correct_headers(): void { Http::fake([ self::API_URL => Http::response([ 'model' => 'gpt-4o-mini', 'choices' => [ ['message' => ['content' => 'Response'], 'finish_reason' => 'stop'], ], 'usage' => ['prompt_tokens' => 10, 'completion_tokens' => 5], ], 200), ]); $service = new OpenAIService('test-api-key-123'); $service->generate('System', 'User'); Http::assertSent(function ($request) { return $request->hasHeader('Authorization', 'Bearer test-api-key-123') && $request->hasHeader('Content-Type', 'application/json'); }); } public function test_generate_tracks_duration(): void { Http::fake([ self::API_URL => Http::response([ 'model' => 'gpt-4o-mini', 'choices' => [ ['message' => ['content' => 'Response'], 'finish_reason' => 'stop'], ], 'usage' => ['prompt_tokens' => 10, 'completion_tokens' => 5], ], 200), ]); $service = new OpenAIService('test-api-key'); $response = $service->generate('System', 'User'); $this->assertIsInt($response->durationMs); $this->assertGreaterThanOrEqual(0, $response->durationMs); } public function test_generate_includes_raw_response(): void { $rawResponse = [ 'id' => 'chatcmpl-123', 'model' => 'gpt-4o-mini', 'choices' => [ ['message' => ['content' => 'Response'], 'finish_reason' => 'stop'], ], 'usage' => ['prompt_tokens' => 10, 'completion_tokens' => 5], ]; Http::fake([ self::API_URL => Http::response($rawResponse, 200), ]); $service = new OpenAIService('test-api-key'); $response = $service->generate('System', 'User'); $this->assertEquals('chatcmpl-123', $response->raw['id']); } // ========================================================================= // Error Handling Tests // ========================================================================= public function test_generate_throws_on_client_error(): void { Http::fake([ self::API_URL => Http::response([ 'error' => ['message' => 'Invalid API key'], ], 401), ]); $service = new OpenAIService('invalid-key'); $this->expectException(RuntimeException::class); $this->expectExceptionMessage('OpenAI API error'); $service->generate('System', 'User'); } public function test_generate_retries_on_rate_limit(): void { Http::fake([ self::API_URL => Http::sequence() ->push(['error' => ['message' => 'Rate limited']], 429) ->push([ 'model' => 'gpt-4o-mini', 'choices' => [ ['message' => ['content' => 'Success after retry'], 'finish_reason' => 'stop'], ], 'usage' => ['prompt_tokens' => 10, 'completion_tokens' => 5], ], 200), ]); $service = new OpenAIService('test-api-key'); $response = $service->generate('System', 'User'); $this->assertEquals('Success after retry', $response->content); } public function test_generate_retries_on_server_error(): void { Http::fake([ self::API_URL => Http::sequence() ->push(['error' => ['message' => 'Server error']], 500) ->push([ 'model' => 'gpt-4o-mini', 'choices' => [ ['message' => ['content' => 'Success after retry'], 'finish_reason' => 'stop'], ], 'usage' => ['prompt_tokens' => 10, 'completion_tokens' => 5], ], 200), ]); $service = new OpenAIService('test-api-key'); $response = $service->generate('System', 'User'); $this->assertEquals('Success after retry', $response->content); } public function test_generate_throws_after_max_retries(): void { Http::fake([ self::API_URL => Http::response(['error' => ['message' => 'Server error']], 500), ]); $service = new OpenAIService('test-api-key'); $this->expectException(RuntimeException::class); $service->generate('System', 'User'); } // ========================================================================= // Stream Tests // ========================================================================= public function test_stream_is_generator(): void { // Create a simple SSE stream response $stream = "data: {\"choices\": [{\"delta\": {\"content\": \"Hello\"}}]}\n\n"; $stream .= "data: {\"choices\": [{\"delta\": {\"content\": \" world\"}}]}\n\n"; $stream .= "data: [DONE]\n\n"; Http::fake([ self::API_URL => Http::response($stream, 200, ['Content-Type' => 'text/event-stream']), ]); $service = new OpenAIService('test-api-key'); $generator = $service->stream('System', 'User'); $this->assertInstanceOf(\Generator::class, $generator); } public function test_stream_sends_stream_flag(): void { Http::fake([ self::API_URL => Http::response('', 200), ]); $service = new OpenAIService('test-api-key'); iterator_to_array($service->stream('System', 'User')); Http::assertSent(function ($request) { return $request->data()['stream'] === true; }); } // ========================================================================= // Response Handling Edge Cases // ========================================================================= public function test_handles_empty_choices(): void { Http::fake([ self::API_URL => Http::response([ 'model' => 'gpt-4o-mini', 'choices' => [], 'usage' => ['prompt_tokens' => 10, 'completion_tokens' => 0], ], 200), ]); $service = new OpenAIService('test-api-key'); $response = $service->generate('System', 'User'); $this->assertEquals('', $response->content); } public function test_handles_missing_usage_data(): void { Http::fake([ self::API_URL => Http::response([ 'model' => 'gpt-4o-mini', 'choices' => [ ['message' => ['content' => 'Response'], 'finish_reason' => 'stop'], ], ], 200), ]); $service = new OpenAIService('test-api-key'); $response = $service->generate('System', 'User'); $this->assertEquals(0, $response->inputTokens); $this->assertEquals(0, $response->outputTokens); } public function test_handles_missing_finish_reason(): void { Http::fake([ self::API_URL => Http::response([ 'model' => 'gpt-4o-mini', 'choices' => [ ['message' => ['content' => 'Response']], ], 'usage' => ['prompt_tokens' => 10, 'completion_tokens' => 5], ], 200), ]); $service = new OpenAIService('test-api-key'); $response = $service->generate('System', 'User'); $this->assertNull($response->stopReason); } public function test_handles_null_content(): void { Http::fake([ self::API_URL => Http::response([ 'model' => 'gpt-4o-mini', 'choices' => [ ['message' => ['content' => null], 'finish_reason' => 'stop'], ], 'usage' => ['prompt_tokens' => 10, 'completion_tokens' => 0], ], 200), ]); $service = new OpenAIService('test-api-key'); $response = $service->generate('System', 'User'); $this->assertEquals('', $response->content); } }