feat: add OpenAI Responses API support for /v1/responses providers

This commit is contained in:
YuanShiJiLoong committed 2026-02-25 02:19:56 +09:00
1 parent 2d30f27e14
commit 0754913f43
5 files changed
+205 -3

No files matched your search

+149
View File
@@ -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' } }
);
}
}
+46
View File
@@ -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(),
});
}
+6 -2
View File
@@ -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');
+1
View File
@@ -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: [] },
+3 -1
View File
@@ -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',