diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile deleted file mode 100644 index 8ea34be96..000000000 --- a/.devcontainer/Dockerfile +++ /dev/null @@ -1,23 +0,0 @@ -# syntax=docker/dockerfile:1 -FROM debian:bookworm-slim AS stainless - -RUN apt-get update && apt-get install -y \ - nodejs \ - npm \ - yarnpkg \ - && apt-get clean autoclean - -# Ensure UTF-8 encoding -ENV LANG=C.UTF-8 -ENV LC_ALL=C.UTF-8 - -# Yarn -RUN ln -sf /usr/bin/yarnpkg /usr/bin/yarn - -WORKDIR /workspace - -COPY package.json yarn.lock /workspace/ - -RUN yarn install - -COPY . /workspace diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index d55fc4d67..763462fad 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,20 +1,17 @@ // For format details, see https://aka.ms/devcontainer.json. For config options, see the // README at: https://github.com/devcontainers/templates/tree/main/src/debian { - "name": "Debian", - "build": { - "dockerfile": "Dockerfile" + "name": "Development", + "image": "mcr.microsoft.com/devcontainers/typescript-node:latest", + "features": { + "ghcr.io/devcontainers/features/node:1": {} + }, + "postCreateCommand": "yarn install", + "customizations": { + "vscode": { + "extensions": [ + "esbenp.prettier-vscode" + ] + } } - - // Features to add to the dev container. More info: https://containers.dev/features. - // "features": {}, - - // Use 'forwardPorts' to make a list of ports inside the container available locally. - // "forwardPorts": [], - - // Configure tool-specific properties. - // "customizations": {}, - - // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. - // "remoteUser": "root" } diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 64ffc67b1..000000000 --- a/.eslintrc.js +++ /dev/null @@ -1,29 +0,0 @@ -module.exports = { - parser: '@typescript-eslint/parser', - plugins: ['@typescript-eslint', 'unused-imports', 'prettier'], - rules: { - 'no-unused-vars': 'off', - 'prettier/prettier': 'error', - 'unused-imports/no-unused-imports': 'error', - 'no-restricted-imports': [ - 'error', - { - patterns: [ - { - group: ['openai', 'openai/*'], - message: 'Use a relative import, not a package import.', - }, - ], - }, - ], - }, - overrides: [ - { - files: ['tests/**', 'examples/**', 'ecosystem-tests/**'], - rules: { - 'no-restricted-imports': 'off', - }, - }, - ], - root: true, -}; diff --git a/.release-please-manifest.json b/.release-please-manifest.json index b33fcdcea..721222a67 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "5.0.0-alpha.0" + ".": "5.0.0-beta.0" } diff --git a/.stats.yml b/.stats.yml index 248cc366d..163146e38 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ -configured_endpoints: 68 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/openai-a39aca84ed97ebafb707ebd5221e2787c5a42ff3d98f2ffaea8a0dcd84cbcbcb.yml +configured_endpoints: 74 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/openai-5d30684c3118d049682ea30cdb4dbef39b97d51667da484689193dc40162af32.yml diff --git a/LICENSE b/LICENSE index 621a6becf..f011417af 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2024 OpenAI + Copyright 2025 OpenAI Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/MIGRATION.md b/MIGRATION.md new file mode 100644 index 000000000..c9c07887c --- /dev/null +++ b/MIGRATION.md @@ -0,0 +1,380 @@ +# Migration guide + +This guide outlines the changes and steps needed to migrate your codebase to the latest version of the OpenAI TypeScript and JavaScript SDK. + +The main changes are that the SDK now relies on the [builtin Web fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) instead of `node-fetch` and has zero dependencies. + +## Environment requirements + +The minimum supported runtime and tooling versions are now: + +- Node.js 18.x last LTS (Required for built-in fetch support) + - This was previously documented as the minimum supported Node.js version but Node.js 16.x mostly worked at runtime; now it will not. +- TypeScript 4.9 +- Jest 28 + +## Minimum types requirements + +### DOM + +`tsconfig.json` + +```jsonc +{ + "target": "ES2015", // note: we recommend ES2020 or higher + "lib": ["DOM", "DOM.Iterable", "ES2018"] +} +``` + +### Node.js + +`tsconfig.json` + +```jsonc +{ + "target": "ES2015" // note: we recommend ES2020 or higher +} +``` + +`package.json` + +```json +{ + "devDependencies": { + "@types/node": ">= 18.18.7" + } +} +``` + +### Cloudflare Workers + +`tsconfig.json` + +```jsonc +{ + "target": "ES2015", // note: we recommend ES2020 or higher + "lib": ["ES2020"], // <- needed by @cloudflare/workers-types + "types": ["@cloudflare/workers-types"] +} +``` + +`package.json` + +```json +{ + "devDependencies": { + "@cloudflare/workers-types": ">= 0.20221111.0" + } +} +``` + +### Bun + +`tsconfig.json` + +```jsonc +{ + "target": "ES2015" // note: we recommend ES2020 or higher +} +``` + +`package.json` + +```json +{ + "devDependencies": { + "@types/bun": ">= 1.2.0" + } +} +``` + +### Deno + +No config needed! + +## Breaking changes + +### Named path parameters + +Methods that take multiple path parameters typically now use named instead of positional arguments for better clarity and to prevent a footgun where it was easy to accidentally pass arguments in the incorrect order. + +For example, for a method that would call an endpoint at `/v1/parents/{parent_id}/children/{child_id}`, only the _last_ path parameter is positional and the rest must be passed as named arguments. + +```ts +// Before +client.parents.children.create('p_123', 'c_456'); + +// After +client.example.create('c_456', { parent_id: 'p_123' }); +``` + +This affects the following methods: + +- `client.beta.vectorStores.files.retrieve()` +- `client.beta.vectorStores.files.delete()` +- `client.beta.vectorStores.fileBatches.retrieve()` +- `client.beta.vectorStores.fileBatches.cancel()` +- `client.beta.vectorStores.fileBatches.listFiles()` +- `client.beta.threads.runs.retrieve()` +- `client.beta.threads.runs.update()` +- `client.beta.threads.runs.cancel()` +- `client.beta.threads.runs.submitToolOutputs()` +- `client.beta.threads.runs.steps.retrieve()` +- `client.beta.threads.runs.steps.list()` +- `client.beta.threads.messages.retrieve()` +- `client.beta.threads.messages.update()` +- `client.beta.threads.messages.delete()` + +### URI encoded path parameters + +Path params are now properly encoded by default. If you were manually encoding path parameters before giving them to the SDK, you must now stop doing that and pass the +param without any encoding applied. + +For example: + +```diff +- client.example.retrieve(encodeURIComponent('string/with/slash')) ++ client.example.retrieve('string/with/slash') // renders example/string%2Fwith%2Fslash +``` + +Previously without the `encodeURIComponent()` call we would have used the path `/example/string/with/slash`; now we'll use `/example/string%2Fwith%2Fslash`. + +### Removed `httpAgent` in favor of `fetchOptions` + +The `httpAgent` client option has been removed in favor of a [platform-specific `fetchOptions` property](https://github.com/stainless-sdks/openai-typescript#fetch-options). +This change was made as `httpAgent` relied on `node:http` agents which are not supported by any runtime's builtin fetch implementation. + +If you were using `httpAgent` for proxy support, check out the [new proxy documentation](https://github.com/stainless-sdks/openai-typescript#configuring-proxies). + +Before: + +```ts +import OpenAI from 'openai'; +import http from 'http'; +import { HttpsProxyAgent } from 'https-proxy-agent'; + +// Configure the default for all requests: +const client = new OpenAI({ + httpAgent: new HttpsProxyAgent(process.env.PROXY_URL), +}); +``` + +After: + +```ts +import OpenAI from 'openai'; +import * as undici from 'undici'; + +const proxyAgent = new undici.ProxyAgent(process.env.PROXY_URL); +const client = new OpenAI({ + fetchOptions: { + dispatcher: proxyAgent, + }, +}); +``` + +### HTTP method naming + +Some methods could not be named intuitively due to an internal naming conflict. This has been resolved and the methods are now correctly named. + +```ts +// Before +client.chat.completions.del(); +client.files.del(); +client.models.del(); +client.beta.vectorStores.del(); +client.beta.vectorStores.files.del(); +client.beta.assistants.del(); +client.beta.threads.del(); +client.beta.threads.messages.del(); + +// After +client.chat.completions.delete(); +client.files.delete(); +client.models.delete(); +client.beta.vectorStores.delete(); +client.beta.vectorStores.files.delete(); +client.beta.assistants.delete(); +client.beta.threads.delete(); +client.beta.threads.messages.delete(); +``` + +### Removed request options overloads + +When making requests with no required body, query or header parameters, you must now explicitly pass `null`, `undefined` or an empty object `{}` to the params argument in order to customise request options. + +```diff +client.example.list(); +client.example.list({}, { headers: { ... } }); +client.example.list(null, { headers: { ... } }); +client.example.list(undefined, { headers: { ... } }); +- client.example.list({ headers: { ... } }); ++ client.example.list({}, { headers: { ... } }); +``` + +This affects the following methods: + +- `client.chat.completions.list()` +- `client.chat.completions.messages.list()` +- `client.files.list()` +- `client.fineTuning.jobs.list()` +- `client.fineTuning.jobs.listEvents()` +- `client.fineTuning.jobs.checkpoints.list()` +- `client.beta.vectorStores.list()` +- `client.beta.vectorStores.files.list()` +- `client.beta.assistants.list()` +- `client.beta.threads.create()` +- `client.beta.threads.runs.list()` +- `client.beta.threads.messages.list()` +- `client.batches.list()` + +### Pagination changes + +Note that the `for await` syntax is _not_ affected. This still works as-is: + +```ts +// Automatically fetches more pages as needed. +for await (const fineTuningJob of client.fineTuning.jobs.list()) { + console.log(fineTuningJob); +} +``` + +#### Simplified interface + +The pagination interface has been simplified: + +```ts +// Before +page.nextPageParams(); +page.nextPageInfo(); +// Required manually handling { url } | { params } type + +// After +page.nextPageRequestOptions(); +``` + +#### Removed unnecessary classes + +Page classes for individual methods are now type aliases: + +```ts +// Before +export class FineTuningJobsPage extends CursorPage {} + +// After +export type FineTuningJobsPage = CursorPage; +``` + +If you were importing these classes at runtime, you'll need to switch to importing the base class or only import them at the type-level. + +### File handling + +The deprecated `fileFromPath` helper has been removed in favor of native Node.js streams: + +```ts +// Before +OpenAI.fileFromPath('path/to/file'); + +// After +import fs from 'fs'; +fs.createReadStream('path/to/file'); +``` + +Note that this function previously only worked on Node.j. If you're using Bun, you can use [`Bun.file`](https://bun.sh/docs/api/file-io) instead. + +### Shims removal + +Previously you could configure the types that the SDK used like this: + +```ts +// Tell TypeScript and the package to use the global Web fetch instead of node-fetch. +import 'openai/shims/web'; +import OpenAI from 'openai'; +``` + +The `openai/shims` imports have been removed. Your global types must now be [correctly configured](#minimum-types-requirements). + +### `openai/src` directory removed + +Previously IDEs may have auto-completed imports from the `openai/src` directory, however this +directory was only included for an improved go-to-definition experience and should not have been used at runtime. + +If you have any `openai/src` imports, you must replace it with `openai`. + +```ts +// Before +import OpenAI from 'openai/src'; + +// After +import OpenAI from 'openai'; +``` + +### Headers + +The `headers` property on `APIError` objects is now an instance of the Web [Headers](https://developer.mozilla.org/en-US/docs/Web/API/Headers) class. It was previously just `Record`. + +### Removed exports + +#### `Response` + +```typescript +// Before +import { Response } from 'openai'; + +// After +// `Response` must now come from the builtin types +``` + +#### Resource classes + +If you were importing resource classes from the root package then you must now import them from the file they are defined in: + +```typescript +// Before +import { Completions } from 'openai'; + +// After +import { Completions } from 'openai/resources/completions'; +``` + +#### `openai/core` + +The `openai/core` file was intended to be internal-only but it was publicly accessible, as such it has been refactored and split up into internal files. + +If you were relying on anything that was only exported from `openai/core` and is also not accessible anywhere else, please open an issue and we'll consider adding it to the public API. + +#### `APIClient` + +The `APIClient` base client class has been removed as it is no longer needed. If you were importing this class then you must now import the main client class: + +```typescript +// Before +import { APIClient } from 'openai/core'; + +// After +import { OpenAI } from 'openai'; +``` + +#### Cleaned up `openai/uploads` exports + +The following exports have been removed from `openai/uploads` as they were not intended to be a part of the public API: + +- `fileFromPath` +- `BlobPart` +- `BlobLike` +- `FileLike` +- `ResponseLike` +- `isResponseLike` +- `isBlobLike` +- `isFileLike` +- `isUploadable` +- `isMultipartBody` +- `maybeMultipartFormRequestOptions` +- `multipartFormRequestOptions` +- `createForm` + +Note that `Uploadable` & `toFile` **are** still exported: + +```typescript +import { type Uploadable, toFile } from 'openai/uploads'; +``` diff --git a/README.md b/README.md index eb659577f..08226e928 100644 --- a/README.md +++ b/README.md @@ -287,8 +287,6 @@ main(); Like with `.stream()`, we provide a variety of [helpers and events](helpers.md#chat-events). -Note that `runFunctions` was previously available as well, but has been deprecated in favor of `runTools`. - Read more about various examples such as with integrating with [zod](helpers.md#integrate-with-zod), [next.js](helpers.md#integrate-with-nextjs), and [proxying a stream to the browser](helpers.md#proxy-streaming-to-a-browser). @@ -388,6 +386,20 @@ const { data: stream, request_id } = await openai.chat.completions .withResponse(); ``` +## Realtime API Beta + +The Realtime API enables you to build low-latency, multi-modal conversational experiences. It currently supports text and audio as both input and output, as well as [function calling](https://platform.openai.com/docs/guides/function-calling) through a `WebSocket` connection. + +```ts +import { OpenAIRealtimeWebSocket } from 'openai/beta/realtime/websocket'; + +const rt = new OpenAIRealtimeWebSocket({ model: 'gpt-4o-realtime-preview-2024-12-17' }); + +rt.on('response.text.delta', (event) => process.stdout.write(event.delta)); +``` + +For more information see [realtime.md](realtime.md). + ## Microsoft Azure OpenAI To use this library with [Azure OpenAI](https://learn.microsoft.com/azure/ai-services/openai/overview), use the `AzureOpenAI` @@ -513,6 +525,59 @@ console.log(raw.headers.get('X-My-Header')); console.log(chatCompletion); ``` +### Logging + +> [!IMPORTANT] +> All log messages are intended for debugging only. The format and content of log messages +> may change between releases. + +#### Log levels + +The log level can be configured in two ways: + +1. Via the `OPENAI_LOG` environment variable +2. Using the `logLevel` client option (overrides the environment variable if set) + +```ts +import OpenAI from 'openai'; + +const client = new OpenAI({ + logLevel: 'debug', // Show all log messages +}); +``` + +Available log levels, from most to least verbose: + +- `'debug'` - Show debug messages, info, warnings, and errors +- `'info'` - Show info messages, warnings, and errors +- `'warn'` - Show warnings and errors (default) +- `'error'` - Show only errors +- `'off'` - Disable all logging + +At the `'debug'` level, all HTTP requests and responses are logged, including headers and bodies. +Some authentication-related headers are redacted, but sensitive data in request and response bodies +may still be visible. + +#### Custom logger + +By default, this library logs to `globalThis.console`. You can also provide a custom logger. +Most logging libraries are supported, including [pino](https://www.npmjs.com/package/pino), [winston](https://www.npmjs.com/package/winston), [bunyan](https://www.npmjs.com/package/bunyan), [consola](https://www.npmjs.com/package/consola), [signale](https://www.npmjs.com/package/signale), and [@std/log](https://jsr.io/@std/log). If your logger doesn't work, please open an issue. + +When providing a custom logger, the `logLevel` option still controls which messages are emitted, messages +below the configured level will not be sent to your logger. + +```ts +import OpenAI from 'openai'; +import pino from 'pino'; + +const logger = pino(); + +const client = new OpenAI({ + logger: logger.child({ name: 'OpenAI' }), + logLevel: 'debug', // Send all messages to pino, allowing it to filter +}); +``` + ### Making custom/undocumented requests This library is typed for convenient access to the documented API. If you need to access undocumented @@ -572,52 +637,67 @@ globalThis.fetch = fetch; Or pass it to the client: ```ts +import OpenAI from 'openai'; import fetch from 'my-fetch'; const client = new OpenAI({ fetch }); ``` -### Logging and middleware +### Fetch options -You may also provide a custom `fetch` function when instantiating the client, -which can be used to inspect or alter the `Request` or `Response` before/after each request: +If you want to set custom `fetch` options without overriding the `fetch` function, you can provide a `fetchOptions` object when instantiating the client or making a request. (Request-specific options override client options.) ```ts -import { fetch } from 'undici'; // as one example import OpenAI from 'openai'; const client = new OpenAI({ - fetch: async (url: RequestInfo, init?: RequestInit): Promise => { - console.log('About to make a request', url, init); - const response = await fetch(url, init); - console.log('Got response', response); - return response; + fetchOptions: { + // `RequestInit` options }, }); ``` -Note that if given a `DEBUG=true` environment variable, this library will log all requests and responses automatically. -This is intended for debugging purposes only and may change in the future without notice. +#### Configuring proxies -### Configuring an HTTP(S) Agent (e.g., for proxies) +To modify proxy behavior, you can provide custom `fetchOptions` that add runtime-specific proxy +options to requests: -By default, this library uses a stable agent for all http/https requests to reuse TCP connections, eliminating many TCP & TLS handshakes and shaving around 100ms off most requests. + **Node** [[docs](https://github.com/nodejs/undici/blob/main/docs/docs/api/ProxyAgent.md#example---proxyagent-with-fetch)] -If you would like to disable or customize this behavior, for example to use the API behind a proxy, you can pass an `httpAgent` which is used for all requests (be they http or https), for example: +```ts +import OpenAI from 'openai'; +import * as undici from 'undici'; + +const proxyAgent = new undici.ProxyAgent('http://localhost:8888'); +const client = new OpenAI({ + fetchOptions: { + dispatcher: proxyAgent, + }, +}); +``` + + **Bun** [[docs](https://bun.sh/guides/http/proxy)] - ```ts -import http from 'http'; -import { HttpsProxyAgent } from 'https-proxy-agent'; +import OpenAI from 'openai'; -// Configure the default for all requests: const client = new OpenAI({ - httpAgent: new HttpsProxyAgent(process.env.PROXY_URL), + fetchOptions: { + proxy: 'http://localhost:8888', + }, }); +``` -// Override per-request: -await client.models.list({ - httpAgent: new http.Agent({ keepAlive: false }), + **Deno** [[docs](https://docs.deno.com/api/deno/~/Deno.createHttpClient)] + +```ts +import OpenAI from 'npm:openai'; + +const httpClient = Deno.createHttpClient({ proxy: { url: 'http://localhost:8888' } }); +const client = new OpenAI({ + fetchOptions: { + client: httpClient, + }, }); ``` @@ -628,7 +708,7 @@ await client.models.list({ This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) conventions, though certain backwards-incompatible changes may be released as minor versions: 1. Changes that only affect static types, without breaking runtime behavior. -2. Changes to library internals which are technically public but not intended or documented for external use. _(Please open a GitHub issue to let us know if you are relying on such internals)_. +2. Changes to library internals which are technically public but not intended or documented for external use. _(Please open a GitHub issue to let us know if you are relying on such internals.)_ 3. Changes that we do not expect to impact the vast majority of users in practice. We take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience. diff --git a/SECURITY.md b/SECURITY.md index c54acaf33..3b3bd8a66 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,9 +2,9 @@ ## Reporting Security Issues -This SDK is generated by [Stainless Software Inc](http://stainlessapi.com). Stainless takes security seriously, and encourages you to report any security vulnerability promptly so that appropriate action can be taken. +This SDK is generated by [Stainless Software Inc](http://stainless.com). Stainless takes security seriously, and encourages you to report any security vulnerability promptly so that appropriate action can be taken. -To report a security issue, please contact the Stainless team at security@stainlessapi.com. +To report a security issue, please contact the Stainless team at security@stainless.com. ## Responsible Disclosure diff --git a/api.md b/api.md index cd4c59e0c..978bff992 100644 --- a/api.md +++ b/api.md @@ -5,6 +5,7 @@ Types: - ErrorObject - FunctionDefinition - FunctionParameters +- Metadata - ResponseFormatJSONObject - ResponseFormatJSONSchema - ResponseFormatText @@ -31,38 +32,50 @@ Types: Types: -- ChatCompletion -- ChatCompletionAssistantMessageParam -- ChatCompletionAudio -- ChatCompletionAudioParam -- ChatCompletionChunk -- ChatCompletionContentPart -- ChatCompletionContentPartImage -- ChatCompletionContentPartInputAudio -- ChatCompletionContentPartRefusal -- ChatCompletionContentPartText -- ChatCompletionDeveloperMessageParam -- ChatCompletionFunctionCallOption -- ChatCompletionFunctionMessageParam -- ChatCompletionMessage -- ChatCompletionMessageParam -- ChatCompletionMessageToolCall -- ChatCompletionModality -- ChatCompletionNamedToolChoice -- ChatCompletionPredictionContent -- ChatCompletionReasoningEffort -- ChatCompletionRole -- ChatCompletionStreamOptions -- ChatCompletionSystemMessageParam -- ChatCompletionTokenLogprob -- ChatCompletionTool -- ChatCompletionToolChoiceOption -- ChatCompletionToolMessageParam -- ChatCompletionUserMessageParam +- ChatCompletion +- ChatCompletionAssistantMessageParam +- ChatCompletionAudio +- ChatCompletionAudioParam +- ChatCompletionChunk +- ChatCompletionContentPart +- ChatCompletionContentPartImage +- ChatCompletionContentPartInputAudio +- ChatCompletionContentPartRefusal +- ChatCompletionContentPartText +- ChatCompletionDeleted +- ChatCompletionDeveloperMessageParam +- ChatCompletionFunctionCallOption +- ChatCompletionFunctionMessageParam +- ChatCompletionMessage +- ChatCompletionMessageParam +- ChatCompletionMessageToolCall +- ChatCompletionModality +- ChatCompletionNamedToolChoice +- ChatCompletionPredictionContent +- ChatCompletionReasoningEffort +- ChatCompletionRole +- ChatCompletionStoreMessage +- ChatCompletionStreamOptions +- ChatCompletionSystemMessageParam +- ChatCompletionTokenLogprob +- ChatCompletionTool +- ChatCompletionToolChoiceOption +- ChatCompletionToolMessageParam +- ChatCompletionUserMessageParam Methods: -- client.chat.completions.create({ ...params }) -> ChatCompletion +- client.chat.completions.create({ ...params }) -> ChatCompletion +- client.chat.completions.retrieve(completionID) -> ChatCompletion +- client.chat.completions.update(completionID, { ...params }) -> ChatCompletion +- client.chat.completions.list({ ...params }) -> ChatCompletionsPage +- client.chat.completions.delete(completionID) -> ChatCompletionDeleted + +### Messages + +Methods: + +- client.chat.completions.messages.list(completionID, { ...params }) -> ChatCompletionStoreMessagesPage # Embeddings @@ -92,7 +105,6 @@ Methods: - client.files.list({ ...params }) -> FileObjectsPage - client.files.delete(fileID) -> FileDeleted - client.files.content(fileID) -> Response -- client.files.retrieveContent(fileID) -> string - client.files.waitForProcessing(id, { pollInterval = 5000, maxWait = 30 _ 60 _ 1000 }) -> Promise<FileObject> # Images @@ -212,6 +224,67 @@ Methods: # Beta +## Realtime + +Types: + +- ConversationCreatedEvent +- ConversationItem +- ConversationItemContent +- ConversationItemCreateEvent +- ConversationItemCreatedEvent +- ConversationItemDeleteEvent +- ConversationItemDeletedEvent +- ConversationItemInputAudioTranscriptionCompletedEvent +- ConversationItemInputAudioTranscriptionFailedEvent +- ConversationItemTruncateEvent +- ConversationItemTruncatedEvent +- ConversationItemWithReference +- ErrorEvent +- InputAudioBufferAppendEvent +- InputAudioBufferClearEvent +- InputAudioBufferClearedEvent +- InputAudioBufferCommitEvent +- InputAudioBufferCommittedEvent +- InputAudioBufferSpeechStartedEvent +- InputAudioBufferSpeechStoppedEvent +- RateLimitsUpdatedEvent +- RealtimeClientEvent +- RealtimeResponse +- RealtimeResponseStatus +- RealtimeResponseUsage +- RealtimeServerEvent +- ResponseAudioDeltaEvent +- ResponseAudioDoneEvent +- ResponseAudioTranscriptDeltaEvent +- ResponseAudioTranscriptDoneEvent +- ResponseCancelEvent +- ResponseContentPartAddedEvent +- ResponseContentPartDoneEvent +- ResponseCreateEvent +- ResponseCreatedEvent +- ResponseDoneEvent +- ResponseFunctionCallArgumentsDeltaEvent +- ResponseFunctionCallArgumentsDoneEvent +- ResponseOutputItemAddedEvent +- ResponseOutputItemDoneEvent +- ResponseTextDeltaEvent +- ResponseTextDoneEvent +- SessionCreatedEvent +- SessionUpdateEvent +- SessionUpdatedEvent + +### Sessions + +Types: + +- Session +- SessionCreateResponse + +Methods: + +- client.beta.realtime.sessions.create({ ...params }) -> SessionCreateResponse + ## VectorStores Types: @@ -222,7 +295,7 @@ Types: - OtherFileChunkingStrategyObject - StaticFileChunkingStrategy - StaticFileChunkingStrategyObject -- StaticFileChunkingStrategyParam +- StaticFileChunkingStrategyObjectParam - VectorStore - VectorStoreDeleted @@ -274,7 +347,6 @@ Methods: Methods: -- client.beta.chat.completions.runFunctions(body, options?) -> ChatCompletionRunner | ChatCompletionStreamingRunner - client.beta.chat.completions.runTools(body, options?) -> ChatCompletionRunner | ChatCompletionStreamingRunner - client.beta.chat.completions.stream(body, options?) -> ChatCompletionStream diff --git a/ecosystem-tests/bun/bun.lockb b/ecosystem-tests/bun/bun.lockb index 4ece80c92..a01f1617e 100755 Binary files a/ecosystem-tests/bun/bun.lockb and b/ecosystem-tests/bun/bun.lockb differ diff --git a/ecosystem-tests/bun/openai.test.ts b/ecosystem-tests/bun/openai.test.ts index 979a4962f..f8facc521 100644 --- a/ecosystem-tests/bun/openai.test.ts +++ b/ecosystem-tests/bun/openai.test.ts @@ -43,6 +43,20 @@ test(`basic request works`, async function () { expectSimilar(completion.choices[0]?.message?.content, 'This is a test', 10); }); +test(`proxied request works`, async function () { + const client = new OpenAI({ + fetchOptions: { + proxy: process.env['ECOSYSTEM_TESTS_PROXY'], + }, + }); + + const completion = await client.chat.completions.create({ + model: 'gpt-4', + messages: [{ role: 'user', content: 'Say this is a test' }], + }); + expectSimilar(completion.choices[0]?.message?.content, 'This is a test', 10); +}); + test(`raw response`, async function () { const response = await client.chat.completions .create({ @@ -117,6 +131,14 @@ test('handles fs.ReadStream', async function () { expectSimilar(result.text, correctAnswer, 12); }); +test('handles Bun.File', async function () { + const result = await client.audio.transcriptions.create({ + file: Bun.file('sample1.mp3'), + model, + }); + expectSimilar(result.text, correctAnswer, 12); +}); + const fineTune = `{"prompt": "", "completion": ""}`; // @ts-ignore avoid DOM lib for testing purposes diff --git a/ecosystem-tests/bun/package.json b/ecosystem-tests/bun/package.json index 5a75b1eb2..35660b7f9 100644 --- a/ecosystem-tests/bun/package.json +++ b/ecosystem-tests/bun/package.json @@ -10,7 +10,7 @@ }, "devDependencies": { "fastest-levenshtein": "^1.0.16", - "bun-types": "latest", + "bun-types": "^1.2.4", "typescript": "5.0.4" } } diff --git a/ecosystem-tests/bun/tsconfig.json b/ecosystem-tests/bun/tsconfig.json index 69a17ce99..f428d6b10 100644 --- a/ecosystem-tests/bun/tsconfig.json +++ b/ecosystem-tests/bun/tsconfig.json @@ -9,7 +9,7 @@ "allowImportingTsExtensions": true, "strict": true, "downlevelIteration": true, - "skipLibCheck": false, + "skipLibCheck": true, "jsx": "preserve", "allowSyntheticDefaultImports": true, "forceConsistentCasingInFileNames": true, diff --git a/ecosystem-tests/cli.ts b/ecosystem-tests/cli.ts index 39361ae2e..9781c2b03 100644 --- a/ecosystem-tests/cli.ts +++ b/ecosystem-tests/cli.ts @@ -3,12 +3,8 @@ import execa from 'execa'; import yargs from 'yargs'; import assert from 'assert'; import path from 'path'; - -try { - // @ts-ignore - var SegfaultHandler = require('segfault-handler'); - SegfaultHandler.registerHandler('crash.log'); -} catch (_) {} +import { createServer } from 'http'; +import { connect } from 'net'; const TAR_NAME = 'openai.tgz'; const PACK_FOLDER = '.pack'; @@ -115,6 +111,35 @@ const projectRunners = { let projectNames = Object.keys(projectRunners) as Array; const projectNamesSet = new Set(projectNames); +async function startProxy() { + const proxy = createServer((_req, res) => { + res.end(); + }); + + proxy.on('connect', (req, clientSocket, head) => { + console.log('got proxied connection'); + const serverSocket = connect(443, 'api.openai.com', () => { + clientSocket.write( + 'HTTP/1.1 200 Connection Established\r\n' + 'Proxy-agent: Node.js-Proxy\r\n' + '\r\n', + ); + serverSocket.write(head); + serverSocket.pipe(clientSocket); + clientSocket.pipe(serverSocket); + }); + }); + + await new Promise((resolve) => proxy.listen(0, '127.0.0.1', resolve)); + + const address = proxy.address(); + assert(address && typeof address !== 'string'); + process.env['ECOSYSTEM_TESTS_PROXY'] = 'http://127.0.0.1:' + address.port; + + return () => { + delete process.env['ECOSYSTEM_TESTS_PROXY']; + proxy.close(); + }; +} + function parseArgs() { return yargs(process.argv.slice(2)) .scriptName('ecosystem-tests') @@ -293,112 +318,118 @@ async function main() { await fileCache.cacheFiles(tmpFolderPath); } - if (jobs > 1) { - const queue = [...projectsToRun]; - const runningProjects = new Set(); - - const cursorLeft = '\x1B[G'; - const eraseLine = '\x1B[2K'; - - let progressDisplayed = false; - function clearProgress() { - if (progressDisplayed) { - process.stderr.write(cursorLeft + eraseLine); - progressDisplayed = false; + const stopProxy = await startProxy(); + try { + if (jobs > 1) { + const queue = [...projectsToRun]; + const runningProjects = new Set(); + + const cursorLeft = '\x1B[G'; + const eraseLine = '\x1B[2K'; + + let progressDisplayed = false; + function clearProgress() { + if (progressDisplayed) { + process.stderr.write(cursorLeft + eraseLine); + progressDisplayed = false; + } + } + const spinner = ['|', '/', '-', '\\']; + + function showProgress() { + clearProgress(); + progressDisplayed = true; + const spin = spinner[Math.floor(Date.now() / 500) % spinner.length]; + process.stderr.write( + `${spin} Running ${[...runningProjects].join(', ')}`.substring(0, process.stdout.columns - 3) + + '...', + ); } - } - const spinner = ['|', '/', '-', '\\']; - function showProgress() { - clearProgress(); - progressDisplayed = true; - const spin = spinner[Math.floor(Date.now() / 500) % spinner.length]; - process.stderr.write( - `${spin} Running ${[...runningProjects].join(', ')}`.substring(0, process.stdout.columns - 3) + '...', - ); - } + const progressInterval = setInterval(showProgress, process.stdout.isTTY ? 500 : 5000); + showProgress(); - const progressInterval = setInterval(showProgress, process.stdout.isTTY ? 500 : 5000); - showProgress(); + await Promise.all( + [...Array(jobs).keys()].map(async () => { + while (queue.length) { + const project = queue.shift(); + if (!project) { + break; + } - await Promise.all( - [...Array(jobs).keys()].map(async () => { - while (queue.length) { - const project = queue.shift(); - if (!project) { - break; - } + // preserve interleaved ordering of writes to stdout/stderr + const chunks: { dest: 'stdout' | 'stderr'; data: string | Buffer }[] = []; + try { + runningProjects.add(project); + const child = execa( + 'yarn', + [ + 'tsn', + __filename, + project, + '--skip-pack', + '--noCleanup', + `--retry=${args.retry}`, + ...(args.live ? ['--live'] : []), + ...(args.verbose ? ['--verbose'] : []), + ...(args.deploy ? ['--deploy'] : []), + ...(args.fromNpm ? ['--from-npm'] : []), + ], + { stdio: 'pipe', encoding: 'utf8', maxBuffer: 100 * 1024 * 1024 }, + ); + child.stdout?.on('data', (data) => chunks.push({ dest: 'stdout', data })); + child.stderr?.on('data', (data) => chunks.push({ dest: 'stderr', data })); + + await child; + } catch (error) { + failed.push(project); + } finally { + runningProjects.delete(project); + } - // preserve interleaved ordering of writes to stdout/stderr - const chunks: { dest: 'stdout' | 'stderr'; data: string | Buffer }[] = []; - try { - runningProjects.add(project); - const child = execa( - 'yarn', - [ - 'tsn', - __filename, - project, - '--skip-pack', - '--noCleanup', - `--retry=${args.retry}`, - ...(args.live ? ['--live'] : []), - ...(args.verbose ? ['--verbose'] : []), - ...(args.deploy ? ['--deploy'] : []), - ...(args.fromNpm ? ['--from-npm'] : []), - ], - { stdio: 'pipe', encoding: 'utf8', maxBuffer: 100 * 1024 * 1024 }, - ); - child.stdout?.on('data', (data) => chunks.push({ dest: 'stdout', data })); - child.stderr?.on('data', (data) => chunks.push({ dest: 'stderr', data })); - - await child; - } catch (error) { - failed.push(project); - } finally { - runningProjects.delete(project); - } + if (IS_CI) { + console.log(`::group::${failed.includes(project) ? '❌' : '✅'} ${project}`); + } - if (IS_CI) { - console.log(`::group::${failed.includes(project) ? '❌' : '✅'} ${project}`); + for (const { data } of chunks) { + process.stdout.write(data); + } + if (IS_CI) console.log('::endgroup::'); } + }), + ); - for (const { data } of chunks) { - process.stdout.write(data); - } - if (IS_CI) console.log('::endgroup::'); - } - }), - ); - - clearInterval(progressInterval); - clearProgress(); - } else { - for (const project of projectsToRun) { - const fn = projectRunners[project]; - - await withChdir(path.join(rootDir, 'ecosystem-tests', project), async () => { - console.error('\n'); - console.error(banner(`▶️ ${project}`)); - console.error('\n'); - - try { - await withRetry(fn, project, state.retry, state.retryDelay); + clearInterval(progressInterval); + clearProgress(); + } else { + for (const project of projectsToRun) { + const fn = projectRunners[project]; + + await withChdir(path.join(rootDir, 'ecosystem-tests', project), async () => { + console.error('\n'); + console.error(banner(`▶️ ${project}`)); console.error('\n'); - console.error(`✅ ${project}`); - } catch (err) { - if (err && (err as any).shortMessage) { - console.error((err as any).shortMessage); - } else { - console.error(err); + + try { + await withRetry(fn, project, state.retry, state.retryDelay); + console.error('\n'); + console.error(`✅ ${project}`); + } catch (err) { + if (err && (err as any).shortMessage) { + console.error((err as any).shortMessage); + } else { + console.error(err); + } + console.error('\n'); + console.error(`❌ ${project}`); + failed.push(project); } console.error('\n'); - console.error(`❌ ${project}`); - failed.push(project); - } - console.error('\n'); - }); + }); + } } + } finally { + stopProxy(); } if (!args.noCleanup) { diff --git a/ecosystem-tests/cloudflare-worker/package-lock.json b/ecosystem-tests/cloudflare-worker/package-lock.json index e359caaf0..51cd6aee3 100644 --- a/ecosystem-tests/cloudflare-worker/package-lock.json +++ b/ecosystem-tests/cloudflare-worker/package-lock.json @@ -7,9 +7,6 @@ "": { "name": "cfw", "version": "0.0.0", - "dependencies": { - "node-fetch": "^3.3.1" - }, "devDependencies": { "@cloudflare/workers-types": "^4.20230419.0", "fastest-levenshtein": "^1.0.16", @@ -2279,14 +2276,6 @@ "node": ">= 8" } }, - "node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", - "engines": { - "node": ">= 12" - } - }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -2572,28 +2561,6 @@ "bser": "2.1.1" } }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], - "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - }, - "engines": { - "node": "^12.20 || >= 14.13" - } - }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -2653,17 +2620,6 @@ "node": ">= 6" } }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "dependencies": { - "fetch-blob": "^3.1.2" - }, - "engines": { - "node": ">=12.20.0" - } - }, "node_modules/from": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", @@ -4023,41 +3979,6 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "engines": { - "node": ">=10.5.0" - } - }, - "node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, "node_modules/node-forge": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", @@ -5018,14 +4939,6 @@ "makeerror": "1.0.12" } }, - "node_modules/web-streams-polyfill": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", - "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==", - "engines": { - "node": ">= 8" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/ecosystem-tests/cloudflare-worker/package.json b/ecosystem-tests/cloudflare-worker/package.json index 64fa0ad25..bec1aab03 100644 --- a/ecosystem-tests/cloudflare-worker/package.json +++ b/ecosystem-tests/cloudflare-worker/package.json @@ -18,8 +18,5 @@ "ts-jest": "^29.1.0", "typescript": "5.0.4", "wrangler": "^3.74.0" - }, - "dependencies": { - "node-fetch": "^3.3.1" } } diff --git a/ecosystem-tests/cloudflare-worker/tests/test.js b/ecosystem-tests/cloudflare-worker/tests/test.js index 3a1ca3ea1..69981a0bb 100644 --- a/ecosystem-tests/cloudflare-worker/tests/test.js +++ b/ecosystem-tests/cloudflare-worker/tests/test.js @@ -1,5 +1,3 @@ -import fetch from 'node-fetch'; - it( 'works', async () => { diff --git a/ecosystem-tests/cloudflare-worker/tsconfig.check.json b/ecosystem-tests/cloudflare-worker/tsconfig.check.json index 22d6f227b..d1b622447 100644 --- a/ecosystem-tests/cloudflare-worker/tsconfig.check.json +++ b/ecosystem-tests/cloudflare-worker/tsconfig.check.json @@ -1,8 +1,5 @@ { "extends": "./tsconfig.json", "include": ["src"], - "exclude": ["tests", "jest.config.cjs"], - "compilerOptions": { - "skipLibCheck": false - } + "exclude": ["tests", "jest.config.cjs"] } diff --git a/ecosystem-tests/deno/package-lock.json b/ecosystem-tests/deno/package-lock.json deleted file mode 100644 index 99ead654a..000000000 --- a/ecosystem-tests/deno/package-lock.json +++ /dev/null @@ -1,98 +0,0 @@ -{ - "name": "deno", - "lockfileVersion": 3, - "requires": true, - "packages": { - "node_modules/.deno/asynckit@0.4.0/node_modules/asynckit": { - "version": "0.4.0", - "extraneous": true, - "license": "MIT", - "devDependencies": { - "browserify": "^13.0.0", - "browserify-istanbul": "^2.0.0", - "coveralls": "^2.11.9", - "eslint": "^2.9.0", - "istanbul": "^0.4.3", - "obake": "^0.1.2", - "phantomjs-prebuilt": "^2.1.7", - "pre-commit": "^1.1.3", - "reamde": "^1.1.0", - "rimraf": "^2.5.2", - "size-table": "^0.2.0", - "tap-spec": "^4.1.1", - "tape": "^4.5.1" - } - }, - "node_modules/.deno/axios@0.26.1": {}, - "node_modules/.deno/combined-stream@1.0.8": {}, - "node_modules/.deno/delayed-stream@1.0.0/node_modules/delayed-stream": { - "version": "1.0.0", - "extraneous": true, - "license": "MIT", - "devDependencies": { - "fake": "0.2.0", - "far": "0.0.1" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/.deno/follow-redirects@1.15.2/node_modules/follow-redirects": { - "version": "1.15.2", - "extraneous": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "devDependencies": { - "concat-stream": "^2.0.0", - "eslint": "^5.16.0", - "express": "^4.16.4", - "lolex": "^3.1.0", - "mocha": "^6.0.2", - "nyc": "^14.1.1" - }, - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/.deno/form-data@4.0.0": {}, - "node_modules/.deno/mime-db@1.52.0/node_modules/mime-db": { - "version": "1.52.0", - "extraneous": true, - "license": "MIT", - "devDependencies": { - "bluebird": "3.7.2", - "co": "4.6.0", - "cogent": "1.0.1", - "csv-parse": "4.16.3", - "eslint": "7.32.0", - "eslint-config-standard": "15.0.1", - "eslint-plugin-import": "2.25.4", - "eslint-plugin-markdown": "2.2.1", - "eslint-plugin-node": "11.1.0", - "eslint-plugin-promise": "5.1.1", - "eslint-plugin-standard": "4.1.0", - "gnode": "0.1.2", - "media-typer": "1.1.0", - "mocha": "9.2.1", - "nyc": "15.1.0", - "raw-body": "2.5.0", - "stream-to-array": "2.3.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/.deno/mime-types@2.1.35": {}, - "node_modules/.deno/openai@3.3.0": {} - } -} diff --git a/ecosystem-tests/node-js/package-lock.json b/ecosystem-tests/node-js/package-lock.json index bb59ccb92..064116bdb 100644 --- a/ecosystem-tests/node-js/package-lock.json +++ b/ecosystem-tests/node-js/package-lock.json @@ -5,240 +5,9 @@ "requires": true, "packages": { "": { - "name": "foo", + "name": "node-js", "version": "1.0.0", - "license": "ISC", - "dependencies": { - "openai": "^4.40.1" - } - }, - "node_modules/@types/node": { - "version": "18.19.31", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.31.tgz", - "integrity": "sha512-ArgCD39YpyyrtFKIqMDvjz79jto5fcI/SVUs2HwB+f0dAzq68yqOdyaSivLiLugSziTpNXLQrVb7RZFmdZzbhA==", - "dependencies": { - "undici-types": "~5.26.4" - } - }, - "node_modules/@types/node-fetch": { - "version": "2.6.11", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz", - "integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==", - "dependencies": { - "@types/node": "*", - "form-data": "^4.0.0" - } - }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "dependencies": { - "event-target-shim": "^5.0.0" - }, - "engines": { - "node": ">=6.5" - } - }, - "node_modules/agentkeepalive": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", - "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", - "dependencies": { - "humanize-ms": "^1.2.1" - }, - "engines": { - "node": ">= 8.0.0" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/form-data-encoder": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", - "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==" - }, - "node_modules/formdata-node": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", - "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", - "dependencies": { - "node-domexception": "1.0.0", - "web-streams-polyfill": "4.0.0-beta.3" - }, - "engines": { - "node": ">= 12.20" - } - }, - "node_modules/formdata-node/node_modules/web-streams-polyfill": { - "version": "4.0.0-beta.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", - "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", - "engines": { - "node": ">= 14" - } - }, - "node_modules/humanize-ms": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", - "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", - "dependencies": { - "ms": "^2.0.0" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "engines": { - "node": ">=10.5.0" - } - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/openai": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/openai/-/openai-4.40.1.tgz", - "integrity": "sha512-mS7LerF4fY1/we0aKGGwIWtosTJFLKuNbBWMBR/G1TAZUHoktAdod0dqIrlQvSD39uS6jNEEbT7jRsXmzfEPBw==", - "dependencies": { - "@types/node": "^18.11.18", - "@types/node-fetch": "^2.6.4", - "abort-controller": "^3.0.0", - "agentkeepalive": "^4.2.1", - "form-data-encoder": "1.7.2", - "formdata-node": "^4.3.2", - "node-fetch": "^2.6.7", - "web-streams-polyfill": "^3.2.1" - }, - "bin": { - "openai": "bin/cli" - } - }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" - }, - "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" - }, - "node_modules/web-streams-polyfill": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", - "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", - "engines": { - "node": ">= 8" - } - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } + "license": "ISC" } } } diff --git a/ecosystem-tests/node-js/package.json b/ecosystem-tests/node-js/package.json index 63f858014..eeb432712 100644 --- a/ecosystem-tests/node-js/package.json +++ b/ecosystem-tests/node-js/package.json @@ -7,8 +7,5 @@ "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", - "license": "ISC", - "dependencies": { - "openai": "^4.40.1" - } + "license": "ISC" } diff --git a/ecosystem-tests/node-ts-cjs-auto/package-lock.json b/ecosystem-tests/node-ts-cjs-auto/package-lock.json index c3880beb2..68233e13b 100644 --- a/ecosystem-tests/node-ts-cjs-auto/package-lock.json +++ b/ecosystem-tests/node-ts-cjs-auto/package-lock.json @@ -9,12 +9,10 @@ "version": "0.0.1", "dependencies": { "formdata-node": "^4.4.1", - "node-fetch": "^2.6.1", "tsconfig-paths": "^4.0.0" }, "devDependencies": { "@types/node": "^20.4.2", - "@types/node-fetch": "^2.6.1", "@types/ws": "^8.5.4", "fastest-levenshtein": "^1.0.16", "jest": "^28.1.3", @@ -1101,16 +1099,6 @@ "undici-types": "~5.26.4" } }, - "node_modules/@types/node-fetch": { - "version": "2.6.11", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz", - "integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==", - "dev": true, - "dependencies": { - "@types/node": "*", - "form-data": "^4.0.0" - } - }, "node_modules/@types/prettier": { "version": "2.7.3", "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz", @@ -1208,12 +1196,6 @@ "sprintf-js": "~1.0.2" } }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true - }, "node_modules/babel-jest": { "version": "28.1.3", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-28.1.3.tgz", @@ -1524,18 +1506,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1594,15 +1564,6 @@ "node": ">=0.10.0" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -1782,20 +1743,6 @@ "node": ">=8" } }, - "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/formdata-node": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", @@ -2886,27 +2833,6 @@ "node": ">=8.6" } }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -2966,25 +2892,6 @@ "node": ">=10.5.0" } }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -3551,11 +3458,6 @@ "node": ">=8.0" } }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" - }, "node_modules/ts-jest": { "version": "28.0.8", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-28.0.8.tgz", @@ -3754,20 +3656,6 @@ "node": ">= 14" } }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/ecosystem-tests/node-ts-cjs-auto/package.json b/ecosystem-tests/node-ts-cjs-auto/package.json index 17e4ae9e6..44f55d778 100644 --- a/ecosystem-tests/node-ts-cjs-auto/package.json +++ b/ecosystem-tests/node-ts-cjs-auto/package.json @@ -9,12 +9,10 @@ }, "dependencies": { "formdata-node": "^4.4.1", - "node-fetch": "^2.6.1", "tsconfig-paths": "^4.0.0" }, "devDependencies": { "@types/node": "^20.4.2", - "@types/node-fetch": "^2.6.1", "@types/ws": "^8.5.4", "fastest-levenshtein": "^1.0.16", "jest": "^28.1.3", diff --git a/ecosystem-tests/node-ts-cjs-auto/tests/test.ts b/ecosystem-tests/node-ts-cjs-auto/tests/test.ts index e99ca56b6..ea6913ef6 100644 --- a/ecosystem-tests/node-ts-cjs-auto/tests/test.ts +++ b/ecosystem-tests/node-ts-cjs-auto/tests/test.ts @@ -1,9 +1,9 @@ import OpenAI, { APIUserAbortError, toFile } from 'openai'; import { TranscriptionCreateParams } from 'openai/resources/audio/transcriptions'; -import fetch from 'node-fetch'; import { File as FormDataFile, Blob as FormDataBlob } from 'formdata-node'; import * as fs from 'fs'; import { distance } from 'fastest-levenshtein'; +import { File } from 'node:buffer'; const url = 'https://audio-samples.github.io/samples/mp3/blizzard_biased/sample-1.mp3'; const filename = 'sample-1.mp3'; @@ -154,17 +154,6 @@ it(`aborting ChatCompletionStream works`, async function () { expect(contents.length).toBeGreaterThan(0); }); -it('handles formdata-node File', async function () { - const file = await fetch(url) - .then((x) => x.arrayBuffer()) - .then((x) => new FormDataFile([x], filename)); - - const params: TranscriptionCreateParams = { file, model }; - - const result = await client.audio.transcriptions.create(params); - expect(result.text).toBeSimilarTo(correctAnswer, 12); -}); - it('handles builtinFile', async function () { const file = await fetch(url) .then((x) => x.arrayBuffer()) @@ -238,4 +227,16 @@ describe('toFile', () => { }); expect(result.filename).toEqual('finetune.jsonl'); }); + it('handles formdata-node File', async function () { + const file = await fetch(url) + .then((x) => x.arrayBuffer()) + .then((x) => toFile(new FormDataFile([x], filename))); + + expect(file.name).toEqual(filename); + + const params: TranscriptionCreateParams = { file, model }; + + const result = await client.audio.transcriptions.create(params); + expect(result.text).toBeSimilarTo(correctAnswer, 12); + }); }); diff --git a/ecosystem-tests/node-ts-cjs-web/package-lock.json b/ecosystem-tests/node-ts-cjs-web/package-lock.json index b1624036b..48980c2f8 100644 --- a/ecosystem-tests/node-ts-cjs-web/package-lock.json +++ b/ecosystem-tests/node-ts-cjs-web/package-lock.json @@ -9,21 +9,15 @@ "version": "0.0.1", "dependencies": { "formdata-node": "^4.4.1", - "node-fetch": "^2.6.1", "tsconfig-paths": "^4.0.0" }, "devDependencies": { "@types/node": "^18.0.0", - "@types/node-fetch": "^2.6.1", "fastest-levenshtein": "^1.0.16", - "formdata-polyfill": "^4.0.10", "jest": "^29.5.0", - "jest-environment-jsdom": "^29.7.0", - "text-encoding-polyfill": "^0.6.7", + "jest-fixed-jsdom": "^0.0.9", "ts-jest": "^29.1.0", - "typescript": "4.7.4", - "web-streams-polyfill": "^3.2.1", - "whatwg-fetch": "^3.6.19" + "typescript": "4.7.4" } }, "node_modules/@ampproject/remapping": { @@ -1047,6 +1041,7 @@ "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", "dev": true, + "peer": true, "engines": { "node": ">= 10" } @@ -1130,6 +1125,7 @@ "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz", "integrity": "sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==", "dev": true, + "peer": true, "dependencies": { "@types/node": "*", "@types/tough-cookie": "*", @@ -1146,16 +1142,6 @@ "undici-types": "~5.26.4" } }, - "node_modules/@types/node-fetch": { - "version": "2.6.11", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz", - "integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==", - "dev": true, - "dependencies": { - "@types/node": "*", - "form-data": "^4.0.0" - } - }, "node_modules/@types/stack-utils": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", @@ -1166,7 +1152,8 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.3.tgz", "integrity": "sha512-THo502dA5PzG/sfQH+42Lw3fvmYkceefOspdCwpHRul8ik2Jv1K8I5OZz1AT3/rs46kwgMCe9bSBmDLYkkOMGg==", - "dev": true + "dev": true, + "peer": true }, "node_modules/@types/yargs": { "version": "17.0.24", @@ -1187,13 +1174,15 @@ "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", - "dev": true + "dev": true, + "peer": true }, "node_modules/acorn": { "version": "8.10.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", "dev": true, + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1206,6 +1195,7 @@ "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", "dev": true, + "peer": true, "dependencies": { "acorn": "^8.1.0", "acorn-walk": "^8.0.2" @@ -1216,6 +1206,7 @@ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", "dev": true, + "peer": true, "engines": { "node": ">=0.4.0" } @@ -1225,6 +1216,7 @@ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", "dev": true, + "peer": true, "dependencies": { "debug": "4" }, @@ -1297,7 +1289,8 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "dev": true, + "peer": true }, "node_modules/babel-jest": { "version": "29.7.0", @@ -1630,6 +1623,7 @@ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "dev": true, + "peer": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -1688,13 +1682,15 @@ "version": "0.5.0", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", - "dev": true + "dev": true, + "peer": true }, "node_modules/cssstyle": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", "dev": true, + "peer": true, "dependencies": { "cssom": "~0.3.6" }, @@ -1706,13 +1702,15 @@ "version": "0.3.8", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", - "dev": true + "dev": true, + "peer": true }, "node_modules/data-urls": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", "dev": true, + "peer": true, "dependencies": { "abab": "^2.0.6", "whatwg-mimetype": "^3.0.0", @@ -1727,6 +1725,7 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", "dev": true, + "peer": true, "dependencies": { "punycode": "^2.1.1" }, @@ -1739,6 +1738,7 @@ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", "dev": true, + "peer": true, "engines": { "node": ">=12" } @@ -1748,6 +1748,7 @@ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", "dev": true, + "peer": true, "dependencies": { "tr46": "^3.0.0", "webidl-conversions": "^7.0.0" @@ -1777,7 +1778,8 @@ "version": "10.4.3", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", - "dev": true + "dev": true, + "peer": true }, "node_modules/dedent": { "version": "1.5.1", @@ -1807,6 +1809,7 @@ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "dev": true, + "peer": true, "engines": { "node": ">=0.4.0" } @@ -1834,6 +1837,7 @@ "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", "dev": true, + "peer": true, "dependencies": { "webidl-conversions": "^7.0.0" }, @@ -1846,6 +1850,7 @@ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", "dev": true, + "peer": true, "engines": { "node": ">=12" } @@ -1879,6 +1884,7 @@ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "dev": true, + "peer": true, "engines": { "node": ">=0.12" }, @@ -1918,6 +1924,7 @@ "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", "dev": true, + "peer": true, "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", @@ -1952,6 +1959,7 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, + "peer": true, "engines": { "node": ">=4.0" } @@ -1961,6 +1969,7 @@ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, + "peer": true, "engines": { "node": ">=0.10.0" } @@ -2037,29 +2046,6 @@ "bser": "2.1.1" } }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], - "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - }, - "engines": { - "node": "^12.20 || >= 14.13" - } - }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -2090,6 +2076,7 @@ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", "dev": true, + "peer": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -2119,18 +2106,6 @@ "node": ">= 14" } }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "dev": true, - "dependencies": { - "fetch-blob": "^3.1.2" - }, - "engines": { - "node": ">=12.20.0" - } - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -2260,6 +2235,7 @@ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", "dev": true, + "peer": true, "dependencies": { "whatwg-encoding": "^2.0.0" }, @@ -2278,6 +2254,7 @@ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", "dev": true, + "peer": true, "dependencies": { "@tootallnate/once": "2", "agent-base": "6", @@ -2292,6 +2269,7 @@ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", "dev": true, + "peer": true, "dependencies": { "agent-base": "6", "debug": "4" @@ -2314,6 +2292,7 @@ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dev": true, + "peer": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -2414,7 +2393,8 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "dev": true + "dev": true, + "peer": true }, "node_modules/is-stream": { "version": "2.0.1", @@ -2730,6 +2710,7 @@ "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz", "integrity": "sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==", "dev": true, + "peer": true, "dependencies": { "@jest/environment": "^29.7.0", "@jest/fake-timers": "^29.7.0", @@ -2769,6 +2750,19 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-fixed-jsdom": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/jest-fixed-jsdom/-/jest-fixed-jsdom-0.0.9.tgz", + "integrity": "sha512-KPfqh2+sn5q2B+7LZktwDcwhCpOpUSue8a1I+BcixWLOQoEVyAjAGfH+IYZGoxZsziNojoHGRTC8xRbB1wDD4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "jest-environment-jsdom": ">=28.0.0" + } + }, "node_modules/jest-get-type": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", @@ -3172,6 +3166,7 @@ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==", "dev": true, + "peer": true, "dependencies": { "abab": "^2.0.6", "acorn": "^8.8.1", @@ -3217,6 +3212,7 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", "dev": true, + "peer": true, "dependencies": { "punycode": "^2.1.1" }, @@ -3229,6 +3225,7 @@ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", "dev": true, + "peer": true, "engines": { "node": ">=12" } @@ -3238,6 +3235,7 @@ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", "dev": true, + "peer": true, "dependencies": { "tr46": "^3.0.0", "webidl-conversions": "^7.0.0" @@ -3413,6 +3411,7 @@ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "dev": true, + "peer": true, "engines": { "node": ">= 0.6" } @@ -3422,6 +3421,7 @@ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, + "peer": true, "dependencies": { "mime-db": "1.52.0" }, @@ -3488,25 +3488,6 @@ "node": ">=10.5.0" } }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -3544,7 +3525,8 @@ "version": "2.2.7", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz", "integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==", - "dev": true + "dev": true, + "peer": true }, "node_modules/once": { "version": "1.4.0", @@ -3644,6 +3626,7 @@ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", "dev": true, + "peer": true, "dependencies": { "entities": "^4.4.0" }, @@ -3766,13 +3749,15 @@ "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", - "dev": true + "dev": true, + "peer": true }, "node_modules/punycode": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", "dev": true, + "peer": true, "engines": { "node": ">=6" } @@ -3797,7 +3782,8 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", - "dev": true + "dev": true, + "peer": true }, "node_modules/react-is": { "version": "18.2.0", @@ -3818,7 +3804,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "dev": true + "dev": true, + "peer": true }, "node_modules/resolve": { "version": "1.22.8", @@ -3871,13 +3858,15 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "dev": true, + "peer": true }, "node_modules/saxes": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", "dev": true, + "peer": true, "dependencies": { "xmlchars": "^2.2.0" }, @@ -4070,7 +4059,8 @@ "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "dev": true + "dev": true, + "peer": true }, "node_modules/test-exclude": { "version": "6.0.0", @@ -4086,12 +4076,6 @@ "node": ">=8" } }, - "node_modules/text-encoding-polyfill": { - "version": "0.6.7", - "resolved": "https://registry.npmjs.org/text-encoding-polyfill/-/text-encoding-polyfill-0.6.7.tgz", - "integrity": "sha512-/DZ1XJqhbqRkCop6s9ZFu8JrFRwmVuHg4quIRm+ziFkR3N3ec6ck6yBvJ1GYeEQZhLVwRW0rZE+C3SSJpy0RTg==", - "dev": true - }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -4124,6 +4108,7 @@ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", "dev": true, + "peer": true, "dependencies": { "psl": "^1.1.33", "punycode": "^2.1.1", @@ -4134,11 +4119,6 @@ "node": ">=6" } }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" - }, "node_modules/ts-jest": { "version": "29.1.2", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.2.tgz", @@ -4282,6 +4262,7 @@ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", "dev": true, + "peer": true, "engines": { "node": ">= 4.0.0" } @@ -4321,6 +4302,7 @@ "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", "dev": true, + "peer": true, "dependencies": { "querystringify": "^2.1.1", "requires-port": "^1.0.0" @@ -4345,6 +4327,7 @@ "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", "dev": true, + "peer": true, "dependencies": { "xml-name-validator": "^4.0.0" }, @@ -4361,25 +4344,12 @@ "makeerror": "1.0.12" } }, - "node_modules/web-streams-polyfill": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", - "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" - }, "node_modules/whatwg-encoding": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", "dev": true, + "peer": true, "dependencies": { "iconv-lite": "0.6.3" }, @@ -4387,30 +4357,16 @@ "node": ">=12" } }, - "node_modules/whatwg-fetch": { - "version": "3.6.20", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", - "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", - "dev": true - }, "node_modules/whatwg-mimetype": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", "dev": true, + "peer": true, "engines": { "node": ">=12" } }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -4467,6 +4423,7 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.1.tgz", "integrity": "sha512-4OOseMUq8AzRBI/7SLMUwO+FEDnguetSk7KMb1sHwvF2w2Wv5Hoj0nlifx8vtGsftE/jWHojPy8sMMzYLJ2G/A==", "dev": true, + "peer": true, "engines": { "node": ">=10.0.0" }, @@ -4488,6 +4445,7 @@ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", "dev": true, + "peer": true, "engines": { "node": ">=12" } @@ -4496,7 +4454,8 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "dev": true + "dev": true, + "peer": true }, "node_modules/y18n": { "version": "5.0.8", diff --git a/ecosystem-tests/node-ts-cjs-web/package.json b/ecosystem-tests/node-ts-cjs-web/package.json index 3673778bc..6683953fd 100644 --- a/ecosystem-tests/node-ts-cjs-web/package.json +++ b/ecosystem-tests/node-ts-cjs-web/package.json @@ -9,20 +9,14 @@ }, "dependencies": { "formdata-node": "^4.4.1", - "node-fetch": "^2.6.1", "tsconfig-paths": "^4.0.0" }, "devDependencies": { + "jest-fixed-jsdom": "^0.0.9", "@types/node": "^18.0.0", - "@types/node-fetch": "^2.6.1", "fastest-levenshtein": "^1.0.16", - "formdata-polyfill": "^4.0.10", "jest": "^29.5.0", - "jest-environment-jsdom": "^29.7.0", - "text-encoding-polyfill": "^0.6.7", "ts-jest": "^29.1.0", - "typescript": "4.7.4", - "web-streams-polyfill": "^3.2.1", - "whatwg-fetch": "^3.6.19" + "typescript": "4.7.4" } } diff --git a/ecosystem-tests/node-ts-cjs-web/tests/test-jsdom.ts b/ecosystem-tests/node-ts-cjs-web/tests/test-jsdom.ts index d4619ab0c..d156fcbfd 100644 --- a/ecosystem-tests/node-ts-cjs-web/tests/test-jsdom.ts +++ b/ecosystem-tests/node-ts-cjs-web/tests/test-jsdom.ts @@ -1,12 +1,9 @@ /** - * @jest-environment jsdom + * @jest-environment jest-fixed-jsdom */ -import 'whatwg-fetch'; import OpenAI, { toFile } from 'openai'; import { distance } from 'fastest-levenshtein'; import { ChatCompletion } from 'openai/resources/chat/completions'; -// @ts-ignore -import { TextEncoder } from 'text-encoding-polyfill'; const url = 'https://audio-samples.github.io/samples/mp3/blizzard_biased/sample-1.mp3'; const filename = 'sample-1.mp3'; diff --git a/ecosystem-tests/node-ts-cjs/package-lock.json b/ecosystem-tests/node-ts-cjs/package-lock.json index 2f5374e35..dcbda28e4 100644 --- a/ecosystem-tests/node-ts-cjs/package-lock.json +++ b/ecosystem-tests/node-ts-cjs/package-lock.json @@ -9,19 +9,17 @@ "version": "0.0.1", "dependencies": { "formdata-node": "^4.4.1", - "node-fetch": "^2.6.1", - "tsconfig-paths": "^4.0.0" + "tsconfig-paths": "^4.0.0", + "typescript": "^5.7.3", + "undici": "^7.2.0" }, "devDependencies": { - "@types/node": "20.4.2", - "@types/node-fetch": "^2.6.1", + "@types/node": "^20.14.8", "@types/ws": "^8.5.4", "fastest-levenshtein": "^1.0.16", "jest": "^29.5.0", - "jest-environment-jsdom": "^29.7.0", - "text-encoding-polyfill": "^0.6.7", - "ts-jest": "^29.1.0", - "typescript": "4.7.4" + "jest-fixed-jsdom": "^0.0.9", + "ts-jest": "^29.1.0" } }, "node_modules/@ampproject/remapping": { @@ -1045,6 +1043,7 @@ "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", "dev": true, + "peer": true, "engines": { "node": ">= 10" } @@ -1128,6 +1127,7 @@ "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz", "integrity": "sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==", "dev": true, + "peer": true, "dependencies": { "@types/node": "*", "@types/tough-cookie": "*", @@ -1135,19 +1135,13 @@ } }, "node_modules/@types/node": { - "version": "20.4.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.2.tgz", - "integrity": "sha512-Dd0BYtWgnWJKwO1jkmTrzofjK2QXXcai0dmtzvIBhcA+RsG5h8R3xlyta0kGOZRNfL9GuRtb1knmPEhQrePCEw==", - "dev": true - }, - "node_modules/@types/node-fetch": { - "version": "2.6.11", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz", - "integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==", + "version": "20.17.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.19.tgz", + "integrity": "sha512-LEwC7o1ifqg/6r2gn9Dns0f1rhK+fPFDoMiceTJ6kWmVk6bgXBI/9IOWfVan4WiAavK9pIVWdX0/e3J+eEUh5A==", "dev": true, + "license": "MIT", "dependencies": { - "@types/node": "*", - "form-data": "^4.0.0" + "undici-types": "~6.19.2" } }, "node_modules/@types/stack-utils": { @@ -1160,7 +1154,8 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.3.tgz", "integrity": "sha512-THo502dA5PzG/sfQH+42Lw3fvmYkceefOspdCwpHRul8ik2Jv1K8I5OZz1AT3/rs46kwgMCe9bSBmDLYkkOMGg==", - "dev": true + "dev": true, + "peer": true }, "node_modules/@types/ws": { "version": "8.5.10", @@ -1190,13 +1185,15 @@ "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", - "dev": true + "dev": true, + "peer": true }, "node_modules/acorn": { "version": "8.10.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", "dev": true, + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1209,6 +1206,7 @@ "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", "dev": true, + "peer": true, "dependencies": { "acorn": "^8.1.0", "acorn-walk": "^8.0.2" @@ -1219,6 +1217,7 @@ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", "dev": true, + "peer": true, "engines": { "node": ">=0.4.0" } @@ -1228,6 +1227,7 @@ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", "dev": true, + "peer": true, "dependencies": { "debug": "4" }, @@ -1300,7 +1300,8 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "dev": true, + "peer": true }, "node_modules/babel-jest": { "version": "29.7.0", @@ -1633,6 +1634,7 @@ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "dev": true, + "peer": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -1691,13 +1693,15 @@ "version": "0.5.0", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", - "dev": true + "dev": true, + "peer": true }, "node_modules/cssstyle": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", "dev": true, + "peer": true, "dependencies": { "cssom": "~0.3.6" }, @@ -1709,13 +1713,15 @@ "version": "0.3.8", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", - "dev": true + "dev": true, + "peer": true }, "node_modules/data-urls": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", "dev": true, + "peer": true, "dependencies": { "abab": "^2.0.6", "whatwg-mimetype": "^3.0.0", @@ -1730,6 +1736,7 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", "dev": true, + "peer": true, "dependencies": { "punycode": "^2.1.1" }, @@ -1742,6 +1749,7 @@ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", "dev": true, + "peer": true, "engines": { "node": ">=12" } @@ -1751,6 +1759,7 @@ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", "dev": true, + "peer": true, "dependencies": { "tr46": "^3.0.0", "webidl-conversions": "^7.0.0" @@ -1780,7 +1789,8 @@ "version": "10.4.3", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", - "dev": true + "dev": true, + "peer": true }, "node_modules/dedent": { "version": "1.5.1", @@ -1810,6 +1820,7 @@ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "dev": true, + "peer": true, "engines": { "node": ">=0.4.0" } @@ -1837,6 +1848,7 @@ "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", "dev": true, + "peer": true, "dependencies": { "webidl-conversions": "^7.0.0" }, @@ -1849,6 +1861,7 @@ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", "dev": true, + "peer": true, "engines": { "node": ">=12" } @@ -1882,6 +1895,7 @@ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "dev": true, + "peer": true, "engines": { "node": ">=0.12" }, @@ -1921,6 +1935,7 @@ "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", "dev": true, + "peer": true, "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", @@ -1955,6 +1970,7 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, + "peer": true, "engines": { "node": ">=4.0" } @@ -1964,6 +1980,7 @@ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, + "peer": true, "engines": { "node": ">=0.10.0" } @@ -2070,6 +2087,7 @@ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", "dev": true, + "peer": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -2220,6 +2238,7 @@ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", "dev": true, + "peer": true, "dependencies": { "whatwg-encoding": "^2.0.0" }, @@ -2238,6 +2257,7 @@ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", "dev": true, + "peer": true, "dependencies": { "@tootallnate/once": "2", "agent-base": "6", @@ -2252,6 +2272,7 @@ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", "dev": true, + "peer": true, "dependencies": { "agent-base": "6", "debug": "4" @@ -2274,6 +2295,7 @@ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dev": true, + "peer": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -2374,7 +2396,8 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "dev": true + "dev": true, + "peer": true }, "node_modules/is-stream": { "version": "2.0.1", @@ -2690,6 +2713,7 @@ "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz", "integrity": "sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==", "dev": true, + "peer": true, "dependencies": { "@jest/environment": "^29.7.0", "@jest/fake-timers": "^29.7.0", @@ -2729,6 +2753,19 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-fixed-jsdom": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/jest-fixed-jsdom/-/jest-fixed-jsdom-0.0.9.tgz", + "integrity": "sha512-KPfqh2+sn5q2B+7LZktwDcwhCpOpUSue8a1I+BcixWLOQoEVyAjAGfH+IYZGoxZsziNojoHGRTC8xRbB1wDD4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "jest-environment-jsdom": ">=28.0.0" + } + }, "node_modules/jest-get-type": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", @@ -3132,6 +3169,7 @@ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==", "dev": true, + "peer": true, "dependencies": { "abab": "^2.0.6", "acorn": "^8.8.1", @@ -3177,6 +3215,7 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", "dev": true, + "peer": true, "dependencies": { "punycode": "^2.1.1" }, @@ -3189,6 +3228,7 @@ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", "dev": true, + "peer": true, "engines": { "node": ">=12" } @@ -3198,6 +3238,7 @@ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", "dev": true, + "peer": true, "dependencies": { "tr46": "^3.0.0", "webidl-conversions": "^7.0.0" @@ -3373,6 +3414,7 @@ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "dev": true, + "peer": true, "engines": { "node": ">= 0.6" } @@ -3382,6 +3424,7 @@ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, + "peer": true, "dependencies": { "mime-db": "1.52.0" }, @@ -3448,25 +3491,6 @@ "node": ">=10.5.0" } }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -3504,7 +3528,8 @@ "version": "2.2.7", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz", "integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==", - "dev": true + "dev": true, + "peer": true }, "node_modules/once": { "version": "1.4.0", @@ -3604,6 +3629,7 @@ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", "dev": true, + "peer": true, "dependencies": { "entities": "^4.4.0" }, @@ -3726,13 +3752,15 @@ "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", - "dev": true + "dev": true, + "peer": true }, "node_modules/punycode": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", "dev": true, + "peer": true, "engines": { "node": ">=6" } @@ -3757,7 +3785,8 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", - "dev": true + "dev": true, + "peer": true }, "node_modules/react-is": { "version": "18.2.0", @@ -3778,7 +3807,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "dev": true + "dev": true, + "peer": true }, "node_modules/resolve": { "version": "1.22.8", @@ -3831,13 +3861,15 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "dev": true, + "peer": true }, "node_modules/saxes": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", "dev": true, + "peer": true, "dependencies": { "xmlchars": "^2.2.0" }, @@ -4030,7 +4062,8 @@ "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "dev": true + "dev": true, + "peer": true }, "node_modules/test-exclude": { "version": "6.0.0", @@ -4046,12 +4079,6 @@ "node": ">=8" } }, - "node_modules/text-encoding-polyfill": { - "version": "0.6.7", - "resolved": "https://registry.npmjs.org/text-encoding-polyfill/-/text-encoding-polyfill-0.6.7.tgz", - "integrity": "sha512-/DZ1XJqhbqRkCop6s9ZFu8JrFRwmVuHg4quIRm+ziFkR3N3ec6ck6yBvJ1GYeEQZhLVwRW0rZE+C3SSJpy0RTg==", - "dev": true - }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -4084,6 +4111,7 @@ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", "dev": true, + "peer": true, "dependencies": { "psl": "^1.1.33", "punycode": "^2.1.1", @@ -4094,11 +4122,6 @@ "node": ">=6" } }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" - }, "node_modules/ts-jest": { "version": "29.1.2", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.2.tgz", @@ -4218,23 +4241,40 @@ } }, "node_modules/typescript": { - "version": "4.7.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", - "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", - "dev": true, + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.2.0.tgz", + "integrity": "sha512-klt+0S55GBViA9nsq48/NSCo4YX5mjydjypxD7UmHh/brMu8h/Mhd/F7qAeoH2NOO8SDTk6kjnTFc4WpzmfYpQ==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" } }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" + }, "node_modules/universalify": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", "dev": true, + "peer": true, "engines": { "node": ">= 4.0.0" } @@ -4274,6 +4314,7 @@ "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", "dev": true, + "peer": true, "dependencies": { "querystringify": "^2.1.1", "requires-port": "^1.0.0" @@ -4298,6 +4339,7 @@ "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", "dev": true, + "peer": true, "dependencies": { "xml-name-validator": "^4.0.0" }, @@ -4322,16 +4364,12 @@ "node": ">= 14" } }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" - }, "node_modules/whatwg-encoding": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", "dev": true, + "peer": true, "dependencies": { "iconv-lite": "0.6.3" }, @@ -4344,19 +4382,11 @@ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", "dev": true, + "peer": true, "engines": { "node": ">=12" } }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -4413,6 +4443,7 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.1.tgz", "integrity": "sha512-4OOseMUq8AzRBI/7SLMUwO+FEDnguetSk7KMb1sHwvF2w2Wv5Hoj0nlifx8vtGsftE/jWHojPy8sMMzYLJ2G/A==", "dev": true, + "peer": true, "engines": { "node": ">=10.0.0" }, @@ -4434,6 +4465,7 @@ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", "dev": true, + "peer": true, "engines": { "node": ">=12" } @@ -4442,7 +4474,8 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "dev": true + "dev": true, + "peer": true }, "node_modules/y18n": { "version": "5.0.8", diff --git a/ecosystem-tests/node-ts-cjs/package.json b/ecosystem-tests/node-ts-cjs/package.json index c675b8f31..7946e8eae 100644 --- a/ecosystem-tests/node-ts-cjs/package.json +++ b/ecosystem-tests/node-ts-cjs/package.json @@ -9,21 +9,17 @@ }, "dependencies": { "formdata-node": "^4.4.1", - "node-fetch": "^2.6.1", - "tsconfig-paths": "^4.0.0" + "tsconfig-paths": "^4.0.0", + "typescript": "^5.7.3", + "undici": "^7.2.0" }, "devDependencies": { - "@types/node": "20.4.2", - "@types/node-fetch": "^2.6.1", + "@types/node": "^20.14.8", "@types/ws": "^8.5.4", "fastest-levenshtein": "^1.0.16", "jest": "^29.5.0", - "jest-environment-jsdom": "^29.7.0", - "text-encoding-polyfill": "^0.6.7", - "ts-jest": "^29.1.0", - "typescript": "4.7.4" + "jest-fixed-jsdom": "^0.0.9", + "ts-jest": "^29.1.0" }, - "overrides": { - "@types/node": "20.4.2" - } + "type": "commonjs" } diff --git a/ecosystem-tests/node-ts-cjs/tests/test-jsdom.ts b/ecosystem-tests/node-ts-cjs/tests/test-jsdom.ts index ddf782aee..7910f62bd 100644 --- a/ecosystem-tests/node-ts-cjs/tests/test-jsdom.ts +++ b/ecosystem-tests/node-ts-cjs/tests/test-jsdom.ts @@ -1,11 +1,8 @@ /** - * @jest-environment jsdom + * @jest-environment jest-fixed-jsdom */ import OpenAI, { toFile } from 'openai'; -import fetch from 'node-fetch'; import { distance } from 'fastest-levenshtein'; -// @ts-ignore -import { TextEncoder } from 'text-encoding-polyfill'; const url = 'https://audio-samples.github.io/samples/mp3/blizzard_biased/sample-1.mp3'; const filename = 'sample-1.mp3'; @@ -17,8 +14,6 @@ const model = 'whisper-1'; const client = new OpenAI({ apiKey: process.env['OPENAI_API_KEY'], dangerouslyAllowBrowser: true, - // @ts-expect-error node-fetch types are not compatible - fetch, }); async function typeTests() { @@ -85,7 +80,7 @@ it(`streaming works`, async function () { it.skip('handles builtinFile', async function () { const file = await fetch(url) - .then((x) => x.arrayBuffer()) + .then((x) => x.blob()) .then((x) => new File([x], filename)); const result = await client.audio.transcriptions.create({ file, model }); diff --git a/ecosystem-tests/node-ts-cjs/tests/test-node.ts b/ecosystem-tests/node-ts-cjs/tests/test-node.ts index 2e27de0f3..ebdc4e091 100644 --- a/ecosystem-tests/node-ts-cjs/tests/test-node.ts +++ b/ecosystem-tests/node-ts-cjs/tests/test-node.ts @@ -1,11 +1,11 @@ import OpenAI, { toFile } from 'openai'; import { TranscriptionCreateParams } from 'openai/resources/audio/transcriptions'; -import fetch from 'node-fetch'; +import * as undici from 'undici'; import { File as FormDataFile, Blob as FormDataBlob } from 'formdata-node'; import * as fs from 'fs'; import { distance } from 'fastest-levenshtein'; import { ChatCompletion } from 'openai/resources/chat/completions'; -import type { ReadableStream as WebReadableStream } from 'node:stream/web'; +import { File } from 'node:buffer'; const url = 'https://audio-samples.github.io/samples/mp3/blizzard_biased/sample-1.mp3'; const filename = 'sample-1.mp3'; @@ -65,12 +65,9 @@ it(`raw response`, async function () { }) .asResponse(); - // As far as I can tell, we need to cast because the jest-jsdom types - const body = response.body as WebReadableStream; - const decoder = new TextDecoder(); const chunks: string[] = []; - for await (const chunk of body) { + for await (const chunk of response.body!) { chunks.push(decoder.decode(chunk)); } @@ -91,15 +88,18 @@ it(`streaming works`, async function () { expect(chunks.map((c) => c.choices[0]?.delta.content || '').join('')).toBeSimilarTo('This is a test', 10); }); -it('handles formdata-node File', async function () { - const file = await fetch(url) - .then((x) => x.arrayBuffer()) - .then((x) => new FormDataFile([x], filename)); - - const params: TranscriptionCreateParams = { file, model }; - - const result = await client.audio.transcriptions.create(params); - expect(result.text).toBeSimilarTo(correctAnswer, 12); +test(`proxied request`, async function () { + const dispatcher = new undici.ProxyAgent(process.env['ECOSYSTEM_TESTS_PROXY']!); + const client = new OpenAI({ + fetchOptions: { + dispatcher, + }, + }); + const completion = await client.chat.completions.create({ + model: 'gpt-4', + messages: [{ role: 'user', content: 'Say this is a test' }], + }); + expect(completion.choices[0]?.message?.content).toBeSimilarTo('This is a test', 10); }); it('handles builtinFile', async function () { @@ -175,4 +175,17 @@ describe('toFile', () => { }); expect(result.filename).toEqual('finetune.jsonl'); }); + + it('handles formdata-node File', async function () { + const file = await fetch(url) + .then((x) => x.arrayBuffer()) + .then((x) => toFile(new FormDataFile([x], filename))); + + expect(file.name).toEqual(filename); + + const params: TranscriptionCreateParams = { file, model }; + + const result = await client.audio.transcriptions.create(params); + expect(result.text).toBeSimilarTo(correctAnswer, 12); + }); }); diff --git a/ecosystem-tests/node-ts-cjs/tsconfig.json b/ecosystem-tests/node-ts-cjs/tsconfig.json index d1ad90efd..a7b5e119f 100644 --- a/ecosystem-tests/node-ts-cjs/tsconfig.json +++ b/ecosystem-tests/node-ts-cjs/tsconfig.json @@ -11,6 +11,7 @@ "target": "ES2015", "lib": ["ES2015"], "jsx": "react", + "types": ["node", "jest"], /* Modules */ "module": "commonjs", diff --git a/ecosystem-tests/node-ts-cjs/tsconfig.nodenext.json b/ecosystem-tests/node-ts-cjs/tsconfig.nodenext.json index 0efe77b4e..eb9e3ec20 100644 --- a/ecosystem-tests/node-ts-cjs/tsconfig.nodenext.json +++ b/ecosystem-tests/node-ts-cjs/tsconfig.nodenext.json @@ -11,9 +11,10 @@ "target": "ES2015", "lib": ["ES2015"], "jsx": "react", + "types": ["node", "jest"], /* Modules */ - "module": "commonjs", + "module": "NodeNext", "rootDir": "./", "moduleResolution": "NodeNext", "baseUrl": "./", diff --git a/ecosystem-tests/node-ts-esm-auto/package-lock.json b/ecosystem-tests/node-ts-esm-auto/package-lock.json index 3e4438d05..729b0f2e9 100644 --- a/ecosystem-tests/node-ts-esm-auto/package-lock.json +++ b/ecosystem-tests/node-ts-esm-auto/package-lock.json @@ -8,8 +8,7 @@ "name": "node-ts-esm-auto", "version": "0.0.1", "dependencies": { - "formdata-node": "^5.0.1", - "node-fetch": "^3.0.0" + "formdata-node": "^5.0.1" }, "devDependencies": { "@types/node": "^20.3.1", @@ -1653,14 +1652,6 @@ "node": ">= 8" } }, - "node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", - "engines": { - "node": ">= 12" - } - }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -1864,36 +1855,6 @@ "bser": "2.1.1" } }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], - "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - }, - "engines": { - "node": "^12.20 || >= 14.13" - } - }, - "node_modules/fetch-blob/node_modules/web-streams-polyfill": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", - "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==", - "engines": { - "node": ">= 8" - } - }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -1931,17 +1892,6 @@ "node": ">= 14.17" } }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "dependencies": { - "fetch-blob": "^3.1.2" - }, - "engines": { - "node": ">=12.20.0" - } - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3108,23 +3058,6 @@ "node": ">=10.5.0" } }, - "node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", diff --git a/ecosystem-tests/node-ts-esm-auto/package.json b/ecosystem-tests/node-ts-esm-auto/package.json index 7c227166c..28cf518c7 100644 --- a/ecosystem-tests/node-ts-esm-auto/package.json +++ b/ecosystem-tests/node-ts-esm-auto/package.json @@ -9,8 +9,7 @@ "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js" }, "dependencies": { - "formdata-node": "^5.0.1", - "node-fetch": "^3.0.0" + "formdata-node": "^5.0.1" }, "devDependencies": { "@types/node": "^20.3.1", diff --git a/ecosystem-tests/node-ts-esm-auto/tests/test.ts b/ecosystem-tests/node-ts-esm-auto/tests/test.ts index ae5db37e6..855a925c5 100644 --- a/ecosystem-tests/node-ts-esm-auto/tests/test.ts +++ b/ecosystem-tests/node-ts-esm-auto/tests/test.ts @@ -1,10 +1,10 @@ import OpenAI, { toFile } from 'openai'; import { TranscriptionCreateParams } from 'openai/resources/audio/transcriptions'; -import fetch from 'node-fetch'; import { File as FormDataFile, Blob as FormDataBlob } from 'formdata-node'; import * as fs from 'fs'; import { distance } from 'fastest-levenshtein'; import { ChatCompletion } from 'openai/resources/chat/completions'; +import { File } from 'node:buffer'; const url = 'https://audio-samples.github.io/samples/mp3/blizzard_biased/sample-1.mp3'; const filename = 'sample-1.mp3'; @@ -87,17 +87,6 @@ it(`streaming works`, async function () { expect(chunks.map((c) => c.choices[0]?.delta.content || '').join('')).toBeSimilarTo('This is a test', 10); }); -it('handles formdata-node File', async function () { - const file = await fetch(url) - .then((x) => x.arrayBuffer()) - .then((x) => new FormDataFile([x], filename)); - - const params: TranscriptionCreateParams = { file, model }; - - const result = await client.audio.transcriptions.create(params); - expect(result.text).toBeSimilarTo(correctAnswer, 12); -}); - it('handles builtinFile', async function () { const file = await fetch(url) .then((x) => x.arrayBuffer()) @@ -171,4 +160,17 @@ describe('toFile', () => { }); expect(result.filename).toEqual('finetune.jsonl'); }); + + it('handles formdata-node File', async function () { + const file = await fetch(url) + .then((x) => x.arrayBuffer()) + .then((x) => toFile(new FormDataFile([x], filename))); + + expect(file.name).toEqual(filename); + + const params: TranscriptionCreateParams = { file, model }; + + const result = await client.audio.transcriptions.create(params); + expect(result.text).toBeSimilarTo(correctAnswer, 12); + }); }); diff --git a/ecosystem-tests/node-ts-esm-web/package-lock.json b/ecosystem-tests/node-ts-esm-web/package-lock.json index 118bf0909..695441ae3 100644 --- a/ecosystem-tests/node-ts-esm-web/package-lock.json +++ b/ecosystem-tests/node-ts-esm-web/package-lock.json @@ -8,8 +8,7 @@ "name": "node-ts-esm-web", "version": "0.0.1", "dependencies": { - "formdata-node": "^5.0.1", - "node-fetch": "^3.0.0" + "formdata-node": "^5.0.1" }, "devDependencies": { "@types/node": "^20.3.1", @@ -1653,14 +1652,6 @@ "node": ">= 8" } }, - "node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", - "engines": { - "node": ">= 12" - } - }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -1864,36 +1855,6 @@ "bser": "2.1.1" } }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], - "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - }, - "engines": { - "node": "^12.20 || >= 14.13" - } - }, - "node_modules/fetch-blob/node_modules/web-streams-polyfill": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", - "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==", - "engines": { - "node": ">= 8" - } - }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -1931,17 +1892,6 @@ "node": ">= 14.17" } }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "dependencies": { - "fetch-blob": "^3.1.2" - }, - "engines": { - "node": ">=12.20.0" - } - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3108,23 +3058,6 @@ "node": ">=10.5.0" } }, - "node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", diff --git a/ecosystem-tests/node-ts-esm-web/package.json b/ecosystem-tests/node-ts-esm-web/package.json index 97a2309f8..f458a97a0 100644 --- a/ecosystem-tests/node-ts-esm-web/package.json +++ b/ecosystem-tests/node-ts-esm-web/package.json @@ -9,8 +9,7 @@ "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js" }, "dependencies": { - "formdata-node": "^5.0.1", - "node-fetch": "^3.0.0" + "formdata-node": "^5.0.1" }, "devDependencies": { "@types/node": "^20.3.1", diff --git a/ecosystem-tests/node-ts-esm/package-lock.json b/ecosystem-tests/node-ts-esm/package-lock.json index dd1b7d3c3..7b167e3e7 100644 --- a/ecosystem-tests/node-ts-esm/package-lock.json +++ b/ecosystem-tests/node-ts-esm/package-lock.json @@ -9,8 +9,7 @@ "version": "0.0.1", "dependencies": { "formdata-node": "^5.0.1", - "import-in-the-middle": "^1.11.2", - "node-fetch": "^3.0.0" + "import-in-the-middle": "^1.11.2" }, "devDependencies": { "@types/node": "^20.3.1", @@ -1661,14 +1660,6 @@ "node": ">= 8" } }, - "node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", - "engines": { - "node": ">= 12" - } - }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -1872,36 +1863,6 @@ "bser": "2.1.1" } }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], - "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - }, - "engines": { - "node": "^12.20 || >= 14.13" - } - }, - "node_modules/fetch-blob/node_modules/web-streams-polyfill": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", - "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==", - "engines": { - "node": ">= 8" - } - }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -1939,17 +1900,6 @@ "node": ">= 14.17" } }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "dependencies": { - "fetch-blob": "^3.1.2" - }, - "engines": { - "node": ">=12.20.0" - } - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3134,23 +3084,6 @@ "node": ">=10.5.0" } }, - "node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", diff --git a/ecosystem-tests/node-ts-esm/package.json b/ecosystem-tests/node-ts-esm/package.json index 68239e747..198615c6f 100644 --- a/ecosystem-tests/node-ts-esm/package.json +++ b/ecosystem-tests/node-ts-esm/package.json @@ -10,8 +10,7 @@ }, "dependencies": { "formdata-node": "^5.0.1", - "import-in-the-middle": "^1.11.2", - "node-fetch": "^3.0.0" + "import-in-the-middle": "^1.11.2" }, "devDependencies": { "@types/node": "^20.3.1", diff --git a/ecosystem-tests/node-ts-esm/tests/test-esnext.ts b/ecosystem-tests/node-ts-esm/tests/test-esnext.ts index a9a7c21f5..0b34026fa 100644 --- a/ecosystem-tests/node-ts-esm/tests/test-esnext.ts +++ b/ecosystem-tests/node-ts-esm/tests/test-esnext.ts @@ -51,7 +51,6 @@ it(`raw response`, async function () { }) .asResponse(); - // test that we can use node-fetch Response API const chunks: string[] = []; if (!response.body) throw new Error(`expected response.body to be defined`); diff --git a/ecosystem-tests/node-ts-esm/tests/test.ts b/ecosystem-tests/node-ts-esm/tests/test.ts index d3f15f9fb..8bbabca11 100644 --- a/ecosystem-tests/node-ts-esm/tests/test.ts +++ b/ecosystem-tests/node-ts-esm/tests/test.ts @@ -1,6 +1,5 @@ import OpenAI, { toFile } from 'openai'; import { TranscriptionCreateParams } from 'openai/resources/audio/transcriptions'; -import fetch from 'node-fetch'; import { File as FormDataFile, Blob as FormDataBlob } from 'formdata-node'; import * as fs from 'fs'; import { distance } from 'fastest-levenshtein'; @@ -68,17 +67,6 @@ it(`streaming works`, async function () { expect(chunks.map((c) => c.choices[0]?.delta.content || '').join('')).toBeSimilarTo('This is a test', 10); }); -it('handles formdata-node File', async function () { - const file = await fetch(url) - .then((x) => x.arrayBuffer()) - .then((x) => new FormDataFile([x], filename)); - - const params: TranscriptionCreateParams = { file, model }; - - const result = await client.audio.transcriptions.create(params); - expect(result.text).toBeSimilarTo(correctAnswer, 12); -}); - if (typeof File !== 'undefined') { it('handles builtinFile', async function () { const file = await fetch(url) @@ -154,4 +142,17 @@ describe('toFile', () => { }); expect(result.filename).toEqual('finetune.jsonl'); }); + + it('handles formdata-node File', async function () { + const file = await fetch(url) + .then((x) => x.arrayBuffer()) + .then((x) => toFile(new FormDataFile([x], filename))); + + expect(file.name).toEqual(filename); + + const params: TranscriptionCreateParams = { file, model }; + + const result = await client.audio.transcriptions.create(params); + expect(result.text).toBeSimilarTo(correctAnswer, 12); + }); }); diff --git a/ecosystem-tests/node-ts4.5-jest28/package-lock.json b/ecosystem-tests/node-ts4.5-jest28/package-lock.json index 4b98e5836..1944471b4 100644 --- a/ecosystem-tests/node-ts4.5-jest28/package-lock.json +++ b/ecosystem-tests/node-ts4.5-jest28/package-lock.json @@ -9,13 +9,11 @@ "version": "0.0.1", "dependencies": { "formdata-node": "^4.4.1", - "node-fetch": "^2.6.1", "tsconfig-paths": "^4.0.0" }, "devDependencies": { "@types/jest": "27.5.2", "@types/node": "20.11.20", - "@types/node-fetch": "^2.6.1", "@types/ws": "^8.5.4", "fastest-levenshtein": "^1.0.16", "jest": "28.1.3", @@ -1089,16 +1087,6 @@ "undici-types": "~5.26.4" } }, - "node_modules/@types/node-fetch": { - "version": "2.6.11", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz", - "integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==", - "dev": true, - "dependencies": { - "@types/node": "*", - "form-data": "^4.0.0" - } - }, "node_modules/@types/prettier": { "version": "2.7.3", "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz", @@ -1196,12 +1184,6 @@ "sprintf-js": "~1.0.2" } }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true - }, "node_modules/babel-jest": { "version": "28.1.3", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-28.1.3.tgz", @@ -1515,18 +1497,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1585,15 +1555,6 @@ "node": ">=0.10.0" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -1854,20 +1815,6 @@ "node": ">=8" } }, - "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/formdata-node": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", @@ -3287,27 +3234,6 @@ "node": ">=8.6" } }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -3367,44 +3293,6 @@ "node": ">=10.5.0" } }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-fetch/node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" - }, - "node_modules/node-fetch/node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" - }, - "node_modules/node-fetch/node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", diff --git a/ecosystem-tests/node-ts4.5-jest28/package.json b/ecosystem-tests/node-ts4.5-jest28/package.json index c378a6fdc..a083e09f3 100644 --- a/ecosystem-tests/node-ts4.5-jest28/package.json +++ b/ecosystem-tests/node-ts4.5-jest28/package.json @@ -9,12 +9,10 @@ }, "dependencies": { "formdata-node": "^4.4.1", - "node-fetch": "^2.6.1", "tsconfig-paths": "^4.0.0" }, "devDependencies": { "@types/node": "20.11.20", - "@types/node-fetch": "^2.6.1", "@types/jest": "27.5.2", "@types/ws": "^8.5.4", "fastest-levenshtein": "^1.0.16", diff --git a/ecosystem-tests/node-ts4.5-jest28/tests/test.ts b/ecosystem-tests/node-ts4.5-jest28/tests/test.ts index ae5db37e6..855a925c5 100644 --- a/ecosystem-tests/node-ts4.5-jest28/tests/test.ts +++ b/ecosystem-tests/node-ts4.5-jest28/tests/test.ts @@ -1,10 +1,10 @@ import OpenAI, { toFile } from 'openai'; import { TranscriptionCreateParams } from 'openai/resources/audio/transcriptions'; -import fetch from 'node-fetch'; import { File as FormDataFile, Blob as FormDataBlob } from 'formdata-node'; import * as fs from 'fs'; import { distance } from 'fastest-levenshtein'; import { ChatCompletion } from 'openai/resources/chat/completions'; +import { File } from 'node:buffer'; const url = 'https://audio-samples.github.io/samples/mp3/blizzard_biased/sample-1.mp3'; const filename = 'sample-1.mp3'; @@ -87,17 +87,6 @@ it(`streaming works`, async function () { expect(chunks.map((c) => c.choices[0]?.delta.content || '').join('')).toBeSimilarTo('This is a test', 10); }); -it('handles formdata-node File', async function () { - const file = await fetch(url) - .then((x) => x.arrayBuffer()) - .then((x) => new FormDataFile([x], filename)); - - const params: TranscriptionCreateParams = { file, model }; - - const result = await client.audio.transcriptions.create(params); - expect(result.text).toBeSimilarTo(correctAnswer, 12); -}); - it('handles builtinFile', async function () { const file = await fetch(url) .then((x) => x.arrayBuffer()) @@ -171,4 +160,17 @@ describe('toFile', () => { }); expect(result.filename).toEqual('finetune.jsonl'); }); + + it('handles formdata-node File', async function () { + const file = await fetch(url) + .then((x) => x.arrayBuffer()) + .then((x) => toFile(new FormDataFile([x], filename))); + + expect(file.name).toEqual(filename); + + const params: TranscriptionCreateParams = { file, model }; + + const result = await client.audio.transcriptions.create(params); + expect(result.text).toBeSimilarTo(correctAnswer, 12); + }); }); diff --git a/ecosystem-tests/proxy.ts b/ecosystem-tests/proxy.ts new file mode 100644 index 000000000..ab05685a3 --- /dev/null +++ b/ecosystem-tests/proxy.ts @@ -0,0 +1,27 @@ +import { createServer } from 'http'; +import { connect } from 'net'; + +async function startProxy() { + const proxy = createServer((_req, res) => { + res.end(); + }); + + proxy.on('connect', (req, clientSocket, head) => { + const serverSocket = connect(443, 'api.openai.com', () => { + clientSocket.write( + 'HTTP/1.1 200 Connection Established\r\n' + 'Proxy-agent: Node.js-Proxy\r\n' + '\r\n', + ); + serverSocket.write(head); + serverSocket.pipe(clientSocket); + clientSocket.pipe(serverSocket); + }); + }); + + await new Promise((resolve) => proxy.listen(0, '127.0.0.1', resolve)); + + console.log(proxy.address()!.toString()) + + return () => { + proxy.close() + } +} diff --git a/ecosystem-tests/vercel-edge/tests/test.ts b/ecosystem-tests/vercel-edge/tests/test.ts index 36a7ea0bf..02414ff7b 100644 --- a/ecosystem-tests/vercel-edge/tests/test.ts +++ b/ecosystem-tests/vercel-edge/tests/test.ts @@ -1,5 +1,3 @@ -import fetch from 'node-fetch'; - const baseUrl = process.env.TEST_BASE_URL || 'http://localhost:3000'; console.log(baseUrl); @@ -18,3 +16,6 @@ it( }, 3 * 60000, ); + +// make isolatedModules happy +export {}; diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 000000000..e44b82f24 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,42 @@ +// @ts-check +import tseslint from 'typescript-eslint'; +import unusedImports from 'eslint-plugin-unused-imports'; +import prettier from 'eslint-plugin-prettier'; + +export default tseslint.config( + { + languageOptions: { + parser: tseslint.parser, + parserOptions: { sourceType: 'module' }, + }, + files: ['**/*.ts', '**/*.mts', '**/*.cts', '**/*.js', '**/*.mjs', '**/*.cjs'], + ignores: ['dist/**', 'ecosystem-tests/**'], + plugins: { + '@typescript-eslint': tseslint.plugin, + 'unused-imports': unusedImports, + prettier, + }, + rules: { + 'no-unused-vars': 'off', + 'prettier/prettier': 'error', + 'unused-imports/no-unused-imports': 'error', + 'no-restricted-imports': [ + 'error', + { + patterns: [ + { + regex: '^openai(/.*)?', + message: 'Use a relative import, not a package import.', + }, + ], + }, + ], + }, + }, + { + files: ['tests/**', 'examples/**'], + rules: { + 'no-restricted-imports': 'off', + }, + }, +); diff --git a/examples/azure.ts b/examples/azure/chat.ts similarity index 91% rename from examples/azure.ts rename to examples/azure/chat.ts index 5fe1718fa..46df820f8 100755 --- a/examples/azure.ts +++ b/examples/azure/chat.ts @@ -2,6 +2,7 @@ import { AzureOpenAI } from 'openai'; import { getBearerTokenProvider, DefaultAzureCredential } from '@azure/identity'; +import 'dotenv/config'; // Corresponds to your Model deployment within your OpenAI resource, e.g. gpt-4-1106-preview // Navigate to the Azure OpenAI Studio to deploy a model. @@ -13,7 +14,7 @@ const azureADTokenProvider = getBearerTokenProvider(credential, scope); // Make sure to set AZURE_OPENAI_ENDPOINT with the endpoint of your Azure resource. // You can find it in the Azure Portal. -const openai = new AzureOpenAI({ azureADTokenProvider }); +const openai = new AzureOpenAI({ azureADTokenProvider, apiVersion: '2024-10-01-preview' }); async function main() { console.log('Non-streaming:'); diff --git a/examples/azure/realtime/websocket.ts b/examples/azure/realtime/websocket.ts new file mode 100644 index 000000000..bec74e654 --- /dev/null +++ b/examples/azure/realtime/websocket.ts @@ -0,0 +1,60 @@ +import { OpenAIRealtimeWebSocket } from 'openai/beta/realtime/websocket'; +import { AzureOpenAI } from 'openai'; +import { DefaultAzureCredential, getBearerTokenProvider } from '@azure/identity'; +import 'dotenv/config'; + +async function main() { + const cred = new DefaultAzureCredential(); + const scope = 'https://cognitiveservices.azure.com/.default'; + const deploymentName = 'gpt-4o-realtime-preview-1001'; + const azureADTokenProvider = getBearerTokenProvider(cred, scope); + const client = new AzureOpenAI({ + azureADTokenProvider, + apiVersion: '2024-10-01-preview', + deployment: deploymentName, + }); + const rt = await OpenAIRealtimeWebSocket.azure(client); + + // access the underlying `ws.WebSocket` instance + rt.socket.addEventListener('open', () => { + console.log('Connection opened!'); + rt.send({ + type: 'session.update', + session: { + modalities: ['text'], + model: 'gpt-4o-realtime-preview', + }, + }); + + rt.send({ + type: 'conversation.item.create', + item: { + type: 'message', + role: 'user', + content: [{ type: 'input_text', text: 'Say a couple paragraphs!' }], + }, + }); + + rt.send({ type: 'response.create' }); + }); + + rt.on('error', (err) => { + // in a real world scenario this should be logged somewhere as you + // likely want to continue procesing events regardless of any errors + throw err; + }); + + rt.on('session.created', (event) => { + console.log('session created!', event.session); + console.log(); + }); + + rt.on('response.text.delta', (event) => process.stdout.write(event.delta)); + rt.on('response.text.done', () => console.log()); + + rt.on('response.done', () => rt.close()); + + rt.socket.addEventListener('close', () => console.log('\nConnection closed!')); +} + +main(); diff --git a/examples/azure/realtime/ws.ts b/examples/azure/realtime/ws.ts new file mode 100644 index 000000000..6ab7b742a --- /dev/null +++ b/examples/azure/realtime/ws.ts @@ -0,0 +1,60 @@ +import { DefaultAzureCredential, getBearerTokenProvider } from '@azure/identity'; +import { OpenAIRealtimeWS } from 'openai/beta/realtime/ws'; +import { AzureOpenAI } from 'openai'; +import 'dotenv/config'; + +async function main() { + const cred = new DefaultAzureCredential(); + const scope = 'https://cognitiveservices.azure.com/.default'; + const deploymentName = 'gpt-4o-realtime-preview-1001'; + const azureADTokenProvider = getBearerTokenProvider(cred, scope); + const client = new AzureOpenAI({ + azureADTokenProvider, + apiVersion: '2024-10-01-preview', + deployment: deploymentName, + }); + const rt = await OpenAIRealtimeWS.azure(client); + + // access the underlying `ws.WebSocket` instance + rt.socket.on('open', () => { + console.log('Connection opened!'); + rt.send({ + type: 'session.update', + session: { + modalities: ['text'], + model: 'gpt-4o-realtime-preview', + }, + }); + + rt.send({ + type: 'conversation.item.create', + item: { + type: 'message', + role: 'user', + content: [{ type: 'input_text', text: 'Say a couple paragraphs!' }], + }, + }); + + rt.send({ type: 'response.create' }); + }); + + rt.on('error', (err) => { + // in a real world scenario this should be logged somewhere as you + // likely want to continue procesing events regardless of any errors + throw err; + }); + + rt.on('session.created', (event) => { + console.log('session created!', event.session); + console.log(); + }); + + rt.on('response.text.delta', (event) => process.stdout.write(event.delta)); + rt.on('response.text.done', () => console.log()); + + rt.on('response.done', () => rt.close()); + + rt.socket.on('close', () => console.log('\nConnection closed!')); +} + +main(); diff --git a/examples/package.json b/examples/package.json index c8a5f7087..65b3216b2 100644 --- a/examples/package.json +++ b/examples/package.json @@ -6,6 +6,8 @@ "license": "MIT", "private": true, "dependencies": { + "@azure/identity": "^4.2.0", + "dotenv": "^16.4.7", "express": "^4.18.2", "next": "^14.1.1", "openai": "file:..", @@ -14,6 +16,7 @@ }, "devDependencies": { "@types/body-parser": "^1.19.3", - "@types/express": "^4.17.19" + "@types/express": "^4.17.19", + "@types/web": "^0.0.194" } } diff --git a/examples/realtime/websocket.ts b/examples/realtime/websocket.ts new file mode 100644 index 000000000..0da131bc3 --- /dev/null +++ b/examples/realtime/websocket.ts @@ -0,0 +1,48 @@ +import { OpenAIRealtimeWebSocket } from 'openai/beta/realtime/websocket'; + +async function main() { + const rt = new OpenAIRealtimeWebSocket({ model: 'gpt-4o-realtime-preview-2024-12-17' }); + + // access the underlying `ws.WebSocket` instance + rt.socket.addEventListener('open', () => { + console.log('Connection opened!'); + rt.send({ + type: 'session.update', + session: { + modalities: ['text'], + model: 'gpt-4o-realtime-preview', + }, + }); + + rt.send({ + type: 'conversation.item.create', + item: { + type: 'message', + role: 'user', + content: [{ type: 'input_text', text: 'Say a couple paragraphs!' }], + }, + }); + + rt.send({ type: 'response.create' }); + }); + + rt.on('error', (err) => { + // in a real world scenario this should be logged somewhere as you + // likely want to continue procesing events regardless of any errors + throw err; + }); + + rt.on('session.created', (event) => { + console.log('session created!', event.session); + console.log(); + }); + + rt.on('response.text.delta', (event) => process.stdout.write(event.delta)); + rt.on('response.text.done', () => console.log()); + + rt.on('response.done', () => rt.close()); + + rt.socket.addEventListener('close', () => console.log('\nConnection closed!')); +} + +main(); diff --git a/examples/realtime/ws.ts b/examples/realtime/ws.ts new file mode 100644 index 000000000..08c6fbcb6 --- /dev/null +++ b/examples/realtime/ws.ts @@ -0,0 +1,48 @@ +import { OpenAIRealtimeWS } from 'openai/beta/realtime/ws'; + +async function main() { + const rt = new OpenAIRealtimeWS({ model: 'gpt-4o-realtime-preview-2024-12-17' }); + + // access the underlying `ws.WebSocket` instance + rt.socket.on('open', () => { + console.log('Connection opened!'); + rt.send({ + type: 'session.update', + session: { + modalities: ['text'], + model: 'gpt-4o-realtime-preview', + }, + }); + + rt.send({ + type: 'conversation.item.create', + item: { + type: 'message', + role: 'user', + content: [{ type: 'input_text', text: 'Say a couple paragraphs!' }], + }, + }); + + rt.send({ type: 'response.create' }); + }); + + rt.on('error', (err) => { + // in a real world scenario this should be logged somewhere as you + // likely want to continue procesing events regardless of any errors + throw err; + }); + + rt.on('session.created', (event) => { + console.log('session created!', event.session); + console.log(); + }); + + rt.on('response.text.delta', (event) => process.stdout.write(event.delta)); + rt.on('response.text.done', () => console.log()); + + rt.on('response.done', () => rt.close()); + + rt.socket.on('close', () => console.log('\nConnection closed!')); +} + +main(); diff --git a/examples/stream-to-client-browser.ts b/examples/stream-to-client-browser.ts index 4430ccc68..4d9697505 100755 --- a/examples/stream-to-client-browser.ts +++ b/examples/stream-to-client-browser.ts @@ -5,10 +5,8 @@ * for easy demo purposes, but simulating use in the browser. * * To run it in a browser application, copy/paste it into a frontend application, - * remove the 'node-fetch' import, and replace `process.stdout.write` with - * a console.log or UI display. + * and replace `process.stdout.write` with a console.log or UI display. */ -import fetch from 'node-fetch'; import { ChatCompletionStream } from 'openai/lib/ChatCompletionStream'; fetch('http://localhost:3000', { diff --git a/examples/tool-call-helpers-zod.ts b/examples/tool-call-helpers-zod.ts index 700f401a6..0d31b4933 100755 --- a/examples/tool-call-helpers-zod.ts +++ b/examples/tool-call-helpers-zod.ts @@ -1,10 +1,8 @@ #!/usr/bin/env -S npm run tsn -T import OpenAI from 'openai'; -import { RunnableToolFunctionWithParse } from 'openai/lib/RunnableFunction'; -import { JSONSchema } from 'openai/lib/jsonschema'; -import { ZodSchema, z } from 'zod'; -import { zodToJsonSchema } from 'zod-to-json-schema'; +import { zodFunction } from 'openai/helpers/zod'; +import { z } from 'zod'; // gets API Key from environment variable OPENAI_API_KEY const openai = new OpenAI(); @@ -42,18 +40,21 @@ async function main() { stream: true, tools: [ zodFunction({ + name: 'listBooks', function: listBooks, - schema: ListParams, + parameters: ListParams, description: 'List queries books by genre, and returns a list of names of books', }), zodFunction({ + name: 'searchBooks', function: searchBooks, - schema: SearchParams, + parameters: SearchParams, description: 'Search queries books by their name and returns a list of book names and their ids', }), zodFunction({ + name: 'getBook', function: getBook, - schema: GetParams, + parameters: GetParams, description: "Get returns a book's detailed information based on the id of the book. Note that this does not accept names, and only IDs, which you can get by using search.", }), @@ -108,37 +109,4 @@ But Kya is not what they say. A born naturalist with just one day of school, she }, ]; -/** - * A generic utility function that returns a RunnableFunction - * you can pass to `.runTools()`, - * with a fully validated, typesafe parameters schema. - * - * You are encouraged to copy/paste this into your codebase! - */ -function zodFunction({ - function: fn, - schema, - description = '', - name, -}: { - function: (args: T) => Promise; - schema: ZodSchema; - description?: string; - name?: string; -}): RunnableToolFunctionWithParse { - return { - type: 'function', - function: { - function: fn, - name: name ?? fn.name, - description: description, - parameters: zodToJsonSchema(schema) as JSONSchema, - parse(input: string): T { - const obj = JSON.parse(input); - return schema.parse(obj); - }, - }, - }; -} - main(); diff --git a/examples/types.ts b/examples/types.ts index 482e1f6be..e9de3911c 100755 --- a/examples/types.ts +++ b/examples/types.ts @@ -7,7 +7,7 @@ const openai = new OpenAI(); async function main() { // Explicit non streaming params type: - const params: OpenAI.Chat.CompletionCreateParams = { + const params: OpenAI.Chat.ChatCompletionCreateParams = { model: 'gpt-4', messages: [{ role: 'user', content: 'Say this is a test!' }], }; @@ -15,7 +15,7 @@ async function main() { console.log(completion.choices[0]?.message?.content); // Explicit streaming params type: - const streaming_params: OpenAI.Chat.CompletionCreateParams = { + const streaming_params: OpenAI.Chat.ChatCompletionCreateParams = { model: 'gpt-4', messages: [{ role: 'user', content: 'Say this is a test!' }], stream: true, diff --git a/helpers.md b/helpers.md index abf980c82..f66ebb39c 100644 --- a/helpers.md +++ b/helpers.md @@ -49,7 +49,7 @@ if (message?.parsed) { The `.parse()` method will also automatically parse `function` tool calls if: -- You use the `zodFunctionTool()` helper method +- You use the `zodFunction()` helper method - You mark your tool schema with `"strict": True` For example: @@ -226,7 +226,7 @@ on in the documentation page [Message](https://platform.openai.com/docs/api-refe ```ts .on('textCreated', (content: Text) => ...) -.on('textDelta', (delta: RunStepDelta, snapshot: Text) => ...) +.on('textDelta', (delta: TextDelta, snapshot: Text) => ...) .on('textDone', (content: Text, snapshot: Message) => ...) ``` @@ -353,8 +353,6 @@ chat completion request, not for the entire call run. See an example of automated function calls in action in [`examples/function-call-helpers.ts`](examples/function-call-helpers.ts). -Note, `runFunctions` was also previously available, but has been deprecated in favor of `runTools`. - ### Chat Events #### `.on('connect', () => …)` diff --git a/jsr.json b/jsr.json index bc854dbcd..dca12b1ff 100644 --- a/jsr.json +++ b/jsr.json @@ -1,7 +1,14 @@ { "name": "@openai/openai", "version": "5.0.0-alpha.0", - "exports": "./index.ts", + "exports": { + ".": "./index.ts", + "./helpers/zod": "./helpers/zod.ts", + "./beta/realtime/websocket": "./beta/realtime/websocket.ts" + }, + "imports": { + "zod": "npm:zod@3" + }, "publish": { "exclude": [ "!." diff --git a/package.json b/package.json index c05351d5f..15e5c4143 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openai", - "version": "5.0.0-alpha.0", + "version": "5.0.0-beta.0", "description": "The official TypeScript library for the OpenAI API", "author": "OpenAI ", "types": "dist/index.d.ts", @@ -29,12 +29,15 @@ "@swc/core": "^1.3.102", "@swc/jest": "^0.2.29", "@types/jest": "^29.4.0", + "@types/ws": "^8.5.13", "@types/node": "^20.17.6", - "@typescript-eslint/eslint-plugin": "^6.7.0", - "@typescript-eslint/parser": "^6.0.0", - "eslint": "^8.49.0", - "eslint-plugin-prettier": "^5.0.1", - "eslint-plugin-unused-imports": "^3.0.0", + "typescript-eslint": "^8.24.0", + "@typescript-eslint/eslint-plugin": "^8.24.0", + "@typescript-eslint/parser": "^8.24.0", + "eslint": "^9.20.1", + "eslint-plugin-prettier": "^5.2.3", + "eslint-plugin-unused-imports": "^4.1.4", + "execa": "^5.1.1", "fast-check": "^3.22.0", "iconv-lite": "^0.6.3", "jest": "^29.4.0", @@ -45,8 +48,12 @@ "tsc-multi": "https://github.com/stainless-api/tsc-multi/releases/download/v1.1.3/tsc-multi.tgz", "tsconfig-paths": "^4.0.0", "typescript": "^4.8.2", + "ws": "^8.18.0", "zod": "^3.23.8" }, + "resolutions": { + "synckit": "0.8.8" + }, "imports": { "openai": ".", "openai/*": "./src/*" @@ -69,9 +76,13 @@ }, "bin": "./bin/cli", "peerDependencies": { + "ws": "^8.18.0", "zod": "^3.23.8" }, "peerDependenciesMeta": { + "ws": { + "optional": true + }, "zod": { "optional": true } diff --git a/realtime.md b/realtime.md new file mode 100644 index 000000000..339dfd847 --- /dev/null +++ b/realtime.md @@ -0,0 +1,86 @@ +## Realtime API beta + +The Realtime API enables you to build low-latency, multi-modal conversational experiences. It currently supports text and audio as both input and output, as well as [function calling](https://platform.openai.com/docs/guides/function-calling) through a `WebSocket` connection. + +The Realtime API works through a combination of client-sent events and server-sent events. Clients can send events to do things like update session configuration or send text and audio inputs. Server events confirm when audio responses have completed, or when a text response from the model has been received. A full event reference can be found [here](https://platform.openai.com/docs/api-reference/realtime-client-events) and a guide can be found [here](https://platform.openai.com/docs/guides/realtime). + +This SDK supports accessing the Realtime API through the [WebSocket API](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) or with [ws](https://github.com/websockets/ws). + +Basic text based example with `ws`: + +```ts +// requires `yarn add ws @types/ws` +import { OpenAIRealtimeWS } from 'openai/beta/realtime/ws'; + +const rt = new OpenAIRealtimeWS({ model: 'gpt-4o-realtime-preview-2024-12-17' }); + +// access the underlying `ws.WebSocket` instance +rt.socket.on('open', () => { + console.log('Connection opened!'); + rt.send({ + type: 'session.update', + session: { + modalities: ['text'], + model: 'gpt-4o-realtime-preview', + }, + }); + + rt.send({ + type: 'conversation.item.create', + item: { + type: 'message', + role: 'user', + content: [{ type: 'input_text', text: 'Say a couple paragraphs!' }], + }, + }); + + rt.send({ type: 'response.create' }); +}); + +rt.on('error', (err) => { + // in a real world scenario this should be logged somewhere as you + // likely want to continue procesing events regardless of any errors + throw err; +}); + +rt.on('session.created', (event) => { + console.log('session created!', event.session); + console.log(); +}); + +rt.on('response.text.delta', (event) => process.stdout.write(event.delta)); +rt.on('response.text.done', () => console.log()); + +rt.on('response.done', () => rt.close()); + +rt.socket.on('close', () => console.log('\nConnection closed!')); +``` + +To use the web API `WebSocket` implementation, replace `OpenAIRealtimeWS` with `OpenAIRealtimeWebSocket` and adjust any `rt.socket` access: + +```ts +import { OpenAIRealtimeWebSocket } from 'openai/beta/realtime/websocket'; + +const rt = new OpenAIRealtimeWebSocket({ model: 'gpt-4o-realtime-preview-2024-12-17' }); +// ... +rt.socket.addEventListener('open', () => { + // ... +}); +``` + +A full example can be found [here](https://github.com/openai/openai-node/blob/master/examples/realtime/websocket.ts). + +### Realtime error handling + +When an error is encountered, either on the client side or returned from the server through the [`error` event](https://platform.openai.com/docs/guides/realtime-model-capabilities#error-handling), the `error` event listener will be fired. However, if you haven't registered an `error` event listener then an `unhandled Promise rejection` error will be thrown. + +It is **highly recommended** that you register an `error` event listener and handle errors approriately as typically the underlying connection is still usable. + +```ts +const rt = new OpenAIRealtimeWS({ model: 'gpt-4o-realtime-preview-2024-12-17' }); +rt.on('error', (err) => { + // in a real world scenario this should be logged somewhere as you + // likely want to continue procesing events regardless of any errors + throw err; +}); +``` diff --git a/scripts/build b/scripts/build index aadf87be6..dd2c9dd57 100755 --- a/scripts/build +++ b/scripts/build @@ -40,8 +40,8 @@ cp dist/index.d.ts dist/index.d.mts cp tsconfig.dist-src.json dist/src/tsconfig.json cp src/internal/shim-types.d.ts dist/internal/shim-types.d.ts cp src/internal/shim-types.d.ts dist/internal/shim-types.d.mts -mkdir -p dist/internal/polyfill -cp src/internal/polyfill/*.{mjs,js,d.ts} dist/internal/polyfill +mkdir -p dist/internal/shims +cp src/internal/shims/*.{mjs,js,d.ts} dist/internal/shims node scripts/utils/postprocess-files.cjs diff --git a/scripts/format b/scripts/format index a6bb9d03a..903b1ef85 100755 --- a/scripts/format +++ b/scripts/format @@ -5,4 +5,4 @@ set -e cd "$(dirname "$0")/.." echo "==> Running eslint --fix" -ESLINT_USE_FLAT_CONFIG="false" ./node_modules/.bin/eslint --fix --ext ts,js . +./node_modules/.bin/eslint --fix . diff --git a/scripts/lint b/scripts/lint index 0096d1e51..3ffb78a64 100755 --- a/scripts/lint +++ b/scripts/lint @@ -5,10 +5,13 @@ set -e cd "$(dirname "$0")/.." echo "==> Running eslint" -ESLINT_USE_FLAT_CONFIG="false" ./node_modules/.bin/eslint --ext ts,js . +./node_modules/.bin/eslint . echo "==> Building" -./scripts/build # also checks types +./scripts/build + +echo "==> Checking types" +./node_modules/typescript/bin/tsc echo "==> Running Are The Types Wrong?" ./node_modules/.bin/attw --pack dist -f json >.attw.json || true diff --git a/scripts/utils/attw-report.cjs b/scripts/utils/attw-report.cjs index e45e7952b..b3477c0ef 100644 --- a/scripts/utils/attw-report.cjs +++ b/scripts/utils/attw-report.cjs @@ -8,7 +8,10 @@ const problems = Object.values(JSON.parse(fs.readFileSync('.attw.json', 'utf-8') ( (problem.kind === 'CJSResolvesToESM' && problem.entrypoint.endsWith('.mjs')) || // This is intentional for backwards compat reasons. - (problem.kind === 'MissingExportEquals' && problem.implementationFileName.endsWith('/index.js')) + (problem.kind === 'MissingExportEquals' && problem.implementationFileName.endsWith('/index.js')) || + // this is intentional, we deliberately attempt to import types that may not exist from parent node_modules + // folders to better support various runtimes without triggering automatic type acquisition. + (problem.kind === 'InternalResolutionError' && problem.moduleSpecifier.includes('node_modules')) ) ), ); diff --git a/scripts/utils/postprocess-files.cjs b/scripts/utils/postprocess-files.cjs index deae575e3..d16c8641c 100644 --- a/scripts/utils/postprocess-files.cjs +++ b/scripts/utils/postprocess-files.cjs @@ -50,14 +50,14 @@ async function postprocess() { if (entry.isDirectory() && entry.name !== 'src' && entry.name !== 'internal' && entry.name !== 'bin') { const subpath = './' + entry.name; newExports[subpath + '/*.mjs'] = { - default: subpath + '/*.mjs', + default: [subpath + '/*.mjs', subpath + '/*/index.mjs'], }; newExports[subpath + '/*.js'] = { - default: subpath + '/*.js', + default: [subpath + '/*.js', subpath + '/*/index.js'], }; newExports[subpath + '/*'] = { - import: subpath + '/*.mjs', - require: subpath + '/*.js', + import: [subpath + '/*.mjs', subpath + '/*/index.mjs'], + require: [subpath + '/*.js', subpath + '/*/index.js'], }; } else if (entry.isFile() && /\.[cm]?js$/.test(entry.name)) { const { name, ext } = path.parse(entry.name); diff --git a/src/api-promise.ts b/src/api-promise.ts index 5786070a6..c9d9a9a9f 100644 --- a/src/api-promise.ts +++ b/src/api-promise.ts @@ -1,5 +1,7 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +import { type OpenAI } from './client'; + import { type PromiseOrValue } from './internal/types'; import { type APIResponseProps, @@ -14,10 +16,13 @@ import { */ export class APIPromise extends Promise> { private parsedPromise: Promise> | undefined; + #client: OpenAI; constructor( + client: OpenAI, private responsePromise: Promise, private parseResponse: ( + client: OpenAI, props: APIResponseProps, ) => PromiseOrValue> = defaultParseResponse, ) { @@ -27,11 +32,12 @@ export class APIPromise extends Promise> { // to parse the response resolve(null as any); }); + this.#client = client; } _thenUnwrap(transform: (data: T, props: APIResponseProps) => U): APIPromise { - return new APIPromise(this.responsePromise, async (props) => - addRequestID(transform(await this.parseResponse(props), props), props.response), + return new APIPromise(this.#client, this.responsePromise, async (client, props) => + addRequestID(transform(await this.parseResponse(client, props), props), props.response), ); } @@ -69,7 +75,9 @@ export class APIPromise extends Promise> { private parse(): Promise> { if (!this.parsedPromise) { - this.parsedPromise = this.responsePromise.then(this.parseResponse) as any as Promise>; + this.parsedPromise = this.responsePromise.then((data) => + this.parseResponse(this.#client, data), + ) as any as Promise>; } return this.parsedPromise; } diff --git a/src/azure.ts b/src/azure.ts index 72924707d..661f288af 100644 --- a/src/azure.ts +++ b/src/azure.ts @@ -38,8 +38,9 @@ export interface AzureClientOptions extends ClientOptions { /** API Client for interfacing with the Azure OpenAI API. */ export class AzureOpenAI extends OpenAI { private _azureADTokenProvider: (() => Promise) | undefined; - private _deployment: string | undefined; + deploymentName: string | undefined; apiVersion: string = ''; + /** * API Client for interfacing with the Azure OpenAI API. * @@ -121,7 +122,7 @@ export class AzureOpenAI extends OpenAI { this._azureADTokenProvider = azureADTokenProvider; this.apiVersion = apiVersion; - this._deployment = deployment; + this.deploymentName = deployment; } override buildRequest( @@ -132,7 +133,7 @@ export class AzureOpenAI extends OpenAI { if (!isObj(options.body)) { throw new Error('Expected request body to be an object'); } - const model = this._deployment || options.body['model']; + const model = this.deploymentName || options.body['model'] || options.__metadata?.['model']; if (model !== undefined && !this.baseURL.includes('/deployments')) { options.path = `/deployments/${model}${options.path}`; } @@ -140,7 +141,7 @@ export class AzureOpenAI extends OpenAI { return super.buildRequest(options, props); } - private async _getAzureADToken(): Promise { + async _getAzureADToken(): Promise { if (typeof this._azureADTokenProvider === 'function') { const token = await this._azureADTokenProvider(); if (!token || typeof token !== 'string') { diff --git a/src/beta/realtime/index.ts b/src/beta/realtime/index.ts new file mode 100644 index 000000000..75f0f3088 --- /dev/null +++ b/src/beta/realtime/index.ts @@ -0,0 +1 @@ +export { OpenAIRealtimeError } from './internal-base'; diff --git a/src/beta/realtime/internal-base.ts b/src/beta/realtime/internal-base.ts new file mode 100644 index 000000000..b704812ee --- /dev/null +++ b/src/beta/realtime/internal-base.ts @@ -0,0 +1,93 @@ +import { RealtimeClientEvent, RealtimeServerEvent, ErrorEvent } from '../../resources/beta/realtime/realtime'; +import { EventEmitter } from '../../lib/EventEmitter'; +import { OpenAIError } from '../../error'; +import OpenAI, { AzureOpenAI } from '../../index'; + +export class OpenAIRealtimeError extends OpenAIError { + /** + * The error data that the API sent back in an `error` event. + */ + error?: ErrorEvent.Error | undefined; + + /** + * The unique ID of the server event. + */ + event_id?: string | undefined; + + constructor(message: string, event: ErrorEvent | null) { + super(message); + + this.error = event?.error; + this.event_id = event?.event_id; + } +} + +type Simplify = { [KeyType in keyof T]: T[KeyType] } & {}; + +type RealtimeEvents = Simplify< + { + event: (event: RealtimeServerEvent) => void; + error: (error: OpenAIRealtimeError) => void; + } & { + [EventType in Exclude]: ( + event: Extract, + ) => unknown; + } +>; + +export abstract class OpenAIRealtimeEmitter extends EventEmitter { + /** + * Send an event to the API. + */ + abstract send(event: RealtimeClientEvent): void; + + /** + * Close the websocket connection. + */ + abstract close(props?: { code: number; reason: string }): void; + + protected _onError(event: null, message: string, cause: any): void; + protected _onError(event: ErrorEvent, message?: string | undefined): void; + protected _onError(event: ErrorEvent | null, message?: string | undefined, cause?: any): void { + message = + event?.error ? + `${event.error.message} code=${event.error.code} param=${event.error.param} type=${event.error.type} event_id=${event.error.event_id}` + : message ?? 'unknown error'; + + if (!this._hasListener('error')) { + const error = new OpenAIRealtimeError( + message + + `\n\nTo resolve these unhandled rejection errors you should bind an \`error\` callback, e.g. \`rt.on('error', (error) => ...)\` `, + event, + ); + // @ts-ignore + error.cause = cause; + Promise.reject(error); + return; + } + + const error = new OpenAIRealtimeError(message, event); + // @ts-ignore + error.cause = cause; + + this._emit('error', error); + } +} + +export function isAzure(client: Pick): client is AzureOpenAI { + return client instanceof AzureOpenAI; +} + +export function buildRealtimeURL(client: Pick, model: string): URL { + const path = '/realtime'; + const baseURL = client.baseURL; + const url = new URL(baseURL + (baseURL.endsWith('/') ? path.slice(1) : path)); + url.protocol = 'wss'; + if (isAzure(client)) { + url.searchParams.set('api-version', client.apiVersion); + url.searchParams.set('deployment', model); + } else { + url.searchParams.set('model', model); + } + return url; +} diff --git a/src/beta/realtime/websocket.ts b/src/beta/realtime/websocket.ts new file mode 100644 index 000000000..2bf0b75d5 --- /dev/null +++ b/src/beta/realtime/websocket.ts @@ -0,0 +1,143 @@ +import { AzureOpenAI, OpenAI } from '../../index'; +import { OpenAIError } from '../../error'; +import type { RealtimeClientEvent, RealtimeServerEvent } from '../../resources/beta/realtime/realtime'; +import { OpenAIRealtimeEmitter, buildRealtimeURL, isAzure } from './internal-base'; +import { isRunningInBrowser } from '../../internal/detect-platform'; + +interface MessageEvent { + data: string; +} + +type _WebSocket = + typeof globalThis extends ( + { + WebSocket: infer ws extends abstract new (...args: any) => any; + } + ) ? + // @ts-ignore + InstanceType + : any; + +export class OpenAIRealtimeWebSocket extends OpenAIRealtimeEmitter { + url: URL; + socket: _WebSocket; + + constructor( + props: { + model: string; + dangerouslyAllowBrowser?: boolean; + /** + * Callback to mutate the URL, needed for Azure. + * @internal + */ + onURL?: (url: URL) => void; + }, + client?: Pick, + ) { + super(); + + const dangerouslyAllowBrowser = + props.dangerouslyAllowBrowser ?? + (client as any)?._options?.dangerouslyAllowBrowser ?? + (client?.apiKey.startsWith('ek_') ? true : null); + + if (!dangerouslyAllowBrowser && isRunningInBrowser()) { + throw new OpenAIError( + "It looks like you're running in a browser-like environment.\n\nThis is disabled by default, as it risks exposing your secret API credentials to attackers.\n\nYou can avoid this error by creating an ephemeral session token:\nhttps://platform.openai.com/docs/api-reference/realtime-sessions\n", + ); + } + + client ??= new OpenAI({ dangerouslyAllowBrowser }); + + this.url = buildRealtimeURL(client, props.model); + props.onURL?.(this.url); + + // @ts-ignore + this.socket = new WebSocket(this.url.toString(), [ + 'realtime', + ...(isAzure(client) ? [] : [`openai-insecure-api-key.${client.apiKey}`]), + 'openai-beta.realtime-v1', + ]); + + this.socket.addEventListener('message', (websocketEvent: MessageEvent) => { + const event = (() => { + try { + return JSON.parse(websocketEvent.data.toString()) as RealtimeServerEvent; + } catch (err) { + this._onError(null, 'could not parse websocket event', err); + return null; + } + })(); + + if (event) { + this._emit('event', event); + + if (event.type === 'error') { + this._onError(event); + } else { + // @ts-expect-error TS isn't smart enough to get the relationship right here + this._emit(event.type, event); + } + } + }); + + this.socket.addEventListener('error', (event: any) => { + this._onError(null, event.message, null); + }); + + if (isAzure(client)) { + if (this.url.searchParams.get('Authorization') !== null) { + this.url.searchParams.set('Authorization', ''); + } else { + this.url.searchParams.set('api-key', ''); + } + } + } + + static async azure( + client: Pick, + options: { deploymentName?: string; dangerouslyAllowBrowser?: boolean } = {}, + ): Promise { + const token = await client._getAzureADToken(); + function onURL(url: URL) { + if (client.apiKey !== '') { + url.searchParams.set('api-key', client.apiKey); + } else { + if (token) { + url.searchParams.set('Authorization', `Bearer ${token}`); + } else { + throw new Error('AzureOpenAI is not instantiated correctly. No API key or token provided.'); + } + } + } + const deploymentName = options.deploymentName ?? client.deploymentName; + if (!deploymentName) { + throw new Error('No deployment name provided'); + } + const { dangerouslyAllowBrowser } = options; + return new OpenAIRealtimeWebSocket( + { + model: deploymentName, + onURL, + ...(dangerouslyAllowBrowser ? { dangerouslyAllowBrowser } : {}), + }, + client, + ); + } + + send(event: RealtimeClientEvent) { + try { + this.socket.send(JSON.stringify(event)); + } catch (err) { + this._onError(null, 'could not send data', err); + } + } + + close(props?: { code: number; reason: string }) { + try { + this.socket.close(props?.code ?? 1000, props?.reason ?? 'OK'); + } catch (err) { + this._onError(null, 'could not close the connection', err); + } + } +} diff --git a/src/beta/realtime/ws.ts b/src/beta/realtime/ws.ts new file mode 100644 index 000000000..3f51dfc4b --- /dev/null +++ b/src/beta/realtime/ws.ts @@ -0,0 +1,96 @@ +import * as WS from 'ws'; +import { AzureOpenAI, OpenAI } from '../../index'; +import type { RealtimeClientEvent, RealtimeServerEvent } from '../../resources/beta/realtime/realtime'; +import { OpenAIRealtimeEmitter, buildRealtimeURL, isAzure } from './internal-base'; + +export class OpenAIRealtimeWS extends OpenAIRealtimeEmitter { + url: URL; + socket: WS.WebSocket; + + constructor( + props: { model: string; options?: WS.ClientOptions | undefined }, + client?: Pick, + ) { + super(); + client ??= new OpenAI(); + + this.url = buildRealtimeURL(client, props.model); + this.socket = new WS.WebSocket(this.url, { + ...props.options, + headers: { + ...props.options?.headers, + ...(isAzure(client) ? {} : { Authorization: `Bearer ${client.apiKey}` }), + 'OpenAI-Beta': 'realtime=v1', + }, + }); + + this.socket.on('message', (wsEvent) => { + const event = (() => { + try { + return JSON.parse(wsEvent.toString()) as RealtimeServerEvent; + } catch (err) { + this._onError(null, 'could not parse websocket event', err); + return null; + } + })(); + + if (event) { + this._emit('event', event); + + if (event.type === 'error') { + this._onError(event); + } else { + // @ts-expect-error TS isn't smart enough to get the relationship right here + this._emit(event.type, event); + } + } + }); + + this.socket.on('error', (err) => { + this._onError(null, err.message, err); + }); + } + + static async azure( + client: Pick, + options: { deploymentName?: string; options?: WS.ClientOptions | undefined } = {}, + ): Promise { + const deploymentName = options.deploymentName ?? client.deploymentName; + if (!deploymentName) { + throw new Error('No deployment name provided'); + } + return new OpenAIRealtimeWS( + { model: deploymentName, options: { headers: await getAzureHeaders(client) } }, + client, + ); + } + + send(event: RealtimeClientEvent) { + try { + this.socket.send(JSON.stringify(event)); + } catch (err) { + this._onError(null, 'could not send data', err); + } + } + + close(props?: { code: number; reason: string }) { + try { + this.socket.close(props?.code ?? 1000, props?.reason ?? 'OK'); + } catch (err) { + this._onError(null, 'could not close the connection', err); + } + } +} + +async function getAzureHeaders(client: Pick) { + if (client.apiKey !== '') { + return { 'api-key': client.apiKey }; + } else { + const token = await client._getAzureADToken(); + if (token) { + return { Authorization: `Bearer ${token}` }; + } else { + throw new Error('AzureOpenAI is not instantiated correctly. No API key or token provided.'); + } + } +} diff --git a/src/client.ts b/src/client.ts index 6d4d6d6bc..9154dc496 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,20 +1,17 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. import type { RequestInit, RequestInfo, BodyInit } from './internal/builtin-types'; -import type { HTTPMethod, PromiseOrValue } from './internal/types'; -import { debug } from './internal/utils/log'; +import type { HTTPMethod, PromiseOrValue, MergedRequestInit } from './internal/types'; import { uuid4 } from './internal/utils/uuid'; -import { validatePositiveInteger, isAbsoluteURL } from './internal/utils/values'; +import { validatePositiveInteger, isAbsoluteURL, hasOwn } from './internal/utils/values'; import { sleep } from './internal/utils/sleep'; -import { castToError } from './internal/errors'; +import { castToError, isAbortError } from './internal/errors'; import type { APIResponseProps } from './internal/parse'; import { getPlatformHeaders } from './internal/detect-platform'; import * as Shims from './internal/shims'; import * as Opts from './internal/request-options'; import * as qs from './internal/qs'; import { VERSION } from './version'; -import { isBlobLike } from './uploads'; -import { buildHeaders } from './internal/headers'; import * as Errors from './error'; import * as Pagination from './pagination'; import { AbstractPage, type CursorPageParams, CursorPageResponse, PageResponse } from './pagination'; @@ -23,7 +20,7 @@ import * as API from './resources/index'; import { APIPromise } from './api-promise'; import { type Fetch } from './internal/builtin-types'; import { isRunningInBrowser } from './internal/detect-platform'; -import { HeadersLike, NullableHeaders } from './internal/headers'; +import { HeadersLike, NullableHeaders, buildHeaders } from './internal/headers'; import { FinalRequestOptions, RequestOptions } from './internal/request-options'; import { Batch, @@ -81,10 +78,18 @@ import { Moderations, } from './resources/moderations'; import { readEnv } from './internal/utils/env'; +import { formatRequestDetails, loggerFor } from './internal/utils/log'; import { isEmptyObj } from './internal/utils/values'; import { Audio, AudioModel, AudioResponseFormat } from './resources/audio/audio'; import { Beta } from './resources/beta/beta'; import { Chat, ChatModel } from './resources/chat/chat'; +import { FineTuning } from './resources/fine-tuning/fine-tuning'; +import { + Upload, + UploadCompleteParams, + UploadCreateParams, + Uploads as UploadsAPIUploads, +} from './resources/uploads/uploads'; import { ChatCompletion, ChatCompletionAssistantMessageParam, @@ -99,9 +104,11 @@ import { ChatCompletionCreateParams, ChatCompletionCreateParamsNonStreaming, ChatCompletionCreateParamsStreaming, + ChatCompletionDeleted, ChatCompletionDeveloperMessageParam, ChatCompletionFunctionCallOption, ChatCompletionFunctionMessageParam, + ChatCompletionListParams, ChatCompletionMessage, ChatCompletionMessageParam, ChatCompletionMessageToolCall, @@ -110,21 +117,17 @@ import { ChatCompletionPredictionContent, ChatCompletionReasoningEffort, ChatCompletionRole, + ChatCompletionStoreMessage, ChatCompletionStreamOptions, ChatCompletionSystemMessageParam, ChatCompletionTokenLogprob, ChatCompletionTool, ChatCompletionToolChoiceOption, ChatCompletionToolMessageParam, + ChatCompletionUpdateParams, ChatCompletionUserMessageParam, -} from './resources/chat/completions'; -import { FineTuning } from './resources/fine-tuning/fine-tuning'; -import { - Upload, - UploadCompleteParams, - UploadCreateParams, - Uploads as UploadsAPIUploads, -} from './resources/uploads/uploads'; + ChatCompletionsPage, +} from './resources/chat/completions/completions'; const safeJSON = (text: string) => { try { @@ -134,6 +137,40 @@ const safeJSON = (text: string) => { } }; +type LogFn = (message: string, ...rest: unknown[]) => void; +export type Logger = { + error: LogFn; + warn: LogFn; + info: LogFn; + debug: LogFn; +}; +export type LogLevel = 'off' | 'error' | 'warn' | 'info' | 'debug'; +const parseLogLevel = ( + maybeLevel: string | undefined, + sourceName: string, + client: OpenAI, +): LogLevel | undefined => { + if (!maybeLevel) { + return undefined; + } + const levels: Record = { + off: true, + error: true, + warn: true, + info: true, + debug: true, + }; + if (hasOwn(levels, maybeLevel)) { + return maybeLevel; + } + loggerFor(client).warn( + `${sourceName} was set to ${JSON.stringify(maybeLevel)}, expected one of ${JSON.stringify( + Object.keys(levels), + )}`, + ); + return undefined; +}; + export interface ClientOptions { /** * Defaults to process.env['OPENAI_API_KEY']. @@ -164,15 +201,12 @@ export interface ClientOptions { * Note that request timeouts are retried by default, so in a worst-case scenario you may wait * much longer than this timeout before the promise succeeds or fails. */ - timeout?: number; - + timeout?: number | undefined; /** - * An HTTP agent used to manage HTTP(S) connections. - * - * If not provided, an agent will be constructed by default in the Node.js environment, - * otherwise no agent is used. + * Additional `RequestInit` options to be passed to `fetch` calls. + * Properties will be overridden by per-request `fetchOptions`. */ - httpAgent?: Shims.Agent; + fetchOptions?: MergedRequestInit | undefined; /** * Specify a custom `fetch` function implementation. @@ -187,7 +221,7 @@ export interface ClientOptions { * * @default 2 */ - maxRetries?: number; + maxRetries?: number | undefined; /** * Default headers to include with every request to the API. @@ -195,7 +229,7 @@ export interface ClientOptions { * These can be removed in individual requests by explicitly setting the * header to `null` in request options. */ - defaultHeaders?: HeadersLike; + defaultHeaders?: HeadersLike | undefined; /** * Default query parameters to include with every request to the API. @@ -203,13 +237,27 @@ export interface ClientOptions { * These can be removed in individual requests by explicitly setting the * param to `undefined` in request options. */ - defaultQuery?: Record; + defaultQuery?: Record | undefined; /** * By default, client-side use of this library is not allowed, as it risks exposing your secret API credentials to attackers. * Only set this option to `true` if you understand the risks and have appropriate mitigations in place. */ - dangerouslyAllowBrowser?: boolean; + dangerouslyAllowBrowser?: boolean | undefined; + + /** + * Set the log level. + * + * Defaults to process.env['OPENAI_LOG'] or 'warn' if it isn't set. + */ + logLevel?: LogLevel | undefined; + + /** + * Set the logger. + * + * Defaults to globalThis.console. + */ + logger?: Logger | undefined; } type FinalizedRequestInit = RequestInit & { headers: Headers }; @@ -225,7 +273,9 @@ export class OpenAI { baseURL: string; maxRetries: number; timeout: number; - httpAgent: Shims.Agent | undefined; + logger: Logger | undefined; + logLevel: LogLevel | undefined; + fetchOptions: MergedRequestInit | undefined; private fetch: Fetch; #encoder: Opts.RequestEncoder; @@ -240,7 +290,7 @@ export class OpenAI { * @param {string | null | undefined} [opts.project=process.env['OPENAI_PROJECT_ID'] ?? null] * @param {string} [opts.baseURL=process.env['OPENAI_BASE_URL'] ?? https://api.openai.com/v1] - Override the default base URL for the API. * @param {number} [opts.timeout=10 minutes] - The maximum amount of time (in milliseconds) the client will wait for a response before timing out. - * @param {number} [opts.httpAgent] - An HTTP agent used to manage HTTP(s) connections. + * @param {MergedRequestInit} [opts.fetchOptions] - Additional `RequestInit` options to be passed to `fetch` calls. * @param {Fetch} [opts.fetch] - Specify a custom `fetch` function implementation. * @param {number} [opts.maxRetries=2] - The maximum number of times the client will retry a request. * @param {HeadersLike} opts.defaultHeaders - Default headers to include with every request to the API. @@ -276,7 +326,15 @@ export class OpenAI { this.baseURL = options.baseURL!; this.timeout = options.timeout ?? OpenAI.DEFAULT_TIMEOUT /* 10 minutes */; - this.httpAgent = options.httpAgent; + this.logger = options.logger ?? console; + const defaultLogLevel = 'warn'; + // Set default logLevel early so that we can log a warning in parseLogLevel. + this.logLevel = defaultLogLevel; + this.logLevel = + parseLogLevel(options.logLevel, 'ClientOptions.logLevel', this) ?? + parseLogLevel(readEnv('OPENAI_LOG'), "process.env['OPENAI_LOG']", this) ?? + defaultLogLevel; + this.fetchOptions = options.fetchOptions; this.maxRetries = options.maxRetries ?? 2; this.fetch = options.fetch ?? Shims.getDefaultFetch(); this.#encoder = Opts.FallbackEncoder; @@ -339,24 +397,6 @@ export class OpenAI { return url.toString(); } - private calculateContentLength(body: unknown): string | null { - if (typeof body === 'string') { - if (typeof Buffer !== 'undefined') { - return Buffer.byteLength(body, 'utf8').toString(); - } - - if (typeof TextEncoder !== 'undefined') { - const encoder = new TextEncoder(); - const encoded = encoder.encode(body); - return encoded.length.toString(); - } - } else if (ArrayBuffer.isView(body)) { - return body.byteLength.toString(); - } - - return null; - } - /** * Used as a callback for mutating the given `FinalRequestOptions` object. */ @@ -399,14 +439,8 @@ export class OpenAI { opts?: PromiseOrValue, ): APIPromise { return this.request( - Promise.resolve(opts).then(async (opts) => { - const body = - opts && isBlobLike(opts?.body) ? new DataView(await opts.body.arrayBuffer()) - : opts?.body instanceof DataView ? opts.body - : opts?.body instanceof ArrayBuffer ? new DataView(opts.body) - : opts && ArrayBuffer.isView(opts?.body) ? new DataView(opts.body.buffer) - : opts?.body; - return { method, path, ...opts, body }; + Promise.resolve(opts).then((opts) => { + return { method, path, ...opts }; }), ); } @@ -415,12 +449,13 @@ export class OpenAI { options: PromiseOrValue, remainingRetries: number | null = null, ): APIPromise { - return new APIPromise(this.makeRequest(options, remainingRetries)); + return new APIPromise(this, this.makeRequest(options, remainingRetries, undefined)); } private async makeRequest( optionsInput: PromiseOrValue, retriesRemaining: number | null, + retryOfRequestLogID: string | undefined, ): Promise { const options = await optionsInput; const maxRetries = options.maxRetries ?? this.maxRetries; @@ -434,7 +469,21 @@ export class OpenAI { await this.prepareRequest(req, { url, options }); - debug('request', url, options, req.headers); + /** Not an API request ID, just for correlating local log entries. */ + const requestLogID = 'log_' + ((Math.random() * (1 << 24)) | 0).toString(16).padStart(6, '0'); + const retryLogStr = retryOfRequestLogID === undefined ? '' : `, retryOf: ${retryOfRequestLogID}`; + const startTime = Date.now(); + + loggerFor(this).debug( + `[${requestLogID}] sending request`, + formatRequestDetails({ + retryOfRequestLogID, + method: options.method, + url, + options, + headers: req.headers, + }), + ); if (options.signal?.aborted) { throw new Errors.APIUserAbortError(); @@ -442,39 +491,124 @@ export class OpenAI { const controller = new AbortController(); const response = await this.fetchWithTimeout(url, req, timeout, controller).catch(castToError); + const headersTime = Date.now(); if (response instanceof Error) { + const retryMessage = `retrying, ${retriesRemaining} attempts remaining`; if (options.signal?.aborted) { throw new Errors.APIUserAbortError(); } + // detect native connection timeout errors + // deno throws "TypeError: error sending request for url (https://example/): client error (Connect): tcp connect error: Operation timed out (os error 60): Operation timed out (os error 60)" + // undici throws "TypeError: fetch failed" with cause "ConnectTimeoutError: Connect Timeout Error (attempted address: example:443, timeout: 1ms)" + // others do not provide enough information to distinguish timeouts from other connection errors + const isTimeout = + isAbortError(response) || + /timed? ?out/i.test(String(response) + ('cause' in response ? String(response.cause) : '')); if (retriesRemaining) { - return this.retryRequest(options, retriesRemaining); + loggerFor(this).info( + `[${requestLogID}] connection ${isTimeout ? 'timed out' : 'failed'} - ${retryMessage}`, + ); + loggerFor(this).debug( + `[${requestLogID}] connection ${isTimeout ? 'timed out' : 'failed'} (${retryMessage})`, + formatRequestDetails({ + retryOfRequestLogID, + url, + durationMs: headersTime - startTime, + message: response.message, + }), + ); + return this.retryRequest(options, retriesRemaining, retryOfRequestLogID ?? requestLogID); } - if (response.name === 'AbortError') { + loggerFor(this).info( + `[${requestLogID}] connection ${isTimeout ? 'timed out' : 'failed'} - error; no more retries left`, + ); + loggerFor(this).debug( + `[${requestLogID}] connection ${isTimeout ? 'timed out' : 'failed'} (error; no more retries left)`, + formatRequestDetails({ + retryOfRequestLogID, + url, + durationMs: headersTime - startTime, + message: response.message, + }), + ); + if (isTimeout) { throw new Errors.APIConnectionTimeoutError(); } throw new Errors.APIConnectionError({ cause: response }); } + const specialHeaders = [...response.headers.entries()] + .filter(([name]) => name === 'x-request-id') + .map(([name, value]) => ', ' + name + ': ' + JSON.stringify(value)) + .join(''); + const responseInfo = `[${requestLogID}${retryLogStr}${specialHeaders}] ${req.method} ${url} ${ + response.ok ? 'succeeded' : 'failed' + } with status ${response.status} in ${headersTime - startTime}ms`; + if (!response.ok) { - if (retriesRemaining && this.shouldRetry(response)) { + const shouldRetry = this.shouldRetry(response); + if (retriesRemaining && shouldRetry) { const retryMessage = `retrying, ${retriesRemaining} attempts remaining`; - debug(`response (error; ${retryMessage})`, response.status, url, response.headers); - return this.retryRequest(options, retriesRemaining, response.headers); + + // We don't need the body of this response. + await Shims.CancelReadableStream(response.body); + loggerFor(this).info(`${responseInfo} - ${retryMessage}`); + loggerFor(this).debug( + `[${requestLogID}] response error (${retryMessage})`, + formatRequestDetails({ + retryOfRequestLogID, + url: response.url, + status: response.status, + headers: response.headers, + durationMs: headersTime - startTime, + }), + ); + return this.retryRequest( + options, + retriesRemaining, + retryOfRequestLogID ?? requestLogID, + response.headers, + ); } + const retryMessage = shouldRetry ? `error; no more retries left` : `error; not retryable`; + + loggerFor(this).info(`${responseInfo} - ${retryMessage}`); + const errText = await response.text().catch((err: any) => castToError(err).message); const errJSON = safeJSON(errText); const errMessage = errJSON ? undefined : errText; - const retryMessage = retriesRemaining ? `(error; no more retries left)` : `(error; not retryable)`; - debug(`response (error; ${retryMessage})`, response.status, url, response.headers, errMessage); + loggerFor(this).debug( + `[${requestLogID}] response error (${retryMessage})`, + formatRequestDetails({ + retryOfRequestLogID, + url: response.url, + status: response.status, + headers: response.headers, + message: errMessage, + durationMs: Date.now() - startTime, + }), + ); const err = this.makeStatusError(response.status, errJSON, errMessage, response.headers); throw err; } - return { response, options, controller }; + loggerFor(this).info(responseInfo); + loggerFor(this).debug( + `[${requestLogID}] response start`, + formatRequestDetails({ + retryOfRequestLogID, + url: response.url, + status: response.status, + headers: response.headers, + durationMs: headersTime - startTime, + }), + ); + + return { response, options, controller, requestLogID, retryOfRequestLogID, startTime }; } getAPIList = Pagination.AbstractPage>( @@ -492,7 +626,7 @@ export class OpenAI { Page: new (...args: ConstructorParameters) => PageClass, options: FinalRequestOptions, ): Pagination.PagePromise { - const request = this.makeRequest(options, null); + const request = this.makeRequest(options, null, undefined); return new Pagination.PagePromise(this as any as OpenAI, request, Page); } @@ -507,7 +641,9 @@ export class OpenAI { const timeout = setTimeout(() => controller.abort(), ms); - const isReadableBody = Shims.isReadableLike(options.body); + const isReadableBody = + ((globalThis as any).ReadableStream && options.body instanceof (globalThis as any).ReadableStream) || + (typeof options.body === 'object' && options.body !== null && Symbol.asyncIterator in options.body); const fetchOptions: RequestInit = { signal: controller.signal as any, @@ -555,6 +691,7 @@ export class OpenAI { private async retryRequest( options: FinalRequestOptions, retriesRemaining: number, + requestLogID: string, responseHeaders?: Headers | undefined, ): Promise { let timeoutMillis: number | undefined; @@ -587,7 +724,7 @@ export class OpenAI { } await sleep(timeoutMillis); - return this.makeRequest(options, retriesRemaining - 1); + return this.makeRequest(options, retriesRemaining - 1, requestLogID); } private calculateDefaultRetryTimeoutMillis(retriesRemaining: number, maxRetries: number): number { @@ -609,38 +746,27 @@ export class OpenAI { options: FinalRequestOptions, { retryCount = 0 }: { retryCount?: number } = {}, ): { req: FinalizedRequestInit; url: string; timeout: number } { + options = { ...options }; const { method, path, query } = options; const url = this.buildURL(path!, query as Record); if ('timeout' in options) validatePositiveInteger('timeout', options.timeout); - const timeout = options.timeout ?? this.timeout; - const httpAgent = options.httpAgent ?? this.httpAgent; - const minAgentTimeout = timeout + 1000; - if ( - typeof (httpAgent as any)?.options?.timeout === 'number' && - minAgentTimeout > ((httpAgent as any).options.timeout ?? 0) - ) { - // Allow any given request to bump our agent active socket timeout. - // This may seem strange, but leaking active sockets should be rare and not particularly problematic, - // and without mutating agent we would need to create more of them. - // This tradeoff optimizes for performance. - (httpAgent as any).options.timeout = minAgentTimeout; - } - + options.timeout = options.timeout ?? this.timeout; const { bodyHeaders, body } = this.buildBody({ options }); const reqHeaders = this.buildHeaders({ options, method, bodyHeaders, retryCount }); const req: FinalizedRequestInit = { method, headers: reqHeaders, - ...(httpAgent && { agent: httpAgent }), ...(options.signal && { signal: options.signal }), ...((globalThis as any).ReadableStream && body instanceof (globalThis as any).ReadableStream && { duplex: 'half' }), ...(body && { body }), + ...((this.fetchOptions as any) ?? {}), + ...((options.fetchOptions as any) ?? {}), }; - return { req, url, timeout }; + return { req, url, timeout: options.timeout }; } private buildHeaders({ @@ -666,6 +792,7 @@ export class OpenAI { Accept: 'application/json', 'User-Agent': this.getUserAgent(), 'X-Stainless-Retry-Count': String(retryCount), + ...(options.timeout ? { 'X-Stainless-Timeout': String(options.timeout) } : {}), ...getPlatformHeaders(), 'OpenAI-Organization': this.organization, 'OpenAI-Project': this.project, @@ -794,6 +921,7 @@ export declare namespace OpenAI { type ChatCompletionContentPartInputAudio as ChatCompletionContentPartInputAudio, type ChatCompletionContentPartRefusal as ChatCompletionContentPartRefusal, type ChatCompletionContentPartText as ChatCompletionContentPartText, + type ChatCompletionDeleted as ChatCompletionDeleted, type ChatCompletionDeveloperMessageParam as ChatCompletionDeveloperMessageParam, type ChatCompletionFunctionCallOption as ChatCompletionFunctionCallOption, type ChatCompletionFunctionMessageParam as ChatCompletionFunctionMessageParam, @@ -805,6 +933,7 @@ export declare namespace OpenAI { type ChatCompletionPredictionContent as ChatCompletionPredictionContent, type ChatCompletionReasoningEffort as ChatCompletionReasoningEffort, type ChatCompletionRole as ChatCompletionRole, + type ChatCompletionStoreMessage as ChatCompletionStoreMessage, type ChatCompletionStreamOptions as ChatCompletionStreamOptions, type ChatCompletionSystemMessageParam as ChatCompletionSystemMessageParam, type ChatCompletionTokenLogprob as ChatCompletionTokenLogprob, @@ -812,9 +941,12 @@ export declare namespace OpenAI { type ChatCompletionToolChoiceOption as ChatCompletionToolChoiceOption, type ChatCompletionToolMessageParam as ChatCompletionToolMessageParam, type ChatCompletionUserMessageParam as ChatCompletionUserMessageParam, + type ChatCompletionsPage as ChatCompletionsPage, type ChatCompletionCreateParams as ChatCompletionCreateParams, type ChatCompletionCreateParamsNonStreaming as ChatCompletionCreateParamsNonStreaming, type ChatCompletionCreateParamsStreaming as ChatCompletionCreateParamsStreaming, + type ChatCompletionUpdateParams as ChatCompletionUpdateParams, + type ChatCompletionListParams as ChatCompletionListParams, }; export { @@ -890,6 +1022,7 @@ export declare namespace OpenAI { export type ErrorObject = API.ErrorObject; export type FunctionDefinition = API.FunctionDefinition; export type FunctionParameters = API.FunctionParameters; + export type Metadata = API.Metadata; export type ResponseFormatJSONObject = API.ResponseFormatJSONObject; export type ResponseFormatJSONSchema = API.ResponseFormatJSONSchema; export type ResponseFormatText = API.ResponseFormatText; diff --git a/src/index.ts b/src/index.ts index 50ce5b01a..9fb1eeb03 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,13 +2,7 @@ export { OpenAI as default } from './client'; -export { - multipartFormRequestOptions, - maybeMultipartFormRequestOptions, - type Uploadable, - createForm, - toFile, -} from './uploads'; +export { type Uploadable, toFile } from './uploads'; export { APIPromise } from './api-promise'; export { OpenAI, type ClientOptions } from './client'; export { PagePromise } from './pagination'; diff --git a/src/internal/builtin-types.ts b/src/internal/builtin-types.ts index ca1be792c..b2e598a81 100644 --- a/src/internal/builtin-types.ts +++ b/src/internal/builtin-types.ts @@ -1,6 +1,6 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -export type Fetch = typeof fetch; +export type Fetch = (input: string | URL | Request, init?: RequestInit) => Promise; /** * An alias to the builtin `RequestInit` type so we can diff --git a/src/internal/decoders/line.ts b/src/internal/decoders/line.ts index 1e0bbf390..335ad7e30 100644 --- a/src/internal/decoders/line.ts +++ b/src/internal/decoders/line.ts @@ -1,6 +1,6 @@ import { OpenAIError } from '../../error'; -type Bytes = string | ArrayBuffer | Uint8Array | Buffer | null | undefined; +export type Bytes = string | ArrayBuffer | Uint8Array | null | undefined; /** * A re-implementation of httpx's `LineDecoder` in Python that handles incrementally @@ -13,52 +13,62 @@ export class LineDecoder { static NEWLINE_CHARS = new Set(['\n', '\r']); static NEWLINE_REGEXP = /\r\n|[\n\r]/g; - buffer: string[]; - trailingCR: boolean; - textDecoder: any; // TextDecoder found in browsers; not typed to avoid pulling in either "dom" or "node" types. + buffer: Uint8Array; + #carriageReturnIndex: number | null; + textDecoder: + | undefined + | { + decode(buffer: Uint8Array | ArrayBuffer): string; + }; constructor() { - this.buffer = []; - this.trailingCR = false; + this.buffer = new Uint8Array(); + this.#carriageReturnIndex = null; } decode(chunk: Bytes): string[] { - let text = this.decodeText(chunk); - - if (this.trailingCR) { - text = '\r' + text; - this.trailingCR = false; - } - if (text.endsWith('\r')) { - this.trailingCR = true; - text = text.slice(0, -1); - } - - if (!text) { + if (chunk == null) { return []; } - const trailingNewline = LineDecoder.NEWLINE_CHARS.has(text[text.length - 1] || ''); - let lines = text.split(LineDecoder.NEWLINE_REGEXP); + const binaryChunk = + chunk instanceof ArrayBuffer ? new Uint8Array(chunk) + : typeof chunk === 'string' ? new TextEncoder().encode(chunk) + : chunk; + + let newData = new Uint8Array(this.buffer.length + binaryChunk.length); + newData.set(this.buffer); + newData.set(binaryChunk, this.buffer.length); + this.buffer = newData; + + const lines: string[] = []; + let patternIndex; + while ((patternIndex = findNewlineIndex(this.buffer, this.#carriageReturnIndex)) != null) { + if (patternIndex.carriage && this.#carriageReturnIndex == null) { + // skip until we either get a corresponding `\n`, a new `\r` or nothing + this.#carriageReturnIndex = patternIndex.index; + continue; + } - // if there is a trailing new line then the last entry will be an empty - // string which we don't care about - if (trailingNewline) { - lines.pop(); - } + // we got double \r or \rtext\n + if ( + this.#carriageReturnIndex != null && + (patternIndex.index !== this.#carriageReturnIndex + 1 || patternIndex.carriage) + ) { + lines.push(this.decodeText(this.buffer.slice(0, this.#carriageReturnIndex - 1))); + this.buffer = this.buffer.slice(this.#carriageReturnIndex); + this.#carriageReturnIndex = null; + continue; + } - if (lines.length === 1 && !trailingNewline) { - this.buffer.push(lines[0]!); - return []; - } + const endIndex = + this.#carriageReturnIndex !== null ? patternIndex.preceding - 1 : patternIndex.preceding; - if (this.buffer.length > 0) { - lines = [this.buffer.join('') + lines[0], ...lines.slice(1)]; - this.buffer = []; - } + const line = this.decodeText(this.buffer.slice(0, endIndex)); + lines.push(line); - if (!trailingNewline) { - this.buffer = [lines.pop() || '']; + this.buffer = this.buffer.slice(patternIndex.index); + this.#carriageReturnIndex = null; } return lines; @@ -69,12 +79,12 @@ export class LineDecoder { if (typeof bytes === 'string') return bytes; // Node: - if (typeof Buffer !== 'undefined') { - if (bytes instanceof Buffer) { + if (typeof (globalThis as any).Buffer !== 'undefined') { + if (bytes instanceof (globalThis as any).Buffer) { return bytes.toString(); } if (bytes instanceof Uint8Array) { - return Buffer.from(bytes).toString(); + return (globalThis as any).Buffer.from(bytes).toString(); } throw new OpenAIError( @@ -83,10 +93,10 @@ export class LineDecoder { } // Browser - if (typeof TextDecoder !== 'undefined') { + if (typeof (globalThis as any).TextDecoder !== 'undefined') { if (bytes instanceof Uint8Array || bytes instanceof ArrayBuffer) { - this.textDecoder ??= new TextDecoder('utf8'); - return this.textDecoder.decode(bytes); + this.textDecoder ??= new (globalThis as any).TextDecoder('utf8'); + return this.textDecoder!.decode(bytes); } throw new OpenAIError( @@ -102,13 +112,69 @@ export class LineDecoder { } flush(): string[] { - if (!this.buffer.length && !this.trailingCR) { + if (!this.buffer.length) { return []; } + return this.decode('\n'); + } +} - const lines = [this.buffer.join('')]; - this.buffer = []; - this.trailingCR = false; - return lines; +/** + * This function searches the buffer for the end patterns, (\r or \n) + * and returns an object with the index preceding the matched newline and the + * index after the newline char. `null` is returned if no new line is found. + * + * ```ts + * findNewLineIndex('abc\ndef') -> { preceding: 2, index: 3 } + * ``` + */ +function findNewlineIndex( + buffer: Uint8Array, + startIndex: number | null, +): { preceding: number; index: number; carriage: boolean } | null { + const newline = 0x0a; // \n + const carriage = 0x0d; // \r + + for (let i = startIndex ?? 0; i < buffer.length; i++) { + if (buffer[i] === newline) { + return { preceding: i, index: i + 1, carriage: false }; + } + + if (buffer[i] === carriage) { + return { preceding: i, index: i + 1, carriage: true }; + } } + + return null; +} + +export function findDoubleNewlineIndex(buffer: Uint8Array): number { + // This function searches the buffer for the end patterns (\r\r, \n\n, \r\n\r\n) + // and returns the index right after the first occurrence of any pattern, + // or -1 if none of the patterns are found. + const newline = 0x0a; // \n + const carriage = 0x0d; // \r + + for (let i = 0; i < buffer.length - 1; i++) { + if (buffer[i] === newline && buffer[i + 1] === newline) { + // \n\n + return i + 2; + } + if (buffer[i] === carriage && buffer[i + 1] === carriage) { + // \r\r + return i + 2; + } + if ( + buffer[i] === carriage && + buffer[i + 1] === newline && + i + 3 < buffer.length && + buffer[i + 2] === carriage && + buffer[i + 3] === newline + ) { + // \r\n\r\n + return i + 4; + } + } + + return -1; } diff --git a/src/internal/detect-platform.ts b/src/internal/detect-platform.ts index 24eec519e..c5e273b97 100644 --- a/src/internal/detect-platform.ts +++ b/src/internal/detect-platform.ts @@ -25,7 +25,11 @@ function getDetectedPlatform(): DetectedPlatform { if (typeof EdgeRuntime !== 'undefined') { return 'edge'; } - if (Object.prototype.toString.call(typeof process !== 'undefined' ? process : 0) === '[object process]') { + if ( + Object.prototype.toString.call( + typeof (globalThis as any).process !== 'undefined' ? (globalThis as any).process : 0, + ) === '[object process]' + ) { return 'node'; } return 'unknown'; @@ -73,7 +77,7 @@ const getPlatformProperties = (): PlatformProperties => { 'X-Stainless-OS': 'Unknown', 'X-Stainless-Arch': `other:${EdgeRuntime}`, 'X-Stainless-Runtime': 'edge', - 'X-Stainless-Runtime-Version': process.version, + 'X-Stainless-Runtime-Version': (globalThis as any).process.version, }; } // Check if Node.js @@ -81,10 +85,10 @@ const getPlatformProperties = (): PlatformProperties => { return { 'X-Stainless-Lang': 'js', 'X-Stainless-Package-Version': VERSION, - 'X-Stainless-OS': normalizePlatform(process.platform), - 'X-Stainless-Arch': normalizeArch(process.arch), + 'X-Stainless-OS': normalizePlatform((globalThis as any).process.platform), + 'X-Stainless-Arch': normalizeArch((globalThis as any).process.arch), 'X-Stainless-Runtime': 'node', - 'X-Stainless-Runtime-Version': process.version, + 'X-Stainless-Runtime-Version': (globalThis as any).process.version, }; } diff --git a/src/internal/errors.ts b/src/internal/errors.ts index 08321f317..82c7b14d5 100644 --- a/src/internal/errors.ts +++ b/src/internal/errors.ts @@ -2,14 +2,29 @@ export function isAbortError(err: unknown) { return ( - (err instanceof Error && err.name === 'AbortError') || - (typeof err === 'object' && err && 'name' in err && (err as any).name === 'AbortError') + typeof err === 'object' && + err !== null && + // Spec-compliant fetch implementations + (('name' in err && (err as any).name === 'AbortError') || + // Expo fetch + ('message' in err && String((err as any).message).includes('FetchRequestCanceledException'))) ); } export const castToError = (err: any): Error => { if (err instanceof Error) return err; if (typeof err === 'object' && err !== null) { + try { + if (Object.prototype.toString.call(err) === '[object Error]') { + // @ts-ignore - not all envs have native support for cause yet + const error = new Error(err.message, err.cause ? { cause: err.cause } : {}); + if (err.stack) error.stack = err.stack; + // @ts-ignore - not all envs have native support for cause yet + if (err.cause && !error.cause) error.cause = err.cause; + if (err.name) error.name = err.name; + return error; + } + } catch {} try { return new Error(JSON.stringify(err)); } catch {} diff --git a/src/internal/parse.ts b/src/internal/parse.ts index cf431e4a6..173991a55 100644 --- a/src/internal/parse.ts +++ b/src/internal/parse.ts @@ -1,56 +1,70 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { debug } from './utils/log'; -import { FinalRequestOptions } from './request-options'; +import type { FinalRequestOptions } from './request-options'; import { Stream } from '../streaming'; +import { type OpenAI } from '../client'; +import { formatRequestDetails, loggerFor } from './utils/log'; import type { AbstractPage } from '../pagination'; export type APIResponseProps = { response: Response; options: FinalRequestOptions; controller: AbortController; + requestLogID: string; + retryOfRequestLogID: string | undefined; + startTime: number; }; -export async function defaultParseResponse(props: APIResponseProps): Promise> { - const { response } = props; - if (props.options.stream) { - debug('response', response.status, response.url, response.headers, response.body); +export async function defaultParseResponse( + client: OpenAI, + props: APIResponseProps, +): Promise> { + const { response, requestLogID, retryOfRequestLogID, startTime } = props; + const body = await (async () => { + if (props.options.stream) { + loggerFor(client).debug('response', response.status, response.url, response.headers, response.body); - // Note: there is an invariant here that isn't represented in the type system - // that if you set `stream: true` the response type must also be `Stream` + // Note: there is an invariant here that isn't represented in the type system + // that if you set `stream: true` the response type must also be `Stream` - if (props.options.__streamClass) { - return props.options.__streamClass.fromSSEResponse(response, props.controller) as any; - } - - return Stream.fromSSEResponse(response, props.controller) as any; - } - - // fetch refuses to read the body when the status code is 204. - if (response.status === 204) { - return null as WithRequestID; - } - - if (props.options.__binaryResponse) { - return response as unknown as WithRequestID; - } + if (props.options.__streamClass) { + return props.options.__streamClass.fromSSEResponse(response, props.controller) as any; + } - const contentType = response.headers.get('content-type'); - const isJSON = - contentType?.includes('application/json') || contentType?.includes('application/vnd.api+json'); - if (isJSON) { - const json = await response.json(); + return Stream.fromSSEResponse(response, props.controller) as any; + } - debug('response', response.status, response.url, response.headers, json); + // fetch refuses to read the body when the status code is 204. + if (response.status === 204) { + return null as T; + } - return addRequestID(json as T, response); - } + if (props.options.__binaryResponse) { + return response as unknown as T; + } - const text = await response.text(); - debug('response', response.status, response.url, response.headers, text); + const contentType = response.headers.get('content-type'); + const isJSON = + contentType?.includes('application/json') || contentType?.includes('application/vnd.api+json'); + if (isJSON) { + const json = await response.json(); + return addRequestID(json as T, response); + } - // TODO handle blob, arraybuffer, other content types, etc. - return text as unknown as WithRequestID; + const text = await response.text(); + return text as unknown as T; + })(); + loggerFor(client).debug( + `[${requestLogID}] response parsed`, + formatRequestDetails({ + retryOfRequestLogID, + url: response.url, + status: response.status, + body, + durationMs: Date.now() - startTime, + }), + ); + return body; } export type WithRequestID = diff --git a/src/internal/polyfill/file.node.d.ts b/src/internal/polyfill/file.node.d.ts deleted file mode 100644 index b2a59bfd5..000000000 --- a/src/internal/polyfill/file.node.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * This file polyfills the global `File` object for you if it's not already defined - * when running on Node.js - * - * This is only needed on Node.js v18 & v19. Newer versions already define `File` - * as a global. - */ - -export {}; diff --git a/src/internal/polyfill/file.node.js b/src/internal/polyfill/file.node.js deleted file mode 100644 index eba997e1d..000000000 --- a/src/internal/polyfill/file.node.js +++ /dev/null @@ -1,17 +0,0 @@ -/** - * This file polyfills the global `File` object for you if it's not already defined - * when running on Node.js - * - * This is only needed on Node.js v18 & v19. Newer versions already define `File` - * as a global. - */ - -if (typeof require !== 'undefined') { - if (!globalThis.File) { - try { - // Use [require][0](...) and not require(...) so bundlers don't try to bundle the - // buffer module. - globalThis.File = [require][0]('node:buffer').File; - } catch (e) {} - } -} diff --git a/src/internal/polyfill/file.node.mjs b/src/internal/polyfill/file.node.mjs deleted file mode 100644 index 520dcb84c..000000000 --- a/src/internal/polyfill/file.node.mjs +++ /dev/null @@ -1,9 +0,0 @@ -/** - * This file polyfills the global `File` object for you if it's not already defined - * when running on Node.js - * - * This is only needed on Node.js v18 & v19. Newer versions already define `File` - * as a global. - */ - -import './file.node.js'; diff --git a/src/internal/request-options.ts b/src/internal/request-options.ts index d4a5c66d8..1c7a81e0b 100644 --- a/src/internal/request-options.ts +++ b/src/internal/request-options.ts @@ -2,11 +2,9 @@ import { NullableHeaders } from './headers'; -import type { Agent } from './shims'; import type { BodyInit } from './builtin-types'; -import { isEmptyObj, hasOwn } from './utils/values'; import { Stream } from '../streaming'; -import type { HTTPMethod, KeysEnum } from './types'; +import type { HTTPMethod, MergedRequestInit } from './types'; import { type HeadersLike } from './headers'; export type FinalRequestOptions = RequestOptions & { method: HTTPMethod; path: string }; @@ -20,44 +18,15 @@ export type RequestOptions = { maxRetries?: number; stream?: boolean | undefined; timeout?: number; - httpAgent?: Agent; + fetchOptions?: MergedRequestInit; signal?: AbortSignal | undefined | null; idempotencyKey?: string; + __metadata?: Record; __binaryResponse?: boolean | undefined; __streamClass?: typeof Stream; }; -// This is required so that we can determine if a given object matches the RequestOptions -// type at runtime. While this requires duplication, it is enforced by the TypeScript -// compiler such that any missing / extraneous keys will cause an error. -const requestOptionsKeys: KeysEnum = { - method: true, - path: true, - query: true, - body: true, - headers: true, - - maxRetries: true, - stream: true, - timeout: true, - httpAgent: true, - signal: true, - idempotencyKey: true, - - __binaryResponse: true, - __streamClass: true, -}; - -export const isRequestOptions = (obj: unknown): obj is RequestOptions => { - return ( - typeof obj === 'object' && - obj !== null && - !isEmptyObj(obj) && - Object.keys(obj).every((k) => hasOwn(requestOptionsKeys, k)) - ); -}; - export type EncodedContent = { bodyHeaders: HeadersLike; body: BodyInit }; export type RequestEncoder = (request: { headers: NullableHeaders; body: unknown }) => EncodedContent; diff --git a/src/internal/shims.ts b/src/internal/shims.ts index 4edbafe41..b1196d141 100644 --- a/src/internal/shims.ts +++ b/src/internal/shims.ts @@ -10,21 +10,9 @@ import { type Fetch } from './builtin-types'; import { type ReadableStream } from './shim-types'; -/** - * A minimal copy of the `Agent` type from `undici-types` so we can - * use it in the `ClientOptions` type. - * - * https://nodejs.org/api/http.html#class-httpagent - */ -export interface Agent { - dispatch(options: any, handler: any): boolean; - closed: boolean; - destroyed: boolean; -} - export function getDefaultFetch(): Fetch { if (typeof fetch !== 'undefined') { - return fetch; + return fetch as any; } throw new Error( @@ -32,62 +20,6 @@ export function getDefaultFetch(): Fetch { ); } -/** - * A minimal copy of the NodeJS `stream.Readable` class so that we can - * accept the NodeJS types in certain places, e.g. file uploads - * - * https://nodejs.org/api/stream.html#class-streamreadable - */ -export interface ReadableLike { - readable: boolean; - readonly readableEnded: boolean; - readonly readableFlowing: boolean | null; - readonly readableHighWaterMark: number; - readonly readableLength: number; - readonly readableObjectMode: boolean; - destroyed: boolean; - read(size?: number): any; - pause(): this; - resume(): this; - isPaused(): boolean; - destroy(error?: Error): this; - [Symbol.asyncIterator](): AsyncIterableIterator; -} - -/** - * Determines if the given value looks like a NodeJS `stream.Readable` - * object and that it is readable, i.e. has not been consumed. - * - * https://nodejs.org/api/stream.html#class-streamreadable - */ -export function isReadableLike(value: any) { - // We declare our own class of Readable here, so it's not feasible to - // do an 'instanceof' check. Instead, check for Readable-like properties. - return !!value && value.readable === true && typeof value.read === 'function'; -} - -/** - * A minimal copy of the NodeJS `fs.ReadStream` class for usage within file uploads. - * - * https://nodejs.org/api/fs.html#class-fsreadstream - */ -export interface FsReadStreamLike extends ReadableLike { - path: {}; // real type is string | Buffer but we can't reference `Buffer` here -} - -/** - * Determines if the given value looks like a NodeJS `fs.ReadStream` - * object. - * - * This just checks if the object matches our `Readable` interface - * and defines a `path` property, there may be false positives. - * - * https://nodejs.org/api/fs.html#class-fsreadstream - */ -export function isFsReadStreamLike(value: any): value is FsReadStreamLike { - return isReadableLike(value) && 'path' in value; -} - type ReadableStreamArgs = ConstructorParameters; export function makeReadableStream(...args: ReadableStreamArgs): ReadableStream { @@ -109,7 +41,7 @@ export function ReadableStreamFrom(iterable: Iterable | AsyncIterable): return makeReadableStream({ start() {}, - async pull(controller) { + async pull(controller: any) { const { done, value } = await iter.next(); if (done) { controller.close(); @@ -122,3 +54,54 @@ export function ReadableStreamFrom(iterable: Iterable | AsyncIterable): }, }); } + +/** + * Most browsers don't yet have async iterable support for ReadableStream, + * and Node has a very different way of reading bytes from its "ReadableStream". + * + * This polyfill was pulled from https://github.com/MattiasBuelens/web-streams-polyfill/pull/122#issuecomment-1627354490 + */ +export function ReadableStreamToAsyncIterable(stream: any): AsyncIterableIterator { + if (stream[Symbol.asyncIterator]) return stream; + + const reader = stream.getReader(); + return { + async next() { + try { + const result = await reader.read(); + if (result?.done) reader.releaseLock(); // release lock when stream becomes closed + return result; + } catch (e) { + reader.releaseLock(); // release lock when stream becomes errored + throw e; + } + }, + async return() { + const cancelPromise = reader.cancel(); + reader.releaseLock(); + await cancelPromise; + return { done: true, value: undefined }; + }, + [Symbol.asyncIterator]() { + return this; + }, + }; +} + +/** + * Cancels a ReadableStream we don't need to consume. + * See https://undici.nodejs.org/#/?id=garbage-collection + */ +export async function CancelReadableStream(stream: any): Promise { + if (stream === null || typeof stream !== 'object') return; + + if (stream[Symbol.asyncIterator]) { + await stream[Symbol.asyncIterator]().return?.(); + return; + } + + const reader = stream.getReader(); + const cancelPromise = reader.cancel(); + reader.releaseLock(); + await cancelPromise; +} diff --git a/src/internal/polyfill/crypto.node.d.ts b/src/internal/shims/crypto.node.d.ts similarity index 100% rename from src/internal/polyfill/crypto.node.d.ts rename to src/internal/shims/crypto.node.d.ts diff --git a/src/internal/polyfill/crypto.node.js b/src/internal/shims/crypto.node.js similarity index 100% rename from src/internal/polyfill/crypto.node.js rename to src/internal/shims/crypto.node.js diff --git a/src/internal/polyfill/crypto.node.mjs b/src/internal/shims/crypto.node.mjs similarity index 100% rename from src/internal/polyfill/crypto.node.mjs rename to src/internal/shims/crypto.node.mjs diff --git a/src/internal/shims/file.node.d.ts b/src/internal/shims/file.node.d.ts new file mode 100644 index 000000000..9dc6b2fcc --- /dev/null +++ b/src/internal/shims/file.node.d.ts @@ -0,0 +1,20 @@ +// The infer is to make TS show it as a nice union type, +// instead of literally `ConstructorParameters[0]` +type FallbackBlobSource = ConstructorParameters[0] extends infer T ? T : never; +/** + * A [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File) provides information about files. + */ +declare class FallbackFile extends Blob { + constructor(sources: FallbackBlobSource, fileName: string, options?: any); + /** + * The name of the `File`. + */ + readonly name: string; + /** + * The last modified date of the `File`. + */ + readonly lastModified: number; +} +export type File = InstanceType; +export const File: typeof globalThis extends { File: infer fileConstructor } ? fileConstructor +: typeof FallbackFile; diff --git a/src/internal/shims/file.node.js b/src/internal/shims/file.node.js new file mode 100644 index 000000000..3f8c2ed68 --- /dev/null +++ b/src/internal/shims/file.node.js @@ -0,0 +1,11 @@ +if (typeof require !== 'undefined') { + if (globalThis.File) { + exports.File = globalThis.File; + } else { + try { + // Use [require][0](...) and not require(...) so bundlers don't try to bundle the + // buffer module. + exports.File = [require][0]('node:buffer').File; + } catch (e) {} + } +} diff --git a/src/internal/shims/file.node.mjs b/src/internal/shims/file.node.mjs new file mode 100644 index 000000000..1f103f5d3 --- /dev/null +++ b/src/internal/shims/file.node.mjs @@ -0,0 +1,2 @@ +import * as mod from './file.node.js'; +export const File = globalThis.File || mod.File; diff --git a/src/internal/to-file.ts b/src/internal/to-file.ts new file mode 100644 index 000000000..69b76d3a6 --- /dev/null +++ b/src/internal/to-file.ts @@ -0,0 +1,152 @@ +import { File } from './shims/file.node.js'; +import { BlobPart, getName, makeFile, isAsyncIterable } from './uploads'; +import type { FilePropertyBag } from './builtin-types'; + +type BlobLikePart = string | ArrayBuffer | ArrayBufferView | BlobLike | DataView; + +/** + * Intended to match DOM Blob, node-fetch Blob, node:buffer Blob, etc. + * Don't add arrayBuffer here, node-fetch doesn't have it + */ +interface BlobLike { + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/size) */ + readonly size: number; + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/type) */ + readonly type: string; + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/text) */ + text(): Promise; + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/slice) */ + slice(start?: number, end?: number): BlobLike; +} + +/** + * This check adds the arrayBuffer() method type because it is available and used at runtime + */ +const isBlobLike = (value: any): value is BlobLike & { arrayBuffer(): Promise } => + value != null && + typeof value === 'object' && + typeof value.size === 'number' && + typeof value.type === 'string' && + typeof value.text === 'function' && + typeof value.slice === 'function' && + typeof value.arrayBuffer === 'function'; + +/** + * Intended to match DOM File, node:buffer File, undici File, etc. + */ +interface FileLike extends BlobLike { + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/File/lastModified) */ + readonly lastModified: number; + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/File/name) */ + readonly name?: string | undefined; +} + +/** + * This check adds the arrayBuffer() method type because it is available and used at runtime + */ +const isFileLike = (value: any): value is FileLike & { arrayBuffer(): Promise } => + value != null && + typeof value === 'object' && + typeof value.name === 'string' && + typeof value.lastModified === 'number' && + isBlobLike(value); + +/** + * Intended to match DOM Response, node-fetch Response, undici Response, etc. + */ +export interface ResponseLike { + url: string; + blob(): Promise; +} + +const isResponseLike = (value: any): value is ResponseLike => + value != null && + typeof value === 'object' && + typeof value.url === 'string' && + typeof value.blob === 'function'; + +export type ToFileInput = + | FileLike + | ResponseLike + | Exclude + | AsyncIterable; + +/** + * Helper for creating a {@link File} to pass to an SDK upload method from a variety of different data formats + * @param value the raw content of the file. Can be an {@link Uploadable}, {@link BlobLikePart}, or {@link AsyncIterable} of {@link BlobLikePart}s + * @param {string=} name the name of the file. If omitted, toFile will try to determine a file name from bits if possible + * @param {Object=} options additional properties + * @param {string=} options.type the MIME type of the content + * @param {number=} options.lastModified the last modified timestamp + * @returns a {@link File} with the given properties + */ +export async function toFile( + value: ToFileInput | PromiseLike, + name?: string | null | undefined, + options?: FilePropertyBag | undefined, +): Promise { + // If it's a promise, resolve it. + value = await value; + + // If we've been given a `File` we don't need to do anything + if (isFileLike(value)) { + if (File && value instanceof File) { + return value; + } + return makeFile([await value.arrayBuffer()], value.name); + } + + if (isResponseLike(value)) { + const blob = await value.blob(); + name ||= new URL(value.url).pathname.split(/[\\/]/).pop(); + + return makeFile(await getBytes(blob), name, options); + } + + const parts = await getBytes(value); + + name ||= getName(value); + + if (!options?.type) { + const type = parts.find((part) => typeof part === 'object' && 'type' in part && part.type); + if (typeof type === 'string') { + options = { ...options, type }; + } + } + + return makeFile(parts, name, options); +} + +async function getBytes(value: BlobLikePart | AsyncIterable): Promise> { + let parts: Array = []; + if ( + typeof value === 'string' || + ArrayBuffer.isView(value) || // includes Uint8Array, Buffer, etc. + value instanceof ArrayBuffer + ) { + parts.push(value); + } else if (isBlobLike(value)) { + parts.push(value instanceof Blob ? value : await value.arrayBuffer()); + } else if ( + isAsyncIterable(value) // includes Readable, ReadableStream, etc. + ) { + for await (const chunk of value) { + parts.push(...(await getBytes(chunk as BlobLikePart))); // TODO, consider validating? + } + } else { + const constructor = value?.constructor?.name; + throw new Error( + `Unexpected data type: ${typeof value}${ + constructor ? `; constructor: ${constructor}` : '' + }${propsForError(value)}`, + ); + } + + return parts; +} + +function propsForError(value: unknown): string { + if (typeof value !== 'object' || value === null) return ''; + const props = Object.getOwnPropertyNames(value); + return `; props: [${props.map((p) => `"${p}"`).join(', ')}]`; +} diff --git a/src/internal/types.ts b/src/internal/types.ts index 99740c34f..50c16e9d2 100644 --- a/src/internal/types.ts +++ b/src/internal/types.ts @@ -4,3 +4,95 @@ export type PromiseOrValue = T | Promise; export type HTTPMethod = 'get' | 'post' | 'put' | 'patch' | 'delete'; export type KeysEnum = { [P in keyof Required]: true }; + +type NotAny = [unknown] extends [T] ? never : T; +type Literal = PropertyKey extends T ? never : T; +type MappedLiteralKeys = T extends any ? Literal : never; +type MappedIndex = + T extends any ? + K extends keyof T ? + T[K] + : never + : never; + +/** + * Some environments overload the global fetch function, and Parameters only gets the last signature. + */ +type OverloadedParameters = + T extends ( + { + (...args: infer A): unknown; + (...args: infer B): unknown; + (...args: infer C): unknown; + (...args: infer D): unknown; + } + ) ? + A | B | C | D + : T extends ( + { + (...args: infer A): unknown; + (...args: infer B): unknown; + (...args: infer C): unknown; + } + ) ? + A | B | C + : T extends ( + { + (...args: infer A): unknown; + (...args: infer B): unknown; + } + ) ? + A | B + : T extends (...args: infer A) => unknown ? A + : never; + +/* eslint-disable */ +/** + * These imports attempt to get types from a parent package's dependencies. + * Unresolved bare specifiers can trigger [automatic type acquisition][1] in some projects, which + * would cause typescript to show types not present at runtime. To avoid this, we import + * directly from parent node_modules folders. + * + * We need to check multiple levels because we don't know what directory structure we'll be in. + * For example, pnpm generates directories like this: + * ``` + * node_modules + * ├── .pnpm + * │ └── pkg@1.0.0 + * │ └── node_modules + * │ └── pkg + * │ └── internal + * │ └── types.d.ts + * ├── pkg -> .pnpm/pkg@1.0.0/node_modules/pkg + * └── undici + * ``` + * + * [1]: https://www.typescriptlang.org/tsconfig/#typeAcquisition + */ +/** @ts-ignore For users with \@types/node */ +type UndiciTypesRequestInit = NotAny | NotAny | NotAny | NotAny | NotAny | NotAny | NotAny | NotAny | NotAny | NotAny; +/** @ts-ignore For users with undici */ +type UndiciRequestInit = NotAny | NotAny | NotAny | NotAny | NotAny | NotAny | NotAny | NotAny | NotAny | NotAny; +/** @ts-ignore For users with \@types/bun */ +type BunRequestInit = globalThis.FetchRequestInit; +/** @ts-ignore For users with node-fetch */ +type NodeFetchRequestInit = NotAny | NotAny | NotAny | NotAny | NotAny | NotAny | NotAny | NotAny | NotAny | NotAny; +/** @ts-ignore For users who use Deno */ +type FetchRequestInit = NonNullable[1]>; +/* eslint-enable */ + +type RequestInits = + | NotAny + | NotAny + | NotAny + | NotAny + | NotAny + | NotAny; + +/** + * This type contains `RequestInit` options that may be available on the current runtime, + * including per-platform extensions like `dispatcher`, `agent`, `client`, etc. + */ +export type MergedRequestInit = { + [K in MappedLiteralKeys]?: MappedIndex | undefined; +}; diff --git a/src/internal/uploads.ts b/src/internal/uploads.ts new file mode 100644 index 000000000..2c286497c --- /dev/null +++ b/src/internal/uploads.ts @@ -0,0 +1,178 @@ +import { type RequestOptions } from './request-options'; +import type { FilePropertyBag, Fetch } from './builtin-types'; +import type { OpenAI } from '../client'; +import { File } from './shims/file.node.js'; +import { ReadableStreamFrom } from './shims'; + +export type BlobPart = string | ArrayBuffer | ArrayBufferView | Blob | DataView; +type FsReadStream = AsyncIterable & { path: string | { toString(): string } }; + +// https://github.com/oven-sh/bun/issues/5980 +interface BunFile extends Blob { + readonly name?: string | undefined; +} + +/** + * Typically, this is a native "File" class. + * + * We provide the {@link toFile} utility to convert a variety of objects + * into the File class. + * + * For convenience, you can also pass a fetch Response, or in Node, + * the result of fs.createReadStream(). + */ +export type Uploadable = File | Response | FsReadStream | BunFile; + +/** + * Construct a `File` instance. This is used to ensure a helpful error is thrown + * for environments that don't define a global `File` yet. + */ +export function makeFile( + fileBits: BlobPart[], + fileName: string | undefined, + options?: FilePropertyBag, +): File { + if (typeof File === 'undefined') { + throw new Error('`File` is not defined as a global which is required for file uploads'); + } + + return new File(fileBits as any, fileName ?? 'unknown_file', options); +} + +export function getName(value: any): string | undefined { + return ( + ( + (typeof value === 'object' && + value !== null && + (('name' in value && value.name && String(value.name)) || + ('url' in value && value.url && String(value.url)) || + ('filename' in value && value.filename && String(value.filename)) || + ('path' in value && value.path && String(value.path)))) || + '' + ) + .split(/[\\/]/) + .pop() || undefined + ); +} + +export const isAsyncIterable = (value: any): value is AsyncIterable => + value != null && typeof value === 'object' && typeof value[Symbol.asyncIterator] === 'function'; + +/** + * Returns a multipart/form-data request if any part of the given request body contains a File / Blob value. + * Otherwise returns the request as is. + */ +export const maybeMultipartFormRequestOptions = async ( + opts: RequestOptions, + fetch: OpenAI | Fetch, +): Promise => { + if (!hasUploadableValue(opts.body)) return opts; + + return { ...opts, body: await createForm(opts.body, fetch) }; +}; + +type MultipartFormRequestOptions = Omit & { body: unknown }; + +export const multipartFormRequestOptions = async ( + opts: MultipartFormRequestOptions, + fetch: OpenAI | Fetch, +): Promise => { + return { ...opts, body: await createForm(opts.body, fetch) }; +}; + +const supportsFormDataMap = new WeakMap>(); + +/** + * node-fetch doesn't support the global FormData object in recent node versions. Instead of sending + * properly-encoded form data, it just stringifies the object, resulting in a request body of "[object FormData]". + * This function detects if the fetch function provided supports the global FormData object to avoid + * confusing error messages later on. + */ +function supportsFormData(fetchObject: OpenAI | Fetch): Promise { + const fetch: Fetch = typeof fetchObject === 'function' ? fetchObject : (fetchObject as any).fetch; + const cached = supportsFormDataMap.get(fetch); + if (cached) return cached; + const promise = (async () => { + try { + const FetchResponse = ( + 'Response' in fetch ? + fetch.Response + : (await fetch('data:,')).constructor) as typeof Response; + const data = new FormData(); + if (data.toString() === (await new FetchResponse(data).text())) { + return false; + } + return true; + } catch { + // avoid false negatives + return true; + } + })(); + supportsFormDataMap.set(fetch, promise); + return promise; +} + +export const createForm = async >( + body: T | undefined, + fetch: OpenAI | Fetch, +): Promise => { + if (!(await supportsFormData(fetch))) { + throw new TypeError( + 'The provided fetch function does not support file uploads with the current global FormData class.', + ); + } + const form = new FormData(); + await Promise.all(Object.entries(body || {}).map(([key, value]) => addFormValue(form, key, value))); + return form; +}; + +// We check for Blob not File because Bun.File doesn't inherit from File, +// but they both inherit from Blob and have a `name` property at runtime. +const isNamedBlob = (value: object) => + (File && value instanceof File) || (value instanceof Blob && 'name' in value); + +const isUploadable = (value: unknown) => + typeof value === 'object' && + value !== null && + (value instanceof Response || isAsyncIterable(value) || isNamedBlob(value)); + +const hasUploadableValue = (value: unknown): boolean => { + if (isUploadable(value)) return true; + if (Array.isArray(value)) return value.some(hasUploadableValue); + if (value && typeof value === 'object') { + for (const k in value) { + if (hasUploadableValue((value as any)[k])) return true; + } + } + return false; +}; + +const addFormValue = async (form: FormData, key: string, value: unknown): Promise => { + if (value === undefined) return; + if (value == null) { + throw new TypeError( + `Received null for "${key}"; to pass null in FormData, you must use the string 'null'`, + ); + } + + // TODO: make nested formats configurable + if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { + form.append(key, String(value)); + } else if (value instanceof Response) { + form.append(key, makeFile([await value.blob()], getName(value))); + } else if (isAsyncIterable(value)) { + form.append(key, makeFile([await new Response(ReadableStreamFrom(value)).blob()], getName(value))); + } else if (isNamedBlob(value)) { + form.append(key, value, getName(value)); + } else if (Array.isArray(value)) { + await Promise.all(value.map((entry) => addFormValue(form, key + '[]', entry))); + } else if (typeof value === 'object') { + await Promise.all( + Object.entries(value).map(([name, prop]) => addFormValue(form, `${key}[${name}]`, prop)), + ); + } else { + throw new TypeError( + `Invalid value given to form, expected a string, number, boolean, object, Array, File or Blob but got ${value} instead`, + ); + } +}; diff --git a/src/internal/utils/base64.ts b/src/internal/utils/base64.ts index b45c3b074..978bacde2 100644 --- a/src/internal/utils/base64.ts +++ b/src/internal/utils/base64.ts @@ -2,18 +2,15 @@ import { OpenAIError } from '../../error'; -// @ts-ignore -declare const Buffer: typeof import('node:buffer').Buffer; - export const toBase64 = (data: string | Uint8Array | null | undefined): string => { if (!data) return ''; if (typeof data === 'string') { - data = new TextEncoder().encode(data); + data = new (globalThis as any).TextEncoder().encode(data); } - if (typeof Buffer !== 'undefined') { - return Buffer.from(data).toString('base64'); + if (typeof (globalThis as any).Buffer !== 'undefined') { + return (globalThis as any).Buffer.from(data).toString('base64'); } if (typeof btoa !== 'undefined') { @@ -24,8 +21,8 @@ export const toBase64 = (data: string | Uint8Array | null | undefined): string = }; export const fromBase64 = (str: string): Uint8Array => { - if (typeof Buffer !== 'undefined') { - return new Uint8Array(Buffer.from(str, 'base64')); + if (typeof (globalThis as any).Buffer !== 'undefined') { + return new Uint8Array((globalThis as any).Buffer.from(str, 'base64')); } if (typeof atob !== 'undefined') { diff --git a/src/internal/utils/env.ts b/src/internal/utils/env.ts index d9b1dc4a8..2d8480077 100644 --- a/src/internal/utils/env.ts +++ b/src/internal/utils/env.ts @@ -1,7 +1,5 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -declare const Deno: any; - /** * Read an environment variable. * @@ -10,11 +8,11 @@ declare const Deno: any; * Will return undefined if the environment variable doesn't exist or cannot be accessed. */ export const readEnv = (env: string): string | undefined => { - if (typeof process !== 'undefined') { - return process.env?.[env]?.trim() ?? undefined; + if (typeof (globalThis as any).process !== 'undefined') { + return (globalThis as any).process.env?.[env]?.trim() ?? undefined; } - if (typeof Deno !== 'undefined') { - return Deno.env?.get?.(env)?.trim(); + if (typeof (globalThis as any).Deno !== 'undefined') { + return (globalThis as any).Deno.env?.get?.(env)?.trim(); } return undefined; }; diff --git a/src/internal/utils/log.ts b/src/internal/utils/log.ts index 39b826eb1..f75d46bba 100644 --- a/src/internal/utils/log.ts +++ b/src/internal/utils/log.ts @@ -1,9 +1,98 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { readEnv } from './env'; +import type { LogLevel, Logger } from '../../client'; +import { type OpenAI } from '../../client'; +import { RequestOptions } from '../request-options'; -export function debug(action: string, ...args: any[]) { - if (readEnv('DEBUG') === 'true') { - console.log(`OpenAI:DEBUG:${action}`, ...args); +const levelNumbers = { + off: 0, + error: 200, + warn: 300, + info: 400, + debug: 500, +}; + +function noop() {} + +function makeLogFn(fnLevel: keyof Logger, logger: Logger | undefined, logLevel: LogLevel) { + if (!logger || levelNumbers[fnLevel] > levelNumbers[logLevel]) { + return noop; + } else { + // Don't wrap logger functions, we want the stacktrace intact! + return logger[fnLevel].bind(logger); + } +} + +const noopLogger = { + error: noop, + warn: noop, + info: noop, + debug: noop, +}; + +let cachedLoggers = new WeakMap(); + +export function loggerFor(client: OpenAI): Logger { + const logger = client.logger; + const logLevel = client.logLevel ?? 'off'; + if (!logger) { + return noopLogger; + } + + const cachedLogger = cachedLoggers.get(logger); + if (cachedLogger && cachedLogger[0] === logLevel) { + return cachedLogger[1]; } + + const levelLogger = { + error: makeLogFn('error', logger, logLevel), + warn: makeLogFn('warn', logger, logLevel), + info: makeLogFn('info', logger, logLevel), + debug: makeLogFn('debug', logger, logLevel), + }; + + cachedLoggers.set(logger, [logLevel, levelLogger]); + + return levelLogger; } + +export const formatRequestDetails = (details: { + options?: RequestOptions | undefined; + headers?: Headers | Record | undefined; + retryOfRequestLogID?: string | undefined; + retryOf?: string | undefined; + url?: string | undefined; + status?: number | undefined; + method?: string | undefined; + durationMs?: number | undefined; + message?: unknown; + body?: unknown; +}) => { + if (details.options) { + details.options = { ...details.options }; + delete details.options['headers']; // redundant + leaks internals + } + if (details.headers) { + details.headers = Object.fromEntries( + (details.headers instanceof Headers ? [...details.headers] : Object.entries(details.headers)).map( + ([name, value]) => [ + name, + ( + name.toLowerCase() === 'authorization' || + name.toLowerCase() === 'cookie' || + name.toLowerCase() === 'set-cookie' + ) ? + '***' + : value, + ], + ), + ); + } + if ('retryOfRequestLogID' in details) { + if (details.retryOfRequestLogID) { + details.retryOf = details.retryOfRequestLogID; + } + delete details.retryOfRequestLogID; + } + return details; +}; diff --git a/src/internal/utils/path.ts b/src/internal/utils/path.ts new file mode 100644 index 000000000..b40291903 --- /dev/null +++ b/src/internal/utils/path.ts @@ -0,0 +1,63 @@ +import { OpenAIError } from '../../error'; + +/** + * Percent-encode everything that isn't safe to have in a path without encoding safe chars. + * + * Taken from https://datatracker.ietf.org/doc/html/rfc3986#section-3.3: + * > unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" + * > sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "=" + * > pchar = unreserved / pct-encoded / sub-delims / ":" / "@" + */ +export function encodeURIPath(str: string) { + return str.replace(/[^A-Za-z0-9\-._~!$&'()*+,;=:@]+/g, encodeURIComponent); +} + +export const createPathTagFunction = (pathEncoder = encodeURIPath) => + function path(statics: readonly string[], ...params: readonly unknown[]): string { + // If there are no params, no processing is needed. + if (statics.length === 1) return statics[0]!; + + let postPath = false; + const path = statics.reduce((previousValue, currentValue, index) => { + if (/[?#]/.test(currentValue)) { + postPath = true; + } + return ( + previousValue + + currentValue + + (index === params.length ? '' : (postPath ? encodeURIComponent : pathEncoder)(String(params[index]))) + ); + }, ''); + + const pathOnly = path.split(/[?#]/, 1)[0]!; + const invalidSegments = []; + const invalidSegmentPattern = /(?<=^|\/)(?:\.|%2e){1,2}(?=\/|$)/gi; + let match; + + // Find all invalid segments + while ((match = invalidSegmentPattern.exec(pathOnly)) !== null) { + invalidSegments.push({ + start: match.index, + length: match[0].length, + }); + } + + if (invalidSegments.length > 0) { + let lastEnd = 0; + const underline = invalidSegments.reduce((acc, segment) => { + const spaces = ' '.repeat(segment.start - lastEnd); + const arrows = '^'.repeat(segment.length); + lastEnd = segment.start + segment.length; + return acc + spaces + arrows; + }, ''); + + throw new OpenAIError(`Path parameters result in path with invalid segments:\n${path}\n${underline}`); + } + + return path; + }; + +/** + * URI-encodes path params and ensures no unsafe /./ or /../ path segments are introduced. + */ +export const path = createPathTagFunction(encodeURIPath); diff --git a/src/internal/utils/sleep.ts b/src/internal/utils/sleep.ts index fcf4e0833..65e52962b 100644 --- a/src/internal/utils/sleep.ts +++ b/src/internal/utils/sleep.ts @@ -1,3 +1,3 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); +export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/src/internal/utils/uuid.ts b/src/internal/utils/uuid.ts index 6c43f81dd..1349c42c3 100644 --- a/src/internal/utils/uuid.ts +++ b/src/internal/utils/uuid.ts @@ -1,6 +1,6 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { crypto } from '../polyfill/crypto.node'; +import { crypto } from '../shims/crypto.node.js'; /** * https://stackoverflow.com/a/2117523 diff --git a/src/internal/utils/values.ts b/src/internal/utils/values.ts index 25decb1f8..bb66cfdc1 100644 --- a/src/internal/utils/values.ts +++ b/src/internal/utils/values.ts @@ -26,7 +26,7 @@ export function isEmptyObj(obj: Object | null | undefined): boolean { } // https://eslint.org/docs/latest/rules/no-prototype-builtins -export function hasOwn(obj: Object, key: string): boolean { +export function hasOwn(obj: T, key: PropertyKey): key is keyof T { return Object.prototype.hasOwnProperty.call(obj, key); } diff --git a/src/lib/AbstractChatCompletionRunner.ts b/src/lib/AbstractChatCompletionRunner.ts index 144196aae..9f31ac119 100644 --- a/src/lib/AbstractChatCompletionRunner.ts +++ b/src/lib/AbstractChatCompletionRunner.ts @@ -13,11 +13,8 @@ import { type BaseFunctionsArgs, RunnableToolFunction, } from './RunnableFunction'; -import { ChatCompletionFunctionRunnerParams, ChatCompletionToolRunnerParams } from './ChatCompletionRunner'; -import { - ChatCompletionStreamingFunctionRunnerParams, - ChatCompletionStreamingToolRunnerParams, -} from './ChatCompletionStreamingRunner'; +import { ChatCompletionToolRunnerParams } from './ChatCompletionRunner'; +import { ChatCompletionStreamingToolRunnerParams } from './ChatCompletionStreamingRunner'; import { isAssistantMessage, isFunctionMessage, isToolMessage } from './chatCompletionUtils'; import { BaseEvents, EventStream } from './EventStream'; import { ParsedChatCompletion } from '../resources/beta/chat/completions'; @@ -266,91 +263,6 @@ export class AbstractChatCompletionRunner< return await this._createChatCompletion(client, params, options); } - protected async _runFunctions( - client: OpenAI, - params: - | ChatCompletionFunctionRunnerParams - | ChatCompletionStreamingFunctionRunnerParams, - options?: RunnerOptions, - ) { - const role = 'function' as const; - const { function_call = 'auto', stream, ...restParams } = params; - const singleFunctionToCall = typeof function_call !== 'string' && function_call?.name; - const { maxChatCompletions = DEFAULT_MAX_CHAT_COMPLETIONS } = options || {}; - - const functionsByName: Record> = {}; - for (const f of params.functions) { - functionsByName[f.name || f.function.name] = f; - } - - const functions: ChatCompletionCreateParams.Function[] = params.functions.map( - (f): ChatCompletionCreateParams.Function => ({ - name: f.name || f.function.name, - parameters: f.parameters as Record, - description: f.description, - }), - ); - - for (const message of params.messages) { - this._addMessage(message, false); - } - - for (let i = 0; i < maxChatCompletions; ++i) { - const chatCompletion: ChatCompletion = await this._createChatCompletion( - client, - { - ...restParams, - function_call, - functions, - messages: [...this.messages], - }, - options, - ); - const message = chatCompletion.choices[0]?.message; - if (!message) { - throw new OpenAIError(`missing message in ChatCompletion response`); - } - if (!message.function_call) return; - const { name, arguments: args } = message.function_call; - const fn = functionsByName[name]; - if (!fn) { - const content = `Invalid function_call: ${JSON.stringify(name)}. Available options are: ${functions - .map((f) => JSON.stringify(f.name)) - .join(', ')}. Please try again`; - - this._addMessage({ role, name, content }); - continue; - } else if (singleFunctionToCall && singleFunctionToCall !== name) { - const content = `Invalid function_call: ${JSON.stringify(name)}. ${JSON.stringify( - singleFunctionToCall, - )} requested. Please try again`; - - this._addMessage({ role, name, content }); - continue; - } - - let parsed; - try { - parsed = isRunnableFunctionWithParse(fn) ? await fn.parse(args) : args; - } catch (error) { - this._addMessage({ - role, - name, - content: error instanceof Error ? error.message : String(error), - }); - continue; - } - - // @ts-expect-error it can't rule out `never` type. - const rawContent = await fn.function(parsed, this); - const content = this.#stringifyFunctionCallResult(rawContent); - - this._addMessage({ role, name, content }); - - if (singleFunctionToCall) return; - } - } - protected async _runTools( client: OpenAI, params: diff --git a/src/lib/AssistantStream.ts b/src/lib/AssistantStream.ts index df7dc419d..b9226bf3a 100644 --- a/src/lib/AssistantStream.ts +++ b/src/lib/AssistantStream.ts @@ -368,6 +368,7 @@ export class AssistantStream case 'thread.run.in_progress': case 'thread.run.requires_action': case 'thread.run.completed': + case 'thread.run.incomplete': case 'thread.run.failed': case 'thread.run.cancelling': case 'thread.run.cancelled': @@ -398,6 +399,8 @@ export class AssistantStream throw new Error( 'Encountered an error event in event processing - errors should be processed earlier', ); + default: + assertNever(event); } } @@ -769,3 +772,5 @@ export class AssistantStream return await this._createToolAssistantStream(runs, runId, params, options); } } + +function assertNever(_x: never) {} diff --git a/src/lib/ChatCompletionRunner.ts b/src/lib/ChatCompletionRunner.ts index 9e68e6671..a5edaf741 100644 --- a/src/lib/ChatCompletionRunner.ts +++ b/src/lib/ChatCompletionRunner.ts @@ -2,7 +2,7 @@ import { type ChatCompletionMessageParam, type ChatCompletionCreateParamsNonStreaming, } from '../resources/chat/completions'; -import { type RunnableFunctions, type BaseFunctionsArgs, RunnableTools } from './RunnableFunction'; +import { type BaseFunctionsArgs, RunnableTools } from './RunnableFunction'; import { AbstractChatCompletionRunner, AbstractChatCompletionRunnerEvents, @@ -16,13 +16,6 @@ export interface ChatCompletionRunnerEvents extends AbstractChatCompletionRunner content: (content: string) => void; } -export type ChatCompletionFunctionRunnerParams = Omit< - ChatCompletionCreateParamsNonStreaming, - 'functions' -> & { - functions: RunnableFunctions; -}; - export type ChatCompletionToolRunnerParams = Omit< ChatCompletionCreateParamsNonStreaming, 'tools' @@ -34,21 +27,6 @@ export class ChatCompletionRunner extends AbstractChatCompletion ChatCompletionRunnerEvents, ParsedT > { - /** @deprecated - please use `runTools` instead. */ - static runFunctions( - client: OpenAI, - params: ChatCompletionFunctionRunnerParams, - options?: RunnerOptions, - ): ChatCompletionRunner { - const runner = new ChatCompletionRunner(); - const opts = { - ...options, - headers: { ...options?.headers, 'X-Stainless-Helper-Method': 'runFunctions' }, - }; - runner._run(() => runner._runFunctions(client, params, opts)); - return runner; - } - static runTools( client: OpenAI, params: ChatCompletionToolRunnerParams, diff --git a/src/lib/ChatCompletionStream.ts b/src/lib/ChatCompletionStream.ts index a6747aeb1..ce64e1aad 100644 --- a/src/lib/ChatCompletionStream.ts +++ b/src/lib/ChatCompletionStream.ts @@ -11,6 +11,7 @@ import { type ChatCompletionCreateParams, type ChatCompletionCreateParamsStreaming, type ChatCompletionCreateParamsBase, + type ChatCompletionRole, } from '../resources/chat/completions'; import { AbstractChatCompletionRunner, @@ -797,7 +798,7 @@ export namespace ChatCompletionSnapshot { /** * The role of the author of this message. */ - role?: 'system' | 'user' | 'assistant' | 'function' | 'tool'; + role?: ChatCompletionRole; } export namespace Message { diff --git a/src/lib/ChatCompletionStreamingRunner.ts b/src/lib/ChatCompletionStreamingRunner.ts index 32b66b006..edd25ab93 100644 --- a/src/lib/ChatCompletionStreamingRunner.ts +++ b/src/lib/ChatCompletionStreamingRunner.ts @@ -3,8 +3,8 @@ import { type ChatCompletionCreateParamsStreaming, } from '../resources/chat/completions'; import { RunnerOptions, type AbstractChatCompletionRunnerEvents } from './AbstractChatCompletionRunner'; -import { type ReadableStream } from '../internal/shim-types'; -import { RunnableTools, type BaseFunctionsArgs, type RunnableFunctions } from './RunnableFunction'; +import type { ReadableStream } from '../internal/shim-types'; +import { RunnableTools, type BaseFunctionsArgs } from './RunnableFunction'; import { ChatCompletionSnapshot, ChatCompletionStream } from './ChatCompletionStream'; import OpenAI from '../index'; import { AutoParseableTool } from '../lib/parser'; @@ -14,13 +14,6 @@ export interface ChatCompletionStreamEvents extends AbstractChatCompletionRunner chunk: (chunk: ChatCompletionChunk, snapshot: ChatCompletionSnapshot) => void; } -export type ChatCompletionStreamingFunctionRunnerParams = Omit< - ChatCompletionCreateParamsStreaming, - 'functions' -> & { - functions: RunnableFunctions; -}; - export type ChatCompletionStreamingToolRunnerParams = Omit< ChatCompletionCreateParamsStreaming, 'tools' @@ -38,21 +31,6 @@ export class ChatCompletionStreamingRunner return runner; } - /** @deprecated - please use `runTools` instead. */ - static runFunctions( - client: OpenAI, - params: ChatCompletionStreamingFunctionRunnerParams, - options?: RunnerOptions, - ): ChatCompletionStreamingRunner { - const runner = new ChatCompletionStreamingRunner(null); - const opts = { - ...options, - headers: { ...options?.headers, 'X-Stainless-Helper-Method': 'runFunctions' }, - }; - runner._run(() => runner._runFunctions(client, params, opts)); - return runner; - } - static runTools( client: OpenAI, params: ChatCompletionStreamingToolRunnerParams, diff --git a/src/lib/EventEmitter.ts b/src/lib/EventEmitter.ts new file mode 100644 index 000000000..9adeebdc3 --- /dev/null +++ b/src/lib/EventEmitter.ts @@ -0,0 +1,98 @@ +type EventListener = Events[EventType]; + +type EventListeners = Array<{ + listener: EventListener; + once?: boolean; +}>; + +export type EventParameters = { + [Event in EventType]: EventListener extends (...args: infer P) => any ? P : never; +}[EventType]; + +export class EventEmitter any>> { + #listeners: { + [Event in keyof EventTypes]?: EventListeners; + } = {}; + + /** + * 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, so that calls can be chained + */ + on(event: Event, listener: EventListener): this { + const listeners: EventListeners = + 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, so that calls can be chained + */ + off(event: Event, listener: EventListener): 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, so that calls can be chained + */ + once(event: Event, listener: EventListener): this { + const listeners: EventListeners = + 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< + EventParameters extends [infer Param] ? Param + : EventParameters extends [] ? void + : EventParameters + > { + return new Promise((resolve, reject) => { + // TODO: handle errors + this.once(event, resolve as any); + }); + } + + protected _emit( + this: EventEmitter, + event: Event, + ...args: EventParameters + ) { + const listeners: EventListeners | undefined = this.#listeners[event]; + if (listeners) { + this.#listeners[event] = listeners.filter((l) => !l.once) as any; + listeners.forEach(({ listener }: any) => listener(...(args as any))); + } + } + + protected _hasListener(event: keyof EventTypes): boolean { + const listeners = this.#listeners[event]; + return listeners && listeners.length > 0; + } +} diff --git a/src/lib/RunnableFunction.ts b/src/lib/RunnableFunction.ts index a645f5ebe..4d1519d5a 100644 --- a/src/lib/RunnableFunction.ts +++ b/src/lib/RunnableFunction.ts @@ -85,13 +85,6 @@ export function isRunnableFunctionWithParse( export type BaseFunctionsArgs = readonly (object | string)[]; -export type RunnableFunctions = - [any[]] extends [FunctionsArgs] ? readonly RunnableFunction[] - : { - [Index in keyof FunctionsArgs]: Index extends number ? RunnableFunction - : FunctionsArgs[Index]; - }; - export type RunnableTools = [any[]] extends [FunctionsArgs] ? readonly RunnableToolFunction[] : { @@ -99,28 +92,6 @@ export type RunnableTools = : FunctionsArgs[Index]; }; -/** - * This is helper class for passing a `function` and `parse` where the `function` - * argument type matches the `parse` return type. - * - * @deprecated - please use ParsingToolFunction instead. - */ -export class ParsingFunction { - function: RunnableFunctionWithParse['function']; - parse: RunnableFunctionWithParse['parse']; - parameters: RunnableFunctionWithParse['parameters']; - description: RunnableFunctionWithParse['description']; - name?: RunnableFunctionWithParse['name']; - - constructor(input: RunnableFunctionWithParse) { - this.function = input.function; - this.parse = input.parse; - this.parameters = input.parameters; - this.description = input.description; - this.name = input.name; - } -} - /** * This is helper class for passing a `function` and `parse` where the `function` * argument type matches the `parse` return type. diff --git a/src/lib/parser.ts b/src/lib/parser.ts index f2678e312..a750375dc 100644 --- a/src/lib/parser.ts +++ b/src/lib/parser.ts @@ -119,7 +119,15 @@ export function maybeParseChatCompletion< ...completion, choices: completion.choices.map((choice) => ({ ...choice, - message: { ...choice.message, parsed: null, tool_calls: choice.message.tool_calls ?? [] }, + message: { + ...choice.message, + parsed: null, + ...(choice.message.tool_calls ? + { + tool_calls: choice.message.tool_calls, + } + : undefined), + }, })), }; } @@ -144,7 +152,12 @@ export function parseChatCompletion< ...choice, message: { ...choice.message, - tool_calls: choice.message.tool_calls?.map((toolCall) => parseToolCall(params, toolCall)) ?? [], + ...(choice.message.tool_calls ? + { + tool_calls: + choice.message.tool_calls?.map((toolCall) => parseToolCall(params, toolCall)) ?? undefined, + } + : undefined), parsed: choice.message.content && !choice.message.refusal ? parseResponseFormat(params, choice.message.content) diff --git a/src/pagination.ts b/src/pagination.ts index b45b64eae..fdcc7e131 100644 --- a/src/pagination.ts +++ b/src/pagination.ts @@ -1,10 +1,10 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import type { OpenAI } from './client'; import { OpenAIError } from './error'; import { FinalRequestOptions } from './internal/request-options'; import { defaultParseResponse, WithRequestID } from './internal/parse'; import { APIPromise } from './api-promise'; +import { type OpenAI } from './client'; import { type APIResponseProps } from './internal/parse'; import { maybeObj } from './internal/utils/values'; @@ -46,7 +46,6 @@ export abstract class AbstractPage implements AsyncIterable { } async *iterPages(): AsyncGenerator { - // eslint-disable-next-line @typescript-eslint/no-this-alias let page: this = this; yield page; while (page.hasNextPage()) { @@ -86,12 +85,13 @@ export class PagePromise< Page: new (...args: ConstructorParameters) => PageClass, ) { super( + client, request, - async (props) => + async (client, props) => new Page( client, props.response, - await defaultParseResponse(props), + await defaultParseResponse(client, props), props.options, ) as WithRequestID, ); @@ -144,6 +144,8 @@ export class Page extends AbstractPage implements PageResponse export interface CursorPageResponse { data: Array; + + has_more: boolean; } export interface CursorPageParams { @@ -158,6 +160,8 @@ export class CursorPage { data: Array; + has_more: boolean; + constructor( client: OpenAI, response: Response, @@ -167,12 +171,21 @@ export class CursorPage super(client, response, body, options); this.data = body.data || []; + this.has_more = body.has_more || false; } getPaginatedItems(): Item[] { return this.data ?? []; } + override hasNextPage(): boolean { + if (this.has_more === false) { + return false; + } + + return super.hasNextPage(); + } + nextPageRequestOptions(): PageRequestOptions | null { const data = this.getPaginatedItems(); const id = data[data.length - 1]?.id; diff --git a/src/resources/audio/speech.ts b/src/resources/audio/speech.ts index c65cea1c8..81dc3e47d 100644 --- a/src/resources/audio/speech.ts +++ b/src/resources/audio/speech.ts @@ -2,6 +2,7 @@ import { APIResource } from '../../resource'; import { APIPromise } from '../../api-promise'; +import { buildHeaders } from '../../internal/headers'; import { RequestOptions } from '../../internal/request-options'; export class Speech extends APIResource { @@ -9,7 +10,12 @@ export class Speech extends APIResource { * Generates audio from the input text. */ create(body: SpeechCreateParams, options?: RequestOptions): APIPromise { - return this._client.post('/audio/speech', { body, ...options, __binaryResponse: true }); + return this._client.post('/audio/speech', { + body, + ...options, + headers: buildHeaders([{ Accept: 'application/octet-stream' }, options?.headers]), + __binaryResponse: true, + }); } } @@ -28,12 +34,12 @@ export interface SpeechCreateParams { model: (string & {}) | SpeechModel; /** - * The voice to use when generating the audio. Supported voices are `alloy`, - * `echo`, `fable`, `onyx`, `nova`, and `shimmer`. Previews of the voices are - * available in the + * The voice to use when generating the audio. Supported voices are `alloy`, `ash`, + * `coral`, `echo`, `fable`, `onyx`, `nova`, `sage` and `shimmer`. Previews of the + * voices are available in the * [Text to speech guide](https://platform.openai.com/docs/guides/text-to-speech#voice-options). */ - voice: 'alloy' | 'echo' | 'fable' | 'onyx' | 'nova' | 'shimmer'; + voice: 'alloy' | 'ash' | 'coral' | 'echo' | 'fable' | 'onyx' | 'nova' | 'sage' | 'shimmer'; /** * The format to audio in. Supported formats are `mp3`, `opus`, `aac`, `flac`, diff --git a/src/resources/audio/transcriptions.ts b/src/resources/audio/transcriptions.ts index 648ee7655..253bd1e40 100644 --- a/src/resources/audio/transcriptions.ts +++ b/src/resources/audio/transcriptions.ts @@ -3,8 +3,9 @@ import { APIResource } from '../../resource'; import * as AudioAPI from './audio'; import { APIPromise } from '../../api-promise'; -import { type Uploadable, multipartFormRequestOptions } from '../../uploads'; +import { type Uploadable } from '../../uploads'; import { RequestOptions } from '../../internal/request-options'; +import { multipartFormRequestOptions } from '../../internal/uploads'; export class Transcriptions extends APIResource { /** @@ -27,7 +28,10 @@ export class Transcriptions extends APIResource { body: TranscriptionCreateParams, options?: RequestOptions, ): APIPromise { - return this._client.post('/audio/transcriptions', multipartFormRequestOptions({ body, ...options })); + return this._client.post( + '/audio/transcriptions', + multipartFormRequestOptions({ body, ...options, __metadata: { model: body.model } }, this._client), + ); } } @@ -105,7 +109,7 @@ export interface TranscriptionVerbose { /** * The duration of the input audio. */ - duration: string; + duration: number; /** * The language of the input audio. @@ -168,8 +172,8 @@ export interface TranscriptionCreateParams< /** * The language of the input audio. Supplying the input language in - * [ISO-639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) format will - * improve accuracy and latency. + * [ISO-639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) (e.g. `en`) + * format will improve accuracy and latency. */ language?: string; diff --git a/src/resources/audio/translations.ts b/src/resources/audio/translations.ts index 71c192ce6..4c309e441 100644 --- a/src/resources/audio/translations.ts +++ b/src/resources/audio/translations.ts @@ -4,8 +4,9 @@ import { APIResource } from '../../resource'; import * as AudioAPI from './audio'; import * as TranscriptionsAPI from './transcriptions'; import { APIPromise } from '../../api-promise'; -import { type Uploadable, multipartFormRequestOptions } from '../../uploads'; +import { type Uploadable } from '../../uploads'; import { RequestOptions } from '../../internal/request-options'; +import { multipartFormRequestOptions } from '../../internal/uploads'; export class Translations extends APIResource { /** @@ -25,7 +26,10 @@ export class Translations extends APIResource { body: TranslationCreateParams, options?: RequestOptions, ): APIPromise { - return this._client.post('/audio/translations', multipartFormRequestOptions({ body, ...options })); + return this._client.post( + '/audio/translations', + multipartFormRequestOptions({ body, ...options, __metadata: { model: body.model } }, this._client), + ); } } @@ -37,7 +41,7 @@ export interface TranslationVerbose { /** * The duration of the input audio. */ - duration: string; + duration: number; /** * The language of the output translation (always `english`). diff --git a/src/resources/batches.ts b/src/resources/batches.ts index b69189d0a..db32d782b 100644 --- a/src/resources/batches.ts +++ b/src/resources/batches.ts @@ -2,9 +2,11 @@ import { APIResource } from '../resource'; import * as BatchesAPI from './batches'; +import * as Shared from './shared'; import { APIPromise } from '../api-promise'; import { CursorPage, type CursorPageParams, PagePromise } from '../pagination'; import { RequestOptions } from '../internal/request-options'; +import { path } from '../internal/utils/path'; export class Batches extends APIResource { /** @@ -18,7 +20,7 @@ export class Batches extends APIResource { * Retrieves a batch. */ retrieve(batchID: string, options?: RequestOptions): APIPromise { - return this._client.get(`/batches/${batchID}`, options); + return this._client.get(path`/batches/${batchID}`, options); } /** @@ -37,7 +39,7 @@ export class Batches extends APIResource { * (if any) available in the output file. */ cancel(batchID: string, options?: RequestOptions): APIPromise { - return this._client.post(`/batches/${batchID}/cancel`, options); + return this._client.post(path`/batches/${batchID}/cancel`, options); } } @@ -133,11 +135,13 @@ export interface Batch { /** * Set of 16 key-value pairs that can be attached to an object. This can be useful - * for storing additional information about the object in a structured format. Keys - * can be a maximum of 64 characters long and values can be a maxium of 512 - * characters long. + * for storing additional information about the object in a structured format, and + * querying for objects via API or the dashboard. + * + * Keys are strings with a maximum length of 64 characters. Values are strings with + * a maximum length of 512 characters. */ - metadata?: unknown | null; + metadata?: Shared.Metadata | null; /** * The ID of the file containing the outputs of successfully executed requests. @@ -232,9 +236,14 @@ export interface BatchCreateParams { input_file_id: string; /** - * Optional custom metadata for the batch. + * Set of 16 key-value pairs that can be attached to an object. This can be useful + * for storing additional information about the object in a structured format, and + * querying for objects via API or the dashboard. + * + * Keys are strings with a maximum length of 64 characters. Values are strings with + * a maximum length of 512 characters. */ - metadata?: Record | null; + metadata?: Shared.Metadata | null; } export interface BatchListParams extends CursorPageParams {} diff --git a/src/resources/beta/assistants.ts b/src/resources/beta/assistants.ts index 8ee717fd7..0e109deed 100644 --- a/src/resources/beta/assistants.ts +++ b/src/resources/beta/assistants.ts @@ -10,7 +10,9 @@ import * as RunsAPI from './threads/runs/runs'; import * as StepsAPI from './threads/runs/steps'; import { APIPromise } from '../../api-promise'; import { CursorPage, type CursorPageParams, PagePromise } from '../../pagination'; +import { buildHeaders } from '../../internal/headers'; import { RequestOptions } from '../../internal/request-options'; +import { path } from '../../internal/utils/path'; export class Assistants extends APIResource { /** @@ -20,7 +22,7 @@ export class Assistants extends APIResource { return this._client.post('/assistants', { body, ...options, - headers: { 'OpenAI-Beta': 'assistants=v2', ...options?.headers }, + headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]), }); } @@ -28,9 +30,9 @@ export class Assistants extends APIResource { * Retrieves an assistant. */ retrieve(assistantID: string, options?: RequestOptions): APIPromise { - return this._client.get(`/assistants/${assistantID}`, { + return this._client.get(path`/assistants/${assistantID}`, { ...options, - headers: { 'OpenAI-Beta': 'assistants=v2', ...options?.headers }, + headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]), }); } @@ -38,10 +40,10 @@ export class Assistants extends APIResource { * Modifies an assistant. */ update(assistantID: string, body: AssistantUpdateParams, options?: RequestOptions): APIPromise { - return this._client.post(`/assistants/${assistantID}`, { + return this._client.post(path`/assistants/${assistantID}`, { body, ...options, - headers: { 'OpenAI-Beta': 'assistants=v2', ...options?.headers }, + headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]), }); } @@ -55,7 +57,7 @@ export class Assistants extends APIResource { return this._client.getAPIList('/assistants', CursorPage, { query, ...options, - headers: { 'OpenAI-Beta': 'assistants=v2', ...options?.headers }, + headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]), }); } @@ -63,9 +65,9 @@ export class Assistants extends APIResource { * Delete an assistant. */ delete(assistantID: string, options?: RequestOptions): APIPromise { - return this._client.delete(`/assistants/${assistantID}`, { + return this._client.delete(path`/assistants/${assistantID}`, { ...options, - headers: { 'OpenAI-Beta': 'assistants=v2', ...options?.headers }, + headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]), }); } } @@ -99,11 +101,13 @@ export interface Assistant { /** * Set of 16 key-value pairs that can be attached to an object. This can be useful - * for storing additional information about the object in a structured format. Keys - * can be a maximum of 64 characters long and values can be a maxium of 512 - * characters long. + * for storing additional information about the object in a structured format, and + * querying for objects via API or the dashboard. + * + * Keys are strings with a maximum length of 64 characters. Values are strings with + * a maximum length of 512 characters. */ - metadata: unknown | null; + metadata: Shared.Metadata | null; /** * ID of the model to use. You can use the @@ -1106,17 +1110,29 @@ export interface AssistantCreateParams { /** * Set of 16 key-value pairs that can be attached to an object. This can be useful - * for storing additional information about the object in a structured format. Keys - * can be a maximum of 64 characters long and values can be a maxium of 512 - * characters long. + * for storing additional information about the object in a structured format, and + * querying for objects via API or the dashboard. + * + * Keys are strings with a maximum length of 64 characters. Values are strings with + * a maximum length of 512 characters. */ - metadata?: unknown | null; + metadata?: Shared.Metadata | null; /** * The name of the assistant. The maximum length is 256 characters. */ name?: string | null; + /** + * **o1 and o3-mini models only** + * + * Constrains effort on reasoning for + * [reasoning models](https://platform.openai.com/docs/guides/reasoning). Currently + * supported values are `low`, `medium`, and `high`. Reducing reasoning effort can + * result in faster responses and fewer tokens used on reasoning in a response. + */ + reasoning_effort?: 'low' | 'medium' | 'high' | null; + /** * Specifies the format that the model must output. Compatible with * [GPT-4o](https://platform.openai.com/docs/models#gpt-4o), @@ -1230,12 +1246,14 @@ export namespace AssistantCreateParams { file_ids?: Array; /** - * Set of 16 key-value pairs that can be attached to a vector store. This can be - * useful for storing additional information about the vector store in a structured - * format. Keys can be a maximum of 64 characters long and values can be a maxium - * of 512 characters long. + * Set of 16 key-value pairs that can be attached to an object. This can be useful + * for storing additional information about the object in a structured format, and + * querying for objects via API or the dashboard. + * + * Keys are strings with a maximum length of 64 characters. Values are strings with + * a maximum length of 512 characters. */ - metadata?: unknown; + metadata?: Shared.Metadata | null; } } } @@ -1255,11 +1273,13 @@ export interface AssistantUpdateParams { /** * Set of 16 key-value pairs that can be attached to an object. This can be useful - * for storing additional information about the object in a structured format. Keys - * can be a maximum of 64 characters long and values can be a maxium of 512 - * characters long. + * for storing additional information about the object in a structured format, and + * querying for objects via API or the dashboard. + * + * Keys are strings with a maximum length of 64 characters. Values are strings with + * a maximum length of 512 characters. */ - metadata?: unknown | null; + metadata?: Shared.Metadata | null; /** * ID of the model to use. You can use the @@ -1268,13 +1288,54 @@ export interface AssistantUpdateParams { * [Model overview](https://platform.openai.com/docs/models) for descriptions of * them. */ - model?: string; + model?: + | (string & {}) + | 'o3-mini' + | 'o3-mini-2025-01-31' + | 'o1' + | 'o1-2024-12-17' + | 'gpt-4o' + | 'gpt-4o-2024-11-20' + | 'gpt-4o-2024-08-06' + | 'gpt-4o-2024-05-13' + | 'gpt-4o-mini' + | 'gpt-4o-mini-2024-07-18' + | 'gpt-4.5-preview' + | 'gpt-4.5-preview-2025-02-27' + | 'gpt-4-turbo' + | 'gpt-4-turbo-2024-04-09' + | 'gpt-4-0125-preview' + | 'gpt-4-turbo-preview' + | 'gpt-4-1106-preview' + | 'gpt-4-vision-preview' + | 'gpt-4' + | 'gpt-4-0314' + | 'gpt-4-0613' + | 'gpt-4-32k' + | 'gpt-4-32k-0314' + | 'gpt-4-32k-0613' + | 'gpt-3.5-turbo' + | 'gpt-3.5-turbo-16k' + | 'gpt-3.5-turbo-0613' + | 'gpt-3.5-turbo-1106' + | 'gpt-3.5-turbo-0125' + | 'gpt-3.5-turbo-16k-0613'; /** * The name of the assistant. The maximum length is 256 characters. */ name?: string | null; + /** + * **o1 and o3-mini models only** + * + * Constrains effort on reasoning for + * [reasoning models](https://platform.openai.com/docs/guides/reasoning). Currently + * supported values are `low`, `medium`, and `high`. Reducing reasoning effort can + * result in faster responses and fewer tokens used on reasoning in a response. + */ + reasoning_effort?: 'low' | 'medium' | 'high' | null; + /** * Specifies the format that the model must output. Compatible with * [GPT-4o](https://platform.openai.com/docs/models#gpt-4o), diff --git a/src/resources/beta/beta.ts b/src/resources/beta/beta.ts index 1d1f8e255..beab540c5 100644 --- a/src/resources/beta/beta.ts +++ b/src/resources/beta/beta.ts @@ -21,6 +21,8 @@ import { RunStreamEvent, ThreadStreamEvent, } from './assistants'; +import * as RealtimeAPI from './realtime/realtime'; +import { Realtime } from './realtime/realtime'; import * as ThreadsAPI from './threads/threads'; import { AssistantResponseFormatOption, @@ -46,7 +48,7 @@ import { OtherFileChunkingStrategyObject, StaticFileChunkingStrategy, StaticFileChunkingStrategyObject, - StaticFileChunkingStrategyParam, + StaticFileChunkingStrategyObjectParam, VectorStore, VectorStoreCreateParams, VectorStoreDeleted, @@ -58,17 +60,21 @@ import { import { Chat } from './chat/chat'; export class Beta extends APIResource { + realtime: RealtimeAPI.Realtime = new RealtimeAPI.Realtime(this._client); vectorStores: VectorStoresAPI.VectorStores = new VectorStoresAPI.VectorStores(this._client); chat: ChatAPI.Chat = new ChatAPI.Chat(this._client); assistants: AssistantsAPI.Assistants = new AssistantsAPI.Assistants(this._client); threads: ThreadsAPI.Threads = new ThreadsAPI.Threads(this._client); } +Beta.Realtime = Realtime; Beta.VectorStores = VectorStores; Beta.Assistants = Assistants; Beta.Threads = Threads; export declare namespace Beta { + export { Realtime as Realtime }; + export { VectorStores as VectorStores, type AutoFileChunkingStrategyParam as AutoFileChunkingStrategyParam, @@ -77,7 +83,7 @@ export declare namespace Beta { type OtherFileChunkingStrategyObject as OtherFileChunkingStrategyObject, type StaticFileChunkingStrategy as StaticFileChunkingStrategy, type StaticFileChunkingStrategyObject as StaticFileChunkingStrategyObject, - type StaticFileChunkingStrategyParam as StaticFileChunkingStrategyParam, + type StaticFileChunkingStrategyObjectParam as StaticFileChunkingStrategyObjectParam, type VectorStore as VectorStore, type VectorStoreDeleted as VectorStoreDeleted, type VectorStoresPage as VectorStoresPage, diff --git a/src/resources/beta/chat/completions.ts b/src/resources/beta/chat/completions.ts index e2423d553..b2e0148bd 100644 --- a/src/resources/beta/chat/completions.ts +++ b/src/resources/beta/chat/completions.ts @@ -3,7 +3,6 @@ import { APIResource } from '../../../resource'; import { ChatCompletionRunner } from '../../../lib/ChatCompletionRunner'; import { ChatCompletionStreamingRunner } from '../../../lib/ChatCompletionStreamingRunner'; -import { RunnerOptions } from '../../../lib/AbstractChatCompletionRunner'; import { ChatCompletionToolRunnerParams } from '../../../lib/ChatCompletionRunner'; import { ChatCompletionStreamingToolRunnerParams } from '../../../lib/ChatCompletionStreamingRunner'; import { ChatCompletionStream, type ChatCompletionStreamParams } from '../../../lib/ChatCompletionStream'; @@ -14,28 +13,21 @@ import { ChatCompletionMessageToolCall, } from '../../chat/completions'; import { ExtractParsedContentFromParams, parseChatCompletion, validateInputTools } from '../../../lib/parser'; +import { RequestOptions } from '../../../internal/request-options'; +import { type APIPromise } from '../../../api-promise'; +import { RunnerOptions } from '../../../lib/AbstractChatCompletionRunner'; -export { - ChatCompletionStreamingRunner, - type ChatCompletionStreamingFunctionRunnerParams, -} from '../../../lib/ChatCompletionStreamingRunner'; +export { ChatCompletionStream, type ChatCompletionStreamParams } from '../../../lib/ChatCompletionStream'; +export { ChatCompletionRunner } from '../../../lib/ChatCompletionRunner'; +export { ChatCompletionStreamingRunner } from '../../../lib/ChatCompletionStreamingRunner'; export { type RunnableFunction, - type RunnableFunctions, type RunnableFunctionWithParse, type RunnableFunctionWithoutParse, - ParsingFunction, ParsingToolFunction, } from '../../../lib/RunnableFunction'; export { type ChatCompletionToolRunnerParams } from '../../../lib/ChatCompletionRunner'; export { type ChatCompletionStreamingToolRunnerParams } from '../../../lib/ChatCompletionStreamingRunner'; -export { ChatCompletionStream, type ChatCompletionStreamParams } from '../../../lib/ChatCompletionStream'; -export { - ChatCompletionRunner, - type ChatCompletionFunctionRunnerParams, -} from '../../../lib/ChatCompletionRunner'; -import { RequestOptions } from '../../../internal/request-options'; -import { type APIPromise } from '../../../index'; export interface ParsedFunction extends ChatCompletionMessageToolCall.Function { parsed_arguments?: unknown; @@ -47,7 +39,7 @@ export interface ParsedFunctionToolCall extends ChatCompletionMessageToolCall { export interface ParsedChatCompletionMessage extends ChatCompletionMessage { parsed: ParsedT | null; - tool_calls: Array; + tool_calls?: Array; } export interface ParsedChoice extends ChatCompletion.Choice { diff --git a/src/resources/beta/index.ts b/src/resources/beta/index.ts index 633df5e82..cedb1791a 100644 --- a/src/resources/beta/index.ts +++ b/src/resources/beta/index.ts @@ -19,6 +19,7 @@ export { type AssistantsPage, } from './assistants'; export { Beta } from './beta'; +export { Realtime } from './realtime/index'; export { Chat } from './chat/index'; export { Threads, @@ -44,7 +45,7 @@ export { type OtherFileChunkingStrategyObject, type StaticFileChunkingStrategy, type StaticFileChunkingStrategyObject, - type StaticFileChunkingStrategyParam, + type StaticFileChunkingStrategyObjectParam, type VectorStore, type VectorStoreDeleted, type VectorStoreCreateParams, diff --git a/src/resources/beta/realtime/index.ts b/src/resources/beta/realtime/index.ts new file mode 100644 index 000000000..66c3ecaae --- /dev/null +++ b/src/resources/beta/realtime/index.ts @@ -0,0 +1,4 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +export { Realtime } from './realtime'; +export { Sessions, type Session, type SessionCreateResponse, type SessionCreateParams } from './sessions'; diff --git a/src/resources/beta/realtime/realtime.ts b/src/resources/beta/realtime/realtime.ts new file mode 100644 index 000000000..5e2b1c833 --- /dev/null +++ b/src/resources/beta/realtime/realtime.ts @@ -0,0 +1,2058 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { APIResource } from '../../../resource'; +import * as RealtimeAPI from './realtime'; +import * as Shared from '../../shared'; +import * as SessionsAPI from './sessions'; +import { + Session as SessionsAPISession, + SessionCreateParams, + SessionCreateResponse, + Sessions, +} from './sessions'; + +export class Realtime extends APIResource { + sessions: SessionsAPI.Sessions = new SessionsAPI.Sessions(this._client); +} + +/** + * Returned when a conversation is created. Emitted right after session creation. + */ +export interface ConversationCreatedEvent { + /** + * The conversation resource. + */ + conversation: ConversationCreatedEvent.Conversation; + + /** + * The unique ID of the server event. + */ + event_id: string; + + /** + * The event type, must be `conversation.created`. + */ + type: 'conversation.created'; +} + +export namespace ConversationCreatedEvent { + /** + * The conversation resource. + */ + export interface Conversation { + /** + * The unique ID of the conversation. + */ + id?: string; + + /** + * The object type, must be `realtime.conversation`. + */ + object?: 'realtime.conversation'; + } +} + +/** + * The item to add to the conversation. + */ +export interface ConversationItem { + /** + * The unique ID of the item, this can be generated by the client to help manage + * server-side context, but is not required because the server will generate one if + * not provided. + */ + id?: string; + + /** + * The arguments of the function call (for `function_call` items). + */ + arguments?: string; + + /** + * The ID of the function call (for `function_call` and `function_call_output` + * items). If passed on a `function_call_output` item, the server will check that a + * `function_call` item with the same ID exists in the conversation history. + */ + call_id?: string; + + /** + * The content of the message, applicable for `message` items. + * + * - Message items of role `system` support only `input_text` content + * - Message items of role `user` support `input_text` and `input_audio` content + * - Message items of role `assistant` support `text` content. + */ + content?: Array; + + /** + * The name of the function being called (for `function_call` items). + */ + name?: string; + + /** + * Identifier for the API object being returned - always `realtime.item`. + */ + object?: 'realtime.item'; + + /** + * The output of the function call (for `function_call_output` items). + */ + output?: string; + + /** + * The role of the message sender (`user`, `assistant`, `system`), only applicable + * for `message` items. + */ + role?: 'user' | 'assistant' | 'system'; + + /** + * The status of the item (`completed`, `incomplete`). These have no effect on the + * conversation, but are accepted for consistency with the + * `conversation.item.created` event. + */ + status?: 'completed' | 'incomplete'; + + /** + * The type of the item (`message`, `function_call`, `function_call_output`). + */ + type?: 'message' | 'function_call' | 'function_call_output'; +} + +export interface ConversationItemContent { + /** + * ID of a previous conversation item to reference (for `item_reference` content + * types in `response.create` events). These can reference both client and server + * created items. + */ + id?: string; + + /** + * Base64-encoded audio bytes, used for `input_audio` content type. + */ + audio?: string; + + /** + * The text content, used for `input_text` and `text` content types. + */ + text?: string; + + /** + * The transcript of the audio, used for `input_audio` content type. + */ + transcript?: string; + + /** + * The content type (`input_text`, `input_audio`, `item_reference`, `text`). + */ + type?: 'input_text' | 'input_audio' | 'item_reference' | 'text'; +} + +/** + * Add a new Item to the Conversation's context, including messages, function + * calls, and function call responses. This event can be used both to populate a + * "history" of the conversation and to add new items mid-stream, but has the + * current limitation that it cannot populate assistant audio messages. + * + * If successful, the server will respond with a `conversation.item.created` event, + * otherwise an `error` event will be sent. + */ +export interface ConversationItemCreateEvent { + /** + * The item to add to the conversation. + */ + item: ConversationItem; + + /** + * The event type, must be `conversation.item.create`. + */ + type: 'conversation.item.create'; + + /** + * Optional client-generated ID used to identify this event. + */ + event_id?: string; + + /** + * The ID of the preceding item after which the new item will be inserted. If not + * set, the new item will be appended to the end of the conversation. If set to + * `root`, the new item will be added to the beginning of the conversation. If set + * to an existing ID, it allows an item to be inserted mid-conversation. If the ID + * cannot be found, an error will be returned and the item will not be added. + */ + previous_item_id?: string; +} + +/** + * Returned when a conversation item is created. There are several scenarios that + * produce this event: + * + * - The server is generating a Response, which if successful will produce either + * one or two Items, which will be of type `message` (role `assistant`) or type + * `function_call`. + * - The input audio buffer has been committed, either by the client or the server + * (in `server_vad` mode). The server will take the content of the input audio + * buffer and add it to a new user message Item. + * - The client has sent a `conversation.item.create` event to add a new Item to + * the Conversation. + */ +export interface ConversationItemCreatedEvent { + /** + * The unique ID of the server event. + */ + event_id: string; + + /** + * The item to add to the conversation. + */ + item: ConversationItem; + + /** + * The ID of the preceding item in the Conversation context, allows the client to + * understand the order of the conversation. + */ + previous_item_id: string; + + /** + * The event type, must be `conversation.item.created`. + */ + type: 'conversation.item.created'; +} + +/** + * Send this event when you want to remove any item from the conversation history. + * The server will respond with a `conversation.item.deleted` event, unless the + * item does not exist in the conversation history, in which case the server will + * respond with an error. + */ +export interface ConversationItemDeleteEvent { + /** + * The ID of the item to delete. + */ + item_id: string; + + /** + * The event type, must be `conversation.item.delete`. + */ + type: 'conversation.item.delete'; + + /** + * Optional client-generated ID used to identify this event. + */ + event_id?: string; +} + +/** + * Returned when an item in the conversation is deleted by the client with a + * `conversation.item.delete` event. This event is used to synchronize the server's + * understanding of the conversation history with the client's view. + */ +export interface ConversationItemDeletedEvent { + /** + * The unique ID of the server event. + */ + event_id: string; + + /** + * The ID of the item that was deleted. + */ + item_id: string; + + /** + * The event type, must be `conversation.item.deleted`. + */ + type: 'conversation.item.deleted'; +} + +/** + * This event is the output of audio transcription for user audio written to the + * user audio buffer. Transcription begins when the input audio buffer is committed + * by the client or server (in `server_vad` mode). Transcription runs + * asynchronously with Response creation, so this event may come before or after + * the Response events. + * + * Realtime API models accept audio natively, and thus input transcription is a + * separate process run on a separate ASR (Automatic Speech Recognition) model, + * currently always `whisper-1`. Thus the transcript may diverge somewhat from the + * model's interpretation, and should be treated as a rough guide. + */ +export interface ConversationItemInputAudioTranscriptionCompletedEvent { + /** + * The index of the content part containing the audio. + */ + content_index: number; + + /** + * The unique ID of the server event. + */ + event_id: string; + + /** + * The ID of the user message item containing the audio. + */ + item_id: string; + + /** + * The transcribed text. + */ + transcript: string; + + /** + * The event type, must be `conversation.item.input_audio_transcription.completed`. + */ + type: 'conversation.item.input_audio_transcription.completed'; +} + +/** + * Returned when input audio transcription is configured, and a transcription + * request for a user message failed. These events are separate from other `error` + * events so that the client can identify the related Item. + */ +export interface ConversationItemInputAudioTranscriptionFailedEvent { + /** + * The index of the content part containing the audio. + */ + content_index: number; + + /** + * Details of the transcription error. + */ + error: ConversationItemInputAudioTranscriptionFailedEvent.Error; + + /** + * The unique ID of the server event. + */ + event_id: string; + + /** + * The ID of the user message item. + */ + item_id: string; + + /** + * The event type, must be `conversation.item.input_audio_transcription.failed`. + */ + type: 'conversation.item.input_audio_transcription.failed'; +} + +export namespace ConversationItemInputAudioTranscriptionFailedEvent { + /** + * Details of the transcription error. + */ + export interface Error { + /** + * Error code, if any. + */ + code?: string; + + /** + * A human-readable error message. + */ + message?: string; + + /** + * Parameter related to the error, if any. + */ + param?: string; + + /** + * The type of error. + */ + type?: string; + } +} + +/** + * Send this event to truncate a previous assistant message’s audio. The server + * will produce audio faster than realtime, so this event is useful when the user + * interrupts to truncate audio that has already been sent to the client but not + * yet played. This will synchronize the server's understanding of the audio with + * the client's playback. + * + * Truncating audio will delete the server-side text transcript to ensure there is + * not text in the context that hasn't been heard by the user. + * + * If successful, the server will respond with a `conversation.item.truncated` + * event. + */ +export interface ConversationItemTruncateEvent { + /** + * Inclusive duration up to which audio is truncated, in milliseconds. If the + * audio_end_ms is greater than the actual audio duration, the server will respond + * with an error. + */ + audio_end_ms: number; + + /** + * The index of the content part to truncate. Set this to 0. + */ + content_index: number; + + /** + * The ID of the assistant message item to truncate. Only assistant message items + * can be truncated. + */ + item_id: string; + + /** + * The event type, must be `conversation.item.truncate`. + */ + type: 'conversation.item.truncate'; + + /** + * Optional client-generated ID used to identify this event. + */ + event_id?: string; +} + +/** + * Returned when an earlier assistant audio message item is truncated by the client + * with a `conversation.item.truncate` event. This event is used to synchronize the + * server's understanding of the audio with the client's playback. + * + * This action will truncate the audio and remove the server-side text transcript + * to ensure there is no text in the context that hasn't been heard by the user. + */ +export interface ConversationItemTruncatedEvent { + /** + * The duration up to which the audio was truncated, in milliseconds. + */ + audio_end_ms: number; + + /** + * The index of the content part that was truncated. + */ + content_index: number; + + /** + * The unique ID of the server event. + */ + event_id: string; + + /** + * The ID of the assistant message item that was truncated. + */ + item_id: string; + + /** + * The event type, must be `conversation.item.truncated`. + */ + type: 'conversation.item.truncated'; +} + +/** + * The item to add to the conversation. + */ +export interface ConversationItemWithReference { + /** + * For an item of type (`message` | `function_call` | `function_call_output`) this + * field allows the client to assign the unique ID of the item. It is not required + * because the server will generate one if not provided. + * + * For an item of type `item_reference`, this field is required and is a reference + * to any item that has previously existed in the conversation. + */ + id?: string; + + /** + * The arguments of the function call (for `function_call` items). + */ + arguments?: string; + + /** + * The ID of the function call (for `function_call` and `function_call_output` + * items). If passed on a `function_call_output` item, the server will check that a + * `function_call` item with the same ID exists in the conversation history. + */ + call_id?: string; + + /** + * The content of the message, applicable for `message` items. + * + * - Message items of role `system` support only `input_text` content + * - Message items of role `user` support `input_text` and `input_audio` content + * - Message items of role `assistant` support `text` content. + */ + content?: Array; + + /** + * The name of the function being called (for `function_call` items). + */ + name?: string; + + /** + * Identifier for the API object being returned - always `realtime.item`. + */ + object?: 'realtime.item'; + + /** + * The output of the function call (for `function_call_output` items). + */ + output?: string; + + /** + * The role of the message sender (`user`, `assistant`, `system`), only applicable + * for `message` items. + */ + role?: 'user' | 'assistant' | 'system'; + + /** + * The status of the item (`completed`, `incomplete`). These have no effect on the + * conversation, but are accepted for consistency with the + * `conversation.item.created` event. + */ + status?: 'completed' | 'incomplete'; + + /** + * The type of the item (`message`, `function_call`, `function_call_output`, + * `item_reference`). + */ + type?: 'message' | 'function_call' | 'function_call_output' | 'item_reference'; +} + +/** + * Returned when an error occurs, which could be a client problem or a server + * problem. Most errors are recoverable and the session will stay open, we + * recommend to implementors to monitor and log error messages by default. + */ +export interface ErrorEvent { + /** + * Details of the error. + */ + error: ErrorEvent.Error; + + /** + * The unique ID of the server event. + */ + event_id: string; + + /** + * The event type, must be `error`. + */ + type: 'error'; +} + +export namespace ErrorEvent { + /** + * Details of the error. + */ + export interface Error { + /** + * A human-readable error message. + */ + message: string; + + /** + * The type of error (e.g., "invalid_request_error", "server_error"). + */ + type: string; + + /** + * Error code, if any. + */ + code?: string | null; + + /** + * The event_id of the client event that caused the error, if applicable. + */ + event_id?: string | null; + + /** + * Parameter related to the error, if any. + */ + param?: string | null; + } +} + +/** + * Send this event to append audio bytes to the input audio buffer. The audio + * buffer is temporary storage you can write to and later commit. In Server VAD + * mode, the audio buffer is used to detect speech and the server will decide when + * to commit. When Server VAD is disabled, you must commit the audio buffer + * manually. + * + * The client may choose how much audio to place in each event up to a maximum of + * 15 MiB, for example streaming smaller chunks from the client may allow the VAD + * to be more responsive. Unlike made other client events, the server will not send + * a confirmation response to this event. + */ +export interface InputAudioBufferAppendEvent { + /** + * Base64-encoded audio bytes. This must be in the format specified by the + * `input_audio_format` field in the session configuration. + */ + audio: string; + + /** + * The event type, must be `input_audio_buffer.append`. + */ + type: 'input_audio_buffer.append'; + + /** + * Optional client-generated ID used to identify this event. + */ + event_id?: string; +} + +/** + * Send this event to clear the audio bytes in the buffer. The server will respond + * with an `input_audio_buffer.cleared` event. + */ +export interface InputAudioBufferClearEvent { + /** + * The event type, must be `input_audio_buffer.clear`. + */ + type: 'input_audio_buffer.clear'; + + /** + * Optional client-generated ID used to identify this event. + */ + event_id?: string; +} + +/** + * Returned when the input audio buffer is cleared by the client with a + * `input_audio_buffer.clear` event. + */ +export interface InputAudioBufferClearedEvent { + /** + * The unique ID of the server event. + */ + event_id: string; + + /** + * The event type, must be `input_audio_buffer.cleared`. + */ + type: 'input_audio_buffer.cleared'; +} + +/** + * Send this event to commit the user input audio buffer, which will create a new + * user message item in the conversation. This event will produce an error if the + * input audio buffer is empty. When in Server VAD mode, the client does not need + * to send this event, the server will commit the audio buffer automatically. + * + * Committing the input audio buffer will trigger input audio transcription (if + * enabled in session configuration), but it will not create a response from the + * model. The server will respond with an `input_audio_buffer.committed` event. + */ +export interface InputAudioBufferCommitEvent { + /** + * The event type, must be `input_audio_buffer.commit`. + */ + type: 'input_audio_buffer.commit'; + + /** + * Optional client-generated ID used to identify this event. + */ + event_id?: string; +} + +/** + * Returned when an input audio buffer is committed, either by the client or + * automatically in server VAD mode. The `item_id` property is the ID of the user + * message item that will be created, thus a `conversation.item.created` event will + * also be sent to the client. + */ +export interface InputAudioBufferCommittedEvent { + /** + * The unique ID of the server event. + */ + event_id: string; + + /** + * The ID of the user message item that will be created. + */ + item_id: string; + + /** + * The ID of the preceding item after which the new item will be inserted. + */ + previous_item_id: string; + + /** + * The event type, must be `input_audio_buffer.committed`. + */ + type: 'input_audio_buffer.committed'; +} + +/** + * Sent by the server when in `server_vad` mode to indicate that speech has been + * detected in the audio buffer. This can happen any time audio is added to the + * buffer (unless speech is already detected). The client may want to use this + * event to interrupt audio playback or provide visual feedback to the user. + * + * The client should expect to receive a `input_audio_buffer.speech_stopped` event + * when speech stops. The `item_id` property is the ID of the user message item + * that will be created when speech stops and will also be included in the + * `input_audio_buffer.speech_stopped` event (unless the client manually commits + * the audio buffer during VAD activation). + */ +export interface InputAudioBufferSpeechStartedEvent { + /** + * Milliseconds from the start of all audio written to the buffer during the + * session when speech was first detected. This will correspond to the beginning of + * audio sent to the model, and thus includes the `prefix_padding_ms` configured in + * the Session. + */ + audio_start_ms: number; + + /** + * The unique ID of the server event. + */ + event_id: string; + + /** + * The ID of the user message item that will be created when speech stops. + */ + item_id: string; + + /** + * The event type, must be `input_audio_buffer.speech_started`. + */ + type: 'input_audio_buffer.speech_started'; +} + +/** + * Returned in `server_vad` mode when the server detects the end of speech in the + * audio buffer. The server will also send an `conversation.item.created` event + * with the user message item that is created from the audio buffer. + */ +export interface InputAudioBufferSpeechStoppedEvent { + /** + * Milliseconds since the session started when speech stopped. This will correspond + * to the end of audio sent to the model, and thus includes the + * `min_silence_duration_ms` configured in the Session. + */ + audio_end_ms: number; + + /** + * The unique ID of the server event. + */ + event_id: string; + + /** + * The ID of the user message item that will be created. + */ + item_id: string; + + /** + * The event type, must be `input_audio_buffer.speech_stopped`. + */ + type: 'input_audio_buffer.speech_stopped'; +} + +/** + * Emitted at the beginning of a Response to indicate the updated rate limits. When + * a Response is created some tokens will be "reserved" for the output tokens, the + * rate limits shown here reflect that reservation, which is then adjusted + * accordingly once the Response is completed. + */ +export interface RateLimitsUpdatedEvent { + /** + * The unique ID of the server event. + */ + event_id: string; + + /** + * List of rate limit information. + */ + rate_limits: Array; + + /** + * The event type, must be `rate_limits.updated`. + */ + type: 'rate_limits.updated'; +} + +export namespace RateLimitsUpdatedEvent { + export interface RateLimit { + /** + * The maximum allowed value for the rate limit. + */ + limit?: number; + + /** + * The name of the rate limit (`requests`, `tokens`). + */ + name?: 'requests' | 'tokens'; + + /** + * The remaining value before the limit is reached. + */ + remaining?: number; + + /** + * Seconds until the rate limit resets. + */ + reset_seconds?: number; + } +} + +/** + * All events that the client can send to the Realtime API + */ +export type RealtimeClientEvent = + | SessionUpdateEvent + | InputAudioBufferAppendEvent + | InputAudioBufferCommitEvent + | InputAudioBufferClearEvent + | ConversationItemCreateEvent + | ConversationItemTruncateEvent + | ConversationItemDeleteEvent + | ResponseCreateEvent + | ResponseCancelEvent; + +/** + * The response resource. + */ +export interface RealtimeResponse { + /** + * The unique ID of the response. + */ + id?: string; + + /** + * Which conversation the response is added to, determined by the `conversation` + * field in the `response.create` event. If `auto`, the response will be added to + * the default conversation and the value of `conversation_id` will be an id like + * `conv_1234`. If `none`, the response will not be added to any conversation and + * the value of `conversation_id` will be `null`. If responses are being triggered + * by server VAD, the response will be added to the default conversation, thus the + * `conversation_id` will be an id like `conv_1234`. + */ + conversation_id?: string; + + /** + * Maximum number of output tokens for a single assistant response, inclusive of + * tool calls, that was used in this response. + */ + max_output_tokens?: number | 'inf'; + + /** + * Set of 16 key-value pairs that can be attached to an object. This can be useful + * for storing additional information about the object in a structured format, and + * querying for objects via API or the dashboard. + * + * Keys are strings with a maximum length of 64 characters. Values are strings with + * a maximum length of 512 characters. + */ + metadata?: Shared.Metadata | null; + + /** + * The set of modalities the model used to respond. If there are multiple + * modalities, the model will pick one, for example if `modalities` is + * `["text", "audio"]`, the model could be responding in either text or audio. + */ + modalities?: Array<'text' | 'audio'>; + + /** + * The object type, must be `realtime.response`. + */ + object?: 'realtime.response'; + + /** + * The list of output items generated by the response. + */ + output?: Array; + + /** + * The format of output audio. Options are `pcm16`, `g711_ulaw`, or `g711_alaw`. + */ + output_audio_format?: 'pcm16' | 'g711_ulaw' | 'g711_alaw'; + + /** + * The final status of the response (`completed`, `cancelled`, `failed`, or + * `incomplete`). + */ + status?: 'completed' | 'cancelled' | 'failed' | 'incomplete'; + + /** + * Additional details about the status. + */ + status_details?: RealtimeResponseStatus; + + /** + * Sampling temperature for the model, limited to [0.6, 1.2]. Defaults to 0.8. + */ + temperature?: number; + + /** + * Usage statistics for the Response, this will correspond to billing. A Realtime + * API session will maintain a conversation context and append new Items to the + * Conversation, thus output from previous turns (text and audio tokens) will + * become the input for later turns. + */ + usage?: RealtimeResponseUsage; + + /** + * The voice the model used to respond. Current voice options are `alloy`, `ash`, + * `ballad`, `coral`, `echo` `sage`, `shimmer` and `verse`. + */ + voice?: 'alloy' | 'ash' | 'ballad' | 'coral' | 'echo' | 'sage' | 'shimmer' | 'verse'; +} + +/** + * Additional details about the status. + */ +export interface RealtimeResponseStatus { + /** + * A description of the error that caused the response to fail, populated when the + * `status` is `failed`. + */ + error?: RealtimeResponseStatus.Error; + + /** + * The reason the Response did not complete. For a `cancelled` Response, one of + * `turn_detected` (the server VAD detected a new start of speech) or + * `client_cancelled` (the client sent a cancel event). For an `incomplete` + * Response, one of `max_output_tokens` or `content_filter` (the server-side safety + * filter activated and cut off the response). + */ + reason?: 'turn_detected' | 'client_cancelled' | 'max_output_tokens' | 'content_filter'; + + /** + * The type of error that caused the response to fail, corresponding with the + * `status` field (`completed`, `cancelled`, `incomplete`, `failed`). + */ + type?: 'completed' | 'cancelled' | 'incomplete' | 'failed'; +} + +export namespace RealtimeResponseStatus { + /** + * A description of the error that caused the response to fail, populated when the + * `status` is `failed`. + */ + export interface Error { + /** + * Error code, if any. + */ + code?: string; + + /** + * The type of error. + */ + type?: string; + } +} + +/** + * Usage statistics for the Response, this will correspond to billing. A Realtime + * API session will maintain a conversation context and append new Items to the + * Conversation, thus output from previous turns (text and audio tokens) will + * become the input for later turns. + */ +export interface RealtimeResponseUsage { + /** + * Details about the input tokens used in the Response. + */ + input_token_details?: RealtimeResponseUsage.InputTokenDetails; + + /** + * The number of input tokens used in the Response, including text and audio + * tokens. + */ + input_tokens?: number; + + /** + * Details about the output tokens used in the Response. + */ + output_token_details?: RealtimeResponseUsage.OutputTokenDetails; + + /** + * The number of output tokens sent in the Response, including text and audio + * tokens. + */ + output_tokens?: number; + + /** + * The total number of tokens in the Response including input and output text and + * audio tokens. + */ + total_tokens?: number; +} + +export namespace RealtimeResponseUsage { + /** + * Details about the input tokens used in the Response. + */ + export interface InputTokenDetails { + /** + * The number of audio tokens used in the Response. + */ + audio_tokens?: number; + + /** + * The number of cached tokens used in the Response. + */ + cached_tokens?: number; + + /** + * The number of text tokens used in the Response. + */ + text_tokens?: number; + } + + /** + * Details about the output tokens used in the Response. + */ + export interface OutputTokenDetails { + /** + * The number of audio tokens used in the Response. + */ + audio_tokens?: number; + + /** + * The number of text tokens used in the Response. + */ + text_tokens?: number; + } +} + +/** + * All events that the Realtime API can send back + */ +export type RealtimeServerEvent = + | ErrorEvent + | SessionCreatedEvent + | SessionUpdatedEvent + | ConversationCreatedEvent + | InputAudioBufferCommittedEvent + | InputAudioBufferClearedEvent + | InputAudioBufferSpeechStartedEvent + | InputAudioBufferSpeechStoppedEvent + | ConversationItemCreatedEvent + | ConversationItemInputAudioTranscriptionCompletedEvent + | ConversationItemInputAudioTranscriptionFailedEvent + | ConversationItemTruncatedEvent + | ConversationItemDeletedEvent + | ResponseCreatedEvent + | ResponseDoneEvent + | ResponseOutputItemAddedEvent + | ResponseOutputItemDoneEvent + | ResponseContentPartAddedEvent + | ResponseContentPartDoneEvent + | ResponseTextDeltaEvent + | ResponseTextDoneEvent + | ResponseAudioTranscriptDeltaEvent + | ResponseAudioTranscriptDoneEvent + | ResponseAudioDeltaEvent + | ResponseAudioDoneEvent + | ResponseFunctionCallArgumentsDeltaEvent + | ResponseFunctionCallArgumentsDoneEvent + | RateLimitsUpdatedEvent; + +/** + * Returned when the model-generated audio is updated. + */ +export interface ResponseAudioDeltaEvent { + /** + * The index of the content part in the item's content array. + */ + content_index: number; + + /** + * Base64-encoded audio data delta. + */ + delta: string; + + /** + * The unique ID of the server event. + */ + event_id: string; + + /** + * The ID of the item. + */ + item_id: string; + + /** + * The index of the output item in the response. + */ + output_index: number; + + /** + * The ID of the response. + */ + response_id: string; + + /** + * The event type, must be `response.audio.delta`. + */ + type: 'response.audio.delta'; +} + +/** + * Returned when the model-generated audio is done. Also emitted when a Response is + * interrupted, incomplete, or cancelled. + */ +export interface ResponseAudioDoneEvent { + /** + * The index of the content part in the item's content array. + */ + content_index: number; + + /** + * The unique ID of the server event. + */ + event_id: string; + + /** + * The ID of the item. + */ + item_id: string; + + /** + * The index of the output item in the response. + */ + output_index: number; + + /** + * The ID of the response. + */ + response_id: string; + + /** + * The event type, must be `response.audio.done`. + */ + type: 'response.audio.done'; +} + +/** + * Returned when the model-generated transcription of audio output is updated. + */ +export interface ResponseAudioTranscriptDeltaEvent { + /** + * The index of the content part in the item's content array. + */ + content_index: number; + + /** + * The transcript delta. + */ + delta: string; + + /** + * The unique ID of the server event. + */ + event_id: string; + + /** + * The ID of the item. + */ + item_id: string; + + /** + * The index of the output item in the response. + */ + output_index: number; + + /** + * The ID of the response. + */ + response_id: string; + + /** + * The event type, must be `response.audio_transcript.delta`. + */ + type: 'response.audio_transcript.delta'; +} + +/** + * Returned when the model-generated transcription of audio output is done + * streaming. Also emitted when a Response is interrupted, incomplete, or + * cancelled. + */ +export interface ResponseAudioTranscriptDoneEvent { + /** + * The index of the content part in the item's content array. + */ + content_index: number; + + /** + * The unique ID of the server event. + */ + event_id: string; + + /** + * The ID of the item. + */ + item_id: string; + + /** + * The index of the output item in the response. + */ + output_index: number; + + /** + * The ID of the response. + */ + response_id: string; + + /** + * The final transcript of the audio. + */ + transcript: string; + + /** + * The event type, must be `response.audio_transcript.done`. + */ + type: 'response.audio_transcript.done'; +} + +/** + * Send this event to cancel an in-progress response. The server will respond with + * a `response.cancelled` event or an error if there is no response to cancel. + */ +export interface ResponseCancelEvent { + /** + * The event type, must be `response.cancel`. + */ + type: 'response.cancel'; + + /** + * Optional client-generated ID used to identify this event. + */ + event_id?: string; + + /** + * A specific response ID to cancel - if not provided, will cancel an in-progress + * response in the default conversation. + */ + response_id?: string; +} + +/** + * Returned when a new content part is added to an assistant message item during + * response generation. + */ +export interface ResponseContentPartAddedEvent { + /** + * The index of the content part in the item's content array. + */ + content_index: number; + + /** + * The unique ID of the server event. + */ + event_id: string; + + /** + * The ID of the item to which the content part was added. + */ + item_id: string; + + /** + * The index of the output item in the response. + */ + output_index: number; + + /** + * The content part that was added. + */ + part: ResponseContentPartAddedEvent.Part; + + /** + * The ID of the response. + */ + response_id: string; + + /** + * The event type, must be `response.content_part.added`. + */ + type: 'response.content_part.added'; +} + +export namespace ResponseContentPartAddedEvent { + /** + * The content part that was added. + */ + export interface Part { + /** + * Base64-encoded audio data (if type is "audio"). + */ + audio?: string; + + /** + * The text content (if type is "text"). + */ + text?: string; + + /** + * The transcript of the audio (if type is "audio"). + */ + transcript?: string; + + /** + * The content type ("text", "audio"). + */ + type?: 'text' | 'audio'; + } +} + +/** + * Returned when a content part is done streaming in an assistant message item. + * Also emitted when a Response is interrupted, incomplete, or cancelled. + */ +export interface ResponseContentPartDoneEvent { + /** + * The index of the content part in the item's content array. + */ + content_index: number; + + /** + * The unique ID of the server event. + */ + event_id: string; + + /** + * The ID of the item. + */ + item_id: string; + + /** + * The index of the output item in the response. + */ + output_index: number; + + /** + * The content part that is done. + */ + part: ResponseContentPartDoneEvent.Part; + + /** + * The ID of the response. + */ + response_id: string; + + /** + * The event type, must be `response.content_part.done`. + */ + type: 'response.content_part.done'; +} + +export namespace ResponseContentPartDoneEvent { + /** + * The content part that is done. + */ + export interface Part { + /** + * Base64-encoded audio data (if type is "audio"). + */ + audio?: string; + + /** + * The text content (if type is "text"). + */ + text?: string; + + /** + * The transcript of the audio (if type is "audio"). + */ + transcript?: string; + + /** + * The content type ("text", "audio"). + */ + type?: 'text' | 'audio'; + } +} + +/** + * This event instructs the server to create a Response, which means triggering + * model inference. When in Server VAD mode, the server will create Responses + * automatically. + * + * A Response will include at least one Item, and may have two, in which case the + * second will be a function call. These Items will be appended to the conversation + * history. + * + * The server will respond with a `response.created` event, events for Items and + * content created, and finally a `response.done` event to indicate the Response is + * complete. + * + * The `response.create` event includes inference configuration like + * `instructions`, and `temperature`. These fields will override the Session's + * configuration for this Response only. + */ +export interface ResponseCreateEvent { + /** + * The event type, must be `response.create`. + */ + type: 'response.create'; + + /** + * Optional client-generated ID used to identify this event. + */ + event_id?: string; + + /** + * Create a new Realtime response with these parameters + */ + response?: ResponseCreateEvent.Response; +} + +export namespace ResponseCreateEvent { + /** + * Create a new Realtime response with these parameters + */ + export interface Response { + /** + * Controls which conversation the response is added to. Currently supports `auto` + * and `none`, with `auto` as the default value. The `auto` value means that the + * contents of the response will be added to the default conversation. Set this to + * `none` to create an out-of-band response which will not add items to default + * conversation. + */ + conversation?: (string & {}) | 'auto' | 'none'; + + /** + * Input items to include in the prompt for the model. Using this field creates a + * new context for this Response instead of using the default conversation. An + * empty array `[]` will clear the context for this Response. Note that this can + * include references to items from the default conversation. + */ + input?: Array; + + /** + * The default system instructions (i.e. system message) prepended to model calls. + * This field allows the client to guide the model on desired responses. The model + * can be instructed on response content and format, (e.g. "be extremely succinct", + * "act friendly", "here are examples of good responses") and on audio behavior + * (e.g. "talk quickly", "inject emotion into your voice", "laugh frequently"). The + * instructions are not guaranteed to be followed by the model, but they provide + * guidance to the model on the desired behavior. + * + * Note that the server sets default instructions which will be used if this field + * is not set and are visible in the `session.created` event at the start of the + * session. + */ + instructions?: string; + + /** + * Maximum number of output tokens for a single assistant response, inclusive of + * tool calls. Provide an integer between 1 and 4096 to limit output tokens, or + * `inf` for the maximum available tokens for a given model. Defaults to `inf`. + */ + max_response_output_tokens?: number | 'inf'; + + /** + * Set of 16 key-value pairs that can be attached to an object. This can be useful + * for storing additional information about the object in a structured format, and + * querying for objects via API or the dashboard. + * + * Keys are strings with a maximum length of 64 characters. Values are strings with + * a maximum length of 512 characters. + */ + metadata?: Shared.Metadata | null; + + /** + * The set of modalities the model can respond with. To disable audio, set this to + * ["text"]. + */ + modalities?: Array<'text' | 'audio'>; + + /** + * The format of output audio. Options are `pcm16`, `g711_ulaw`, or `g711_alaw`. + */ + output_audio_format?: 'pcm16' | 'g711_ulaw' | 'g711_alaw'; + + /** + * Sampling temperature for the model, limited to [0.6, 1.2]. Defaults to 0.8. + */ + temperature?: number; + + /** + * How the model chooses tools. Options are `auto`, `none`, `required`, or specify + * a function, like `{"type": "function", "function": {"name": "my_function"}}`. + */ + tool_choice?: string; + + /** + * Tools (functions) available to the model. + */ + tools?: Array; + + /** + * The voice the model uses to respond. Voice cannot be changed during the session + * once the model has responded with audio at least once. Current voice options are + * `alloy`, `ash`, `ballad`, `coral`, `echo` `sage`, `shimmer` and `verse`. + */ + voice?: 'alloy' | 'ash' | 'ballad' | 'coral' | 'echo' | 'sage' | 'shimmer' | 'verse'; + } + + export namespace Response { + export interface Tool { + /** + * The description of the function, including guidance on when and how to call it, + * and guidance about what to tell the user when calling (if anything). + */ + description?: string; + + /** + * The name of the function. + */ + name?: string; + + /** + * Parameters of the function in JSON Schema. + */ + parameters?: unknown; + + /** + * The type of the tool, i.e. `function`. + */ + type?: 'function'; + } + } +} + +/** + * Returned when a new Response is created. The first event of response creation, + * where the response is in an initial state of `in_progress`. + */ +export interface ResponseCreatedEvent { + /** + * The unique ID of the server event. + */ + event_id: string; + + /** + * The response resource. + */ + response: RealtimeResponse; + + /** + * The event type, must be `response.created`. + */ + type: 'response.created'; +} + +/** + * Returned when a Response is done streaming. Always emitted, no matter the final + * state. The Response object included in the `response.done` event will include + * all output Items in the Response but will omit the raw audio data. + */ +export interface ResponseDoneEvent { + /** + * The unique ID of the server event. + */ + event_id: string; + + /** + * The response resource. + */ + response: RealtimeResponse; + + /** + * The event type, must be `response.done`. + */ + type: 'response.done'; +} + +/** + * Returned when the model-generated function call arguments are updated. + */ +export interface ResponseFunctionCallArgumentsDeltaEvent { + /** + * The ID of the function call. + */ + call_id: string; + + /** + * The arguments delta as a JSON string. + */ + delta: string; + + /** + * The unique ID of the server event. + */ + event_id: string; + + /** + * The ID of the function call item. + */ + item_id: string; + + /** + * The index of the output item in the response. + */ + output_index: number; + + /** + * The ID of the response. + */ + response_id: string; + + /** + * The event type, must be `response.function_call_arguments.delta`. + */ + type: 'response.function_call_arguments.delta'; +} + +/** + * Returned when the model-generated function call arguments are done streaming. + * Also emitted when a Response is interrupted, incomplete, or cancelled. + */ +export interface ResponseFunctionCallArgumentsDoneEvent { + /** + * The final arguments as a JSON string. + */ + arguments: string; + + /** + * The ID of the function call. + */ + call_id: string; + + /** + * The unique ID of the server event. + */ + event_id: string; + + /** + * The ID of the function call item. + */ + item_id: string; + + /** + * The index of the output item in the response. + */ + output_index: number; + + /** + * The ID of the response. + */ + response_id: string; + + /** + * The event type, must be `response.function_call_arguments.done`. + */ + type: 'response.function_call_arguments.done'; +} + +/** + * Returned when a new Item is created during Response generation. + */ +export interface ResponseOutputItemAddedEvent { + /** + * The unique ID of the server event. + */ + event_id: string; + + /** + * The item to add to the conversation. + */ + item: ConversationItem; + + /** + * The index of the output item in the Response. + */ + output_index: number; + + /** + * The ID of the Response to which the item belongs. + */ + response_id: string; + + /** + * The event type, must be `response.output_item.added`. + */ + type: 'response.output_item.added'; +} + +/** + * Returned when an Item is done streaming. Also emitted when a Response is + * interrupted, incomplete, or cancelled. + */ +export interface ResponseOutputItemDoneEvent { + /** + * The unique ID of the server event. + */ + event_id: string; + + /** + * The item to add to the conversation. + */ + item: ConversationItem; + + /** + * The index of the output item in the Response. + */ + output_index: number; + + /** + * The ID of the Response to which the item belongs. + */ + response_id: string; + + /** + * The event type, must be `response.output_item.done`. + */ + type: 'response.output_item.done'; +} + +/** + * Returned when the text value of a "text" content part is updated. + */ +export interface ResponseTextDeltaEvent { + /** + * The index of the content part in the item's content array. + */ + content_index: number; + + /** + * The text delta. + */ + delta: string; + + /** + * The unique ID of the server event. + */ + event_id: string; + + /** + * The ID of the item. + */ + item_id: string; + + /** + * The index of the output item in the response. + */ + output_index: number; + + /** + * The ID of the response. + */ + response_id: string; + + /** + * The event type, must be `response.text.delta`. + */ + type: 'response.text.delta'; +} + +/** + * Returned when the text value of a "text" content part is done streaming. Also + * emitted when a Response is interrupted, incomplete, or cancelled. + */ +export interface ResponseTextDoneEvent { + /** + * The index of the content part in the item's content array. + */ + content_index: number; + + /** + * The unique ID of the server event. + */ + event_id: string; + + /** + * The ID of the item. + */ + item_id: string; + + /** + * The index of the output item in the response. + */ + output_index: number; + + /** + * The ID of the response. + */ + response_id: string; + + /** + * The final text content. + */ + text: string; + + /** + * The event type, must be `response.text.done`. + */ + type: 'response.text.done'; +} + +/** + * Returned when a Session is created. Emitted automatically when a new connection + * is established as the first server event. This event will contain the default + * Session configuration. + */ +export interface SessionCreatedEvent { + /** + * The unique ID of the server event. + */ + event_id: string; + + /** + * Realtime session object configuration. + */ + session: SessionsAPI.Session; + + /** + * The event type, must be `session.created`. + */ + type: 'session.created'; +} + +/** + * Send this event to update the session’s default configuration. The client may + * send this event at any time to update any field, except for `voice`. However, + * note that once a session has been initialized with a particular `model`, it + * can’t be changed to another model using `session.update`. + * + * When the server receives a `session.update`, it will respond with a + * `session.updated` event showing the full, effective configuration. Only the + * fields that are present are updated. To clear a field like `instructions`, pass + * an empty string. + */ +export interface SessionUpdateEvent { + /** + * Realtime session object configuration. + */ + session: SessionUpdateEvent.Session; + + /** + * The event type, must be `session.update`. + */ + type: 'session.update'; + + /** + * Optional client-generated ID used to identify this event. + */ + event_id?: string; +} + +export namespace SessionUpdateEvent { + /** + * Realtime session object configuration. + */ + export interface Session { + /** + * The format of input audio. Options are `pcm16`, `g711_ulaw`, or `g711_alaw`. For + * `pcm16`, input audio must be 16-bit PCM at a 24kHz sample rate, single channel + * (mono), and little-endian byte order. + */ + input_audio_format?: 'pcm16' | 'g711_ulaw' | 'g711_alaw'; + + /** + * Configuration for input audio transcription, defaults to off and can be set to + * `null` to turn off once on. Input audio transcription is not native to the + * model, since the model consumes audio directly. Transcription runs + * asynchronously through + * [OpenAI Whisper transcription](https://platform.openai.com/docs/api-reference/audio/createTranscription) + * and should be treated as rough guidance rather than the representation + * understood by the model. The client can optionally set the language and prompt + * for transcription, these fields will be passed to the Whisper API. + */ + input_audio_transcription?: Session.InputAudioTranscription; + + /** + * The default system instructions (i.e. system message) prepended to model calls. + * This field allows the client to guide the model on desired responses. The model + * can be instructed on response content and format, (e.g. "be extremely succinct", + * "act friendly", "here are examples of good responses") and on audio behavior + * (e.g. "talk quickly", "inject emotion into your voice", "laugh frequently"). The + * instructions are not guaranteed to be followed by the model, but they provide + * guidance to the model on the desired behavior. + * + * Note that the server sets default instructions which will be used if this field + * is not set and are visible in the `session.created` event at the start of the + * session. + */ + instructions?: string; + + /** + * Maximum number of output tokens for a single assistant response, inclusive of + * tool calls. Provide an integer between 1 and 4096 to limit output tokens, or + * `inf` for the maximum available tokens for a given model. Defaults to `inf`. + */ + max_response_output_tokens?: number | 'inf'; + + /** + * The set of modalities the model can respond with. To disable audio, set this to + * ["text"]. + */ + modalities?: Array<'text' | 'audio'>; + + /** + * The Realtime model used for this session. + */ + model?: + | 'gpt-4o-realtime-preview' + | 'gpt-4o-realtime-preview-2024-10-01' + | 'gpt-4o-realtime-preview-2024-12-17' + | 'gpt-4o-mini-realtime-preview' + | 'gpt-4o-mini-realtime-preview-2024-12-17'; + + /** + * The format of output audio. Options are `pcm16`, `g711_ulaw`, or `g711_alaw`. + * For `pcm16`, output audio is sampled at a rate of 24kHz. + */ + output_audio_format?: 'pcm16' | 'g711_ulaw' | 'g711_alaw'; + + /** + * Sampling temperature for the model, limited to [0.6, 1.2]. Defaults to 0.8. + */ + temperature?: number; + + /** + * How the model chooses tools. Options are `auto`, `none`, `required`, or specify + * a function. + */ + tool_choice?: string; + + /** + * Tools (functions) available to the model. + */ + tools?: Array; + + /** + * Configuration for turn detection. Can be set to `null` to turn off. Server VAD + * means that the model will detect the start and end of speech based on audio + * volume and respond at the end of user speech. + */ + turn_detection?: Session.TurnDetection; + + /** + * The voice the model uses to respond. Voice cannot be changed during the session + * once the model has responded with audio at least once. Current voice options are + * `alloy`, `ash`, `ballad`, `coral`, `echo` `sage`, `shimmer` and `verse`. + */ + voice?: 'alloy' | 'ash' | 'ballad' | 'coral' | 'echo' | 'sage' | 'shimmer' | 'verse'; + } + + export namespace Session { + /** + * Configuration for input audio transcription, defaults to off and can be set to + * `null` to turn off once on. Input audio transcription is not native to the + * model, since the model consumes audio directly. Transcription runs + * asynchronously through + * [OpenAI Whisper transcription](https://platform.openai.com/docs/api-reference/audio/createTranscription) + * and should be treated as rough guidance rather than the representation + * understood by the model. The client can optionally set the language and prompt + * for transcription, these fields will be passed to the Whisper API. + */ + export interface InputAudioTranscription { + /** + * The language of the input audio. Supplying the input language in + * [ISO-639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) (e.g. `en`) + * format will improve accuracy and latency. + */ + language?: string; + + /** + * The model to use for transcription, `whisper-1` is the only currently supported + * model. + */ + model?: string; + + /** + * An optional text to guide the model's style or continue a previous audio + * segment. The + * [prompt](https://platform.openai.com/docs/guides/speech-to-text#prompting) + * should match the audio language. + */ + prompt?: string; + } + + export interface Tool { + /** + * The description of the function, including guidance on when and how to call it, + * and guidance about what to tell the user when calling (if anything). + */ + description?: string; + + /** + * The name of the function. + */ + name?: string; + + /** + * Parameters of the function in JSON Schema. + */ + parameters?: unknown; + + /** + * The type of the tool, i.e. `function`. + */ + type?: 'function'; + } + + /** + * Configuration for turn detection. Can be set to `null` to turn off. Server VAD + * means that the model will detect the start and end of speech based on audio + * volume and respond at the end of user speech. + */ + export interface TurnDetection { + /** + * Whether or not to automatically generate a response when a VAD stop event + * occurs. `true` by default. + */ + create_response?: boolean; + + /** + * Whether or not to automatically interrupt any ongoing response with output to + * the default conversation (i.e. `conversation` of `auto`) when a VAD start event + * occurs. `true` by default. + */ + interrupt_response?: boolean; + + /** + * Amount of audio to include before the VAD detected speech (in milliseconds). + * Defaults to 300ms. + */ + prefix_padding_ms?: number; + + /** + * Duration of silence to detect speech stop (in milliseconds). Defaults to 500ms. + * With shorter values the model will respond more quickly, but may jump in on + * short pauses from the user. + */ + silence_duration_ms?: number; + + /** + * Activation threshold for VAD (0.0 to 1.0), this defaults to 0.5. A higher + * threshold will require louder audio to activate the model, and thus might + * perform better in noisy environments. + */ + threshold?: number; + + /** + * Type of turn detection, only `server_vad` is currently supported. + */ + type?: string; + } + } +} + +/** + * Returned when a session is updated with a `session.update` event, unless there + * is an error. + */ +export interface SessionUpdatedEvent { + /** + * The unique ID of the server event. + */ + event_id: string; + + /** + * Realtime session object configuration. + */ + session: SessionsAPI.Session; + + /** + * The event type, must be `session.updated`. + */ + type: 'session.updated'; +} + +Realtime.Sessions = Sessions; + +export declare namespace Realtime { + export { + Sessions as Sessions, + type SessionsAPISession as Session, + type SessionCreateResponse as SessionCreateResponse, + type SessionCreateParams as SessionCreateParams, + }; +} diff --git a/src/resources/beta/realtime/sessions.ts b/src/resources/beta/realtime/sessions.ts new file mode 100644 index 000000000..0ece95bcd --- /dev/null +++ b/src/resources/beta/realtime/sessions.ts @@ -0,0 +1,595 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { APIResource } from '../../../resource'; +import { APIPromise } from '../../../api-promise'; +import { buildHeaders } from '../../../internal/headers'; +import { RequestOptions } from '../../../internal/request-options'; + +export class Sessions extends APIResource { + /** + * Create an ephemeral API token for use in client-side applications with the + * Realtime API. Can be configured with the same session parameters as the + * `session.update` client event. + * + * It responds with a session object, plus a `client_secret` key which contains a + * usable ephemeral API token that can be used to authenticate browser clients for + * the Realtime API. + */ + create(body: SessionCreateParams, options?: RequestOptions): APIPromise { + return this._client.post('/realtime/sessions', { + body, + ...options, + headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]), + }); + } +} + +/** + * Realtime session object configuration. + */ +export interface Session { + /** + * Unique identifier for the session object. + */ + id?: string; + + /** + * The format of input audio. Options are `pcm16`, `g711_ulaw`, or `g711_alaw`. For + * `pcm16`, input audio must be 16-bit PCM at a 24kHz sample rate, single channel + * (mono), and little-endian byte order. + */ + input_audio_format?: 'pcm16' | 'g711_ulaw' | 'g711_alaw'; + + /** + * Configuration for input audio transcription, defaults to off and can be set to + * `null` to turn off once on. Input audio transcription is not native to the + * model, since the model consumes audio directly. Transcription runs + * asynchronously through Whisper and should be treated as rough guidance rather + * than the representation understood by the model. + */ + input_audio_transcription?: Session.InputAudioTranscription; + + /** + * The default system instructions (i.e. system message) prepended to model calls. + * This field allows the client to guide the model on desired responses. The model + * can be instructed on response content and format, (e.g. "be extremely succinct", + * "act friendly", "here are examples of good responses") and on audio behavior + * (e.g. "talk quickly", "inject emotion into your voice", "laugh frequently"). The + * instructions are not guaranteed to be followed by the model, but they provide + * guidance to the model on the desired behavior. + * + * Note that the server sets default instructions which will be used if this field + * is not set and are visible in the `session.created` event at the start of the + * session. + */ + instructions?: string; + + /** + * Maximum number of output tokens for a single assistant response, inclusive of + * tool calls. Provide an integer between 1 and 4096 to limit output tokens, or + * `inf` for the maximum available tokens for a given model. Defaults to `inf`. + */ + max_response_output_tokens?: number | 'inf'; + + /** + * The set of modalities the model can respond with. To disable audio, set this to + * ["text"]. + */ + modalities?: Array<'text' | 'audio'>; + + /** + * The Realtime model used for this session. + */ + model?: + | (string & {}) + | 'gpt-4o-realtime-preview' + | 'gpt-4o-realtime-preview-2024-10-01' + | 'gpt-4o-realtime-preview-2024-12-17' + | 'gpt-4o-mini-realtime-preview' + | 'gpt-4o-mini-realtime-preview-2024-12-17'; + + /** + * The format of output audio. Options are `pcm16`, `g711_ulaw`, or `g711_alaw`. + * For `pcm16`, output audio is sampled at a rate of 24kHz. + */ + output_audio_format?: 'pcm16' | 'g711_ulaw' | 'g711_alaw'; + + /** + * Sampling temperature for the model, limited to [0.6, 1.2]. Defaults to 0.8. + */ + temperature?: number; + + /** + * How the model chooses tools. Options are `auto`, `none`, `required`, or specify + * a function. + */ + tool_choice?: string; + + /** + * Tools (functions) available to the model. + */ + tools?: Array; + + /** + * Configuration for turn detection. Can be set to `null` to turn off. Server VAD + * means that the model will detect the start and end of speech based on audio + * volume and respond at the end of user speech. + */ + turn_detection?: Session.TurnDetection | null; + + /** + * The voice the model uses to respond. Voice cannot be changed during the session + * once the model has responded with audio at least once. Current voice options are + * `alloy`, `ash`, `ballad`, `coral`, `echo` `sage`, `shimmer` and `verse`. + */ + voice?: 'alloy' | 'ash' | 'ballad' | 'coral' | 'echo' | 'sage' | 'shimmer' | 'verse'; +} + +export namespace Session { + /** + * Configuration for input audio transcription, defaults to off and can be set to + * `null` to turn off once on. Input audio transcription is not native to the + * model, since the model consumes audio directly. Transcription runs + * asynchronously through Whisper and should be treated as rough guidance rather + * than the representation understood by the model. + */ + export interface InputAudioTranscription { + /** + * The model to use for transcription, `whisper-1` is the only currently supported + * model. + */ + model?: string; + } + + export interface Tool { + /** + * The description of the function, including guidance on when and how to call it, + * and guidance about what to tell the user when calling (if anything). + */ + description?: string; + + /** + * The name of the function. + */ + name?: string; + + /** + * Parameters of the function in JSON Schema. + */ + parameters?: unknown; + + /** + * The type of the tool, i.e. `function`. + */ + type?: 'function'; + } + + /** + * Configuration for turn detection. Can be set to `null` to turn off. Server VAD + * means that the model will detect the start and end of speech based on audio + * volume and respond at the end of user speech. + */ + export interface TurnDetection { + /** + * Whether or not to automatically generate a response when a VAD stop event + * occurs. `true` by default. + */ + create_response?: boolean; + + /** + * Whether or not to automatically interrupt any ongoing response with output to + * the default conversation (i.e. `conversation` of `auto`) when a VAD start event + * occurs. `true` by default. + */ + interrupt_response?: boolean; + + /** + * Amount of audio to include before the VAD detected speech (in milliseconds). + * Defaults to 300ms. + */ + prefix_padding_ms?: number; + + /** + * Duration of silence to detect speech stop (in milliseconds). Defaults to 500ms. + * With shorter values the model will respond more quickly, but may jump in on + * short pauses from the user. + */ + silence_duration_ms?: number; + + /** + * Activation threshold for VAD (0.0 to 1.0), this defaults to 0.5. A higher + * threshold will require louder audio to activate the model, and thus might + * perform better in noisy environments. + */ + threshold?: number; + + /** + * Type of turn detection, only `server_vad` is currently supported. + */ + type?: 'server_vad'; + } +} + +/** + * A new Realtime session configuration, with an ephermeral key. Default TTL for + * keys is one minute. + */ +export interface SessionCreateResponse { + /** + * Ephemeral key returned by the API. + */ + client_secret: SessionCreateResponse.ClientSecret; + + /** + * The format of input audio. Options are `pcm16`, `g711_ulaw`, or `g711_alaw`. + */ + input_audio_format?: string; + + /** + * Configuration for input audio transcription, defaults to off and can be set to + * `null` to turn off once on. Input audio transcription is not native to the + * model, since the model consumes audio directly. Transcription runs + * asynchronously through Whisper and should be treated as rough guidance rather + * than the representation understood by the model. + */ + input_audio_transcription?: SessionCreateResponse.InputAudioTranscription; + + /** + * The default system instructions (i.e. system message) prepended to model calls. + * This field allows the client to guide the model on desired responses. The model + * can be instructed on response content and format, (e.g. "be extremely succinct", + * "act friendly", "here are examples of good responses") and on audio behavior + * (e.g. "talk quickly", "inject emotion into your voice", "laugh frequently"). The + * instructions are not guaranteed to be followed by the model, but they provide + * guidance to the model on the desired behavior. + * + * Note that the server sets default instructions which will be used if this field + * is not set and are visible in the `session.created` event at the start of the + * session. + */ + instructions?: string; + + /** + * Maximum number of output tokens for a single assistant response, inclusive of + * tool calls. Provide an integer between 1 and 4096 to limit output tokens, or + * `inf` for the maximum available tokens for a given model. Defaults to `inf`. + */ + max_response_output_tokens?: number | 'inf'; + + /** + * The set of modalities the model can respond with. To disable audio, set this to + * ["text"]. + */ + modalities?: Array<'text' | 'audio'>; + + /** + * The format of output audio. Options are `pcm16`, `g711_ulaw`, or `g711_alaw`. + */ + output_audio_format?: string; + + /** + * Sampling temperature for the model, limited to [0.6, 1.2]. Defaults to 0.8. + */ + temperature?: number; + + /** + * How the model chooses tools. Options are `auto`, `none`, `required`, or specify + * a function. + */ + tool_choice?: string; + + /** + * Tools (functions) available to the model. + */ + tools?: Array; + + /** + * Configuration for turn detection. Can be set to `null` to turn off. Server VAD + * means that the model will detect the start and end of speech based on audio + * volume and respond at the end of user speech. + */ + turn_detection?: SessionCreateResponse.TurnDetection; + + /** + * The voice the model uses to respond. Voice cannot be changed during the session + * once the model has responded with audio at least once. Current voice options are + * `alloy`, `ash`, `ballad`, `coral`, `echo` `sage`, `shimmer` and `verse`. + */ + voice?: 'alloy' | 'ash' | 'ballad' | 'coral' | 'echo' | 'sage' | 'shimmer' | 'verse'; +} + +export namespace SessionCreateResponse { + /** + * Ephemeral key returned by the API. + */ + export interface ClientSecret { + /** + * Timestamp for when the token expires. Currently, all tokens expire after one + * minute. + */ + expires_at: number; + + /** + * Ephemeral key usable in client environments to authenticate connections to the + * Realtime API. Use this in client-side environments rather than a standard API + * token, which should only be used server-side. + */ + value: string; + } + + /** + * Configuration for input audio transcription, defaults to off and can be set to + * `null` to turn off once on. Input audio transcription is not native to the + * model, since the model consumes audio directly. Transcription runs + * asynchronously through Whisper and should be treated as rough guidance rather + * than the representation understood by the model. + */ + export interface InputAudioTranscription { + /** + * The model to use for transcription, `whisper-1` is the only currently supported + * model. + */ + model?: string; + } + + export interface Tool { + /** + * The description of the function, including guidance on when and how to call it, + * and guidance about what to tell the user when calling (if anything). + */ + description?: string; + + /** + * The name of the function. + */ + name?: string; + + /** + * Parameters of the function in JSON Schema. + */ + parameters?: unknown; + + /** + * The type of the tool, i.e. `function`. + */ + type?: 'function'; + } + + /** + * Configuration for turn detection. Can be set to `null` to turn off. Server VAD + * means that the model will detect the start and end of speech based on audio + * volume and respond at the end of user speech. + */ + export interface TurnDetection { + /** + * Amount of audio to include before the VAD detected speech (in milliseconds). + * Defaults to 300ms. + */ + prefix_padding_ms?: number; + + /** + * Duration of silence to detect speech stop (in milliseconds). Defaults to 500ms. + * With shorter values the model will respond more quickly, but may jump in on + * short pauses from the user. + */ + silence_duration_ms?: number; + + /** + * Activation threshold for VAD (0.0 to 1.0), this defaults to 0.5. A higher + * threshold will require louder audio to activate the model, and thus might + * perform better in noisy environments. + */ + threshold?: number; + + /** + * Type of turn detection, only `server_vad` is currently supported. + */ + type?: string; + } +} + +export interface SessionCreateParams { + /** + * The format of input audio. Options are `pcm16`, `g711_ulaw`, or `g711_alaw`. For + * `pcm16`, input audio must be 16-bit PCM at a 24kHz sample rate, single channel + * (mono), and little-endian byte order. + */ + input_audio_format?: 'pcm16' | 'g711_ulaw' | 'g711_alaw'; + + /** + * Configuration for input audio transcription, defaults to off and can be set to + * `null` to turn off once on. Input audio transcription is not native to the + * model, since the model consumes audio directly. Transcription runs + * asynchronously through + * [OpenAI Whisper transcription](https://platform.openai.com/docs/api-reference/audio/createTranscription) + * and should be treated as rough guidance rather than the representation + * understood by the model. The client can optionally set the language and prompt + * for transcription, these fields will be passed to the Whisper API. + */ + input_audio_transcription?: SessionCreateParams.InputAudioTranscription; + + /** + * The default system instructions (i.e. system message) prepended to model calls. + * This field allows the client to guide the model on desired responses. The model + * can be instructed on response content and format, (e.g. "be extremely succinct", + * "act friendly", "here are examples of good responses") and on audio behavior + * (e.g. "talk quickly", "inject emotion into your voice", "laugh frequently"). The + * instructions are not guaranteed to be followed by the model, but they provide + * guidance to the model on the desired behavior. + * + * Note that the server sets default instructions which will be used if this field + * is not set and are visible in the `session.created` event at the start of the + * session. + */ + instructions?: string; + + /** + * Maximum number of output tokens for a single assistant response, inclusive of + * tool calls. Provide an integer between 1 and 4096 to limit output tokens, or + * `inf` for the maximum available tokens for a given model. Defaults to `inf`. + */ + max_response_output_tokens?: number | 'inf'; + + /** + * The set of modalities the model can respond with. To disable audio, set this to + * ["text"]. + */ + modalities?: Array<'text' | 'audio'>; + + /** + * The Realtime model used for this session. + */ + model?: + | 'gpt-4o-realtime-preview' + | 'gpt-4o-realtime-preview-2024-10-01' + | 'gpt-4o-realtime-preview-2024-12-17' + | 'gpt-4o-mini-realtime-preview' + | 'gpt-4o-mini-realtime-preview-2024-12-17'; + + /** + * The format of output audio. Options are `pcm16`, `g711_ulaw`, or `g711_alaw`. + * For `pcm16`, output audio is sampled at a rate of 24kHz. + */ + output_audio_format?: 'pcm16' | 'g711_ulaw' | 'g711_alaw'; + + /** + * Sampling temperature for the model, limited to [0.6, 1.2]. Defaults to 0.8. + */ + temperature?: number; + + /** + * How the model chooses tools. Options are `auto`, `none`, `required`, or specify + * a function. + */ + tool_choice?: string; + + /** + * Tools (functions) available to the model. + */ + tools?: Array; + + /** + * Configuration for turn detection. Can be set to `null` to turn off. Server VAD + * means that the model will detect the start and end of speech based on audio + * volume and respond at the end of user speech. + */ + turn_detection?: SessionCreateParams.TurnDetection; + + /** + * The voice the model uses to respond. Voice cannot be changed during the session + * once the model has responded with audio at least once. Current voice options are + * `alloy`, `ash`, `ballad`, `coral`, `echo` `sage`, `shimmer` and `verse`. + */ + voice?: 'alloy' | 'ash' | 'ballad' | 'coral' | 'echo' | 'sage' | 'shimmer' | 'verse'; +} + +export namespace SessionCreateParams { + /** + * Configuration for input audio transcription, defaults to off and can be set to + * `null` to turn off once on. Input audio transcription is not native to the + * model, since the model consumes audio directly. Transcription runs + * asynchronously through + * [OpenAI Whisper transcription](https://platform.openai.com/docs/api-reference/audio/createTranscription) + * and should be treated as rough guidance rather than the representation + * understood by the model. The client can optionally set the language and prompt + * for transcription, these fields will be passed to the Whisper API. + */ + export interface InputAudioTranscription { + /** + * The language of the input audio. Supplying the input language in + * [ISO-639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) (e.g. `en`) + * format will improve accuracy and latency. + */ + language?: string; + + /** + * The model to use for transcription, `whisper-1` is the only currently supported + * model. + */ + model?: string; + + /** + * An optional text to guide the model's style or continue a previous audio + * segment. The + * [prompt](https://platform.openai.com/docs/guides/speech-to-text#prompting) + * should match the audio language. + */ + prompt?: string; + } + + export interface Tool { + /** + * The description of the function, including guidance on when and how to call it, + * and guidance about what to tell the user when calling (if anything). + */ + description?: string; + + /** + * The name of the function. + */ + name?: string; + + /** + * Parameters of the function in JSON Schema. + */ + parameters?: unknown; + + /** + * The type of the tool, i.e. `function`. + */ + type?: 'function'; + } + + /** + * Configuration for turn detection. Can be set to `null` to turn off. Server VAD + * means that the model will detect the start and end of speech based on audio + * volume and respond at the end of user speech. + */ + export interface TurnDetection { + /** + * Whether or not to automatically generate a response when a VAD stop event + * occurs. `true` by default. + */ + create_response?: boolean; + + /** + * Whether or not to automatically interrupt any ongoing response with output to + * the default conversation (i.e. `conversation` of `auto`) when a VAD start event + * occurs. `true` by default. + */ + interrupt_response?: boolean; + + /** + * Amount of audio to include before the VAD detected speech (in milliseconds). + * Defaults to 300ms. + */ + prefix_padding_ms?: number; + + /** + * Duration of silence to detect speech stop (in milliseconds). Defaults to 500ms. + * With shorter values the model will respond more quickly, but may jump in on + * short pauses from the user. + */ + silence_duration_ms?: number; + + /** + * Activation threshold for VAD (0.0 to 1.0), this defaults to 0.5. A higher + * threshold will require louder audio to activate the model, and thus might + * perform better in noisy environments. + */ + threshold?: number; + + /** + * Type of turn detection, only `server_vad` is currently supported. + */ + type?: string; + } +} + +export declare namespace Sessions { + export { + type Session as Session, + type SessionCreateResponse as SessionCreateResponse, + type SessionCreateParams as SessionCreateParams, + }; +} diff --git a/src/resources/beta/threads/messages.ts b/src/resources/beta/threads/messages.ts index 2c613b49d..3bba148b9 100644 --- a/src/resources/beta/threads/messages.ts +++ b/src/resources/beta/threads/messages.ts @@ -1,20 +1,23 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. import { APIResource } from '../../../resource'; +import * as Shared from '../../shared'; import * as AssistantsAPI from '../assistants'; import { APIPromise } from '../../../api-promise'; import { CursorPage, type CursorPageParams, PagePromise } from '../../../pagination'; +import { buildHeaders } from '../../../internal/headers'; import { RequestOptions } from '../../../internal/request-options'; +import { path } from '../../../internal/utils/path'; export class Messages extends APIResource { /** * Create a message. */ create(threadID: string, body: MessageCreateParams, options?: RequestOptions): APIPromise { - return this._client.post(`/threads/${threadID}/messages`, { + return this._client.post(path`/threads/${threadID}/messages`, { body, ...options, - headers: { 'OpenAI-Beta': 'assistants=v2', ...options?.headers }, + headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]), }); } @@ -23,9 +26,9 @@ export class Messages extends APIResource { */ retrieve(messageID: string, params: MessageRetrieveParams, options?: RequestOptions): APIPromise { const { thread_id } = params; - return this._client.get(`/threads/${thread_id}/messages/${messageID}`, { + return this._client.get(path`/threads/${thread_id}/messages/${messageID}`, { ...options, - headers: { 'OpenAI-Beta': 'assistants=v2', ...options?.headers }, + headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]), }); } @@ -34,10 +37,10 @@ export class Messages extends APIResource { */ update(messageID: string, params: MessageUpdateParams, options?: RequestOptions): APIPromise { const { thread_id, ...body } = params; - return this._client.post(`/threads/${thread_id}/messages/${messageID}`, { + return this._client.post(path`/threads/${thread_id}/messages/${messageID}`, { body, ...options, - headers: { 'OpenAI-Beta': 'assistants=v2', ...options?.headers }, + headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]), }); } @@ -49,10 +52,10 @@ export class Messages extends APIResource { query: MessageListParams | null | undefined = {}, options?: RequestOptions, ): PagePromise { - return this._client.getAPIList(`/threads/${threadID}/messages`, CursorPage, { + return this._client.getAPIList(path`/threads/${threadID}/messages`, CursorPage, { query, ...options, - headers: { 'OpenAI-Beta': 'assistants=v2', ...options?.headers }, + headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]), }); } @@ -65,9 +68,9 @@ export class Messages extends APIResource { options?: RequestOptions, ): APIPromise { const { thread_id } = params; - return this._client.delete(`/threads/${thread_id}/messages/${messageID}`, { + return this._client.delete(path`/threads/${thread_id}/messages/${messageID}`, { ...options, - headers: { 'OpenAI-Beta': 'assistants=v2', ...options?.headers }, + headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]), }); } } @@ -396,11 +399,13 @@ export interface Message { /** * Set of 16 key-value pairs that can be attached to an object. This can be useful - * for storing additional information about the object in a structured format. Keys - * can be a maximum of 64 characters long and values can be a maxium of 512 - * characters long. + * for storing additional information about the object in a structured format, and + * querying for objects via API or the dashboard. + * + * Keys are strings with a maximum length of 64 characters. Values are strings with + * a maximum length of 512 characters. */ - metadata: unknown | null; + metadata: Shared.Metadata | null; /** * The object type, which is always `thread.message`. @@ -649,11 +654,13 @@ export interface MessageCreateParams { /** * Set of 16 key-value pairs that can be attached to an object. This can be useful - * for storing additional information about the object in a structured format. Keys - * can be a maximum of 64 characters long and values can be a maxium of 512 - * characters long. + * for storing additional information about the object in a structured format, and + * querying for objects via API or the dashboard. + * + * Keys are strings with a maximum length of 64 characters. Values are strings with + * a maximum length of 512 characters. */ - metadata?: unknown | null; + metadata?: Shared.Metadata | null; } export namespace MessageCreateParams { @@ -696,10 +703,12 @@ export interface MessageUpdateParams { /** * Body param: Set of 16 key-value pairs that can be attached to an object. This * can be useful for storing additional information about the object in a - * structured format. Keys can be a maximum of 64 characters long and values can be - * a maximum of 512 characters long. + * structured format, and querying for objects via API or the dashboard. + * + * Keys are strings with a maximum length of 64 characters. Values are strings with + * a maximum length of 512 characters. */ - metadata?: unknown | null; + metadata?: Shared.Metadata | null; } export interface MessageListParams extends CursorPageParams { diff --git a/src/resources/beta/threads/runs/runs.ts b/src/resources/beta/threads/runs/runs.ts index 1cbf7dde6..1aafffc92 100644 --- a/src/resources/beta/threads/runs/runs.ts +++ b/src/resources/beta/threads/runs/runs.ts @@ -2,6 +2,7 @@ import { APIResource } from '../../../../resource'; import * as RunsAPI from './runs'; +import * as Shared from '../../../shared'; import * as AssistantsAPI from '../../assistants'; import * as ChatAPI from '../../../chat/chat'; import * as MessagesAPI from '../messages'; @@ -34,11 +35,12 @@ import { import { APIPromise } from '../../../../api-promise'; import { CursorPage, type CursorPageParams, PagePromise } from '../../../../pagination'; import { Stream } from '../../../../streaming'; +import { buildHeaders } from '../../../../internal/headers'; import { RequestOptions } from '../../../../internal/request-options'; import { AssistantStream, RunCreateParamsBaseStream } from '../../../../lib/AssistantStream'; import { sleep } from '../../../../internal/utils/sleep'; import { RunSubmitToolOutputsParamsStream } from '../../../../lib/AssistantStream'; -import { buildHeaders } from '../../../../internal/headers'; +import { path } from '../../../../internal/utils/path'; export class Runs extends APIResource { steps: StepsAPI.Steps = new StepsAPI.Steps(this._client); @@ -63,11 +65,11 @@ export class Runs extends APIResource { options?: RequestOptions, ): APIPromise | APIPromise> { const { include, ...body } = params; - return this._client.post(`/threads/${threadID}/runs`, { + return this._client.post(path`/threads/${threadID}/runs`, { query: { include }, body, ...options, - headers: { 'OpenAI-Beta': 'assistants=v2', ...options?.headers }, + headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]), stream: params.stream ?? false, }) as APIPromise | APIPromise>; } @@ -77,9 +79,9 @@ export class Runs extends APIResource { */ retrieve(runID: string, params: RunRetrieveParams, options?: RequestOptions): APIPromise { const { thread_id } = params; - return this._client.get(`/threads/${thread_id}/runs/${runID}`, { + return this._client.get(path`/threads/${thread_id}/runs/${runID}`, { ...options, - headers: { 'OpenAI-Beta': 'assistants=v2', ...options?.headers }, + headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]), }); } @@ -88,10 +90,10 @@ export class Runs extends APIResource { */ update(runID: string, params: RunUpdateParams, options?: RequestOptions): APIPromise { const { thread_id, ...body } = params; - return this._client.post(`/threads/${thread_id}/runs/${runID}`, { + return this._client.post(path`/threads/${thread_id}/runs/${runID}`, { body, ...options, - headers: { 'OpenAI-Beta': 'assistants=v2', ...options?.headers }, + headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]), }); } @@ -103,10 +105,10 @@ export class Runs extends APIResource { query: RunListParams | null | undefined = {}, options?: RequestOptions, ): PagePromise { - return this._client.getAPIList(`/threads/${threadID}/runs`, CursorPage, { + return this._client.getAPIList(path`/threads/${threadID}/runs`, CursorPage, { query, ...options, - headers: { 'OpenAI-Beta': 'assistants=v2', ...options?.headers }, + headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]), }); } @@ -115,9 +117,9 @@ export class Runs extends APIResource { */ cancel(runID: string, params: RunCancelParams, options?: RequestOptions): APIPromise { const { thread_id } = params; - return this._client.post(`/threads/${thread_id}/runs/${runID}/cancel`, { + return this._client.post(path`/threads/${thread_id}/runs/${runID}/cancel`, { ...options, - headers: { 'OpenAI-Beta': 'assistants=v2', ...options?.headers }, + headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]), }); } @@ -238,10 +240,10 @@ export class Runs extends APIResource { options?: RequestOptions, ): APIPromise | APIPromise> { const { thread_id, ...body } = params; - return this._client.post(`/threads/${thread_id}/runs/${runID}/submit_tool_outputs`, { + return this._client.post(path`/threads/${thread_id}/runs/${runID}/submit_tool_outputs`, { body, ...options, - headers: { 'OpenAI-Beta': 'assistants=v2', ...options?.headers }, + headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]), stream: params.stream ?? false, }) as APIPromise | APIPromise>; } @@ -391,11 +393,13 @@ export interface Run { /** * Set of 16 key-value pairs that can be attached to an object. This can be useful - * for storing additional information about the object in a structured format. Keys - * can be a maximum of 64 characters long and values can be a maxium of 512 - * characters long. + * for storing additional information about the object in a structured format, and + * querying for objects via API or the dashboard. + * + * Keys are strings with a maximum length of 64 characters. Values are strings with + * a maximum length of 512 characters. */ - metadata: unknown | null; + metadata: Shared.Metadata | null; /** * The model that the @@ -681,10 +685,12 @@ export interface RunCreateParamsBase { /** * Body param: Set of 16 key-value pairs that can be attached to an object. This * can be useful for storing additional information about the object in a - * structured format. Keys can be a maximum of 64 characters long and values can be - * a maxium of 512 characters long. + * structured format, and querying for objects via API or the dashboard. + * + * Keys are strings with a maximum length of 64 characters. Values are strings with + * a maximum length of 512 characters. */ - metadata?: unknown | null; + metadata?: Shared.Metadata | null; /** * Body param: The ID of the @@ -702,6 +708,16 @@ export interface RunCreateParamsBase { */ parallel_tool_calls?: boolean; + /** + * Body param: **o1 and o3-mini models only** + * + * Constrains effort on reasoning for + * [reasoning models](https://platform.openai.com/docs/guides/reasoning). Currently + * supported values are `low`, `medium`, and `high`. Reducing reasoning effort can + * result in faster responses and fewer tokens used on reasoning in a response. + */ + reasoning_effort?: 'low' | 'medium' | 'high' | null; + /** * Body param: Specifies the format that the model must output. Compatible with * [GPT-4o](https://platform.openai.com/docs/models#gpt-4o), @@ -799,11 +815,13 @@ export namespace RunCreateParams { /** * Set of 16 key-value pairs that can be attached to an object. This can be useful - * for storing additional information about the object in a structured format. Keys - * can be a maximum of 64 characters long and values can be a maxium of 512 - * characters long. + * for storing additional information about the object in a structured format, and + * querying for objects via API or the dashboard. + * + * Keys are strings with a maximum length of 64 characters. Values are strings with + * a maximum length of 512 characters. */ - metadata?: unknown | null; + metadata?: Shared.Metadata | null; } export namespace AdditionalMessage { @@ -889,10 +907,12 @@ export interface RunUpdateParams { /** * Body param: Set of 16 key-value pairs that can be attached to an object. This * can be useful for storing additional information about the object in a - * structured format. Keys can be a maximum of 64 characters long and values can be - * a maximum of 512 characters long. + * structured format, and querying for objects via API or the dashboard. + * + * Keys are strings with a maximum length of 64 characters. Values are strings with + * a maximum length of 512 characters. */ - metadata?: unknown | null; + metadata?: Shared.Metadata | null; } export interface RunListParams extends CursorPageParams { @@ -918,13 +938,16 @@ export interface RunCancelParams { thread_id: string; } +export type RunCreateAndPollParams = ThreadsAPI.ThreadCreateAndRunParamsNonStreaming; + +export type RunCreateAndStreamParams = RunCreateParamsBaseStream; + +export type RunStreamParams = RunCreateParamsBaseStream; + export type RunSubmitToolOutputsParams = | RunSubmitToolOutputsParamsNonStreaming | RunSubmitToolOutputsParamsStreaming; -// TODO -// RunCreateAndPollParams - export interface RunSubmitToolOutputsParamsBase { /** * Path param: The ID of the @@ -936,630 +959,13 @@ export interface RunSubmitToolOutputsParamsBase { /** * Body param: A list of tools for which the outputs are being submitted. */ - additional_instructions?: string | null; - - /** - * Body param: If `true`, returns a stream of events that happen during the Run as - * server-sent events, terminating when the Run enters a terminal state with a - * `data: [DONE]` message. - */ - additional_messages?: Array | null; - - /** - * Overrides the - * [instructions](https://platform.openai.com/docs/api-reference/assistants/createAssistant) - * of the assistant. This is useful for modifying the behavior on a per-run basis. - */ - instructions?: string | null; - - /** - * The maximum number of completion tokens that may be used over the course of the - * run. The run will make a best effort to use only the number of completion tokens - * specified, across multiple turns of the run. If the run exceeds the number of - * completion tokens specified, the run will end with status `incomplete`. See - * `incomplete_details` for more info. - */ - max_completion_tokens?: number | null; - - /** - * The maximum number of prompt tokens that may be used over the course of the run. - * The run will make a best effort to use only the number of prompt tokens - * specified, across multiple turns of the run. If the run exceeds the number of - * prompt tokens specified, the run will end with status `incomplete`. See - * `incomplete_details` for more info. - */ - max_prompt_tokens?: number | null; - - /** - * Set of 16 key-value pairs that can be attached to an object. This can be useful - * for storing additional information about the object in a structured format. Keys - * can be a maximum of 64 characters long and values can be a maxium of 512 - * characters long. - */ - metadata?: unknown | null; - - /** - * The ID of the [Model](https://platform.openai.com/docs/api-reference/models) to - * be used to execute this run. If a value is provided here, it will override the - * model associated with the assistant. If not, the model associated with the - * assistant will be used. - */ - model?: - | (string & {}) - | 'gpt-4o' - | 'gpt-4o-2024-05-13' - | 'gpt-4-turbo' - | 'gpt-4-turbo-2024-04-09' - | 'gpt-4-0125-preview' - | 'gpt-4-turbo-preview' - | 'gpt-4-1106-preview' - | 'gpt-4-vision-preview' - | 'gpt-4' - | 'gpt-4-0314' - | 'gpt-4-0613' - | 'gpt-4-32k' - | 'gpt-4-32k-0314' - | 'gpt-4-32k-0613' - | 'gpt-3.5-turbo' - | 'gpt-3.5-turbo-16k' - | 'gpt-3.5-turbo-0613' - | 'gpt-3.5-turbo-1106' - | 'gpt-3.5-turbo-0125' - | 'gpt-3.5-turbo-16k-0613' - | null; - - /** - * Specifies the format that the model must output. Compatible with - * [GPT-4o](https://platform.openai.com/docs/models/gpt-4o), - * [GPT-4 Turbo](https://platform.openai.com/docs/models/gpt-4-turbo-and-gpt-4), - * and all GPT-3.5 Turbo models since `gpt-3.5-turbo-1106`. - * - * Setting to `{ "type": "json_object" }` enables JSON mode, which guarantees the - * message the model generates is valid JSON. - * - * **Important:** when using JSON mode, you **must** also instruct the model to - * produce JSON yourself via a system or user message. Without this, the model may - * generate an unending stream of whitespace until the generation reaches the token - * limit, resulting in a long-running and seemingly "stuck" request. Also note that - * the message content may be partially cut off if `finish_reason="length"`, which - * indicates the generation exceeded `max_tokens` or the conversation exceeded the - * max context length. - */ - response_format?: ThreadsAPI.AssistantResponseFormatOption | 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. - */ - temperature?: number | null; - - /** - * Controls which (if any) tool is called by the model. `none` means the model will - * not call any tools and instead generates a message. `auto` is the default value - * and means the model can pick between generating a message or calling one or more - * tools. `required` means the model must call one or more tools before responding - * to the user. Specifying a particular tool like `{"type": "file_search"}` or - * `{"type": "function", "function": {"name": "my_function"}}` forces the model to - * call that tool. - */ - tool_choice?: ThreadsAPI.AssistantToolChoiceOption | null; - - /** - * Override the tools the assistant can use for this run. This is useful for - * modifying the behavior on a per-run basis. - */ - tools?: Array | null; - - /** - * An alternative to sampling with temperature, called nucleus sampling, where the - * model considers the results of the tokens with top_p probability mass. So 0.1 - * means only the tokens comprising the top 10% probability mass are considered. - * - * We generally recommend altering this or temperature but not both. - */ - top_p?: number | null; - - /** - * Controls for how a thread will be truncated prior to the run. Use this to - * control the intial context window of the run. - */ - truncation_strategy?: RunCreateAndPollParams.TruncationStrategy | null; -} - -export namespace RunCreateAndPollParams { - export interface AdditionalMessage { - /** - * The text contents of the message. - */ - content: string | Array; - - /** - * The role of the entity that is creating the message. Allowed values include: - * - * - `user`: Indicates the message is sent by an actual user and should be used in - * most cases to represent user-generated messages. - * - `assistant`: Indicates the message is generated by the assistant. Use this - * value to insert messages from the assistant into the conversation. - */ - role: 'user' | 'assistant'; - - /** - * A list of files attached to the message, and the tools they should be added to. - */ - attachments?: Array | null; - - /** - * Set of 16 key-value pairs that can be attached to an object. This can be useful - * for storing additional information about the object in a structured format. Keys - * can be a maximum of 64 characters long and values can be a maxium of 512 - * characters long. - */ - metadata?: unknown | null; - } - - export namespace AdditionalMessage { - export interface Attachment { - /** - * The ID of the file to attach to the message. - */ - file_id?: string; - - /** - * The tools to add this file to. - */ - tools?: Array; - } - } + tool_outputs: Array; /** * Body param: If `true`, returns a stream of events that happen during the Run as * server-sent events, terminating when the Run enters a terminal state with a * `data: [DONE]` message. */ - export interface TruncationStrategy { - /** - * The truncation strategy to use for the thread. The default is `auto`. If set to - * `last_messages`, the thread will be truncated to the n most recent messages in - * the thread. When set to `auto`, messages in the middle of the thread will be - * dropped to fit the context length of the model, `max_prompt_tokens`. - */ - type: 'auto' | 'last_messages'; - - /** - * The number of most recent messages from the thread when constructing the context - * for the run. - */ - last_messages?: number | null; - } -} - -export interface RunCreateAndStreamParams { - /** - * The ID of the - * [assistant](https://platform.openai.com/docs/api-reference/assistants) to use to - * execute this run. - */ - assistant_id: string; - /** - * Appends additional instructions at the end of the instructions for the run. This - * is useful for modifying the behavior on a per-run basis without overriding other - * instructions. - */ - additional_instructions?: string | null; - - /** - * Adds additional messages to the thread before creating the run. - */ - additional_messages?: Array | null; - - /** - * Overrides the - * [instructions](https://platform.openai.com/docs/api-reference/assistants/createAssistant) - * of the assistant. This is useful for modifying the behavior on a per-run basis. - */ - instructions?: string | null; - - /** - * The maximum number of completion tokens that may be used over the course of the - * run. The run will make a best effort to use only the number of completion tokens - * specified, across multiple turns of the run. If the run exceeds the number of - * completion tokens specified, the run will end with status `incomplete`. See - * `incomplete_details` for more info. - */ - max_completion_tokens?: number | null; - - /** - * The maximum number of prompt tokens that may be used over the course of the run. - * The run will make a best effort to use only the number of prompt tokens - * specified, across multiple turns of the run. If the run exceeds the number of - * prompt tokens specified, the run will end with status `incomplete`. See - * `incomplete_details` for more info. - */ - max_prompt_tokens?: number | null; - - /** - * Set of 16 key-value pairs that can be attached to an object. This can be useful - * for storing additional information about the object in a structured format. Keys - * can be a maximum of 64 characters long and values can be a maxium of 512 - * characters long. - */ - metadata?: unknown | null; - - /** - * The ID of the [Model](https://platform.openai.com/docs/api-reference/models) to - * be used to execute this run. If a value is provided here, it will override the - * model associated with the assistant. If not, the model associated with the - * assistant will be used. - */ - model?: - | (string & {}) - | 'gpt-4o' - | 'gpt-4o-2024-05-13' - | 'gpt-4-turbo' - | 'gpt-4-turbo-2024-04-09' - | 'gpt-4-0125-preview' - | 'gpt-4-turbo-preview' - | 'gpt-4-1106-preview' - | 'gpt-4-vision-preview' - | 'gpt-4' - | 'gpt-4-0314' - | 'gpt-4-0613' - | 'gpt-4-32k' - | 'gpt-4-32k-0314' - | 'gpt-4-32k-0613' - | 'gpt-3.5-turbo' - | 'gpt-3.5-turbo-16k' - | 'gpt-3.5-turbo-0613' - | 'gpt-3.5-turbo-1106' - | 'gpt-3.5-turbo-0125' - | 'gpt-3.5-turbo-16k-0613' - | null; - - /** - * Specifies the format that the model must output. Compatible with - * [GPT-4o](https://platform.openai.com/docs/models/gpt-4o), - * [GPT-4 Turbo](https://platform.openai.com/docs/models/gpt-4-turbo-and-gpt-4), - * and all GPT-3.5 Turbo models since `gpt-3.5-turbo-1106`. - * - * Setting to `{ "type": "json_object" }` enables JSON mode, which guarantees the - * message the model generates is valid JSON. - * - * **Important:** when using JSON mode, you **must** also instruct the model to - * produce JSON yourself via a system or user message. Without this, the model may - * generate an unending stream of whitespace until the generation reaches the token - * limit, resulting in a long-running and seemingly "stuck" request. Also note that - * the message content may be partially cut off if `finish_reason="length"`, which - * indicates the generation exceeded `max_tokens` or the conversation exceeded the - * max context length. - */ - response_format?: ThreadsAPI.AssistantResponseFormatOption | 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. - */ - temperature?: number | null; - - /** - * Controls which (if any) tool is called by the model. `none` means the model will - * not call any tools and instead generates a message. `auto` is the default value - * and means the model can pick between generating a message or calling one or more - * tools. `required` means the model must call one or more tools before responding - * to the user. Specifying a particular tool like `{"type": "file_search"}` or - * `{"type": "function", "function": {"name": "my_function"}}` forces the model to - * call that tool. - */ - tool_choice?: ThreadsAPI.AssistantToolChoiceOption | null; - - /** - * Override the tools the assistant can use for this run. This is useful for - * modifying the behavior on a per-run basis. - */ - tools?: Array | null; - - /** - * An alternative to sampling with temperature, called nucleus sampling, where the - * model considers the results of the tokens with top_p probability mass. So 0.1 - * means only the tokens comprising the top 10% probability mass are considered. - * - * We generally recommend altering this or temperature but not both. - */ - top_p?: number | null; - - /** - * Controls for how a thread will be truncated prior to the run. Use this to - * control the intial context window of the run. - */ - truncation_strategy?: RunCreateAndStreamParams.TruncationStrategy | null; -} - -export namespace RunCreateAndStreamParams { - export interface AdditionalMessage { - /** - * The text contents of the message. - */ - content: string | Array; - - /** - * The role of the entity that is creating the message. Allowed values include: - * - * - `user`: Indicates the message is sent by an actual user and should be used in - * most cases to represent user-generated messages. - * - `assistant`: Indicates the message is generated by the assistant. Use this - * value to insert messages from the assistant into the conversation. - */ - role: 'user' | 'assistant'; - - /** - * A list of files attached to the message, and the tools they should be added to. - */ - attachments?: Array | null; - - /** - * Set of 16 key-value pairs that can be attached to an object. This can be useful - * for storing additional information about the object in a structured format. Keys - * can be a maximum of 64 characters long and values can be a maxium of 512 - * characters long. - */ - metadata?: unknown | null; - } - - export namespace AdditionalMessage { - export interface Attachment { - /** - * The ID of the file to attach to the message. - */ - file_id?: string; - - /** - * The tools to add this file to. - */ - tools?: Array; - } - } - - /** - * Controls for how a thread will be truncated prior to the run. Use this to - * control the intial context window of the run. - */ - export interface TruncationStrategy { - /** - * The truncation strategy to use for the thread. The default is `auto`. If set to - * `last_messages`, the thread will be truncated to the n most recent messages in - * the thread. When set to `auto`, messages in the middle of the thread will be - * dropped to fit the context length of the model, `max_prompt_tokens`. - */ - type: 'auto' | 'last_messages'; - - /** - * The number of most recent messages from the thread when constructing the context - * for the run. - */ - last_messages?: number | null; - } -} - -export interface RunStreamParams { - /** - * The ID of the - * [assistant](https://platform.openai.com/docs/api-reference/assistants) to use to - * execute this run. - */ - assistant_id: string; - - /** - * Appends additional instructions at the end of the instructions for the run. This - * is useful for modifying the behavior on a per-run basis without overriding other - * instructions. - */ - additional_instructions?: string | null; - - /** - * Adds additional messages to the thread before creating the run. - */ - additional_messages?: Array | null; - - /** - * Overrides the - * [instructions](https://platform.openai.com/docs/api-reference/assistants/createAssistant) - * of the assistant. This is useful for modifying the behavior on a per-run basis. - */ - instructions?: string | null; - - /** - * The maximum number of completion tokens that may be used over the course of the - * run. The run will make a best effort to use only the number of completion tokens - * specified, across multiple turns of the run. If the run exceeds the number of - * completion tokens specified, the run will end with status `incomplete`. See - * `incomplete_details` for more info. - */ - max_completion_tokens?: number | null; - - /** - * The maximum number of prompt tokens that may be used over the course of the run. - * The run will make a best effort to use only the number of prompt tokens - * specified, across multiple turns of the run. If the run exceeds the number of - * prompt tokens specified, the run will end with status `incomplete`. See - * `incomplete_details` for more info. - */ - max_prompt_tokens?: number | null; - - /** - * Set of 16 key-value pairs that can be attached to an object. This can be useful - * for storing additional information about the object in a structured format. Keys - * can be a maximum of 64 characters long and values can be a maxium of 512 - * characters long. - */ - metadata?: unknown | null; - - /** - * The ID of the [Model](https://platform.openai.com/docs/api-reference/models) to - * be used to execute this run. If a value is provided here, it will override the - * model associated with the assistant. If not, the model associated with the - * assistant will be used. - */ - model?: - | (string & {}) - | 'gpt-4o' - | 'gpt-4o-2024-05-13' - | 'gpt-4-turbo' - | 'gpt-4-turbo-2024-04-09' - | 'gpt-4-0125-preview' - | 'gpt-4-turbo-preview' - | 'gpt-4-1106-preview' - | 'gpt-4-vision-preview' - | 'gpt-4' - | 'gpt-4-0314' - | 'gpt-4-0613' - | 'gpt-4-32k' - | 'gpt-4-32k-0314' - | 'gpt-4-32k-0613' - | 'gpt-3.5-turbo' - | 'gpt-3.5-turbo-16k' - | 'gpt-3.5-turbo-0613' - | 'gpt-3.5-turbo-1106' - | 'gpt-3.5-turbo-0125' - | 'gpt-3.5-turbo-16k-0613' - | null; - - /** - * Specifies the format that the model must output. Compatible with - * [GPT-4o](https://platform.openai.com/docs/models/gpt-4o), - * [GPT-4 Turbo](https://platform.openai.com/docs/models/gpt-4-turbo-and-gpt-4), - * and all GPT-3.5 Turbo models since `gpt-3.5-turbo-1106`. - * - * Setting to `{ "type": "json_object" }` enables JSON mode, which guarantees the - * message the model generates is valid JSON. - * - * **Important:** when using JSON mode, you **must** also instruct the model to - * produce JSON yourself via a system or user message. Without this, the model may - * generate an unending stream of whitespace until the generation reaches the token - * limit, resulting in a long-running and seemingly "stuck" request. Also note that - * the message content may be partially cut off if `finish_reason="length"`, which - * indicates the generation exceeded `max_tokens` or the conversation exceeded the - * max context length. - */ - response_format?: ThreadsAPI.AssistantResponseFormatOption | 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. - */ - temperature?: number | null; - - /** - * Controls which (if any) tool is called by the model. `none` means the model will - * not call any tools and instead generates a message. `auto` is the default value - * and means the model can pick between generating a message or calling one or more - * tools. `required` means the model must call one or more tools before responding - * to the user. Specifying a particular tool like `{"type": "file_search"}` or - * `{"type": "function", "function": {"name": "my_function"}}` forces the model to - * call that tool. - */ - tool_choice?: ThreadsAPI.AssistantToolChoiceOption | null; - - /** - * Override the tools the assistant can use for this run. This is useful for - * modifying the behavior on a per-run basis. - */ - tools?: Array | null; - - /** - * An alternative to sampling with temperature, called nucleus sampling, where the - * model considers the results of the tokens with top_p probability mass. So 0.1 - * means only the tokens comprising the top 10% probability mass are considered. - * - * We generally recommend altering this or temperature but not both. - */ - top_p?: number | null; - - /** - * Controls for how a thread will be truncated prior to the run. Use this to - * control the intial context window of the run. - */ - truncation_strategy?: RunStreamParams.TruncationStrategy | null; -} - -export namespace RunStreamParams { - export interface AdditionalMessage { - /** - * The text contents of the message. - */ - content: string | Array; - - /** - * The role of the entity that is creating the message. Allowed values include: - * - * - `user`: Indicates the message is sent by an actual user and should be used in - * most cases to represent user-generated messages. - * - `assistant`: Indicates the message is generated by the assistant. Use this - * value to insert messages from the assistant into the conversation. - */ - role: 'user' | 'assistant'; - - /** - * A list of files attached to the message, and the tools they should be added to. - */ - attachments?: Array | null; - - /** - * Set of 16 key-value pairs that can be attached to an object. This can be useful - * for storing additional information about the object in a structured format. Keys - * can be a maximum of 64 characters long and values can be a maxium of 512 - * characters long. - */ - metadata?: unknown | null; - } - - export namespace AdditionalMessage { - export interface Attachment { - /** - * The ID of the file to attach to the message. - */ - file_id?: string; - - /** - * The tools to add this file to. - */ - tools?: Array; - } - } - - /** - * Controls for how a thread will be truncated prior to the run. Use this to - * control the intial context window of the run. - */ - export interface TruncationStrategy { - /** - * The truncation strategy to use for the thread. The default is `auto`. If set to - * `last_messages`, the thread will be truncated to the n most recent messages in - * the thread. When set to `auto`, messages in the middle of the thread will be - * dropped to fit the context length of the model, `max_prompt_tokens`. - */ - type: 'auto' | 'last_messages'; - - /** - * The number of most recent messages from the thread when constructing the context - * for the run. - */ - last_messages?: number | null; - } -} - -export interface RunSubmitToolOutputsParamsBase { - /** - * A list of tools for which the outputs are being submitted. - */ - tool_outputs: Array; - - /** - * If `true`, returns a stream of events that happen during the Run as server-sent - * events, terminating when the Run enters a terminal state with a `data: [DONE]` - * message. - */ stream?: boolean | null; } @@ -1583,65 +989,24 @@ export namespace RunSubmitToolOutputsParams { export interface RunSubmitToolOutputsParamsNonStreaming extends RunSubmitToolOutputsParamsBase { /** - * If `true`, returns a stream of events that happen during the Run as server-sent - * events, terminating when the Run enters a terminal state with a `data: [DONE]` - * message. + * Body param: If `true`, returns a stream of events that happen during the Run as + * server-sent events, terminating when the Run enters a terminal state with a + * `data: [DONE]` message. */ stream?: false | null; } export interface RunSubmitToolOutputsParamsStreaming extends RunSubmitToolOutputsParamsBase { /** - * If `true`, returns a stream of events that happen during the Run as server-sent - * events, terminating when the Run enters a terminal state with a `data: [DONE]` - * message. + * Body param: If `true`, returns a stream of events that happen during the Run as + * server-sent events, terminating when the Run enters a terminal state with a + * `data: [DONE]` message. */ stream: true; } -export interface RunSubmitToolOutputsAndPollParams { - /** - * A list of tools for which the outputs are being submitted. - */ - tool_outputs: Array; -} - -export namespace RunSubmitToolOutputsAndPollParams { - export interface ToolOutput { - /** - * The output of the tool call to be submitted to continue the run. - */ - output?: string; - - /** - * The ID of the tool call in the `required_action` object within the run object - * the output is being submitted for. - */ - tool_call_id?: string; - } -} - -export interface RunSubmitToolOutputsStreamParams { - /** - * A list of tools for which the outputs are being submitted. - */ - tool_outputs: Array; -} - -export namespace RunSubmitToolOutputsStreamParams { - export interface ToolOutput { - /** - * The output of the tool call to be submitted to continue the run. - */ - output?: string; - - /** - * The ID of the tool call in the `required_action` object within the run object - * the output is being submitted for. - */ - tool_call_id?: string; - } -} +export type RunSubmitToolOutputsAndPollParams = RunSubmitToolOutputsParamsNonStreaming; +export type RunSubmitToolOutputsStreamParams = RunSubmitToolOutputsParamsStream; Runs.Steps = Steps; diff --git a/src/resources/beta/threads/runs/steps.ts b/src/resources/beta/threads/runs/steps.ts index 444488b41..adaa4c9a1 100644 --- a/src/resources/beta/threads/runs/steps.ts +++ b/src/resources/beta/threads/runs/steps.ts @@ -2,9 +2,12 @@ import { APIResource } from '../../../../resource'; import * as StepsAPI from './steps'; +import * as Shared from '../../../shared'; import { APIPromise } from '../../../../api-promise'; import { CursorPage, type CursorPageParams, PagePromise } from '../../../../pagination'; +import { buildHeaders } from '../../../../internal/headers'; import { RequestOptions } from '../../../../internal/request-options'; +import { path } from '../../../../internal/utils/path'; export class Steps extends APIResource { /** @@ -12,10 +15,10 @@ export class Steps extends APIResource { */ retrieve(stepID: string, params: StepRetrieveParams, options?: RequestOptions): APIPromise { const { thread_id, run_id, ...query } = params; - return this._client.get(`/threads/${thread_id}/runs/${run_id}/steps/${stepID}`, { + return this._client.get(path`/threads/${thread_id}/runs/${run_id}/steps/${stepID}`, { query, ...options, - headers: { 'OpenAI-Beta': 'assistants=v2', ...options?.headers }, + headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]), }); } @@ -24,10 +27,10 @@ export class Steps extends APIResource { */ list(runID: string, params: StepListParams, options?: RequestOptions): PagePromise { const { thread_id, ...query } = params; - return this._client.getAPIList(`/threads/${thread_id}/runs/${runID}/steps`, CursorPage, { + return this._client.getAPIList(path`/threads/${thread_id}/runs/${runID}/steps`, CursorPage, { query, ...options, - headers: { 'OpenAI-Beta': 'assistants=v2', ...options?.headers }, + headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]), }); } } @@ -476,11 +479,13 @@ export interface RunStep { /** * Set of 16 key-value pairs that can be attached to an object. This can be useful - * for storing additional information about the object in a structured format. Keys - * can be a maximum of 64 characters long and values can be a maxium of 512 - * characters long. + * for storing additional information about the object in a structured format, and + * querying for objects via API or the dashboard. + * + * Keys are strings with a maximum length of 64 characters. Values are strings with + * a maximum length of 512 characters. */ - metadata: unknown | null; + metadata: Shared.Metadata | null; /** * The object type, which is always `thread.run.step`. diff --git a/src/resources/beta/threads/threads.ts b/src/resources/beta/threads/threads.ts index ece8234d6..be1014f68 100644 --- a/src/resources/beta/threads/threads.ts +++ b/src/resources/beta/threads/threads.ts @@ -69,8 +69,10 @@ import { } from './runs/runs'; import { APIPromise } from '../../../api-promise'; import { Stream } from '../../../streaming'; +import { buildHeaders } from '../../../internal/headers'; import { RequestOptions } from '../../../internal/request-options'; import { AssistantStream, ThreadCreateAndRunParamsBaseStream } from '../../../lib/AssistantStream'; +import { path } from '../../../internal/utils/path'; export class Threads extends APIResource { runs: RunsAPI.Runs = new RunsAPI.Runs(this._client); @@ -83,7 +85,7 @@ export class Threads extends APIResource { return this._client.post('/threads', { body, ...options, - headers: { 'OpenAI-Beta': 'assistants=v2', ...options?.headers }, + headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]), }); } @@ -91,9 +93,9 @@ export class Threads extends APIResource { * Retrieves a thread. */ retrieve(threadID: string, options?: RequestOptions): APIPromise { - return this._client.get(`/threads/${threadID}`, { + return this._client.get(path`/threads/${threadID}`, { ...options, - headers: { 'OpenAI-Beta': 'assistants=v2', ...options?.headers }, + headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]), }); } @@ -101,10 +103,10 @@ export class Threads extends APIResource { * Modifies a thread. */ update(threadID: string, body: ThreadUpdateParams, options?: RequestOptions): APIPromise { - return this._client.post(`/threads/${threadID}`, { + return this._client.post(path`/threads/${threadID}`, { body, ...options, - headers: { 'OpenAI-Beta': 'assistants=v2', ...options?.headers }, + headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]), }); } @@ -112,9 +114,9 @@ export class Threads extends APIResource { * Delete a thread. */ delete(threadID: string, options?: RequestOptions): APIPromise { - return this._client.delete(`/threads/${threadID}`, { + return this._client.delete(path`/threads/${threadID}`, { ...options, - headers: { 'OpenAI-Beta': 'assistants=v2', ...options?.headers }, + headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]), }); } @@ -137,7 +139,7 @@ export class Threads extends APIResource { return this._client.post('/threads/runs', { body, ...options, - headers: { 'OpenAI-Beta': 'assistants=v2', ...options?.headers }, + headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]), stream: body.stream ?? false, }) as APIPromise | APIPromise>; } @@ -239,11 +241,13 @@ export interface Thread { /** * Set of 16 key-value pairs that can be attached to an object. This can be useful - * for storing additional information about the object in a structured format. Keys - * can be a maximum of 64 characters long and values can be a maxium of 512 - * characters long. + * for storing additional information about the object in a structured format, and + * querying for objects via API or the dashboard. + * + * Keys are strings with a maximum length of 64 characters. Values are strings with + * a maximum length of 512 characters. */ - metadata: unknown | null; + metadata: Shared.Metadata | null; /** * The object type, which is always `thread`. @@ -311,11 +315,13 @@ export interface ThreadCreateParams { /** * Set of 16 key-value pairs that can be attached to an object. This can be useful - * for storing additional information about the object in a structured format. Keys - * can be a maximum of 64 characters long and values can be a maxium of 512 - * characters long. + * for storing additional information about the object in a structured format, and + * querying for objects via API or the dashboard. + * + * Keys are strings with a maximum length of 64 characters. Values are strings with + * a maximum length of 512 characters. */ - metadata?: unknown | null; + metadata?: Shared.Metadata | null; /** * A set of resources that are made available to the assistant's tools in this @@ -350,11 +356,13 @@ export namespace ThreadCreateParams { /** * Set of 16 key-value pairs that can be attached to an object. This can be useful - * for storing additional information about the object in a structured format. Keys - * can be a maximum of 64 characters long and values can be a maxium of 512 - * characters long. + * for storing additional information about the object in a structured format, and + * querying for objects via API or the dashboard. + * + * Keys are strings with a maximum length of 64 characters. Values are strings with + * a maximum length of 512 characters. */ - metadata?: unknown | null; + metadata?: Shared.Metadata | null; } export namespace Message { @@ -436,12 +444,14 @@ export namespace ThreadCreateParams { file_ids?: Array; /** - * Set of 16 key-value pairs that can be attached to a vector store. This can be - * useful for storing additional information about the vector store in a structured - * format. Keys can be a maximum of 64 characters long and values can be a maxium - * of 512 characters long. + * Set of 16 key-value pairs that can be attached to an object. This can be useful + * for storing additional information about the object in a structured format, and + * querying for objects via API or the dashboard. + * + * Keys are strings with a maximum length of 64 characters. Values are strings with + * a maximum length of 512 characters. */ - metadata?: unknown; + metadata?: Shared.Metadata | null; } } } @@ -450,11 +460,13 @@ export namespace ThreadCreateParams { export interface ThreadUpdateParams { /** * Set of 16 key-value pairs that can be attached to an object. This can be useful - * for storing additional information about the object in a structured format. Keys - * can be a maximum of 64 characters long and values can be a maxium of 512 - * characters long. + * for storing additional information about the object in a structured format, and + * querying for objects via API or the dashboard. + * + * Keys are strings with a maximum length of 64 characters. Values are strings with + * a maximum length of 512 characters. */ - metadata?: unknown | null; + metadata?: Shared.Metadata | null; /** * A set of resources that are made available to the assistant's tools in this @@ -538,11 +550,13 @@ export interface ThreadCreateAndRunParamsBase { /** * Set of 16 key-value pairs that can be attached to an object. This can be useful - * for storing additional information about the object in a structured format. Keys - * can be a maximum of 64 characters long and values can be a maxium of 512 - * characters long. + * for storing additional information about the object in a structured format, and + * querying for objects via API or the dashboard. + * + * Keys are strings with a maximum length of 64 characters. Values are strings with + * a maximum length of 512 characters. */ - metadata?: unknown | null; + metadata?: Shared.Metadata | null; /** * The ID of the [Model](https://platform.openai.com/docs/api-reference/models) to @@ -598,7 +612,8 @@ export interface ThreadCreateAndRunParamsBase { temperature?: number | null; /** - * If no thread is provided, an empty thread will be created. + * Options to create a new thread. If no thread is provided when running a request, + * an empty thread will be created. */ thread?: ThreadCreateAndRunParams.Thread; @@ -647,7 +662,8 @@ export interface ThreadCreateAndRunParamsBase { export namespace ThreadCreateAndRunParams { /** - * If no thread is provided, an empty thread will be created. + * Options to create a new thread. If no thread is provided when running a request, + * an empty thread will be created. */ export interface Thread { /** @@ -658,11 +674,13 @@ export namespace ThreadCreateAndRunParams { /** * Set of 16 key-value pairs that can be attached to an object. This can be useful - * for storing additional information about the object in a structured format. Keys - * can be a maximum of 64 characters long and values can be a maxium of 512 - * characters long. + * for storing additional information about the object in a structured format, and + * querying for objects via API or the dashboard. + * + * Keys are strings with a maximum length of 64 characters. Values are strings with + * a maximum length of 512 characters. */ - metadata?: unknown | null; + metadata?: Shared.Metadata | null; /** * A set of resources that are made available to the assistant's tools in this @@ -697,11 +715,13 @@ export namespace ThreadCreateAndRunParams { /** * Set of 16 key-value pairs that can be attached to an object. This can be useful - * for storing additional information about the object in a structured format. Keys - * can be a maximum of 64 characters long and values can be a maxium of 512 - * characters long. + * for storing additional information about the object in a structured format, and + * querying for objects via API or the dashboard. + * + * Keys are strings with a maximum length of 64 characters. Values are strings with + * a maximum length of 512 characters. */ - metadata?: unknown | null; + metadata?: Shared.Metadata | null; } export namespace Message { @@ -783,12 +803,14 @@ export namespace ThreadCreateAndRunParams { file_ids?: Array; /** - * Set of 16 key-value pairs that can be attached to a vector store. This can be - * useful for storing additional information about the vector store in a structured - * format. Keys can be a maximum of 64 characters long and values can be a maxium - * of 512 characters long. + * Set of 16 key-value pairs that can be attached to an object. This can be useful + * for storing additional information about the object in a structured format, and + * querying for objects via API or the dashboard. + * + * Keys are strings with a maximum length of 64 characters. Values are strings with + * a maximum length of 512 characters. */ - metadata?: unknown; + metadata?: Shared.Metadata | null; } } } @@ -1201,337 +1223,7 @@ export namespace ThreadCreateAndRunPollParams { } } -export interface ThreadCreateAndRunStreamParams { - /** - * The ID of the - * [assistant](https://platform.openai.com/docs/api-reference/assistants) to use to - * execute this run. - */ - assistant_id: string; - - /** - * Override the default system message of the assistant. This is useful for - * modifying the behavior on a per-run basis. - */ - instructions?: string | null; - - /** - * The maximum number of completion tokens that may be used over the course of the - * run. The run will make a best effort to use only the number of completion tokens - * specified, across multiple turns of the run. If the run exceeds the number of - * completion tokens specified, the run will end with status `incomplete`. See - * `incomplete_details` for more info. - */ - max_completion_tokens?: number | null; - - /** - * The maximum number of prompt tokens that may be used over the course of the run. - * The run will make a best effort to use only the number of prompt tokens - * specified, across multiple turns of the run. If the run exceeds the number of - * prompt tokens specified, the run will end with status `incomplete`. See - * `incomplete_details` for more info. - */ - max_prompt_tokens?: number | null; - - /** - * Set of 16 key-value pairs that can be attached to an object. This can be useful - * for storing additional information about the object in a structured format. Keys - * can be a maximum of 64 characters long and values can be a maxium of 512 - * characters long. - */ - metadata?: unknown | null; - - /** - * The ID of the [Model](https://platform.openai.com/docs/api-reference/models) to - * be used to execute this run. If a value is provided here, it will override the - * model associated with the assistant. If not, the model associated with the - * assistant will be used. - */ - model?: - | (string & {}) - | 'gpt-4o' - | 'gpt-4o-2024-05-13' - | 'gpt-4-turbo' - | 'gpt-4-turbo-2024-04-09' - | 'gpt-4-0125-preview' - | 'gpt-4-turbo-preview' - | 'gpt-4-1106-preview' - | 'gpt-4-vision-preview' - | 'gpt-4' - | 'gpt-4-0314' - | 'gpt-4-0613' - | 'gpt-4-32k' - | 'gpt-4-32k-0314' - | 'gpt-4-32k-0613' - | 'gpt-3.5-turbo' - | 'gpt-3.5-turbo-16k' - | 'gpt-3.5-turbo-0613' - | 'gpt-3.5-turbo-1106' - | 'gpt-3.5-turbo-0125' - | 'gpt-3.5-turbo-16k-0613' - | null; - - /** - * Specifies the format that the model must output. Compatible with - * [GPT-4o](https://platform.openai.com/docs/models/gpt-4o), - * [GPT-4 Turbo](https://platform.openai.com/docs/models/gpt-4-turbo-and-gpt-4), - * and all GPT-3.5 Turbo models since `gpt-3.5-turbo-1106`. - * - * Setting to `{ "type": "json_object" }` enables JSON mode, which guarantees the - * message the model generates is valid JSON. - * - * **Important:** when using JSON mode, you **must** also instruct the model to - * produce JSON yourself via a system or user message. Without this, the model may - * generate an unending stream of whitespace until the generation reaches the token - * limit, resulting in a long-running and seemingly "stuck" request. Also note that - * the message content may be partially cut off if `finish_reason="length"`, which - * indicates the generation exceeded `max_tokens` or the conversation exceeded the - * max context length. - */ - response_format?: AssistantResponseFormatOption | 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. - */ - temperature?: number | null; - - /** - * If no thread is provided, an empty thread will be created. - */ - thread?: ThreadCreateAndRunStreamParams.Thread; - - /** - * Controls which (if any) tool is called by the model. `none` means the model will - * not call any tools and instead generates a message. `auto` is the default value - * and means the model can pick between generating a message or calling one or more - * tools. `required` means the model must call one or more tools before responding - * to the user. Specifying a particular tool like `{"type": "file_search"}` or - * `{"type": "function", "function": {"name": "my_function"}}` forces the model to - * call that tool. - */ - tool_choice?: AssistantToolChoiceOption | null; - - /** - * A set of resources that are used by the assistant's tools. The resources are - * specific to the type of tool. For example, the `code_interpreter` tool requires - * a list of file IDs, while the `file_search` tool requires a list of vector store - * IDs. - */ - tool_resources?: ThreadCreateAndRunStreamParams.ToolResources | null; - - /** - * Override the tools the assistant can use for this run. This is useful for - * modifying the behavior on a per-run basis. - */ - tools?: Array< - AssistantsAPI.CodeInterpreterTool | AssistantsAPI.FileSearchTool | AssistantsAPI.FunctionTool - > | null; - - /** - * An alternative to sampling with temperature, called nucleus sampling, where the - * model considers the results of the tokens with top_p probability mass. So 0.1 - * means only the tokens comprising the top 10% probability mass are considered. - * - * We generally recommend altering this or temperature but not both. - */ - top_p?: number | null; - - /** - * Controls for how a thread will be truncated prior to the run. Use this to - * control the intial context window of the run. - */ - truncation_strategy?: ThreadCreateAndRunStreamParams.TruncationStrategy | null; -} - -export namespace ThreadCreateAndRunStreamParams { - /** - * If no thread is provided, an empty thread will be created. - */ - export interface Thread { - /** - * A list of [messages](https://platform.openai.com/docs/api-reference/messages) to - * start the thread with. - */ - messages?: Array; - - /** - * Set of 16 key-value pairs that can be attached to an object. This can be useful - * for storing additional information about the object in a structured format. Keys - * can be a maximum of 64 characters long and values can be a maxium of 512 - * characters long. - */ - metadata?: unknown | null; - - /** - * A set of resources that are made available to the assistant's tools in this - * thread. The resources are specific to the type of tool. For example, the - * `code_interpreter` tool requires a list of file IDs, while the `file_search` - * tool requires a list of vector store IDs. - */ - tool_resources?: Thread.ToolResources | null; - } - - export namespace Thread { - export interface Message { - /** - * The text contents of the message. - */ - content: string | Array; - - /** - * The role of the entity that is creating the message. Allowed values include: - * - * - `user`: Indicates the message is sent by an actual user and should be used in - * most cases to represent user-generated messages. - * - `assistant`: Indicates the message is generated by the assistant. Use this - * value to insert messages from the assistant into the conversation. - */ - role: 'user' | 'assistant'; - - /** - * A list of files attached to the message, and the tools they should be added to. - */ - attachments?: Array | null; - - /** - * Set of 16 key-value pairs that can be attached to an object. This can be useful - * for storing additional information about the object in a structured format. Keys - * can be a maximum of 64 characters long and values can be a maxium of 512 - * characters long. - */ - metadata?: unknown | null; - } - - export namespace Message { - export interface Attachment { - /** - * The ID of the file to attach to the message. - */ - file_id?: string; - - /** - * The tools to add this file to. - */ - tools?: Array; - } - } - - /** - * A set of resources that are made available to the assistant's tools in this - * thread. The resources are specific to the type of tool. For example, the - * `code_interpreter` tool requires a list of file IDs, while the `file_search` - * tool requires a list of vector store IDs. - */ - export interface ToolResources { - code_interpreter?: ToolResources.CodeInterpreter; - - file_search?: ToolResources.FileSearch; - } - - export namespace ToolResources { - export interface CodeInterpreter { - /** - * A list of [file](https://platform.openai.com/docs/api-reference/files) IDs made - * available to the `code_interpreter` tool. There can be a maximum of 20 files - * associated with the tool. - */ - file_ids?: Array; - } - - export interface FileSearch { - /** - * The - * [vector store](https://platform.openai.com/docs/api-reference/vector-stores/object) - * attached to this thread. There can be a maximum of 1 vector store attached to - * the thread. - */ - vector_store_ids?: Array; - - /** - * A helper to create a - * [vector store](https://platform.openai.com/docs/api-reference/vector-stores/object) - * with file_ids and attach it to this thread. There can be a maximum of 1 vector - * store attached to the thread. - */ - vector_stores?: Array; - } - - export namespace FileSearch { - export interface VectorStore { - /** - * A list of [file](https://platform.openai.com/docs/api-reference/files) IDs to - * add to the vector store. There can be a maximum of 10000 files in a vector - * store. - */ - file_ids?: Array; - - /** - * Set of 16 key-value pairs that can be attached to a vector store. This can be - * useful for storing additional information about the vector store in a structured - * format. Keys can be a maximum of 64 characters long and values can be a maxium - * of 512 characters long. - */ - metadata?: unknown; - } - } - } - } - - /** - * A set of resources that are used by the assistant's tools. The resources are - * specific to the type of tool. For example, the `code_interpreter` tool requires - * a list of file IDs, while the `file_search` tool requires a list of vector store - * IDs. - */ - export interface ToolResources { - code_interpreter?: ToolResources.CodeInterpreter; - - file_search?: ToolResources.FileSearch; - } - - export namespace ToolResources { - export interface CodeInterpreter { - /** - * A list of [file](https://platform.openai.com/docs/api-reference/files) IDs made - * available to the `code_interpreter` tool. There can be a maximum of 20 files - * associated with the tool. - */ - file_ids?: Array; - } - - export interface FileSearch { - /** - * The ID of the - * [vector store](https://platform.openai.com/docs/api-reference/vector-stores/object) - * attached to this assistant. There can be a maximum of 1 vector store attached to - * the assistant. - */ - vector_store_ids?: Array; - } - } - - /** - * Controls for how a thread will be truncated prior to the run. Use this to - * control the intial context window of the run. - */ - export interface TruncationStrategy { - /** - * The truncation strategy to use for the thread. The default is `auto`. If set to - * `last_messages`, the thread will be truncated to the n most recent messages in - * the thread. When set to `auto`, messages in the middle of the thread will be - * dropped to fit the context length of the model, `max_prompt_tokens`. - */ - type: 'auto' | 'last_messages'; - - /** - * The number of most recent messages from the thread when constructing the context - * for the run. - */ - last_messages?: number | null; - } -} +export type ThreadCreateAndRunStreamParams = ThreadCreateAndRunParamsBaseStream; Threads.Runs = Runs; Threads.Messages = Messages; diff --git a/src/resources/beta/vector-stores/file-batches.ts b/src/resources/beta/vector-stores/file-batches.ts index 845903e24..4cb62feb8 100644 --- a/src/resources/beta/vector-stores/file-batches.ts +++ b/src/resources/beta/vector-stores/file-batches.ts @@ -6,11 +6,12 @@ import { VectorStoreFilesPage } from './files'; import * as VectorStoresAPI from './vector-stores'; import { APIPromise } from '../../../api-promise'; import { CursorPage, type CursorPageParams, PagePromise } from '../../../pagination'; +import { buildHeaders } from '../../../internal/headers'; import { RequestOptions } from '../../../internal/request-options'; import { sleep } from '../../../internal/utils/sleep'; import { type Uploadable } from '../../../uploads'; import { allSettledWithThrow } from '../../../lib/Util'; -import { buildHeaders } from '../../../internal/headers'; +import { path } from '../../../internal/utils/path'; export class FileBatches extends APIResource { /** @@ -21,10 +22,10 @@ export class FileBatches extends APIResource { body: FileBatchCreateParams, options?: RequestOptions, ): APIPromise { - return this._client.post(`/vector_stores/${vectorStoreID}/file_batches`, { + return this._client.post(path`/vector_stores/${vectorStoreID}/file_batches`, { body, ...options, - headers: { 'OpenAI-Beta': 'assistants=v2', ...options?.headers }, + headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]), }); } @@ -37,9 +38,9 @@ export class FileBatches extends APIResource { options?: RequestOptions, ): APIPromise { const { vector_store_id } = params; - return this._client.get(`/vector_stores/${vector_store_id}/file_batches/${batchID}`, { + return this._client.get(path`/vector_stores/${vector_store_id}/file_batches/${batchID}`, { ...options, - headers: { 'OpenAI-Beta': 'assistants=v2', ...options?.headers }, + headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]), }); } @@ -53,9 +54,9 @@ export class FileBatches extends APIResource { options?: RequestOptions, ): APIPromise { const { vector_store_id } = params; - return this._client.post(`/vector_stores/${vector_store_id}/file_batches/${batchID}/cancel`, { + return this._client.post(path`/vector_stores/${vector_store_id}/file_batches/${batchID}/cancel`, { ...options, - headers: { 'OpenAI-Beta': 'assistants=v2', ...options?.headers }, + headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]), }); } @@ -81,9 +82,9 @@ export class FileBatches extends APIResource { ): PagePromise { const { vector_store_id, ...query } = params; return this._client.getAPIList( - `/vector_stores/${vector_store_id}/file_batches/${batchID}/files`, + path`/vector_stores/${vector_store_id}/file_batches/${batchID}/files`, CursorPage, - { query, ...options, headers: { 'OpenAI-Beta': 'assistants=v2', ...options?.headers } }, + { query, ...options, headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]) }, ); } diff --git a/src/resources/beta/vector-stores/files.ts b/src/resources/beta/vector-stores/files.ts index 5561b6a2b..2c499930e 100644 --- a/src/resources/beta/vector-stores/files.ts +++ b/src/resources/beta/vector-stores/files.ts @@ -4,10 +4,11 @@ import { APIResource } from '../../../resource'; import * as VectorStoresAPI from './vector-stores'; import { APIPromise } from '../../../api-promise'; import { CursorPage, type CursorPageParams, PagePromise } from '../../../pagination'; +import { buildHeaders } from '../../../internal/headers'; import { RequestOptions } from '../../../internal/request-options'; import { sleep } from '../../../internal/utils'; import { Uploadable } from '../../../uploads'; -import { buildHeaders } from '../../../internal/headers'; +import { path } from '../../../internal/utils/path'; export class Files extends APIResource { /** @@ -20,10 +21,10 @@ export class Files extends APIResource { body: FileCreateParams, options?: RequestOptions, ): APIPromise { - return this._client.post(`/vector_stores/${vectorStoreID}/files`, { + return this._client.post(path`/vector_stores/${vectorStoreID}/files`, { body, ...options, - headers: { 'OpenAI-Beta': 'assistants=v2', ...options?.headers }, + headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]), }); } @@ -36,9 +37,9 @@ export class Files extends APIResource { options?: RequestOptions, ): APIPromise { const { vector_store_id } = params; - return this._client.get(`/vector_stores/${vector_store_id}/files/${fileID}`, { + return this._client.get(path`/vector_stores/${vector_store_id}/files/${fileID}`, { ...options, - headers: { 'OpenAI-Beta': 'assistants=v2', ...options?.headers }, + headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]), }); } @@ -50,10 +51,10 @@ export class Files extends APIResource { query: FileListParams | null | undefined = {}, options?: RequestOptions, ): PagePromise { - return this._client.getAPIList(`/vector_stores/${vectorStoreID}/files`, CursorPage, { + return this._client.getAPIList(path`/vector_stores/${vectorStoreID}/files`, CursorPage, { query, ...options, - headers: { 'OpenAI-Beta': 'assistants=v2', ...options?.headers }, + headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]), }); } @@ -69,9 +70,9 @@ export class Files extends APIResource { options?: RequestOptions, ): APIPromise { const { vector_store_id } = params; - return this._client.delete(`/vector_stores/${vector_store_id}/files/${fileID}`, { + return this._client.delete(path`/vector_stores/${vector_store_id}/files/${fileID}`, { ...options, - headers: { 'OpenAI-Beta': 'assistants=v2', ...options?.headers }, + headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]), }); } diff --git a/src/resources/beta/vector-stores/index.ts b/src/resources/beta/vector-stores/index.ts index d41fce089..d3353db63 100644 --- a/src/resources/beta/vector-stores/index.ts +++ b/src/resources/beta/vector-stores/index.ts @@ -26,7 +26,7 @@ export { type OtherFileChunkingStrategyObject, type StaticFileChunkingStrategy, type StaticFileChunkingStrategyObject, - type StaticFileChunkingStrategyParam, + type StaticFileChunkingStrategyObjectParam, type VectorStore, type VectorStoreDeleted, type VectorStoreCreateParams, diff --git a/src/resources/beta/vector-stores/vector-stores.ts b/src/resources/beta/vector-stores/vector-stores.ts index c1a98c87d..94f32905e 100644 --- a/src/resources/beta/vector-stores/vector-stores.ts +++ b/src/resources/beta/vector-stores/vector-stores.ts @@ -1,6 +1,7 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. import { APIResource } from '../../../resource'; +import * as Shared from '../../shared'; import * as FileBatchesAPI from './file-batches'; import { FileBatchCancelParams, @@ -23,7 +24,9 @@ import { } from './files'; import { APIPromise } from '../../../api-promise'; import { CursorPage, type CursorPageParams, PagePromise } from '../../../pagination'; +import { buildHeaders } from '../../../internal/headers'; import { RequestOptions } from '../../../internal/request-options'; +import { path } from '../../../internal/utils/path'; export class VectorStores extends APIResource { files: FilesAPI.Files = new FilesAPI.Files(this._client); @@ -36,7 +39,7 @@ export class VectorStores extends APIResource { return this._client.post('/vector_stores', { body, ...options, - headers: { 'OpenAI-Beta': 'assistants=v2', ...options?.headers }, + headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]), }); } @@ -44,9 +47,9 @@ export class VectorStores extends APIResource { * Retrieves a vector store. */ retrieve(vectorStoreID: string, options?: RequestOptions): APIPromise { - return this._client.get(`/vector_stores/${vectorStoreID}`, { + return this._client.get(path`/vector_stores/${vectorStoreID}`, { ...options, - headers: { 'OpenAI-Beta': 'assistants=v2', ...options?.headers }, + headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]), }); } @@ -58,10 +61,10 @@ export class VectorStores extends APIResource { body: VectorStoreUpdateParams, options?: RequestOptions, ): APIPromise { - return this._client.post(`/vector_stores/${vectorStoreID}`, { + return this._client.post(path`/vector_stores/${vectorStoreID}`, { body, ...options, - headers: { 'OpenAI-Beta': 'assistants=v2', ...options?.headers }, + headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]), }); } @@ -75,7 +78,7 @@ export class VectorStores extends APIResource { return this._client.getAPIList('/vector_stores', CursorPage, { query, ...options, - headers: { 'OpenAI-Beta': 'assistants=v2', ...options?.headers }, + headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]), }); } @@ -83,9 +86,9 @@ export class VectorStores extends APIResource { * Delete a vector store. */ delete(vectorStoreID: string, options?: RequestOptions): APIPromise { - return this._client.delete(`/vector_stores/${vectorStoreID}`, { + return this._client.delete(path`/vector_stores/${vectorStoreID}`, { ...options, - headers: { 'OpenAI-Beta': 'assistants=v2', ...options?.headers }, + headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]), }); } } @@ -112,7 +115,7 @@ export type FileChunkingStrategy = StaticFileChunkingStrategyObject | OtherFileC * The chunking strategy used to chunk the file(s). If not set, will use the `auto` * strategy. Only applicable if `file_ids` is non-empty. */ -export type FileChunkingStrategyParam = AutoFileChunkingStrategyParam | StaticFileChunkingStrategyParam; +export type FileChunkingStrategyParam = AutoFileChunkingStrategyParam | StaticFileChunkingStrategyObjectParam; /** * This is returned when the chunking strategy is unknown. Typically, this is @@ -150,7 +153,7 @@ export interface StaticFileChunkingStrategyObject { type: 'static'; } -export interface StaticFileChunkingStrategyParam { +export interface StaticFileChunkingStrategyObjectParam { static: StaticFileChunkingStrategy; /** @@ -183,11 +186,13 @@ export interface VectorStore { /** * Set of 16 key-value pairs that can be attached to an object. This can be useful - * for storing additional information about the object in a structured format. Keys - * can be a maximum of 64 characters long and values can be a maxium of 512 - * characters long. + * for storing additional information about the object in a structured format, and + * querying for objects via API or the dashboard. + * + * Keys are strings with a maximum length of 64 characters. Values are strings with + * a maximum length of 512 characters. */ - metadata: unknown | null; + metadata: Shared.Metadata | null; /** * The name of the vector store. @@ -296,11 +301,13 @@ export interface VectorStoreCreateParams { /** * Set of 16 key-value pairs that can be attached to an object. This can be useful - * for storing additional information about the object in a structured format. Keys - * can be a maximum of 64 characters long and values can be a maxium of 512 - * characters long. + * for storing additional information about the object in a structured format, and + * querying for objects via API or the dashboard. + * + * Keys are strings with a maximum length of 64 characters. Values are strings with + * a maximum length of 512 characters. */ - metadata?: unknown | null; + metadata?: Shared.Metadata | null; /** * The name of the vector store. @@ -334,11 +341,13 @@ export interface VectorStoreUpdateParams { /** * Set of 16 key-value pairs that can be attached to an object. This can be useful - * for storing additional information about the object in a structured format. Keys - * can be a maximum of 64 characters long and values can be a maxium of 512 - * characters long. + * for storing additional information about the object in a structured format, and + * querying for objects via API or the dashboard. + * + * Keys are strings with a maximum length of 64 characters. Values are strings with + * a maximum length of 512 characters. */ - metadata?: unknown | null; + metadata?: Shared.Metadata | null; /** * The name of the vector store. @@ -391,7 +400,7 @@ export declare namespace VectorStores { type OtherFileChunkingStrategyObject as OtherFileChunkingStrategyObject, type StaticFileChunkingStrategy as StaticFileChunkingStrategy, type StaticFileChunkingStrategyObject as StaticFileChunkingStrategyObject, - type StaticFileChunkingStrategyParam as StaticFileChunkingStrategyParam, + type StaticFileChunkingStrategyObjectParam as StaticFileChunkingStrategyObjectParam, type VectorStore as VectorStore, type VectorStoreDeleted as VectorStoreDeleted, type VectorStoresPage as VectorStoresPage, diff --git a/src/resources/chat/chat.ts b/src/resources/chat/chat.ts index 57d246067..40358c331 100644 --- a/src/resources/chat/chat.ts +++ b/src/resources/chat/chat.ts @@ -1,7 +1,7 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. import { APIResource } from '../../resource'; -import * as CompletionsAPI from './completions'; +import * as CompletionsAPI from './completions/completions'; import { ChatCompletion, ChatCompletionAssistantMessageParam, @@ -16,9 +16,11 @@ import { ChatCompletionCreateParams, ChatCompletionCreateParamsNonStreaming, ChatCompletionCreateParamsStreaming, + ChatCompletionDeleted, ChatCompletionDeveloperMessageParam, ChatCompletionFunctionCallOption, ChatCompletionFunctionMessageParam, + ChatCompletionListParams, ChatCompletionMessage, ChatCompletionMessageParam, ChatCompletionMessageToolCall, @@ -27,30 +29,34 @@ import { ChatCompletionPredictionContent, ChatCompletionReasoningEffort, ChatCompletionRole, + ChatCompletionStoreMessage, ChatCompletionStreamOptions, ChatCompletionSystemMessageParam, ChatCompletionTokenLogprob, ChatCompletionTool, ChatCompletionToolChoiceOption, ChatCompletionToolMessageParam, + ChatCompletionUpdateParams, ChatCompletionUserMessageParam, - CompletionCreateParams, - CompletionCreateParamsNonStreaming, - CompletionCreateParamsStreaming, + ChatCompletionsPage, Completions, -} from './completions'; +} from './completions/completions'; export class Chat extends APIResource { completions: CompletionsAPI.Completions = new CompletionsAPI.Completions(this._client); } export type ChatModel = + | 'o3-mini' + | 'o3-mini-2025-01-31' | 'o1' | 'o1-2024-12-17' | 'o1-preview' | 'o1-preview-2024-09-12' | 'o1-mini' | 'o1-mini-2024-09-12' + | 'gpt-4.5-preview' + | 'gpt-4.5-preview-2025-02-27' | 'gpt-4o' | 'gpt-4o-2024-11-20' | 'gpt-4o-2024-08-06' @@ -100,6 +106,7 @@ export declare namespace Chat { type ChatCompletionContentPartInputAudio as ChatCompletionContentPartInputAudio, type ChatCompletionContentPartRefusal as ChatCompletionContentPartRefusal, type ChatCompletionContentPartText as ChatCompletionContentPartText, + type ChatCompletionDeleted as ChatCompletionDeleted, type ChatCompletionDeveloperMessageParam as ChatCompletionDeveloperMessageParam, type ChatCompletionFunctionCallOption as ChatCompletionFunctionCallOption, type ChatCompletionFunctionMessageParam as ChatCompletionFunctionMessageParam, @@ -111,6 +118,7 @@ export declare namespace Chat { type ChatCompletionPredictionContent as ChatCompletionPredictionContent, type ChatCompletionReasoningEffort as ChatCompletionReasoningEffort, type ChatCompletionRole as ChatCompletionRole, + type ChatCompletionStoreMessage as ChatCompletionStoreMessage, type ChatCompletionStreamOptions as ChatCompletionStreamOptions, type ChatCompletionSystemMessageParam as ChatCompletionSystemMessageParam, type ChatCompletionTokenLogprob as ChatCompletionTokenLogprob, @@ -118,11 +126,11 @@ export declare namespace Chat { type ChatCompletionToolChoiceOption as ChatCompletionToolChoiceOption, type ChatCompletionToolMessageParam as ChatCompletionToolMessageParam, type ChatCompletionUserMessageParam as ChatCompletionUserMessageParam, + type ChatCompletionsPage as ChatCompletionsPage, type ChatCompletionCreateParams as ChatCompletionCreateParams, - type CompletionCreateParams as CompletionCreateParams, type ChatCompletionCreateParamsNonStreaming as ChatCompletionCreateParamsNonStreaming, - type CompletionCreateParamsNonStreaming as CompletionCreateParamsNonStreaming, type ChatCompletionCreateParamsStreaming as ChatCompletionCreateParamsStreaming, - type CompletionCreateParamsStreaming as CompletionCreateParamsStreaming, + type ChatCompletionUpdateParams as ChatCompletionUpdateParams, + type ChatCompletionListParams as ChatCompletionListParams, }; } diff --git a/src/resources/chat/completions.ts b/src/resources/chat/completions.ts index ecb69099f..55b151e8b 100644 --- a/src/resources/chat/completions.ts +++ b/src/resources/chat/completions.ts @@ -1,1305 +1 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import { APIResource } from '../../resource'; -import * as ChatCompletionsAPI from './completions'; -import * as CompletionsAPI from '../completions'; -import * as Shared from '../shared'; -import * as ChatAPI from './chat'; -import { APIPromise } from '../../api-promise'; -import { Stream } from '../../streaming'; -import { RequestOptions } from '../../internal/request-options'; - -export class Completions extends APIResource { - /** - * Creates a model response for the given chat conversation. Learn more in the - * [text generation](https://platform.openai.com/docs/guides/text-generation), - * [vision](https://platform.openai.com/docs/guides/vision), and - * [audio](https://platform.openai.com/docs/guides/audio) guides. - * - * Parameter support can differ depending on the model used to generate the - * response, particularly for newer reasoning models. Parameters that are only - * supported for reasoning models are noted below. For the current state of - * unsupported parameters in reasoning models, - * [refer to the reasoning guide](https://platform.openai.com/docs/guides/reasoning). - */ - create(body: ChatCompletionCreateParamsNonStreaming, options?: RequestOptions): APIPromise; - create( - body: ChatCompletionCreateParamsStreaming, - options?: RequestOptions, - ): APIPromise>; - create( - body: ChatCompletionCreateParamsBase, - options?: RequestOptions, - ): APIPromise | ChatCompletion>; - create( - body: ChatCompletionCreateParams, - options?: RequestOptions, - ): APIPromise | APIPromise> { - return this._client.post('/chat/completions', { body, ...options, stream: body.stream ?? false }) as - | APIPromise - | APIPromise>; - } -} - -/** - * Represents a chat completion response returned by model, based on the provided - * input. - */ -export interface ChatCompletion { - /** - * A unique identifier for the chat completion. - */ - id: string; - - /** - * A list of chat completion choices. Can be more than one if `n` is greater - * than 1. - */ - choices: Array; - - /** - * The Unix timestamp (in seconds) of when the chat completion was created. - */ - created: number; - - /** - * The model used for the chat completion. - */ - model: string; - - /** - * The object type, which is always `chat.completion`. - */ - object: 'chat.completion'; - - /** - * The service tier used for processing the request. This field is only included if - * the `service_tier` parameter is specified in the request. - */ - service_tier?: 'scale' | 'default' | null; - - /** - * This fingerprint represents the backend configuration that the model runs with. - * - * Can be used in conjunction with the `seed` request parameter to understand when - * backend changes have been made that might impact determinism. - */ - system_fingerprint?: string; - - /** - * Usage statistics for the completion request. - */ - usage?: CompletionsAPI.CompletionUsage; -} - -export namespace ChatCompletion { - export interface Choice { - /** - * The reason the model stopped generating tokens. This will be `stop` if the model - * hit a natural stop point or a provided stop sequence, `length` if the maximum - * number of tokens specified in the request was reached, `content_filter` if - * content was omitted due to a flag from our content filters, `tool_calls` if the - * model called a tool, or `function_call` (deprecated) if the model called a - * function. - */ - finish_reason: 'stop' | 'length' | 'tool_calls' | 'content_filter' | 'function_call'; - - /** - * The index of the choice in the list of choices. - */ - index: number; - - /** - * Log probability information for the choice. - */ - logprobs: Choice.Logprobs | null; - - /** - * A chat completion message generated by the model. - */ - message: ChatCompletionsAPI.ChatCompletionMessage; - } - - export namespace Choice { - /** - * Log probability information for the choice. - */ - export interface Logprobs { - /** - * A list of message content tokens with log probability information. - */ - content: Array | null; - - /** - * A list of message refusal tokens with log probability information. - */ - refusal: Array | null; - } - } -} - -/** - * Messages sent by the model in response to user messages. - */ -export interface ChatCompletionAssistantMessageParam { - /** - * The role of the messages author, in this case `assistant`. - */ - role: 'assistant'; - - /** - * Data about a previous audio response from the model. - * [Learn more](https://platform.openai.com/docs/guides/audio). - */ - audio?: ChatCompletionAssistantMessageParam.Audio | null; - - /** - * The contents of the assistant message. Required unless `tool_calls` or - * `function_call` is specified. - */ - content?: string | Array | null; - - /** - * @deprecated: Deprecated and replaced by `tool_calls`. The name and arguments of - * a function that should be called, as generated by the model. - */ - function_call?: ChatCompletionAssistantMessageParam.FunctionCall | null; - - /** - * An optional name for the participant. Provides the model information to - * differentiate between participants of the same role. - */ - name?: string; - - /** - * The refusal message by the assistant. - */ - refusal?: string | null; - - /** - * The tool calls generated by the model, such as function calls. - */ - tool_calls?: Array; -} - -export namespace ChatCompletionAssistantMessageParam { - /** - * Data about a previous audio response from the model. - * [Learn more](https://platform.openai.com/docs/guides/audio). - */ - export interface Audio { - /** - * Unique identifier for a previous audio response from the model. - */ - id: string; - } - - /** - * @deprecated: Deprecated and replaced by `tool_calls`. The name and arguments of - * a function that should be called, as generated by the model. - */ - export interface FunctionCall { - /** - * The arguments to call the function with, as generated by the model in JSON - * format. Note that the model does not always generate valid JSON, and may - * hallucinate parameters not defined by your function schema. Validate the - * arguments in your code before calling your function. - */ - arguments: string; - - /** - * The name of the function to call. - */ - name: string; - } -} - -/** - * If the audio output modality is requested, this object contains data about the - * audio response from the model. - * [Learn more](https://platform.openai.com/docs/guides/audio). - */ -export interface ChatCompletionAudio { - /** - * Unique identifier for this audio response. - */ - id: string; - - /** - * Base64 encoded audio bytes generated by the model, in the format specified in - * the request. - */ - data: string; - - /** - * The Unix timestamp (in seconds) for when this audio response will no longer be - * accessible on the server for use in multi-turn conversations. - */ - expires_at: number; - - /** - * Transcript of the audio generated by the model. - */ - transcript: string; -} - -/** - * Parameters for audio output. Required when audio output is requested with - * `modalities: ["audio"]`. - * [Learn more](https://platform.openai.com/docs/guides/audio). - */ -export interface ChatCompletionAudioParam { - /** - * Specifies the output audio format. Must be one of `wav`, `mp3`, `flac`, `opus`, - * or `pcm16`. - */ - format: 'wav' | 'mp3' | 'flac' | 'opus' | 'pcm16'; - - /** - * The voice the model uses to respond. Supported voices are `ash`, `ballad`, - * `coral`, `sage`, and `verse` (also supported but not recommended are `alloy`, - * `echo`, and `shimmer`; these voices are less expressive). - */ - voice: 'alloy' | 'ash' | 'ballad' | 'coral' | 'echo' | 'sage' | 'shimmer' | 'verse'; -} - -/** - * Represents a streamed chunk of a chat completion response returned by model, - * based on the provided input. - */ -export interface ChatCompletionChunk { - /** - * A unique identifier for the chat completion. Each chunk has the same ID. - */ - id: string; - - /** - * A list of chat completion choices. Can contain more than one elements if `n` is - * greater than 1. Can also be empty for the last chunk if you set - * `stream_options: {"include_usage": true}`. - */ - choices: Array; - - /** - * The Unix timestamp (in seconds) of when the chat completion was created. Each - * chunk has the same timestamp. - */ - created: number; - - /** - * The model to generate the completion. - */ - model: string; - - /** - * The object type, which is always `chat.completion.chunk`. - */ - object: 'chat.completion.chunk'; - - /** - * The service tier used for processing the request. This field is only included if - * the `service_tier` parameter is specified in the request. - */ - service_tier?: 'scale' | 'default' | null; - - /** - * This fingerprint represents the backend configuration that the model runs with. - * Can be used in conjunction with the `seed` request parameter to understand when - * backend changes have been made that might impact determinism. - */ - system_fingerprint?: string; - - /** - * An optional field that will only be present when you set - * `stream_options: {"include_usage": true}` in your request. When present, it - * contains a null value except for the last chunk which contains the token usage - * statistics for the entire request. - */ - usage?: CompletionsAPI.CompletionUsage | null; -} - -export namespace ChatCompletionChunk { - export interface Choice { - /** - * A chat completion delta generated by streamed model responses. - */ - delta: Choice.Delta; - - /** - * The reason the model stopped generating tokens. This will be `stop` if the model - * hit a natural stop point or a provided stop sequence, `length` if the maximum - * number of tokens specified in the request was reached, `content_filter` if - * content was omitted due to a flag from our content filters, `tool_calls` if the - * model called a tool, or `function_call` (deprecated) if the model called a - * function. - */ - finish_reason: 'stop' | 'length' | 'tool_calls' | 'content_filter' | 'function_call' | null; - - /** - * The index of the choice in the list of choices. - */ - index: number; - - /** - * Log probability information for the choice. - */ - logprobs?: Choice.Logprobs | null; - } - - export namespace Choice { - /** - * A chat completion delta generated by streamed model responses. - */ - export interface Delta { - /** - * The contents of the chunk message. - */ - content?: string | null; - - /** - * @deprecated: Deprecated and replaced by `tool_calls`. The name and arguments of - * a function that should be called, as generated by the model. - */ - function_call?: Delta.FunctionCall; - - /** - * The refusal message generated by the model. - */ - refusal?: string | null; - - /** - * The role of the author of this message. - */ - role?: 'system' | 'user' | 'assistant' | 'tool'; - - tool_calls?: Array; - } - - export namespace Delta { - /** - * @deprecated: Deprecated and replaced by `tool_calls`. The name and arguments of - * a function that should be called, as generated by the model. - */ - export interface FunctionCall { - /** - * The arguments to call the function with, as generated by the model in JSON - * format. Note that the model does not always generate valid JSON, and may - * hallucinate parameters not defined by your function schema. Validate the - * arguments in your code before calling your function. - */ - arguments?: string; - - /** - * The name of the function to call. - */ - name?: string; - } - - export interface ToolCall { - index: number; - - /** - * The ID of the tool call. - */ - id?: string; - - function?: ToolCall.Function; - - /** - * The type of the tool. Currently, only `function` is supported. - */ - type?: 'function'; - } - - export namespace ToolCall { - export interface Function { - /** - * The arguments to call the function with, as generated by the model in JSON - * format. Note that the model does not always generate valid JSON, and may - * hallucinate parameters not defined by your function schema. Validate the - * arguments in your code before calling your function. - */ - arguments?: string; - - /** - * The name of the function to call. - */ - name?: string; - } - } - } - - /** - * Log probability information for the choice. - */ - export interface Logprobs { - /** - * A list of message content tokens with log probability information. - */ - content: Array | null; - - /** - * A list of message refusal tokens with log probability information. - */ - refusal: Array | null; - } - } -} - -/** - * Learn about - * [text inputs](https://platform.openai.com/docs/guides/text-generation). - */ -export type ChatCompletionContentPart = - | ChatCompletionContentPartText - | ChatCompletionContentPartImage - | ChatCompletionContentPartInputAudio; - -/** - * Learn about [image inputs](https://platform.openai.com/docs/guides/vision). - */ -export interface ChatCompletionContentPartImage { - image_url: ChatCompletionContentPartImage.ImageURL; - - /** - * The type of the content part. - */ - type: 'image_url'; -} - -export namespace ChatCompletionContentPartImage { - export interface ImageURL { - /** - * Either a URL of the image or the base64 encoded image data. - */ - url: string; - - /** - * Specifies the detail level of the image. Learn more in the - * [Vision guide](https://platform.openai.com/docs/guides/vision#low-or-high-fidelity-image-understanding). - */ - detail?: 'auto' | 'low' | 'high'; - } -} - -/** - * Learn about [audio inputs](https://platform.openai.com/docs/guides/audio). - */ -export interface ChatCompletionContentPartInputAudio { - input_audio: ChatCompletionContentPartInputAudio.InputAudio; - - /** - * The type of the content part. Always `input_audio`. - */ - type: 'input_audio'; -} - -export namespace ChatCompletionContentPartInputAudio { - export interface InputAudio { - /** - * Base64 encoded audio data. - */ - data: string; - - /** - * The format of the encoded audio data. Currently supports "wav" and "mp3". - */ - format: 'wav' | 'mp3'; - } -} - -export interface ChatCompletionContentPartRefusal { - /** - * The refusal message generated by the model. - */ - refusal: string; - - /** - * The type of the content part. - */ - type: 'refusal'; -} - -/** - * Learn about - * [text inputs](https://platform.openai.com/docs/guides/text-generation). - */ -export interface ChatCompletionContentPartText { - /** - * The text content. - */ - text: string; - - /** - * The type of the content part. - */ - type: 'text'; -} - -/** - * Developer-provided instructions that the model should follow, regardless of - * messages sent by the user. With o1 models and newer, `developer` messages - * replace the previous `system` messages. - */ -export interface ChatCompletionDeveloperMessageParam { - /** - * The contents of the developer message. - */ - content: string | Array; - - /** - * The role of the messages author, in this case `developer`. - */ - role: 'developer'; - - /** - * An optional name for the participant. Provides the model information to - * differentiate between participants of the same role. - */ - name?: string; -} - -/** - * Specifying a particular function via `{"name": "my_function"}` forces the model - * to call that function. - */ -export interface ChatCompletionFunctionCallOption { - /** - * The name of the function to call. - */ - name: string; -} - -/** - * @deprecated - */ -export interface ChatCompletionFunctionMessageParam { - /** - * The contents of the function message. - */ - content: string | null; - - /** - * The name of the function to call. - */ - name: string; - - /** - * The role of the messages author, in this case `function`. - */ - role: 'function'; -} - -/** - * A chat completion message generated by the model. - */ -export interface ChatCompletionMessage { - /** - * The contents of the message. - */ - content: string | null; - - /** - * The refusal message generated by the model. - */ - refusal: string | null; - - /** - * The role of the author of this message. - */ - role: 'assistant'; - - /** - * If the audio output modality is requested, this object contains data about the - * audio response from the model. - * [Learn more](https://platform.openai.com/docs/guides/audio). - */ - audio?: ChatCompletionAudio | null; - - /** - * @deprecated: Deprecated and replaced by `tool_calls`. The name and arguments of - * a function that should be called, as generated by the model. - */ - function_call?: ChatCompletionMessage.FunctionCall | null; - - /** - * The tool calls generated by the model, such as function calls. - */ - tool_calls?: Array; -} - -export namespace ChatCompletionMessage { - /** - * @deprecated: Deprecated and replaced by `tool_calls`. The name and arguments of - * a function that should be called, as generated by the model. - */ - export interface FunctionCall { - /** - * The arguments to call the function with, as generated by the model in JSON - * format. Note that the model does not always generate valid JSON, and may - * hallucinate parameters not defined by your function schema. Validate the - * arguments in your code before calling your function. - */ - arguments: string; - - /** - * The name of the function to call. - */ - name: string; - } -} - -/** - * Developer-provided instructions that the model should follow, regardless of - * messages sent by the user. With o1 models and newer, `developer` messages - * replace the previous `system` messages. - */ -export type ChatCompletionMessageParam = - | ChatCompletionDeveloperMessageParam - | ChatCompletionSystemMessageParam - | ChatCompletionUserMessageParam - | ChatCompletionAssistantMessageParam - | ChatCompletionToolMessageParam - | ChatCompletionFunctionMessageParam; - -export interface ChatCompletionMessageToolCall { - /** - * The ID of the tool call. - */ - id: string; - - /** - * The function that the model called. - */ - function: ChatCompletionMessageToolCall.Function; - - /** - * The type of the tool. Currently, only `function` is supported. - */ - type: 'function'; -} - -export namespace ChatCompletionMessageToolCall { - /** - * The function that the model called. - */ - export interface Function { - /** - * The arguments to call the function with, as generated by the model in JSON - * format. Note that the model does not always generate valid JSON, and may - * hallucinate parameters not defined by your function schema. Validate the - * arguments in your code before calling your function. - */ - arguments: string; - - /** - * The name of the function to call. - */ - name: string; - } -} - -export type ChatCompletionModality = 'text' | 'audio'; - -/** - * Specifies a tool the model should use. Use to force the model to call a specific - * function. - */ -export interface ChatCompletionNamedToolChoice { - function: ChatCompletionNamedToolChoice.Function; - - /** - * The type of the tool. Currently, only `function` is supported. - */ - type: 'function'; -} - -export namespace ChatCompletionNamedToolChoice { - export interface Function { - /** - * The name of the function to call. - */ - name: string; - } -} - -/** - * Static predicted output content, such as the content of a text file that is - * being regenerated. - */ -export interface ChatCompletionPredictionContent { - /** - * The content that should be matched when generating a model response. If - * generated tokens would match this content, the entire model response can be - * returned much more quickly. - */ - content: string | Array; - - /** - * The type of the predicted content you want to provide. This type is currently - * always `content`. - */ - type: 'content'; -} - -/** - * **o1 models only** - * - * Constrains effort on reasoning for - * [reasoning models](https://platform.openai.com/docs/guides/reasoning). Currently - * supported values are `low`, `medium`, and `high`. Reducing reasoning effort can - * result in faster responses and fewer tokens used on reasoning in a response. - */ -export type ChatCompletionReasoningEffort = 'low' | 'medium' | 'high'; - -/** - * The role of the author of a message - */ -export type ChatCompletionRole = 'system' | 'user' | 'assistant' | 'tool' | 'function'; - -/** - * Options for streaming response. Only set this when you set `stream: true`. - */ -export interface ChatCompletionStreamOptions { - /** - * If set, an additional chunk will be streamed before the `data: [DONE]` message. - * The `usage` field on this chunk shows the token usage statistics for the entire - * request, and the `choices` field will always be an empty array. All other chunks - * will also include a `usage` field, but with a null value. - */ - include_usage?: boolean; -} - -/** - * Developer-provided instructions that the model should follow, regardless of - * messages sent by the user. With o1 models and newer, use `developer` messages - * for this purpose instead. - */ -export interface ChatCompletionSystemMessageParam { - /** - * The contents of the system message. - */ - content: string | Array; - - /** - * The role of the messages author, in this case `system`. - */ - role: 'system'; - - /** - * An optional name for the participant. Provides the model information to - * differentiate between participants of the same role. - */ - name?: string; -} - -export interface ChatCompletionTokenLogprob { - /** - * The token. - */ - token: string; - - /** - * A list of integers representing the UTF-8 bytes representation of the token. - * Useful in instances where characters are represented by multiple tokens and - * their byte representations must be combined to generate the correct text - * representation. Can be `null` if there is no bytes representation for the token. - */ - bytes: Array | null; - - /** - * The log probability of this token, if it is within the top 20 most likely - * tokens. Otherwise, the value `-9999.0` is used to signify that the token is very - * unlikely. - */ - logprob: number; - - /** - * List of the most likely tokens and their log probability, at this token - * position. In rare cases, there may be fewer than the number of requested - * `top_logprobs` returned. - */ - top_logprobs: Array; -} - -export namespace ChatCompletionTokenLogprob { - export interface TopLogprob { - /** - * The token. - */ - token: string; - - /** - * A list of integers representing the UTF-8 bytes representation of the token. - * Useful in instances where characters are represented by multiple tokens and - * their byte representations must be combined to generate the correct text - * representation. Can be `null` if there is no bytes representation for the token. - */ - bytes: Array | null; - - /** - * The log probability of this token, if it is within the top 20 most likely - * tokens. Otherwise, the value `-9999.0` is used to signify that the token is very - * unlikely. - */ - logprob: number; - } -} - -export interface ChatCompletionTool { - function: Shared.FunctionDefinition; - - /** - * The type of the tool. Currently, only `function` is supported. - */ - type: 'function'; -} - -/** - * Controls which (if any) tool is called by the model. `none` means the model will - * not call any tool and instead generates a message. `auto` means the model can - * pick between generating a message or calling one or more tools. `required` means - * the model must call one or more tools. Specifying a particular tool via - * `{"type": "function", "function": {"name": "my_function"}}` forces the model to - * call that tool. - * - * `none` is the default when no tools are present. `auto` is the default if tools - * are present. - */ -export type ChatCompletionToolChoiceOption = 'none' | 'auto' | 'required' | ChatCompletionNamedToolChoice; - -export interface ChatCompletionToolMessageParam { - /** - * The contents of the tool message. - */ - content: string | Array; - - /** - * The role of the messages author, in this case `tool`. - */ - role: 'tool'; - - /** - * Tool call that this message is responding to. - */ - tool_call_id: string; -} - -/** - * Messages sent by an end user, containing prompts or additional context - * information. - */ -export interface ChatCompletionUserMessageParam { - /** - * The contents of the user message. - */ - content: string | Array; - - /** - * The role of the messages author, in this case `user`. - */ - role: 'user'; - - /** - * An optional name for the participant. Provides the model information to - * differentiate between participants of the same role. - */ - name?: string; -} - -export type ChatCompletionCreateParams = - | ChatCompletionCreateParamsNonStreaming - | ChatCompletionCreateParamsStreaming; - -export interface ChatCompletionCreateParamsBase { - /** - * A list of messages comprising the conversation so far. Depending on the - * [model](https://platform.openai.com/docs/models) you use, different message - * types (modalities) are supported, like - * [text](https://platform.openai.com/docs/guides/text-generation), - * [images](https://platform.openai.com/docs/guides/vision), and - * [audio](https://platform.openai.com/docs/guides/audio). - */ - messages: Array; - - /** - * ID of the model to use. See the - * [model endpoint compatibility](https://platform.openai.com/docs/models#model-endpoint-compatibility) - * table for details on which models work with the Chat API. - */ - model: (string & {}) | ChatAPI.ChatModel; - - /** - * Parameters for audio output. Required when audio output is requested with - * `modalities: ["audio"]`. - * [Learn more](https://platform.openai.com/docs/guides/audio). - */ - audio?: ChatCompletionAudioParam | 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. - */ - frequency_penalty?: number | null; - - /** - * Deprecated in favor of `tool_choice`. - * - * Controls which (if any) function is called by the model. - * - * `none` means the model will not call a function and instead generates a message. - * - * `auto` means the model can pick between generating a message or calling a - * function. - * - * Specifying a particular function via `{"name": "my_function"}` forces the model - * to call that function. - * - * `none` is the default when no functions are present. `auto` is the default if - * functions are present. - */ - function_call?: 'none' | 'auto' | ChatCompletionFunctionCallOption; - - /** - * Deprecated in favor of `tools`. - * - * A list of functions the model may generate JSON inputs for. - */ - functions?: Array; - - /** - * Modify the likelihood of specified tokens appearing in the completion. - * - * Accepts a JSON object that maps tokens (specified by their token ID in the - * tokenizer) to an associated bias value from -100 to 100. Mathematically, the - * bias is added to the logits generated by the model prior to sampling. The exact - * effect will vary per model, but values between -1 and 1 should decrease or - * increase likelihood of selection; values like -100 or 100 should result in a ban - * or exclusive selection of the relevant token. - */ - logit_bias?: Record | null; - - /** - * Whether to return log probabilities of the output tokens or not. If true, - * returns the log probabilities of each output token returned in the `content` of - * `message`. - */ - logprobs?: boolean | null; - - /** - * An upper bound for the number of tokens that can be generated for a completion, - * including visible output tokens and - * [reasoning tokens](https://platform.openai.com/docs/guides/reasoning). - */ - max_completion_tokens?: number | null; - - /** - * The maximum number of [tokens](/tokenizer) that can be generated in the chat - * completion. This value can be used to control - * [costs](https://openai.com/api/pricing/) for text generated via API. - * - * This value is now deprecated in favor of `max_completion_tokens`, and is not - * compatible with - * [o1 series models](https://platform.openai.com/docs/guides/reasoning). - */ - max_tokens?: number | null; - - /** - * Developer-defined tags and values used for filtering completions in the - * [dashboard](https://platform.openai.com/chat-completions). - */ - metadata?: Record | null; - - /** - * Output types that you would like the model to generate for this request. Most - * models are capable of generating text, which is the default: - * - * `["text"]` - * - * The `gpt-4o-audio-preview` model can also be used to - * [generate audio](https://platform.openai.com/docs/guides/audio). To request that - * this model generate both text and audio responses, you can use: - * - * `["text", "audio"]` - */ - modalities?: Array | null; - - /** - * How many chat completion choices to generate for each input message. Note that - * you will be charged based on the number of generated tokens across all of the - * choices. Keep `n` as `1` to minimize costs. - */ - n?: number | null; - - /** - * Whether to enable - * [parallel function calling](https://platform.openai.com/docs/guides/function-calling#configuring-parallel-function-calling) - * during tool use. - */ - parallel_tool_calls?: boolean; - - /** - * Static predicted output content, such as the content of a text file that is - * being regenerated. - */ - prediction?: ChatCompletionPredictionContent | null; - - /** - * Number between -2.0 and 2.0. Positive values penalize new tokens based on - * whether they appear in the text so far, increasing the model's likelihood to - * talk about new topics. - */ - presence_penalty?: number | null; - - /** - * **o1 models only** - * - * Constrains effort on reasoning for - * [reasoning models](https://platform.openai.com/docs/guides/reasoning). Currently - * supported values are `low`, `medium`, and `high`. Reducing reasoning effort can - * result in faster responses and fewer tokens used on reasoning in a response. - */ - reasoning_effort?: ChatCompletionReasoningEffort; - - /** - * An object specifying the format that the model must output. - * - * Setting to `{ "type": "json_schema", "json_schema": {...} }` enables Structured - * Outputs which ensures the model will match your supplied JSON schema. Learn more - * in the - * [Structured Outputs guide](https://platform.openai.com/docs/guides/structured-outputs). - * - * Setting to `{ "type": "json_object" }` enables JSON mode, which ensures the - * message the model generates is valid JSON. - * - * **Important:** when using JSON mode, you **must** also instruct the model to - * produce JSON yourself via a system or user message. Without this, the model may - * generate an unending stream of whitespace until the generation reaches the token - * limit, resulting in a long-running and seemingly "stuck" request. Also note that - * the message content may be partially cut off if `finish_reason="length"`, which - * indicates the generation exceeded `max_tokens` or the conversation exceeded the - * max context length. - */ - response_format?: - | Shared.ResponseFormatText - | Shared.ResponseFormatJSONObject - | Shared.ResponseFormatJSONSchema; - - /** - * This feature is in Beta. If specified, our system will make a best effort to - * sample deterministically, such that repeated requests with the same `seed` and - * parameters should return the same result. Determinism is not guaranteed, and you - * should refer to the `system_fingerprint` response parameter to monitor changes - * in the backend. - */ - seed?: number | null; - - /** - * Specifies the latency tier to use for processing the request. This parameter is - * relevant for customers subscribed to the scale tier service: - * - * - If set to 'auto', and the Project is Scale tier enabled, the system will - * utilize scale tier credits until they are exhausted. - * - If set to 'auto', and the Project is not Scale tier enabled, the request will - * be processed using the default service tier with a lower uptime SLA and no - * latency guarentee. - * - If set to 'default', the request will be processed using the default service - * tier with a lower uptime SLA and no latency guarentee. - * - When not set, the default behavior is 'auto'. - * - * When this parameter is set, the response body will include the `service_tier` - * utilized. - */ - service_tier?: 'auto' | 'default' | null; - - /** - * Up to 4 sequences where the API will stop generating further tokens. - */ - stop?: string | null | Array; - - /** - * Whether or not to store the output of this chat completion request for use in - * our [model distillation](https://platform.openai.com/docs/guides/distillation) - * or [evals](https://platform.openai.com/docs/guides/evals) products. - */ - store?: boolean | null; - - /** - * If set, partial message deltas will be sent, like in ChatGPT. Tokens will be - * sent as data-only - * [server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format) - * as they become available, with the stream terminated by a `data: [DONE]` - * message. - * [Example Python code](https://cookbook.openai.com/examples/how_to_stream_completions). - */ - stream?: boolean | null; - - /** - * Options for streaming response. Only set this when you set `stream: true`. - */ - stream_options?: ChatCompletionStreamOptions | 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 generally recommend altering this or `top_p` but - * not both. - */ - temperature?: number | null; - - /** - * Controls which (if any) tool is called by the model. `none` means the model will - * not call any tool and instead generates a message. `auto` means the model can - * pick between generating a message or calling one or more tools. `required` means - * the model must call one or more tools. Specifying a particular tool via - * `{"type": "function", "function": {"name": "my_function"}}` forces the model to - * call that tool. - * - * `none` is the default when no tools are present. `auto` is the default if tools - * are present. - */ - tool_choice?: ChatCompletionToolChoiceOption; - - /** - * A list of tools the model may call. Currently, only functions are supported as a - * tool. Use this to provide a list of functions the model may generate JSON inputs - * for. A max of 128 functions are supported. - */ - tools?: Array; - - /** - * An integer between 0 and 20 specifying the number of most likely tokens to - * return at each token position, each with an associated log probability. - * `logprobs` must be set to `true` if this parameter is used. - */ - top_logprobs?: number | null; - - /** - * An alternative to sampling with temperature, called nucleus sampling, where the - * model considers the results of the tokens with top_p probability mass. So 0.1 - * means only the tokens comprising the top 10% probability mass are considered. - * - * We generally recommend altering this or `temperature` but not both. - */ - top_p?: number | null; - - /** - * A unique identifier representing your end-user, which can help OpenAI to monitor - * and detect abuse. - * [Learn more](https://platform.openai.com/docs/guides/safety-best-practices#end-user-ids). - */ - user?: string; -} - -export namespace ChatCompletionCreateParams { - /** - * @deprecated - */ - export interface Function { - /** - * The name of the function to be called. Must be a-z, A-Z, 0-9, or contain - * underscores and dashes, with a maximum length of 64. - */ - name: string; - - /** - * A description of what the function does, used by the model to choose when and - * how to call the function. - */ - description?: string; - - /** - * The parameters the functions accepts, described as a JSON Schema object. See the - * [guide](https://platform.openai.com/docs/guides/function-calling) for examples, - * and the - * [JSON Schema reference](https://json-schema.org/understanding-json-schema/) for - * documentation about the format. - * - * Omitting `parameters` defines a function with an empty parameter list. - */ - parameters?: Shared.FunctionParameters; - } - - export type ChatCompletionCreateParamsNonStreaming = - ChatCompletionsAPI.ChatCompletionCreateParamsNonStreaming; - export type ChatCompletionCreateParamsStreaming = ChatCompletionsAPI.ChatCompletionCreateParamsStreaming; -} - -/** - * @deprecated Use ChatCompletionCreateParams instead - */ -export type CompletionCreateParams = ChatCompletionCreateParams; - -export interface ChatCompletionCreateParamsNonStreaming extends ChatCompletionCreateParamsBase { - /** - * If set, partial message deltas will be sent, like in ChatGPT. Tokens will be - * sent as data-only - * [server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format) - * as they become available, with the stream terminated by a `data: [DONE]` - * message. - * [Example Python code](https://cookbook.openai.com/examples/how_to_stream_completions). - */ - stream?: false | null; -} - -/** - * @deprecated Use ChatCompletionCreateParamsNonStreaming instead - */ -export type CompletionCreateParamsNonStreaming = ChatCompletionCreateParamsNonStreaming; - -export interface ChatCompletionCreateParamsStreaming extends ChatCompletionCreateParamsBase { - /** - * If set, partial message deltas will be sent, like in ChatGPT. Tokens will be - * sent as data-only - * [server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format) - * as they become available, with the stream terminated by a `data: [DONE]` - * message. - * [Example Python code](https://cookbook.openai.com/examples/how_to_stream_completions). - */ - stream: true; -} - -/** - * @deprecated Use ChatCompletionCreateParamsStreaming instead - */ -export type CompletionCreateParamsStreaming = ChatCompletionCreateParamsStreaming; - -export declare namespace Completions { - export { - type ChatCompletion as ChatCompletion, - type ChatCompletionAssistantMessageParam as ChatCompletionAssistantMessageParam, - type ChatCompletionAudio as ChatCompletionAudio, - type ChatCompletionAudioParam as ChatCompletionAudioParam, - type ChatCompletionChunk as ChatCompletionChunk, - type ChatCompletionContentPart as ChatCompletionContentPart, - type ChatCompletionContentPartImage as ChatCompletionContentPartImage, - type ChatCompletionContentPartInputAudio as ChatCompletionContentPartInputAudio, - type ChatCompletionContentPartRefusal as ChatCompletionContentPartRefusal, - type ChatCompletionContentPartText as ChatCompletionContentPartText, - type ChatCompletionDeveloperMessageParam as ChatCompletionDeveloperMessageParam, - type ChatCompletionFunctionCallOption as ChatCompletionFunctionCallOption, - type ChatCompletionFunctionMessageParam as ChatCompletionFunctionMessageParam, - type ChatCompletionMessage as ChatCompletionMessage, - type ChatCompletionMessageParam as ChatCompletionMessageParam, - type ChatCompletionMessageToolCall as ChatCompletionMessageToolCall, - type ChatCompletionModality as ChatCompletionModality, - type ChatCompletionNamedToolChoice as ChatCompletionNamedToolChoice, - type ChatCompletionPredictionContent as ChatCompletionPredictionContent, - type ChatCompletionReasoningEffort as ChatCompletionReasoningEffort, - type ChatCompletionRole as ChatCompletionRole, - type ChatCompletionStreamOptions as ChatCompletionStreamOptions, - type ChatCompletionSystemMessageParam as ChatCompletionSystemMessageParam, - type ChatCompletionTokenLogprob as ChatCompletionTokenLogprob, - type ChatCompletionTool as ChatCompletionTool, - type ChatCompletionToolChoiceOption as ChatCompletionToolChoiceOption, - type ChatCompletionToolMessageParam as ChatCompletionToolMessageParam, - type ChatCompletionUserMessageParam as ChatCompletionUserMessageParam, - type ChatCompletionCreateParams as ChatCompletionCreateParams, - type CompletionCreateParams as CompletionCreateParams, - type ChatCompletionCreateParamsNonStreaming as ChatCompletionCreateParamsNonStreaming, - type CompletionCreateParamsNonStreaming as CompletionCreateParamsNonStreaming, - type ChatCompletionCreateParamsStreaming as ChatCompletionCreateParamsStreaming, - type CompletionCreateParamsStreaming as CompletionCreateParamsStreaming, - }; -} +export * from './completions/completions'; diff --git a/src/resources/chat/completions/completions.ts b/src/resources/chat/completions/completions.ts new file mode 100644 index 000000000..29c9ad390 --- /dev/null +++ b/src/resources/chat/completions/completions.ts @@ -0,0 +1,1405 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { APIResource } from '../../../resource'; +import * as CompletionsCompletionsAPI from './completions'; +import * as CompletionsAPI from '../../completions'; +import * as Shared from '../../shared'; +import * as ChatAPI from '../chat'; +import * as MessagesAPI from './messages'; +import { MessageListParams, Messages } from './messages'; +import { APIPromise } from '../../../api-promise'; +import { CursorPage, type CursorPageParams, PagePromise } from '../../../pagination'; +import { Stream } from '../../../streaming'; +import { RequestOptions } from '../../../internal/request-options'; +import { path } from '../../../internal/utils/path'; + +export class Completions extends APIResource { + messages: MessagesAPI.Messages = new MessagesAPI.Messages(this._client); + + /** + * Creates a model response for the given chat conversation. Learn more in the + * [text generation](https://platform.openai.com/docs/guides/text-generation), + * [vision](https://platform.openai.com/docs/guides/vision), and + * [audio](https://platform.openai.com/docs/guides/audio) guides. + * + * Parameter support can differ depending on the model used to generate the + * response, particularly for newer reasoning models. Parameters that are only + * supported for reasoning models are noted below. For the current state of + * unsupported parameters in reasoning models, + * [refer to the reasoning guide](https://platform.openai.com/docs/guides/reasoning). + */ + create(body: ChatCompletionCreateParamsNonStreaming, options?: RequestOptions): APIPromise; + create( + body: ChatCompletionCreateParamsStreaming, + options?: RequestOptions, + ): APIPromise>; + create( + body: ChatCompletionCreateParamsBase, + options?: RequestOptions, + ): APIPromise | ChatCompletion>; + create( + body: ChatCompletionCreateParams, + options?: RequestOptions, + ): APIPromise | APIPromise> { + return this._client.post('/chat/completions', { body, ...options, stream: body.stream ?? false }) as + | APIPromise + | APIPromise>; + } + + /** + * Get a stored chat completion. Only chat completions that have been created with + * the `store` parameter set to `true` will be returned. + */ + retrieve(completionID: string, options?: RequestOptions): APIPromise { + return this._client.get(path`/chat/completions/${completionID}`, options); + } + + /** + * Modify a stored chat completion. Only chat completions that have been created + * with the `store` parameter set to `true` can be modified. Currently, the only + * supported modification is to update the `metadata` field. + */ + update( + completionID: string, + body: ChatCompletionUpdateParams, + options?: RequestOptions, + ): APIPromise { + return this._client.post(path`/chat/completions/${completionID}`, { body, ...options }); + } + + /** + * List stored chat completions. Only chat completions that have been stored with + * the `store` parameter set to `true` will be returned. + */ + list( + query: ChatCompletionListParams | null | undefined = {}, + options?: RequestOptions, + ): PagePromise { + return this._client.getAPIList('/chat/completions', CursorPage, { query, ...options }); + } + + /** + * Delete a stored chat completion. Only chat completions that have been created + * with the `store` parameter set to `true` can be deleted. + */ + delete(completionID: string, options?: RequestOptions): APIPromise { + return this._client.delete(path`/chat/completions/${completionID}`, options); + } +} + +export type ChatCompletionsPage = CursorPage; + +export type ChatCompletionStoreMessagesPage = CursorPage; + +/** + * Represents a chat completion response returned by model, based on the provided + * input. + */ +export interface ChatCompletion { + /** + * A unique identifier for the chat completion. + */ + id: string; + + /** + * A list of chat completion choices. Can be more than one if `n` is greater + * than 1. + */ + choices: Array; + + /** + * The Unix timestamp (in seconds) of when the chat completion was created. + */ + created: number; + + /** + * The model used for the chat completion. + */ + model: string; + + /** + * The object type, which is always `chat.completion`. + */ + object: 'chat.completion'; + + /** + * The service tier used for processing the request. + */ + service_tier?: 'scale' | 'default' | null; + + /** + * This fingerprint represents the backend configuration that the model runs with. + * + * Can be used in conjunction with the `seed` request parameter to understand when + * backend changes have been made that might impact determinism. + */ + system_fingerprint?: string; + + /** + * Usage statistics for the completion request. + */ + usage?: CompletionsAPI.CompletionUsage; +} + +export namespace ChatCompletion { + export interface Choice { + /** + * The reason the model stopped generating tokens. This will be `stop` if the model + * hit a natural stop point or a provided stop sequence, `length` if the maximum + * number of tokens specified in the request was reached, `content_filter` if + * content was omitted due to a flag from our content filters, `tool_calls` if the + * model called a tool, or `function_call` (deprecated) if the model called a + * function. + */ + finish_reason: 'stop' | 'length' | 'tool_calls' | 'content_filter' | 'function_call'; + + /** + * The index of the choice in the list of choices. + */ + index: number; + + /** + * Log probability information for the choice. + */ + logprobs: Choice.Logprobs | null; + + /** + * A chat completion message generated by the model. + */ + message: CompletionsCompletionsAPI.ChatCompletionMessage; + } + + export namespace Choice { + /** + * Log probability information for the choice. + */ + export interface Logprobs { + /** + * A list of message content tokens with log probability information. + */ + content: Array | null; + + /** + * A list of message refusal tokens with log probability information. + */ + refusal: Array | null; + } + } +} + +/** + * Messages sent by the model in response to user messages. + */ +export interface ChatCompletionAssistantMessageParam { + /** + * The role of the messages author, in this case `assistant`. + */ + role: 'assistant'; + + /** + * Data about a previous audio response from the model. + * [Learn more](https://platform.openai.com/docs/guides/audio). + */ + audio?: ChatCompletionAssistantMessageParam.Audio | null; + + /** + * The contents of the assistant message. Required unless `tool_calls` or + * `function_call` is specified. + */ + content?: string | Array | null; + + /** + * @deprecated Deprecated and replaced by `tool_calls`. The name and arguments of a + * function that should be called, as generated by the model. + */ + function_call?: ChatCompletionAssistantMessageParam.FunctionCall | null; + + /** + * An optional name for the participant. Provides the model information to + * differentiate between participants of the same role. + */ + name?: string; + + /** + * The refusal message by the assistant. + */ + refusal?: string | null; + + /** + * The tool calls generated by the model, such as function calls. + */ + tool_calls?: Array; +} + +export namespace ChatCompletionAssistantMessageParam { + /** + * Data about a previous audio response from the model. + * [Learn more](https://platform.openai.com/docs/guides/audio). + */ + export interface Audio { + /** + * Unique identifier for a previous audio response from the model. + */ + id: string; + } + + /** + * @deprecated Deprecated and replaced by `tool_calls`. The name and arguments of a + * function that should be called, as generated by the model. + */ + export interface FunctionCall { + /** + * The arguments to call the function with, as generated by the model in JSON + * format. Note that the model does not always generate valid JSON, and may + * hallucinate parameters not defined by your function schema. Validate the + * arguments in your code before calling your function. + */ + arguments: string; + + /** + * The name of the function to call. + */ + name: string; + } +} + +/** + * If the audio output modality is requested, this object contains data about the + * audio response from the model. + * [Learn more](https://platform.openai.com/docs/guides/audio). + */ +export interface ChatCompletionAudio { + /** + * Unique identifier for this audio response. + */ + id: string; + + /** + * Base64 encoded audio bytes generated by the model, in the format specified in + * the request. + */ + data: string; + + /** + * The Unix timestamp (in seconds) for when this audio response will no longer be + * accessible on the server for use in multi-turn conversations. + */ + expires_at: number; + + /** + * Transcript of the audio generated by the model. + */ + transcript: string; +} + +/** + * Parameters for audio output. Required when audio output is requested with + * `modalities: ["audio"]`. + * [Learn more](https://platform.openai.com/docs/guides/audio). + */ +export interface ChatCompletionAudioParam { + /** + * Specifies the output audio format. Must be one of `wav`, `mp3`, `flac`, `opus`, + * or `pcm16`. + */ + format: 'wav' | 'mp3' | 'flac' | 'opus' | 'pcm16'; + + /** + * The voice the model uses to respond. Supported voices are `ash`, `ballad`, + * `coral`, `sage`, and `verse` (also supported but not recommended are `alloy`, + * `echo`, and `shimmer`; these voices are less expressive). + */ + voice: 'alloy' | 'ash' | 'ballad' | 'coral' | 'echo' | 'sage' | 'shimmer' | 'verse'; +} + +/** + * Represents a streamed chunk of a chat completion response returned by model, + * based on the provided input. + */ +export interface ChatCompletionChunk { + /** + * A unique identifier for the chat completion. Each chunk has the same ID. + */ + id: string; + + /** + * A list of chat completion choices. Can contain more than one elements if `n` is + * greater than 1. Can also be empty for the last chunk if you set + * `stream_options: {"include_usage": true}`. + */ + choices: Array; + + /** + * The Unix timestamp (in seconds) of when the chat completion was created. Each + * chunk has the same timestamp. + */ + created: number; + + /** + * The model to generate the completion. + */ + model: string; + + /** + * The object type, which is always `chat.completion.chunk`. + */ + object: 'chat.completion.chunk'; + + /** + * The service tier used for processing the request. + */ + service_tier?: 'scale' | 'default' | null; + + /** + * This fingerprint represents the backend configuration that the model runs with. + * Can be used in conjunction with the `seed` request parameter to understand when + * backend changes have been made that might impact determinism. + */ + system_fingerprint?: string; + + /** + * An optional field that will only be present when you set + * `stream_options: {"include_usage": true}` in your request. When present, it + * contains a null value except for the last chunk which contains the token usage + * statistics for the entire request. + */ + usage?: CompletionsAPI.CompletionUsage | null; +} + +export namespace ChatCompletionChunk { + export interface Choice { + /** + * A chat completion delta generated by streamed model responses. + */ + delta: Choice.Delta; + + /** + * The reason the model stopped generating tokens. This will be `stop` if the model + * hit a natural stop point or a provided stop sequence, `length` if the maximum + * number of tokens specified in the request was reached, `content_filter` if + * content was omitted due to a flag from our content filters, `tool_calls` if the + * model called a tool, or `function_call` (deprecated) if the model called a + * function. + */ + finish_reason: 'stop' | 'length' | 'tool_calls' | 'content_filter' | 'function_call' | null; + + /** + * The index of the choice in the list of choices. + */ + index: number; + + /** + * Log probability information for the choice. + */ + logprobs?: Choice.Logprobs | null; + } + + export namespace Choice { + /** + * A chat completion delta generated by streamed model responses. + */ + export interface Delta { + /** + * The contents of the chunk message. + */ + content?: string | null; + + /** + * @deprecated Deprecated and replaced by `tool_calls`. The name and arguments of a + * function that should be called, as generated by the model. + */ + function_call?: Delta.FunctionCall; + + /** + * The refusal message generated by the model. + */ + refusal?: string | null; + + /** + * The role of the author of this message. + */ + role?: 'developer' | 'system' | 'user' | 'assistant' | 'tool'; + + tool_calls?: Array; + } + + export namespace Delta { + /** + * @deprecated Deprecated and replaced by `tool_calls`. The name and arguments of a + * function that should be called, as generated by the model. + */ + export interface FunctionCall { + /** + * The arguments to call the function with, as generated by the model in JSON + * format. Note that the model does not always generate valid JSON, and may + * hallucinate parameters not defined by your function schema. Validate the + * arguments in your code before calling your function. + */ + arguments?: string; + + /** + * The name of the function to call. + */ + name?: string; + } + + export interface ToolCall { + index: number; + + /** + * The ID of the tool call. + */ + id?: string; + + function?: ToolCall.Function; + + /** + * The type of the tool. Currently, only `function` is supported. + */ + type?: 'function'; + } + + export namespace ToolCall { + export interface Function { + /** + * The arguments to call the function with, as generated by the model in JSON + * format. Note that the model does not always generate valid JSON, and may + * hallucinate parameters not defined by your function schema. Validate the + * arguments in your code before calling your function. + */ + arguments?: string; + + /** + * The name of the function to call. + */ + name?: string; + } + } + } + + /** + * Log probability information for the choice. + */ + export interface Logprobs { + /** + * A list of message content tokens with log probability information. + */ + content: Array | null; + + /** + * A list of message refusal tokens with log probability information. + */ + refusal: Array | null; + } + } +} + +/** + * Learn about + * [text inputs](https://platform.openai.com/docs/guides/text-generation). + */ +export type ChatCompletionContentPart = + | ChatCompletionContentPartText + | ChatCompletionContentPartImage + | ChatCompletionContentPartInputAudio; + +/** + * Learn about [image inputs](https://platform.openai.com/docs/guides/vision). + */ +export interface ChatCompletionContentPartImage { + image_url: ChatCompletionContentPartImage.ImageURL; + + /** + * The type of the content part. + */ + type: 'image_url'; +} + +export namespace ChatCompletionContentPartImage { + export interface ImageURL { + /** + * Either a URL of the image or the base64 encoded image data. + */ + url: string; + + /** + * Specifies the detail level of the image. Learn more in the + * [Vision guide](https://platform.openai.com/docs/guides/vision#low-or-high-fidelity-image-understanding). + */ + detail?: 'auto' | 'low' | 'high'; + } +} + +/** + * Learn about [audio inputs](https://platform.openai.com/docs/guides/audio). + */ +export interface ChatCompletionContentPartInputAudio { + input_audio: ChatCompletionContentPartInputAudio.InputAudio; + + /** + * The type of the content part. Always `input_audio`. + */ + type: 'input_audio'; +} + +export namespace ChatCompletionContentPartInputAudio { + export interface InputAudio { + /** + * Base64 encoded audio data. + */ + data: string; + + /** + * The format of the encoded audio data. Currently supports "wav" and "mp3". + */ + format: 'wav' | 'mp3'; + } +} + +export interface ChatCompletionContentPartRefusal { + /** + * The refusal message generated by the model. + */ + refusal: string; + + /** + * The type of the content part. + */ + type: 'refusal'; +} + +/** + * Learn about + * [text inputs](https://platform.openai.com/docs/guides/text-generation). + */ +export interface ChatCompletionContentPartText { + /** + * The text content. + */ + text: string; + + /** + * The type of the content part. + */ + type: 'text'; +} + +export interface ChatCompletionDeleted { + /** + * The ID of the chat completion that was deleted. + */ + id: string; + + /** + * Whether the chat completion was deleted. + */ + deleted: boolean; + + /** + * The type of object being deleted. + */ + object: 'chat.completion.deleted'; +} + +/** + * Developer-provided instructions that the model should follow, regardless of + * messages sent by the user. With o1 models and newer, `developer` messages + * replace the previous `system` messages. + */ +export interface ChatCompletionDeveloperMessageParam { + /** + * The contents of the developer message. + */ + content: string | Array; + + /** + * The role of the messages author, in this case `developer`. + */ + role: 'developer'; + + /** + * An optional name for the participant. Provides the model information to + * differentiate between participants of the same role. + */ + name?: string; +} + +/** + * Specifying a particular function via `{"name": "my_function"}` forces the model + * to call that function. + */ +export interface ChatCompletionFunctionCallOption { + /** + * The name of the function to call. + */ + name: string; +} + +/** + * @deprecated + */ +export interface ChatCompletionFunctionMessageParam { + /** + * The contents of the function message. + */ + content: string | null; + + /** + * The name of the function to call. + */ + name: string; + + /** + * The role of the messages author, in this case `function`. + */ + role: 'function'; +} + +/** + * A chat completion message generated by the model. + */ +export interface ChatCompletionMessage { + /** + * The contents of the message. + */ + content: string | null; + + /** + * The refusal message generated by the model. + */ + refusal: string | null; + + /** + * The role of the author of this message. + */ + role: 'assistant'; + + /** + * If the audio output modality is requested, this object contains data about the + * audio response from the model. + * [Learn more](https://platform.openai.com/docs/guides/audio). + */ + audio?: ChatCompletionAudio | null; + + /** + * @deprecated Deprecated and replaced by `tool_calls`. The name and arguments of a + * function that should be called, as generated by the model. + */ + function_call?: ChatCompletionMessage.FunctionCall | null; + + /** + * The tool calls generated by the model, such as function calls. + */ + tool_calls?: Array; +} + +export namespace ChatCompletionMessage { + /** + * @deprecated Deprecated and replaced by `tool_calls`. The name and arguments of a + * function that should be called, as generated by the model. + */ + export interface FunctionCall { + /** + * The arguments to call the function with, as generated by the model in JSON + * format. Note that the model does not always generate valid JSON, and may + * hallucinate parameters not defined by your function schema. Validate the + * arguments in your code before calling your function. + */ + arguments: string; + + /** + * The name of the function to call. + */ + name: string; + } +} + +/** + * Developer-provided instructions that the model should follow, regardless of + * messages sent by the user. With o1 models and newer, `developer` messages + * replace the previous `system` messages. + */ +export type ChatCompletionMessageParam = + | ChatCompletionDeveloperMessageParam + | ChatCompletionSystemMessageParam + | ChatCompletionUserMessageParam + | ChatCompletionAssistantMessageParam + | ChatCompletionToolMessageParam + | ChatCompletionFunctionMessageParam; + +export interface ChatCompletionMessageToolCall { + /** + * The ID of the tool call. + */ + id: string; + + /** + * The function that the model called. + */ + function: ChatCompletionMessageToolCall.Function; + + /** + * The type of the tool. Currently, only `function` is supported. + */ + type: 'function'; +} + +export namespace ChatCompletionMessageToolCall { + /** + * The function that the model called. + */ + export interface Function { + /** + * The arguments to call the function with, as generated by the model in JSON + * format. Note that the model does not always generate valid JSON, and may + * hallucinate parameters not defined by your function schema. Validate the + * arguments in your code before calling your function. + */ + arguments: string; + + /** + * The name of the function to call. + */ + name: string; + } +} + +export type ChatCompletionModality = 'text' | 'audio'; + +/** + * Specifies a tool the model should use. Use to force the model to call a specific + * function. + */ +export interface ChatCompletionNamedToolChoice { + function: ChatCompletionNamedToolChoice.Function; + + /** + * The type of the tool. Currently, only `function` is supported. + */ + type: 'function'; +} + +export namespace ChatCompletionNamedToolChoice { + export interface Function { + /** + * The name of the function to call. + */ + name: string; + } +} + +/** + * Static predicted output content, such as the content of a text file that is + * being regenerated. + */ +export interface ChatCompletionPredictionContent { + /** + * The content that should be matched when generating a model response. If + * generated tokens would match this content, the entire model response can be + * returned much more quickly. + */ + content: string | Array; + + /** + * The type of the predicted content you want to provide. This type is currently + * always `content`. + */ + type: 'content'; +} + +/** + * **o1 and o3-mini models only** + * + * Constrains effort on reasoning for + * [reasoning models](https://platform.openai.com/docs/guides/reasoning). Currently + * supported values are `low`, `medium`, and `high`. Reducing reasoning effort can + * result in faster responses and fewer tokens used on reasoning in a response. + */ +export type ChatCompletionReasoningEffort = 'low' | 'medium' | 'high' | null; + +/** + * The role of the author of a message + */ +export type ChatCompletionRole = 'developer' | 'system' | 'user' | 'assistant' | 'tool' | 'function'; + +/** + * A chat completion message generated by the model. + */ +export interface ChatCompletionStoreMessage extends ChatCompletionMessage { + /** + * The identifier of the chat message. + */ + id: string; +} + +/** + * Options for streaming response. Only set this when you set `stream: true`. + */ +export interface ChatCompletionStreamOptions { + /** + * If set, an additional chunk will be streamed before the `data: [DONE]` message. + * The `usage` field on this chunk shows the token usage statistics for the entire + * request, and the `choices` field will always be an empty array. All other chunks + * will also include a `usage` field, but with a null value. + */ + include_usage?: boolean; +} + +/** + * Developer-provided instructions that the model should follow, regardless of + * messages sent by the user. With o1 models and newer, use `developer` messages + * for this purpose instead. + */ +export interface ChatCompletionSystemMessageParam { + /** + * The contents of the system message. + */ + content: string | Array; + + /** + * The role of the messages author, in this case `system`. + */ + role: 'system'; + + /** + * An optional name for the participant. Provides the model information to + * differentiate between participants of the same role. + */ + name?: string; +} + +export interface ChatCompletionTokenLogprob { + /** + * The token. + */ + token: string; + + /** + * A list of integers representing the UTF-8 bytes representation of the token. + * Useful in instances where characters are represented by multiple tokens and + * their byte representations must be combined to generate the correct text + * representation. Can be `null` if there is no bytes representation for the token. + */ + bytes: Array | null; + + /** + * The log probability of this token, if it is within the top 20 most likely + * tokens. Otherwise, the value `-9999.0` is used to signify that the token is very + * unlikely. + */ + logprob: number; + + /** + * List of the most likely tokens and their log probability, at this token + * position. In rare cases, there may be fewer than the number of requested + * `top_logprobs` returned. + */ + top_logprobs: Array; +} + +export namespace ChatCompletionTokenLogprob { + export interface TopLogprob { + /** + * The token. + */ + token: string; + + /** + * A list of integers representing the UTF-8 bytes representation of the token. + * Useful in instances where characters are represented by multiple tokens and + * their byte representations must be combined to generate the correct text + * representation. Can be `null` if there is no bytes representation for the token. + */ + bytes: Array | null; + + /** + * The log probability of this token, if it is within the top 20 most likely + * tokens. Otherwise, the value `-9999.0` is used to signify that the token is very + * unlikely. + */ + logprob: number; + } +} + +export interface ChatCompletionTool { + function: Shared.FunctionDefinition; + + /** + * The type of the tool. Currently, only `function` is supported. + */ + type: 'function'; +} + +/** + * Controls which (if any) tool is called by the model. `none` means the model will + * not call any tool and instead generates a message. `auto` means the model can + * pick between generating a message or calling one or more tools. `required` means + * the model must call one or more tools. Specifying a particular tool via + * `{"type": "function", "function": {"name": "my_function"}}` forces the model to + * call that tool. + * + * `none` is the default when no tools are present. `auto` is the default if tools + * are present. + */ +export type ChatCompletionToolChoiceOption = 'none' | 'auto' | 'required' | ChatCompletionNamedToolChoice; + +export interface ChatCompletionToolMessageParam { + /** + * The contents of the tool message. + */ + content: string | Array; + + /** + * The role of the messages author, in this case `tool`. + */ + role: 'tool'; + + /** + * Tool call that this message is responding to. + */ + tool_call_id: string; +} + +/** + * Messages sent by an end user, containing prompts or additional context + * information. + */ +export interface ChatCompletionUserMessageParam { + /** + * The contents of the user message. + */ + content: string | Array; + + /** + * The role of the messages author, in this case `user`. + */ + role: 'user'; + + /** + * An optional name for the participant. Provides the model information to + * differentiate between participants of the same role. + */ + name?: string; +} + +export type ChatCompletionCreateParams = + | ChatCompletionCreateParamsNonStreaming + | ChatCompletionCreateParamsStreaming; + +export interface ChatCompletionCreateParamsBase { + /** + * A list of messages comprising the conversation so far. Depending on the + * [model](https://platform.openai.com/docs/models) you use, different message + * types (modalities) are supported, like + * [text](https://platform.openai.com/docs/guides/text-generation), + * [images](https://platform.openai.com/docs/guides/vision), and + * [audio](https://platform.openai.com/docs/guides/audio). + */ + messages: Array; + + /** + * ID of the model to use. See the + * [model endpoint compatibility](https://platform.openai.com/docs/models#model-endpoint-compatibility) + * table for details on which models work with the Chat API. + */ + model: (string & {}) | ChatAPI.ChatModel; + + /** + * Parameters for audio output. Required when audio output is requested with + * `modalities: ["audio"]`. + * [Learn more](https://platform.openai.com/docs/guides/audio). + */ + audio?: ChatCompletionAudioParam | 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. + */ + frequency_penalty?: number | null; + + /** + * Deprecated in favor of `tool_choice`. + * + * Controls which (if any) function is called by the model. + * + * `none` means the model will not call a function and instead generates a message. + * + * `auto` means the model can pick between generating a message or calling a + * function. + * + * Specifying a particular function via `{"name": "my_function"}` forces the model + * to call that function. + * + * `none` is the default when no functions are present. `auto` is the default if + * functions are present. + */ + function_call?: 'none' | 'auto' | ChatCompletionFunctionCallOption; + + /** + * Deprecated in favor of `tools`. + * + * A list of functions the model may generate JSON inputs for. + */ + functions?: Array; + + /** + * Modify the likelihood of specified tokens appearing in the completion. + * + * Accepts a JSON object that maps tokens (specified by their token ID in the + * tokenizer) to an associated bias value from -100 to 100. Mathematically, the + * bias is added to the logits generated by the model prior to sampling. The exact + * effect will vary per model, but values between -1 and 1 should decrease or + * increase likelihood of selection; values like -100 or 100 should result in a ban + * or exclusive selection of the relevant token. + */ + logit_bias?: Record | null; + + /** + * Whether to return log probabilities of the output tokens or not. If true, + * returns the log probabilities of each output token returned in the `content` of + * `message`. + */ + logprobs?: boolean | null; + + /** + * An upper bound for the number of tokens that can be generated for a completion, + * including visible output tokens and + * [reasoning tokens](https://platform.openai.com/docs/guides/reasoning). + */ + max_completion_tokens?: number | null; + + /** + * The maximum number of [tokens](/tokenizer) that can be generated in the chat + * completion. This value can be used to control + * [costs](https://openai.com/api/pricing/) for text generated via API. + * + * This value is now deprecated in favor of `max_completion_tokens`, and is not + * compatible with + * [o1 series models](https://platform.openai.com/docs/guides/reasoning). + */ + max_tokens?: number | null; + + /** + * Set of 16 key-value pairs that can be attached to an object. This can be useful + * for storing additional information about the object in a structured format, and + * querying for objects via API or the dashboard. + * + * Keys are strings with a maximum length of 64 characters. Values are strings with + * a maximum length of 512 characters. + */ + metadata?: Shared.Metadata | null; + + /** + * Output types that you would like the model to generate for this request. Most + * models are capable of generating text, which is the default: + * + * `["text"]` + * + * The `gpt-4o-audio-preview` model can also be used to + * [generate audio](https://platform.openai.com/docs/guides/audio). To request that + * this model generate both text and audio responses, you can use: + * + * `["text", "audio"]` + */ + modalities?: Array | null; + + /** + * How many chat completion choices to generate for each input message. Note that + * you will be charged based on the number of generated tokens across all of the + * choices. Keep `n` as `1` to minimize costs. + */ + n?: number | null; + + /** + * Whether to enable + * [parallel function calling](https://platform.openai.com/docs/guides/function-calling#configuring-parallel-function-calling) + * during tool use. + */ + parallel_tool_calls?: boolean; + + /** + * Static predicted output content, such as the content of a text file that is + * being regenerated. + */ + prediction?: ChatCompletionPredictionContent | null; + + /** + * Number between -2.0 and 2.0. Positive values penalize new tokens based on + * whether they appear in the text so far, increasing the model's likelihood to + * talk about new topics. + */ + presence_penalty?: number | null; + + /** + * **o1 and o3-mini models only** + * + * Constrains effort on reasoning for + * [reasoning models](https://platform.openai.com/docs/guides/reasoning). Currently + * supported values are `low`, `medium`, and `high`. Reducing reasoning effort can + * result in faster responses and fewer tokens used on reasoning in a response. + */ + reasoning_effort?: ChatCompletionReasoningEffort | null; + + /** + * An object specifying the format that the model must output. + * + * Setting to `{ "type": "json_schema", "json_schema": {...} }` enables Structured + * Outputs which ensures the model will match your supplied JSON schema. Learn more + * in the + * [Structured Outputs guide](https://platform.openai.com/docs/guides/structured-outputs). + * + * Setting to `{ "type": "json_object" }` enables JSON mode, which ensures the + * message the model generates is valid JSON. + * + * **Important:** when using JSON mode, you **must** also instruct the model to + * produce JSON yourself via a system or user message. Without this, the model may + * generate an unending stream of whitespace until the generation reaches the token + * limit, resulting in a long-running and seemingly "stuck" request. Also note that + * the message content may be partially cut off if `finish_reason="length"`, which + * indicates the generation exceeded `max_tokens` or the conversation exceeded the + * max context length. + */ + response_format?: + | Shared.ResponseFormatText + | Shared.ResponseFormatJSONObject + | Shared.ResponseFormatJSONSchema; + + /** + * This feature is in Beta. If specified, our system will make a best effort to + * sample deterministically, such that repeated requests with the same `seed` and + * parameters should return the same result. Determinism is not guaranteed, and you + * should refer to the `system_fingerprint` response parameter to monitor changes + * in the backend. + */ + seed?: number | null; + + /** + * Specifies the latency tier to use for processing the request. This parameter is + * relevant for customers subscribed to the scale tier service: + * + * - If set to 'auto', and the Project is Scale tier enabled, the system will + * utilize scale tier credits until they are exhausted. + * - If set to 'auto', and the Project is not Scale tier enabled, the request will + * be processed using the default service tier with a lower uptime SLA and no + * latency guarantee. + * - If set to 'default', the request will be processed using the default service + * tier with a lower uptime SLA and no latency guarantee. + * - When not set, the default behavior is 'auto'. + */ + service_tier?: 'auto' | 'default' | null; + + /** + * Up to 4 sequences where the API will stop generating further tokens. + */ + stop?: string | null | Array; + + /** + * Whether or not to store the output of this chat completion request for use in + * our [model distillation](https://platform.openai.com/docs/guides/distillation) + * or [evals](https://platform.openai.com/docs/guides/evals) products. + */ + store?: boolean | null; + + /** + * If set, partial message deltas will be sent, like in ChatGPT. Tokens will be + * sent as data-only + * [server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format) + * as they become available, with the stream terminated by a `data: [DONE]` + * message. + * [Example Python code](https://cookbook.openai.com/examples/how_to_stream_completions). + */ + stream?: boolean | null; + + /** + * Options for streaming response. Only set this when you set `stream: true`. + */ + stream_options?: ChatCompletionStreamOptions | 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 generally recommend altering this or `top_p` but + * not both. + */ + temperature?: number | null; + + /** + * Controls which (if any) tool is called by the model. `none` means the model will + * not call any tool and instead generates a message. `auto` means the model can + * pick between generating a message or calling one or more tools. `required` means + * the model must call one or more tools. Specifying a particular tool via + * `{"type": "function", "function": {"name": "my_function"}}` forces the model to + * call that tool. + * + * `none` is the default when no tools are present. `auto` is the default if tools + * are present. + */ + tool_choice?: ChatCompletionToolChoiceOption; + + /** + * A list of tools the model may call. Currently, only functions are supported as a + * tool. Use this to provide a list of functions the model may generate JSON inputs + * for. A max of 128 functions are supported. + */ + tools?: Array; + + /** + * An integer between 0 and 20 specifying the number of most likely tokens to + * return at each token position, each with an associated log probability. + * `logprobs` must be set to `true` if this parameter is used. + */ + top_logprobs?: number | null; + + /** + * An alternative to sampling with temperature, called nucleus sampling, where the + * model considers the results of the tokens with top_p probability mass. So 0.1 + * means only the tokens comprising the top 10% probability mass are considered. + * + * We generally recommend altering this or `temperature` but not both. + */ + top_p?: number | null; + + /** + * A unique identifier representing your end-user, which can help OpenAI to monitor + * and detect abuse. + * [Learn more](https://platform.openai.com/docs/guides/safety-best-practices#end-user-ids). + */ + user?: string; +} + +export namespace ChatCompletionCreateParams { + /** + * @deprecated + */ + export interface Function { + /** + * The name of the function to be called. Must be a-z, A-Z, 0-9, or contain + * underscores and dashes, with a maximum length of 64. + */ + name: string; + + /** + * A description of what the function does, used by the model to choose when and + * how to call the function. + */ + description?: string; + + /** + * The parameters the functions accepts, described as a JSON Schema object. See the + * [guide](https://platform.openai.com/docs/guides/function-calling) for examples, + * and the + * [JSON Schema reference](https://json-schema.org/understanding-json-schema/) for + * documentation about the format. + * + * Omitting `parameters` defines a function with an empty parameter list. + */ + parameters?: Shared.FunctionParameters; + } + + export type ChatCompletionCreateParamsNonStreaming = + CompletionsCompletionsAPI.ChatCompletionCreateParamsNonStreaming; + export type ChatCompletionCreateParamsStreaming = + CompletionsCompletionsAPI.ChatCompletionCreateParamsStreaming; +} + +export interface ChatCompletionCreateParamsNonStreaming extends ChatCompletionCreateParamsBase { + /** + * If set, partial message deltas will be sent, like in ChatGPT. Tokens will be + * sent as data-only + * [server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format) + * as they become available, with the stream terminated by a `data: [DONE]` + * message. + * [Example Python code](https://cookbook.openai.com/examples/how_to_stream_completions). + */ + stream?: false | null; +} + +export interface ChatCompletionCreateParamsStreaming extends ChatCompletionCreateParamsBase { + /** + * If set, partial message deltas will be sent, like in ChatGPT. Tokens will be + * sent as data-only + * [server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format) + * as they become available, with the stream terminated by a `data: [DONE]` + * message. + * [Example Python code](https://cookbook.openai.com/examples/how_to_stream_completions). + */ + stream: true; +} + +export interface ChatCompletionUpdateParams { + /** + * Set of 16 key-value pairs that can be attached to an object. This can be useful + * for storing additional information about the object in a structured format, and + * querying for objects via API or the dashboard. + * + * Keys are strings with a maximum length of 64 characters. Values are strings with + * a maximum length of 512 characters. + */ + metadata: Shared.Metadata | null; +} + +export interface ChatCompletionListParams extends CursorPageParams { + /** + * A list of metadata keys to filter the chat completions by. Example: + * + * `metadata[key1]=value1&metadata[key2]=value2` + */ + metadata?: Shared.Metadata | null; + + /** + * The model used to generate the chat completions. + */ + model?: string; + + /** + * Sort order for chat completions by timestamp. Use `asc` for ascending order or + * `desc` for descending order. Defaults to `asc`. + */ + order?: 'asc' | 'desc'; +} + +Completions.Messages = Messages; + +export declare namespace Completions { + export { + type ChatCompletion as ChatCompletion, + type ChatCompletionAssistantMessageParam as ChatCompletionAssistantMessageParam, + type ChatCompletionAudio as ChatCompletionAudio, + type ChatCompletionAudioParam as ChatCompletionAudioParam, + type ChatCompletionChunk as ChatCompletionChunk, + type ChatCompletionContentPart as ChatCompletionContentPart, + type ChatCompletionContentPartImage as ChatCompletionContentPartImage, + type ChatCompletionContentPartInputAudio as ChatCompletionContentPartInputAudio, + type ChatCompletionContentPartRefusal as ChatCompletionContentPartRefusal, + type ChatCompletionContentPartText as ChatCompletionContentPartText, + type ChatCompletionDeleted as ChatCompletionDeleted, + type ChatCompletionDeveloperMessageParam as ChatCompletionDeveloperMessageParam, + type ChatCompletionFunctionCallOption as ChatCompletionFunctionCallOption, + type ChatCompletionFunctionMessageParam as ChatCompletionFunctionMessageParam, + type ChatCompletionMessage as ChatCompletionMessage, + type ChatCompletionMessageParam as ChatCompletionMessageParam, + type ChatCompletionMessageToolCall as ChatCompletionMessageToolCall, + type ChatCompletionModality as ChatCompletionModality, + type ChatCompletionNamedToolChoice as ChatCompletionNamedToolChoice, + type ChatCompletionPredictionContent as ChatCompletionPredictionContent, + type ChatCompletionReasoningEffort as ChatCompletionReasoningEffort, + type ChatCompletionRole as ChatCompletionRole, + type ChatCompletionStoreMessage as ChatCompletionStoreMessage, + type ChatCompletionStreamOptions as ChatCompletionStreamOptions, + type ChatCompletionSystemMessageParam as ChatCompletionSystemMessageParam, + type ChatCompletionTokenLogprob as ChatCompletionTokenLogprob, + type ChatCompletionTool as ChatCompletionTool, + type ChatCompletionToolChoiceOption as ChatCompletionToolChoiceOption, + type ChatCompletionToolMessageParam as ChatCompletionToolMessageParam, + type ChatCompletionUserMessageParam as ChatCompletionUserMessageParam, + type ChatCompletionsPage as ChatCompletionsPage, + type ChatCompletionCreateParams as ChatCompletionCreateParams, + type ChatCompletionCreateParamsNonStreaming as ChatCompletionCreateParamsNonStreaming, + type ChatCompletionCreateParamsStreaming as ChatCompletionCreateParamsStreaming, + type ChatCompletionUpdateParams as ChatCompletionUpdateParams, + type ChatCompletionListParams as ChatCompletionListParams, + }; + + export { Messages as Messages, type MessageListParams as MessageListParams }; +} diff --git a/src/resources/chat/completions/index.ts b/src/resources/chat/completions/index.ts new file mode 100644 index 000000000..6a3fdec83 --- /dev/null +++ b/src/resources/chat/completions/index.ts @@ -0,0 +1,43 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +export { + Completions, + type ChatCompletion, + type ChatCompletionAssistantMessageParam, + type ChatCompletionAudio, + type ChatCompletionAudioParam, + type ChatCompletionChunk, + type ChatCompletionContentPart, + type ChatCompletionContentPartImage, + type ChatCompletionContentPartInputAudio, + type ChatCompletionContentPartRefusal, + type ChatCompletionContentPartText, + type ChatCompletionDeleted, + type ChatCompletionDeveloperMessageParam, + type ChatCompletionFunctionCallOption, + type ChatCompletionFunctionMessageParam, + type ChatCompletionMessage, + type ChatCompletionMessageParam, + type ChatCompletionMessageToolCall, + type ChatCompletionModality, + type ChatCompletionNamedToolChoice, + type ChatCompletionPredictionContent, + type ChatCompletionReasoningEffort, + type ChatCompletionRole, + type ChatCompletionStoreMessage, + type ChatCompletionStreamOptions, + type ChatCompletionSystemMessageParam, + type ChatCompletionTokenLogprob, + type ChatCompletionTool, + type ChatCompletionToolChoiceOption, + type ChatCompletionToolMessageParam, + type ChatCompletionUserMessageParam, + type ChatCompletionCreateParams, + type ChatCompletionCreateParamsNonStreaming, + type ChatCompletionCreateParamsStreaming, + type ChatCompletionUpdateParams, + type ChatCompletionListParams, + type ChatCompletionStoreMessagesPage, + type ChatCompletionsPage, +} from './completions'; +export { Messages, type MessageListParams } from './messages'; diff --git a/src/resources/chat/completions/messages.ts b/src/resources/chat/completions/messages.ts new file mode 100644 index 000000000..f00acbdfc --- /dev/null +++ b/src/resources/chat/completions/messages.ts @@ -0,0 +1,40 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { APIResource } from '../../../resource'; +import * as CompletionsAPI from './completions'; +import { ChatCompletionStoreMessagesPage } from './completions'; +import { CursorPage, type CursorPageParams, PagePromise } from '../../../pagination'; +import { RequestOptions } from '../../../internal/request-options'; +import { path } from '../../../internal/utils/path'; + +export class Messages extends APIResource { + /** + * Get the messages in a stored chat completion. Only chat completions that have + * been created with the `store` parameter set to `true` will be returned. + */ + list( + completionID: string, + query: MessageListParams | null | undefined = {}, + options?: RequestOptions, + ): PagePromise { + return this._client.getAPIList( + path`/chat/completions/${completionID}/messages`, + CursorPage, + { query, ...options }, + ); + } +} + +export interface MessageListParams extends CursorPageParams { + /** + * Sort order for messages by timestamp. Use `asc` for ascending order or `desc` + * for descending order. Defaults to `asc`. + */ + order?: 'asc' | 'desc'; +} + +export declare namespace Messages { + export { type MessageListParams as MessageListParams }; +} + +export { type ChatCompletionStoreMessagesPage }; diff --git a/src/resources/chat/index.ts b/src/resources/chat/index.ts index ea7371d9f..f098e5ce7 100644 --- a/src/resources/chat/index.ts +++ b/src/resources/chat/index.ts @@ -13,6 +13,7 @@ export { type ChatCompletionContentPartInputAudio, type ChatCompletionContentPartRefusal, type ChatCompletionContentPartText, + type ChatCompletionDeleted, type ChatCompletionDeveloperMessageParam, type ChatCompletionFunctionCallOption, type ChatCompletionFunctionMessageParam, @@ -24,6 +25,7 @@ export { type ChatCompletionPredictionContent, type ChatCompletionReasoningEffort, type ChatCompletionRole, + type ChatCompletionStoreMessage, type ChatCompletionStreamOptions, type ChatCompletionSystemMessageParam, type ChatCompletionTokenLogprob, @@ -32,9 +34,10 @@ export { type ChatCompletionToolMessageParam, type ChatCompletionUserMessageParam, type ChatCompletionCreateParams, - type CompletionCreateParams, type ChatCompletionCreateParamsNonStreaming, - type CompletionCreateParamsNonStreaming, type ChatCompletionCreateParamsStreaming, - type CompletionCreateParamsStreaming, -} from './completions'; + type ChatCompletionUpdateParams, + type ChatCompletionListParams, + type ChatCompletionStoreMessagesPage, + type ChatCompletionsPage, +} from './completions/index'; diff --git a/src/resources/completions.ts b/src/resources/completions.ts index be9cc6051..742f682fe 100644 --- a/src/resources/completions.ts +++ b/src/resources/completions.ts @@ -2,7 +2,7 @@ import { APIResource } from '../resource'; import * as CompletionsAPI from './completions'; -import * as ChatCompletionsAPI from './chat/completions'; +import * as CompletionsCompletionsAPI from './chat/completions/completions'; import { APIPromise } from '../api-promise'; import { Stream } from '../streaming'; import { RequestOptions } from '../internal/request-options'; @@ -308,7 +308,7 @@ export interface CompletionCreateParamsBase { /** * Options for streaming response. Only set this when you set `stream: true`. */ - stream_options?: ChatCompletionsAPI.ChatCompletionStreamOptions | null; + stream_options?: CompletionsCompletionsAPI.ChatCompletionStreamOptions | null; /** * The suffix that comes after a completion of inserted text. diff --git a/src/resources/embeddings.ts b/src/resources/embeddings.ts index 65d328725..dcd542a1a 100644 --- a/src/resources/embeddings.ts +++ b/src/resources/embeddings.ts @@ -84,7 +84,8 @@ export interface EmbeddingCreateParams { * `text-embedding-ada-002`), cannot be an empty string, and any array must be 2048 * dimensions or less. * [Example Python code](https://cookbook.openai.com/examples/how_to_count_tokens_with_tiktoken) - * for counting tokens. + * for counting tokens. Some models may also impose a limit on total number of + * tokens summed across inputs. */ input: string | Array | Array | Array>; diff --git a/src/resources/files.ts b/src/resources/files.ts index 39ad3f4c9..56bafc224 100644 --- a/src/resources/files.ts +++ b/src/resources/files.ts @@ -3,10 +3,13 @@ import { APIResource } from '../resource'; import { APIPromise } from '../api-promise'; import { CursorPage, type CursorPageParams, PagePromise } from '../pagination'; -import { type Uploadable, multipartFormRequestOptions } from '../uploads'; +import { type Uploadable } from '../uploads'; +import { buildHeaders } from '../internal/headers'; import { RequestOptions } from '../internal/request-options'; import { sleep } from '../internal/utils/sleep'; import { APIConnectionTimeoutError } from '../error'; +import { multipartFormRequestOptions } from '../internal/uploads'; +import { path } from '../internal/utils/path'; export class Files extends APIResource { /** @@ -33,14 +36,14 @@ export class Files extends APIResource { * storage limits. */ create(body: FileCreateParams, options?: RequestOptions): APIPromise { - return this._client.post('/files', multipartFormRequestOptions({ body, ...options })); + return this._client.post('/files', multipartFormRequestOptions({ body, ...options }, this._client)); } /** * Returns information about a specific file. */ retrieve(fileID: string, options?: RequestOptions): APIPromise { - return this._client.get(`/files/${fileID}`, options); + return this._client.get(path`/files/${fileID}`, options); } /** @@ -57,25 +60,17 @@ export class Files extends APIResource { * Delete a file. */ delete(fileID: string, options?: RequestOptions): APIPromise { - return this._client.delete(`/files/${fileID}`, options); + return this._client.delete(path`/files/${fileID}`, options); } /** * Returns the contents of the specified file. */ content(fileID: string, options?: RequestOptions): APIPromise { - return this._client.get(`/files/${fileID}/content`, { ...options, __binaryResponse: true }); - } - - /** - * Returns the contents of the specified file. - * - * @deprecated The `.content()` method should be used instead - */ - retrieveContent(fileID: string, options?: RequestOptions): APIPromise { - return this._client.get(`/files/${fileID}/content`, { + return this._client.get(path`/files/${fileID}/content`, { ...options, - headers: { Accept: 'application/json', ...options?.headers }, + headers: buildHeaders([{ Accept: 'application/binary' }, options?.headers]), + __binaryResponse: true, }); } @@ -162,13 +157,18 @@ export interface FileObject { | 'vision'; /** - * @deprecated: Deprecated. The current status of the file, which can be either + * @deprecated Deprecated. The current status of the file, which can be either * `uploaded`, `processed`, or `error`. */ status: 'uploaded' | 'processed' | 'error'; /** - * @deprecated: Deprecated. For details on why a fine-tuning training file failed + * The Unix timestamp (in seconds) for when the file will expire. + */ + expires_at?: number; + + /** + * @deprecated Deprecated. For details on why a fine-tuning training file failed * validation, see the `error` field on `fine_tuning.job`. */ status_details?: string; diff --git a/src/resources/fine-tuning/jobs/checkpoints.ts b/src/resources/fine-tuning/jobs/checkpoints.ts index 2b7ea0e95..134715cec 100644 --- a/src/resources/fine-tuning/jobs/checkpoints.ts +++ b/src/resources/fine-tuning/jobs/checkpoints.ts @@ -3,6 +3,7 @@ import { APIResource } from '../../../resource'; import { CursorPage, type CursorPageParams, PagePromise } from '../../../pagination'; import { RequestOptions } from '../../../internal/request-options'; +import { path } from '../../../internal/utils/path'; export class Checkpoints extends APIResource { /** @@ -14,7 +15,7 @@ export class Checkpoints extends APIResource { options?: RequestOptions, ): PagePromise { return this._client.getAPIList( - `/fine_tuning/jobs/${fineTuningJobID}/checkpoints`, + path`/fine_tuning/jobs/${fineTuningJobID}/checkpoints`, CursorPage, { query, ...options }, ); diff --git a/src/resources/fine-tuning/jobs/jobs.ts b/src/resources/fine-tuning/jobs/jobs.ts index 3c842de34..51cac957c 100644 --- a/src/resources/fine-tuning/jobs/jobs.ts +++ b/src/resources/fine-tuning/jobs/jobs.ts @@ -11,6 +11,7 @@ import { import { APIPromise } from '../../../api-promise'; import { CursorPage, type CursorPageParams, PagePromise } from '../../../pagination'; import { RequestOptions } from '../../../internal/request-options'; +import { path } from '../../../internal/utils/path'; export class Jobs extends APIResource { checkpoints: CheckpointsAPI.Checkpoints = new CheckpointsAPI.Checkpoints(this._client); @@ -34,7 +35,7 @@ export class Jobs extends APIResource { * [Learn more about fine-tuning](https://platform.openai.com/docs/guides/fine-tuning) */ retrieve(fineTuningJobID: string, options?: RequestOptions): APIPromise { - return this._client.get(`/fine_tuning/jobs/${fineTuningJobID}`, options); + return this._client.get(path`/fine_tuning/jobs/${fineTuningJobID}`, options); } /** @@ -51,7 +52,7 @@ export class Jobs extends APIResource { * Immediately cancel a fine-tune job. */ cancel(fineTuningJobID: string, options?: RequestOptions): APIPromise { - return this._client.post(`/fine_tuning/jobs/${fineTuningJobID}/cancel`, options); + return this._client.post(path`/fine_tuning/jobs/${fineTuningJobID}/cancel`, options); } /** @@ -63,7 +64,7 @@ export class Jobs extends APIResource { options?: RequestOptions, ): PagePromise { return this._client.getAPIList( - `/fine_tuning/jobs/${fineTuningJobID}/events`, + path`/fine_tuning/jobs/${fineTuningJobID}/events`, CursorPage, { query, ...options }, ); @@ -497,7 +498,7 @@ export interface JobCreateParams { export namespace JobCreateParams { /** - * @deprecated: The hyperparameters used for the fine-tuning job. This value is now + * @deprecated The hyperparameters used for the fine-tuning job. This value is now * deprecated in favor of `method`, and should be passed in under the `method` * parameter. */ diff --git a/src/resources/images.ts b/src/resources/images.ts index ca3578d24..d8eb5be4c 100644 --- a/src/resources/images.ts +++ b/src/resources/images.ts @@ -2,22 +2,29 @@ import { APIResource } from '../resource'; import { APIPromise } from '../api-promise'; -import { type Uploadable, multipartFormRequestOptions } from '../uploads'; +import { type Uploadable } from '../uploads'; import { RequestOptions } from '../internal/request-options'; +import { multipartFormRequestOptions } from '../internal/uploads'; export class Images extends APIResource { /** * Creates a variation of a given image. */ createVariation(body: ImageCreateVariationParams, options?: RequestOptions): APIPromise { - return this._client.post('/images/variations', multipartFormRequestOptions({ body, ...options })); + return this._client.post( + '/images/variations', + multipartFormRequestOptions({ body, ...options }, this._client), + ); } /** * Creates an edited or extended image given an original image and a prompt. */ edit(body: ImageEditParams, options?: RequestOptions): APIPromise { - return this._client.post('/images/edits', multipartFormRequestOptions({ body, ...options })); + return this._client.post( + '/images/edits', + multipartFormRequestOptions({ body, ...options }, this._client), + ); } /** diff --git a/src/resources/models.ts b/src/resources/models.ts index f6ec034ec..69ba58279 100644 --- a/src/resources/models.ts +++ b/src/resources/models.ts @@ -4,6 +4,7 @@ import { APIResource } from '../resource'; import { APIPromise } from '../api-promise'; import { Page, PagePromise } from '../pagination'; import { RequestOptions } from '../internal/request-options'; +import { path } from '../internal/utils/path'; export class Models extends APIResource { /** @@ -11,7 +12,7 @@ export class Models extends APIResource { * the owner and permissioning. */ retrieve(model: string, options?: RequestOptions): APIPromise { - return this._client.get(`/models/${model}`, options); + return this._client.get(path`/models/${model}`, options); } /** @@ -27,7 +28,7 @@ export class Models extends APIResource { * delete a model. */ delete(model: string, options?: RequestOptions): APIPromise { - return this._client.delete(`/models/${model}`, options); + return this._client.delete(path`/models/${model}`, options); } } diff --git a/src/resources/moderations.ts b/src/resources/moderations.ts index 63ae59d39..478850e5e 100644 --- a/src/resources/moderations.ts +++ b/src/resources/moderations.ts @@ -73,14 +73,14 @@ export namespace Moderation { * execution of wrongdoing, or that gives advice or instruction on how to commit * illicit acts. For example, "how to shoplift" would fit this category. */ - illicit: boolean; + illicit: boolean | null; /** * Content that includes instructions or advice that facilitate the planning or * execution of wrongdoing that also includes violence, or that gives advice or * instruction on the procurement of any weapon. */ - 'illicit/violent': boolean; + 'illicit/violent': boolean | null; /** * Content that promotes, encourages, or depicts acts of self-harm, such as diff --git a/src/resources/shared.ts b/src/resources/shared.ts index f44fda8a7..3bb11582f 100644 --- a/src/resources/shared.ts +++ b/src/resources/shared.ts @@ -55,6 +55,16 @@ export interface FunctionDefinition { */ export type FunctionParameters = Record; +/** + * Set of 16 key-value pairs that can be attached to an object. This can be useful + * for storing additional information about the object in a structured format, and + * querying for objects via API or the dashboard. + * + * Keys are strings with a maximum length of 64 characters. Values are strings with + * a maximum length of 512 characters. + */ +export type Metadata = Record; + export interface ResponseFormatJSONObject { /** * The type of response format being defined: `json_object` diff --git a/src/resources/uploads/parts.ts b/src/resources/uploads/parts.ts index 2bdd6bb4f..18568c3e4 100644 --- a/src/resources/uploads/parts.ts +++ b/src/resources/uploads/parts.ts @@ -2,8 +2,10 @@ import { APIResource } from '../../resource'; import { APIPromise } from '../../api-promise'; -import { type Uploadable, multipartFormRequestOptions } from '../../uploads'; +import { type Uploadable } from '../../uploads'; import { RequestOptions } from '../../internal/request-options'; +import { multipartFormRequestOptions } from '../../internal/uploads'; +import { path } from '../../internal/utils/path'; export class Parts extends APIResource { /** @@ -20,7 +22,10 @@ export class Parts extends APIResource { * [complete the Upload](https://platform.openai.com/docs/api-reference/uploads/complete). */ create(uploadID: string, body: PartCreateParams, options?: RequestOptions): APIPromise { - return this._client.post(`/uploads/${uploadID}/parts`, multipartFormRequestOptions({ body, ...options })); + return this._client.post( + path`/uploads/${uploadID}/parts`, + multipartFormRequestOptions({ body, ...options }, this._client), + ); } } diff --git a/src/resources/uploads/uploads.ts b/src/resources/uploads/uploads.ts index ff72798b0..a2f1b5250 100644 --- a/src/resources/uploads/uploads.ts +++ b/src/resources/uploads/uploads.ts @@ -6,6 +6,7 @@ import * as PartsAPI from './parts'; import { PartCreateParams, Parts, UploadPart } from './parts'; import { APIPromise } from '../../api-promise'; import { RequestOptions } from '../../internal/request-options'; +import { path } from '../../internal/utils/path'; export class Uploads extends APIResource { parts: PartsAPI.Parts = new PartsAPI.Parts(this._client); @@ -40,7 +41,7 @@ export class Uploads extends APIResource { * Cancels the Upload. No Parts may be added after an Upload is cancelled. */ cancel(uploadID: string, options?: RequestOptions): APIPromise { - return this._client.post(`/uploads/${uploadID}/cancel`, options); + return this._client.post(path`/uploads/${uploadID}/cancel`, options); } /** @@ -59,7 +60,7 @@ export class Uploads extends APIResource { * an Upload is completed. */ complete(uploadID: string, body: UploadCompleteParams, options?: RequestOptions): APIPromise { - return this._client.post(`/uploads/${uploadID}/complete`, { body, ...options }); + return this._client.post(path`/uploads/${uploadID}/complete`, { body, ...options }); } } @@ -83,7 +84,7 @@ export interface Upload { created_at: number; /** - * The Unix timestamp (in seconds) for when the Upload was created. + * The Unix timestamp (in seconds) for when the Upload will expire. */ expires_at: number; @@ -110,7 +111,7 @@ export interface Upload { status: 'pending' | 'completed' | 'cancelled' | 'expired'; /** - * The ready File object after the Upload is completed. + * The `File` object represents a document that has been uploaded to OpenAI. */ file?: FilesAPI.FileObject | null; } diff --git a/src/streaming.ts b/src/streaming.ts index 841a748a9..47c366071 100644 --- a/src/streaming.ts +++ b/src/streaming.ts @@ -1,11 +1,13 @@ import { OpenAIError } from './error'; import { type ReadableStream } from './internal/shim-types'; import { makeReadableStream } from './internal/shims'; -import { LineDecoder } from './internal/decoders/line'; +import { findDoubleNewlineIndex, LineDecoder } from './internal/decoders/line'; +import { ReadableStreamToAsyncIterable } from './internal/shims'; +import { isAbortError } from './internal/errors'; import { APIError } from './error'; -type Bytes = string | ArrayBuffer | Uint8Array | Buffer | null | undefined; +type Bytes = string | ArrayBuffer | Uint8Array | null | undefined; export type ServerSentEvent = { event: string | null; @@ -76,7 +78,7 @@ export class Stream implements AsyncIterable { done = true; } catch (e) { // If the user calls `stream.controller.abort()`, we should exit without throwing. - if (e instanceof Error && e.name === 'AbortError') return; + if (isAbortError(e)) return; throw e; } finally { // If the user `break`s, abort the ongoing request. @@ -97,7 +99,7 @@ export class Stream implements AsyncIterable { async function* iterLines(): AsyncGenerator { const lineDecoder = new LineDecoder(); - const iter = readableStreamAsyncIterable(readableStream); + const iter = ReadableStreamToAsyncIterable(readableStream); for await (const chunk of iter) { for (const line of lineDecoder.decode(chunk)) { yield line; @@ -123,7 +125,7 @@ export class Stream implements AsyncIterable { done = true; } catch (e) { // If the user calls `stream.controller.abort()`, we should exit without throwing. - if (e instanceof Error && e.name === 'AbortError') return; + if (isAbortError(e)) return; throw e; } finally { // If the user `break`s, abort the ongoing request. @@ -174,7 +176,9 @@ export class Stream implements AsyncIterable { toReadableStream(): ReadableStream { const self = this; let iter: AsyncIterator; - const encoder = new TextEncoder(); + const encoder: { + encode(str: string): Uint8Array; + } = new (globalThis as any).TextEncoder(); return makeReadableStream({ async start() { @@ -205,13 +209,21 @@ export async function* _iterSSEMessages( ): AsyncGenerator { if (!response.body) { controller.abort(); + if ( + typeof (globalThis as any).navigator !== 'undefined' && + (globalThis as any).navigator.product === 'ReactNative' + ) { + throw new OpenAIError( + `The default react-native fetch implementation does not support streaming. Please use expo/fetch: https://docs.expo.dev/versions/latest/sdk/expo/#expofetch-api`, + ); + } throw new OpenAIError(`Attempted to iterate over a response with no body`); } const sseDecoder = new SSEDecoder(); const lineDecoder = new LineDecoder(); - const iter = readableStreamAsyncIterable(response.body); + const iter = ReadableStreamToAsyncIterable(response.body); for await (const sseChunk of iterSSEChunks(iter)) { for (const line of lineDecoder.decode(sseChunk)) { const sse = sseDecoder.decode(line); @@ -239,7 +251,7 @@ async function* iterSSEChunks(iterator: AsyncIterableIterator): AsyncGene const binaryChunk = chunk instanceof ArrayBuffer ? new Uint8Array(chunk) - : typeof chunk === 'string' ? new TextEncoder().encode(chunk) + : typeof chunk === 'string' ? new (globalThis as any).TextEncoder().encode(chunk) : chunk; let newData = new Uint8Array(data.length + binaryChunk.length); @@ -259,37 +271,6 @@ async function* iterSSEChunks(iterator: AsyncIterableIterator): AsyncGene } } -function findDoubleNewlineIndex(buffer: Uint8Array): number { - // This function searches the buffer for the end patterns (\r\r, \n\n, \r\n\r\n) - // and returns the index right after the first occurrence of any pattern, - // or -1 if none of the patterns are found. - const newline = 0x0a; // \n - const carriage = 0x0d; // \r - - for (let i = 0; i < buffer.length - 2; i++) { - if (buffer[i] === newline && buffer[i + 1] === newline) { - // \n\n - return i + 2; - } - if (buffer[i] === carriage && buffer[i + 1] === carriage) { - // \r\r - return i + 2; - } - if ( - buffer[i] === carriage && - buffer[i + 1] === newline && - i + 3 < buffer.length && - buffer[i + 2] === carriage && - buffer[i + 3] === newline - ) { - // \r\n\r\n - return i + 4; - } - } - - return -1; -} - class SSEDecoder { private data: string[]; private event: string | null; @@ -345,17 +326,6 @@ class SSEDecoder { } } -/** This is an internal helper function that's just used for testing */ -export function _decodeChunks(chunks: string[]): string[] { - const decoder = new LineDecoder(); - const lines: string[] = []; - for (const chunk of chunks) { - lines.push(...decoder.decode(chunk)); - } - - return lines; -} - function partition(str: string, delimiter: string): [string, string, string] { const index = str.indexOf(delimiter); if (index !== -1) { @@ -364,36 +334,3 @@ function partition(str: string, delimiter: string): [string, string, string] { return [str, '', '']; } - -/** - * Most browsers don't yet have async iterable support for ReadableStream, - * and Node has a very different way of reading bytes from its "ReadableStream". - * - * This polyfill was pulled from https://github.com/MattiasBuelens/web-streams-polyfill/pull/122#issuecomment-1627354490 - */ -export function readableStreamAsyncIterable(stream: any): AsyncIterableIterator { - if (stream[Symbol.asyncIterator]) return stream; - - const reader = stream.getReader(); - return { - async next() { - try { - const result = await reader.read(); - if (result?.done) reader.releaseLock(); // release lock when stream becomes closed - return result; - } catch (e) { - reader.releaseLock(); // release lock when stream becomes errored - throw e; - } - }, - async return() { - const cancelPromise = reader.cancel(); - reader.releaseLock(); - await cancelPromise; - return { done: true, value: undefined }; - }, - [Symbol.asyncIterator]() { - return this; - }, - }; -} diff --git a/src/uploads.ts b/src/uploads.ts index 58f3782be..79d3073ea 100644 --- a/src/uploads.ts +++ b/src/uploads.ts @@ -1,258 +1,2 @@ -import { type RequestOptions } from './internal/request-options'; -import { type FilePropertyBag } from './internal/builtin-types'; -import { isFsReadStreamLike, type FsReadStreamLike } from './internal/shims'; -import './internal/polyfill/file.node.js'; - -type BlobLikePart = string | ArrayBuffer | ArrayBufferView | BlobLike | Uint8Array | DataView; -export type BlobPart = string | ArrayBuffer | ArrayBufferView | Blob | Uint8Array | DataView; - -/** - * Typically, this is a native "File" class. - * - * We provide the {@link toFile} utility to convert a variety of objects - * into the File class. - * - * For convenience, you can also pass a fetch Response, or in Node, - * the result of fs.createReadStream(). - */ -export type Uploadable = FileLike | ResponseLike | FsReadStreamLike; - -/** - * Intended to match web.Blob, node.Blob, undici.Blob, etc. - */ -export interface BlobLike { - /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/size) */ - readonly size: number; - /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/type) */ - readonly type: string; - /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/text) */ - text(): Promise; - /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/slice) */ - slice(start?: number, end?: number): BlobLike; -} - -/** - * This check adds the arrayBuffer() method type because it is available and used at runtime - */ -export const isBlobLike = (value: any): value is BlobLike & { arrayBuffer(): Promise } => - value != null && - typeof value === 'object' && - typeof value.size === 'number' && - typeof value.type === 'string' && - typeof value.text === 'function' && - typeof value.slice === 'function' && - typeof value.arrayBuffer === 'function'; - -/** - * Intended to match web.File, node.File, undici.File, etc. - */ -export interface FileLike extends BlobLike { - /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/File/lastModified) */ - readonly lastModified: number; - /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/File/name) */ - readonly name: string; -} -declare var FileClass: { - prototype: FileLike; - new (fileBits: BlobPart[], fileName: string, options?: FilePropertyBag): FileLike; -}; - -export const isFileLike = (value: any): value is FileLike => - value != null && - typeof value === 'object' && - typeof value.name === 'string' && - typeof value.lastModified === 'number' && - isBlobLike(value); - -/** - * Intended to match web.Response, node.Response, undici.Response, etc. - */ -export interface ResponseLike { - url: string; - blob(): Promise; -} - -export const isResponseLike = (value: any): value is ResponseLike => - value != null && - typeof value === 'object' && - typeof value.url === 'string' && - typeof value.blob === 'function'; - -export const isUploadable = (value: any): value is Uploadable => { - return isFileLike(value) || isResponseLike(value) || isFsReadStreamLike(value); -}; - -export type ToFileInput = Uploadable | Exclude | AsyncIterable; - -/** - * Construct a `File` instance. This is used to ensure a helpful error is thrown - * for environments that don't define a global `File` yet and so that we don't - * accidentally rely on a global `File` type in our annotations. - */ -function makeFile(fileBits: BlobPart[], fileName: string, options?: FilePropertyBag): FileLike { - const File = (globalThis as any).File as typeof FileClass | undefined; - if (typeof File === 'undefined') { - throw new Error('`File` is not defined as a global which is required for file uploads'); - } - - return new File(fileBits, fileName, options); -} - -/** - * Helper for creating a {@link File} to pass to an SDK upload method from a variety of different data formats - * @param value the raw content of the file. Can be an {@link Uploadable}, {@link BlobLikePart}, or {@link AsyncIterable} of {@link BlobLikePart}s - * @param {string=} name the name of the file. If omitted, toFile will try to determine a file name from bits if possible - * @param {Object=} options additional properties - * @param {string=} options.type the MIME type of the content - * @param {number=} options.lastModified the last modified timestamp - * @returns a {@link File} with the given properties - */ -export async function toFile( - value: ToFileInput | PromiseLike, - name?: string | null | undefined, - options?: FilePropertyBag | undefined, -): Promise { - // If it's a promise, resolve it. - value = await value; - - // If we've been given a `File` we don't need to do anything - if (isFileLike(value)) { - return value; - } - - if (isResponseLike(value)) { - const blob = await value.blob(); - name ||= new URL(value.url).pathname.split(/[\\/]/).pop() ?? 'unknown_file'; - - // we need to convert the `Blob` into an array buffer because the `Blob` class - // that `node-fetch` defines is incompatible with the web standard which results - // in `new File` interpreting it as a string instead of binary data. - const data = isBlobLike(blob) ? [(await blob.arrayBuffer()) as any] : [blob]; - - return makeFile(data, name, options); - } - - const bits = await getBytes(value); - - name ||= getName(value) ?? 'unknown_file'; - - if (!options?.type) { - const type = (bits[0] as any)?.type; - if (typeof type === 'string') { - options = { ...options, type }; - } - } - - return makeFile(bits, name, options); -} - -async function getBytes(value: ToFileInput): Promise> { - let parts: Array = []; - if ( - typeof value === 'string' || - ArrayBuffer.isView(value) || // includes Uint8Array, Buffer, etc. - value instanceof ArrayBuffer - ) { - parts.push(value); - } else if (isBlobLike(value)) { - parts.push(await value.arrayBuffer()); - } else if ( - isAsyncIterableIterator(value) // includes Readable, ReadableStream, etc. - ) { - for await (const chunk of value) { - parts.push(chunk as BlobPart); // TODO, consider validating? - } - } else { - throw new Error( - `Unexpected data type: ${typeof value}; constructor: ${value?.constructor - ?.name}; props: ${propsForError(value)}`, - ); - } - - return parts; -} - -function propsForError(value: any): string { - const props = Object.getOwnPropertyNames(value); - return `[${props.map((p) => `"${p}"`).join(', ')}]`; -} - -function getName(value: any): string | undefined { - return ( - getStringFromMaybeBuffer(value.name) || - getStringFromMaybeBuffer(value.filename) || - // For fs.ReadStream - getStringFromMaybeBuffer(value.path)?.split(/[\\/]/).pop() - ); -} - -const getStringFromMaybeBuffer = (x: string | Buffer | unknown): string | undefined => { - if (typeof x === 'string') return x; - if (typeof Buffer !== 'undefined' && x instanceof Buffer) return String(x); - return undefined; -}; - -const isAsyncIterableIterator = (value: any): value is AsyncIterableIterator => - value != null && typeof value === 'object' && typeof value[Symbol.asyncIterator] === 'function'; - -/** - * Returns a multipart/form-data request if any part of the given request body contains a File / Blob value. - * Otherwise returns the request as is. - */ -export const maybeMultipartFormRequestOptions = async (opts: RequestOptions): Promise => { - if (!hasUploadableValue(opts.body)) return opts; - - return { ...opts, body: await createForm(opts.body) }; -}; - -type MultipartFormRequestOptions = Omit & { body: unknown }; - -export const multipartFormRequestOptions = async ( - opts: MultipartFormRequestOptions, -): Promise => { - return { ...opts, body: await createForm(opts.body) }; -}; - -export const createForm = async >(body: T | undefined): Promise => { - const form = new FormData(); - await Promise.all(Object.entries(body || {}).map(([key, value]) => addFormValue(form, key, value))); - return form; -}; - -const hasUploadableValue = (value: unknown): boolean => { - if (isUploadable(value)) return true; - if (Array.isArray(value)) return value.some(hasUploadableValue); - if (value && typeof value === 'object') { - for (const k in value) { - if (hasUploadableValue((value as any)[k])) return true; - } - } - return false; -}; - -const addFormValue = async (form: FormData, key: string, value: unknown): Promise => { - if (value === undefined) return; - if (value == null) { - throw new TypeError( - `Received null for "${key}"; to pass null in FormData, you must use the string 'null'`, - ); - } - - // TODO: make nested formats configurable - if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { - form.append(key, String(value)); - } else if (isUploadable(value)) { - const file = await toFile(value); - form.append(key, file as any); - } else if (Array.isArray(value)) { - await Promise.all(value.map((entry) => addFormValue(form, key + '[]', entry))); - } else if (typeof value === 'object') { - await Promise.all( - Object.entries(value).map(([name, prop]) => addFormValue(form, `${key}[${name}]`, prop)), - ); - } else { - throw new TypeError( - `Invalid value given to form, expected a string, number, boolean, object, Array, File or Blob but got ${value} instead`, - ); - } -}; +export { type Uploadable } from './internal/uploads'; +export { toFile, type ToFileInput } from './internal/to-file'; diff --git a/src/version.ts b/src/version.ts index 2205d0775..58d8f3c50 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const VERSION = '5.0.0-alpha.0'; // x-release-please-version +export const VERSION = '5.0.0-beta.0'; // x-release-please-version diff --git a/tests/api-resources/beta/assistants.test.ts b/tests/api-resources/beta/assistants.test.ts index 8d04127e1..8bdbc408e 100644 --- a/tests/api-resources/beta/assistants.test.ts +++ b/tests/api-resources/beta/assistants.test.ts @@ -24,15 +24,18 @@ describe('resource assistants', () => { model: 'gpt-4o', description: 'description', instructions: 'instructions', - metadata: {}, + metadata: { foo: 'string' }, name: 'name', + reasoning_effort: 'low', response_format: 'auto', temperature: 1, tool_resources: { code_interpreter: { file_ids: ['string'] }, file_search: { vector_store_ids: ['string'], - vector_stores: [{ chunking_strategy: { type: 'auto' }, file_ids: ['string'], metadata: {} }], + vector_stores: [ + { chunking_strategy: { type: 'auto' }, file_ids: ['string'], metadata: { foo: 'string' } }, + ], }, }, tools: [{ type: 'code_interpreter' }], diff --git a/tests/api-resources/beta/realtime/sessions.test.ts b/tests/api-resources/beta/realtime/sessions.test.ts new file mode 100644 index 000000000..1a75a532c --- /dev/null +++ b/tests/api-resources/beta/realtime/sessions.test.ts @@ -0,0 +1,21 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import OpenAI from 'openai'; + +const client = new OpenAI({ + apiKey: 'My API Key', + baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010', +}); + +describe('resource sessions', () => { + test('create', async () => { + const responsePromise = client.beta.realtime.sessions.create({}); + const rawResponse = await responsePromise.asResponse(); + expect(rawResponse).toBeInstanceOf(Response); + const response = await responsePromise; + expect(response).not.toBeInstanceOf(Response); + const dataAndResponse = await responsePromise.withResponse(); + expect(dataAndResponse.data).toBe(response); + expect(dataAndResponse.response).toBe(rawResponse); + }); +}); diff --git a/tests/api-resources/beta/threads/messages.test.ts b/tests/api-resources/beta/threads/messages.test.ts index 98f607b18..587daf0a1 100644 --- a/tests/api-resources/beta/threads/messages.test.ts +++ b/tests/api-resources/beta/threads/messages.test.ts @@ -27,7 +27,7 @@ describe('resource messages', () => { content: 'string', role: 'user', attachments: [{ file_id: 'file_id', tools: [{ type: 'code_interpreter' }] }], - metadata: {}, + metadata: { foo: 'string' }, }); }); @@ -60,7 +60,7 @@ describe('resource messages', () => { test('update: required and optional params', async () => { const response = await client.beta.threads.messages.update('message_id', { thread_id: 'thread_id', - metadata: {}, + metadata: { foo: 'string' }, }); }); diff --git a/tests/api-resources/beta/threads/runs/runs.test.ts b/tests/api-resources/beta/threads/runs/runs.test.ts index 18474bd94..118a4f324 100644 --- a/tests/api-resources/beta/threads/runs/runs.test.ts +++ b/tests/api-resources/beta/threads/runs/runs.test.ts @@ -29,15 +29,16 @@ describe('resource runs', () => { content: 'string', role: 'user', attachments: [{ file_id: 'file_id', tools: [{ type: 'code_interpreter' }] }], - metadata: {}, + metadata: { foo: 'string' }, }, ], instructions: 'instructions', max_completion_tokens: 256, max_prompt_tokens: 256, - metadata: {}, + metadata: { foo: 'string' }, model: 'gpt-4o', parallel_tool_calls: true, + reasoning_effort: 'low', response_format: 'auto', stream: false, temperature: 1, @@ -77,7 +78,7 @@ describe('resource runs', () => { test('update: required and optional params', async () => { const response = await client.beta.threads.runs.update('run_id', { thread_id: 'thread_id', - metadata: {}, + metadata: { foo: 'string' }, }); }); diff --git a/tests/api-resources/beta/threads/threads.test.ts b/tests/api-resources/beta/threads/threads.test.ts index 67e015ef1..532bacb7c 100644 --- a/tests/api-resources/beta/threads/threads.test.ts +++ b/tests/api-resources/beta/threads/threads.test.ts @@ -29,15 +29,17 @@ describe('resource threads', () => { content: 'string', role: 'user', attachments: [{ file_id: 'file_id', tools: [{ type: 'code_interpreter' }] }], - metadata: {}, + metadata: { foo: 'string' }, }, ], - metadata: {}, + metadata: { foo: 'string' }, tool_resources: { code_interpreter: { file_ids: ['string'] }, file_search: { vector_store_ids: ['string'], - vector_stores: [{ chunking_strategy: { type: 'auto' }, file_ids: ['string'], metadata: {} }], + vector_stores: [ + { chunking_strategy: { type: 'auto' }, file_ids: ['string'], metadata: { foo: 'string' } }, + ], }, }, }, @@ -96,7 +98,7 @@ describe('resource threads', () => { instructions: 'instructions', max_completion_tokens: 256, max_prompt_tokens: 256, - metadata: {}, + metadata: { foo: 'string' }, model: 'gpt-4o', parallel_tool_calls: true, response_format: 'auto', @@ -108,15 +110,17 @@ describe('resource threads', () => { content: 'string', role: 'user', attachments: [{ file_id: 'file_id', tools: [{ type: 'code_interpreter' }] }], - metadata: {}, + metadata: { foo: 'string' }, }, ], - metadata: {}, + metadata: { foo: 'string' }, tool_resources: { code_interpreter: { file_ids: ['string'] }, file_search: { vector_store_ids: ['string'], - vector_stores: [{ chunking_strategy: { type: 'auto' }, file_ids: ['string'], metadata: {} }], + vector_stores: [ + { chunking_strategy: { type: 'auto' }, file_ids: ['string'], metadata: { foo: 'string' } }, + ], }, }, }, diff --git a/tests/api-resources/chat/completions.test.ts b/tests/api-resources/chat/completions.test.ts deleted file mode 100644 index 7aa8c5d99..000000000 --- a/tests/api-resources/chat/completions.test.ts +++ /dev/null @@ -1,64 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import OpenAI from 'openai'; - -const client = new OpenAI({ - apiKey: 'My API Key', - baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010', -}); - -describe('resource completions', () => { - test('create: only required params', async () => { - const responsePromise = client.chat.completions.create({ - messages: [{ content: 'string', role: 'developer' }], - model: 'gpt-4o', - }); - const rawResponse = await responsePromise.asResponse(); - expect(rawResponse).toBeInstanceOf(Response); - const response = await responsePromise; - expect(response).not.toBeInstanceOf(Response); - const dataAndResponse = await responsePromise.withResponse(); - expect(dataAndResponse.data).toBe(response); - expect(dataAndResponse.response).toBe(rawResponse); - }); - - test('create: required and optional params', async () => { - const response = await client.chat.completions.create({ - messages: [{ content: 'string', role: 'developer', name: 'name' }], - model: 'gpt-4o', - audio: { format: 'wav', voice: 'alloy' }, - frequency_penalty: -2, - function_call: 'none', - functions: [{ name: 'name', description: 'description', parameters: { foo: 'bar' } }], - logit_bias: { foo: 0 }, - logprobs: true, - max_completion_tokens: 0, - max_tokens: 0, - metadata: { foo: 'string' }, - modalities: ['text'], - n: 1, - parallel_tool_calls: true, - prediction: { content: 'string', type: 'content' }, - presence_penalty: -2, - reasoning_effort: 'low', - response_format: { type: 'text' }, - seed: -9007199254740991, - service_tier: 'auto', - stop: 'string', - store: true, - stream: false, - stream_options: { include_usage: true }, - temperature: 1, - tool_choice: 'none', - tools: [ - { - function: { name: 'name', description: 'description', parameters: { foo: 'bar' }, strict: true }, - type: 'function', - }, - ], - top_logprobs: 0, - top_p: 1, - user: 'user-1234', - }); - }); -}); diff --git a/tests/api-resources/chat/completions/completions.test.ts b/tests/api-resources/chat/completions/completions.test.ts new file mode 100644 index 000000000..5de69484a --- /dev/null +++ b/tests/api-resources/chat/completions/completions.test.ts @@ -0,0 +1,122 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import OpenAI from 'openai'; + +const client = new OpenAI({ + apiKey: 'My API Key', + baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010', +}); + +describe('resource completions', () => { + test('create: only required params', async () => { + const responsePromise = client.chat.completions.create({ + messages: [{ content: 'string', role: 'developer' }], + model: 'gpt-4o', + }); + const rawResponse = await responsePromise.asResponse(); + expect(rawResponse).toBeInstanceOf(Response); + const response = await responsePromise; + expect(response).not.toBeInstanceOf(Response); + const dataAndResponse = await responsePromise.withResponse(); + expect(dataAndResponse.data).toBe(response); + expect(dataAndResponse.response).toBe(rawResponse); + }); + + test('create: required and optional params', async () => { + const response = await client.chat.completions.create({ + messages: [{ content: 'string', role: 'developer', name: 'name' }], + model: 'gpt-4o', + audio: { format: 'wav', voice: 'alloy' }, + frequency_penalty: -2, + function_call: 'none', + functions: [{ name: 'name', description: 'description', parameters: { foo: 'bar' } }], + logit_bias: { foo: 0 }, + logprobs: true, + max_completion_tokens: 0, + max_tokens: 0, + metadata: { foo: 'string' }, + modalities: ['text'], + n: 1, + parallel_tool_calls: true, + prediction: { content: 'string', type: 'content' }, + presence_penalty: -2, + reasoning_effort: 'low', + response_format: { type: 'text' }, + seed: 0, + service_tier: 'auto', + stop: 'string', + store: true, + stream: false, + stream_options: { include_usage: true }, + temperature: 1, + tool_choice: 'none', + tools: [ + { + function: { name: 'name', description: 'description', parameters: { foo: 'bar' }, strict: true }, + type: 'function', + }, + ], + top_logprobs: 0, + top_p: 1, + user: 'user-1234', + }); + }); + + test('retrieve', async () => { + const responsePromise = client.chat.completions.retrieve('completion_id'); + const rawResponse = await responsePromise.asResponse(); + expect(rawResponse).toBeInstanceOf(Response); + const response = await responsePromise; + expect(response).not.toBeInstanceOf(Response); + const dataAndResponse = await responsePromise.withResponse(); + expect(dataAndResponse.data).toBe(response); + expect(dataAndResponse.response).toBe(rawResponse); + }); + + test('update: only required params', async () => { + const responsePromise = client.chat.completions.update('completion_id', { metadata: { foo: 'string' } }); + const rawResponse = await responsePromise.asResponse(); + expect(rawResponse).toBeInstanceOf(Response); + const response = await responsePromise; + expect(response).not.toBeInstanceOf(Response); + const dataAndResponse = await responsePromise.withResponse(); + expect(dataAndResponse.data).toBe(response); + expect(dataAndResponse.response).toBe(rawResponse); + }); + + test('update: required and optional params', async () => { + const response = await client.chat.completions.update('completion_id', { metadata: { foo: 'string' } }); + }); + + test('list', async () => { + const responsePromise = client.chat.completions.list(); + const rawResponse = await responsePromise.asResponse(); + expect(rawResponse).toBeInstanceOf(Response); + const response = await responsePromise; + expect(response).not.toBeInstanceOf(Response); + const dataAndResponse = await responsePromise.withResponse(); + expect(dataAndResponse.data).toBe(response); + expect(dataAndResponse.response).toBe(rawResponse); + }); + + test('list: request options and params are passed correctly', async () => { + // ensure the request options are being passed correctly by passing an invalid HTTP method in order to cause an error + await expect( + client.chat.completions.list( + { after: 'after', limit: 0, metadata: { foo: 'string' }, model: 'model', order: 'asc' }, + { path: '/_stainless_unknown_path' }, + ), + ).rejects.toThrow(OpenAI.NotFoundError); + }); + + test('delete', async () => { + const responsePromise = client.chat.completions.delete('completion_id'); + const rawResponse = await responsePromise.asResponse(); + expect(rawResponse).toBeInstanceOf(Response); + const response = await responsePromise; + expect(response).not.toBeInstanceOf(Response); + const dataAndResponse = await responsePromise.withResponse(); + expect(dataAndResponse.data).toBe(response); + expect(dataAndResponse.response).toBe(rawResponse); + }); +}); diff --git a/tests/api-resources/chat/completions/messages.test.ts b/tests/api-resources/chat/completions/messages.test.ts new file mode 100644 index 000000000..98dd22167 --- /dev/null +++ b/tests/api-resources/chat/completions/messages.test.ts @@ -0,0 +1,32 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import OpenAI from 'openai'; + +const client = new OpenAI({ + apiKey: 'My API Key', + baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010', +}); + +describe('resource messages', () => { + test('list', async () => { + const responsePromise = client.chat.completions.messages.list('completion_id'); + const rawResponse = await responsePromise.asResponse(); + expect(rawResponse).toBeInstanceOf(Response); + const response = await responsePromise; + expect(response).not.toBeInstanceOf(Response); + const dataAndResponse = await responsePromise.withResponse(); + expect(dataAndResponse.data).toBe(response); + expect(dataAndResponse.response).toBe(rawResponse); + }); + + test('list: request options and params are passed correctly', async () => { + // ensure the request options are being passed correctly by passing an invalid HTTP method in order to cause an error + await expect( + client.chat.completions.messages.list( + 'completion_id', + { after: 'after', limit: 0, order: 'asc' }, + { path: '/_stainless_unknown_path' }, + ), + ).rejects.toThrow(OpenAI.NotFoundError); + }); +}); diff --git a/tests/api-resources/completions.test.ts b/tests/api-resources/completions.test.ts index 96a2262ae..a9d188a2b 100644 --- a/tests/api-resources/completions.test.ts +++ b/tests/api-resources/completions.test.ts @@ -31,7 +31,7 @@ describe('resource completions', () => { max_tokens: 16, n: 1, presence_penalty: -2, - seed: -9007199254740991, + seed: 0, stop: '\n', stream: false, stream_options: { include_usage: true }, diff --git a/tests/api-resources/files.test.ts b/tests/api-resources/files.test.ts index 647742af2..96c8ac563 100644 --- a/tests/api-resources/files.test.ts +++ b/tests/api-resources/files.test.ts @@ -71,15 +71,4 @@ describe('resource files', () => { expect(dataAndResponse.data).toBe(response); expect(dataAndResponse.response).toBe(rawResponse); }); - - test('retrieveContent', async () => { - const responsePromise = client.files.retrieveContent('file_id'); - const rawResponse = await responsePromise.asResponse(); - expect(rawResponse).toBeInstanceOf(Response); - const response = await responsePromise; - expect(response).not.toBeInstanceOf(Response); - const dataAndResponse = await responsePromise.withResponse(); - expect(dataAndResponse.data).toBe(response); - expect(dataAndResponse.response).toBe(rawResponse); - }); }); diff --git a/tests/form.test.ts b/tests/form.test.ts index 0bd22c549..5ca5b75f2 100644 --- a/tests/form.test.ts +++ b/tests/form.test.ts @@ -1,63 +1,85 @@ -import { multipartFormRequestOptions, createForm, toFile } from 'openai'; +import { multipartFormRequestOptions, createForm } from 'openai/internal/uploads'; +import { toFile } from 'openai/uploads'; describe('form data validation', () => { test('valid values do not error', async () => { - await multipartFormRequestOptions({ - body: { - foo: 'foo', - string: 1, - bool: true, - file: await toFile(Buffer.from('some-content')), - blob: new Blob(['Some content'], { type: 'text/plain' }), + await multipartFormRequestOptions( + { + body: { + foo: 'foo', + string: 1, + bool: true, + file: await toFile(Buffer.from('some-content')), + blob: new Blob(['Some content'], { type: 'text/plain' }), + }, }, - }); + fetch, + ); }); test('null', async () => { await expect(() => - multipartFormRequestOptions({ - body: { - null: null, + multipartFormRequestOptions( + { + body: { + null: null, + }, }, - }), + fetch, + ), ).rejects.toThrow(TypeError); }); test('undefined is stripped', async () => { - const form = await createForm({ - foo: undefined, - bar: 'baz', - }); + const form = await createForm( + { + foo: undefined, + bar: 'baz', + }, + fetch, + ); expect(form.has('foo')).toBe(false); expect(form.get('bar')).toBe('baz'); }); test('nested undefined property is stripped', async () => { - const form = await createForm({ - bar: { - baz: undefined, + const form = await createForm( + { + bar: { + baz: undefined, + }, }, - }); + fetch, + ); expect(Array.from(form.entries())).toEqual([]); - const form2 = await createForm({ - bar: { - foo: 'string', - baz: undefined, + const form2 = await createForm( + { + bar: { + foo: 'string', + baz: undefined, + }, }, - }); + fetch, + ); expect(Array.from(form2.entries())).toEqual([['bar[foo]', 'string']]); }); test('nested undefined array item is stripped', async () => { - const form = await createForm({ - bar: [undefined, undefined], - }); + const form = await createForm( + { + bar: [undefined, undefined], + }, + fetch, + ); expect(Array.from(form.entries())).toEqual([]); - const form2 = await createForm({ - bar: [undefined, 'foo'], - }); + const form2 = await createForm( + { + bar: [undefined, 'foo'], + }, + fetch, + ); expect(Array.from(form2.entries())).toEqual([['bar[]', 'foo']]); }); }); diff --git a/tests/index.test.ts b/tests/index.test.ts index 34b7f5860..07c99ba87 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -1,5 +1,7 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +import { APIPromise } from 'openai/api-promise'; + import util from 'node:util'; import OpenAI from 'openai'; import { APIUserAbortError } from 'openai'; @@ -11,8 +13,6 @@ describe('instantiate client', () => { beforeEach(() => { jest.resetModules(); process.env = { ...env }; - - console.warn = jest.fn(); }); afterEach(() => { @@ -49,6 +49,135 @@ describe('instantiate client', () => { expect(req.headers.has('x-my-default-header')).toBe(false); }); }); + describe('logging', () => { + const env = process.env; + + beforeEach(() => { + process.env = { ...env }; + process.env['OPENAI_LOG'] = undefined; + }); + + afterEach(() => { + process.env = env; + }); + + const forceAPIResponseForClient = async (client: OpenAI) => { + await new APIPromise( + client, + Promise.resolve({ + response: new Response(), + controller: new AbortController(), + requestLogID: 'log_000000', + retryOfRequestLogID: undefined, + startTime: Date.now(), + options: { + method: 'get', + path: '/', + }, + }), + ); + }; + + test('debug logs when log level is debug', async () => { + const debugMock = jest.fn(); + const logger = { + debug: debugMock, + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }; + + const client = new OpenAI({ logger: logger, logLevel: 'debug', apiKey: 'My API Key' }); + + await forceAPIResponseForClient(client); + expect(debugMock).toHaveBeenCalled(); + }); + + test('default logLevel is warn', async () => { + const client = new OpenAI({ apiKey: 'My API Key' }); + expect(client.logLevel).toBe('warn'); + }); + + test('debug logs are skipped when log level is info', async () => { + const debugMock = jest.fn(); + const logger = { + debug: debugMock, + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }; + + const client = new OpenAI({ logger: logger, logLevel: 'info', apiKey: 'My API Key' }); + + await forceAPIResponseForClient(client); + expect(debugMock).not.toHaveBeenCalled(); + }); + + test('debug logs happen with debug env var', async () => { + const debugMock = jest.fn(); + const logger = { + debug: debugMock, + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }; + + process.env['OPENAI_LOG'] = 'debug'; + const client = new OpenAI({ logger: logger, apiKey: 'My API Key' }); + expect(client.logLevel).toBe('debug'); + + await forceAPIResponseForClient(client); + expect(debugMock).toHaveBeenCalled(); + }); + + test('warn when env var level is invalid', async () => { + const warnMock = jest.fn(); + const logger = { + debug: jest.fn(), + info: jest.fn(), + warn: warnMock, + error: jest.fn(), + }; + + process.env['OPENAI_LOG'] = 'not a log level'; + const client = new OpenAI({ logger: logger, apiKey: 'My API Key' }); + expect(client.logLevel).toBe('warn'); + expect(warnMock).toHaveBeenCalledWith( + 'process.env[\'OPENAI_LOG\'] was set to "not a log level", expected one of ["off","error","warn","info","debug"]', + ); + }); + + test('client log level overrides env var', async () => { + const debugMock = jest.fn(); + const logger = { + debug: debugMock, + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }; + + process.env['OPENAI_LOG'] = 'debug'; + const client = new OpenAI({ logger: logger, logLevel: 'off', apiKey: 'My API Key' }); + + await forceAPIResponseForClient(client); + expect(debugMock).not.toHaveBeenCalled(); + }); + + test('no warning logged for invalid env var level + valid client level', async () => { + const warnMock = jest.fn(); + const logger = { + debug: jest.fn(), + info: jest.fn(), + warn: warnMock, + error: jest.fn(), + }; + + process.env['OPENAI_LOG'] = 'not a log level'; + const client = new OpenAI({ logger: logger, logLevel: 'debug', apiKey: 'My API Key' }); + expect(client.logLevel).toBe('debug'); + expect(warnMock).not.toHaveBeenCalled(); + }); + }); describe('defaultQuery', () => { test('with null query params given', () => { @@ -96,6 +225,15 @@ describe('instantiate client', () => { expect(response).toEqual({ url: 'http://localhost:5000/foo', custom: true }); }); + test('explicit global fetch', async () => { + // make sure the global fetch type is assignable to our Fetch type + const client = new OpenAI({ + baseURL: 'http://localhost:5000/', + apiKey: 'My API Key', + fetch: defaultFetch, + }); + }); + test('custom signal', async () => { const client = new OpenAI({ baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010', diff --git a/tests/internal/decoders/line.test.ts b/tests/internal/decoders/line.test.ts new file mode 100644 index 000000000..e76858e55 --- /dev/null +++ b/tests/internal/decoders/line.test.ts @@ -0,0 +1,128 @@ +import { findDoubleNewlineIndex, LineDecoder } from 'openai/internal/decoders/line'; + +function decodeChunks(chunks: string[], { flush }: { flush: boolean } = { flush: false }): string[] { + const decoder = new LineDecoder(); + const lines: string[] = []; + for (const chunk of chunks) { + lines.push(...decoder.decode(chunk)); + } + + if (flush) { + lines.push(...decoder.flush()); + } + + return lines; +} + +describe('line decoder', () => { + test('basic', () => { + // baz is not included because the line hasn't ended yet + expect(decodeChunks(['foo', ' bar\nbaz'])).toEqual(['foo bar']); + }); + + test('basic with \\r', () => { + expect(decodeChunks(['foo', ' bar\r\nbaz'])).toEqual(['foo bar']); + expect(decodeChunks(['foo', ' bar\r\nbaz'], { flush: true })).toEqual(['foo bar', 'baz']); + }); + + test('trailing new lines', () => { + expect(decodeChunks(['foo', ' bar', 'baz\n', 'thing\n'])).toEqual(['foo barbaz', 'thing']); + }); + + test('trailing new lines with \\r', () => { + expect(decodeChunks(['foo', ' bar', 'baz\r\n', 'thing\r\n'])).toEqual(['foo barbaz', 'thing']); + }); + + test('escaped new lines', () => { + expect(decodeChunks(['foo', ' bar\\nbaz\n'])).toEqual(['foo bar\\nbaz']); + }); + + test('escaped new lines with \\r', () => { + expect(decodeChunks(['foo', ' bar\\r\\nbaz\n'])).toEqual(['foo bar\\r\\nbaz']); + }); + + test('\\r & \\n split across multiple chunks', () => { + expect(decodeChunks(['foo\r', '\n', 'bar'], { flush: true })).toEqual(['foo', 'bar']); + }); + + test('single \\r', () => { + expect(decodeChunks(['foo\r', 'bar'], { flush: true })).toEqual(['foo', 'bar']); + }); + + test('double \\r', () => { + expect(decodeChunks(['foo\r', 'bar\r'], { flush: true })).toEqual(['foo', 'bar']); + expect(decodeChunks(['foo\r', '\r', 'bar'], { flush: true })).toEqual(['foo', '', 'bar']); + // implementation detail that we don't yield the single \r line until a new \r or \n is encountered + expect(decodeChunks(['foo\r', '\r', 'bar'], { flush: false })).toEqual(['foo']); + }); + + test('double \\r then \\r\\n', () => { + expect(decodeChunks(['foo\r', '\r', '\r', '\n', 'bar', '\n'])).toEqual(['foo', '', '', 'bar']); + expect(decodeChunks(['foo\n', '\n', '\n', 'bar', '\n'])).toEqual(['foo', '', '', 'bar']); + }); + + test('double newline', () => { + expect(decodeChunks(['foo\n\nbar'], { flush: true })).toEqual(['foo', '', 'bar']); + expect(decodeChunks(['foo', '\n', '\nbar'], { flush: true })).toEqual(['foo', '', 'bar']); + expect(decodeChunks(['foo\n', '\n', 'bar'], { flush: true })).toEqual(['foo', '', 'bar']); + expect(decodeChunks(['foo', '\n', '\n', 'bar'], { flush: true })).toEqual(['foo', '', 'bar']); + }); + + test('multi-byte characters across chunks', () => { + const decoder = new LineDecoder(); + + // bytes taken from the string 'известни' and arbitrarily split + // so that some multi-byte characters span multiple chunks + expect(decoder.decode(new Uint8Array([0xd0]))).toHaveLength(0); + expect(decoder.decode(new Uint8Array([0xb8, 0xd0, 0xb7, 0xd0]))).toHaveLength(0); + expect( + decoder.decode(new Uint8Array([0xb2, 0xd0, 0xb5, 0xd1, 0x81, 0xd1, 0x82, 0xd0, 0xbd, 0xd0, 0xb8])), + ).toHaveLength(0); + + const decoded = decoder.decode(new Uint8Array([0xa])); + expect(decoded).toEqual(['известни']); + }); + + test('flushing trailing newlines', () => { + expect(decodeChunks(['foo\n', '\nbar'], { flush: true })).toEqual(['foo', '', 'bar']); + }); + + test('flushing empty buffer', () => { + expect(decodeChunks([], { flush: true })).toEqual([]); + }); +}); + +describe('findDoubleNewlineIndex', () => { + test('finds \\n\\n', () => { + expect(findDoubleNewlineIndex(new TextEncoder().encode('foo\n\nbar'))).toBe(5); + expect(findDoubleNewlineIndex(new TextEncoder().encode('\n\nbar'))).toBe(2); + expect(findDoubleNewlineIndex(new TextEncoder().encode('foo\n\n'))).toBe(5); + expect(findDoubleNewlineIndex(new TextEncoder().encode('\n\n'))).toBe(2); + }); + + test('finds \\r\\r', () => { + expect(findDoubleNewlineIndex(new TextEncoder().encode('foo\r\rbar'))).toBe(5); + expect(findDoubleNewlineIndex(new TextEncoder().encode('\r\rbar'))).toBe(2); + expect(findDoubleNewlineIndex(new TextEncoder().encode('foo\r\r'))).toBe(5); + expect(findDoubleNewlineIndex(new TextEncoder().encode('\r\r'))).toBe(2); + }); + + test('finds \\r\\n\\r\\n', () => { + expect(findDoubleNewlineIndex(new TextEncoder().encode('foo\r\n\r\nbar'))).toBe(7); + expect(findDoubleNewlineIndex(new TextEncoder().encode('\r\n\r\nbar'))).toBe(4); + expect(findDoubleNewlineIndex(new TextEncoder().encode('foo\r\n\r\n'))).toBe(7); + expect(findDoubleNewlineIndex(new TextEncoder().encode('\r\n\r\n'))).toBe(4); + }); + + test('returns -1 when no double newline found', () => { + expect(findDoubleNewlineIndex(new TextEncoder().encode('foo\nbar'))).toBe(-1); + expect(findDoubleNewlineIndex(new TextEncoder().encode('foo\rbar'))).toBe(-1); + expect(findDoubleNewlineIndex(new TextEncoder().encode('foo\r\nbar'))).toBe(-1); + expect(findDoubleNewlineIndex(new TextEncoder().encode(''))).toBe(-1); + }); + + test('handles incomplete patterns', () => { + expect(findDoubleNewlineIndex(new TextEncoder().encode('foo\r\n\r'))).toBe(-1); + expect(findDoubleNewlineIndex(new TextEncoder().encode('foo\r\n'))).toBe(-1); + }); +}); diff --git a/tests/lib/ChatCompletionRunFunctions.test.ts b/tests/lib/ChatCompletionRunFunctions.test.ts index dcdc086de..f2ce1c029 100644 --- a/tests/lib/ChatCompletionRunFunctions.test.ts +++ b/tests/lib/ChatCompletionRunFunctions.test.ts @@ -4,9 +4,9 @@ import { PassThrough } from 'stream'; import { ParsingToolFunction, type ChatCompletionRunner, - type ChatCompletionFunctionRunnerParams, + type ChatCompletionToolRunnerParams, ChatCompletionStreamingRunner, - type ChatCompletionStreamingFunctionRunnerParams, + type ChatCompletionStreamingToolRunnerParams, } from 'openai/resources/beta/chat/completions'; import type { ChatCompletionMessageParam } from 'openai/resources/chat/completions'; import { isAssistantMessage } from '../../src/lib/chatCompletionUtils'; @@ -18,12 +18,12 @@ function mockChatCompletionFetch() { const { fetch, handleRequest: handleRawRequest } = mockFetch(); function handleRequest( - handler: (body: ChatCompletionFunctionRunnerParams) => Promise, + handler: (body: ChatCompletionToolRunnerParams) => Promise, ): Promise { return handleRawRequest(async (req, init) => { const rawBody = init?.body; if (typeof rawBody !== 'string') throw new Error(`expected init.body to be a string`); - const body: ChatCompletionFunctionRunnerParams = JSON.parse(rawBody); + const body: ChatCompletionToolRunnerParams = JSON.parse(rawBody); return new Response(JSON.stringify(await handler(body)), { headers: { 'Content-Type': 'application/json' }, }); @@ -39,13 +39,13 @@ function mockStreamingChatCompletionFetch() { function handleRequest( handler: ( - body: ChatCompletionStreamingFunctionRunnerParams, + body: ChatCompletionStreamingToolRunnerParams, ) => AsyncIterable, ): Promise { return handleRawRequest(async (req, init) => { const rawBody = init?.body; if (typeof rawBody !== 'string') throw new Error(`expected init.body to be a string`); - const body: ChatCompletionStreamingFunctionRunnerParams = JSON.parse(rawBody); + const body: ChatCompletionStreamingToolRunnerParams = JSON.parse(rawBody); const stream = new PassThrough(); (async () => { for await (const chunk of handler(body)) { @@ -627,7 +627,7 @@ describe('resource completions', () => { content: "it's raining", parsed: null, refusal: null, - tool_calls: [], + tool_calls: undefined, }, ]); expect(listener.functionCallResults).toEqual([`it's raining`]); @@ -875,7 +875,7 @@ describe('resource completions', () => { content: 'there are 3 properties in {"a": 1, "b": 2, "c": 3}', parsed: null, refusal: null, - tool_calls: [], + tool_calls: undefined, }, ]); expect(listener.functionCallResults).toEqual(['3']); @@ -1124,7 +1124,7 @@ describe('resource completions', () => { content: 'there are 3 properties in {"a": 1, "b": 2, "c": 3}', parsed: null, refusal: null, - tool_calls: [], + tool_calls: undefined, }, ]); expect(listener.functionCallResults).toEqual([`must be an object`, '3']); @@ -1442,7 +1442,7 @@ describe('resource completions', () => { content: "it's raining", parsed: null, refusal: null, - tool_calls: [], + tool_calls: undefined, }, ]); expect(listener.functionCallResults).toEqual([ @@ -1571,7 +1571,7 @@ describe('resource completions', () => { content: "it's raining", parsed: null, refusal: null, - tool_calls: [], + tool_calls: undefined, }, ]); expect(listener.eventFunctionCallResults).toEqual([`it's raining`]); @@ -1794,7 +1794,7 @@ describe('resource completions', () => { content: 'there are 3 properties in {"a": 1, "b": 2, "c": 3}', parsed: null, refusal: null, - tool_calls: [], + tool_calls: undefined, }, ]); expect(listener.eventFunctionCallResults).toEqual(['3']); @@ -1996,7 +1996,7 @@ describe('resource completions', () => { content: 'there are 3 properties in {"a": 1, "b": 2, "c": 3}', parsed: null, refusal: null, - tool_calls: [], + tool_calls: undefined, }, ]); expect(listener.eventFunctionCallResults).toEqual([`must be an object`, '3']); @@ -2300,7 +2300,7 @@ describe('resource completions', () => { content: "it's raining", parsed: null, refusal: null, - tool_calls: [], + tool_calls: undefined, }, ]); expect(listener.eventFunctionCallResults).toEqual([ @@ -2346,7 +2346,7 @@ describe('resource completions', () => { content: 'The weather is great today!', parsed: null, refusal: null, - tool_calls: [], + tool_calls: undefined, }); await listener.sanityCheck(); }); @@ -2385,7 +2385,7 @@ describe('resource completions', () => { content: 'The weather is great today!', parsed: null, refusal: null, - tool_calls: [], + tool_calls: undefined, }); await listener.sanityCheck(); }); diff --git a/tests/lib/ChatCompletionStream.test.ts b/tests/lib/ChatCompletionStream.test.ts index e5ef20c9e..34c5fd204 100644 --- a/tests/lib/ChatCompletionStream.test.ts +++ b/tests/lib/ChatCompletionStream.test.ts @@ -39,7 +39,6 @@ describe('.stream()', () => { }, "refusal": null, "role": "assistant", - "tool_calls": [], }, } `); @@ -198,7 +197,6 @@ describe('.stream()', () => { }, "refusal": null, "role": "assistant", - "tool_calls": [], }, } `); @@ -386,7 +384,6 @@ describe('.stream()', () => { "parsed": null, "refusal": "I'm very sorry, but I can't assist with that request.", "role": "assistant", - "tool_calls": [], }, } `); diff --git a/tests/lib/azure.test.ts b/tests/lib/azure.test.ts index 220a448df..1b9b2546c 100644 --- a/tests/lib/azure.test.ts +++ b/tests/lib/azure.test.ts @@ -1,6 +1,7 @@ import { AzureOpenAI } from 'openai'; import { APIUserAbortError } from 'openai'; import { type Response, RequestInit, RequestInfo } from 'openai/internal/builtin-types'; +import { File } from 'node:buffer'; const defaultFetch = fetch; @@ -376,7 +377,7 @@ describe('azure request building', () => { expect( await client.audio.translations.create({ model, - file: { url: 'https://example.com', blob: () => 0 as any }, + file: new File([], ''), }), ).toMatchObject({ url: `https://example.com/openai/deployments/${deployment}/audio/translations?api-version=${apiVersion}`, @@ -387,7 +388,7 @@ describe('azure request building', () => { expect( await client.audio.transcriptions.create({ model, - file: { url: 'https://example.com', blob: () => 0 as any }, + file: new File([], ''), }), ).toMatchObject({ url: `https://example.com/openai/deployments/${deployment}/audio/transcriptions?api-version=${apiVersion}`, @@ -432,7 +433,7 @@ describe('azure request building', () => { test('handles files', async () => { expect( await client.files.create({ - file: { url: 'https://example.com', blob: () => 0 as any }, + file: new File([], ''), purpose: 'assistants', }), ).toMatchObject({ @@ -505,25 +506,25 @@ describe('azure request building', () => { }); }); - test('Audio translations is not handled', async () => { + test('handles audio translations', async () => { expect( await client.audio.translations.create({ model: deployment, - file: { url: 'https://example.com', blob: () => 0 as any }, + file: new File([], ''), }), ).toMatchObject({ - url: `https://example.com/openai/audio/translations?api-version=${apiVersion}`, + url: `https://example.com/openai/deployments/${deployment}/audio/translations?api-version=${apiVersion}`, }); }); - test('Audio transcriptions is not handled', async () => { + test('handles audio transcriptions', async () => { expect( await client.audio.transcriptions.create({ model: deployment, - file: { url: 'https://example.com', blob: () => 0 as any }, + file: new File([], ''), }), ).toMatchObject({ - url: `https://example.com/openai/audio/transcriptions?api-version=${apiVersion}`, + url: `https://example.com/openai/deployments/${deployment}/audio/transcriptions?api-version=${apiVersion}`, }); }); @@ -565,7 +566,7 @@ describe('azure request building', () => { test('handles files', async () => { expect( await client.files.create({ - file: { url: 'https://example.com', blob: () => 0 as any }, + file: new File([], ''), purpose: 'assistants', }), ).toMatchObject({ diff --git a/tests/lib/parser.test.ts b/tests/lib/parser.test.ts index b220e92d3..fa8123f5c 100644 --- a/tests/lib/parser.test.ts +++ b/tests/lib/parser.test.ts @@ -39,7 +39,6 @@ describe('.parse()', () => { }, "refusal": null, "role": "assistant", - "tool_calls": [], }, } `); @@ -154,7 +153,6 @@ describe('.parse()', () => { }, "refusal": null, "role": "assistant", - "tool_calls": [], } `); @@ -488,7 +486,6 @@ describe('.parse()', () => { }, "refusal": null, "role": "assistant", - "tool_calls": [], } `); }); @@ -787,7 +784,6 @@ describe('.parse()', () => { }, "refusal": null, "role": "assistant", - "tool_calls": [], } `); }); @@ -947,7 +943,6 @@ describe('.parse()', () => { }, "refusal": null, "role": "assistant", - "tool_calls": [], } `); }); @@ -1061,7 +1056,6 @@ describe('.parse()', () => { }, "refusal": null, "role": "assistant", - "tool_calls": [], } `); }); diff --git a/tests/path.test.ts b/tests/path.test.ts new file mode 100644 index 000000000..832e4a676 --- /dev/null +++ b/tests/path.test.ts @@ -0,0 +1,318 @@ +import { createPathTagFunction, encodeURIPath } from 'openai/internal/utils/path'; +import { inspect } from 'node:util'; + +describe('path template tag function', () => { + test('validates input', () => { + const testParams = ['', '.', '..', 'x', '%2e', '%2E', '%2e%2e', '%2E%2e', '%2e%2E', '%2E%2E']; + const testCases = [ + ['/path_params/', '/a'], + ['/path_params/', '/'], + ['/path_params/', ''], + ['', '/a'], + ['', '/'], + ['', ''], + ['a'], + [''], + ['/path_params/', ':initiate'], + ['/path_params/', '.json'], + ['/path_params/', '?beta=true'], + ['/path_params/', '.?beta=true'], + ['/path_params/', '/', '/download'], + ['/path_params/', '-', '/download'], + ['/path_params/', '', '/download'], + ['/path_params/', '.', '/download'], + ['/path_params/', '..', '/download'], + ['/plain/path'], + ]; + + function paramPermutations(len: number): string[][] { + if (len === 0) return []; + if (len === 1) return testParams.map((e) => [e]); + const rest = paramPermutations(len - 1); + return testParams.flatMap((e) => rest.map((r) => [e, ...r])); + } + + // we need to test how %2E is handled so we use a custom encoder that does no escaping + const rawPath = createPathTagFunction((s) => s); + + const results: { + [pathParts: string]: { + [params: string]: { valid: boolean; result?: string; error?: string }; + }; + } = {}; + + for (const pathParts of testCases) { + const pathResults: Record = {}; + results[JSON.stringify(pathParts)] = pathResults; + for (const params of paramPermutations(pathParts.length - 1)) { + const stringRaw = String.raw({ raw: pathParts }, ...params); + const plainString = String.raw( + { raw: pathParts.map((e) => e.replace(/\./g, 'x')) }, + ...params.map((e) => 'X'.repeat(e.length)), + ); + const normalizedStringRaw = new URL(stringRaw, 'https://example.com').href; + const normalizedPlainString = new URL(plainString, 'https://example.com').href; + const pathResultsKey = JSON.stringify(params); + try { + const result = rawPath(pathParts, ...params); + expect(result).toBe(stringRaw); + // there are no special segments, so the length of the normalized path is + // equal to the length of the normalized plain path. + expect(normalizedStringRaw.length).toBe(normalizedPlainString.length); + pathResults[pathResultsKey] = { + valid: true, + result, + }; + } catch (e) { + const error = String(e); + expect(error).toMatch(/Path parameters result in path with invalid segment/); + // there are special segments, so the length of the normalized path is + // different than the length of the normalized plain path. + expect(normalizedStringRaw.length).not.toBe(normalizedPlainString.length); + pathResults[pathResultsKey] = { + valid: false, + error, + }; + } + } + } + + expect(results).toMatchObject({ + '["/path_params/","/a"]': { + '["x"]': { valid: true, result: '/path_params/x/a' }, + '[""]': { valid: true, result: '/path_params//a' }, + '["%2E%2e"]': { + valid: false, + error: + 'Error: Path parameters result in path with invalid segments:\n' + + '/path_params/%2E%2e/a\n' + + ' ^^^^^^', + }, + '["%2E"]': { + valid: false, + error: + 'Error: Path parameters result in path with invalid segments:\n' + + '/path_params/%2E/a\n' + + ' ^^^', + }, + }, + '["/path_params/","/"]': { + '["x"]': { valid: true, result: '/path_params/x/' }, + '[""]': { valid: true, result: '/path_params//' }, + '["%2e%2E"]': { + valid: false, + error: + 'Error: Path parameters result in path with invalid segments:\n' + + '/path_params/%2e%2E/\n' + + ' ^^^^^^', + }, + '["%2e"]': { + valid: false, + error: + 'Error: Path parameters result in path with invalid segments:\n' + + '/path_params/%2e/\n' + + ' ^^^', + }, + }, + '["/path_params/",""]': { + '[""]': { valid: true, result: '/path_params/' }, + '["x"]': { valid: true, result: '/path_params/x' }, + '["%2E"]': { + valid: false, + error: + 'Error: Path parameters result in path with invalid segments:\n' + + '/path_params/%2E\n' + + ' ^^^', + }, + '["%2E%2e"]': { + valid: false, + error: + 'Error: Path parameters result in path with invalid segments:\n' + + '/path_params/%2E%2e\n' + + ' ^^^^^^', + }, + }, + '["","/a"]': { + '[""]': { valid: true, result: '/a' }, + '["x"]': { valid: true, result: 'x/a' }, + '["%2E"]': { + valid: false, + error: 'Error: Path parameters result in path with invalid segments:\n%2E/a\n^^^', + }, + '["%2e%2E"]': { + valid: false, + error: 'Error: Path parameters result in path with invalid segments:\n' + '%2e%2E/a\n' + '^^^^^^', + }, + }, + '["","/"]': { + '["x"]': { valid: true, result: 'x/' }, + '[""]': { valid: true, result: '/' }, + '["%2E%2e"]': { + valid: false, + error: 'Error: Path parameters result in path with invalid segments:\n' + '%2E%2e/\n' + '^^^^^^', + }, + '["."]': { + valid: false, + error: 'Error: Path parameters result in path with invalid segments:\n./\n^', + }, + }, + '["",""]': { + '[""]': { valid: true, result: '' }, + '["x"]': { valid: true, result: 'x' }, + '[".."]': { + valid: false, + error: 'Error: Path parameters result in path with invalid segments:\n..\n^^', + }, + '["."]': { + valid: false, + error: 'Error: Path parameters result in path with invalid segments:\n.\n^', + }, + }, + '["a"]': {}, + '[""]': {}, + '["/path_params/",":initiate"]': { + '[""]': { valid: true, result: '/path_params/:initiate' }, + '["."]': { valid: true, result: '/path_params/.:initiate' }, + }, + '["/path_params/",".json"]': { + '["x"]': { valid: true, result: '/path_params/x.json' }, + '["."]': { valid: true, result: '/path_params/..json' }, + }, + '["/path_params/","?beta=true"]': { + '["x"]': { valid: true, result: '/path_params/x?beta=true' }, + '[""]': { valid: true, result: '/path_params/?beta=true' }, + '["%2E%2E"]': { + valid: false, + error: + 'Error: Path parameters result in path with invalid segments:\n' + + '/path_params/%2E%2E?beta=true\n' + + ' ^^^^^^', + }, + '["%2e%2E"]': { + valid: false, + error: + 'Error: Path parameters result in path with invalid segments:\n' + + '/path_params/%2e%2E?beta=true\n' + + ' ^^^^^^', + }, + }, + '["/path_params/",".?beta=true"]': { + '[".."]': { valid: true, result: '/path_params/...?beta=true' }, + '["x"]': { valid: true, result: '/path_params/x.?beta=true' }, + '[""]': { + valid: false, + error: + 'Error: Path parameters result in path with invalid segments:\n' + + '/path_params/.?beta=true\n' + + ' ^', + }, + '["%2e"]': { + valid: false, + error: + 'Error: Path parameters result in path with invalid segments:\n' + + '/path_params/%2e.?beta=true\n' + + ' ^^^^', + }, + }, + '["/path_params/","/","/download"]': { + '["",""]': { valid: true, result: '/path_params///download' }, + '["","x"]': { valid: true, result: '/path_params//x/download' }, + '[".","%2e"]': { + valid: false, + error: + 'Error: Path parameters result in path with invalid segments:\n' + + '/path_params/./%2e/download\n' + + ' ^ ^^^', + }, + '["%2E%2e","%2e"]': { + valid: false, + error: + 'Error: Path parameters result in path with invalid segments:\n' + + '/path_params/%2E%2e/%2e/download\n' + + ' ^^^^^^ ^^^', + }, + }, + '["/path_params/","-","/download"]': { + '["","%2e"]': { valid: true, result: '/path_params/-%2e/download' }, + '["%2E",".."]': { valid: true, result: '/path_params/%2E-../download' }, + }, + '["/path_params/","","/download"]': { + '["%2E%2e","%2e%2E"]': { valid: true, result: '/path_params/%2E%2e%2e%2E/download' }, + '["%2E",".."]': { valid: true, result: '/path_params/%2E../download' }, + '["","%2E"]': { + valid: false, + error: + 'Error: Path parameters result in path with invalid segments:\n' + + '/path_params/%2E/download\n' + + ' ^^^', + }, + '["%2E","."]': { + valid: false, + error: + 'Error: Path parameters result in path with invalid segments:\n' + + '/path_params/%2E./download\n' + + ' ^^^^', + }, + }, + '["/path_params/",".","/download"]': { + '["%2e%2e",""]': { valid: true, result: '/path_params/%2e%2e./download' }, + '["","%2e%2e"]': { valid: true, result: '/path_params/.%2e%2e/download' }, + '["",""]': { + valid: false, + error: + 'Error: Path parameters result in path with invalid segments:\n' + + '/path_params/./download\n' + + ' ^', + }, + '["","."]': { + valid: false, + error: + 'Error: Path parameters result in path with invalid segments:\n' + + '/path_params/../download\n' + + ' ^^', + }, + }, + '["/path_params/","..","/download"]': { + '["","%2E"]': { valid: true, result: '/path_params/..%2E/download' }, + '["","x"]': { valid: true, result: '/path_params/..x/download' }, + '["",""]': { + valid: false, + error: + 'Error: Path parameters result in path with invalid segments:\n' + + '/path_params/../download\n' + + ' ^^', + }, + }, + }); + }); +}); + +describe('encodeURIPath', () => { + const testCases: string[] = [ + '', + // Every ASCII character + ...Array.from({ length: 0x7f }, (_, i) => String.fromCharCode(i)), + // Unicode BMP codepoint + 'å', + // Unicode supplementary codepoint + '😃', + ]; + + for (const param of testCases) { + test('properly encodes ' + inspect(param), () => { + const encoded = encodeURIPath(param); + const naiveEncoded = encodeURIComponent(param); + // we should never encode more characters than encodeURIComponent + expect(naiveEncoded.length).toBeGreaterThanOrEqual(encoded.length); + expect(decodeURIComponent(encoded)).toBe(param); + }); + } + + test("leaves ':' intact", () => { + expect(encodeURIPath(':')).toBe(':'); + }); + + test("leaves '@' intact", () => { + expect(encodeURIPath('@')).toBe('@'); + }); +}); diff --git a/tests/qs/utils.test.ts b/tests/qs/utils.test.ts index 3df95e5bd..f97b125ea 100644 --- a/tests/qs/utils.test.ts +++ b/tests/qs/utils.test.ts @@ -66,7 +66,7 @@ describe('merge()', function () { // st.equal(getCount, 1); expect(setCount).toEqual(0); expect(getCount).toEqual(1); - observed[0] = observed[0]; // eslint-disable-line no-self-assign + observed[0] = observed[0]; // st.equal(setCount, 1); // st.equal(getCount, 2); expect(setCount).toEqual(1); diff --git a/tests/responses.test.ts b/tests/responses.test.ts index f24937cc0..b8a3db906 100644 --- a/tests/responses.test.ts +++ b/tests/responses.test.ts @@ -2,6 +2,8 @@ import { APIPromise } from 'openai/api-promise'; import OpenAI from 'openai/index'; import { compareType } from './utils/typing'; +const client = new OpenAI({ apiKey: '...' }); + describe('request id', () => { test('types', () => { compareType>, string>(true); @@ -54,6 +56,7 @@ describe('request id', () => { test('envelope response', async () => { const promise = new APIPromise<{ data: { foo: string } }>( + client, (async () => { return { response: new Response(JSON.stringify({ data: { foo: 'bar' } }), { @@ -61,6 +64,9 @@ describe('request id', () => { }), controller: {} as any, options: {} as any, + requestLogID: 'log_...', + retryOfRequestLogID: undefined, + startTime: Date.now(), }; })(), )._thenUnwrap((d) => d.data); @@ -86,6 +92,7 @@ describe('request id', () => { test('array response', async () => { const promise = new APIPromise>( + client, (async () => { return { response: new Response(JSON.stringify([{ foo: 'bar' }]), { @@ -93,6 +100,9 @@ describe('request id', () => { }), controller: {} as any, options: {} as any, + requestLogID: 'log_...', + retryOfRequestLogID: undefined, + startTime: Date.now(), }; })(), ); @@ -105,6 +115,7 @@ describe('request id', () => { test('string response', async () => { const promise = new APIPromise( + client, (async () => { return { response: new Response('hello world', { @@ -112,6 +123,9 @@ describe('request id', () => { }), controller: {} as any, options: {} as any, + requestLogID: 'log_...', + retryOfRequestLogID: undefined, + startTime: Date.now(), }; })(), ); diff --git a/tests/streaming.test.ts b/tests/streaming.test.ts index 1bbdc861d..857cf4620 100644 --- a/tests/streaming.test.ts +++ b/tests/streaming.test.ts @@ -1,34 +1,6 @@ -import { PassThrough } from 'stream'; import assert from 'assert'; -import { _iterSSEMessages, _decodeChunks as decodeChunks } from 'openai/streaming'; - -describe('line decoder', () => { - test('basic', () => { - // baz is not included because the line hasn't ended yet - expect(decodeChunks(['foo', ' bar\nbaz'])).toEqual(['foo bar']); - }); - - test('basic with \\r', () => { - // baz is not included because the line hasn't ended yet - expect(decodeChunks(['foo', ' bar\r\nbaz'])).toEqual(['foo bar']); - }); - - test('trailing new lines', () => { - expect(decodeChunks(['foo', ' bar', 'baz\n', 'thing\n'])).toEqual(['foo barbaz', 'thing']); - }); - - test('trailing new lines with \\r', () => { - expect(decodeChunks(['foo', ' bar', 'baz\r\n', 'thing\r\n'])).toEqual(['foo barbaz', 'thing']); - }); - - test('escaped new lines', () => { - expect(decodeChunks(['foo', ' bar\\nbaz\n'])).toEqual(['foo bar\\nbaz']); - }); - - test('escaped new lines with \\r', () => { - expect(decodeChunks(['foo', ' bar\\r\\nbaz\n'])).toEqual(['foo bar\\r\\nbaz']); - }); -}); +import { _iterSSEMessages } from 'openai/streaming'; +import { ReadableStreamFrom } from 'openai/internal/shims'; describe('streaming decoding', () => { test('basic', async () => { @@ -38,7 +10,7 @@ describe('streaming decoding', () => { yield Buffer.from('\n'); } - const stream = _iterSSEMessages(new Response(await iteratorToStream(body())), new AbortController())[ + const stream = _iterSSEMessages(new Response(ReadableStreamFrom(body())), new AbortController())[ Symbol.asyncIterator ](); @@ -56,7 +28,7 @@ describe('streaming decoding', () => { yield Buffer.from('\n'); } - const stream = _iterSSEMessages(new Response(await iteratorToStream(body())), new AbortController())[ + const stream = _iterSSEMessages(new Response(ReadableStreamFrom(body())), new AbortController())[ Symbol.asyncIterator ](); @@ -75,7 +47,7 @@ describe('streaming decoding', () => { yield Buffer.from('\n'); } - const stream = _iterSSEMessages(new Response(await iteratorToStream(body())), new AbortController())[ + const stream = _iterSSEMessages(new Response(ReadableStreamFrom(body())), new AbortController())[ Symbol.asyncIterator ](); @@ -96,7 +68,7 @@ describe('streaming decoding', () => { yield Buffer.from('\n'); } - const stream = _iterSSEMessages(new Response(await iteratorToStream(body())), new AbortController())[ + const stream = _iterSSEMessages(new Response(ReadableStreamFrom(body())), new AbortController())[ Symbol.asyncIterator ](); @@ -124,7 +96,7 @@ describe('streaming decoding', () => { yield Buffer.from('\n'); } - const stream = _iterSSEMessages(new Response(await iteratorToStream(body())), new AbortController())[ + const stream = _iterSSEMessages(new Response(ReadableStreamFrom(body())), new AbortController())[ Symbol.asyncIterator ](); @@ -153,7 +125,7 @@ describe('streaming decoding', () => { yield Buffer.from('\n\n'); } - const stream = _iterSSEMessages(new Response(await iteratorToStream(body())), new AbortController())[ + const stream = _iterSSEMessages(new Response(ReadableStreamFrom(body())), new AbortController())[ Symbol.asyncIterator ](); @@ -174,7 +146,7 @@ describe('streaming decoding', () => { yield Buffer.from('\n\n'); } - const stream = _iterSSEMessages(new Response(await iteratorToStream(body())), new AbortController())[ + const stream = _iterSSEMessages(new Response(ReadableStreamFrom(body())), new AbortController())[ Symbol.asyncIterator ](); @@ -199,7 +171,7 @@ describe('streaming decoding', () => { yield Buffer.from('\n'); } - const stream = _iterSSEMessages(new Response(await iteratorToStream(body())), new AbortController())[ + const stream = _iterSSEMessages(new Response(ReadableStreamFrom(body())), new AbortController())[ Symbol.asyncIterator ](); @@ -232,7 +204,7 @@ describe('streaming decoding', () => { yield Buffer.from('\n'); } - const stream = _iterSSEMessages(new Response(await iteratorToStream(body())), new AbortController())[ + const stream = _iterSSEMessages(new Response(ReadableStreamFrom(body())), new AbortController())[ Symbol.asyncIterator ](); @@ -245,27 +217,3 @@ describe('streaming decoding', () => { expect(event.done).toBeTruthy(); }); }); - -async function iteratorToStream(iterator: AsyncGenerator): Promise { - const parts: unknown[] = []; - - for await (const chunk of iterator) { - parts.push(chunk); - } - - let index = 0; - - const stream = new PassThrough({ - read() { - const value = parts[index]; - if (value === undefined) { - stream.end(); - } else { - index += 1; - stream.write(value); - } - }, - }); - - return stream; -} diff --git a/tests/uploads.test.ts b/tests/uploads.test.ts index 16456daf6..508fce58f 100644 --- a/tests/uploads.test.ts +++ b/tests/uploads.test.ts @@ -1,5 +1,7 @@ import fs from 'fs'; -import { toFile, type ResponseLike } from 'openai/uploads'; +import type { ResponseLike } from 'openai/internal/to-file'; +import { toFile } from 'openai/uploads'; +import { File } from 'node:buffer'; class MyClass { name: string = 'foo'; @@ -8,7 +10,7 @@ class MyClass { function mockResponse({ url, content }: { url: string; content?: Blob }): ResponseLike { return { url, - blob: async () => content as any, + blob: async () => content || new Blob([]), }; } @@ -61,15 +63,45 @@ describe('toFile', () => { expect(file.name).toEqual('input.jsonl'); expect(file.type).toBe('jsonl'); }); + + it('is assignable to File and Blob', async () => { + const input = new File(['foo'], 'input.jsonl', { type: 'jsonl' }); + const result = await toFile(input); + const file: File = result; + const blob: Blob = result; + void file, blob; + }); }); -test('missing File error message', async () => { - // @ts-ignore - globalThis.File = undefined; +describe('missing File error message', () => { + let prevGlobalFile: unknown; + let prevNodeFile: unknown; + beforeEach(() => { + // The file shim captures the global File object when it's first imported. + // Reset modules before each test so we can test the error thrown when it's undefined. + jest.resetModules(); + const buffer = require('node:buffer'); + // @ts-ignore + prevGlobalFile = globalThis.File; + prevNodeFile = buffer.File; + // @ts-ignore + globalThis.File = undefined; + buffer.File = undefined; + }); + afterEach(() => { + // Clean up + // @ts-ignore + globalThis.File = prevGlobalFile; + require('node:buffer').File = prevNodeFile; + jest.resetModules(); + }); - await expect( - toFile(mockResponse({ url: 'https://example.com/my/audio.mp3' })), - ).rejects.toMatchInlineSnapshot( - `[Error: \`File\` is not defined as a global which is required for file uploads]`, - ); + test('is thrown', async () => { + const uploads = await import('openai/uploads'); + await expect( + uploads.toFile(mockResponse({ url: 'https://example.com/my/audio.mp3' })), + ).rejects.toMatchInlineSnapshot( + `[Error: \`File\` is not defined as a global which is required for file uploads]`, + ); + }); }); diff --git a/tsconfig.dist-src.json b/tsconfig.dist-src.json index 0f6aba91b..c550e2996 100644 --- a/tsconfig.dist-src.json +++ b/tsconfig.dist-src.json @@ -4,8 +4,8 @@ // via declaration maps "include": ["index.ts"], "compilerOptions": { - "target": "es2015", - "lib": ["DOM", "DOM.Iterable"], + "target": "ES2015", + "lib": ["DOM", "DOM.Iterable", "ES2018"], "moduleResolution": "node" } } diff --git a/yarn.lock b/yarn.lock index de41e5502..b8808db07 100644 --- a/yarn.lock +++ b/yarn.lock @@ -357,54 +357,94 @@ dependencies: eslint-visitor-keys "^3.3.0" -"@eslint-community/regexpp@^4.5.1": - version "4.11.1" - resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.11.1.tgz#a547badfc719eb3e5f4b556325e542fbe9d7a18f" - integrity sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q== +"@eslint-community/regexpp@^4.10.0", "@eslint-community/regexpp@^4.12.1": + version "4.12.1" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.1.tgz#cfc6cffe39df390a3841cde2abccf92eaa7ae0e0" + integrity sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ== -"@eslint-community/regexpp@^4.6.1": - version "4.6.2" - resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.6.2.tgz#1816b5f6948029c5eaacb0703b850ee0cb37d8f8" - integrity sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw== +"@eslint/config-array@^0.19.0": + version "0.19.2" + resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.19.2.tgz#3060b809e111abfc97adb0bb1172778b90cb46aa" + integrity sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w== + dependencies: + "@eslint/object-schema" "^2.1.6" + debug "^4.3.1" + minimatch "^3.1.2" -"@eslint/eslintrc@^2.1.2": - version "2.1.2" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.2.tgz#c6936b4b328c64496692f76944e755738be62396" - integrity sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g== +"@eslint/core@^0.10.0": + version "0.10.0" + resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.10.0.tgz#23727063c21b335f752dbb3a16450f6f9cbc9091" + integrity sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw== + dependencies: + "@types/json-schema" "^7.0.15" + +"@eslint/core@^0.11.0": + version "0.11.0" + resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.11.0.tgz#7a9226e850922e42cbd2ba71361eacbe74352a12" + integrity sha512-DWUB2pksgNEb6Bz2fggIy1wh6fGgZP4Xyy/Mt0QZPiloKKXerbqq9D3SBQTlCRYOrcRPu4vuz+CGjwdfqxnoWA== + dependencies: + "@types/json-schema" "^7.0.15" + +"@eslint/eslintrc@^3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.2.0.tgz#57470ac4e2e283a6bf76044d63281196e370542c" + integrity sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w== dependencies: ajv "^6.12.4" debug "^4.3.2" - espree "^9.6.0" - globals "^13.19.0" + espree "^10.0.1" + globals "^14.0.0" ignore "^5.2.0" import-fresh "^3.2.1" js-yaml "^4.1.0" minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@8.50.0": - version "8.50.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.50.0.tgz#9e93b850f0f3fa35f5fa59adfd03adae8488e484" - integrity sha512-NCC3zz2+nvYd+Ckfh87rA47zfu2QsQpvc6k1yzTk+b9KzRj0wkGa8LSoGOXN6Zv4lRf/EIoZ80biDh9HOI+RNQ== +"@eslint/js@9.20.0": + version "9.20.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.20.0.tgz#7421bcbe74889fcd65d1be59f00130c289856eb4" + integrity sha512-iZA07H9io9Wn836aVTytRaNqh00Sad+EamwOVJT12GTLw1VGMFV/4JaME+JjLtr9fiGaoWgYnS54wrfWsSs4oQ== + +"@eslint/object-schema@^2.1.6": + version "2.1.6" + resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-2.1.6.tgz#58369ab5b5b3ca117880c0f6c0b0f32f6950f24f" + integrity sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA== -"@humanwhocodes/config-array@^0.11.11": - version "0.11.11" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.11.tgz#88a04c570dbbc7dd943e4712429c3df09bc32844" - integrity sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA== +"@eslint/plugin-kit@^0.2.5": + version "0.2.5" + resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.2.5.tgz#ee07372035539e7847ef834e3f5e7b79f09e3a81" + integrity sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A== dependencies: - "@humanwhocodes/object-schema" "^1.2.1" - debug "^4.1.1" - minimatch "^3.0.5" + "@eslint/core" "^0.10.0" + levn "^0.4.1" + +"@humanfs/core@^0.19.1": + version "0.19.1" + resolved "https://registry.yarnpkg.com/@humanfs/core/-/core-0.19.1.tgz#17c55ca7d426733fe3c561906b8173c336b40a77" + integrity sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA== + +"@humanfs/node@^0.16.6": + version "0.16.6" + resolved "https://registry.yarnpkg.com/@humanfs/node/-/node-0.16.6.tgz#ee2a10eaabd1131987bf0488fd9b820174cd765e" + integrity sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw== + dependencies: + "@humanfs/core" "^0.19.1" + "@humanwhocodes/retry" "^0.3.0" "@humanwhocodes/module-importer@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== -"@humanwhocodes/object-schema@^1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" - integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== +"@humanwhocodes/retry@^0.3.0": + version "0.3.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.3.1.tgz#c72a5c76a9fbaf3488e231b13dc52c0da7bab42a" + integrity sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA== + +"@humanwhocodes/retry@^0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.4.1.tgz#9a96ce501bc62df46c4031fbd970e3cc6b10f07b" + integrity sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA== "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" @@ -666,7 +706,7 @@ resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== -"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": +"@nodelib/fs.walk@^1.2.3": version "1.2.8" resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== @@ -674,17 +714,10 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@pkgr/utils@^2.4.2": - version "2.4.2" - resolved "https://registry.yarnpkg.com/@pkgr/utils/-/utils-2.4.2.tgz#9e638bbe9a6a6f165580dc943f138fd3309a2cbc" - integrity sha512-POgTXhjrTfbTV63DiFXav4lBHiICLKKwDeaKn9Nphwj7WH6m0hMMCaJkMyRWjgtPFyRKRVoMXXjczsTQRDEhYw== - dependencies: - cross-spawn "^7.0.3" - fast-glob "^3.3.0" - is-glob "^4.0.3" - open "^9.1.0" - picocolors "^1.0.0" - tslib "^2.6.0" +"@pkgr/core@^0.1.0": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.1.1.tgz#1ec17e2edbec25c8306d424ecfbf13c7de1aaa31" + integrity sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA== "@sinclair/typebox@^0.27.8": version "0.27.8" @@ -853,6 +886,11 @@ dependencies: "@babel/types" "^7.20.7" +"@types/estree@^1.0.6": + version "1.0.6" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50" + integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw== + "@types/graceful-fs@^4.1.3": version "4.1.9" resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.9.tgz#2a06bc0f68a20ab37b3e36aa238be6abdf49e8b4" @@ -887,7 +925,7 @@ expect "^29.0.0" pretty-format "^29.0.0" -"@types/json-schema@^7.0.12": +"@types/json-schema@^7.0.15": version "7.0.15" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== @@ -906,16 +944,18 @@ dependencies: undici-types "~6.19.2" -"@types/semver@^7.5.0": - version "7.5.8" - resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e" - integrity sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ== - "@types/stack-utils@^2.0.0": version "2.0.3" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8" integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw== +"@types/ws@^8.5.13": + version "8.5.14" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.14.tgz#93d44b268c9127d96026cf44353725dd9b6c3c21" + integrity sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw== + dependencies: + "@types/node" "*" + "@types/yargs-parser@*": version "21.0.3" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.3.tgz#815e30b786d2e8f0dcd85fd5bcf5e1a04d008f15" @@ -928,91 +968,86 @@ dependencies: "@types/yargs-parser" "*" -"@typescript-eslint/eslint-plugin@^6.7.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz#30830c1ca81fd5f3c2714e524c4303e0194f9cd3" - integrity sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA== +"@typescript-eslint/eslint-plugin@8.24.0", "@typescript-eslint/eslint-plugin@^8.24.0": + version "8.24.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.24.0.tgz#574a95d67660a1e4544ae131d672867a5b40abb3" + integrity sha512-aFcXEJJCI4gUdXgoo/j9udUYIHgF23MFkg09LFz2dzEmU0+1Plk4rQWv/IYKvPHAtlkkGoB3m5e6oUp+JPsNaQ== dependencies: - "@eslint-community/regexpp" "^4.5.1" - "@typescript-eslint/scope-manager" "6.21.0" - "@typescript-eslint/type-utils" "6.21.0" - "@typescript-eslint/utils" "6.21.0" - "@typescript-eslint/visitor-keys" "6.21.0" - debug "^4.3.4" + "@eslint-community/regexpp" "^4.10.0" + "@typescript-eslint/scope-manager" "8.24.0" + "@typescript-eslint/type-utils" "8.24.0" + "@typescript-eslint/utils" "8.24.0" + "@typescript-eslint/visitor-keys" "8.24.0" graphemer "^1.4.0" - ignore "^5.2.4" + ignore "^5.3.1" natural-compare "^1.4.0" - semver "^7.5.4" - ts-api-utils "^1.0.1" + ts-api-utils "^2.0.1" -"@typescript-eslint/parser@^6.0.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.21.0.tgz#af8fcf66feee2edc86bc5d1cf45e33b0630bf35b" - integrity sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ== +"@typescript-eslint/parser@8.24.0", "@typescript-eslint/parser@^8.24.0": + version "8.24.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.24.0.tgz#bba837f9ee125b78f459ad947ff9b61be8139085" + integrity sha512-MFDaO9CYiard9j9VepMNa9MTcqVvSny2N4hkY6roquzj8pdCBRENhErrteaQuu7Yjn1ppk0v1/ZF9CG3KIlrTA== dependencies: - "@typescript-eslint/scope-manager" "6.21.0" - "@typescript-eslint/types" "6.21.0" - "@typescript-eslint/typescript-estree" "6.21.0" - "@typescript-eslint/visitor-keys" "6.21.0" + "@typescript-eslint/scope-manager" "8.24.0" + "@typescript-eslint/types" "8.24.0" + "@typescript-eslint/typescript-estree" "8.24.0" + "@typescript-eslint/visitor-keys" "8.24.0" debug "^4.3.4" -"@typescript-eslint/scope-manager@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz#ea8a9bfc8f1504a6ac5d59a6df308d3a0630a2b1" - integrity sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg== +"@typescript-eslint/scope-manager@8.24.0": + version "8.24.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.24.0.tgz#2e34b3eb2ce768f2ffb109474174ced5417002b1" + integrity sha512-HZIX0UByphEtdVBKaQBgTDdn9z16l4aTUz8e8zPQnyxwHBtf5vtl1L+OhH+m1FGV9DrRmoDuYKqzVrvWDcDozw== dependencies: - "@typescript-eslint/types" "6.21.0" - "@typescript-eslint/visitor-keys" "6.21.0" + "@typescript-eslint/types" "8.24.0" + "@typescript-eslint/visitor-keys" "8.24.0" -"@typescript-eslint/type-utils@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz#6473281cfed4dacabe8004e8521cee0bd9d4c01e" - integrity sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag== +"@typescript-eslint/type-utils@8.24.0": + version "8.24.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.24.0.tgz#6ee3ec4db06f9e5e7b01ca6c2b5dd5843a9fd1e8" + integrity sha512-8fitJudrnY8aq0F1wMiPM1UUgiXQRJ5i8tFjq9kGfRajU+dbPyOuHbl0qRopLEidy0MwqgTHDt6CnSeXanNIwA== dependencies: - "@typescript-eslint/typescript-estree" "6.21.0" - "@typescript-eslint/utils" "6.21.0" + "@typescript-eslint/typescript-estree" "8.24.0" + "@typescript-eslint/utils" "8.24.0" debug "^4.3.4" - ts-api-utils "^1.0.1" + ts-api-utils "^2.0.1" -"@typescript-eslint/types@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.21.0.tgz#205724c5123a8fef7ecd195075fa6e85bac3436d" - integrity sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg== +"@typescript-eslint/types@8.24.0": + version "8.24.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.24.0.tgz#694e7fb18d70506c317b816de9521300b0f72c8e" + integrity sha512-VacJCBTyje7HGAw7xp11q439A+zeGG0p0/p2zsZwpnMzjPB5WteaWqt4g2iysgGFafrqvyLWqq6ZPZAOCoefCw== -"@typescript-eslint/typescript-estree@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz#c47ae7901db3b8bddc3ecd73daff2d0895688c46" - integrity sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ== +"@typescript-eslint/typescript-estree@8.24.0": + version "8.24.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.24.0.tgz#0487349be174097bb329a58273100a9629e03c6c" + integrity sha512-ITjYcP0+8kbsvT9bysygfIfb+hBj6koDsu37JZG7xrCiy3fPJyNmfVtaGsgTUSEuTzcvME5YI5uyL5LD1EV5ZQ== dependencies: - "@typescript-eslint/types" "6.21.0" - "@typescript-eslint/visitor-keys" "6.21.0" + "@typescript-eslint/types" "8.24.0" + "@typescript-eslint/visitor-keys" "8.24.0" debug "^4.3.4" - globby "^11.1.0" + fast-glob "^3.3.2" is-glob "^4.0.3" - minimatch "9.0.3" - semver "^7.5.4" - ts-api-utils "^1.0.1" + minimatch "^9.0.4" + semver "^7.6.0" + ts-api-utils "^2.0.1" -"@typescript-eslint/utils@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.21.0.tgz#4714e7a6b39e773c1c8e97ec587f520840cd8134" - integrity sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ== +"@typescript-eslint/utils@8.24.0": + version "8.24.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.24.0.tgz#21cb1195ae79230af825bfeed59574f5cb70a749" + integrity sha512-07rLuUBElvvEb1ICnafYWr4hk8/U7X9RDCOqd9JcAMtjh/9oRmcfN4yGzbPVirgMR0+HLVHehmu19CWeh7fsmQ== dependencies: "@eslint-community/eslint-utils" "^4.4.0" - "@types/json-schema" "^7.0.12" - "@types/semver" "^7.5.0" - "@typescript-eslint/scope-manager" "6.21.0" - "@typescript-eslint/types" "6.21.0" - "@typescript-eslint/typescript-estree" "6.21.0" - semver "^7.5.4" + "@typescript-eslint/scope-manager" "8.24.0" + "@typescript-eslint/types" "8.24.0" + "@typescript-eslint/typescript-estree" "8.24.0" -"@typescript-eslint/visitor-keys@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz#87a99d077aa507e20e238b11d56cc26ade45fe47" - integrity sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A== +"@typescript-eslint/visitor-keys@8.24.0": + version "8.24.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.24.0.tgz#36ecf0b9b1d819ad88a3bd4157ab7d594cb797c9" + integrity sha512-kArLq83QxGLbuHrTMoOEWO+l2MwsNS2TGISEdx8xgqpkbytB07XmlQyQdNDrCc1ecSqx0cnmhGvpX+VBwqqSkg== dependencies: - "@typescript-eslint/types" "6.21.0" - eslint-visitor-keys "^3.4.1" + "@typescript-eslint/types" "8.24.0" + eslint-visitor-keys "^4.2.0" acorn-jsx@^5.3.2: version "5.3.2" @@ -1024,16 +1059,16 @@ acorn-walk@^8.1.1: resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== +acorn@^8.14.0: + version "8.14.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.0.tgz#063e2c70cac5fb4f6467f0b11152e04c682795b0" + integrity sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA== + acorn@^8.4.1: version "8.7.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.0.tgz#90951fde0f8f09df93549481e5fc141445b791cf" integrity sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ== -acorn@^8.9.0: - version "8.10.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" - integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== - aggregate-error@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" @@ -1125,11 +1160,6 @@ argparse@^2.0.1: resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== -array-union@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" - integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== - babel-jest@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.7.0.tgz#f4369919225b684c56085998ac63dbd05be020d5" @@ -1195,18 +1225,6 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== -big-integer@^1.6.44: - version "1.6.52" - resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.52.tgz#60a887f3047614a8e1bffe5d7173490a97dc8c85" - integrity sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg== - -bplist-parser@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/bplist-parser/-/bplist-parser-0.2.0.tgz#43a9d183e5bf9d545200ceac3e712f79ebbe8d0e" - integrity sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw== - dependencies: - big-integer "^1.6.44" - brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -1258,13 +1276,6 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== -bundle-name@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/bundle-name/-/bundle-name-3.0.0.tgz#ba59bcc9ac785fb67ccdbf104a2bf60c099f0e1a" - integrity sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw== - dependencies: - run-applescript "^5.0.0" - callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -1438,7 +1449,7 @@ create-require@^1.1.0: resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== -cross-spawn@^7.0.2, cross-spawn@^7.0.3: +cross-spawn@^7.0.3, cross-spawn@^7.0.6: version "7.0.6" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== @@ -1476,29 +1487,6 @@ deepmerge@^4.2.2: resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== -default-browser-id@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/default-browser-id/-/default-browser-id-3.0.0.tgz#bee7bbbef1f4e75d31f98f4d3f1556a14cea790c" - integrity sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA== - dependencies: - bplist-parser "^0.2.0" - untildify "^4.0.0" - -default-browser@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/default-browser/-/default-browser-4.0.0.tgz#53c9894f8810bf86696de117a6ce9085a3cbc7da" - integrity sha512-wX5pXO1+BrhMkSbROFsyxUm0i/cJEScyNhA4PPxc41ICuv05ZZB/MX28s8aZx6xjmatvebIapF6hLEKEcpneUA== - dependencies: - bundle-name "^3.0.0" - default-browser-id "^3.0.0" - execa "^7.1.1" - titleize "^3.0.0" - -define-lazy-prop@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz#dbb19adfb746d7fc6d734a06b72f4a00d021255f" - integrity sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg== - detect-newline@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" @@ -1514,20 +1502,6 @@ diff@^4.0.1: resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== -dir-glob@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" - integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== - dependencies: - path-type "^4.0.0" - -doctrine@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" - integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== - dependencies: - esutils "^2.0.2" - electron-to-chromium@^1.4.601: version "1.4.614" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.614.tgz#2fe789d61fa09cb875569f37c309d0c2701f91c0" @@ -1580,100 +1554,95 @@ escape-string-regexp@^4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== -eslint-plugin-prettier@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.0.1.tgz#a3b399f04378f79f066379f544e42d6b73f11515" - integrity sha512-m3u5RnR56asrwV/lDC4GHorlW75DsFfmUcjfCYylTUs85dBRnB7VM6xG8eCMJdeDRnppzmxZVf1GEPJvl1JmNg== +eslint-plugin-prettier@^5.2.3: + version "5.2.3" + resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.3.tgz#c4af01691a6fa9905207f0fbba0d7bea0902cce5" + integrity sha512-qJ+y0FfCp/mQYQ/vWQ3s7eUlFEL4PyKfAJxsnYTJ4YT73nsJBWqmEpFryxV9OeUiqmsTsYJ5Y+KDNaeP31wrRw== dependencies: prettier-linter-helpers "^1.0.0" - synckit "^0.8.5" + synckit "^0.9.1" -eslint-plugin-unused-imports@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-3.0.0.tgz#d25175b0072ff16a91892c3aa72a09ca3a9e69e7" - integrity sha512-sduiswLJfZHeeBJ+MQaG+xYzSWdRXoSw61DpU13mzWumCkR0ufD0HmO4kdNokjrkluMHpj/7PJeN35pgbhW3kw== - dependencies: - eslint-rule-composer "^0.3.0" +eslint-plugin-unused-imports@^4.1.4: + version "4.1.4" + resolved "https://registry.yarnpkg.com/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-4.1.4.tgz#62ddc7446ccbf9aa7b6f1f0b00a980423cda2738" + integrity sha512-YptD6IzQjDardkl0POxnnRBhU1OEePMV0nd6siHaRBbd+lyh6NAhFEobiznKU7kTsSsDeSD62Pe7kAM1b7dAZQ== -eslint-rule-composer@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz#79320c927b0c5c0d3d3d2b76c8b4a488f25bbaf9" - integrity sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg== - -eslint-scope@^7.2.2: - version "7.2.2" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f" - integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== +eslint-scope@^8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-8.2.0.tgz#377aa6f1cb5dc7592cfd0b7f892fd0cf352ce442" + integrity sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A== dependencies: esrecurse "^4.3.0" estraverse "^5.2.0" -eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: +eslint-visitor-keys@^3.3.0: version "3.4.3" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== -eslint@^8.49.0: - version "8.50.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.50.0.tgz#2ae6015fee0240fcd3f83e1e25df0287f487d6b2" - integrity sha512-FOnOGSuFuFLv/Sa+FDVRZl4GGVAAFFi8LecRsI5a1tMO5HIE8nCm4ivAlzt4dT3ol/PaaGC0rJEEXQmHJBGoOg== +eslint-visitor-keys@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz#687bacb2af884fcdda8a6e7d65c606f46a14cd45" + integrity sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw== + +eslint@^9.20.1: + version "9.20.1" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.20.1.tgz#923924c078f5226832449bac86662dd7e53c91d6" + integrity sha512-m1mM33o6dBUjxl2qb6wv6nGNwCAsns1eKtaQ4l/NPHeTvhiUPbtdfMyktxN4B3fgHIgsYh1VT3V9txblpQHq+g== dependencies: "@eslint-community/eslint-utils" "^4.2.0" - "@eslint-community/regexpp" "^4.6.1" - "@eslint/eslintrc" "^2.1.2" - "@eslint/js" "8.50.0" - "@humanwhocodes/config-array" "^0.11.11" + "@eslint-community/regexpp" "^4.12.1" + "@eslint/config-array" "^0.19.0" + "@eslint/core" "^0.11.0" + "@eslint/eslintrc" "^3.2.0" + "@eslint/js" "9.20.0" + "@eslint/plugin-kit" "^0.2.5" + "@humanfs/node" "^0.16.6" "@humanwhocodes/module-importer" "^1.0.1" - "@nodelib/fs.walk" "^1.2.8" + "@humanwhocodes/retry" "^0.4.1" + "@types/estree" "^1.0.6" + "@types/json-schema" "^7.0.15" ajv "^6.12.4" chalk "^4.0.0" - cross-spawn "^7.0.2" + cross-spawn "^7.0.6" debug "^4.3.2" - doctrine "^3.0.0" escape-string-regexp "^4.0.0" - eslint-scope "^7.2.2" - eslint-visitor-keys "^3.4.3" - espree "^9.6.1" - esquery "^1.4.2" + eslint-scope "^8.2.0" + eslint-visitor-keys "^4.2.0" + espree "^10.3.0" + esquery "^1.5.0" esutils "^2.0.2" fast-deep-equal "^3.1.3" - file-entry-cache "^6.0.1" + file-entry-cache "^8.0.0" find-up "^5.0.0" glob-parent "^6.0.2" - globals "^13.19.0" - graphemer "^1.4.0" ignore "^5.2.0" imurmurhash "^0.1.4" is-glob "^4.0.0" - is-path-inside "^3.0.3" - js-yaml "^4.1.0" json-stable-stringify-without-jsonify "^1.0.1" - levn "^0.4.1" lodash.merge "^4.6.2" minimatch "^3.1.2" natural-compare "^1.4.0" optionator "^0.9.3" - strip-ansi "^6.0.1" - text-table "^0.2.0" -espree@^9.6.0, espree@^9.6.1: - version "9.6.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" - integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== +espree@^10.0.1, espree@^10.3.0: + version "10.3.0" + resolved "https://registry.yarnpkg.com/espree/-/espree-10.3.0.tgz#29267cf5b0cb98735b65e64ba07e0ed49d1eed8a" + integrity sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg== dependencies: - acorn "^8.9.0" + acorn "^8.14.0" acorn-jsx "^5.3.2" - eslint-visitor-keys "^3.4.1" + eslint-visitor-keys "^4.2.0" esprima@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== -esquery@^1.4.2: - version "1.5.0" - resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.5.0.tgz#6ce17738de8577694edd7361c57182ac8cb0db0b" - integrity sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg== +esquery@^1.5.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7" + integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg== dependencies: estraverse "^5.1.0" @@ -1694,7 +1663,7 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== -execa@^5.0.0: +execa@^5.0.0, execa@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== @@ -1709,21 +1678,6 @@ execa@^5.0.0: signal-exit "^3.0.3" strip-final-newline "^2.0.0" -execa@^7.1.1: - version "7.2.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-7.2.0.tgz#657e75ba984f42a70f38928cedc87d6f2d4fe4e9" - integrity sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA== - dependencies: - cross-spawn "^7.0.3" - get-stream "^6.0.1" - human-signals "^4.3.0" - is-stream "^3.0.0" - merge-stream "^2.0.0" - npm-run-path "^5.1.0" - onetime "^6.0.0" - signal-exit "^3.0.7" - strip-final-newline "^3.0.0" - exit@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" @@ -1757,7 +1711,7 @@ fast-diff@^1.1.2: resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.3.0.tgz#ece407fa550a64d638536cd727e129c61616e0f0" integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw== -fast-glob@^3.2.9, fast-glob@^3.3.0, fast-glob@^3.3.2: +fast-glob@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== @@ -1797,12 +1751,12 @@ fflate@^0.8.2: resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.8.2.tgz#fc8631f5347812ad6028bbe4a2308b2792aa1dea" integrity sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A== -file-entry-cache@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" - integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== +file-entry-cache@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz#7787bddcf1131bffb92636c69457bbc0edd6d81f" + integrity sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ== dependencies: - flat-cache "^3.0.4" + flat-cache "^4.0.0" fill-range@^7.1.1: version "7.1.1" @@ -1827,18 +1781,18 @@ find-up@^5.0.0: locate-path "^6.0.0" path-exists "^4.0.0" -flat-cache@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" - integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== +flat-cache@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-4.0.1.tgz#0ece39fcb14ee012f4b0410bd33dd9c1f011127c" + integrity sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw== dependencies: - flatted "^3.1.0" - rimraf "^3.0.2" + flatted "^3.2.9" + keyv "^4.5.4" -flatted@^3.1.0: - version "3.2.7" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" - integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== +flatted@^3.2.9: + version "3.3.2" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.2.tgz#adba1448a9841bec72b42c532ea23dbbedef1a27" + integrity sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA== fs.realpath@^1.0.0: version "1.0.0" @@ -1875,7 +1829,7 @@ get-stdin@^8.0.0: resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-8.0.0.tgz#cbad6a73feb75f6eeb22ba9e01f89aa28aa97a53" integrity sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg== -get-stream@^6.0.0, get-stream@^6.0.1: +get-stream@^6.0.0: version "6.0.1" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== @@ -1922,24 +1876,10 @@ globals@^11.1.0: resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== -globals@^13.19.0: - version "13.20.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.20.0.tgz#ea276a1e508ffd4f1612888f9d1bad1e2717bf82" - integrity sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ== - dependencies: - type-fest "^0.20.2" - -globby@^11.1.0: - version "11.1.0" - resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" - integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== - dependencies: - array-union "^2.1.0" - dir-glob "^3.0.1" - fast-glob "^3.2.9" - ignore "^5.2.0" - merge2 "^1.4.1" - slash "^3.0.0" +globals@^14.0.0: + version "14.0.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e" + integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ== graceful-fs@^4.2.9: version "4.2.11" @@ -1983,11 +1923,6 @@ human-signals@^2.1.0: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== -human-signals@^4.3.0: - version "4.3.1" - resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-4.3.1.tgz#ab7f811e851fca97ffbd2c1fe9a958964de321b2" - integrity sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ== - iconv-lite@^0.6.3: version "0.6.3" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" @@ -2002,7 +1937,7 @@ ignore-walk@^5.0.1: dependencies: minimatch "^5.0.1" -ignore@^5.2.0, ignore@^5.2.4: +ignore@^5.2.0, ignore@^5.3.1: version "5.3.2" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== @@ -2058,16 +1993,6 @@ is-core-module@^2.13.0: dependencies: hasown "^2.0.0" -is-docker@^2.0.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" - integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== - -is-docker@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-3.0.0.tgz#90093aa3106277d8a77a5910dbae71747e15a200" - integrity sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ== - is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" @@ -2090,40 +2015,16 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3: dependencies: is-extglob "^2.1.1" -is-inside-container@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-inside-container/-/is-inside-container-1.0.0.tgz#e81fba699662eb31dbdaf26766a61d4814717ea4" - integrity sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA== - dependencies: - is-docker "^3.0.0" - is-number@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== -is-path-inside@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" - integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== - is-stream@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== -is-stream@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-3.0.0.tgz#e6bfd7aa6bef69f4f472ce9bb681e3e57b4319ac" - integrity sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA== - -is-wsl@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" - integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== - dependencies: - is-docker "^2.0.0" - isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" @@ -2565,6 +2466,11 @@ jsesc@^2.5.1: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== + json-parse-even-better-errors@^2.3.0: version "2.3.1" resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" @@ -2590,6 +2496,13 @@ jsonc-parser@^3.2.0: resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.2.1.tgz#031904571ccf929d7670ee8c547545081cb37f1a" integrity sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA== +keyv@^4.5.4: + version "4.5.4" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" + integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== + dependencies: + json-buffer "3.0.1" + kleur@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" @@ -2698,7 +2611,7 @@ merge-stream@^2.0.0: resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== -merge2@^1.3.0, merge2@^1.4.1: +merge2@^1.3.0: version "1.4.1" resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== @@ -2716,19 +2629,7 @@ mimic-fn@^2.1.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== -mimic-fn@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-4.0.0.tgz#60a90550d5cb0b239cca65d893b1a53b29871ecc" - integrity sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw== - -minimatch@9.0.3: - version "9.0.3" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" - integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== - dependencies: - brace-expansion "^2.0.1" - -minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: +minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== @@ -2742,6 +2643,13 @@ minimatch@^5.0.1: dependencies: brace-expansion "^2.0.1" +minimatch@^9.0.4: + version "9.0.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== + dependencies: + brace-expansion "^2.0.1" + minimist@^1.2.6: version "1.2.6" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" @@ -2830,13 +2738,6 @@ npm-run-path@^4.0.1: dependencies: path-key "^3.0.0" -npm-run-path@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-5.1.0.tgz#bc62f7f3f6952d9894bd08944ba011a6ee7b7e00" - integrity sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q== - dependencies: - path-key "^4.0.0" - object-assign@^4.0.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" @@ -2856,23 +2757,6 @@ onetime@^5.1.2: dependencies: mimic-fn "^2.1.0" -onetime@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-6.0.0.tgz#7c24c18ed1fd2e9bca4bd26806a33613c77d34b4" - integrity sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ== - dependencies: - mimic-fn "^4.0.0" - -open@^9.1.0: - version "9.1.0" - resolved "https://registry.yarnpkg.com/open/-/open-9.1.0.tgz#684934359c90ad25742f5a26151970ff8c6c80b6" - integrity sha512-OS+QTnw1/4vrf+9hh1jc1jnYjzSG4ttTBB8UxOwAnInG3Uo4ssetzC1ihqaIHjLJnA5GGlRl6QlZXOTQhRBUvg== - dependencies: - default-browser "^4.0.0" - define-lazy-prop "^3.0.0" - is-inside-container "^1.0.0" - is-wsl "^2.2.0" - optionator@^0.9.3: version "0.9.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64" @@ -2981,21 +2865,11 @@ path-key@^3.0.0, path-key@^3.1.0: resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== -path-key@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-4.0.0.tgz#295588dc3aee64154f877adb9d780b81c554bf18" - integrity sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ== - path-parse@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== -path-type@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" - integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== - picocolors@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" @@ -3141,20 +3015,6 @@ reusify@^1.0.4: resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== -rimraf@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" - integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== - dependencies: - glob "^7.1.3" - -run-applescript@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/run-applescript/-/run-applescript-5.0.0.tgz#e11e1c932e055d5c6b40d98374e0268d9b11899c" - integrity sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg== - dependencies: - execa "^5.0.0" - run-parallel@^1.1.9: version "1.2.0" resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" @@ -3196,6 +3056,11 @@ semver@^7.5.4: resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== +semver@^7.6.0: + version "7.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.1.tgz#abd5098d82b18c6c81f6074ff2647fd3e7220c9f" + integrity sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA== + shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" @@ -3308,11 +3173,6 @@ strip-final-newline@^2.0.0: resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== -strip-final-newline@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-3.0.0.tgz#52894c313fbff318835280aed60ff71ebf12b8fd" - integrity sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw== - strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" @@ -3357,12 +3217,12 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== -synckit@^0.8.5: - version "0.8.6" - resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.8.6.tgz#b69b7fbce3917c2673cbdc0d87fb324db4a5b409" - integrity sha512-laHF2savN6sMeHCjLRkheIU4wo3Zg9Ln5YOjOo7sZ5dVQW8yF5pPE5SIw1dsPhq3TRp1jisKRCdPhfs/1WMqDA== +synckit@0.8.8, synckit@^0.9.1: + version "0.8.8" + resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.8.8.tgz#fe7fe446518e3d3d49f5e429f443cf08b6edfcd7" + integrity sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ== dependencies: - "@pkgr/utils" "^2.4.2" + "@pkgr/core" "^0.1.0" tslib "^2.6.2" test-exclude@^6.0.0: @@ -3374,11 +3234,6 @@ test-exclude@^6.0.0: glob "^7.1.4" minimatch "^3.0.4" -text-table@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" - integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== - thenify-all@^1.0.0: version "1.6.0" resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726" @@ -3393,11 +3248,6 @@ thenify-all@^1.0.0: dependencies: any-promise "^1.0.0" -titleize@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/titleize/-/titleize-3.0.0.tgz#71c12eb7fdd2558aa8a44b0be83b8a76694acd53" - integrity sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ== - tmpl@1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" @@ -3415,10 +3265,10 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" -ts-api-utils@^1.0.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.3.0.tgz#4b490e27129f1e8e686b45cc4ab63714dc60eea1" - integrity sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ== +ts-api-utils@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.0.1.tgz#660729385b625b939aaa58054f45c058f33f10cd" + integrity sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w== ts-jest@^29.1.0: version "29.1.1" @@ -3477,7 +3327,7 @@ tsconfig-paths@^4.0.0: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@^2.6.0, tslib@^2.6.2: +tslib@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== @@ -3499,16 +3349,20 @@ type-detect@4.0.8: resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== -type-fest@^0.20.2: - version "0.20.2" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" - integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== - type-fest@^0.21.3: version "0.21.3" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== +typescript-eslint@^8.24.0: + version "8.24.0" + resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.24.0.tgz#cc655e71885ecb8280342b422ad839a2e2e46a96" + integrity sha512-/lmv4366en/qbB32Vz5+kCNZEMf6xYHwh1z48suBwZvAtnXKbP+YhGe8OLE2BqC67LMqKkCNLtjejdwsdW6uOQ== + dependencies: + "@typescript-eslint/eslint-plugin" "8.24.0" + "@typescript-eslint/parser" "8.24.0" + "@typescript-eslint/utils" "8.24.0" + typescript@5.6.1-rc: version "5.6.1-rc" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.1-rc.tgz#d5e4d7d8170174fed607b74cc32aba3d77018e02" @@ -3534,11 +3388,6 @@ unicode-emoji-modifier-base@^1.0.0: resolved "https://registry.yarnpkg.com/unicode-emoji-modifier-base/-/unicode-emoji-modifier-base-1.0.0.tgz#dbbd5b54ba30f287e2a8d5a249da6c0cef369459" integrity sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g== -untildify@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b" - integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw== - update-browserslist-db@^1.0.13: version "1.0.13" resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz#3c5e4f5c083661bd38ef64b6328c26ed6c8248c4" @@ -3614,6 +3463,11 @@ write-file-atomic@^4.0.2: imurmurhash "^0.1.4" signal-exit "^3.0.7" +ws@^8.18.0: + version "8.18.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" + integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== + y18n@^5.0.5: version "5.0.8" resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"