Skip to content

Commit

Permalink
openai: add stuff
Browse files Browse the repository at this point in the history
  • Loading branch information
paperdave committed Apr 6, 2023
1 parent 23c2b3d commit 7435168
Show file tree
Hide file tree
Showing 18 changed files with 1,197 additions and 883 deletions.
5 changes: 5 additions & 0 deletions .changeset/calm-horses-impress.md
@@ -0,0 +1,5 @@
---
'@paperdave/openai': minor
---

Add Text Edit
5 changes: 5 additions & 0 deletions .changeset/late-pumas-visit.md
@@ -0,0 +1,5 @@
---
'@paperdave/openai': minor
---

Implement Retry Count
5 changes: 5 additions & 0 deletions .changeset/slow-fishes-chew.md
@@ -0,0 +1,5 @@
---
'@paperdave/openai': minor
---

Move to using camel case everywhere
81 changes: 71 additions & 10 deletions packages/openai/README.md
Expand Up @@ -82,16 +82,15 @@ const response = await generateChatCompletion({

// Optional: See OpenAI Docs
stream: boolean,
max_tokens: number,
maxTokens: number,
temperature: number,
top_p: number,
topP: number,
n: number,
logprobs: number,
stop: string | string[],
presence_penalty: number,
frequency_penalty: number,
best_of: number,
logit_bias: object,
presencePenalty: number,
frequencyPenalty: number,
bestOf: number,
logitBias: Record<string, number>,
user: string,
});
```
Expand Down Expand Up @@ -171,13 +170,75 @@ interface ChatCompletionMultiStream {
}
```

## Text Completions
## Text Completions / Insertions

