Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 4b9f108

Browse filesBrowse files
committed
Add OpenAI requests card for Laravel Pulse
1 parent b25394f commit 4b9f108
Copy full SHA for 4b9f108

File tree

Expand file treeCollapse file tree

9 files changed

+330
-5
lines changed
Filter options
Expand file treeCollapse file tree

9 files changed

+330
-5
lines changed

‎composer.json

Copy file name to clipboardExpand all lines: composer.json
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
},
1818
"require-dev": {
1919
"laravel/pint": "^1.13.6",
20+
"laravel/pulse": "^1.0.0",
2021
"pestphp/pest": "^2.8.2",
2122
"pestphp/pest-plugin-arch": "^2.2.2",
2223
"pestphp/pest-plugin-mock": "^2.0.0",
+104Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
<x-pulse::card :cols="$cols" :rows="$rows" :class="$class">
2+
<x-pulse::card-header
3+
name="{{ $this->label }}"
4+
title="Time: {{ number_format($time) }}ms; Run at: {{ $runAt }};"
5+
details="past {{ $this->periodForHumans() }}"
6+
>
7+
<x-slot:icon>
8+
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 24 24" role="img"
9+
xmlns="http://www.w3.org/2000/svg">
10+
<path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z"/>
11+
</svg>
12+
</x-slot:icon>
13+
<x-slot:actions>
14+
@if(!$this->type)
15+
<x-pulse::select
16+
wire:model.live="openaiRequests"
17+
label="By"
18+
:options="[
19+
'user' => 'Users',
20+
'endpoint' => 'API endpoint',
21+
]"
22+
class="flex-1"
23+
@change="loading = true"
24+
/>
25+
@endif
26+
</x-slot:actions>
27+
</x-pulse::card-header>
28+
29+
<x-pulse::scroll :expand="$expand" wire:poll.5s="">
30+
@if ($requests->isEmpty())
31+
<x-pulse::no-results/>
32+
@else
33+
@if($aggregate === 'user')
34+
<div class="grid grid-cols-1 @lg:grid-cols-2 @3xl:grid-cols-3 @6xl:grid-cols-4 gap-2">
35+
@foreach ($requests as $requestCount)
36+
<x-pulse::user-card wire:key="{{ $requestCount->user->id.$this->period }}"
37+
:name="$requestCount->user->name" :extra="$requestCount->user->extra">
38+
@if ($requestCount->user->avatar ?? false)
39+
<x-slot:avatar>
40+
<img height="32" width="32" src="{{ $requestCount->user->avatar }}" loading="lazy"
41+
class="rounded-full">
42+
</x-slot:avatar>
43+
@endif
44+
45+
<x-slot:stats>
46+
@php
47+
$sampleRate = $config['sample_rate'];
48+
@endphp
49+
50+
@if ($sampleRate < 1)
51+
<span title="Sample rate: {{ $sampleRate }}, Raw value: {{ number_format($requestCount->count) }}">~{{ number_format($requestCount->count * (1 / $sampleRate)) }}</span>
52+
@else
53+
{{ number_format($requestCount->count) }}
54+
@endif
55+
</x-slot:stats>
56+
</x-pulse::user-card>
57+
@endforeach
58+
</div>
59+
@else
60+
<x-pulse::table>
61+
<colgroup>
62+
<col width="0%"/>
63+
<col width="100%"/>
64+
<col width="0%"/>
65+
</colgroup>
66+
<x-pulse::thead>
67+
<tr>
68+
<x-pulse::th>Method</x-pulse::th>
69+
<x-pulse::th>Uri</x-pulse::th>
70+
<x-pulse::th class="text-right">Count</x-pulse::th>
71+
</tr>
72+
</x-pulse::thead>
73+
<tbody>
74+
@foreach ($requests->take(10) as $request)
75+
<tr class="h-2 first:h-0"></tr>
76+
<tr wire:key="{{ $request->method.$request->uri.$this->period }}">
77+
<x-pulse::td>
78+
<x-pulse::http-method-badge :method="$request->method"/>
79+
</x-pulse::td>
80+
<x-pulse::td class="overflow-hidden max-w-[1px]">
81+
<code class="block text-xs text-gray-900 dark:text-gray-100 truncate"
82+
title="{{ $request->uri }}">
83+
/{{ $request->uri }}
84+
</code>
85+
</x-pulse::td>
86+
<x-pulse::td numeric class="text-gray-700 dark:text-gray-300 font-bold">
87+
@if ($config['sample_rate'] < 1)
88+
<span title="Sample rate: {{ $config['sample_rate'] }}, Raw value: {{ number_format($request->count) }}">~{{ number_format($request->count * (1 / $config['sample_rate'])) }}</span>
89+
@else
90+
{{ number_format($request->count) }}
91+
@endif
92+
</x-pulse::td>
93+
</tr>
94+
@endforeach
95+
</tbody>
96+
</x-pulse::table>
97+
98+
@if ($requests->count() > 10)
99+
<div class="mt-2 text-xs text-gray-400 text-center">Limited to 10 entries</div>
100+
@endif
101+
@endif
102+
@endif
103+
</x-pulse::scroll>
104+
</x-pulse::card>

