Skip to content

Commit 93e19c7

Browse files
committed
Implement hf_token support, viewApi, and unit tests
- Wire hf_token as Authorization Bearer header on all HTTP and WebSocket requests via RemoteClient - Add viewApi() method to Client that calls GET /info endpoint - Add 16 unit tests using Guzzle MockHandler (client construction, auth headers, viewApi, predict, client options, events, DTOs) - Replace live API tests with deterministic mocked tests - Update README with comprehensive documentation and examples
1 parent f4d6861 commit 93e19c7

5 files changed

Lines changed: 481 additions & 62 deletions

File tree

README.md

Lines changed: 148 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,11 @@ A PHP client to call [Gradio](https://www.gradio.app) APIs.
1010
- [x] HTTP and WS support
1111
- [x] `predict`
1212
- [x] getConfig
13-
- [ ] client options
14-
- [ ] hf_token support
15-
- [ ] viewApi
13+
- [x] client options
14+
- [x] hf_token support
15+
- [x] viewApi
1616
- [x] Finish event system
17-
- [ ] Add tests
18-
- [ ] Add more examples
19-
- [ ] Add documentation
17+
- [x] Add tests
2018

2119
## Installation
2220

@@ -28,13 +26,157 @@ composer require sergix44/gradio-client-php
2826

2927
## Usage
3028

29+
### Basic Usage
30+
3131
```php
3232
use SergiX44\Gradio\Client;
3333

3434
$client = new Client('https://my-special.hf.space');
3535

3636
$result = $client->predict(['arg', 1, 2], apiName: 'myFunction');
3737

38+
$outputs = $result->getOutputs(); // returns array of all outputs
39+
$first = $result->getOutput(0); // returns a single output by index
40+
```
41+
42+
### Hugging Face Token Authentication
43+
44+
To access private Hugging Face Spaces, pass your HF token:
45+
46+
```php
47+
use SergiX44\Gradio\Client;
48+
49+
$client = new Client('https://my-private.hf.space', hfToken: 'hf_your_token_here');
50+
51+
$result = $client->predict(['hello'], apiName: 'chat');
52+
```
53+
54+
The token is automatically sent as a Bearer token in the `Authorization` header on all HTTP and WebSocket requests.
55+
56+
### Viewing Available API Endpoints
57+
58+
Use `viewApi()` to inspect the available API endpoints, their parameters, and return types:
59+
60+
```php
61+
use SergiX44\Gradio\Client;
62+
63+
$client = new Client('https://my-special.hf.space');
64+
65+
$apiInfo = $client->viewApi();
66+
67+
// Returns an array with 'named_endpoints' and 'unnamed_endpoints'
68+
// Each endpoint includes parameter and return type information
69+
print_r($apiInfo);
70+
```
71+
72+
### Client Options
73+
74+
You can pass custom HTTP client options (Guzzle options) to customize the underlying HTTP client:
75+
76+
```php
77+
use SergiX44\Gradio\Client;
78+
79+
$client = new Client('https://my-special.hf.space', httpClientOptions: [
80+
'timeout' => 30,
81+
'headers' => [
82+
'X-Custom-Header' => 'value',
83+
],
84+
'proxy' => 'http://proxy.example.com:8080',
85+
]);
86+
```
87+
88+
### Using Function Index
89+
90+
If you know the function index instead of the API name, you can use `fnIndex`:
91+
92+
```php
93+
$result = $client->predict(['input'], fnIndex: 0);
94+
```
95+
96+
### Raw Response
97+
98+
To get the raw decoded response instead of an `Output` DTO:
99+
100+
```php
101+
$result = $client->predict(['input'], apiName: 'myFunction', raw: true);
102+
// $result is an associative array
103+
```
104+
105+
### Event System
106+
107+
You can register callbacks for various events during the prediction process:
108+
109+
```php
110+
use SergiX44\Gradio\Client;
111+
use SergiX44\Gradio\DTO\Messages\Estimation;
112+
use SergiX44\Gradio\DTO\Messages\ProcessCompleted;
113+
114+
$client = new Client('https://my-special.hf.space');
115+
116+
// Called when a prediction is submitted
117+
$client->onSubmit(function (array $payload) {
118+
echo "Submitted!\n";
119+
});
120+
121+
// Called when queue position is estimated
122+
$client->onQueueEstimation(function (Estimation $estimation) {
123+
echo "Queue position: {$estimation->rank}\n";
124+
});
125+
126+
// Called when processing starts
127+
$client->onProcessStarts(function () {
128+
echo "Processing started\n";
129+
});
130+
131+
// Called when processing completes (success or failure)
132+
$client->onProcessCompleted(function (ProcessCompleted $message) {
133+
echo "Completed: " . ($message->success ? 'success' : 'failed') . "\n";
134+
});
135+
136+
// Called only on success
137+
$client->onProcessSuccess(function (ProcessCompleted $message) {
138+
$output = $message->output;
139+
});
140+
141+
// Called only on failure
142+
$client->onProcessFailed(function (ProcessCompleted $message) {
143+
echo "Failed!\n";
144+
});
145+
146+
// Called when the queue is full
147+
$client->onQueueFull(function () {
148+
echo "Queue is full!\n";
149+
});
150+
151+
// Called during streaming/generating
152+
$client->onProcessGenerating(function () {
153+
echo "Generating...\n";
154+
});
155+
156+
$result = $client->predict(['input'], apiName: 'myFunction');
157+
```
158+
159+
### Accessing Config
160+
161+
```php
162+
$config = $client->getConfig();
163+
164+
echo $config->version; // Gradio version
165+
echo $config->protocol; // 'sse_v3', 'ws', etc.
166+
echo $config->title; // App title
167+
```
168+
169+
### File Upload
170+
171+
You can pass file paths or resources as arguments, and they will be automatically encoded as base64:
172+
173+
```php
174+
// Using a file path
175+
$result = $client->predict(['/path/to/image.png'], apiName: 'classify');
176+
177+
// Using a resource
178+
$stream = fopen('/path/to/audio.mp3', 'r');
179+
$result = $client->predict([$stream], apiName: 'transcribe');
38180
```
39181

40182
## Testing

src/Client.php

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,21 +29,20 @@ class Client extends RemoteClient
2929

3030
private const HTTP_CONFIG = 'config';
3131

32+
private const HTTP_API_INFO = 'info';
33+
3234
protected Config $config;
3335

3436
private string $sessionHash;
3537

3638
private array $endpoints = [];
3739

38-
private ?string $hfToken;
39-
4040
public function __construct(string $src, ?string $hfToken = null, ?Config $config = null, array $httpClientOptions = [])
4141
{
42-
parent::__construct($src, $httpClientOptions);
42+
parent::__construct($src, $hfToken, $httpClientOptions);
4343
$this->config = $config ?? $this->http('get', self::HTTP_CONFIG, dto: Config::class);
4444
$this->loadEndpoints($this->config->dependencies);
4545
$this->sessionHash = substr(md5(microtime()), 0, 11);
46-
$this->hfToken = $hfToken;
4746
}
4847

4948
protected function loadEndpoints(array $dependencies): void
@@ -62,6 +61,11 @@ public function getConfig(): Config
6261
return $this->config;
6362
}
6463

