1+ <?php
2+
3+ declare (strict_types=1 );
4+
5+ namespace PhpLlm \LlmChain \Tests \Platform \Bridge \Albert ;
6+
7+ use PhpLlm \LlmChain \Platform \Bridge \Albert \EmbeddingsModelClient ;
8+ use PhpLlm \LlmChain \Platform \Bridge \OpenAI \Embeddings ;
9+ use PhpLlm \LlmChain \Platform \Bridge \OpenAI \GPT ;
10+ use PhpLlm \LlmChain \Platform \Exception \InvalidArgumentException ;
11+ use PHPUnit \Framework \Attributes \CoversClass ;
12+ use PHPUnit \Framework \Attributes \DataProvider ;
13+ use PHPUnit \Framework \Attributes \Small ;
14+ use PHPUnit \Framework \Attributes \Test ;
15+ use PHPUnit \Framework \TestCase ;
16+ use Symfony \Component \HttpClient \MockHttpClient ;
17+ use Symfony \Component \HttpClient \Response \JsonMockResponse ;
18+
19+ #[CoversClass(EmbeddingsModelClient::class)]
20+ #[Small]
21+ final class EmbeddingsModelClientTest extends TestCase
22+ {
23+ #[Test]
24+ public function constructorThrowsExceptionForEmptyApiKey (): void
25+ {
26+ $ this ->expectException (InvalidArgumentException::class);
27+ $ this ->expectExceptionMessage ('The API key must not be empty. ' );
28+
29+ new EmbeddingsModelClient (
30+ new MockHttpClient (),
31+ '' ,
32+ 'https://albert.example.com/ '
33+ );
34+ }
35+
36+ #[Test]
37+ public function constructorThrowsExceptionForEmptyBaseUrl (): void
38+ {
39+ $ this ->expectException (InvalidArgumentException::class);
40+ $ this ->expectExceptionMessage ('The base URL must not be empty. ' );
41+
42+ new EmbeddingsModelClient (
43+ new MockHttpClient (),
44+ 'test-api-key ' ,
45+ ''
46+ );
47+ }
48+
49+ #[Test]
50+ public function supportsEmbeddingsModel (): void
51+ {
52+ $ client = new EmbeddingsModelClient (
53+ new MockHttpClient (),
54+ 'test-api-key ' ,
55+ 'https://albert.example.com/ '
56+ );
57+
58+ $ embeddingsModel = new Embeddings ('text-embedding-ada-002 ' );
59+ self ::assertTrue ($ client ->supports ($ embeddingsModel ));
60+ }
61+
62+ #[Test]
63+ public function doesNotSupportNonEmbeddingsModel (): void
64+ {
65+ $ client = new EmbeddingsModelClient (
66+ new MockHttpClient (),
67+ 'test-api-key ' ,
68+ 'https://albert.example.com/ '
69+ );
70+
71+ $ gptModel = new GPT ('gpt-3.5-turbo ' );
72+ self ::assertFalse ($ client ->supports ($ gptModel ));
73+ }
74+
75+ #[Test]
76+ #[DataProvider('requestDataProvider ' )]
77+ public function requestSendsCorrectHttpRequest (array |string $ payload , array $ options , array |string $ expectedJson ): void
78+ {
79+ $ capturedRequest = null ;
80+ $ httpClient = new MockHttpClient (function ($ method , $ url , $ options ) use (&$ capturedRequest ) {
81+ $ capturedRequest = ['method ' => $ method , 'url ' => $ url , 'options ' => $ options ];
82+ return new JsonMockResponse (['data ' => []]);
83+ });
84+
85+ $ client = new EmbeddingsModelClient (
86+ $ httpClient ,
87+ 'test-api-key ' ,
88+ 'https://albert.example.com/v1/ '
89+ );
90+
91+ $ model = new Embeddings ('text-embedding-ada-002 ' );
92+ $ response = $ client ->request ($ model , $ payload , $ options );
93+
94+ self ::assertNotNull ($ capturedRequest );
95+ self ::assertSame ('POST ' , $ capturedRequest ['method ' ]);
96+ self ::assertSame ('https://albert.example.com/v1/embeddings ' , $ capturedRequest ['url ' ]);
97+ self ::assertArrayHasKey ('normalized_headers ' , $ capturedRequest ['options ' ]);
98+ self ::assertArrayHasKey ('authorization ' , $ capturedRequest ['options ' ]['normalized_headers ' ]);
99+ self ::assertStringContainsString ('Bearer test-api-key ' , $ capturedRequest ['options ' ]['normalized_headers ' ]['authorization ' ][0 ]);
100+
101+ // Check JSON body - it might be in 'body' after processing
102+ if (isset ($ capturedRequest ['options ' ]['body ' ])) {
103+ $ actualJson = json_decode ($ capturedRequest ['options ' ]['body ' ], true );
104+ self ::assertEquals ($ expectedJson , $ actualJson );
105+ } else {
106+ self ::assertSame ($ expectedJson , $ capturedRequest ['options ' ]['json ' ]);
107+ }
108+ }
109+
110+ public static function requestDataProvider (): \Iterator
111+ {
112+ yield 'with array payload and no options ' => [
113+ 'payload ' => ['input ' => 'test text ' , 'model ' => 'text-embedding-ada-002 ' ],
114+ 'options ' => [],
115+ 'expectedJson ' => ['input ' => 'test text ' , 'model ' => 'text-embedding-ada-002 ' ],
116+ ];
117+
118+ yield 'with string payload and no options ' => [
119+ 'payload ' => 'test text ' ,
120+ 'options ' => [],
121+ 'expectedJson ' => 'test text ' ,
122+ ];
123+
124+ yield 'with array payload and options ' => [
125+ 'payload ' => ['input ' => 'test text ' , 'model ' => 'text-embedding-ada-002 ' ],
126+ 'options ' => ['dimensions ' => 1536 ],
127+ 'expectedJson ' => ['dimensions ' => 1536 , 'input ' => 'test text ' , 'model ' => 'text-embedding-ada-002 ' ],
128+ ];
129+
130+ yield 'options override payload values ' => [
131+ 'payload ' => ['input ' => 'test text ' , 'model ' => 'text-embedding-ada-002 ' ],
132+ 'options ' => ['model ' => 'text-embedding-3-small ' ],
133+ 'expectedJson ' => ['model ' => 'text-embedding-3-small ' , 'input ' => 'test text ' ],
134+ ];
135+ }
136+
137+ #[Test]
138+ public function requestHandlesBaseUrlWithoutTrailingSlash (): void
139+ {
140+ $ capturedUrl = null ;
141+ $ httpClient = new MockHttpClient (function ($ method , $ url ) use (&$ capturedUrl ) {
142+ $ capturedUrl = $ url ;
143+ return new JsonMockResponse (['data ' => []]);
144+ });
145+
146+ $ client = new EmbeddingsModelClient (
147+ $ httpClient ,
148+ 'test-api-key ' ,
149+ 'https://albert.example.com/v1 '
150+ );
151+
152+ $ model = new Embeddings ('text-embedding-ada-002 ' );
153+ $ client ->request ($ model , ['input ' => 'test ' ]);
154+
155+ self ::assertSame ('https://albert.example.com/v1/embeddings ' , $ capturedUrl );
156+ }
157+
158+ #[Test]
159+ public function requestHandlesBaseUrlWithTrailingSlash (): void
160+ {
161+ $ capturedUrl = null ;
162+ $ httpClient = new MockHttpClient (function ($ method , $ url ) use (&$ capturedUrl ) {
163+ $ capturedUrl = $ url ;
164+ return new JsonMockResponse (['data ' => []]);
165+ });
166+
167+ $ client = new EmbeddingsModelClient (
168+ $ httpClient ,
169+ 'test-api-key ' ,
170+ 'https://albert.example.com/v1/ '
171+ );
172+
173+ $ model = new Embeddings ('text-embedding-ada-002 ' );
174+ $ client ->request ($ model , ['input ' => 'test ' ]);
175+
176+ self ::assertSame ('https://albert.example.com/v1/embeddings ' , $ capturedUrl );
177+ }
178+ }
0 commit comments