OPENAI_API_KEY获取渠道:(都需要替换服务端域名)
API2D—— 请求包装过的接口,订阅价格为原价的1.5倍,加拿大付费合法渠道。
AI Proxy——请求openai接口,订阅价格为原价的1.12倍,新加坡付费合法渠道,账号安全。
OhMyGPT——请求openai接口(需加代理), 按分钟使用次数收费,60次/分内免费,个人付费渠道,安全性低。
应用方向:(基本都为直接收费模式;可以考虑间接收费,通过广告,流量盈利)
- 出售api_key,因为openai接口收费,且需代理,所以这个在国内是有较大需求的,包括二次出售,例如: API2D
- 精化prompt,根据指定方向训练,来单一化提供功能,例如小红书标题,周报,月报等,例如: aitxt.app
- ......
关于流式调用openAI接口的尝试:
要了解chatGPT的流式获取,首先介绍一下Server-Sent Events服务器信息推送
SSE(Server-Sent Events) 的客户端 API 部署在EventSource对象上, EventSource是服务器推送的一个网络事件接口。一个EventSource实例会对HTTP服务开启一个持久化的连接,以text/event-stream格式发送事件, 会一直保持开启直到被要求关闭。
一、在服务端openai返回的流式数据后,首先尝试了使用new EventSource()来建立持久化连接
注意点:请求头通过EventSource制定了请求头为text/event-stream,服务端也需要指定返回格式为text/event-stream
不同的model,返回的流内容不同,需要做对应解析:
text-davinci-003 返回:
data: {"id":"cmpl-7dguOmZt40yelK9VLxOattYQPutXC","object":"text_completion","created":1689694088,"choices":[{"text":"发","index":0,"logprobs":null,"finish_reason":null}],"model":"text-davinci-003"}
gpt-3.5-turbo 返回:
data: {"id":"chatcmpl-7dgvw4NaEd67U01ZsFmXgZMjuDW3X","object":"chat.completion.chunk","created":1689694184,"model":"gpt-3.5-turbo-0613","choices":[{"index":0,"delta":{"content":"?"},"finish_reason":null}]}
// 服务端...
// 响应头需要设置text/event-steam
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Connection", "keep-alive");
const gptResult = await openai.createCompletion({
model:"text-davinci-003",
prompt: req.query.data,
max_tokens: 100,
temperature: 0,
stream: true,
}, { responseType: 'stream' });
gptResult.data.on('data', chunk => { //buffer流
const lines = chunk.toString().split('\n').filter(line => line.trim() !== '');
for (const line of lines) {
const message = line.replace(/^data: /, '');
if (message === '[DONE]') {
res.end();
return; // Stream finished
}
try {
const parsed = JSON.parse(message);
const content = parsed.choices[0].text;
console.log(res.write, parsed.choices[0].text);
res.write(`data: ${content}\n\n`); // Send SSE message to the browser client
} catch(error) {
console.error('Could not JSON parse stream message', message, error);
}
}
});
// 客户端...
useEffect(() => {
source.onmessage = (event) => {
setMessages((prevMessages) => [...prevMessages, event.data]);
};
source.onerror = (error) => {
console.error('EventSource error:', error);
source.close();
};
return () => {
source?.close?.();
setMessages([]);
};
}, [source]);
const onBlur =async (data) => {
const source = new EventSource('/api/event-source?data='+data.target.value);
setSource(source);
}
二、后来在开发中,发现new EventSource()这种长链接,有一个较大的问题,就是它只能发送get请求,没有办法发送post请求
于是尝试使用fetchEventSource, fetchEventSource本质 利用了fetch返回promise的特点以及修改请求头为text/event-stream来实现持续获取数据
并且 1. 可以通过和new AbortController()结合,来中止fetch请求 2. 接口失败默认会触发重试
//服务端...
// 响应头需要设置text/event-steam
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Connection", "keep-alive");
const gptResult = await openai.createChatCompletion({
messages:req.body,
stream: true,
model: 'gpt-3.5-turbo',
}, { responseType: 'stream' });
gptResult.data.on('data', chunk => {
const lines = chunk.toString().split('\n').filter(line => line.trim() !== '');
for (const line of lines) {
const message = line.replace(/^data: /, '');
if (message === '[DONE]') {
res.end();
return; // Stream finished
}
try {
const parsed = JSON.parse(message);
const content = parsed.choices[0].delta.content || '';
console.log(res.write, parsed.choices[0].delta.content);
res.write(`data: ${content}\n\n`); // Send SSE message to the browser client
} catch(error) {
console.error('Could not JSON parse stream message', message, error);
}
}
});
//客户端...
await fetchEventSource('/api/fetch-event-source', {
signal:updateController().signal,
method: 'POST',
body: JSON.stringify([{"role": "system", "content": data.target.value}]),
headers: {
'Content-Type': 'application/json'
},
onmessage:((event) => {
setMessages((prevMessages) => [...prevMessages, event.data]);
}),
onerror:((error) => {
// setAvailable(true);
console.error('EventSource error:', error);
}),
onclose:((a) => {
// setAvailable(true);
console.log(a,23);
})
});
三、测试时发现,极易出现上一轮请求未结束就开始下一轮请求
所以做出调整
// 客户端...
const updateController=()=>{
controllerRef.current.abort(); //中止旧的
const controller = new AbortController(); //创建新的
controllerRef.current = controller;
return controller;
}
遇到的问题:
1. 当使用流式获取信息时,如果不加开关,容易出现上一个请求没结束,就开始下一个请求的情况
解决方案:
- 若是new EventSource(url),则可直接调用 实例上的close()方法,关闭连接。
- 若是fetch-event-source,则有两种方式
-
通过状态控制,在第一次请求没有停止前,禁止调用第二次请求。(有点类似现在的openai官网使用方式)
-
通过 new AbortController().abort() 来中止进行中的异步任务,即fetch返回的promise
-
参考: