diff --git a/app/api/ai/responses/route.js b/app/api/ai/responses/route.js new file mode 100644 index 0000000..3b0e444 --- /dev/null +++ b/app/api/ai/responses/route.js @@ -0,0 +1,149 @@ +// OpenAI Responses API — SSE 流式转发(Edge Runtime 确保流式不被缓冲) +// 支持使用 /v1/responses 格式的提供商 + +export const runtime = 'edge'; + +export async function POST(request) { + try { + const { systemPrompt, userPrompt, apiConfig, maxTokens } = await request.json(); + + const apiKey = apiConfig?.apiKey; + const baseUrl = (apiConfig?.baseUrl || 'https://api.openai.com/v1').replace(/\/$/, ''); + const model = apiConfig?.model || 'gpt-4o-mini'; + + if (!apiKey) { + return new Response( + JSON.stringify({ error: '请先配置 API Key。点击左下角 ⚙️ → API配置,填入你的 Key' }), + { status: 400, headers: { 'Content-Type': 'application/json' } } + ); + } + + const url = `${baseUrl}/responses`; + + // 构造 Responses API 请求体 + const requestBody = { + model, + input: [ + { role: 'developer', content: systemPrompt }, + { role: 'user', content: userPrompt }, + ], + max_output_tokens: maxTokens || 4096, + stream: true, + }; + + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${apiKey}`, + }, + body: JSON.stringify(requestBody), + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error('Responses API 错误:', response.status, errorText); + + const errorMessages = { + 401: 'API Key 无效或已过期,请检查后重新填写', + 429: '请求频率过高或额度不足,请稍后再试', + }; + const errMsg = errorMessages[response.status] + || `AI服务返回错误(${response.status}),请检查 API 配置`; + + return new Response( + JSON.stringify({ error: errMsg }), + { status: response.status, headers: { 'Content-Type': 'application/json' } } + ); + } + + // 将上游 SSE 流解析并转发为统一的 {text, thinking} 格式 + const encoder = new TextEncoder(); + const decoder = new TextDecoder(); + + const stream = new ReadableStream({ + async start(controller) { + const reader = response.body.getReader(); + let buffer = ''; + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split('\n'); + buffer = lines.pop() || ''; + + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith(':')) continue; + + // Responses API 格式: event: xxx\ndata: {...} + if (trimmed.startsWith('data: ')) { + try { + const json = JSON.parse(trimmed.slice(6)); + const eventType = json.type; + + // 处理文本输出 delta + if (eventType === 'response.output_text.delta') { + const delta = json.delta; + if (delta) { + controller.enqueue(encoder.encode(`data: ${JSON.stringify({ text: delta })}\n\n`)); + } + } + + // 处理思维链/推理摘要 delta + if (eventType === 'response.reasoning_summary_text.delta') { + const delta = json.delta; + if (delta) { + controller.enqueue(encoder.encode(`data: ${JSON.stringify({ thinking: delta })}\n\n`)); + } + } + + // 处理完成事件 + if (eventType === 'response.completed') { + // 提取 usage + const usage = json.response?.usage; + if (usage) { + controller.enqueue(encoder.encode(`data: ${JSON.stringify({ + usage: { + promptTokens: usage.input_tokens || 0, + completionTokens: usage.output_tokens || 0, + totalTokens: (usage.input_tokens || 0) + (usage.output_tokens || 0), + } + })}\n\n`)); + } + controller.enqueue(encoder.encode('data: [DONE]\n\n')); + } + } catch { + // 解析失败的行直接跳过 + } + } + } + } + } catch (err) { + console.error('Stream 读取错误:', err.message); + } finally { + controller.close(); + reader.releaseLock(); + } + } + }); + + return new Response(stream, { + headers: { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + }, + }); + + } catch (error) { + console.error('Responses API 接口错误:', error); + return new Response( + JSON.stringify({ error: '网络连接失败,请检查 API 地址是否正确' }), + { status: 500, headers: { 'Content-Type': 'application/json' } } + ); + } +} diff --git a/app/api/ai/test/route.js b/app/api/ai/test/route.js index 66cdeda..9dbe579 100644 --- a/app/api/ai/test/route.js +++ b/app/api/ai/test/route.js @@ -18,6 +18,11 @@ export async function POST(request) { return await testGeminiNative(apiKey, baseUrl, model); } + // OpenAI Responses 格式的测试 + if (provider === 'openai-responses') { + return await testResponsesAPI(apiKey, baseUrl, model); + } + // OpenAI 兼容格式的测试 return await testOpenAICompat(apiKey, baseUrl, model); @@ -103,3 +108,44 @@ async function testOpenAICompat(apiKey, baseUrl, model) { reply: reply.trim(), }); } + +async function testResponsesAPI(apiKey, baseUrl, model) { + const base = (baseUrl || 'https://api.openai.com/v1').replace(/\/$/, ''); + const m = model || 'gpt-4o-mini'; + const url = `${base}/responses`; + + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${apiKey}`, + }, + body: JSON.stringify({ + model: m, + input: '说"连接成功"', + max_output_tokens: 20, + }), + }); + + if (!response.ok) { + const errText = await response.text(); + let errMsg = `连接失败(${response.status})`; + try { + const errObj = JSON.parse(errText); + errMsg = errObj?.error?.message || errMsg; + } catch { /* ignore parse error */ } + return NextResponse.json({ success: false, error: errMsg }); + } + + const data = await response.json(); + const reply = data.output?.[0]?.content?.[0]?.text + || data.output_text + || ''; + + return NextResponse.json({ + success: true, + message: `✅ Responses API 连接成功!`, + model: m, + reply: reply.trim(), + }); +} diff --git a/app/components/AiSidebar.js b/app/components/AiSidebar.js index 24b0513..2ed7b05 100644 --- a/app/components/AiSidebar.js +++ b/app/components/AiSidebar.js @@ -195,7 +195,9 @@ export default function AiSidebar({ onInsertText }) { try { const { apiConfig } = getProjectSettings(); - const apiEndpoint = apiConfig?.provider === 'gemini-native' ? '/api/ai/gemini' : '/api/ai'; + const apiEndpoint = apiConfig?.provider === 'gemini-native' ? '/api/ai/gemini' + : apiConfig?.provider === 'openai-responses' ? '/api/ai/responses' + : '/api/ai'; const context = await buildContext(activeChapterId, text, contextSelection.size > 0 ? contextSelection : null); const systemPrompt = compileSystemPrompt(context, 'chat'); @@ -259,7 +261,9 @@ export default function AiSidebar({ onInsertText }) { try { const { apiConfig } = getProjectSettings(); - const apiEndpoint = apiConfig?.provider === 'gemini-native' ? '/api/ai/gemini' : '/api/ai'; + const apiEndpoint = apiConfig?.provider === 'gemini-native' ? '/api/ai/gemini' + : apiConfig?.provider === 'openai-responses' ? '/api/ai/responses' + : '/api/ai'; const context = await buildContext(activeChapterId, userMsg.content, contextSelection.size > 0 ? contextSelection : null); const systemPrompt = compileSystemPrompt(context, 'chat'); diff --git a/app/components/SettingsPanel.js b/app/components/SettingsPanel.js index 148cc70..5e153cf 100644 --- a/app/components/SettingsPanel.js +++ b/app/components/SettingsPanel.js @@ -415,6 +415,7 @@ const PROVIDERS = [ { key: 'openai', label: 'OpenAI', baseUrl: 'https://api.openai.com/v1', models: ['gpt-4o-mini', 'gpt-4o', 'gpt-4-turbo', 'gpt-3.5-turbo'] }, { key: 'gemini', label: 'Gemini (OpenAI兼容)', baseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai', models: ['gemini-2.0-flash', 'gemini-2.0-flash-lite', 'gemini-1.5-flash', 'gemini-1.5-pro'] }, { key: 'gemini-native', label: 'Gemini(原生格式)', baseUrl: 'https://generativelanguage.googleapis.com/v1beta', models: ['gemini-2.0-flash', 'gemini-2.0-flash-lite', 'gemini-1.5-flash', 'gemini-1.5-pro'] }, + { key: 'openai-responses', label: 'OpenAI Responses', baseUrl: 'https://api.openai.com/v1', models: [] }, { key: 'siliconflow', label: 'SiliconFlow (硅基流动)', baseUrl: 'https://api.siliconflow.cn/v1', models: ['deepseek-ai/DeepSeek-V3', 'Qwen/Qwen2.5-72B-Instruct', 'THUDM/glm-4-9b-chat'] }, { key: 'moonshot', label: 'Moonshot (Kimi)', baseUrl: 'https://api.moonshot.cn/v1', models: ['moonshot-v1-8k', 'moonshot-v1-32k', 'moonshot-v1-128k'] }, { key: 'custom', label: '自定义 (OpenAI兼容)', baseUrl: '', models: [] }, diff --git a/app/page.js b/app/page.js index 04450da..c69fb5f 100644 --- a/app/page.js +++ b/app/page.js @@ -182,7 +182,9 @@ export default function Home() { const userPrompt = compileUserPrompt(mode, text, instruction); const { apiConfig } = getProjectSettings(); - const apiEndpoint = apiConfig?.provider === 'gemini-native' ? '/api/ai/gemini' : '/api/ai'; + const apiEndpoint = apiConfig?.provider === 'gemini-native' ? '/api/ai/gemini' + : apiConfig?.provider === 'openai-responses' ? '/api/ai/responses' + : '/api/ai'; const res = await fetch(apiEndpoint, { method: 'POST',