feat: add OpenAI Responses API support for /v1/responses providers
This commit is contained in:
1 parent
2d30f27e14
commit
0754913f43
5 files changed
+205
-3
No files matched your search
@@ -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' } }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
});
|
||||
}
|
||||
@@ -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');
|
||||
|
||||
@@ -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
@@ -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',
|
||||
|
||||
Reference in new issue
Block a user