API ответов web_search
Используйте размещенный веб-поиск с PHP cURL и безопасно декодируйте ответы JSON или SSE.
API ответов web_search
Используйте размещенный web_search инструмент, когда ответ зависит от текущей общедоступной информации: последних выпусков, обновлений документации, цен, расписаний, правил, изменений продукта или других фактов, которые могли измениться после периода обучения модели.
На этой странице описывается web_search на POST /v1/responses. Он отличается от автономного `POST/v1/веб-поиск` конечная точка.
Рекомендуемый запрос
Отправьте канонический входной массив API ответов. Инструмент необходим, когда ответ должен быть основан на реальных источниках.
{
"model": "gpt-5.5",
"input": [
{
"role": "user",
"content": [
{
"type": "input_text",
"text": "Find the current stable PHP version. Use web search and cite the official sources."
}
]
}
],
"tools": [
{
"type": "web_search"
}
],
"tool_choice": "required",
"include": [
"web_search_call.action.sources"
],
"stream": false
}
Почему эти поля важны
toolsделает размещенный поиск доступным для модели.tool_choice: requiredпредотвращает ответ только в памяти, когда обязательна живая проверка.include: ["web_search_call.action.sources"]запрашивает API включить URL-адреса, рассматриваемые поисковым вызовом.stream: falseзапрашивает непотоковый ответ, но совместимые шлюзы все равно могут возвращать поток событий «Ответы». Ваш клиент должен поддерживать оба формата.
Не добавляйте параметры генерации, специфичные для поставщика, если они не поддерживаются выбранными документами модели. Маршрутизируемые провайдеры могут отклонять такие параметры, как temperature, backgroundили элементы управления выходными токенами.
Полный пример PHP cURL
В следующем примере запрос отправляется, сохраняется тело ошибки восходящего потока и декодируется либо обычный ответ JSON, либо ответ о событиях, отправленных сервером.
<?php
declare(strict_types=1);
$baseUrl = 'https://api.model-gate.com';
$apiKey = getenv('MODEL_GATE_API_KEY') ?: '';
if ($apiKey === '') {
throw new RuntimeException('MODEL_GATE_API_KEY is not configured.');
}
$url = rtrim($baseUrl, '/') . '/v1/responses';
$request = [
'model' => 'gpt-5.5',
'input' => [[
'role' => 'user',
'content' => [[
'type' => 'input_text',
'text' => 'Find the current stable PHP version. '
. 'Use web search and cite official sources.',
]],
]],
'tools' => [[
'type' => 'web_search',
]],
'tool_choice' => 'required',
'include' => [
'web_search_call.action.sources',
],
'stream' => false,
];
$json = json_encode(
$request,
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR
);
$responseHeaders = [];
$ch = curl_init($url);
if ($ch === false) {
throw new RuntimeException('Unable to initialize cURL.');
}
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 15,
CURLOPT_TIMEOUT => 900,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
'Accept: application/json, text/event-stream',
],
CURLOPT_POSTFIELDS => $json,
CURLOPT_HEADERFUNCTION => static function ($ch, string $line) use (&$responseHeaders): int {
$length = strlen($line);
$line = trim($line);
if ($line === '' || !str_contains($line, ':')) {
return $length;
}
[$name, $value] = array_map('trim', explode(':', $line, 2));
$responseHeaders[strtolower($name)] = $value;
return $length;
},
]);
$responseBody = curl_exec($ch);
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlErrorNumber = curl_errno($ch);
$curlError = curl_error($ch);
curl_close($ch);
if ($responseBody === false) {
throw new RuntimeException(
sprintf('cURL error %d: %s', $curlErrorNumber, $curlError)
);
}
if ($httpCode < 200 || $httpCode >= 300) {
// Keep the exact upstream body. It usually contains the parameter or model error.
throw new RuntimeException(
"API returned HTTP {$httpCode}: {$responseBody}"
);
}
$response = decodeResponsesBody(
$responseBody,
$responseHeaders['content-type'] ?? ''
);
$text = '';
$sources = [];
$searchCalls = 0;
foreach (($response['output'] ?? []) as $item) {
if (($item['type'] ?? null) === 'web_search_call') {
$searchCalls++;
foreach (($item['action']['sources'] ?? []) as $source) {
$url = trim((string) ($source['url'] ?? ''));
if ($url !== '') {
$sources[$url] = $url;
}
}
}
if (($item['type'] ?? null) !== 'message') {
continue;
}
foreach (($item['content'] ?? []) as $part) {
if (($part['type'] ?? null) !== 'output_text') {
continue;
}
$text .= (string) ($part['text'] ?? '');
foreach (($part['annotations'] ?? []) as $annotation) {
if (($annotation['type'] ?? null) !== 'url_citation') {
continue;
}
$url = trim((string) ($annotation['url'] ?? ''));
if ($url !== '') {
$sources[$url] = $url;
}
}
}
}
if ($searchCalls === 0) {
throw new RuntimeException('The response did not contain web_search_call.');
}
if ($sources === []) {
throw new RuntimeException('The response did not contain auditable source URLs.');
}
echo $text . PHP_EOL;
echo "Sources:" . PHP_EOL;
foreach ($sources as $sourceUrl) {
echo '- ' . $sourceUrl . PHP_EOL;
}
function decodeResponsesBody(string $body, string $contentType): array
{
$trimmed = ltrim($body);
$looksLikeSse = str_contains(strtolower($contentType), 'text/event-stream')
|| str_starts_with($trimmed, 'event:')
|| str_starts_with($trimmed, 'data:');
if (!$looksLikeSse) {
$decoded = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
if (!is_array($decoded)) {
throw new RuntimeException('Responses API returned a non-object JSON value.');
}
return $decoded;
}
return decodeResponsesSse($body);
}
function decodeResponsesSse(string $body): array
{
$output = [];
$completedResponse = [];
$eventName = '';
$dataLines = [];
$flush = static function () use (&$eventName, &$dataLines, &$output, &$completedResponse): void {
if ($dataLines === []) {
$eventName = '';
return;
}
$rawData = implode("\n", $dataLines);
$dataLines = [];
if ($rawData === '[DONE]') {
$eventName = '';
return;
}
$event = json_decode($rawData, true, 512, JSON_THROW_ON_ERROR);
$type = (string) ($event['type'] ?? $eventName);
if ($type === 'response.output_item.done' && is_array($event['item'] ?? null)) {
$output[] = $event['item'];
}
if ($type === 'response.completed' && is_array($event['response'] ?? null)) {
$completedResponse = $event['response'];
}
$eventName = '';
};
foreach (preg_split('/\r\n|\r|\n/', $body) ?: [] as $line) {
if ($line === '') {
$flush();
continue;
}
if (str_starts_with($line, 'event:')) {
$eventName = trim(substr($line, 6));
continue;
}
if (str_starts_with($line, 'data:')) {
$dataLines[] = ltrim(substr($line, 5));
}
}
$flush();
if ($completedResponse === [] && $output === []) {
throw new RuntimeException('No completed Responses API event was found.');
}
// Some compatible gateways send an empty output array in response.completed.
// Preserve the completed metadata but reconstruct output from output_item.done events.
$completedResponse['output'] = $output !== []
? $output
: ($completedResponse['output'] ?? []);
return $completedResponse;
}
Важный: stream: false может еще вернуть SSE
stream: false выражает запрошенный режим ответа. Совместимый прокси-сервер или маршрутизируемый восходящий поток все равно могут сериализовать завершенный жизненный цикл ответов как text/event-stream с такими событиями, как:
event: response.output_item.done
data: {"type":"response.output_item.done","item":{"type":"web_search_call",...}}
event: response.completed
data: {"type":"response.completed","response":{"status":"completed",...}}
Не беги json_decode() непосредственно на всё тело, пока не определите, JSON это или SSE. Для SSE соберите выполненные response.output_item.done предметы и финал response.completed метаданные.
Проверка источника
Для рабочих процессов с текущей информацией проверьте все следующее, прежде чем принять ответ:
- Хоть один завершен
web_search_callсуществует. - По крайней мере один URL-адрес существует в
web_search_call.action.sourcesилиoutput_textURL-цитирование. - Окончательный статус ответа:
completed. - Источники подтверждают утверждения, которые вы собираетесь использовать.
- Важные факты желательно подтверждены официальным или первоисточником.
Не рассматривайте беглый ответ как доказательство того, что поиск выполнен. Проверьте выходные данные инструмента программно.
Обработка ошибок и повторные попытки
- Сохранять тела ответов, отличные от 2xx. Они содержат полезные сообщения проверки поставщика.
- Не повторять HTTP
400без изменения отклоненного запроса. - Повторить переходный процесс
429,502,503,504и отказы транспорта с экспоненциальной задержкой и джиттером. - Используйте ключ идемпотентности в своем приложении, если повторная попытка может вызвать бизнес-действие.
- Сохраняйте тайм-ауты приложения, PHP, обратного прокси-сервера и шлюза согласованными для длинных поисковых запросов.
- Никогда не предоставляйте конечным пользователям ключи API или захваченные необработанные полезные данные.