diff --git a/examples/tools-streaming.ts b/examples/tools-streaming.ts index 11d9589f..96d9cbdc 100644 --- a/examples/tools-streaming.ts +++ b/examples/tools-streaming.ts @@ -7,7 +7,7 @@ import { inspect } from 'util'; const client = new Anthropic(); async function main() { - const stream = client.beta.tools.messages + const stream = client.messages .stream({ messages: [ { diff --git a/examples/tools.ts b/examples/tools.ts index b4887a24..b237043b 100644 --- a/examples/tools.ts +++ b/examples/tools.ts @@ -6,11 +6,11 @@ import assert from 'node:assert'; const client = new Anthropic(); // gets API Key from environment variable ANTHROPIC_API_KEY async function main() { - const userMessage: Anthropic.Beta.Tools.ToolsBetaMessageParam = { + const userMessage: Anthropic.MessageParam = { role: 'user', content: 'What is the weather in SF?', }; - const tools: Anthropic.Beta.Tools.Tool[] = [ + const tools: Anthropic.Tool[] = [ { name: 'get_weather', description: 'Get the weather for a specific location', @@ -21,7 +21,7 @@ async function main() { }, ]; - const message = await client.beta.tools.messages.create({ + const message = await client.messages.create({ model: 'claude-3-opus-20240229', max_tokens: 1024, messages: [userMessage], @@ -33,11 +33,11 @@ async function main() { assert(message.stop_reason === 'tool_use'); const tool = message.content.find( - (content): content is Anthropic.Beta.Tools.ToolUseBlock => content.type === 'tool_use', + (content): content is Anthropic.ToolUseBlock => content.type === 'tool_use', ); assert(tool); - const result = await client.beta.tools.messages.create({ + const result = await client.messages.create({ model: 'claude-3-opus-20240229', max_tokens: 1024, messages: [ diff --git a/src/lib/MessageStream.ts b/src/lib/MessageStream.ts index cf90bda6..5fb629fe 100644 --- a/src/lib/MessageStream.ts +++ b/src/lib/MessageStream.ts @@ -7,15 +7,18 @@ import { MessageStreamEvent, MessageParam, MessageCreateParams, - MessageStreamParams, + MessageCreateParamsBase, } from '@anthropic-ai/sdk/resources/messages'; import { type ReadableStream } from '@anthropic-ai/sdk/_shims/index'; import { Stream } from '@anthropic-ai/sdk/streaming'; +import { TextBlock } from '@anthropic-ai/sdk/resources'; +import { partialParse } from '../_vendor/partial-json-parser/parser'; export interface MessageStreamEvents { connect: () => void; streamEvent: (event: MessageStreamEvent, snapshot: Message) => void; text: (textDelta: string, textSnapshot: string) => void; + inputJson: (jsonDelta: string, jsonSnapshot: unknown) => void; message: (message: Message) => void; contentBlock: (content: ContentBlock) => void; finalMessage: (message: Message) => void; @@ -29,6 +32,8 @@ type MessageStreamEventListeners = { once?: boolean; }[]; +const JSON_BUF_PROPERTY = '__json_buf'; + export class MessageStream implements AsyncIterable { messages: MessageParam[] = []; receivedMessages: Message[] = []; @@ -85,7 +90,7 @@ export class MessageStream implements AsyncIterable { static createMessage( messages: Messages, - params: MessageStreamParams, + params: MessageCreateParamsBase, options?: Core.RequestOptions, ): MessageStream { const runner = new MessageStream(); @@ -264,7 +269,7 @@ export class MessageStream implements AsyncIterable { } const textBlocks = this.receivedMessages .at(-1)! - .content.filter((block) => block.type === 'text') + .content.filter((block): block is TextBlock => block.type === 'text') .map((block) => block.text); if (textBlocks.length === 0) { throw new AnthropicError('stream ended without producing a content block with type=text'); @@ -369,8 +374,13 @@ export class MessageStream implements AsyncIterable { switch (event.type) { case 'content_block_delta': { - if (event.delta.type === 'text_delta') { - this._emit('text', event.delta.text, messageSnapshot.content.at(-1)!.text || ''); + const content = messageSnapshot.content.at(-1)!; + if (event.delta.type === 'text_delta' && content.type === 'text') { + this._emit('text', event.delta.text, content.text || ''); + } else if (event.delta.type === 'input_json_delta' && content.type === 'tool_use') { + if (content.input) { + this._emit('inputJson', event.delta.partial_json, content.input); + } } break; } @@ -459,6 +469,22 @@ export class MessageStream implements AsyncIterable { const snapshotContent = snapshot.content.at(event.index); if (snapshotContent?.type === 'text' && event.delta.type === 'text_delta') { snapshotContent.text += event.delta.text; + } else if (snapshotContent?.type === 'tool_use' && event.delta.type === 'input_json_delta') { + // we need to keep track of the raw JSON string as well so that we can + // re-parse it for each delta, for now we just store it as an untyped + // non-enumerable property on the snapshot + let jsonBuf = (snapshotContent as any)[JSON_BUF_PROPERTY] || ''; + jsonBuf += event.delta.partial_json; + + Object.defineProperty(snapshotContent, JSON_BUF_PROPERTY, { + value: jsonBuf, + enumerable: false, + writable: true, + }); + + if (jsonBuf) { + snapshotContent.input = partialParse(jsonBuf); + } } return snapshot; } diff --git a/src/lib/ToolsBetaMessageStream.ts b/src/lib/ToolsBetaMessageStream.ts deleted file mode 100644 index 7c1612e9..00000000 --- a/src/lib/ToolsBetaMessageStream.ts +++ /dev/null @@ -1,561 +0,0 @@ -import * as Core from '@anthropic-ai/sdk/core'; -import { AnthropicError, APIUserAbortError } from '@anthropic-ai/sdk/error'; -import { - ToolsBetaContentBlock, - Messages, - ToolsBetaMessage, - ToolsBetaMessageStreamEvent, - ToolsBetaMessageParam, - MessageCreateParams, - MessageCreateParamsBase, -} from '@anthropic-ai/sdk/resources/beta/tools/messages'; -import { type ReadableStream } from '@anthropic-ai/sdk/_shims/index'; -import { Stream } from '@anthropic-ai/sdk/streaming'; -import { TextBlock } from '@anthropic-ai/sdk/resources'; -import { partialParse } from '../_vendor/partial-json-parser/parser'; - -export interface MessageStreamEvents { - connect: () => void; - streamEvent: (event: ToolsBetaMessageStreamEvent, snapshot: ToolsBetaMessage) => void; - text: (textDelta: string, textSnapshot: string) => void; - inputJson: (jsonDelta: string, jsonSnapshot: unknown) => void; - message: (message: ToolsBetaMessage) => void; - contentBlock: (content: ToolsBetaContentBlock) => void; - finalMessage: (message: ToolsBetaMessage) => void; - error: (error: AnthropicError) => void; - abort: (error: APIUserAbortError) => void; - end: () => void; -} - -type MessageStreamEventListeners = { - listener: MessageStreamEvents[Event]; - once?: boolean; -}[]; - -const JSON_BUF_PROPERTY = '__json_buf'; - -export class ToolsBetaMessageStream implements AsyncIterable { - messages: ToolsBetaMessageParam[] = []; - receivedMessages: ToolsBetaMessage[] = []; - #currentMessageSnapshot: ToolsBetaMessage | undefined; - - controller: AbortController = new AbortController(); - - #connectedPromise: Promise; - #resolveConnectedPromise: () => void = () => {}; - #rejectConnectedPromise: (error: AnthropicError) => void = () => {}; - - #endPromise: Promise; - #resolveEndPromise: () => void = () => {}; - #rejectEndPromise: (error: AnthropicError) => void = () => {}; - - #listeners: { [Event in keyof MessageStreamEvents]?: MessageStreamEventListeners } = {}; - - #ended = false; - #errored = false; - #aborted = false; - #catchingPromiseCreated = false; - - constructor() { - this.#connectedPromise = new Promise((resolve, reject) => { - this.#resolveConnectedPromise = resolve; - this.#rejectConnectedPromise = reject; - }); - - this.#endPromise = new Promise((resolve, reject) => { - this.#resolveEndPromise = resolve; - this.#rejectEndPromise = reject; - }); - - // Don't let these promises cause unhandled rejection errors. - // we will manually cause an unhandled rejection error later - // if the user hasn't registered any error listener or called - // any promise-returning method. - this.#connectedPromise.catch(() => {}); - this.#endPromise.catch(() => {}); - } - - /** - * Intended for use on the frontend, consuming a stream produced with - * `.toReadableStream()` on the backend. - * - * Note that messages sent to the model do not appear in `.on('message')` - * in this context. - */ - static fromReadableStream(stream: ReadableStream): ToolsBetaMessageStream { - const runner = new ToolsBetaMessageStream(); - runner._run(() => runner._fromReadableStream(stream)); - return runner; - } - - static createMessage( - messages: Messages, - params: MessageCreateParamsBase, - options?: Core.RequestOptions, - ): ToolsBetaMessageStream { - const runner = new ToolsBetaMessageStream(); - for (const message of params.messages) { - runner._addMessageParam(message); - } - runner._run(() => - runner._createMessage( - messages, - { ...params, stream: true }, - { ...options, headers: { ...options?.headers, 'X-Stainless-Helper-Method': 'stream' } }, - ), - ); - return runner; - } - - protected _run(executor: () => Promise) { - executor().then(() => { - this._emitFinal(); - this._emit('end'); - }, this.#handleError); - } - - protected _addMessageParam(message: ToolsBetaMessageParam) { - this.messages.push(message); - } - - protected _addMessage(message: ToolsBetaMessage, emit = true) { - this.receivedMessages.push(message); - if (emit) { - this._emit('message', message); - } - } - - protected async _createMessage( - messages: Messages, - params: MessageCreateParams, - options?: Core.RequestOptions, - ): Promise { - const signal = options?.signal; - if (signal) { - if (signal.aborted) this.controller.abort(); - signal.addEventListener('abort', () => this.controller.abort()); - } - this.#beginRequest(); - const stream = await messages.create( - { ...params, stream: true }, - { ...options, signal: this.controller.signal }, - ); - this._connected(); - for await (const event of stream) { - this.#addStreamEvent(event); - } - if (stream.controller.signal?.aborted) { - throw new APIUserAbortError(); - } - this.#endRequest(); - } - - protected _connected() { - if (this.ended) return; - this.#resolveConnectedPromise(); - this._emit('connect'); - } - - get ended(): boolean { - return this.#ended; - } - - get errored(): boolean { - return this.#errored; - } - - get aborted(): boolean { - return this.#aborted; - } - - abort() { - this.controller.abort(); - } - - /** - * Adds the listener function to the end of the listeners array for the event. - * No checks are made to see if the listener has already been added. Multiple calls passing - * the same combination of event and listener will result in the listener being added, and - * called, multiple times. - * @returns this MessageStream, so that calls can be chained - */ - on(event: Event, listener: MessageStreamEvents[Event]): this { - const listeners: MessageStreamEventListeners = - this.#listeners[event] || (this.#listeners[event] = []); - listeners.push({ listener }); - return this; - } - - /** - * Removes the specified listener from the listener array for the event. - * off() will remove, at most, one instance of a listener from the listener array. If any single - * listener has been added multiple times to the listener array for the specified event, then - * off() must be called multiple times to remove each instance. - * @returns this MessageStream, so that calls can be chained - */ - off(event: Event, listener: MessageStreamEvents[Event]): this { - const listeners = this.#listeners[event]; - if (!listeners) return this; - const index = listeners.findIndex((l) => l.listener === listener); - if (index >= 0) listeners.splice(index, 1); - return this; - } - - /** - * Adds a one-time listener function for the event. The next time the event is triggered, - * this listener is removed and then invoked. - * @returns this MessageStream, so that calls can be chained - */ - once(event: Event, listener: MessageStreamEvents[Event]): this { - const listeners: MessageStreamEventListeners = - this.#listeners[event] || (this.#listeners[event] = []); - listeners.push({ listener, once: true }); - return this; - } - - /** - * This is similar to `.once()`, but returns a Promise that resolves the next time - * the event is triggered, instead of calling a listener callback. - * @returns a Promise that resolves the next time given event is triggered, - * or rejects if an error is emitted. (If you request the 'error' event, - * returns a promise that resolves with the error). - * - * Example: - * - * const message = await stream.emitted('message') // rejects if the stream errors - */ - emitted( - event: Event, - ): Promise< - Parameters extends [infer Param] ? Param - : Parameters extends [] ? void - : Parameters - > { - return new Promise((resolve, reject) => { - this.#catchingPromiseCreated = true; - if (event !== 'error') this.once('error', reject); - this.once(event, resolve as any); - }); - } - - async done(): Promise { - this.#catchingPromiseCreated = true; - await this.#endPromise; - } - - get currentMessage(): ToolsBetaMessage | undefined { - return this.#currentMessageSnapshot; - } - - #getFinalMessage(): ToolsBetaMessage { - if (this.receivedMessages.length === 0) { - throw new AnthropicError('stream ended without producing a Message with role=assistant'); - } - return this.receivedMessages.at(-1)!; - } - - /** - * @returns a promise that resolves with the the final assistant Message response, - * or rejects if an error occurred or the stream ended prematurely without producing a Message. - */ - async finalMessage(): Promise { - await this.done(); - return this.#getFinalMessage(); - } - - #getFinalText(): string { - if (this.receivedMessages.length === 0) { - throw new AnthropicError('stream ended without producing a Message with role=assistant'); - } - const textBlocks = this.receivedMessages - .at(-1)! - .content.filter((block): block is TextBlock => block.type === 'text') - .map((block) => block.text); - if (textBlocks.length === 0) { - throw new AnthropicError('stream ended without producing a content block with type=text'); - } - return textBlocks.join(' '); - } - - /** - * @returns a promise that resolves with the the final assistant Message's text response, concatenated - * together if there are more than one text blocks. - * Rejects if an error occurred or the stream ended prematurely without producing a Message. - */ - async finalText(): Promise { - await this.done(); - return this.#getFinalText(); - } - - #handleError = (error: unknown) => { - this.#errored = true; - if (error instanceof Error && error.name === 'AbortError') { - error = new APIUserAbortError(); - } - if (error instanceof APIUserAbortError) { - this.#aborted = true; - return this._emit('abort', error); - } - if (error instanceof AnthropicError) { - return this._emit('error', error); - } - if (error instanceof Error) { - const anthropicError: AnthropicError = new AnthropicError(error.message); - // @ts-ignore - anthropicError.cause = error; - return this._emit('error', anthropicError); - } - return this._emit('error', new AnthropicError(String(error))); - }; - - protected _emit( - event: Event, - ...args: Parameters - ) { - // make sure we don't emit any MessageStreamEvents after end - if (this.#ended) return; - - if (event === 'end') { - this.#ended = true; - this.#resolveEndPromise(); - } - - const listeners: MessageStreamEventListeners | undefined = this.#listeners[event]; - if (listeners) { - this.#listeners[event] = listeners.filter((l) => !l.once) as any; - listeners.forEach(({ listener }: any) => listener(...args)); - } - - if (event === 'abort') { - const error = args[0] as APIUserAbortError; - if (!this.#catchingPromiseCreated && !listeners?.length) { - Promise.reject(error); - } - this.#rejectConnectedPromise(error); - this.#rejectEndPromise(error); - this._emit('end'); - return; - } - - if (event === 'error') { - // NOTE: _emit('error', error) should only be called from #handleError(). - - const error = args[0] as AnthropicError; - if (!this.#catchingPromiseCreated && !listeners?.length) { - // Trigger an unhandled rejection if the user hasn't registered any error handlers. - // If you are seeing stack traces here, make sure to handle errors via either: - // - runner.on('error', () => ...) - // - await runner.done() - // - await runner.final...() - // - etc. - Promise.reject(error); - } - this.#rejectConnectedPromise(error); - this.#rejectEndPromise(error); - this._emit('end'); - } - } - - protected _emitFinal() { - const finalMessage = this.receivedMessages.at(-1); - if (finalMessage) { - this._emit('finalMessage', this.#getFinalMessage()); - } - } - - #beginRequest() { - if (this.ended) return; - this.#currentMessageSnapshot = undefined; - } - #addStreamEvent(event: ToolsBetaMessageStreamEvent) { - if (this.ended) return; - const messageSnapshot = this.#accumulateMessage(event); - this._emit('streamEvent', event, messageSnapshot); - - switch (event.type) { - case 'content_block_delta': { - const content = messageSnapshot.content.at(-1)!; - if (event.delta.type === 'text_delta' && content.type === 'text') { - this._emit('text', event.delta.text, content.text || ''); - } else if (event.delta.type === 'input_json_delta' && content.type === 'tool_use') { - if (content.input) { - this._emit('inputJson', event.delta.partial_json, content.input); - } - } - break; - } - case 'message_stop': { - this._addMessageParam(messageSnapshot); - this._addMessage(messageSnapshot, true); - break; - } - case 'content_block_stop': { - this._emit('contentBlock', messageSnapshot.content.at(-1)!); - break; - } - case 'message_start': { - this.#currentMessageSnapshot = messageSnapshot; - break; - } - case 'content_block_start': - case 'message_delta': - break; - } - } - #endRequest(): ToolsBetaMessage { - if (this.ended) { - throw new AnthropicError(`stream has ended, this shouldn't happen`); - } - const snapshot = this.#currentMessageSnapshot; - if (!snapshot) { - throw new AnthropicError(`request ended without sending any chunks`); - } - this.#currentMessageSnapshot = undefined; - return snapshot; - } - - protected async _fromReadableStream( - readableStream: ReadableStream, - options?: Core.RequestOptions, - ): Promise { - const signal = options?.signal; - if (signal) { - if (signal.aborted) this.controller.abort(); - signal.addEventListener('abort', () => this.controller.abort()); - } - this.#beginRequest(); - this._connected(); - const stream = Stream.fromReadableStream(readableStream, this.controller); - for await (const event of stream) { - this.#addStreamEvent(event); - } - if (stream.controller.signal?.aborted) { - throw new APIUserAbortError(); - } - this.#endRequest(); - } - - /** - * Mutates this.#currentMessage with the current event. Handling the accumulation of multiple messages - * will be needed to be handled by the caller, this method will throw if you try to accumulate for multiple - * messages. - */ - #accumulateMessage(event: ToolsBetaMessageStreamEvent): ToolsBetaMessage { - let snapshot = this.#currentMessageSnapshot; - - if (event.type === 'message_start') { - if (snapshot) { - throw new AnthropicError(`Unexpected event order, got ${event.type} before receiving "message_stop"`); - } - return event.message; - } - - if (!snapshot) { - throw new AnthropicError(`Unexpected event order, got ${event.type} before "message_start"`); - } - - switch (event.type) { - case 'message_stop': - return snapshot; - case 'message_delta': - snapshot.stop_reason = event.delta.stop_reason; - snapshot.stop_sequence = event.delta.stop_sequence; - snapshot.usage.output_tokens = event.usage.output_tokens; - return snapshot; - case 'content_block_start': - snapshot.content.push(event.content_block); - return snapshot; - case 'content_block_delta': { - const snapshotContent = snapshot.content.at(event.index); - if (snapshotContent?.type === 'text' && event.delta.type === 'text_delta') { - snapshotContent.text += event.delta.text; - } else if (snapshotContent?.type === 'tool_use' && event.delta.type === 'input_json_delta') { - // we need to keep track of the raw JSON string as well so that we can - // re-parse it for each delta, for now we just store it as an untyped - // non-enumerable property on the snapshot - let jsonBuf = (snapshotContent as any)[JSON_BUF_PROPERTY] || ''; - jsonBuf += event.delta.partial_json; - - Object.defineProperty(snapshotContent, JSON_BUF_PROPERTY, { - value: jsonBuf, - enumerable: false, - writable: true, - }); - - if (jsonBuf) { - snapshotContent.input = partialParse(jsonBuf); - } - } - return snapshot; - } - case 'content_block_stop': - return snapshot; - } - } - - [Symbol.asyncIterator](): AsyncIterator { - const pushQueue: ToolsBetaMessageStreamEvent[] = []; - const readQueue: { - resolve: (chunk: ToolsBetaMessageStreamEvent | undefined) => void; - reject: (error: unknown) => void; - }[] = []; - let done = false; - - this.on('streamEvent', (event) => { - const reader = readQueue.shift(); - if (reader) { - reader.resolve(event); - } else { - pushQueue.push(event); - } - }); - - this.on('end', () => { - done = true; - for (const reader of readQueue) { - reader.resolve(undefined); - } - readQueue.length = 0; - }); - - this.on('abort', (err) => { - done = true; - for (const reader of readQueue) { - reader.reject(err); - } - readQueue.length = 0; - }); - - this.on('error', (err) => { - done = true; - for (const reader of readQueue) { - reader.reject(err); - } - readQueue.length = 0; - }); - - return { - next: async (): Promise> => { - if (!pushQueue.length) { - if (done) { - return { value: undefined, done: true }; - } - return new Promise((resolve, reject) => - readQueue.push({ resolve, reject }), - ).then((chunk) => (chunk ? { value: chunk, done: false } : { value: undefined, done: true })); - } - const chunk = pushQueue.shift()!; - return { value: chunk, done: false }; - }, - return: async () => { - this.abort(); - return { value: undefined, done: true }; - }, - }; - } - - toReadableStream(): ReadableStream { - const stream = new Stream(this[Symbol.asyncIterator].bind(this), this.controller); - return stream.toReadableStream(); - } -} diff --git a/src/resources/messages.ts b/src/resources/messages.ts index d9604596..b4fabb66 100644 --- a/src/resources/messages.ts +++ b/src/resources/messages.ts @@ -704,201 +704,7 @@ export interface MessageCreateParamsStreaming extends MessageCreateParamsBase { stream: true; } -export interface MessageStreamParams { - /** - * The maximum number of tokens to generate before stopping. - * - * Note that our models may stop _before_ reaching this maximum. This parameter - * only specifies the absolute maximum number of tokens to generate. - * - * Different models have different maximum values for this parameter. See - * [models](https://docs.anthropic.com/en/docs/models-overview) for details. - */ - max_tokens: number; - - /** - * Input messages. - * - * Our models are trained to operate on alternating `user` and `assistant` - * conversational turns. When creating a new `Message`, you specify the prior - * conversational turns with the `messages` parameter, and the model then generates - * the next `Message` in the conversation. - * - * Each input message must be an object with a `role` and `content`. You can - * specify a single `user`-role message, or you can include multiple `user` and - * `assistant` messages. The first message must always use the `user` role. - * - * If the final message uses the `assistant` role, the response content will - * continue immediately from the content in that message. This can be used to - * constrain part of the model's response. - * - * Example with a single `user` message: - * - * ```json - * [{ "role": "user", "content": "Hello, Claude" }] - * ``` - * - * Example with multiple conversational turns: - * - * ```json - * [ - * { "role": "user", "content": "Hello there." }, - * { "role": "assistant", "content": "Hi, I'm Claude. How can I help you?" }, - * { "role": "user", "content": "Can you explain LLMs in plain English?" } - * ] - * ``` - * - * Example with a partially-filled response from Claude: - * - * ```json - * [ - * { - * "role": "user", - * "content": "What's the Greek name for Sun? (A) Sol (B) Helios (C) Sun" - * }, - * { "role": "assistant", "content": "The best answer is (" } - * ] - * ``` - * - * Each input message `content` may be either a single `string` or an array of - * content blocks, where each block has a specific `type`. Using a `string` for - * `content` is shorthand for an array of one content block of type `"text"`. The - * following input messages are equivalent: - * - * ```json - * { "role": "user", "content": "Hello, Claude" } - * ``` - * - * ```json - * { "role": "user", "content": [{ "type": "text", "text": "Hello, Claude" }] } - * ``` - * - * Starting with Claude 3 models, you can also send image content blocks: - * - * ```json - * { - * "role": "user", - * "content": [ - * { - * "type": "image", - * "source": { - * "type": "base64", - * "media_type": "image/jpeg", - * "data": "/9j/4AAQSkZJRg..." - * } - * }, - * { "type": "text", "text": "What is in this image?" } - * ] - * } - * ``` - * - * We currently support the `base64` source type for images, and the `image/jpeg`, - * `image/png`, `image/gif`, and `image/webp` media types. - * - * See [examples](https://docs.anthropic.com/en/api/messages-examples) for more - * input examples. - * - * Note that if you want to include a - * [system prompt](https://docs.anthropic.com/en/docs/system-prompts), you can use - * the top-level `system` parameter — there is no `"system"` role for input - * messages in the Messages API. - */ - messages: Array; - - /** - * The model that will complete your prompt. - * - * See [models](https://docs.anthropic.com/en/docs/models-overview) for additional - * details and options. - */ - model: - | (string & {}) - | 'claude-3-opus-20240229' - | 'claude-3-sonnet-20240229' - | 'claude-3-haiku-20240307' - | 'claude-2.1' - | 'claude-2.0' - | 'claude-instant-1.2'; - - /** - * An object describing metadata about the request. - */ - metadata?: MessageStreamParams.Metadata; - - /** - * Custom text sequences that will cause the model to stop generating. - * - * Our models will normally stop when they have naturally completed their turn, - * which will result in a response `stop_reason` of `"end_turn"`. - * - * If you want the model to stop generating when it encounters custom strings of - * text, you can use the `stop_sequences` parameter. If the model encounters one of - * the custom sequences, the response `stop_reason` value will be `"stop_sequence"` - * and the response `stop_sequence` value will contain the matched stop sequence. - */ - stop_sequences?: Array; - - /** - * System prompt. - * - * A system prompt is a way of providing context and instructions to Claude, such - * as specifying a particular goal or role. See our - * [guide to system prompts](https://docs.anthropic.com/en/docs/system-prompts). - */ - system?: string; - - /** - * Amount of randomness injected into the response. - * - * Defaults to `1.0`. Ranges from `0.0` to `1.0`. Use `temperature` closer to `0.0` - * for analytical / multiple choice, and closer to `1.0` for creative and - * generative tasks. - * - * Note that even with `temperature` of `0.0`, the results will not be fully - * deterministic. - */ - temperature?: number; - - /** - * Only sample from the top K options for each subsequent token. - * - * Used to remove "long tail" low probability responses. - * [Learn more technical details here](https://towardsdatascience.com/how-to-sample-from-language-models-682bceb97277). - * - * Recommended for advanced use cases only. You usually only need to use - * `temperature`. - */ - top_k?: number; - - /** - * Use nucleus sampling. - * - * In nucleus sampling, we compute the cumulative distribution over all the options - * for each subsequent token in decreasing probability order and cut it off once it - * reaches a particular probability specified by `top_p`. You should either alter - * `temperature` or `top_p`, but not both. - * - * Recommended for advanced use cases only. You usually only need to use - * `temperature`. - */ - top_p?: number; -} - -export namespace MessageStreamParams { - /** - * An object describing metadata about the request. - */ - export interface Metadata { - /** - * An external identifier for the user who is associated with the request. - * - * This should be a uuid, hash value, or other opaque identifier. Anthropic may use - * this id to help detect abuse. Do not include any identifying information such as - * name, email address, or phone number. - */ - user_id?: string | null; - } -} +export type MessageStreamParams = MessageCreateParamsBase; export namespace Messages { export import ContentBlock = MessagesAPI.ContentBlock; diff --git a/tests/api-resources/MessageStream.test.ts b/tests/api-resources/MessageStream.test.ts index 35cf5c1d..81b9c81e 100644 --- a/tests/api-resources/MessageStream.test.ts +++ b/tests/api-resources/MessageStream.test.ts @@ -6,6 +6,10 @@ import { type RequestInfo, type RequestInit } from '@anthropic-ai/sdk/_shims/ind type Fetch = (req: string | RequestInfo, init?: RequestInit) => Promise; +function assertNever(x: never): never { + throw new Error(`unreachable: ${x}`); +} + async function* messageIterable(message: Message): AsyncGenerator { yield { type: 'message_start', @@ -17,16 +21,39 @@ async function* messageIterable(message: Message): AsyncGenerator { ); for await (const event of stream) { - if (event.type === 'content_block_delta' && event.delta.text.includes('He')) { + if ( + event.type === 'content_block_delta' && + event.delta.type == 'text_delta' && + event.delta.text.includes('He') + ) { break; } } @@ -386,7 +417,11 @@ describe('MessageStream class', () => { async function runStream() { for await (const event of stream) { - if (event.type === 'content_block_delta' && event.delta.text.includes('He')) { + if ( + event.type === 'content_block_delta' && + event.delta.type === 'text_delta' && + event.delta.text.includes('He') + ) { break; } }