Obsługa API od Open AI i omówienie Batch processing w PHP
GPT (Generative Pre-trained Transformer) to rodzina modeli językowych opracowanych przez OpenAI, które potrafią rozumieć i generować tekst zbliżony do ludzkiego. Modele GPT są trenowane na ogromnych zbiorach danych i wykorzystywane do odpowiadania na pytania, tłumaczenia, generowania treści, podsumowań czy automatyzacji rozmów. Obsługuje polecenia wielojęzyczne, przetwarza obrazy czy dźwięk. GPT jest dziś podstawą wielu narzędzi AI, chatbotów i rozwiązań wspierających codzienną pracę programistów, marketerów oraz twórców treści.
W tym artykule przyjrzymy się tylko niektórym możliwością jakie oferują rozwiązania od Open AI oraz zbudujemy klienta w PHP.
Lista przydatnych linków z oficjalnej dokumentacji od Open AI.
Klient
Zbudujmy prostego klienta, którego później w miarę potrzeby rozbudujemy.
<?php
namespace OpenAI;
class Client
{
private $baseUrl = 'https://api.openai.com/v1/';
private $apiKey;
public function __construct($apiKey)
{
$this->apiKey = $apiKey;
}
public function request(string $method,string $endpoint, array $params = []): array
{
$url = $this->baseUrl . $endpoint;
$headers = [
'Authorization: Bearer ' . $this->apiKey,
'Content-Type: application/json',
];
$curl = curl_init();
$method = strtoupper($method);
// Ustaw metodę i parametry zgodnie z metodą HTTP
switch ($method) {
case 'POST':
curl_setopt($curl, CURLOPT_POST, true);
if (!empty($params)) {
curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($params));
}
break;
case 'GET':
if (!empty($params)) {
$url .= (strpos($url, '?') === false ? '?' : '&') . http_build_query($params);
}
curl_setopt($curl, CURLOPT_HTTPGET, true);
break;
default:
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);
if (!empty($params)) {
curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($params));
}
break;
}
curl_setopt_array($curl, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_TIMEOUT => 30,
]);
$response = curl_exec($curl);
$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
$error = curl_error($curl);
curl_close($curl);
if ($error) {
throw new \Exception("cURL Error: " . $error);
}
$res = json_decode($response, true);
if ($httpCode >= 200 && $httpCode < 300) {
return $res;
}
$msg = $res['error']['message'] ?? 'Unknown API error';
throw new \Exception(
"OpenAI API Error ($httpCode): $msg\n\nResponse: " . print_r($res, true),
$httpCode
);
}
}
?>Nie będę się rozwodził nad budową API, gdyż robiłem to wielokrotnie. W dużym skrócie – korzystamy z cURL, którym wysyłamy zapytania pod endpoint do konkretnego modelu i obsługujemy odpowiedź w formacie JSON. Przetestujmy API.
$client = new Client('API_KEY');
try {
$response = $client->request( 'POST', 'responses', [
'model' => 'gpt-4o',
'input' => 'Napisz żart o PHP.'
]);
echo $response['output'][0]['content'][0]['text'];
//print_r($response);
} catch (\Exception $e) {
echo "Błąd: " . $e->getMessage();
}W odpowiedzi otrzymamy żart „na poziomie” GPT-4o 😉
Jak się nazywa programista PHP na snowboardzie?
Sliding-Scale!
Modele
Aktualna lista modeli znajduje się w dokumentacji. Poniżej w menu mamy zaś ceny (za milion tokenów). W praktyce milion tokenów to naprawdę sporo zapytań. Korzystam z API od bardzo dawna i głównie z topowych modeli i większość zapytań liczę w ułamkach centa, choć to zależy od wielu czynników. W każdym momencie możemy sprawdzić łączne koszty jakie ponieśliśmy oraz ile kosztowało dane użycie.
Rodzaje modeli
- tekstowe np. 4.1, 4o itd.
- tekstowe (flex processing) – o3 i o4-mini – charakteryzują się zmienną stawką za token w zależności od poziomu skomplikowania zapytania
- audio – do generowania dźwięku np. gpt-4o-audio-preview
- graficzne -do generowania obrazów (gpt-image-1)
- z opcją szkolenia na własnych danych (fine-tuning) – np. gpt-4o-2024-08-06
- do kategoryzacji, porównania itd. (embeddings) np. text-embedding-3-large
- do moderacji np. omni-moderation-latest (na dzień pisania tego artykułu są darmowe)
Endpoint responses
Aby zintegrować modele GPT i funkcje sztucznej inteligencji OpenAI z własnym projektem, musisz komunikować się z odpowiednimi „endpointami” API. Każdy endpoint odpowiada za inny zakres możliwości – od prostego generowania tekstu, przez obsługę multimodalną (tekst, obrazy, dźwięk), aż po zaawansowane funkcje narzędzi i integracji z zewnętrznymi systemami.
W najnowszych wersjach OpenAI coraz większy nacisk kładziony jest na uniwersalne i multimodalne interfejsy, które pozwalają realizować zaawansowane workflow w jednej rozmowie – niezależnie od rodzaju danych wejściowych czy potrzebnych narzędzi.
responses to aktualnie najnowszy i najbardziej zaawansowany endpoint OpenAI, łączący generowanie tekstu, obrazów oraz korzystanie z narzędzi w jednym interfejsie.
Zastępuje dotychczasowe /chat/completions oraz /images/generations w zastosowaniach multimodalnych i z narzędziami.
Przykładowo – aby prowadzić konwersację musieliśmy poprzednio wysyłać wszystkie wysłane i odebrane wiadomości przy każdym zapytaniu. Aktualnie wystarczy uzupełnić parametr previous_response_id w którym podajemy id ostatniej odpowiedzi.
Batch processing
Batch processing (przetwarzanie wsadowe) oznacza wysyłanie wielu zapytań naraz w jednej paczce – zamiast wykonywać pojedyncze żądania dla każdego promptu/osobnej wiadomości, możesz zrealizować całą serię operacji na jednym połączeniu z API.
OpenAI udostępnia specjalny endpoint /v1/batches, gdzie możesz przesłać plik lub listę żądań. Batch nie zwraca odpowiedzi natychmiast – wysyłasz zadanie i dostajesz batch_id, a po zakończeniu możesz pobrać wyniki.
Korzystając z tej metody zwykle zmniejszamy nasze koszty dwukrotnie, ale odpowiedź możemy odebrać do 24 godzin.
Aby skorzystać z tej opcji musimy najpierw utworzyć plik w formacie jsonl, a następnie go załadować na serwer i podpiąć pod zapytanie.
Zmodyfikujmy metodę request. Dodajmy dwa parametry – contentType oraz jsonRequest. Pierwszy pozwoli zmienić nam rodzaj przesyłanych danych, a drugi określi czy dane wysyłane w POST mają być zamieniane na json.
public function request(string $method,string $endpoint, array $params = [], $contentType='application/json', $jsonRequest = true): arrayi teraz w nagłówku
$headers = [ ... 'Content-Type: ' . $contentType, ... ]i obsłudze POST / default
if ($jsonRequest) {
curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($params));
} else {
curl_setopt($curl, CURLOPT_POSTFIELDS, $params);
}Teraz możemy przygotować dane w formacie jsonl i wysłać je do serwera. Ważne, by w purpose podać batch (inaczej nie przejdzie).
Przygotujmy plik i zapiszmy go na dysku.
$data = [
[
'custom_id' => 'dowcip_1',
'method' => 'POST',
'url' => '/v1/responses',
'body' => [
'model' => 'gpt-4o',
'input' => 'Napisz żart o PHP.'
]
],
[
'custom_id' => 'dowcip_2',
'method' => 'POST',
'url' => '/v1/responses',
'body' => [
'model' => 'gpt-4o',
'input' => 'Napisz żart o PHP.'
]
],
];
file_put_contents(
'batch.jsonl',
implode("\n", array_map('json_encode', $data))
);A teraz prześlijmy do API i usuńmy plik
try {
$response = $client->request( 'POST', 'files', [
'purpose' => 'batch',
'file' => new \CURLFile('batch.jsonl')
], 'multipart/form-data', false);
print_r($response);
} catch (\Exception $e) {
echo "Błąd: " . $e->getMessage();
}
unlink('batch.jsonl');W odpowiedzi otrzymaliśmy identyfikator pliku, który użyjemy dalej.
[object] => file
[id] => file-S7HwKu4LUnchCzxw1jevSj
[purpose] => batch
[filename] => batch.jsonl
[bytes] => 249
[created_at] => 1749374001
[expires_at] =>
[status] => processed
[status_details] =>Teraz możemy przekazać plik do przetworzenia przez batch. Cały proces przetwarzania trwa do 24 godzin.
$response = $client->request( 'POST', 'batches', [
'completion_window' => '24h',
'endpoint' => '/v1/responses',
'input_file_id' => 'file-S7HwKu4LUnchCzxw1jevSj'
]);
print_r($response);W odpowiedzi otrzymamy id, którego użyjemy, aby uzyskać dane.
[id] => batch_6845585085d881908231ec2fef8d441e
[object] => batch
[endpoint] => /v1/responses
[errors] =>
[input_file_id] => file-S7HwKu4LUnchCzxw1jevSj
[completion_window] => 24h
[status] => validating
[output_file_id] =>
[error_file_id] =>
[created_at] => 1749375056
[in_progress_at] =>
[expires_at] => 1749461456
[finalizing_at] =>
[completed_at] =>
[failed_at] =>
[expired_at] =>
[cancelling_at] =>
[cancelled_at] =>
[request_counts] => Array
(
[total] => 0
[completed] => 0
[failed] => 0
)
[metadata] =>Status można sprawdzić wykorzystując GET i identyfikator.
$response = $client->request( 'GET', 'batches/batch_6845585085d881908231ec2fef8d441e');
print_r($response);W odpowiedzi otrzymamy czy taski zostały wykonane i id pliku z wynikami
[id] => batch_6845585085d881908231ec2fef8d441e
[object] => batch
[endpoint] => /v1/responses
[errors] =>
[input_file_id] => file-S7HwKu4LUnchCzxw1jevSj
[completion_window] => 24h
[status] => completed
[output_file_id] => file-7TvgbcM53oBhqGVo2PWSpb
[error_file_id] =>
[created_at] => 1749375056
[in_progress_at] => 1749375057
[expires_at] => 1749461456
[finalizing_at] => 1749375060
[completed_at] => 1749375061
[failed_at] =>
[expired_at] =>
[cancelling_at] =>
[cancelled_at] =>
[request_counts] => Array
(
[total] => 2
[completed] => 2
[failed] => 0
)
[metadata] =>Wystarczy teraz pobrać plik
$response = $client->request( 'GET', 'files/file-7TvgbcM53oBhqGVo2PWSpb/content')w odpowiedzi otrzymamy format jsonl
{"id": "batch_req_6845585504e0819093f881ccc3a041c8", "custom_id": "dowcip_1", "response": {"status_code": 200, "request_id": "45a06235724bed4fe6249f4c73e4a1d3", "body": {"id": "resp_68455852fda481978d1b2ac797a1fe45014eb1f25fb163a0", "object": "response", "created_at": 1749375059, "status": "completed", "background": false, "error": null, "incomplete_details": null, "instructions": null, "max_output_tokens": null, "model": "gpt-4o-2024-08-06", "output": [{"id": "msg_6845585396a881979a41229e2de4094b014eb1f25fb163a0", "type": "message", "status": "completed", "content": [{"type": "output_text", "annotations": [], "text": "Czemu programista PHP nie wraca z imprezy?\n\nBo nie mo\u017ce znale\u017a\u0107 funkcji `exit()`."}], "role": "assistant"}], "parallel_tool_calls": true, "previous_response_id": null, "reasoning": {"effort": null, "summary": null}, "service_tier": "default", "store": true, "temperature": 1.0, "text": {"format": {"type": "text"}}, "tool_choice": "auto", "tools": [], "top_p": 1.0, "truncation": "disabled", "usage": {"input_tokens": 15, "input_tokens_details": {"cached_tokens": 0}, "output_tokens": 24, "output_tokens_details": {"reasoning_tokens": 0}, "total_tokens": 39}, "user": null, "metadata": {}}}, "error": null}
{"id": "batch_req_6845585511508190b0440a7aa97ee3db", "custom_id": "dowcip_2", "response": {"status_code": 200, "request_id": "84de3eb5956185f0ec7544449177d145", "body": {"id": "resp_6845585374ec81958d526d3727e2341b0e22f2070521003c", "object": "response", "created_at": 1749375059, "status": "completed", "background": false, "error": null, "incomplete_details": null, "instructions": null, "max_output_tokens": null, "model": "gpt-4o-2024-08-06", "output": [{"id": "msg_68455853a37c81959674e940262f79090e22f2070521003c", "type": "message", "status": "completed", "content": [{"type": "output_text", "annotations": [], "text": "Dlaczego programi\u015bci PHP nigdy nie zagin\u0105 w lesie?\n\nBo zawsze znajd\u0105 \u201eecho\u201d drogi!"}], "role": "assistant"}], "parallel_tool_calls": true, "previous_response_id": null, "reasoning": {"effort": null, "summary": null}, "service_tier": "default", "store": true, "temperature": 1.0, "text": {"format": {"type": "text"}}, "tool_choice": "auto", "tools": [], "top_p": 1.0, "truncation": "disabled", "usage": {"input_tokens": 15, "input_tokens_details": {"cached_tokens": 0}, "output_tokens": 28, "output_tokens_details": {"reasoning_tokens": 0}, "total_tokens": 43}, "user": null, "metadata": {}}}, "error": null}I tutaj mała uwaga. Nasza metoda request wyrzuci błąd, bo zawsze próbuje dekodować jsona, a nie jest to typowy json, więc operacja się nie uda. Należy zmodyfikować request, albo jeszcze lepiej przenieść cały batch do osobnej metody / metod.
Aby przekonwertować jsonl można zrobić na przykład tak:
$response = "..."; // surowy tekst JSONL
$lines = explode("\n", $response);
$array = array_map('json_decode', array_filter($lines));Na koniec usuwamy pliki. Aby pobrać listę wszystkich wywołujemy
$response = $client->request( 'GET', 'files');W odpowiedzi otrzymamy:
[object] => list
[data] => Array
(
[0] => Array
(
[object] => file
[id] => file-7TvgbcM53oBhqGVo2PWSpb
[purpose] => batch_output
[filename] => batch_6845585085d881908231ec2fef8d441e_output.jsonl
[bytes] => 2513
[created_at] => 1749375061
[expires_at] => 1751967061
[status] => processed
[status_details] =>
)
[1] => Array
(
[object] => file
[id] => file-S7HwKu4LUnchCzxw1jevSj
[purpose] => batch
[filename] => batch.jsonl
[bytes] => 249
[created_at] => 1749374001
[expires_at] => 1751966001
[status] => processed
[status_details] =>
)
)
[has_more] =>
[first_id] => file-7TvgbcM53oBhqGVo2PWSpb
[last_id] => file-S7HwKu4LUnchCzxw1jevSjWystarczy teraz wywołać delete
$client->request( 'DELETE', 'files/file-7TvgbcM53oBhqGVo2PWSpb');
$client->request( 'DELETE', 'files/file-S7HwKu4LUnchCzxw1jevSj');