‎src/Facades/OpenAI.php

Copy file name to clipboardExpand all lines: src/Facades/OpenAI.php
-1Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
use Illuminate\Support\Facades\Facade;
88
use OpenAI\Contracts\ResponseContract;
99
use OpenAI\Laravel\Testing\OpenAIFake;
10-
use OpenAI\Resources\Assistants;
1110
use OpenAI\Responses\StreamResponse;
1211

1312
/**
+112Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
<?php
2+
3+
namespace OpenAI\Laravel\Pulse\Livewire;
4+
5+
use Illuminate\Contracts\Support\Renderable;
6+
use Illuminate\Support\Collection;
7+
use Illuminate\Support\Facades\Config;
8+
use Illuminate\Support\Facades\View;
9+
use Laravel\Pulse\Facades\Pulse;
10+
use Laravel\Pulse\Livewire\Card;
11+
use Laravel\Pulse\Livewire\Concerns\HasPeriod;
12+
use Laravel\Pulse\Livewire\Concerns\RemembersQueries;
13+
use Livewire\Attributes\Computed;
14+
use Livewire\Attributes\Lazy;
15+
use Livewire\Attributes\Url;
16+
use OpenAI\Laravel\Pulse\Recorders\OpenAIRequests;
17+
18+
/**
19+
* @internal
20+
*/
21+
#[Lazy]
22+
class OpenAIRequestsCard extends Card
23+
{
24+
use HasPeriod, RemembersQueries;
25+
26+
/**
27+
* The type of request aggregation to show.
28+
*
29+
* @var 'user'|'endpoint'|null
30+
*/
31+
public ?string $type = null;
32+
33+
/**
34+
* The openai requests type.
35+
*
36+
* @var 'user'|'endpoint'
37+
*/
38+
#[Url]
39+
public string $openaiRequests = 'user';
40+
41+
#[Computed]
42+
public function label(): string
43+
{
44+
return match ($this->type ?? $this->openaiRequests) {
45+
'user' => 'Top 10 OpenAI Users',
46+
'endpoint' => 'Top 10 OpenAI Endpoints',
47+
};
48+
}
49+
50+
/**
51+
* Render the component.
52+
*/
53+
public function render(): Renderable
54+
{
55+
$aggregate = $this->type ?? $this->openaiRequests;
56+
57+
[$requests, $time, $runAt] = $this->remember(
58+
function () use ($aggregate) {
59+
/** @var Collection<int, object{key: string, count: int}> $counts */
60+
$counts = Pulse::aggregate(
61+
match ($aggregate) {
62+
'user' => 'openai_request_handled_per_user',
63+
'endpoint' => 'openai_request_handled_per_endpoint',
64+
},
65+
'count', // @phpstan-ignore-line
66+
$this->periodAsInterval(),
67+
limit: 10,
68+
);
69+
70+
if ($aggregate === 'user') {
71+
/** @var Collection<int, array{id: string|int, name: string, email?: ?string, avatar?: ?string, extra?: ?string}> $users */
72+
$users = Pulse::resolveUsers($counts->pluck('key'));
73+
74+
return $counts->map(function ($row) use ($users) {
75+
$user = $users->firstWhere('id', $row->key);
76+
77+
return (object) [
78+
'user' => (object) [
79+
'id' => $row->key,
80+
'name' => $user['name'] ?? ($row->key === 'null' ? 'Guest' : 'Unknown'),
81+
'extra' => $user['extra'] ?? $user['email'] ?? '',
82+
'avatar' => $user['avatar'] ?? (($user['email'] ?? false)
83+
? sprintf('https://gravatar.com/avatar/%s?d=mp', hash('sha256', trim(strtolower($user['email']))))
84+
: null),
85+
],
86+
'count' => (int) $row->count,
87+
];
88+
});
89+
}
90+
91+
return $counts->map(function ($row) {
92+
[$method, $uri] = json_decode($row->key, flags: JSON_THROW_ON_ERROR); // @phpstan-ignore-line
93+
94+
return (object) [
95+
'uri' => $uri,
96+
'method' => $method,
97+
'count' => (int) $row->count,
98+
];
99+
});
100+
},
101+
$aggregate
102+
);
103+
104+
return View::make('openai-php::livewire.openai-requests', [
105+
'time' => $time,
106+
'runAt' => $runAt,
107+
'config' => Config::get('pulse.recorders.'.OpenAIRequests::class),
108+
'requests' => $requests,
109+
'aggregate' => $aggregate,
110+
]);
111+
}
112+
}
+69Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
namespace OpenAI\Laravel\Pulse\Recorders;
4+
5+
use Carbon\CarbonImmutable;
6+
use Illuminate\Config\Repository;
7+
use Laravel\Pulse\Pulse;
8+
use Laravel\Pulse\Recorders\Concerns\Groups;
9+
use Laravel\Pulse\Recorders\Concerns\Ignores;
10+
use Laravel\Pulse\Recorders\Concerns\Sampling;
11+
use OpenAI\Events\RequestHandled;
12+
13+
/**
14+
* @internal
15+
*/
16+
class OpenAIRequests
17+
{
18+
use Groups, Ignores, Sampling;
19+
20+
/**
21+
* The events to listen for.
22+
*
23+
* @var list<class-string>
24+
*/
25+
public array $listen = [
26+
RequestHandled::class,
27+
];
28+
29+
/**
30+
* Create a new recorder instance.
31+
*/
32+
public function __construct(
33+
protected Pulse $pulse,
34+
protected Repository $config,
35+
) {
36+
//
37+
}
38+
39+
/**
40+
* Record the request.
41+
*/
42+
public function record(RequestHandled $event): void
43+
{
44+
[$timestamp, $method, $uri, $userId] = [
45+
CarbonImmutable::now()->getTimestamp(),
46+
$event->payload->method->value,
47+
$event->payload->uri->toString(),
48+
$this->pulse->resolveAuthenticatedUserId(),
49+
];
50+
51+
$this->pulse->lazy(function () use ($timestamp, $method, $uri, $userId) {
52+
if (! $this->shouldSample() || $this->shouldIgnore($uri)) {
53+
return;
54+
}
55+
56+
$this->pulse->record(
57+
type: 'openai_request_handled_per_user',
58+
key: json_encode($userId, flags: JSON_THROW_ON_ERROR),
59+
timestamp: $timestamp,
60+
)->count();
61+
62+
$this->pulse->record(
63+
type: 'openai_request_handled_per_endpoint',
64+
key: json_encode([$method, $this->group($uri)], flags: JSON_THROW_ON_ERROR),
65+
timestamp: $timestamp,
66+
)->count();
67+
});
68+
}
69+
}