64+
public function viewApi(): array
65+
{
66+
return $this->http('get', self::HTTP_API_INFO);
67+
}
68+
6569
public function predict(array $arguments, ?string $apiName = null, ?int $fnIndex = null, bool $raw = false, ?int $triggerId = null): Output|array|null
6670
{
6771
if ($apiName === null && $fnIndex === null) {
@@ -129,19 +133,13 @@ private function preparePayload(array $arguments): array
129133
}, $arguments);
130134
}
131135

132-
/**
133-
* @throws GradioException
134-
* @throws QueueFullException
135-
* @throws \JsonException
136-
*/
137136
private function websocketLoop(Endpoint $endpoint, array $payload): ?Output
138137
{
139138
$ws = $this->ws(self::QUEUE_JOIN);
140139

141140
while (true) {
142141
$data = $ws->receive();
143142

144-
// why sometimes $data is null?
145143
if ($data === null) {
146144
continue;
147145
}
@@ -231,10 +229,7 @@ private function sseLoop(Endpoint $endpoint, array $payload, string $protocol, ?
231229
continue;
232230
}
233231

234-
// read second \n
235232
$response->getBody()->read(1);
236-
237-
// remove data:
238233
$buffer = str_replace('data: ', '', $buffer);
239234
$message = $this->hydrator->hydrateWithJson(Message::class, $buffer);
240235

src/Client/RemoteClient.php

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ abstract class RemoteClient extends RegisterEvents
1717

1818
protected HydratorInterface $hydrator;
1919

20-
public function __construct(string $src, array $httpClientOptions = [])
20+
private ?string $hfToken;
21+
22+
public function __construct(string $src, ?string $hfToken = null, array $httpClientOptions = [])
2123
{
2224
if (
2325
! str_starts_with($src, 'http://') &&
@@ -29,15 +31,22 @@ public function __construct(string $src, array $httpClientOptions = [])
2931
}
3032

3133
$this->src = str_ends_with($src, '/') ? $src : "{$src}/";
34+
$this->hfToken = $hfToken;
3235

3336
$this->hydrator = new Hydrator();
3437

38+
$defaultHeaders = [
39+
'User-Agent' => 'gradio_client_php/1.0',
40+
'Accept' => 'application/json',
41+
];
42+
43+
if ($this->hfToken !== null) {
44+
$defaultHeaders['Authorization'] = 'Bearer ' . $this->hfToken;
45+
}
46+
3547
$this->httpClient = new Guzzle(array_merge([
3648
'base_uri' => str_replace('ws', 'http', $this->src),
37-
'headers' => [
38-
'User-Agent' => 'gradio_client_php/1.0',
39-
'Accept' => 'application/json',
40-
],
49+
'headers' => $defaultHeaders,
4150
], $httpClientOptions));
4251
}
4352

@@ -59,6 +68,10 @@ protected function httpRaw(string $method, string $uri, array $params = [], arra
5968

6069
protected function ws(string $uri, array $options = []): EnhancedClient
6170
{
71+
if ($this->hfToken !== null && ! isset($options['headers']['Authorization'])) {
72+
$options['headers']['Authorization'] = 'Bearer ' . $this->hfToken;
73+
}
74+
6275
return new EnhancedClient(str_replace('http', 'ws', $this->src).$uri, $options);
6376
}
6477

0 commit comments

Comments
 (0)