198 lines
4.3 KiB
TypeScript
198 lines
4.3 KiB
TypeScript
import {
|
|
AIStream,
|
|
readableFromAsyncIterable,
|
|
type AIStreamCallbacksAndOptions,
|
|
createCallbacksTransformer,
|
|
} from './ai-stream';
|
|
import { createStreamDataTransformer } from './stream-data';
|
|
|
|
// from anthropic sdk (Completion)
|
|
interface CompletionChunk {
|
|
/**
|
|
* Unique object identifier.
|
|
*
|
|
* The format and length of IDs may change over time.
|
|
*/
|
|
id: string;
|
|
|
|
/**
|
|
* The resulting completion up to and excluding the stop sequences.
|
|
*/
|
|
completion: string;
|
|
|
|
/**
|
|
* The model that handled the request.
|
|
*/
|
|
model: string;
|
|
|
|
/**
|
|
* The reason that we stopped.
|
|
*
|
|
* This may be one the following values:
|
|
*
|
|
* - `"stop_sequence"`: we reached a stop sequence — either provided by you via the
|
|
* `stop_sequences` parameter, or a stop sequence built into the model
|
|
* - `"max_tokens"`: we exceeded `max_tokens_to_sample` or the model's maximum
|
|
*/
|
|
stop_reason: string | null;
|
|
|
|
/**
|
|
* Object type.
|
|
*
|
|
* For Text Completions, this is always `"completion"`.
|
|
*/
|
|
type: 'completion';
|
|
}
|
|
|
|
interface StreamError {
|
|
error: {
|
|
type: string;
|
|
message: string;
|
|
};
|
|
}
|
|
|
|
interface StreamPing {}
|
|
|
|
type StreamData = CompletionChunk | StreamError | StreamPing;
|
|
|
|
interface Message {
|
|
id: string;
|
|
content: Array<ContentBlock>;
|
|
model: string;
|
|
role: 'assistant';
|
|
stop_reason: 'end_turn' | 'max_tokens' | 'stop_sequence' | null;
|
|
stop_sequence: string | null;
|
|
type: 'message';
|
|
}
|
|
|
|
interface ContentBlock {
|
|
text: string;
|
|
type: 'text';
|
|
}
|
|
|
|
interface TextDelta {
|
|
text: string;
|
|
type: 'text_delta';
|
|
}
|
|
|
|
interface ContentBlockDeltaEvent {
|
|
delta: TextDelta;
|
|
index: number;
|
|
type: 'content_block_delta';
|
|
}
|
|
|
|
interface ContentBlockStartEvent {
|
|
content_block: ContentBlock;
|
|
index: number;
|
|
type: 'content_block_start';
|
|
}
|
|
|
|
interface ContentBlockStopEvent {
|
|
index: number;
|
|
type: 'content_block_stop';
|
|
}
|
|
|
|
interface MessageDeltaEventDelta {
|
|
stop_reason: 'end_turn' | 'max_tokens' | 'stop_sequence' | null;
|
|
stop_sequence: string | null;
|
|
}
|
|
|
|
interface MessageDeltaEvent {
|
|
delta: MessageDeltaEventDelta;
|
|
type: 'message_delta';
|
|
}
|
|
|
|
type MessageStreamEvent =
|
|
| MessageStartEvent
|
|
| MessageDeltaEvent
|
|
| MessageStopEvent
|
|
| ContentBlockStartEvent
|
|
| ContentBlockDeltaEvent
|
|
| ContentBlockStopEvent;
|
|
|
|
interface MessageStartEvent {
|
|
message: Message;
|
|
type: 'message_start';
|
|
}
|
|
|
|
interface MessageStopEvent {
|
|
type: 'message_stop';
|
|
}
|
|
|
|
function parseAnthropicStream(): (data: string) => string | void {
|
|
let previous = '';
|
|
|
|
return data => {
|
|
const json = JSON.parse(data as string) as StreamData;
|
|
|
|
// error event
|
|
if ('error' in json) {
|
|
throw new Error(`${json.error.type}: ${json.error.message}`);
|
|
}
|
|
|
|
// ping event
|
|
if (!('completion' in json)) {
|
|
return;
|
|
}
|
|
|
|
// On API versions older than 2023-06-01,
|
|
// Anthropic's `completion` field is cumulative unlike OpenAI's
|
|
// deltas. In order to compute the delta, we must slice out the text
|
|
// we previously received.
|
|
const text = json.completion;
|
|
if (
|
|
!previous ||
|
|
(text.length > previous.length && text.startsWith(previous))
|
|
) {
|
|
const delta = text.slice(previous.length);
|
|
previous = text;
|
|
|
|
return delta;
|
|
}
|
|
|
|
return text;
|
|
};
|
|
}
|
|
|
|
async function* streamable(
|
|
stream: AsyncIterable<CompletionChunk> | AsyncIterable<MessageStreamEvent>,
|
|
) {
|
|
for await (const chunk of stream) {
|
|
if ('completion' in chunk) {
|
|
// completion stream
|
|
const text = chunk.completion;
|
|
if (text) yield text;
|
|
} else if ('delta' in chunk) {
|
|
// messge stream
|
|
const { delta } = chunk;
|
|
if ('text' in delta) {
|
|
const text = delta.text;
|
|
if (text) yield text;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Accepts either a fetch Response from the Anthropic `POST /v1/complete` endpoint,
|
|
* or the return value of `await client.completions.create({ stream: true })`
|
|
* from the `@anthropic-ai/sdk` package.
|
|
*/
|
|
export function AnthropicStream(
|
|
res:
|
|
| Response
|
|
| AsyncIterable<CompletionChunk>
|
|
| AsyncIterable<MessageStreamEvent>,
|
|
cb?: AIStreamCallbacksAndOptions,
|
|
): ReadableStream {
|
|
if (Symbol.asyncIterator in res) {
|
|
return readableFromAsyncIterable(streamable(res))
|
|
.pipeThrough(createCallbacksTransformer(cb))
|
|
.pipeThrough(createStreamDataTransformer());
|
|
} else {
|
|
return AIStream(res, parseAnthropicStream(), cb).pipeThrough(
|
|
createStreamDataTransformer(),
|
|
);
|
|
}
|
|
}
|