‎src/ServiceProvider.php

Copy file name to clipboardExpand all lines: src/ServiceProvider.php
+12-4Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,28 @@
44

55
namespace OpenAI\Laravel;
66

7+
use Illuminate\Container\Container;
78
use Illuminate\Contracts\Events\Dispatcher as DispatcherContract;
8-
use Illuminate\Contracts\Support\DeferrableProvider;
99
use Illuminate\Support\ServiceProvider as BaseServiceProvider;
10+
use Livewire\Livewire;
1011
use OpenAI;
1112
use OpenAI\Client;
1213
use OpenAI\Contracts\ClientContract;
1314
use OpenAI\Laravel\Commands\InstallCommand;
1415
use OpenAI\Laravel\Exceptions\ApiKeyIsMissing;
16+
use OpenAI\Laravel\Pulse\Livewire\OpenAIRequestsCard;
1517

1618
/**
1719
* @internal
1820
*/
19-
final class ServiceProvider extends BaseServiceProvider implements DeferrableProvider
21+
final class ServiceProvider extends BaseServiceProvider
2022
{
2123
/**
2224
* Register any application services.
2325
*/
2426
public function register(): void
2527
{
26-
$this->app->singleton(ClientContract::class, static function (): Client {
28+
$this->app->singleton(ClientContract::class, static function (Container $container): Client {
2729
$apiKey = config('openai.api_key');
2830
$organization = config('openai.organization');
2931

@@ -36,7 +38,7 @@ public function register(): void
3638
->withOrganization($organization)
3739
->withHttpHeader('OpenAI-Beta', 'assistants=v1')
3840
->withHttpClient(new \GuzzleHttp\Client(['timeout' => config('openai.request_timeout', 30)]))
39-
->withEventDispatcher(resolve(DispatcherContract::class)) // @phpstan-ignore-line
41+
->withEventDispatcher($container->make(DispatcherContract::class)) // @phpstan-ignore-line
4042
->make();
4143
});
4244

@@ -58,6 +60,12 @@ public function boot(): void
5860
InstallCommand::class,
5961
]);
6062
}
63+
64+
$this->loadViewsFrom(__DIR__.'/../resources/views', 'openai-php');
65+
66+
if (class_exists(Livewire::class)) {
67+
Livewire::component('openai.pulse.requests', OpenAIRequestsCard::class);
68+
}
6169
}
6270

