Responses API web_search
Использование встроенного web search через PHP cURL и безопасный разбор ответов JSON или SSE.
Используйте встроенный инструмент web_search, когда ответ зависит от актуальных публичных сведений: последних выпусков, обновлений документации, цен, расписаний, правил, изменений продуктов и других фактов, которые могли измениться после периода обучения модели.
Эта страница описывает поддерживаемый контракт встроенного поиска на POST /v1/responses. Model Gate не публикует отдельный эндпоинт /v1/web-search.
Рекомендуемый запрос
Отправляйте канонический массив входных данных Responses 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"]запрашивает URL, рассмотренные при поиске.stream: falseзапрашивает непотоковый ответ, но совместимые шлюзы всё равно могут вернуть поток событий Responses. Клиент должен поддерживать оба формата.
Не добавляйте специфичные для поставщика параметры генерации, если выбранная модель не заявляет их поддержку. Поставщики на маршруте могут отклонять такие параметры, как temperature, background или ограничения числа выходных токенов.
Полный пример PHP cURL
Следующий пример отправляет запрос, сохраняет тело ошибки поставщика и разбирает обычный JSON-ответ либо ответ Server-Sent Events.
<?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 задаёт запрошенный режим ответа. Совместимый прокси или поставщик на маршруте всё равно может сериализовать завершённый жизненный цикл Responses как 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или URL-цитата вoutput_text. - Конечный статус ответа —
completed. - Источники подтверждают утверждения, которые вы собираетесь использовать.
- Важные факты по возможности подтверждены официальным или первичным источником.
Связный ответ не доказывает, что поиск выполнялся. Проверяйте результат инструмента программно.
Обработка ошибок и повторные попытки
- Сохраняйте тела ответов с кодами вне 2xx: в них содержатся полезные сообщения поставщика об ошибках проверки.
- Не повторяйте HTTP
400, не изменив отклонённый запрос. - Повторяйте временные ошибки
429,502,503,504и транспортные сбои с экспоненциальной задержкой и случайным разбросом. - Используйте ключ идемпотентности в своём приложении, если повтор может запустить бизнес-действие.
- Согласуйте тайм-ауты приложения, PHP, обратного прокси и шлюза для длительных поисковых запросов.
- Никогда не раскрывайте конечным пользователям API-ключи или захваченные необработанные тела запросов и ответов.