Skip to content

Commit 5cfe16c

Browse files
authored
feat: stream support (#14)
1 parent 3cc02ee commit 5cfe16c

6 files changed

Lines changed: 180 additions & 30 deletions

File tree

deno.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
{
2-
"tasks": {}
2+
"tasks": {},
3+
"lock": false
34
}

examples/chatCompletionStream.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { OpenAI } from "../mod.ts";
2+
3+
const openAI = new OpenAI(Deno.env.get("YOUR_API_KEY")!);
4+
5+
await openAI.createChatCompletionStream({
6+
model: "gpt-3.5-turbo",
7+
messages: [
8+
{ "role": "system", "content": "You are a helpful assistant." },
9+
{ "role": "user", "content": "Who won the world series in 2020?" },
10+
{
11+
"role": "assistant",
12+
"content": "The Los Angeles Dodgers won the World Series in 2020.",
13+
},
14+
{ "role": "user", "content": "Where was it played?" },
15+
],
16+
}, (chunk) => {
17+
console.log(chunk);
18+
});

examples/completionStream.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { OpenAI } from "../mod.ts";
2+
3+
const openAI = new OpenAI(Deno.env.get("YOUR_API_KEY")!);
4+
5+
openAI.createCompletionStream({
6+
model: "davinci",
7+
prompt: "The meaning of life is",
8+
}, (chunk) => {
9+
console.log(chunk);
10+
});

src/openai.ts

Lines changed: 82 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1+
import { basename } from "https://deno.land/std@0.189.0/path/mod.ts";
2+
import { decodeStream, throwError } from "./util.ts";
13
import type {
24
ChatCompletion,
35
ChatCompletionOptions,
6+
ChatCompletionStream,
47
Completion,
58
CompletionOptions,
9+
CompletionStream,
610
DeletedFile,
711
DeletedFineTune,
812
Edit,
@@ -29,7 +33,6 @@ import type {
2933
Translation,
3034
TranslationOptions,
3135
} from "./types.ts";
32-
import { basename } from "https://deno.land/std@0.187.0/path/mod.ts";
3336

3437
const defaultBaseUrl = "https://api.openai.com/v1";
3538

@@ -67,17 +70,7 @@ export class OpenAI {
6770
);
6871
const data = await response.json();
6972

70-
if (data.error) {
71-
let errorMessage = `${data.error.type}`;
72-
if (data.error.message) {
73-
errorMessage += ": " + data.error.message;
74-
}
75-
if (data.error.code) {
76-
errorMessage += ` (${data.error.code})`;
77-
}
78-
console.log(data.error);
79-
throw new Error(errorMessage);
80-
}
73+
throwError(data);
8174

8275
return data;
8376
}
@@ -108,7 +101,6 @@ export class OpenAI {
108101
* https://platform.openai.com/docs/api-reference/completions/create
109102
*/
110103
async createCompletion(options: CompletionOptions): Promise<Completion> {
111-
// TODO: make options.stream work
112104
return await this.#request(`/completions`, {
113105
model: options.model,
114106
prompt: options.prompt,
@@ -117,7 +109,6 @@ export class OpenAI {
117109
temperature: options.temperature,
118110
top_p: options.topP,
119111
n: options.n,
120-
stream: options.stream,
121112
logprobs: options.logprobs,
122113
echo: options.echo,
123114
stop: options.stop,
@@ -129,6 +120,46 @@ export class OpenAI {
129120
});
130121
}
131122

123+
/**
124+
* Creates a completion stream for the provided prompt and parameters
125+
*
126+
* https://platform.openai.com/docs/api-reference/completions/create
127+
*/
128+
async createCompletionStream(
129+
options: Omit<CompletionOptions, "bestOf">,
130+
callback: (chunk: CompletionStream) => void,
131+
): Promise<void> {
132+
const res = await fetch(
133+
`${this.#baseUrl}/completions`,
134+
{
135+
method: "POST",
136+
headers: {
137+
Authorization: `Bearer ${this.#privateKey}`,
138+
"Content-Type": "application/json",
139+
},
140+
body: JSON.stringify({
141+
model: options.model,
142+
prompt: options.prompt,
143+
suffix: options.suffix,
144+
max_tokens: options.maxTokens,
145+
temperature: options.temperature,
146+
top_p: options.topP,
147+
n: options.n,
148+
stream: true,
149+
logprobs: options.logprobs,
150+
echo: options.echo,
151+
stop: options.stop,
152+
presence_penalty: options.presencePenalty,
153+
frequency_penalty: options.frequencyPenalty,
154+
logit_bias: options.logitBias,
155+
user: options.user,
156+
}),
157+
},
158+
);
159+
160+
await decodeStream(res, callback);
161+
}
162+
132163
/**
133164
* Creates a completion for the chat message
134165
*
@@ -143,7 +174,6 @@ export class OpenAI {
143174
temperature: options.temperature,
144175
top_p: options.topP,
145176
n: options.n,
146-
stream: options.stream,
147177
stop: options.stop,
148178
max_tokens: options.maxTokens,
149179
presence_penalty: options.presencePenalty,
@@ -153,6 +183,43 @@ export class OpenAI {
153183
});
154184
}
155185

186+
/**
187+
* Creates a completion stream for the chat message
188+
*
189+
* https://platform.openai.com/docs/api-reference/chat/create
190+
*/
191+
async createChatCompletionStream(
192+
options: ChatCompletionOptions,
193+
callback: (chunk: ChatCompletionStream) => void,
194+
): Promise<void> {
195+
const res = await fetch(
196+
`${this.#baseUrl}/chat/completions`,
197+
{
198+
method: "POST",
199+
headers: {
200+
Authorization: `Bearer ${this.#privateKey}`,
201+
"Content-Type": "application/json",
202+
},
203+
body: JSON.stringify({
204+
model: options.model,
205+
messages: options.messages,
206+
temperature: options.temperature,
207+
top_p: options.topP,
208+
n: options.n,
209+
stream: true,
210+
stop: options.stop,
211+
max_tokens: options.maxTokens,
212+
presence_penalty: options.presencePenalty,
213+
frequency_penalty: options.frequencyPenalty,
214+
logit_bias: options.logitBias,
215+
user: options.user,
216+
}),
217+
},
218+
);
219+
220+
await decodeStream(res, callback);
221+
}
222+
156223
/**
157224
* Creates a new edit for the provided input, instruction, and parameters.
158225
*

src/types.ts

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,6 @@ export interface CompletionOptions {
5252
*/
5353
n?: number;
5454

55-
/**
56-
* Whether to stream back partial progress.
57-
* If set, tokens will be sent as data-only server-sent events as they become available, with the stream terminated by a data: [DONE] message.
58-
* https://platform.openai.com/docs/api-reference/completions/create#completions/create-stream
59-
*/
60-
stream?: boolean;
61-
6255
/**
6356
* Include the log probabilities on the logprobs most likely tokens, as well the chosen tokens.
6457
* For example, if logprobs is 5, the API will return a list of the 5 most likely tokens.
@@ -161,13 +154,6 @@ export interface ChatCompletionOptions {
161154
*/
162155
n?: number;
163156

164-
/**
165-
* If set, partial message deltas will be sent, like in ChatGPT.
166-
* Tokens will be sent as data-only server-sent events as they become available, with the stream terminated by a data: [DONE] message.
167-
* https://platform.openai.com/docs/api-reference/chat/create#chat/create-stream
168-
*/
169-
stream?: boolean;
170-
171157
/**
172158
* Up to 4 sequences where the API will stop generating further tokens.
173159
* https://platform.openai.com/docs/api-reference/chat/create#chat/create-stop
@@ -607,6 +593,19 @@ export interface Completion {
607593
};
608594
}
609595

596+
export interface CompletionStream {
597+
id: string;
598+
object: "text_completion";
599+
created: number;
600+
model: string;
601+
choices: {
602+
text: string;
603+
index: number;
604+
logprobs: number | null;
605+
finish_reason: string;
606+
}[];
607+
}
608+
610609
export interface ChatCompletion {
611610
id: string;
612611
object: "chat.completion";
@@ -627,6 +626,21 @@ export interface ChatCompletion {
627626
};
628627
}
629628

629+
export interface ChatCompletionStream {
630+
id: string;
631+
object: "chat.completion.chunk";
632+
created: number;
633+
choices: {
634+
index: number;
635+
delta: {
636+
name?: string;
637+
role?: "system" | "assistant" | "user";
638+
content?: string;
639+
};
640+
finish_reason: string;
641+
}[];
642+
}
643+
630644
export interface Edit {
631645
object: "edit";
632646
created: number;

src/util.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { TextDelimiterStream } from "https://deno.land/std@0.189.0/streams/mod.ts";
2+
3+
export function throwError(
4+
data: { error?: { type: string; message: string; code: string } },
5+
) {
6+
if (data.error) {
7+
let errorMessage = `${data.error.type}`;
8+
if (data.error.message) {
9+
errorMessage += ": " + data.error.message;
10+
}
11+
if (data.error.code) {
12+
errorMessage += ` (${data.error.code})`;
13+
}
14+
// console.log(data.error);
15+
throw new Error(errorMessage);
16+
}
17+
}
18+
19+
// deno-lint-ignore no-explicit-any
20+
export async function decodeStream(
21+
res: Response,
22+
callback: (data: any) => void,
23+
) {
24+
const chunks = res.body!
25+
.pipeThrough(new TextDecoderStream())
26+
.pipeThrough(new TextDelimiterStream("\n\n"));
27+
28+
for await (const chunk of chunks) {
29+
let data;
30+
try {
31+
data = JSON.parse(chunk);
32+
} catch {
33+
// no-op (just checking if error message)
34+
}
35+
if (data) throwError(data);
36+
37+
if (chunk === "data: [DONE]") break;
38+
callback(JSON.parse(chunk.slice(6)));
39+
}
40+
}

0 commit comments

Comments
 (0)