← 返回聊天
新建
删除
Models
gpt5.php
gpt5_file.php
gpt5_mini_file.php
openai_chat.php
Tools
get_time.php
get_weather.php
global_search_messages.php
math.php
memo.php
news.php
search_arxiv.php
search_crossref.php
search_github_code.php
search_pubmed.php
search_semantic_scholar.php
stock_market.php
url.php
<?php declare(strict_types=1); $BASE_URL = 'https://api.openai.com/v1'; $API_KEY = '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'; $MODEL_NAME = 'gpt-5-mini-2025-08-07'; $DEFAULTS = [ 'max_tokens' => 2048, ]; return [ 'id' => 'gpt5_mini_file', 'label' => 'gpt5_mini_file', 'supports_tools' => true, 'call' => function(array $messages, array $tools = [], array $options = []) use ($BASE_URL, $API_KEY, $MODEL_NAME, $DEFAULTS): array { $endpoint = rtrim($BASE_URL, '/\\') . '/responses'; $extraSystem = [ 'role' => 'system', 'content' => '' ]; $outMessages = $messages; array_splice($outMessages, 1, 0, [$extraSystem]); // ========== 文件注入:从 options 读取 ========== $hasFiles = !empty($options['has_files']); $contextFiles = []; if ($hasFiles && isset($options['context_files']) && is_array($options['context_files'])) { $contextFiles = $options['context_files']; } // 可选:调试/兜底拉取(如果 has_files=true 但没拿到列表) if ($hasFiles && empty($contextFiles) && !empty($options['context_files_endpoint'])) { $url = (string)$options['context_files_endpoint']; $ch2 = curl_init($url); if ($ch2 !== false) { curl_setopt_array($ch2, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 15, ]); $resp2 = curl_exec($ch2); $status2 = (int)curl_getinfo($ch2, CURLINFO_HTTP_CODE); curl_close($ch2); if ($status2 >= 200 && $status2 < 300) { $data2 = json_decode((string)$resp2, true); if (is_array($data2) && isset($data2['context_files']) && is_array($data2['context_files'])) { $contextFiles = $data2['context_files']; } elseif (is_array($data2) && isset($data2[0]) && is_array($data2[0])) { // 有的实现可能直接返回数组 $contextFiles = $data2; } } } } // 1) system 合并进 instructions(更稳) $instructionsParts = []; $chatLike = []; foreach ($outMessages as $m) { if (!is_array($m)) continue; $role = (string)($m['role'] ?? 'user'); if ($role === 'system') { $instructionsParts[] = (string)($m['content'] ?? ''); } else { $chatLike[] = $m; } } $instructions = trim(implode("\n\n", array_filter($instructionsParts))); // 找到最后一条 user 消息索引(只给这一条追加 files) $lastUserIdx = -1; foreach ($chatLike as $i => $m) { if (is_array($m) && (($m['role'] ?? '') === 'user')) $lastUserIdx = $i; } // 2) messages -> responses input items $input = []; foreach ($chatLike as $i => $m) { $role = (string)($m['role'] ?? 'user'); // tool 输出:role=tool -> function_call_output if ($role === 'tool') { $callId = (string)($m['tool_call_id'] ?? $m['call_id'] ?? ''); $out = $m['content'] ?? ''; $outStr = is_string($out) ? $out : json_encode($out, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); if ($callId !== '') { $input[] = [ 'type' => 'function_call_output', 'call_id' => $callId, 'output' => $outStr, ]; } else { $input[] = [ 'type' => 'message', 'role' => 'user', 'content' => [ ['type' => 'input_text', 'text' => "[tool output]\n" . $outStr], ], ]; } continue; } // assistant 带 tool_calls:转成 function_call items if ($role === 'assistant' && isset($m['tool_calls']) && is_array($m['tool_calls'])) { foreach ($m['tool_calls'] as $tc) { if (!is_array($tc)) continue; $fn = $tc['function'] ?? []; $callId = (string)($tc['id'] ?? $tc['call_id'] ?? ('call_' . bin2hex(random_bytes(6)))); $name = (string)($fn['name'] ?? ''); $args = $fn['arguments'] ?? ''; $argsStr = is_string($args) ? $args : json_encode($args, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); if ($name !== '') { $input[] = [ 'type' => 'function_call', 'call_id' => $callId, 'name' => $name, 'arguments' => $argsStr, ]; } } // assistant 的可见文本 $txt = (string)($m['content'] ?? ''); if ($txt !== '') { $input[] = [ 'type' => 'message', 'role' => 'assistant', 'content' => [ ['type' => 'output_text', 'text' => $txt], ], ]; } continue; } // 普通消息:重点处理最后一条 user(注入 files) $txt = $m['content'] ?? ''; $txtStr = is_string($txt) ? $txt : json_encode($txt, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); if ($role === 'user') { $contentParts = [ ['type' => 'input_text', 'text' => $txtStr], ]; // ✅ 只在最后一条 user 消息追加 if ($hasFiles && $i === $lastUserIdx && !empty($contextFiles)) { foreach ($contextFiles as $f) { if (!is_array($f)) continue; $ftype = (string)($f['type'] ?? ''); $url = (string)($f['url'] ?? ''); if ($url === '') continue; if ($ftype === 'image') { $contentParts[] = [ 'type' => 'input_image', 'image_url' => $url, ]; continue; } if ($ftype === 'text') { $fc = (string)($f['content'] ?? ''); if ($fc !== '') { $contentParts[] = [ 'type' => 'input_text', 'text' => "【附件文本】{$url}\n{$fc}\n(内容可能被截断)", ]; } else { // 没有 content 就当文件丢给模型 $contentParts[] = [ 'type' => 'input_file', 'file_url' => $url, ]; } continue; } // binary(如 PDF/Doc/Zip 等):优先走 input_file $contentParts[] = [ 'type' => 'input_file', 'file_url' => $url, ]; } } $input[] = [ 'type' => 'message', 'role' => 'user', 'content' => $contentParts, ]; continue; } // assistant/developer 这些历史消息 $contentType = ($role === 'assistant') ? 'output_text' : 'input_text'; $input[] = [ 'type' => 'message', 'role' => $role, 'content' => [ ['type' => $contentType, 'text' => $txtStr], ], ]; } // 3) tools:ChatCompletions 形状 -> Responses 形状(平铺) $normalizedTools = []; if (!empty($tools)) { foreach ($tools as $t) { if (!is_array($t)) continue; if (($t['type'] ?? '') !== 'function') continue; if (isset($t['function']) && is_array($t['function'])) { $fn = $t['function']; $normalizedTools[] = [ 'type' => 'function', 'name' => (string)($fn['name'] ?? ''), 'description' => (string)($fn['description'] ?? ''), 'parameters' => $fn['parameters'] ?? ['type' => 'object', 'properties' => new stdClass()], 'strict' => (bool)($fn['strict'] ?? false), ]; } else { $normalizedTools[] = $t; } } } $payload = [ 'model' => (string)($options['model'] ?? $MODEL_NAME), 'input' => $input, 'max_output_tokens' => $options['max_tokens'] ?? $DEFAULTS['max_tokens'], ]; if ($instructions !== '') $payload['instructions'] = $instructions; if (!empty($normalizedTools)) { $payload['tools'] = $normalizedTools; $payload['tool_choice'] = 'auto'; $payload['parallel_tool_calls'] = true; } $headers = ['Content-Type: application/json']; if (trim($API_KEY) !== '') $headers[] = 'Authorization: Bearer ' . $API_KEY; // ========== 发请求(封装成可重试) ========== $doRequest = function(array $payload) use ($endpoint, $headers): array { $ch = curl_init($endpoint); if ($ch === false) return ['ok' => false, 'error' => 'curl_init failed']; $json = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => $headers, CURLOPT_POSTFIELDS => $json, CURLOPT_CONNECTTIMEOUT => 10, CURLOPT_TIMEOUT => 120, ]); $resp = curl_exec($ch); $errno = curl_errno($ch); $err = curl_error($ch); $status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($errno !== 0) return ['ok' => false, 'error' => 'cURL error: ' . $err]; $data = json_decode((string)$resp, true); if (!is_array($data)) return ['ok' => false, 'error' => 'Invalid JSON', 'raw' => (string)$resp, 'status' => $status]; if ($status < 200 || $status >= 300) { $msg = $data['error']['message'] ?? ('HTTP ' . $status); return ['ok' => false, 'error' => (string)$msg, 'raw' => $data, 'status' => $status]; } return ['ok' => true, 'raw' => $data, 'status' => $status]; }; $r = $doRequest($payload); // ========== 兜底:如果多模态/文件不被模型支持,自动降级重试一次 ========== if (!$r['ok'] && $hasFiles) { $errMsg = (string)($r['error'] ?? ''); if (stripos($errMsg, 'input_image') !== false || stripos($errMsg, 'input_file') !== false || stripos($errMsg, 'image_url') !== false || stripos($errMsg, 'file_url') !== false) { // 降级:把文件信息拼进最后一条 user 的第一段文本里(不再传 input_image/input_file) $fileLines = []; foreach ($contextFiles as $f) { if (!is_array($f)) continue; $url = (string)($f['url'] ?? ''); if ($url === '') continue; $ftype = (string)($f['type'] ?? ''); if ($ftype === 'text' && !empty($f['content'])) { $fileLines[] = "【文本附件】{$url}\n" . (string)$f['content']; } else { $fileLines[] = "【附件链接】{$url}"; } } $append = "\n\n---\n我附加了这些文件/图片(模型可能不支持结构化多模态输入,请从链接/文本中分析):\n" . implode("\n\n", $fileLines); // 找到 payload 里最后一条 user message,改成纯 input_text foreach ($payload['input'] as $idx => $item) { if (is_array($item) && ($item['type'] ?? '') === 'message' && ($item['role'] ?? '') === 'user') { // 记录最后一个 user $last = $idx; } } if (isset($last)) { $orig = ''; $c = $payload['input'][$last]['content'] ?? []; if (is_array($c) && isset($c[0]['text'])) $orig = (string)$c[0]['text']; $payload['input'][$last]['content'] = [ ['type' => 'input_text', 'text' => $orig . $append], ]; } $r = $doRequest($payload); } } if (!$r['ok']) { // 保证链路不断 return ['ok' => false, 'error' => (string)($r['error'] ?? 'Unknown error'), 'raw' => $r['raw'] ?? null, 'status' => $r['status'] ?? 0]; } $data = $r['raw']; $status = (int)$r['status']; // 4) Responses output -> 原项目 message 结构(content + tool_calls) $assistantText = ''; $toolCalls = []; $output = $data['output'] ?? []; if (is_array($output)) { foreach ($output as $item) { if (!is_array($item)) continue; if (($item['type'] ?? '') === 'message' && ($item['role'] ?? '') === 'assistant') { $content = $item['content'] ?? []; if (is_array($content)) { foreach ($content as $c) { if (is_array($c) && ($c['type'] ?? '') === 'output_text') { $assistantText .= (string)($c['text'] ?? ''); } if (is_array($c) && ($c['type'] ?? '') === 'refusal') { $assistantText .= (string)($c['refusal'] ?? ''); } } } } if (($item['type'] ?? '') === 'function_call') { $toolCalls[] = [ 'id' => (string)($item['call_id'] ?? ('call_' . bin2hex(random_bytes(6)))), 'type' => 'function', 'function' => [ 'name' => (string)($item['name'] ?? ''), 'arguments' => (string)($item['arguments'] ?? ''), ], ]; } } } $msg = [ 'role' => 'assistant', 'content' => $assistantText, ]; if (!empty($toolCalls)) { $msg['tool_calls'] = $toolCalls; } return ['ok' => true, 'message' => $msg, 'raw' => $data, 'status' => $status]; }, ];
保存