Skip to content

Commit 3b6c40e

Browse files
committed
Allow custom streaming client
1 parent 055a60e commit 3b6c40e

File tree

3 files changed

+135
-13
lines changed

3 files changed

+135
-13
lines changed

README.md

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ _This library is not developed or endorsed by Google._
2626
- [Streaming Chat Session](#streaming-chat-session)
2727
- [Tokens counting](#tokens-counting)
2828
- [Listing models](#listing-models)
29+
- [Advanced Usages](#advanced-usages)
30+
- [Safety Settings and Generation Configuration](#safety-settings-and-generation-configuration)
31+
- [Using your own HTTP client](#using-your-own-http-client)
32+
- [Using your own HTTP client for streaming responses](#using-your-own-http-client-for-streaming-responses)
2933

3034
## Installation
3135

@@ -173,7 +177,7 @@ $callback = function (GenerateContentResponse $response): void {
173177

174178
$client->geminiPro()->generateContentStream(
175179
$callback,
176-
new TextPart('PHP in less than 100 chars')
180+
[new TextPart('PHP in less than 100 chars')],
177181
);
178182
// Response #0
179183
// PHP: a versatile, general-purpose scripting language for web development, popular for
@@ -287,3 +291,79 @@ print_r($response->models);
287291
// )
288292
//]
289293
```
294+
295+
### Advanced Usages
296+
297+
#### Safety Settings and Generation Configuration
298+
299+
```php
300+
$client = new GeminiAPI\Client('GEMINI_API_KEY');
301+
$safetySetting = new GeminiAPI\SafetySetting(
302+
HarmCategory::HARM_CATEGORY_HATE_SPEECH,
303+
HarmBlockThreshold::BLOCK_LOW_AND_ABOVE,
304+
);
305+
$generationConfig = (new GeminiAPI\GenerationConfig())
306+
->withCandidateCount(1)
307+
->withMaxOutputTokens(40)
308+
->withTemperature(0.5)
309+
->withTopK(40)
310+
->withTopP(0.6)
311+
->withStopSequences(['STOP']);
312+
313+
$response = $client->geminiPro()
314+
->withAddedSafetySetting($safetySetting)
315+
->withGenerationConfig($generationConfig)
316+
->generateContent(
317+
new TextPart('PHP in less than 100 chars')
318+
);
319+
```
320+
321+
#### Using your own HTTP client
322+
323+
```php
324+
$guzzle = new GuzzleHttp\Client([
325+
'proxy' => 'http://localhost:8125',
326+
]);
327+
$client = new GeminiAPI\Client('GEMINI_API_KEY', $guzzle);
328+
329+
$response = $client->geminiPro()->generateContent(
330+
new TextPart('PHP in less than 100 chars')
331+
);
332+
```
333+
334+
#### Using your own HTTP client for streaming responses
335+
336+
> Requires `curl` extension to be enabled
337+
338+
Since streaming responses are fetched using `curl` extension, they cannot use the custom HTTP client passed to the Gemini Client.
339+
You need to pass a `CurlHandler` if you want to override connection options.
340+
341+
The following curl options will be overwritten by the Gemini Client.
342+
343+
- `CURLOPT_URL`
344+
- `CURLOPT_POST`
345+
- `CURLOPT_POSTFIELDS`
346+
- `CURLOPT_WRITEFUNCTION`
347+
348+
You can also pass the headers you want to be used in the requests.
349+
350+
```php
351+
$client = new GeminiAPI\Client('GEMINI_API_KEY');
352+
353+
$callback = function (GenerateContentResponse $response): void {
354+
print $response->text();
355+
};
356+
357+
$ch = curl_init();
358+
curl_setopt($ch, CURLOPT_PROXY, 'http://localhost:8125');
359+
360+
$client->withRequestHeaders([
361+
'User-Agent' => 'My Gemini-backed app'
362+
])
363+
->geminiPro()
364+
->generateContentStream(
365+
$callback,
366+
[new TextPart('PHP in less than 100 chars')],
367+
$ch,
368+
);
369+
```

src/Client.php

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
use Psr\Http\Message\StreamFactoryInterface;
2828
use RuntimeException;
2929

30+
use function array_map;
3031
use function curl_close;
3132
use function curl_exec;
3233
use function curl_getinfo;
@@ -35,10 +36,17 @@
3536
use function extension_loaded;
3637
use function json_decode;
3738
use function sprintf;
39+
use function strtolower;
3840

3941
class Client implements GeminiClientInterface
4042
{
4143
private string $baseUrl = 'https://generativelanguage.googleapis.com';
44+
45+
/**
46+
* @var array<string, string>
47+
*/
48+
private array $requestHeaders = [];
49+
4250
public function __construct(
4351
private readonly string $apiKey,
4452
private ?HttpClientInterface $client = null,
@@ -88,13 +96,16 @@ public function generateContent(GenerateContentRequest $request): GenerateConten
8896
}
8997

9098
/**
99+
* @param GenerateContentStreamRequest $request
91100
* @param callable(GenerateContentResponse): void $callback
101+
* @param CurlHandle|null $curl
92102
* @throws BadMethodCallException
93103
* @throws RuntimeException
94104
*/
95105
public function generateContentStream(
96106
GenerateContentStreamRequest $request,
97107
callable $callback,
108+
?CurlHandle $curl = null,
98109
): void {
99110
if (!extension_loaded('curl')) {
100111
throw new BadMethodCallException('Gemini API requires `curl` extension for streaming responses');
@@ -120,18 +131,25 @@ public function generateContentStream(
120131
);
121132
};
122133

123-
$ch = curl_init("{$this->baseUrl}/v1/{$request->getOperation()}");
134+
$ch = $curl ?? curl_init();
124135

125136
if ($ch === false) {
126137
throw new RuntimeException('Gemini API cannot initialize streaming content request');
127138
}
128139

140+
$headers = $this->requestHeaders + [
141+
'content-type' => 'application/json',
142+
self::API_KEY_HEADER_NAME => $this->apiKey,
143+
];
144+
$headerLines = [];
145+
foreach ($headers as $name => $value) {
146+
$headerLines[] = "{$name}: {$value}";
147+
}
148+
149+
curl_setopt($ch, CURLOPT_URL, "{$this->baseUrl}/v1/{$request->getOperation()}");
129150
curl_setopt($ch, CURLOPT_POST, true);
130151
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($request));
131-
curl_setopt($ch, CURLOPT_HTTPHEADER, [
132-
'Content-type: application/json',
133-
self::API_KEY_HEADER_NAME . ": {$this->apiKey}",
134-
]);
152+
curl_setopt($ch, CURLOPT_HTTPHEADER, $headerLines);
135153
curl_setopt($ch, CURLOPT_WRITEFUNCTION, $writeFunction);
136154
curl_exec($ch);
137155
curl_close($ch);
@@ -179,6 +197,18 @@ public function withBaseUrl(string $baseUrl): self
179197
return $clone;
180198
}
181199

200+
/**
201+
* @param array<string, string> $headers
202+
* @return self
203+
*/
204+
public function withRequestHeaders(array $headers): self
205+
{
206+
$clone = clone $this;
207+
$clone->requestHeaders = array_map(strtolower(...), $headers);
208+
209+
return $clone;
210+
}
211+
182212
/**
183213
* @throws ClientExceptionInterface
184214
*/
@@ -193,6 +223,10 @@ private function doRequest(RequestInterface $request): string
193223
->createRequest($request->getHttpMethod(), $uri)
194224
->withAddedHeader(self::API_KEY_HEADER_NAME, $this->apiKey);
195225

226+
foreach ($this->requestHeaders as $name => $value) {
227+
$httpRequest = $httpRequest->withAddedHeader($name, $value);
228+
}
229+
196230
$payload = $request->getHttpPayload();
197231
if (!empty($payload)) {
198232
$stream = $this->streamFactory->createStream($payload);

src/GenerativeModel.php

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace GeminiAPI;
66

77
use BadMethodCallException;
8+
use CurlHandle;
89
use GeminiAPI\Enums\ModelName;
910
use GeminiAPI\Enums\Role;
1011
use GeminiAPI\Requests\CountTokensRequest;
@@ -62,26 +63,33 @@ public function generateContentWithContents(array $contents): GenerateContentRes
6263

6364
/**
6465
* @param callable(GenerateContentResponse): void $callback
65-
* @param PartInterface ...$parts
66+
* @param PartInterface[] $parts
67+
* @param CurlHandle|null $ch
6668
* @return void
67-
* @throws BadMethodCallException
6869
*/
6970
public function generateContentStream(
7071
callable $callback,
71-
PartInterface ...$parts,
72+
array $parts,
73+
?CurlHandle $ch = null,
7274
): void {
75+
$this->ensureArrayOfType($parts, PartInterface::class);
76+
7377
$content = new Content($parts, Role::User);
7478

75-
$this->generateContentStreamWithContents($callback, [$content]);
79+
$this->generateContentStreamWithContents($callback, [$content], $ch);
7680
}
7781

7882
/**
7983
* @param callable(GenerateContentResponse): void $callback
8084
* @param Content[] $contents
85+
* @param CurlHandle|null $ch
8186
* @return void
8287
*/
83-
public function generateContentStreamWithContents(callable $callback, array $contents): void
84-
{
88+
public function generateContentStreamWithContents(
89+
callable $callback,
90+
array $contents,
91+
?CurlHandle $ch = null,
92+
): void {
8593
$this->ensureArrayOfType($contents, Content::class);
8694

8795
$request = new GenerateContentStreamRequest(
@@ -91,7 +99,7 @@ public function generateContentStreamWithContents(callable $callback, array $con
9199
$this->generationConfig,
92100
);
93101

94-
$this->client->generateContentStream($request, $callback);
102+
$this->client->generateContentStream($request, $callback, $ch);
95103
}
96104

97105
public function startChat(): ChatSession

0 commit comments

Comments
 (0)