set('agent.openai.model', 'test-model'); config()->set('agent.openai.temperature', 0.2); config()->set('agent.openai.top_p', 0.9); config()->set('agent.openai.include_usage', true); config()->set('agent.tools.tool_choice', 'auto'); $context = new AgentContext('run-1', 'session-1', 'system prompt', [ [ 'message_id' => 'm1', 'role' => Message::ROLE_USER, 'type' => 'user.prompt', 'content' => 'hello', 'payload' => [], 'seq' => 1, ], [ 'message_id' => 'm2', 'role' => Message::ROLE_AGENT, 'type' => 'agent.message', 'content' => 'hi', 'payload' => [], 'seq' => 2, ], ]); $payload = (new ChatCompletionsRequestBuilder(new ToolRegistry()))->build($context); $this->assertSame('test-model', $payload['model']); $this->assertTrue($payload['stream']); $this->assertSame(0.2, $payload['temperature']); $this->assertSame(0.9, $payload['top_p']); $this->assertSame(['include_usage' => true], $payload['stream_options']); $this->assertNotEmpty($payload['tools']); $this->assertSame('auto', $payload['tool_choice']); $this->assertSame([ ['role' => 'system', 'content' => 'system prompt'], ['role' => 'user', 'content' => 'hello'], ['role' => 'assistant', 'content' => 'hi'], ], $payload['messages']); } public function test_request_builder_maps_tool_messages(): void { $context = new AgentContext('run-1', 'session-1', 'system prompt', [ [ 'message_id' => 'm1', 'role' => Message::ROLE_USER, 'type' => 'user.prompt', 'content' => 'call tool', 'payload' => [], 'seq' => 1, ], [ 'message_id' => 'm2', 'role' => Message::ROLE_AGENT, 'type' => 'tool.call', 'content' => null, 'payload' => [ 'tool_call_id' => 'call_1', 'name' => 'get_time', 'arguments' => ['format' => 'c'], ], 'seq' => 2, ], [ 'message_id' => 'm3', 'role' => Message::ROLE_TOOL, 'type' => 'tool.result', 'content' => '2024-01-01', 'payload' => [ 'tool_call_id' => 'call_1', 'name' => 'get_time', 'output' => '2024-01-01', ], 'seq' => 3, ], ]); $payload = (new ChatCompletionsRequestBuilder(new ToolRegistry()))->build($context); $assistantMessage = collect($payload['messages'])->first(fn ($message) => isset($message['tool_calls'])); $toolResultMessage = collect($payload['messages'])->first(fn ($message) => ($message['role'] ?? '') === 'tool'); $this->assertNotNull($assistantMessage); $this->assertSame('assistant', $assistantMessage['role']); $this->assertSame('get_time', $assistantMessage['tool_calls'][0]['function']['name']); $this->assertNotNull($toolResultMessage); $this->assertSame('call_1', $toolResultMessage['tool_call_id']); } public function test_disable_tools_still_includes_definitions_when_history_has_tools(): void { $context = new AgentContext('run-1', 'session-1', 'system prompt', [ [ 'message_id' => 'm1', 'role' => Message::ROLE_USER, 'type' => 'user.prompt', 'content' => 'call tool', 'payload' => [], 'seq' => 1, ], [ 'message_id' => 'm2', 'role' => Message::ROLE_AGENT, 'type' => 'tool.call', 'content' => null, 'payload' => [ 'tool_call_id' => 'call_1', 'name' => 'get_time', 'arguments' => ['format' => 'c'], ], 'seq' => 2, ], [ 'message_id' => 'm3', 'role' => Message::ROLE_TOOL, 'type' => 'tool.result', 'content' => '2024-01-01', 'payload' => [ 'tool_call_id' => 'call_1', 'name' => 'get_time', 'output' => '2024-01-01', ], 'seq' => 3, ], ]); $payload = (new ChatCompletionsRequestBuilder(new ToolRegistry()))->build($context, [ 'disable_tools' => true, ]); $this->assertSame('none', $payload['tool_choice']); $this->assertNotEmpty($payload['tools']); } public function test_event_normalizer_maps_delta_and_done(): void { $normalizer = new OpenAiEventNormalizer(); $delta = json_encode([ 'choices' => [ [ 'delta' => ['content' => 'Hi'], 'finish_reason' => null, ], ], ]); $events = $normalizer->normalize($delta); $this->assertCount(1, $events); $this->assertSame(ProviderEventType::MessageDelta, $events[0]->type); $this->assertSame('Hi', $events[0]->payload['text']); $done = json_encode([ 'choices' => [ [ 'delta' => [], 'finish_reason' => 'stop', ], ], ]); $events = $normalizer->normalize($done); $this->assertCount(1, $events); $this->assertSame(ProviderEventType::Done, $events[0]->type); $this->assertSame('stop', $events[0]->payload['reason']); } public function test_event_normalizer_handles_invalid_json(): void { $normalizer = new OpenAiEventNormalizer(); $events = $normalizer->normalize('{invalid'); $this->assertCount(1, $events); $this->assertSame(ProviderEventType::Error, $events[0]->type); $this->assertSame('INVALID_JSON', $events[0]->payload['code']); } public function test_event_normalizer_handles_done_marker(): void { $normalizer = new OpenAiEventNormalizer(); $events = $normalizer->normalize('[DONE]'); $this->assertCount(1, $events); $this->assertSame(ProviderEventType::Done, $events[0]->type); } public function test_event_normalizer_handles_tool_delta(): void { $normalizer = new OpenAiEventNormalizer(); $toolDelta = json_encode([ 'choices' => [ [ 'delta' => [ 'tool_calls' => [ [ 'id' => 'call_1', 'index' => 0, 'function' => [ 'name' => 'get_time', 'arguments' => '{"format":"Y-m-d"}', ], ], ], ], 'finish_reason' => null, ], ], ]); $events = $normalizer->normalize($toolDelta); $this->assertCount(1, $events); $this->assertSame(ProviderEventType::ToolDelta, $events[0]->type); $this->assertSame('call_1', $events[0]->payload['tool_calls'][0]['id']); $this->assertSame('get_time', $events[0]->payload['tool_calls'][0]['name']); } public function test_stream_parser_splits_sse_events(): void { $stream = Utils::streamFor("data: {\"id\":1}\n\ndata: [DONE]\n\n"); $parser = new OpenAiStreamParser(5); $chunks = iterator_to_array($parser->parse($stream)); $this->assertSame(['{"id":1}', '[DONE]'], $chunks); } }