diff --git a/.release-please-manifest.json b/.release-please-manifest.json index abba81558..383863a35 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "6.32.0" + ".": "6.33.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 0121ace9a..89e484970 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +## 6.33.0 (2026-03-22) + +Full Changelog: [v6.32.0...v6.33.0](https://github.com/openai/openai-node/compare/v6.32.0...v6.33.0) + +### Features + +* **client:** add async iterator and stream() to WebSocket classes ([e1c16ee](https://github.com/openai/openai-node/commit/e1c16ee35b8ef9db30e9a99a2b3460368f3044d0)) + + +### Chores + +* **internal:** refactor imports ([cfe9c60](https://github.com/openai/openai-node/commit/cfe9c60aa41e9ed53e7d5f9187d31baf4364f8bd)) +* **tests:** bump steady to v0.19.4 ([f2e9dea](https://github.com/openai/openai-node/commit/f2e9dea844405f189cc63a1d1493de3eabfcb7e7)) +* **tests:** bump steady to v0.19.5 ([37c6cf4](https://github.com/openai/openai-node/commit/37c6cf495b9a05128572f9e955211b67d01410f3)) + + +### Refactors + +* **tests:** switch from prism to steady ([47c0581](https://github.com/openai/openai-node/commit/47c0581a1923c9e700a619dd6bfa3fb93a188899)) + ## 6.32.0 (2026-03-17) Full Changelog: [v6.31.0...v6.32.0](https://github.com/openai/openai-node/compare/v6.31.0...v6.32.0) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1a147be45..b481ef00d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -65,7 +65,7 @@ $ pnpm link --global openai ## Running tests -Most tests require you to [set up a mock server](https://github.com/stoplightio/prism) against the OpenAPI spec to run the tests. +Most tests require you to [set up a mock server](https://github.com/dgellow/steady) against the OpenAPI spec to run the tests. ```sh $ ./scripts/mock diff --git a/ecosystem-tests/browser-direct-import/package-lock.json b/ecosystem-tests/browser-direct-import/package-lock.json index de1f65654..7f48fef9e 100644 --- a/ecosystem-tests/browser-direct-import/package-lock.json +++ b/ecosystem-tests/browser-direct-import/package-lock.json @@ -420,10 +420,11 @@ ] }, "node_modules/basic-ftp": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", - "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.0.tgz", + "integrity": "sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==", "dev": true, + "license": "MIT", "engines": { "node": ">=10.0.0" } @@ -1564,21 +1565,6 @@ } } }, - "node_modules/puppeteer/node_modules/typescript": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", - "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==", - "dev": true, - "optional": true, - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, "node_modules/queue-tick": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", diff --git a/ecosystem-tests/vercel-edge/package-lock.json b/ecosystem-tests/vercel-edge/package-lock.json index aaca4370c..33c53a798 100644 --- a/ecosystem-tests/vercel-edge/package-lock.json +++ b/ecosystem-tests/vercel-edge/package-lock.json @@ -5789,12 +5789,26 @@ } }, "node_modules/seroval": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/seroval/-/seroval-0.5.1.tgz", - "integrity": "sha512-ZfhQVB59hmIauJG5Ydynupy8KHyr5imGNtdDhbZG68Ufh1Ynkv9KOYOAABf71oVbQxJ8VkWnMHAjEHE7fWkH5g==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.3.2.tgz", + "integrity": "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/seroval-plugins": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.3.3.tgz", + "integrity": "sha512-16OL3NnUBw8JG1jBLUoZJsLnQq0n5Ua6aHalhJK4fMQkz1lqR7Osz1sA30trBtd9VUDc2NgkuRCn8+/pBwqZ+w==", + "license": "MIT", "peer": true, "engines": { "node": ">=10" + }, + "peerDependencies": { + "seroval": "^1.0" } }, "node_modules/set-blocking": { @@ -5852,13 +5866,15 @@ } }, "node_modules/solid-js": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.7.11.tgz", - "integrity": "sha512-JkuvsHt8jqy7USsy9xJtT18aF9r2pFO+GB8JQ2XGTvtF49rGTObB46iebD25sE3qVNvIbwglXOXdALnJq9IHtQ==", + "version": "1.9.10", + "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.10.tgz", + "integrity": "sha512-Coz956cos/EPDlhs6+jsdTxKuJDPT7B5SVIWgABwROyxjY7Xbr8wkzD68Et+NxnV7DLJ3nJdAC2r9InuV/4Jew==", + "license": "MIT", "peer": true, "dependencies": { "csstype": "^3.1.0", - "seroval": "^0.5.0" + "seroval": "~1.3.0", + "seroval-plugins": "~1.3.0" } }, "node_modules/solid-swr-store": { diff --git a/jsr.json b/jsr.json index fc4be0bc6..ec1dee2a6 100644 --- a/jsr.json +++ b/jsr.json @@ -1,6 +1,6 @@ { "name": "@openai/openai", - "version": "6.32.0", + "version": "6.33.0", "exports": { ".": "./index.ts", "./helpers/zod": "./helpers/zod.ts", diff --git a/package.json b/package.json index f20fc0f14..bb8e10dcc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openai", - "version": "6.32.0", + "version": "6.33.0", "description": "The official TypeScript library for the OpenAI API", "author": "OpenAI ", "types": "dist/index.d.ts", diff --git a/scripts/mock b/scripts/mock index bcf3b392b..4f7dfd12b 100755 --- a/scripts/mock +++ b/scripts/mock @@ -19,34 +19,34 @@ fi echo "==> Starting mock server with URL ${URL}" -# Run prism mock on the given spec +# Run steady mock on the given spec if [ "$1" == "--daemon" ]; then # Pre-install the package so the download doesn't eat into the startup timeout - npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism --version + npm exec --package=@stdy/cli@0.19.5 -- steady --version - npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" &> .prism.log & + npm exec --package=@stdy/cli@0.19.5 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=brackets --validator-query-array-format=brackets --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" &> .stdy.log & - # Wait for server to come online (max 30s) + # Wait for server to come online via health endpoint (max 30s) echo -n "Waiting for server" attempts=0 - while ! grep -q "✖ fatal\|Prism is listening" ".prism.log" ; do + while ! curl --silent --fail "http://127.0.0.1:4010/_x-steady/health" >/dev/null 2>&1; do + if ! kill -0 $! 2>/dev/null; then + echo + cat .stdy.log + exit 1 + fi attempts=$((attempts + 1)) if [ "$attempts" -ge 300 ]; then echo - echo "Timed out waiting for Prism server to start" - cat .prism.log + echo "Timed out waiting for Steady server to start" + cat .stdy.log exit 1 fi echo -n "." sleep 0.1 done - if grep -q "✖ fatal" ".prism.log"; then - cat .prism.log - exit 1 - fi - echo else - npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" + npm exec --package=@stdy/cli@0.19.5 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=brackets --validator-query-array-format=brackets --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" fi diff --git a/scripts/test b/scripts/test index 7bce0516b..f03486d8f 100755 --- a/scripts/test +++ b/scripts/test @@ -9,8 +9,8 @@ GREEN='\033[0;32m' YELLOW='\033[0;33m' NC='\033[0m' # No Color -function prism_is_running() { - curl --silent "http://localhost:4010" >/dev/null 2>&1 +function steady_is_running() { + curl --silent "http://127.0.0.1:4010/_x-steady/health" >/dev/null 2>&1 } kill_server_on_port() { @@ -25,7 +25,7 @@ function is_overriding_api_base_url() { [ -n "$TEST_API_BASE_URL" ] } -if ! is_overriding_api_base_url && ! prism_is_running ; then +if ! is_overriding_api_base_url && ! steady_is_running ; then # When we exit this script, make sure to kill the background mock server process trap 'kill_server_on_port 4010' EXIT @@ -36,19 +36,19 @@ fi if is_overriding_api_base_url ; then echo -e "${GREEN}✔ Running tests against ${TEST_API_BASE_URL}${NC}" echo -elif ! prism_is_running ; then - echo -e "${RED}ERROR:${NC} The test suite will not run without a mock Prism server" +elif ! steady_is_running ; then + echo -e "${RED}ERROR:${NC} The test suite will not run without a mock Steady server" echo -e "running against your OpenAPI spec." echo echo -e "To run the server, pass in the path or url of your OpenAPI" - echo -e "spec to the prism command:" + echo -e "spec to the steady command:" echo - echo -e " \$ ${YELLOW}npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock path/to/your.openapi.yml${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.5 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=brackets --validator-query-array-format=brackets --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}" echo exit 1 else - echo -e "${GREEN}✔ Mock prism server is running with your OpenAPI spec${NC}" + echo -e "${GREEN}✔ Mock steady server is running with your OpenAPI spec${NC}" echo fi diff --git a/src/resources/responses/internal-base.ts b/src/resources/responses/internal-base.ts index b24fc2a81..180c034d2 100644 --- a/src/resources/responses/internal-base.ts +++ b/src/resources/responses/internal-base.ts @@ -2,11 +2,15 @@ import * as ResponsesAPI from './responses'; import { OpenAI } from '../../client'; - import { EventEmitter } from '../../core/EventEmitter'; import { OpenAIError } from '../../core/error'; import { stringifyQuery } from '../../internal/utils'; +export type ResponsesStreamMessage = + | { type: 'connecting' | 'open' | 'closing' | 'close' } + | { type: 'message'; message: ResponsesAPI.ResponsesServerEvent } + | { type: 'error'; error: WebSocketError }; + export class WebSocketError extends OpenAIError { /** * The error data that the API sent back in an error event. diff --git a/src/resources/responses/ws.ts b/src/resources/responses/ws.ts index 3ca11d28c..004f6765c 100644 --- a/src/resources/responses/ws.ts +++ b/src/resources/responses/ws.ts @@ -1,7 +1,7 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. import * as WS from 'ws'; -import { ResponsesEmitter, buildURL } from './internal-base'; +import { ResponsesEmitter, ResponsesStreamMessage, WebSocketError, buildURL } from './internal-base'; import * as ResponsesAPI from './responses'; import { OpenAI } from '../../client'; @@ -65,6 +65,135 @@ export class ResponsesWS extends ResponsesEmitter { } } + /** + * Returns an async iterator over WebSocket lifecycle and message events, + * providing an alternative to the event-based `.on()` API. + * The iterator will exit if the socket closes but breaking out of the iterator + * does not close the socket. + * + * @example + * ```ts + * for await (const event of connection.stream()) { + * switch (event.type) { + * case 'message': + * console.log('received:', event.message); + * break; + * case 'error': + * console.error(event.error); + * break; + * case 'close': + * console.log('connection closed'); + * break; + * } + * } + * ``` + */ + stream(): AsyncIterableIterator { + return this[Symbol.asyncIterator](); + } + + [Symbol.asyncIterator](): AsyncIterableIterator { + // Two-queue async iterator: `queue` buffers incoming messages, + // `resolvers` buffers waiting next() calls. A push wakes the + // oldest next(); a next() drains the oldest message. + const queue: ResponsesStreamMessage[] = []; + const resolvers: (() => void)[] = []; + let done = false; + + const push = (msg: ResponsesStreamMessage) => { + queue.push(msg); + resolvers.shift()?.(); + }; + + const onEvent = (event: ResponsesAPI.ResponsesServerEvent) => { + if (event.type === 'error') return; // handled by onEmitterError + push({ type: 'message', message: event }); + }; + + // Catches both API-level and socket-level errors via _onError → _emit('error') + const onEmitterError = (err: WebSocketError) => { + push({ type: 'error', error: err }); + }; + + const onOpen = () => { + push({ type: 'open' }); + }; + + const flushResolvers = () => { + for (let resolver = resolvers.shift(); resolver; resolver = resolvers.shift()) { + resolver(); + } + }; + + const onClose = () => { + push({ type: 'close' }); + done = true; + flushResolvers(); + cleanup(); + }; + + const cleanup = () => { + this.off('event', onEvent); + this.off('error', onEmitterError); + this.socket.off('open', onOpen); + this.socket.off('close', onClose); + }; + + this.on('event', onEvent); + this.on('error', onEmitterError); + this.socket.on('open', onOpen); + this.socket.on('close', onClose); + + switch (this.socket.readyState) { + case WS.WebSocket.CONNECTING: + push({ type: 'connecting' }); + break; + case WS.WebSocket.OPEN: + push({ type: 'open' }); + break; + case WS.WebSocket.CLOSING: + push({ type: 'closing' }); + break; + case WS.WebSocket.CLOSED: + push({ type: 'close' }); + done = true; + cleanup(); + break; + } + + const resolve = (res: (value: IteratorResult) => void) => { + if (queue.length > 0) { + res({ value: queue.shift()!, done: false }); + } else if (done) { + res({ value: undefined, done: true }); + } else { + return false; + } + return true; + }; + + const next = (): Promise> => + new Promise((res) => { + if (resolve(res)) return; + resolvers.push(() => { + resolve(res); + }); + }); + + return { + next, + return: (): Promise> => { + done = true; + cleanup(); + flushResolvers(); + return Promise.resolve({ value: undefined, done: true }); + }, + [Symbol.asyncIterator]() { + return this; + }, + }; + } + private authHeaders(): Record { return { Authorization: `Bearer ${this.client.apiKey}` }; return {}; diff --git a/src/version.ts b/src/version.ts index 50533e3e4..0fc10ffff 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const VERSION = '6.32.0'; // x-release-please-version +export const VERSION = '6.33.0'; // x-release-please-version diff --git a/tests/api-resources/containers/files/content.test.ts b/tests/api-resources/containers/files/content.test.ts index a499534a1..1792ed49a 100644 --- a/tests/api-resources/containers/files/content.test.ts +++ b/tests/api-resources/containers/files/content.test.ts @@ -8,8 +8,7 @@ const client = new OpenAI({ }); describe('resource content', () => { - // Mock server doesn't support application/binary responses - test.skip('retrieve: required and optional params', async () => { + test('retrieve: required and optional params', async () => { const response = await client.containers.files.content.retrieve('file_id', { container_id: 'container_id', }); diff --git a/tests/api-resources/skills/versions/content.test.ts b/tests/api-resources/skills/versions/content.test.ts index 1cffeccfa..80d49fb52 100644 --- a/tests/api-resources/skills/versions/content.test.ts +++ b/tests/api-resources/skills/versions/content.test.ts @@ -8,8 +8,7 @@ const client = new OpenAI({ }); describe('resource content', () => { - // Mock server (Prism) doesn't support the generated binary Accept header (`application/binary`) for this endpoint. - test.skip('retrieve: required and optional params', async () => { + test('retrieve: required and optional params', async () => { const response = await client.skills.versions.content.retrieve('version', { skill_id: 'skill_123' }); }); }); diff --git a/tests/api-resources/videos.test.ts b/tests/api-resources/videos.test.ts index e79204378..30373022c 100644 --- a/tests/api-resources/videos.test.ts +++ b/tests/api-resources/videos.test.ts @@ -97,8 +97,7 @@ describe('resource videos', () => { }); }); - // Mock server doesn't support application/binary responses - test.skip('downloadContent: request options and params are passed correctly', async () => { + test('downloadContent: 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.videos.downloadContent('video_123', { variant: 'video' }, { path: '/_stainless_unknown_path' }),