Implemented, Docs coming soon.
Text completions allow you to complete a text prompt. [OpenAI's Guide](https://platform.openai.com/docs/guides/completion)

One function is given to call this api: `generateTextCompletion`, which functions almost identically to `generateChatCompletion`, except that instead of a `messages` array, you pass a `prompt` string.

```ts
import { generateTextCompletion } from '@paperdave/openai';

const response = await generateTextCompletion({
// Required arguments, see OpenAI Docs
model: TextModel,
prompt: string,

// Optionally override API key and organization.
auth: AuthOverride,
// Number of times to retry due to network/ratelimit issues, default 3
retry: number,

// Optional: See OpenAI Docs
suffix: string,
max_tokens: number,
temperature: number,
topP: number,
n: number,
logProbs: number,
stop: string | string[],
echo: boolean,
presencePenalty: number,
frequencyPenalty: number,
bestOf: number,
logitBias: Record<string, number>,
user: string,
});
```

The return type is almost identical to `generateChatCompletion`. To understand exactly how it works and how to stream results, see the [Chat Completions](#chat-completions) section above.

In addition, you can pass `logProbs: number` to get a `logProbs` object on the response.

## Text Edits

Coming Soon
The edits endpoint can be used to edit text, rather than just completing it. [OpenAI's Guide](https://platform.openai.com/docs/guides/completion/editing-text)

Editing text is done with the `generateTextEdit` function, which accepts the following arguments:

```ts
import { generateTextEdit } from '@paperdave/openai';

const response = await generateTextEdit({
// Required arguments, see OpenAI Docs
model: TextEditModel,
input?: string,
instruction: string,

// Optionally override API key and organization.
auth: AuthOverride,
// Number of times to retry due to network/ratelimit issues, default 3
retry: number,

// Optional: See OpenAI Docs
temperature: number,
topP: number,
n: number,
// user: string, // OpenAI should have this, but they dont
});
```

Other than the lack of streaming, the return type is nearly identical to `generateChatCompletion`. To understand exactly how it works, see the [Chat Completions](#chat-completions) section above.

## Images

Expand Down
8 changes: 8 additions & 0 deletions packages/openai/demos/edit-text.ts
@@ -0,0 +1,8 @@
import { generateTextEdit } from '../src/text-edit';

const generation = await generateTextEdit({
model: 'text-davinci-edit-001',
input: 'The quick brown fox jumps over the lazy dog.',
instruction: 'Convert to leet speak.',
});
console.log(generation);
File renamed without changes.
Expand Up @@ -2,10 +2,10 @@ import { generateTextCompletion } from '../src/text';

const stream = await generateTextCompletion({
model: 'ada',
prompt: 'say this is a test.',
prompt: '',
stream: true,
max_tokens: 10,
temperature: 1.25,
max_tokens: 500,
temperature: 2,
});

for await (const text of stream.content) {
Expand Down
58 changes: 28 additions & 30 deletions packages/openai/src/chat.ts
@@ -1,7 +1,8 @@
import { createArray, IterableStream } from '@paperdave/utils';
import { AuthOverride, getAuthHeaders } from './api-key';
import { AuthOverride } from './api-key';
import { fetchOpenAI, FetchOptions } from './fetch';
import { ChatModel, PRICING_CHAT, PRICING_TEXT } from './models';
import { FinishReason, RawCompletionUsage, td } from './shared';
import { CompletionUsage, FinishReason, RawCompletionUsage, td } from './shared';
import { countChatPromptTokens, countTokens } from './tokenization';

export type GPTMessageRole = 'system' | 'user' | 'assistant';
Expand Down Expand Up @@ -46,10 +47,8 @@ export interface RawChatCompletionChunkChoice {
index: number;
}

export interface ChatCompletionOptions<
Stream extends boolean = boolean,
N extends number = number
> {
export interface ChatCompletionOptions<Stream extends boolean = boolean, N extends number = number>
extends FetchOptions {
/**
* ID of the model to use. You can use the [List models](/docs/api-reference/models/list) API to
* see all of your available models, or see our [Model overview](/docs/models/overview) for
Expand All @@ -72,7 +71,7 @@ export interface ChatCompletionOptions<
* your prompt plus `max_tokens` cannot exceed the model's context length. Most models have a
* context length of 2048 tokens (except for the newest models, which support 4096).
*/
max_tokens?: number | null;
maxTokens?: number | null;
/**
* What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output
* more random, while lower values like 0.2 will make it more focused and deterministic. We
Expand All @@ -85,7 +84,7 @@ export interface ChatCompletionOptions<
* the top 10% probability mass are considered. We generally recommend altering this or
* `temperature` but not both.
*/
top_p?: number | null;
topP?: number | null;
/**
* How many completions to generate for each prompt. **Note:** Because this parameter generates
* many completions, it can quickly consume your token quota. Use carefully and ensure that you
Expand All @@ -99,14 +98,14 @@ export interface ChatCompletionOptions<
* in the text so far, increasing the model's likelihood to talk about new topics. [See more
* information about frequency and presence penalties.](/docs/api-reference/parameter-details)
*/
presence_penalty?: number | null;
presencePenalty?: number | null;
/**
* Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing
* frequency in the text so far, decreasing the model's likelihood to repeat the same line
* verbatim. [See more information about frequency and presence
* penalties.](/docs/api-reference/parameter-details)
*/
frequency_penalty?: number | null;
frequencyPenalty?: number | null;
/**
* Modify the likelihood of specified tokens appearing in the completion. Accepts a json object
* that maps tokens (specified by their token ID in the GPT tokenizer) to an associated bias value
Expand All @@ -117,7 +116,7 @@ export interface ChatCompletionOptions<
* should result in a ban or exclusive selection of the relevant token. As an example, you can
* pass `{\"50256\": -100}` to prevent the <|endoftext|> token from being generated.
*/
logit_bias?: Record<string, number> | null;
logitBias?: Record<string, number> | null;
/**
* A unique identifier representing your end-user, which can help OpenAI to monitor and detect
* abuse. [Learn more](/docs/guides/safety-best-practices/end-user-ids).
Expand All @@ -134,13 +133,6 @@ export interface ChatCompletionMetadata {
usage: CompletionUsage;
}

export interface CompletionUsage {
promptTokens: number;
completionTokens: number;
totalTokens: number;
price: number;
}

export interface ChatCompletion extends ChatCompletionMetadata, ChatCompletionChoice {}

export interface ChatCompletionMulti extends ChatCompletionMetadata {
Expand Down Expand Up @@ -176,22 +168,28 @@ export type ChatCompletionResultFromOptions<
export async function generateChatCompletion<Stream extends boolean, N extends number>(
options: ChatCompletionOptions<Stream, N>
): Promise<ChatCompletionResultFromOptions<Stream, N>> {
const { retry: retryCount, auth, ...gptOptions } = options;
const { retry, auth, ...gptOptions } = options;

const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'post',
headers: {
'Content-Type': 'application/json',
...getAuthHeaders(auth),
const response = await fetchOpenAI({
endpoint: '/chat/completions',
method: 'POST',
body: {
model: gptOptions.model,
prompt: gptOptions.messages,
max_tokens: gptOptions.maxTokens ?? (gptOptions as any).max_tokens,
temperature: gptOptions.temperature,
top_p: gptOptions.topP ?? (gptOptions as any).top_p,
n: gptOptions.n,
stop: gptOptions.stop,
presence_penalty: gptOptions.presencePenalty ?? (gptOptions as any).presence_penalty,
frequency_penalty: gptOptions.frequencyPenalty ?? (gptOptions as any).frequency_penalty,
logit_bias: gptOptions.logitBias ?? (gptOptions as any).logit_bias,
user: gptOptions.user,
},
body: JSON.stringify(gptOptions),
auth,
retry,
});

if (!response.ok) {
const error = await response.json();
throw new Error(`${response.status} ${response.statusText}: ${JSON.stringify(error)}`);
}

if (gptOptions.stream) {
const reader = (response as any).body.getReader();
if (!reader) {
Expand Down
64 changes: 64 additions & 0 deletions packages/openai/src/fetch.ts
@@ -0,0 +1,64 @@
import { delay } from '@paperdave/utils';
import { AuthOverride, getAuthHeaders } from './api-key';

export interface FetchOptions {
/** Number of retries before giving up. Defaults to 3. */
retry?: number;
/** Override authentication settings. */
auth?: AuthOverride;
}

export interface InternalFetchOptions extends FetchOptions {
endpoint: string;
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
body?: any;
init?: RequestInit;
}

const base = 'https://api.openai.com/v1';

export async function fetchOpenAI(options: InternalFetchOptions) {
const { endpoint, method, body, init, retry = 3, auth } = options;
const url = `${base}${endpoint}`;
const headers = {
...(body ? { 'Content-Type': 'application/json' } : {}),
...getAuthHeaders(auth),
};
const fullInit = {
...init,
method,
headers: {
...headers,
...init?.headers,
},
body: body ? JSON.stringify(body) : undefined,
};
let tries = 0;
while (true) {
try {
const response = await fetch(url, fullInit);
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After');
if (retryAfter) {
await delay(parseInt(retryAfter, 10) * 1000);
} else {
await delay(1000);
}
continue;
}
if (response.status >= 400) {
const error = await response.json();
throw new Error(
(error as any)?.error.message ??
(typeof error === 'string' ? error : JSON.stringify(error))
);
}
return response;
} catch (error) {
if (tries >= retry) {
throw error;
}
tries++;
}
}
}
2 changes: 2 additions & 0 deletions packages/openai/src/index.ts
@@ -1,6 +1,8 @@
export * from './api-key';
export type { FetchOptions } from './fetch';
export * from './chat';
export * from './log-probs';
export * from './models';
export * from './text';
export * from './text-edit';
export * from './tokenization';
2 changes: 0 additions & 2 deletions packages/openai/src/log-probs.ts
Expand Up @@ -21,8 +21,6 @@ export class LogProbs {
this.topLogProbs = raw.top_logprobs;
this.textOffsets = raw.text_offset;
}

// TODO: methods to make this more interesting
}

// hidden please
Expand Down
9 changes: 8 additions & 1 deletion packages/openai/src/models.ts
Expand Up @@ -20,9 +20,16 @@ export const PRICING_TEXT = {
babbage: 0.0005 / 1000,
ada: 0.0004 / 1000,
};

export type TextModel = keyof typeof PRICING_TEXT;

/** "During this initial beta period, usage of the edits endpoint is free." */
export const PRICING_TEXT_EDIT = {
'text-davinci-edit-001': 0,
'code-davinci-edit-001': 0,
};

export type TextEditModel = keyof typeof PRICING_TEXT_EDIT;

export const PRICING_IMAGE = {
1024: 0.02,
512: 0.018,
Expand Down
6 changes: 3 additions & 3 deletions packages/openai/src/shared.ts
@@ -1,9 +1,9 @@
export type FinishReason = 'stop' | 'length';

export interface RawCompletionUsage {
prompt_tokens: 10;
completion_tokens: 10;
total_tokens: 20;
prompt_tokens: number;
completion_tokens: number;
total_tokens: number;
}

export interface CompletionUsage {
Expand Down

0 comments on commit 7435168

Please sign in to comment.