6371
/**

‎tests/Arch.php

Copy file name to clipboardExpand all lines: tests/Arch.php
+3Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,12 @@
1717
->expect('OpenAI\Laravel\ServiceProvider')
1818
->toOnlyUse([
1919
'GuzzleHttp\Client',
20+
'Illuminate\Container\Container',
2021
'Illuminate\Support\ServiceProvider',
22+
'Livewire\Livewire',
2123
'OpenAI\Laravel',
2224
'OpenAI',
25+
'Illuminate\Contracts\Events\Dispatcher',
2326
'Illuminate\Contracts\Support\DeferrableProvider',
2427

2528
// helpers...

‎tests/Facades/OpenAI.php

Copy file name to clipboardExpand all lines: tests/Facades/OpenAI.php
+9Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
<?php
22

33
use Illuminate\Config\Repository;
4+
use Illuminate\Contracts\Events\Dispatcher;
45
use OpenAI\Laravel\Facades\OpenAI;
56
use OpenAI\Laravel\ServiceProvider;
67
use OpenAI\Resources\Completions;
78
use OpenAI\Responses\Completions\CreateResponse;
89
use PHPUnit\Framework\ExpectationFailedException;
10+
use Psr\EventDispatcher\EventDispatcherInterface;
911

1012
it('resolves resources', function () {
1113
$app = app();
@@ -16,6 +18,13 @@
1618
],
1719
]));
1820

21+
$app->bind(Dispatcher::class, fn () => new class implements EventDispatcherInterface
22+
{
23+
public function dispatch(object $event)
24+
{
25+
}
26+
});
27+
1928
(new ServiceProvider($app))->register();
2029

2130
OpenAI::setFacadeApplication($app);

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.