diff --git a/.changeset/eleven-terms-clap.md b/.changeset/eleven-terms-clap.md new file mode 100644 index 0000000..a6d15b4 --- /dev/null +++ b/.changeset/eleven-terms-clap.md @@ -0,0 +1,5 @@ +--- +'token.js': minor +--- + +Upgrade the openai client library to 4.91.1 diff --git a/package.json b/package.json index 2904552..3c8235c 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "cohere-ai": "7.10.6", "mime-types": "^2.1.35", "nanoid": "^5.0.7", - "openai": "4.52.2" + "openai": "4.91.1" }, "devDependencies": { "@babel/eslint-parser": "^7.18.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a2415fb..0eebdff 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -30,8 +30,8 @@ dependencies: specifier: ^5.0.7 version: 5.0.8 openai: - specifier: 4.52.2 - version: 4.52.2 + specifier: 4.91.1 + version: 4.91.1 devDependencies: '@babel/eslint-parser': @@ -6609,9 +6609,17 @@ packages: mimic-fn: 2.1.0 dev: true - /openai@4.52.2: - resolution: {integrity: sha512-mMc0XgFuVSkcm0lRIi8zaw++otC82ZlfkCur1qguXYWPETr/+ZwL9A/vvp3YahX+shpaT6j03dwsmUyLAfmEfg==} + /openai@4.91.1: + resolution: {integrity: sha512-DbjrR0hIMQFbxz8+3qBsfPJnh3+I/skPgoSlT7f9eiZuhGBUissPQULNgx6gHNkLoZ3uS0uYS6eXPUdtg4nHzw==} hasBin: true + peerDependencies: + ws: ^8.18.0 + zod: ^3.23.8 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true dependencies: '@types/node': 18.19.64 '@types/node-fetch': 2.6.12 @@ -6620,7 +6628,6 @@ packages: form-data-encoder: 1.7.2 formdata-node: 4.4.1 node-fetch: 2.7.0 - web-streams-polyfill: 3.3.3 transitivePeerDependencies: - encoding dev: false diff --git a/src/handlers/ai21.ts b/src/handlers/ai21.ts index f4a1033..3b4910c 100644 --- a/src/handlers/ai21.ts +++ b/src/handlers/ai21.ts @@ -10,7 +10,7 @@ import { } from '../userTypes/index.js' import { BaseHandler } from './base.js' import { InputError } from './types.js' -import { getTimestamp } from './utils.js' +import { convertMessageContentToString, getTimestamp } from './utils.js' type AI21ChatCompletionParams = { model: string @@ -73,7 +73,7 @@ const convertMessages = ( if (i === 0 && message.role === 'system') { output.push({ role: 'system', - content: message.content, + content: convertMessageContentToString(message.content), }) } else if ( message.role === 'user' || @@ -268,6 +268,10 @@ export class AI21Handler extends BaseHandler { const convertedChoices = data.choices.map((choice) => { return { ...choice, + message: { + ...choice.message, + refusal: null, + }, logprobs: null, } }) diff --git a/src/handlers/anthropic.ts b/src/handlers/anthropic.ts index d356239..f7a9455 100644 --- a/src/handlers/anthropic.ts +++ b/src/handlers/anthropic.ts @@ -28,6 +28,7 @@ import { BaseHandler } from './base.js' import { InputError, InvariantError } from './types.js' import { consoleWarn, + convertMessageContentToString, fetchThenParseImage, getTimestamp, isEmptyObject, @@ -247,12 +248,14 @@ const toChatCompletionChoiceMessage = ( const messageContent = content.every(isToolUseBlock) ? null : '' return { role, + refusal: null, content: messageContent, tool_calls: toolCalls, } } else { return { role, + refusal: null, content: textBlocks.map((textBlock) => textBlock.text).join('\n'), tool_calls: toolCalls, } @@ -361,7 +364,7 @@ export const convertMessages = async ( // unchanged. let systemMessage: string | undefined if (clonedMessages.length > 0 && clonedMessages[0].role === 'system') { - systemMessage = clonedMessages[0].content + systemMessage = convertMessageContentToString(clonedMessages[0].content) clonedMessages.shift() } @@ -448,7 +451,7 @@ export const convertMessages = async ( return { type: 'text', text, - } + } as TextBlockParam } else { const parsedImage = await fetchThenParseImage(e.image_url.url) return { @@ -458,7 +461,7 @@ export const convertMessages = async ( media_type: parsedImage.mimeType, type: 'base64', }, - } + } as ImageBlockParam } }) ) diff --git a/src/handlers/bedrock.ts b/src/handlers/bedrock.ts index e267d78..ccc8b69 100644 --- a/src/handlers/bedrock.ts +++ b/src/handlers/bedrock.ts @@ -9,7 +9,10 @@ import { ConverseStreamCommandOutput, ImageFormat, SystemContentBlock, + Tool, ToolChoice, + ToolConfiguration, + ToolSpecification, } from '@aws-sdk/client-bedrock-runtime' import { ChatCompletionMessageToolCall } from 'openai/resources/index' @@ -29,6 +32,7 @@ import { BaseHandler } from './base.js' import { InputError, InvariantError, MIMEType } from './types.js' import { consoleWarn, + convertMessageContentToString, fetchThenParseImage, getTimestamp, normalizeTemperature, @@ -84,6 +88,7 @@ const toChatCompletionChoiceMessage = ( ): CompletionResponse['choices'][0]['message'] => { if (output?.message?.content === undefined) { return { + refusal: null, content: '', role: 'assistant', } @@ -146,6 +151,7 @@ const toChatCompletionChoiceMessage = ( : '' return { role, + refusal: null, content: messageContent, tool_calls: toolCalls, } @@ -153,6 +159,7 @@ const toChatCompletionChoiceMessage = ( const content = textBlocks.map((textBlock) => textBlock.text).join('\n') return { role, + refusal: null, content, tool_calls: toolCalls, } @@ -187,7 +194,10 @@ export const convertMessages = async ( const systemMessages: Array = [] if (supportsSystemMessages(model)) { while (clonedMessages.length > 0 && clonedMessages[0].role === 'system') { - systemMessages.push({ text: clonedMessages[0].content }) + const messageContent = convertMessageContentToString( + clonedMessages[0].content + ) + systemMessages.push({ text: messageContent }) clonedMessages.shift() } } @@ -247,7 +257,7 @@ export const convertMessages = async ( toolUseId: message.tool_call_id, content: [ { - text: message.content, + text: convertMessageContentToString(message.content), }, ], }, @@ -288,7 +298,7 @@ export const convertMessages = async ( const text = makeTextContent(message.role, e.text) return { text, - } + } as ContentBlock.TextMember } else { const parsedImage = await fetchThenParseImage(e.image_url.url) return { @@ -298,7 +308,7 @@ export const convertMessages = async ( bytes: Buffer.from(parsedImage.content, 'base64'), }, }, - } + } as ContentBlock.ImageMember } }) ) @@ -354,27 +364,24 @@ export const convertToolParams = ( return undefined } - const convertedTools = + const convertedTools: (Tool | undefined)[] = tools.length > 0 ? tools.map((tool) => { - const inputSchema = tool.function.parameters - ? { + const inputSchema: ToolSpecification['inputSchema'] | undefined = tool + .function.parameters + ? ({ // Bedrock and OpenAI's function parameter types are incompatible even though they both // adhere to the JSON schema, so we set the type to `any` to prevent a TypeScript error. json: tool.function.parameters as any, - // TypeScript throws a type error if we don't define this field: - $unknown: undefined, - } + } satisfies ToolSpecification['inputSchema']) : undefined return { - // TypeScript throws a type error if we don't define this field: - $unknown: undefined, toolSpec: { name: tool.function.name, description: tool.function.description, inputSchema, }, - } + } satisfies Tool }) : undefined @@ -387,7 +394,10 @@ export const convertToolParams = ( convertedToolChoice = { tool: { name: toolChoice.function.name } } } - return { toolChoice: convertedToolChoice, tools: convertedTools } + return { + toolChoice: convertedToolChoice, + tools: convertedTools, + } satisfies ToolConfiguration } const convertStopReason = ( diff --git a/src/handlers/cohere.ts b/src/handlers/cohere.ts index 5db1c17..dae0b66 100644 --- a/src/handlers/cohere.ts +++ b/src/handlers/cohere.ts @@ -28,7 +28,11 @@ import { } from '../userTypes/index.js' import { BaseHandler } from './base.js' import { InputError, InvariantError, MessageRole } from './types.js' -import { consoleWarn, getTimestamp } from './utils.js' +import { + consoleWarn, + convertMessageContentToString, + getTimestamp, +} from './utils.js' type CohereMessageRole = 'CHATBOT' | 'SYSTEM' | 'USER' | 'TOOL' @@ -41,6 +45,8 @@ const convertRole = (role: MessageRole): CohereMessageRole => { return 'TOOL' } else if (role === 'user') { return 'USER' + } else if (role === 'developer') { + return 'SYSTEM' } else { throw new InputError(`Unknown role: ${role}`) } @@ -259,12 +265,13 @@ const toToolResult = ( ) } + const tollCallContentStr = convertMessageContentToString(toolMessage.content) const toolResult: ToolResult = { call: { name: toolCall.function.name, parameters: JSON.parse(toolCall.function.arguments), }, - outputs: [JSON.parse(toolMessage.content)], + outputs: [JSON.parse(tollCallContentStr)], } return toolResult } @@ -318,9 +325,10 @@ export const convertMessages = ( }) } } else if (message.role === 'assistant') { + const messageContentStr = convertMessageContentToString(message.content) chatHistory.push({ role: convertRole(message.role), - message: message.content ?? '', + message: messageContentStr, toolCalls: message.tool_calls?.map((toolCall) => { return { name: toolCall.function.name, @@ -567,6 +575,7 @@ export class CohereHandler extends BaseHandler { logprobs: null, message: { role: 'assistant', + refusal: null, // openai requires this field, fill in if Cohere ever supports content: response.text, tool_calls: toolCalls, }, diff --git a/src/handlers/gemini.ts b/src/handlers/gemini.ts index 2c3b570..bceb967 100644 --- a/src/handlers/gemini.ts +++ b/src/handlers/gemini.ts @@ -10,6 +10,7 @@ import { GenerateContentResult, GenerateContentStreamResult, GoogleGenerativeAI, + InlineDataPart, Part, TextPart, Tool, @@ -31,7 +32,12 @@ import { } from '../userTypes/index.js' import { BaseHandler } from './base.js' import { InputError } from './types.js' -import { consoleWarn, fetchThenParseImage, getTimestamp } from './utils.js' +import { + consoleWarn, + convertMessageContentToString, + fetchThenParseImage, + getTimestamp, +} from './utils.js' // Google's `GenerateContentCandidate.content` field should be optional, but it's a required field // in Google's types. This field can be undefined if a content filter is triggered when the user @@ -81,29 +87,28 @@ export const convertContentsToParts = async ( }, ] } else { - return Promise.all( - contents.map(async (part) => { - if (part.type === 'text') { - return { - text: `${systemPrefix}${part.text}`, - } - } else if (part.type === 'image_url') { - const imageData = await fetchThenParseImage(part.image_url.url) - return { - inlineData: { - mimeType: imageData.mimeType, - data: imageData.content, - }, - } - } else { - throw new InputError( - `Invalid content part type: ${ - (part as any).type - }. Must be "text" or "image_url".` - ) - } - }) - ) + const allParts: Promise[] = contents.map(async (part) => { + if (part.type === 'text') { + return { + text: `${systemPrefix}${part.text}`, + } as TextPart + } else if (part.type === 'image_url') { + const imageData = await fetchThenParseImage(part.image_url.url) + return { + inlineData: { + mimeType: imageData.mimeType, + data: imageData.content, + }, + } satisfies InlineDataPart + } else { + throw new InputError( + `Invalid content part type: ${ + (part as any).type + }. Must be "text" or "image_url".` + ) + } + }) + return Promise.all(allParts) } } @@ -164,7 +169,9 @@ export const convertMessageToContent = async ( { functionResponse: { name: message.tool_call_id, - response: JSON.parse(message.content), + response: JSON.parse( + convertMessageContentToString(message.content) + ), }, }, ], @@ -304,6 +311,7 @@ export const convertResponseMessage = ( content: candidate.content?.parts.map((part) => part.text).join('') ?? null, role: 'assistant', tool_calls: convertToolCalls(candidate), + refusal: null, } } diff --git a/src/handlers/mistral.ts b/src/handlers/mistral.ts index 715f930..6f2969a 100644 --- a/src/handlers/mistral.ts +++ b/src/handlers/mistral.ts @@ -26,6 +26,7 @@ import { } from '../userTypes/index.js' import { BaseHandler } from './base.js' import { InputError } from './types.js' +import { convertMessageContentToString } from './utils.js' export const findLinkedToolCallName = ( messages: ChatCompletionMessage[], @@ -46,6 +47,7 @@ export const convertMessages = ( messages: (ChatCompletionMessageParam | ChatCompletionMessage)[] ): Array => { return messages.map((message) => { + const messageContent = convertMessageContentToString(message.content) if (message.role === 'tool') { const name = findLinkedToolCallName( messages as ChatCompletionMessage[], @@ -55,7 +57,7 @@ export const convertMessages = ( return { name, role: 'tool', - content: message.content, + content: messageContent, tool_call_id: message.tool_call_id, } } @@ -63,12 +65,12 @@ export const convertMessages = ( if (message.role === 'system') { return { role: message.role, - content: message.content ?? '', + content: messageContent, } } else if (message.role === 'assistant') { return { role: message.role, - content: message.content ?? '', + content: messageContent, tool_calls: message.tool_calls ?? null, } } else if (message.role === 'user') { @@ -239,6 +241,7 @@ const toCompletionResponse = ( index: choice.index, message: { role: 'assistant', + refusal: null, content: choice.message.content, tool_calls: convertToolCalls(choice.message.tool_calls), }, diff --git a/src/handlers/types.ts b/src/handlers/types.ts index ebcb286..598e505 100644 --- a/src/handlers/types.ts +++ b/src/handlers/types.ts @@ -1,4 +1,10 @@ -export type MessageRole = 'system' | 'user' | 'assistant' | 'tool' | 'function' +export type MessageRole = + | 'system' + | 'user' + | 'assistant' + | 'tool' + | 'function' + | 'developer' export type MIMEType = 'image/jpeg' | 'image/png' | 'image/gif' | 'image/webp' export class InputError extends Error { diff --git a/src/handlers/utils.ts b/src/handlers/utils.ts index 07c00a5..162afd1 100644 --- a/src/handlers/utils.ts +++ b/src/handlers/utils.ts @@ -1,5 +1,6 @@ import chalk from 'chalk' import { lookup } from 'mime-types' +import OpenAI from 'openai' import { LLMChatModel, LLMProvider } from '../chat/index.js' import { models } from '../models.js' @@ -270,3 +271,21 @@ export const isObject = (variable: any): boolean => { variable && typeof variable === 'object' && variable.constructor === Object ) } + +export const convertMessageContentToString = ( + messageContent: OpenAI.Chat.Completions.ChatCompletionMessageParam['content'] +): string => { + if (!messageContent) { + return '' + } + + return ( + (typeof messageContent === 'string' + ? messageContent + : messageContent + .map( + (m: OpenAI.Chat.Completions.ChatCompletionContentPartText) => m.text + ) + .join('\n')) ?? '' + ) +} diff --git a/test/automated/handlers/cohere.test.ts b/test/automated/handlers/cohere.test.ts index 6300f33..167a9e2 100644 --- a/test/automated/handlers/cohere.test.ts +++ b/test/automated/handlers/cohere.test.ts @@ -299,6 +299,13 @@ describe('convertMessages', () => { role: 'tool', content: '{"temperature":"22","unit":"fahrenheit"}', }, + { + tool_call_id: 'call_lhMKBlOSnwwq5BZDCGo5SVTJ', + role: 'tool', + content: [ + { text: '{"temperature":"100","unit":"fahrenheit"}', type: 'text' }, + ], + }, ] const { messages, lastUserMessage, toolResults } = @@ -379,6 +386,20 @@ describe('convertMessages', () => { }, ], }, + { + call: { + name: 'get_current_weather', + parameters: { + location: 'Paris, France', + }, + }, + outputs: [ + { + temperature: '100', + unit: 'fahrenheit', + }, + ], + }, ]) }) }) diff --git a/test/automated/handlers/gemini.test.ts b/test/automated/handlers/gemini.test.ts index fd658c4..34e5853 100644 --- a/test/automated/handlers/gemini.test.ts +++ b/test/automated/handlers/gemini.test.ts @@ -143,6 +143,7 @@ describe('convertAssistantMessage', () => { it('should convert a message with text content and no tool calls', () => { const message: OpenAI.Chat.Completions.ChatCompletionMessage = { role: 'assistant', + refusal: null, content: 'Hello, world!', tool_calls: undefined, } @@ -158,6 +159,7 @@ describe('convertAssistantMessage', () => { it('should convert a message with tool calls and no text content', () => { const message: OpenAI.Chat.Completions.ChatCompletionMessage = { role: 'assistant', + refusal: null, content: null, tool_calls: [ { @@ -181,6 +183,7 @@ describe('convertAssistantMessage', () => { it('should convert a message with both text content and tool calls', () => { const message: OpenAI.Chat.Completions.ChatCompletionMessage = { role: 'assistant', + refusal: null, content: 'Hello, world!', tool_calls: [ { @@ -205,6 +208,7 @@ describe('convertAssistantMessage', () => { it('should convert a message with null content and no tool calls', () => { const message: OpenAI.Chat.Completions.ChatCompletionMessage = { role: 'assistant', + refusal: null, content: null, tool_calls: undefined, } @@ -220,6 +224,7 @@ describe('convertAssistantMessage', () => { it('should handle messages with multiple tool calls', () => { const message: OpenAI.Chat.Completions.ChatCompletionMessage = { role: 'assistant', + refusal: null, content: null, tool_calls: [ { @@ -249,6 +254,7 @@ describe('convertAssistantMessage', () => { it('should throw an InputError for an unexpected message role', () => { const message: OpenAI.Chat.Completions.ChatCompletionMessage = { role: 'unexpectedRole' as any, + refusal: null, content: 'Hello, world!', tool_calls: undefined, } @@ -1012,6 +1018,7 @@ describe('convertResponseMessage', () => { expect(result).toEqual({ content: 'Hello world', role: 'assistant', + refusal: null, tool_calls: [ { id: 'mockId', @@ -1040,6 +1047,7 @@ describe('convertResponseMessage', () => { expect(result).toEqual({ content: 'Hello world', role: 'assistant', + refusal: null, tool_calls: undefined, }) }) @@ -1058,6 +1066,7 @@ describe('convertResponseMessage', () => { expect(result).toEqual({ content: '', role: 'assistant', + refusal: null, tool_calls: undefined, }) }) @@ -1079,6 +1088,7 @@ describe('convertResponseMessage', () => { expect(result).toEqual({ content: '', role: 'assistant', + refusal: null, tool_calls: [ { id: 'mockId', @@ -1421,6 +1431,7 @@ describe('GeminiHandler', () => { index: 0, message: { role: 'assistant', + refusal: null, content: mockBasicChatResponse.response.candidates![0].content.parts[0] .text, @@ -1464,6 +1475,7 @@ describe('GeminiHandler', () => { index: 0, message: { role: 'assistant', + refusal: null, content: '', tool_calls: [ { diff --git a/test/automated/handlers/mistral.test.ts b/test/automated/handlers/mistral.test.ts index 44dff16..62e9953 100644 --- a/test/automated/handlers/mistral.test.ts +++ b/test/automated/handlers/mistral.test.ts @@ -776,6 +776,7 @@ describe('MistralHandler', () => { index: 0, message: { role: 'assistant', + refusal: null, content: mockBasicChatResponse.choices[0].message.content, tool_calls: undefined, }, @@ -809,6 +810,7 @@ describe('MistralHandler', () => { index: 0, message: { role: 'assistant', + refusal: null, content: mockToolChatResponse.choices[0].message.content, tool_calls: [ { diff --git a/tsconfig.json b/tsconfig.json index 897c535..1b8fee4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,26 +1,25 @@ { "compilerOptions": { + "composite": true, + "declaration": true, + "declarationDir": "./dist", + "emitDeclarationOnly": true, + "emitDecoratorMetadata": true, + "esModuleInterop": true, + "experimentalDecorators": true, "module": "NodeNext", - "target": "ES2015", "moduleResolution": "NodeNext", - "sourceMap": true, - "esModuleInterop": true, - "composite": true, "noImplicitAny": false, - "removeComments": true, "noLib": false, - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "strictNullChecks": true, - "typeRoots": [ - "node_modules/@types" - ], - "rootDir": "./src", "outDir": "./dist", + "removeComments": true, + "rootDir": "./src", "skipLibCheck": true, - "declaration": true, - "declarationDir": "./dist", - "emitDeclarationOnly": true + "sourceMap": true, + "target": "ES2015", + "typeRoots": [ + "node_modules/@types" + ] }, "exclude": [ "node_modules",