|
|
@@ -28,6 +28,7 @@ class DeepSeekService
|
|
|
private $headers;
|
|
|
private $valid_text_models;
|
|
|
private $gpt_text_models;
|
|
|
+ private $gemini_text_models;
|
|
|
protected $aiImageGenerationService;
|
|
|
|
|
|
public function __construct(AIImageGenerationService $aiImageGenerationService) {
|
|
|
@@ -42,6 +43,8 @@ class DeepSeekService
|
|
|
$this->valid_text_models = DB::table('mp_text_models')->where('is_enabled', 1)->pluck('model')->toArray();
|
|
|
// GPT文本模型列表
|
|
|
$this->gpt_text_models = BaseConst::GPT_TEXT_MODELS;
|
|
|
+ // Gemini文本模型列表
|
|
|
+ $this->gemini_text_models = BaseConst::GEMINI_TEXT_MODELS;
|
|
|
$this->sys_message = [
|
|
|
'role' => 'system',
|
|
|
'content' => "你是一个专业的文档分析助手及资深编剧,请根据用户提供的文档内容完成示例格式的内容输出(需通过以下几个板块进行回复: <故事梗概><剧本亮点><人物关系><核心矛盾><主体列表><美术风格><场景列表><分集剧本>,每个板块之间必须使用###分隔,#不能多也不能少;同时板块之间需满足以下要求:\n
|
|
|
@@ -254,6 +257,9 @@ class DeepSeekService
|
|
|
} else if (in_array($model, $this->gpt_text_models)) {
|
|
|
// GPT 模型使用 TokenRouter API
|
|
|
return $this->gpt54StreamResponse($post_data);
|
|
|
+ } else if (in_array($model, $this->gemini_text_models)) {
|
|
|
+ // Gemini 模型使用 Gemini API
|
|
|
+ return $this->geminiStreamResponse($post_data, $model);
|
|
|
} else if (in_array($model, $this->valid_text_models)) {
|
|
|
// 火山引擎支持的模型使用火山引擎 API
|
|
|
return $this->volcEngineChatCompletion($post_data);
|
|
|
@@ -606,6 +612,19 @@ class DeepSeekService
|
|
|
'model' => $model,
|
|
|
'finish_reason' => 'stop'
|
|
|
];
|
|
|
+ } else if (in_array($model, $this->gemini_text_models)) {
|
|
|
+ // Gemini 模型使用 Gemini API(非流式)
|
|
|
+ $result = $this->geminiChatOnly($post_data, $model);
|
|
|
+
|
|
|
+ dLog('deepseek')->info('newGenerateText使用Gemini模型', ['requested_model' => $model]);
|
|
|
+
|
|
|
+ $aiResult = [
|
|
|
+ 'content' => $result['fullContent'],
|
|
|
+ 'reasoning_content' => $result['fullReasoningContent'] ?? '',
|
|
|
+ 'usage' => $result['usage'],
|
|
|
+ 'model' => $model,
|
|
|
+ 'finish_reason' => $result['finish_reason'] ?? 'stop'
|
|
|
+ ];
|
|
|
} else if (in_array($model, $this->valid_text_models)) {
|
|
|
// 火山引擎支持的模型使用火山引擎 API(非流式)
|
|
|
$post_data['stream'] = false;
|
|
|
@@ -721,6 +740,818 @@ class DeepSeekService
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
+ * 通用 Gemini 文生文方法(支持流式和非流式)
|
|
|
+ *
|
|
|
+ * 与 chatWithFileStream / newGenerateText 相同的入参格式:
|
|
|
+ * model 模型ID(gemini-3-flash-preview 等,默认 gemini-3-flash-preview)
|
|
|
+ * messages OpenAI 风格消息数组(可选,与 system_prompt/prompt 二选一)
|
|
|
+ * system_prompt 系统提示词(可选)
|
|
|
+ * prompt 用户提示词(可选)
|
|
|
+ * images 上传的图片/视频文件列表(可选,自动转 inlineData,超限自动用 ffmpeg 压缩)
|
|
|
+ * image_urls 图片/视频URL列表(可选,同上)
|
|
|
+ * temperature 温度(可选,默认 1)
|
|
|
+ * top_p 核采样(可选)
|
|
|
+ * max_tokens 最大输出 token(可选,默认 8192)
|
|
|
+ * response_format text/json(可选)
|
|
|
+ * thinking enabled/disabled(可选,pro 系列默认 enabled)
|
|
|
+ * thinking_budget 思考预算(可选,默认 20)
|
|
|
+ * stream 是否流式(可选,默认 false)
|
|
|
+ *
|
|
|
+ * @param array $data 请求参数
|
|
|
+ * @return array|\Generator 非流式返回数组;流式返回生成器(yield type=content/reasoning/done)
|
|
|
+ */
|
|
|
+ public function geminiGenerateText($data) {
|
|
|
+ $model = getProp($data, 'model', 'gemini-3-flash-preview');
|
|
|
+ $isStream = (bool)getProp($data, 'stream', false);
|
|
|
+
|
|
|
+ if (!in_array($model, $this->gemini_text_models)) {
|
|
|
+ Utils::throwError('20003:不支持的Gemini模型: ' . $model);
|
|
|
+ }
|
|
|
+
|
|
|
+ $requestData = $this->buildGeminiRequestBody($data, $model);
|
|
|
+
|
|
|
+ dLog('deepseek')->info('Gemini请求参数', ['model' => $model, 'stream' => $isStream]);
|
|
|
+
|
|
|
+ if ($isStream) {
|
|
|
+ // 流式返回生成器
|
|
|
+ return $this->geminiStreamResponse($requestData, $model);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 非流式
|
|
|
+ return $this->geminiChatOnly($requestData, $model);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Gemini 多媒体文件大小上限(原始字节,base64 后约 19MB,留出 20MB 请求体余量)
|
|
|
+ */
|
|
|
+ private function geminiMediaMaxBytes() {
|
|
|
+ return 14 * 1024 * 1024;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 构建 Gemini 请求体(将 OpenAI 风格消息转换为 Gemini contents 格式)
|
|
|
+ */
|
|
|
+ private function buildGeminiRequestBody($data, $model) {
|
|
|
+ $messages = getProp($data, 'messages', []);
|
|
|
+
|
|
|
+ // 如果没有提供 messages,则根据 system_prompt、prompt 和 images 构建
|
|
|
+ if (empty($messages)) {
|
|
|
+ $systemPrompt = getProp($data, 'system_prompt', '');
|
|
|
+ $prompt = getProp($data, 'prompt', '');
|
|
|
+ $images = getProp($data, 'images', []);
|
|
|
+ $imageUrls = getProp($data, 'image_urls', []);
|
|
|
+
|
|
|
+ $messages = [];
|
|
|
+ if (!empty($systemPrompt)) {
|
|
|
+ $messages[] = [
|
|
|
+ 'role' => 'system',
|
|
|
+ 'content' => $systemPrompt
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ if (!empty($prompt) || !empty($images) || !empty($imageUrls)) {
|
|
|
+ $userMessage = ['role' => 'user', 'content' => []];
|
|
|
+ if (!empty($prompt)) {
|
|
|
+ $userMessage['content'][] = ['type' => 'text', 'text' => $prompt];
|
|
|
+ }
|
|
|
+ if (!empty($images)) {
|
|
|
+ if (!is_array($images)) {
|
|
|
+ $images = [$images];
|
|
|
+ }
|
|
|
+ foreach ($images as $image) {
|
|
|
+ $inlineData = $this->processMediaToInlineData($image);
|
|
|
+ if ($inlineData) {
|
|
|
+ $userMessage['content'][] = [
|
|
|
+ 'type' => 'image',
|
|
|
+ 'inline_data' => $inlineData
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (!empty($imageUrls)) {
|
|
|
+ if (!is_array($imageUrls)) {
|
|
|
+ $imageUrls = [$imageUrls];
|
|
|
+ }
|
|
|
+ foreach ($imageUrls as $url) {
|
|
|
+ if (!empty($url)) {
|
|
|
+ $inlineData = $this->processMediaToInlineData($url);
|
|
|
+ if ($inlineData) {
|
|
|
+ $userMessage['content'][] = [
|
|
|
+ 'type' => 'image',
|
|
|
+ 'inline_data' => $inlineData
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (count($userMessage['content']) === 1 && isset($userMessage['content'][0]['type']) && $userMessage['content'][0]['type'] === 'text') {
|
|
|
+ $userMessage['content'] = $userMessage['content'][0]['text'];
|
|
|
+ }
|
|
|
+ $messages[] = $userMessage;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (empty($messages)) {
|
|
|
+ Utils::throwError('20003:请提供有效的对话内容');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 转换为 Gemini contents 格式
|
|
|
+ $systemInstruction = '';
|
|
|
+ $contents = [];
|
|
|
+ foreach ($messages as $message) {
|
|
|
+ $role = getProp($message, 'role', 'user');
|
|
|
+ $content = getProp($message, 'content', '');
|
|
|
+
|
|
|
+ if ($role === 'system') {
|
|
|
+ if (is_array($content)) {
|
|
|
+ $systemInstruction = $this->extractGeminiTextFromParts($content);
|
|
|
+ } else {
|
|
|
+ $systemInstruction = (string)$content;
|
|
|
+ }
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ $geminiRole = $role === 'assistant' ? 'model' : 'user';
|
|
|
+ $parts = [];
|
|
|
+ if (is_array($content)) {
|
|
|
+ foreach ($content as $part) {
|
|
|
+ $partType = getProp($part, 'type', 'text');
|
|
|
+ if ($partType === 'text') {
|
|
|
+ $text = (string)getProp($part, 'text', '');
|
|
|
+ if ($text !== '') {
|
|
|
+ $parts[] = ['text' => $text];
|
|
|
+ }
|
|
|
+ } elseif (in_array($partType, ['image_url', 'image', 'video_url', 'video'])) {
|
|
|
+ if (in_array($partType, ['image_url', 'video_url'])) {
|
|
|
+ $mediaPart = getProp($part, $partType === 'image_url' ? 'image_url' : 'video_url', []);
|
|
|
+ $url = is_array($mediaPart) ? (string)getProp($mediaPart, 'url', '') : (string)$mediaPart;
|
|
|
+ } elseif (isset($part['inline_data'])) {
|
|
|
+ // 已处理过的 inline_data 直接使用
|
|
|
+ $parts[] = ['inline_data' => $part['inline_data']];
|
|
|
+ continue;
|
|
|
+ } else {
|
|
|
+ $url = '';
|
|
|
+ }
|
|
|
+ $inlineData = $this->processMediaToInlineData($url);
|
|
|
+ if ($inlineData) {
|
|
|
+ $parts[] = ['inline_data' => $inlineData];
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ $text = (string)$content;
|
|
|
+ if ($text !== '') {
|
|
|
+ $parts[] = ['text' => $text];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!empty($parts)) {
|
|
|
+ $contents[] = ['role' => $geminiRole, 'parts' => $parts];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (empty($contents)) {
|
|
|
+ Utils::throwError('20003:请提供有效的对话内容');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 构建生成配置
|
|
|
+ $generationConfig = [];
|
|
|
+ if (getProp($data, 'temperature', null) !== null) {
|
|
|
+ $generationConfig['temperature'] = (float)getProp($data, 'temperature');
|
|
|
+ }
|
|
|
+ if (getProp($data, 'top_p', null) !== null) {
|
|
|
+ $generationConfig['topP'] = (float)getProp($data, 'top_p');
|
|
|
+ }
|
|
|
+ if (getProp($data, 'frequency_penalty', null) !== null) {
|
|
|
+ $generationConfig['frequencyPenalty'] = (float)getProp($data, 'frequency_penalty');
|
|
|
+ }
|
|
|
+ if (getProp($data, 'presence_penalty', null) !== null) {
|
|
|
+ $generationConfig['presencePenalty'] = (float)getProp($data, 'presence_penalty');
|
|
|
+ }
|
|
|
+ $generationConfig['maxOutputTokens'] = (int)getProp($data, 'max_tokens', 8192);
|
|
|
+
|
|
|
+ // response_format 映射:text -> text/plain,json -> application/json
|
|
|
+ $responseFormat = getProp($data, 'response_format', 'text');
|
|
|
+ if (is_array($responseFormat)) {
|
|
|
+ $responseFormat = getProp($responseFormat, 'type', 'text');
|
|
|
+ }
|
|
|
+ if (in_array($responseFormat, ['json', 'json_object'])) {
|
|
|
+ $generationConfig['responseMimeType'] = 'application/json';
|
|
|
+ } else {
|
|
|
+ $generationConfig['responseMimeType'] = 'text/plain';
|
|
|
+ }
|
|
|
+
|
|
|
+ $requestData = [
|
|
|
+ 'contents' => $contents,
|
|
|
+ 'generationConfig' => $generationConfig,
|
|
|
+ ];
|
|
|
+
|
|
|
+ if ($systemInstruction !== '') {
|
|
|
+ $requestData['systemInstruction'] = ['parts' => [['text' => $systemInstruction]]];
|
|
|
+ }
|
|
|
+
|
|
|
+ // thinking 配置(Gemini 通过 thinkingConfig.thinkingBudget 控制,0 表示关闭)
|
|
|
+ $thinkingMode = getProp($data, 'thinking', null);
|
|
|
+ if (is_array($thinkingMode)) {
|
|
|
+ $thinkingMode = getProp($thinkingMode, 'type', 'disabled');
|
|
|
+ }
|
|
|
+ $defaultThinking = in_array($model, ['gemini-3-pro-preview', 'gemini-3.1-pro-preview']);
|
|
|
+ if ($thinkingMode === null) {
|
|
|
+ $thinkingMode = $defaultThinking ? 'enabled' : 'disabled';
|
|
|
+ }
|
|
|
+ if ($thinkingMode === 'enabled') {
|
|
|
+ $requestData['generationConfig']['thinkingConfig'] = [
|
|
|
+ 'thinkingBudget' => (int)getProp($data, 'thinking_budget', 20)
|
|
|
+ ];
|
|
|
+ } else {
|
|
|
+ $requestData['generationConfig']['thinkingConfig'] = ['thinkingBudget' => 0];
|
|
|
+ }
|
|
|
+
|
|
|
+ return $requestData;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取指定 Gemini 模型对应的 API Key(env 配置,优先取模型专属 Key,缺失时回退 GEMINI_API_KEY)
|
|
|
+ *
|
|
|
+ * 模型名 -> env 变量名映射:
|
|
|
+ * gemini-3-pro-preview => GEMINI_API_KEY_GEMINI_3_PRO_PREVIEW
|
|
|
+ * gemini-3-flash-preview => GEMINI_API_KEY_GEMINI_3_FLASH_PREVIEW
|
|
|
+ * gemini-3.1-pro-preview => GEMINI_API_KEY_GEMINI_3_1_PRO_PREVIEW
|
|
|
+ * gemini-3.1-flash-image-preview-t => GEMINI_API_KEY_GEMINI_3_1_FLASH_IMAGE_PREVIEW_T
|
|
|
+ * gemini-3.6-flash => GEMINI_API_KEY_GEMINI_3_6_FLASH
|
|
|
+ */
|
|
|
+ private function getGeminiApiKey($model) {
|
|
|
+ $map = [
|
|
|
+ 'gemini-3-pro-preview' => 'GEMINI_API_KEY_GEMINI_3_PRO_PREVIEW',
|
|
|
+ 'gemini-3-flash-preview' => 'GEMINI_API_KEY_GEMINI_3_FLASH_PREVIEW',
|
|
|
+ 'gemini-3.1-pro-preview' => 'GEMINI_API_KEY_GEMINI_3_1_PRO_PREVIEW',
|
|
|
+ 'gemini-3.1-flash-image-preview-t' => 'GEMINI_API_KEY_GEMINI_3_1_FLASH_IMAGE_PREVIEW_T',
|
|
|
+ 'gemini-3.6-flash' => 'GEMINI_API_KEY_GEMINI_3_6_FLASH',
|
|
|
+ ];
|
|
|
+
|
|
|
+ if (isset($map[$model])) {
|
|
|
+ $key = env($map[$model]);
|
|
|
+ if (!empty($key)) {
|
|
|
+ return $key;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return env('GEMINI_API_KEY', '');
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Gemini 非流式对话(返回与 chatOnly 一致的结构)
|
|
|
+ */
|
|
|
+ private function geminiChatOnly($requestData, $model) {
|
|
|
+ $apiKey = $this->getGeminiApiKey($model);
|
|
|
+ if (empty($apiKey)) {
|
|
|
+ Utils::throwError('20003:Gemini API Key未配置: ' . $model);
|
|
|
+ }
|
|
|
+
|
|
|
+ $url = rtrim(env('GEMINI_API_BASE_URL', 'https://ai-api.kkidc.com/v1beta'), '/')
|
|
|
+ . '/models/' . $model . ':generateContent';
|
|
|
+
|
|
|
+ $client = new Client(['timeout' => 1800, 'verify' => false]);
|
|
|
+ $headers = [
|
|
|
+ 'Authorization' => 'Bearer ' . $apiKey,
|
|
|
+ 'Content-Type' => 'application/json'
|
|
|
+ ];
|
|
|
+
|
|
|
+ $maxRetries = $this->gptRetryTimes();
|
|
|
+ $interval = $this->gptRetryInterval();
|
|
|
+ $attempt = 0;
|
|
|
+
|
|
|
+ while (true) {
|
|
|
+ try {
|
|
|
+ $response = $client->post($url, [
|
|
|
+ 'json' => $requestData,
|
|
|
+ 'headers' => $headers
|
|
|
+ ]);
|
|
|
+ $responseArr = json_decode($response->getBody()->getContents(), true);
|
|
|
+ dLog('deepseek')->info('Gemini请求完成', ['model' => $model, 'response' => $responseArr]);
|
|
|
+
|
|
|
+ $fullContent = $this->extractGeminiContent($responseArr);
|
|
|
+ $fullReasoningContent = $this->extractGeminiReasoningContent($responseArr);
|
|
|
+ $usage = isset($responseArr['usageMetadata']) ? $responseArr['usageMetadata'] : [];
|
|
|
+ $finishReason = isset($responseArr['candidates'][0]['finishReason'])
|
|
|
+ ? $responseArr['candidates'][0]['finishReason']
|
|
|
+ : '';
|
|
|
+
|
|
|
+ return [
|
|
|
+ 'fullContent' => $fullContent,
|
|
|
+ 'fullReasoningContent' => $fullReasoningContent,
|
|
|
+ 'usage' => $usage,
|
|
|
+ 'finish_reason' => $finishReason
|
|
|
+ ];
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ $retryable = true;
|
|
|
+ if ($e instanceof \GuzzleHttp\Exception\BadResponseException && $e->getResponse()) {
|
|
|
+ $status = $e->getResponse()->getStatusCode();
|
|
|
+ $retryable = $status >= 500 || $status == 429;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!$retryable || $attempt >= $maxRetries) {
|
|
|
+ dLog('deepseek')->error('Gemini请求失败: ' . $e->getMessage());
|
|
|
+ logDB('deepseek', 'error', 'Gemini请求失败', ['error' => $e->getMessage(), 'model' => $model]);
|
|
|
+ Utils::throwError('20003:Gemini请求失败: ' . $e->getMessage());
|
|
|
+ }
|
|
|
+
|
|
|
+ dLog('deepseek')->warning('Gemini请求失败,准备重试', [
|
|
|
+ 'retry' => $attempt + 1,
|
|
|
+ 'max_retries' => $maxRetries,
|
|
|
+ 'error' => $e->getMessage()
|
|
|
+ ]);
|
|
|
+ sleep($interval);
|
|
|
+ $attempt++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Gemini 流式对话(SSE 输出,格式与 deepSeekStreamResponse 一致)
|
|
|
+ */
|
|
|
+ private function geminiStreamResponse($requestData, $model) {
|
|
|
+ $apiKey = $this->getGeminiApiKey($model);
|
|
|
+ if (empty($apiKey)) {
|
|
|
+ Utils::throwError('20003:Gemini API Key未配置: ' . $model);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 兼容 OpenAI 风格 post_data(包含 messages 字段)
|
|
|
+ if (isset($requestData['messages'])) {
|
|
|
+ $requestData = $this->buildGeminiRequestBody($requestData, $model);
|
|
|
+ }
|
|
|
+
|
|
|
+ $url = rtrim(env('GEMINI_API_BASE_URL', 'https://ai-api.kkidc.com/v1beta'), '/')
|
|
|
+ . '/models/' . $model . ':generateContent?alt=sse';
|
|
|
+
|
|
|
+ $client = new Client(['timeout' => 1800, 'verify' => false]);
|
|
|
+ $headers = [
|
|
|
+ 'Authorization' => 'Bearer ' . $apiKey,
|
|
|
+ 'Content-Type' => 'application/json'
|
|
|
+ ];
|
|
|
+
|
|
|
+ try {
|
|
|
+ $response = $client->post($url, [
|
|
|
+ 'json' => $requestData,
|
|
|
+ 'headers' => $headers,
|
|
|
+ 'stream' => true
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $body = $response->getBody();
|
|
|
+ $fullContent = '';
|
|
|
+ $fullReasoningContent = '';
|
|
|
+ $usage = [];
|
|
|
+ $buffer = '';
|
|
|
+
|
|
|
+ dLog('deepseek')->info('开始读取 Gemini 流式响应');
|
|
|
+
|
|
|
+ while (!$body->eof()) {
|
|
|
+ $chunk = $body->read(1024);
|
|
|
+ $buffer .= $chunk;
|
|
|
+
|
|
|
+ $lines = explode("\n", $buffer);
|
|
|
+ $buffer = array_pop($lines);
|
|
|
+
|
|
|
+ foreach ($lines as $line) {
|
|
|
+ $line = trim($line);
|
|
|
+ if (empty($line) || strpos($line, 'data: ') !== 0) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ $data = substr($line, 6);
|
|
|
+ if ($data === '[DONE]') {
|
|
|
+ break 2;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ $json = json_decode($data, true);
|
|
|
+ if (json_last_error() !== JSON_ERROR_NONE) {
|
|
|
+ dLog('deepseek')->warning('Gemini JSON解析错误: ' . json_last_error_msg(), ['data' => $data]);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 思考内容(thoughts)
|
|
|
+ $thoughts = $this->extractGeminiThoughts($json);
|
|
|
+ if ($thoughts !== '') {
|
|
|
+ $fullReasoningContent .= $thoughts;
|
|
|
+ yield [
|
|
|
+ 'type' => 'reasoning',
|
|
|
+ 'content' => $thoughts,
|
|
|
+ 'full_reasoning' => $fullReasoningContent
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ // 回答内容
|
|
|
+ $partText = $this->extractGeminiContent($json);
|
|
|
+ if ($partText !== '') {
|
|
|
+ $fullContent .= $partText;
|
|
|
+ yield [
|
|
|
+ 'type' => 'content',
|
|
|
+ 'content' => $partText,
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ // 使用统计
|
|
|
+ if (isset($json['usageMetadata'])) {
|
|
|
+ $usage = $json['usageMetadata'];
|
|
|
+ dLog('deepseek')->info('Gemini收到使用统计', ['usage' => $usage]);
|
|
|
+ }
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ dLog('deepseek')->error('Gemini解析流式响应失败: ' . $e->getMessage(), ['line' => $line]);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ dLog('deepseek')->info('Gemini流式读取完成', [
|
|
|
+ 'content_length' => strlen($fullContent),
|
|
|
+ 'reasoning_length' => strlen($fullReasoningContent)
|
|
|
+ ]);
|
|
|
+
|
|
|
+ yield [
|
|
|
+ 'type' => 'done',
|
|
|
+ 'full_content' => $fullContent,
|
|
|
+ 'full_reasoning' => $fullReasoningContent,
|
|
|
+ 'usage' => $usage
|
|
|
+ ];
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ dLog('deepseek')->error('Gemini流式请求失败: ' . $e->getMessage());
|
|
|
+ yield [
|
|
|
+ 'type' => 'error',
|
|
|
+ 'msg' => $e->getMessage(),
|
|
|
+ 'code' => $e->getCode()
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 从 Gemini 响应中提取回答文本
|
|
|
+ */
|
|
|
+ private function extractGeminiContent($responseArr) {
|
|
|
+ $text = '';
|
|
|
+ if (isset($responseArr['candidates'][0]['content']['parts']) && is_array($responseArr['candidates'][0]['content']['parts'])) {
|
|
|
+ foreach ($responseArr['candidates'][0]['content']['parts'] as $part) {
|
|
|
+ if (isset($part['text'])) {
|
|
|
+ $text .= $part['text'];
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return $text;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 从 Gemini 响应中提取思考内容
|
|
|
+ */
|
|
|
+ private function extractGeminiReasoningContent($responseArr) {
|
|
|
+ $reasoning = '';
|
|
|
+ if (isset($responseArr['candidates'][0]['thoughts']) && is_array($responseArr['candidates'][0]['thoughts'])) {
|
|
|
+ foreach ($responseArr['candidates'][0]['thoughts'] as $thought) {
|
|
|
+ if (isset($thought['text'])) {
|
|
|
+ $reasoning .= $thought['text'];
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return $reasoning;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 从 Gemini 流式响应中提取本次增量思考内容
|
|
|
+ */
|
|
|
+ private function extractGeminiThoughts($responseArr) {
|
|
|
+ $thoughts = '';
|
|
|
+ if (isset($responseArr['candidates'][0]['thoughts']) && is_array($responseArr['candidates'][0]['thoughts'])) {
|
|
|
+ foreach ($responseArr['candidates'][0]['thoughts'] as $thought) {
|
|
|
+ if (isset($thought['text'])) {
|
|
|
+ $thoughts .= $thought['text'];
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return $thoughts;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 提取消息数组中的纯文本
|
|
|
+ */
|
|
|
+ private function extractGeminiTextFromParts($parts) {
|
|
|
+ $text = '';
|
|
|
+ if (is_array($parts)) {
|
|
|
+ foreach ($parts as $part) {
|
|
|
+ if (is_array($part) && getProp($part, 'type', 'text') === 'text') {
|
|
|
+ $text .= (string)getProp($part, 'text', '');
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return $text;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 将多媒体文件(上传文件路径或 http(s) URL)转换为 Gemini inline_data 格式
|
|
|
+ *
|
|
|
+ * 限制:中转站请求体最大 20MB,base64 体积膨胀约 1.37 倍,
|
|
|
+ * 因此压缩目标为原始文件 <= 14MB(base64 后约 19.2MB,留有余量);
|
|
|
+ * 视频时长超过 5 分钟时通过加速压缩到 5 分钟以内。
|
|
|
+ *
|
|
|
+ * @param mixed $media 文件路径 / UploadedFile / http(s) URL / data URI
|
|
|
+ * @return array|null ['mime_type' => string, 'data' => string]
|
|
|
+ */
|
|
|
+ private function processMediaToInlineData($media) {
|
|
|
+ if (empty($media)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ $localPath = null;
|
|
|
+ $isTempFile = false;
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 1. data URI:直接解析使用(无法压缩,体积本身已受限)
|
|
|
+ if (is_string($media) && strpos($media, 'data:') === 0) {
|
|
|
+ if (preg_match('/^data:([^;,]+);base64,(.+)$/s', $media, $matches)) {
|
|
|
+ return [
|
|
|
+ 'mime_type' => $matches[1],
|
|
|
+ 'data' => $matches[2]
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 获取本地文件路径
|
|
|
+ if (is_string($media) && preg_match('#^https?://#i', $media)) {
|
|
|
+ $localPath = $this->downloadMediaToTemp($media);
|
|
|
+ $isTempFile = true;
|
|
|
+ } elseif (is_string($media)) {
|
|
|
+ $localPath = $media;
|
|
|
+ } else {
|
|
|
+ // Laravel UploadedFile 对象
|
|
|
+ $localPath = $media->getRealPath();
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!$localPath || !file_exists($localPath)) {
|
|
|
+ dLog('deepseek')->warning('Gemini多媒体文件不存在', ['path' => $localPath]);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ $fileSize = filesize($localPath);
|
|
|
+ $maxBytes = $this->geminiMediaMaxBytes();
|
|
|
+
|
|
|
+ // 3. 判断是否需要压缩(超过大小上限或视频时长超过5分钟)
|
|
|
+ $mimeType = $this->detectMediaMime($localPath);
|
|
|
+ $isVideo = strpos($mimeType, 'video/') === 0;
|
|
|
+ $needCompress = $fileSize > $maxBytes;
|
|
|
+
|
|
|
+ $duration = 0;
|
|
|
+ if ($isVideo) {
|
|
|
+ $duration = $this->getMediaDuration($localPath);
|
|
|
+ if ($duration > 300) {
|
|
|
+ $needCompress = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ $finalPath = $localPath;
|
|
|
+ $compressedPath = null;
|
|
|
+ if ($needCompress && $this->ffmpegAvailable()) {
|
|
|
+ $compressed = $this->compressMediaFile($localPath, $isVideo, $duration);
|
|
|
+ if ($compressed && file_exists($compressed)) {
|
|
|
+ $compressedPath = $compressed;
|
|
|
+ if (filesize($compressed) < $fileSize || $isVideo) {
|
|
|
+ $finalPath = $compressed;
|
|
|
+ $mimeType = $isVideo ? 'video/mp4' : $this->detectMediaMime($compressed);
|
|
|
+ } else {
|
|
|
+ @unlink($compressed);
|
|
|
+ $compressedPath = null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 兜底校验:压缩后(或未压缩)仍超过大小上限,直接报错,不静默发送
|
|
|
+ $finalSize = filesize($finalPath);
|
|
|
+ if ($finalSize > $maxBytes) {
|
|
|
+ dLog('deepseek')->error('Gemini多媒体文件压缩后仍超过大小上限', [
|
|
|
+ 'path' => $localPath,
|
|
|
+ 'size_mb' => round($finalSize / 1024 / 1024, 2),
|
|
|
+ 'limit_mb' => 14
|
|
|
+ ]);
|
|
|
+ Utils::throwError('20003:多媒体文件过大(' . round($finalSize / 1024 / 1024, 2) . 'MB),压缩后仍超过14MB限制,请先自行压缩后重试');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 兜底校验:视频时长超过5分钟且未能压缩成功
|
|
|
+ if ($isVideo && $duration > 300 && $finalPath === $localPath && !$this->ffmpegAvailable()) {
|
|
|
+ Utils::throwError('20003:视频时长超过5分钟(' . round($duration, 1) . '秒),且服务器ffmpeg不可用,无法压缩,请先自行处理视频后重试');
|
|
|
+ }
|
|
|
+
|
|
|
+ $data = base64_encode(file_get_contents($finalPath));
|
|
|
+ return [
|
|
|
+ 'mime_type' => $mimeType,
|
|
|
+ 'data' => $data
|
|
|
+ ];
|
|
|
+ } catch (\App\Exceptions\ApiException $e) {
|
|
|
+ // 业务错误(如文件过大)原样抛出,让前端看到明确提示
|
|
|
+ throw $e;
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ dLog('deepseek')->error('Gemini多媒体处理失败: ' . $e->getMessage());
|
|
|
+ return null;
|
|
|
+ } finally {
|
|
|
+ if (!empty($compressedPath) && $compressedPath !== $localPath && file_exists($compressedPath)) {
|
|
|
+ @unlink($compressedPath);
|
|
|
+ }
|
|
|
+ if ($isTempFile && $localPath && file_exists($localPath)) {
|
|
|
+ @unlink($localPath);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 下载远程多媒体到临时目录
|
|
|
+ *
|
|
|
+ * @return string|null 本地临时文件路径
|
|
|
+ */
|
|
|
+ private function downloadMediaToTemp($url) {
|
|
|
+ try {
|
|
|
+ $tempDir = storage_path('app/temp/gemini');
|
|
|
+ if (!is_dir($tempDir)) {
|
|
|
+ mkdir($tempDir, 0775, true);
|
|
|
+ }
|
|
|
+
|
|
|
+ $extension = 'tmp';
|
|
|
+ $pathInfo = parse_url($url, PHP_URL_PATH);
|
|
|
+ if ($pathInfo) {
|
|
|
+ $ext = pathinfo($pathInfo, PATHINFO_EXTENSION);
|
|
|
+ if ($ext && strlen($ext) <= 10) {
|
|
|
+ $extension = strtolower($ext);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ $tmpFile = $tempDir . '/' . uniqid('gemini_media_') . '.' . $extension;
|
|
|
+ $client = new Client(['timeout' => 300, 'verify' => false]);
|
|
|
+ $response = $client->get($url, ['sink' => $tmpFile]);
|
|
|
+
|
|
|
+ if (!file_exists($tmpFile) || filesize($tmpFile) <= 0) {
|
|
|
+ @unlink($tmpFile);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ return $tmpFile;
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ dLog('deepseek')->warning('Gemini多媒体下载失败: ' . $e->getMessage(), ['url' => $url]);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 检测多媒体文件 MIME 类型
|
|
|
+ */
|
|
|
+ private function detectMediaMime($filePath) {
|
|
|
+ $imageInfo = @getimagesize($filePath);
|
|
|
+ if ($imageInfo !== false && isset($imageInfo['mime'])) {
|
|
|
+ return $imageInfo['mime'];
|
|
|
+ }
|
|
|
+
|
|
|
+ // 视频按扩展名粗略判断
|
|
|
+ $ext = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
|
|
|
+ $videoExts = ['mp4' => 'video/mp4', 'mov' => 'video/quicktime', 'avi' => 'video/x-msvideo',
|
|
|
+ 'mkv' => 'video/x-matroska', 'webm' => 'video/webm', 'm4v' => 'video/x-m4v',
|
|
|
+ 'flv' => 'video/x-flv', 'wmv' => 'video/x-ms-wmv', 'ts' => 'video/mp2t'];
|
|
|
+ if (isset($videoExts[$ext])) {
|
|
|
+ return $videoExts[$ext];
|
|
|
+ }
|
|
|
+
|
|
|
+ $imageExts = ['jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'png' => 'image/png',
|
|
|
+ 'gif' => 'image/gif', 'webp' => 'image/webp', 'bmp' => 'image/bmp'];
|
|
|
+ if (isset($imageExts[$ext])) {
|
|
|
+ return $imageExts[$ext];
|
|
|
+ }
|
|
|
+
|
|
|
+ return 'application/octet-stream';
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取多媒体时长(秒),非视频或失败返回 0
|
|
|
+ */
|
|
|
+ private function getMediaDuration($filePath) {
|
|
|
+ $ffprobePath = env('FFPROBE_PATH', 'ffprobe');
|
|
|
+ $cmd = '"' . $ffprobePath . '" -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "' . $filePath . '" 2>&1';
|
|
|
+ $output = trim((string)shell_exec($cmd));
|
|
|
+ $duration = (float)$output;
|
|
|
+ return $duration > 0 ? $duration : 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 检查 ffmpeg 是否可用
|
|
|
+ */
|
|
|
+ private function ffmpegAvailable() {
|
|
|
+ static $available = null;
|
|
|
+ if ($available !== null) {
|
|
|
+ return $available;
|
|
|
+ }
|
|
|
+
|
|
|
+ $ffmpegPath = env('FFMPEG_PATH', 'ffmpeg');
|
|
|
+ if (strtoupper((string)env('APP_ENV', 'production')) === 'LOCAL') {
|
|
|
+ // 本地环境不执行 ffmpeg 压缩(与现有图片超分逻辑保持一致)
|
|
|
+ $available = false;
|
|
|
+ return $available;
|
|
|
+ }
|
|
|
+
|
|
|
+ $cmd = '"' . $ffmpegPath . '" -version 2>&1';
|
|
|
+ $output = shell_exec($cmd);
|
|
|
+ $available = is_string($output) && stripos($output, 'ffmpeg') !== false;
|
|
|
+ return $available;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 使用 ffmpeg 压缩多媒体文件
|
|
|
+ *
|
|
|
+ * @param string $inputFile 输入文件路径
|
|
|
+ * @param bool $isVideo 是否为视频
|
|
|
+ * @param float $duration 视频时长(秒),非视频传 0
|
|
|
+ * @return string|null 压缩后的文件路径,失败返回 null
|
|
|
+ */
|
|
|
+ private function compressMediaFile($inputFile, $isVideo, $duration = 0) {
|
|
|
+ try {
|
|
|
+ $tempDir = storage_path('app/temp/gemini');
|
|
|
+ if (!is_dir($tempDir)) {
|
|
|
+ mkdir($tempDir, 0775, true);
|
|
|
+ }
|
|
|
+
|
|
|
+ $ffmpegPath = env('FFMPEG_PATH', 'ffmpeg');
|
|
|
+ $uniqueId = uniqid('gemini_cmp_') . bin2hex(random_bytes(4));
|
|
|
+ $outputFile = $tempDir . '/' . $uniqueId . ($isVideo ? '.mp4' : '.jpg');
|
|
|
+
|
|
|
+ if ($isVideo) {
|
|
|
+ // 视频:时长超过5分钟则加速压缩到5分钟以内
|
|
|
+ $speedFactor = 1.0;
|
|
|
+ if ($duration > 300) {
|
|
|
+ $speedFactor = $duration / 300;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 目标文件大小:14MB,按加速后时长估算码率(不高于 6000kbps,不低于 300kbps)
|
|
|
+ $finalDuration = $duration / max(1.0, $speedFactor);
|
|
|
+ $targetBitrate = (int)((14 * 1024 * 8) / max(1, $finalDuration));
|
|
|
+ $targetBitrate = max(300, min(6000, $targetBitrate));
|
|
|
+
|
|
|
+ $filters = [];
|
|
|
+ $filters[] = 'scale=min(1280,iw):-2';
|
|
|
+ $filters[] = 'fps=24';
|
|
|
+
|
|
|
+ if ($speedFactor > 1.0) {
|
|
|
+ $filters[] = 'setpts=' . number_format(1 / $speedFactor, 6, '.', '') . '*PTS';
|
|
|
+ // 用多个 atempo 级联实现加速:前 n-1 个为 2.0,最后一个补足剩余倍数
|
|
|
+ $fullCount = (int)ceil($speedFactor / 2.0);
|
|
|
+ $lastTempo = $speedFactor / pow(2.0, $fullCount - 1);
|
|
|
+ if ($lastTempo < 0.5) {
|
|
|
+ $fullCount++;
|
|
|
+ $lastTempo = $speedFactor / pow(2.0, $fullCount - 1);
|
|
|
+ }
|
|
|
+ for ($i = 0; $i < $fullCount; $i++) {
|
|
|
+ $tempoValue = $i === $fullCount - 1 ? $lastTempo : 2.0;
|
|
|
+ $filters[] = 'atempo=' . number_format($tempoValue, 6, '.', '');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ $filterStr = implode(',', $filters);
|
|
|
+ $cmd = '"' . $ffmpegPath . '" -i "' . $inputFile . '" -vf "' . $filterStr
|
|
|
+ . '" -c:v libx264 -b:v ' . $targetBitrate . 'k -preset medium'
|
|
|
+ . ' -c:a aac -b:a 96k -movflags +faststart -y "' . $outputFile . '" 2>&1';
|
|
|
+ } else {
|
|
|
+ // 图片:缩放至最长边2048px(保持纵横比),JPEG质量压缩
|
|
|
+ $imageInfo = @getimagesize($inputFile);
|
|
|
+ $scaleW = 2048;
|
|
|
+ $scaleH = 2048;
|
|
|
+ if ($imageInfo !== false && $imageInfo[0] > 0 && $imageInfo[1] > 0) {
|
|
|
+ if ($imageInfo[0] >= $imageInfo[1]) {
|
|
|
+ $scaleH = (int)round($imageInfo[1] * 2048 / $imageInfo[0]);
|
|
|
+ $scaleW = 2048;
|
|
|
+ } else {
|
|
|
+ $scaleW = (int)round($imageInfo[0] * 2048 / $imageInfo[1]);
|
|
|
+ $scaleH = 2048;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ $cmd = '"' . $ffmpegPath . '" -i "' . $inputFile
|
|
|
+ . '" -vf "scale=' . $scaleW . ':' . $scaleH . '"'
|
|
|
+ . ' -q:v 3 -y "' . $outputFile . '" 2>&1';
|
|
|
+ }
|
|
|
+
|
|
|
+ dLog('deepseek')->info('Gemini开始压缩多媒体', ['command' => $cmd]);
|
|
|
+ $output = shell_exec($cmd);
|
|
|
+
|
|
|
+ if (!file_exists($outputFile) || filesize($outputFile) <= 0) {
|
|
|
+ dLog('deepseek')->error('Gemini多媒体压缩失败', [
|
|
|
+ 'input' => $inputFile,
|
|
|
+ 'output' => $output
|
|
|
+ ]);
|
|
|
+ @unlink($outputFile);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ dLog('deepseek')->info('Gemini多媒体压缩成功', [
|
|
|
+ 'input_size' => round(filesize($inputFile) / 1024 / 1024, 2) . 'MB',
|
|
|
+ 'output_size' => round(filesize($outputFile) / 1024 / 1024, 2) . 'MB'
|
|
|
+ ]);
|
|
|
+
|
|
|
+ return $outputFile;
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ dLog('deepseek')->error('Gemini多媒体压缩异常: ' . $e->getMessage());
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
* 获取剧本资产生成任务列表(按用户分页,每页15条)
|
|
|
*
|
|
|
* @param array $data
|
|
|
@@ -2701,6 +3532,9 @@ Q版卡通风格,头大身小,造型圆润可爱,线条简单,色彩明
|
|
|
} else if (in_array($model, $this->gpt_text_models)) {
|
|
|
// GPT-5.4 模型使用 TokenRouter API
|
|
|
$streamGenerator = $this->gpt54StreamResponse($post_data);
|
|
|
+ } else if (in_array($model, $this->gemini_text_models)) {
|
|
|
+ // Gemini 模型使用 Gemini API
|
|
|
+ $streamGenerator = $this->geminiStreamResponse($post_data, $model);
|
|
|
} else {
|
|
|
// 其他模型使用火山引擎API
|
|
|
$streamGenerator = $this->volcEngineChatCompletion($post_data);
|
|
|
@@ -2985,6 +3819,9 @@ Q版卡通风格,头大身小,造型圆润可爱,线条简单,色彩明
|
|
|
} else if (in_array($model, $this->gpt_text_models)) {
|
|
|
// GPT-5.4 模型使用 TokenRouter API
|
|
|
$streamGenerator = $this->gpt54StreamResponse($post_data);
|
|
|
+ } else if (in_array($model, $this->gemini_text_models)) {
|
|
|
+ // Gemini 模型使用 Gemini API
|
|
|
+ $streamGenerator = $this->geminiStreamResponse($post_data, $model);
|
|
|
} else {
|
|
|
// 其他模型使用火山引擎API
|
|
|
$streamGenerator = $this->volcEngineChatCompletion($post_data);
|
|
|
@@ -4112,6 +4949,9 @@ Q版卡通风格,头大身小,造型圆润可爱,线条简单,色彩明
|
|
|
} else if (in_array($model, $this->gpt_text_models)) {
|
|
|
// GPT-5.4 模型使用 TokenRouter API
|
|
|
$streamGenerator = $this->gpt54StreamResponse($post_data);
|
|
|
+ } else if (in_array($model, $this->gemini_text_models)) {
|
|
|
+ // Gemini 模型使用 Gemini API
|
|
|
+ $streamGenerator = $this->geminiStreamResponse($post_data, $model);
|
|
|
} else {
|
|
|
// 其他模型使用火山引擎API
|
|
|
$streamGenerator = $this->volcEngineChatCompletion($post_data);
|
|
|
@@ -4961,6 +5801,9 @@ Q版卡通风格,头大身小,造型圆润可爱,线条简单,色彩明
|
|
|
} else if (in_array($model, $this->gpt_text_models)) {
|
|
|
// GPT-5.4 模型使用 TokenRouter API
|
|
|
$streamGenerator = $this->gpt54StreamResponse($post_data);
|
|
|
+ } else if (in_array($model, $this->gemini_text_models)) {
|
|
|
+ // Gemini 模型使用 Gemini API
|
|
|
+ $streamGenerator = $this->geminiStreamResponse($post_data, $model);
|
|
|
} else {
|
|
|
$streamGenerator = $this->volcEngineChatCompletion($post_data);
|
|
|
}
|
|
|
@@ -5826,6 +6669,9 @@ Q版卡通风格,头大身小,造型圆润可爱,线条简单,色彩明
|
|
|
} else if (in_array($model, $this->gpt_text_models)) {
|
|
|
// GPT-5.4 模型使用 TokenRouter API
|
|
|
$streamGenerator = $this->gpt54StreamResponse($post_data);
|
|
|
+ } else if (in_array($model, $this->gemini_text_models)) {
|
|
|
+ // Gemini 模型使用 Gemini API
|
|
|
+ $streamGenerator = $this->geminiStreamResponse($post_data, $model);
|
|
|
} else {
|
|
|
// 其他模型使用火山引擎API
|
|
|
$streamGenerator = $this->volcEngineChatCompletion($post_data);
|
|
|
@@ -6575,6 +7421,9 @@ Q版卡通风格,头大身小,造型圆润可爱,线条简单,色彩明
|
|
|
} else if (in_array($model, $this->gpt_text_models)) {
|
|
|
// GPT-5.4 模型使用 TokenRouter API
|
|
|
$streamGenerator = $this->gpt54StreamResponse($post_data);
|
|
|
+ } else if (in_array($model, $this->gemini_text_models)) {
|
|
|
+ // Gemini 模型使用 Gemini API
|
|
|
+ $streamGenerator = $this->geminiStreamResponse($post_data, $model);
|
|
|
} else {
|
|
|
// 其他模型使用火山引擎API
|
|
|
$streamGenerator = $this->volcEngineChatCompletion($post_data);
|
|
|
@@ -7598,6 +8447,9 @@ Q版卡通风格,头大身小,造型圆润可爱,线条简单,色彩明
|
|
|
} else if (in_array($model, $this->gpt_text_models)) {
|
|
|
// GPT-5.4 模型使用 TokenRouter API
|
|
|
$streamGenerator = $this->gpt54StreamResponse($post_data);
|
|
|
+ } else if (in_array($model, $this->gemini_text_models)) {
|
|
|
+ // Gemini 模型使用 Gemini API
|
|
|
+ $streamGenerator = $this->geminiStreamResponse($post_data, $model);
|
|
|
} else {
|
|
|
$streamGenerator = $this->volcEngineChatCompletion($post_data);
|
|
|
}
|
|
|
@@ -8699,6 +9551,9 @@ Q版卡通风格,头大身小,造型圆润可爱,线条简单,色彩明
|
|
|
} else if (in_array($model, $this->gpt_text_models)) {
|
|
|
// GPT-5.4 模型使用 TokenRouter API
|
|
|
$streamGenerator = $this->gpt54StreamResponse($post_data);
|
|
|
+ } else if (in_array($model, $this->gemini_text_models)) {
|
|
|
+ // Gemini 模型使用 Gemini API
|
|
|
+ $streamGenerator = $this->geminiStreamResponse($post_data, $model);
|
|
|
} else {
|
|
|
// 其他模型使用火山引擎API
|
|
|
$streamGenerator = $this->volcEngineChatCompletion($post_data);
|
|
|
@@ -8876,6 +9731,9 @@ Q版卡通风格,头大身小,造型圆润可爱,线条简单,色彩明
|
|
|
$streamGenerator = $this->deepSeekStreamResponse($post_data);
|
|
|
} else if (in_array($model, $this->gpt_text_models)) {
|
|
|
$streamGenerator = $this->gpt54StreamResponse($post_data);
|
|
|
+ } else if (in_array($model, $this->gemini_text_models)) {
|
|
|
+ // Gemini 模型使用 Gemini API
|
|
|
+ $streamGenerator = $this->geminiStreamResponse($post_data, $model);
|
|
|
} else {
|
|
|
$streamGenerator = $this->volcEngineChatCompletion($post_data);
|
|
|
}
|