Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .castiron.stats.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
schema_version: 1
generation_id: c181b60a-9c3f-4fab-adb0-ec99e1b9164b
generation_id: 5f43d20e-1fa7-4356-a5cf-9ba76ebd71ce
openapi_spec_hash: d86a987b0943f67209611629cfe2926f
openapi_transformed_spec_hash: f98b6939664e5171dadad31a12ecf50a
config_hash: 0e042fd083471fc089bcbfe7011a7d01
codegen_sha: e7d3eec72178efe4bd1c41d2923c5636e5f41500
codegen_hash: 5808ff69b455e321be73db3a9c2640505dddd64b0418583f71301147baa3171d
codegen_sha: eaa28252a89f885820afa16bbd1a753ec589a3c1
codegen_hash: e1d1fbd9e1e10bd7f346d72af1f6a726fbf8c6f7ef655552bfc339f2be788c90
4 changes: 2 additions & 2 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@
"customizations": {
"vscode": {
"extensions": [
"ms-python.python"
"ms-python.python",
"astral-sh.ty"
],
"settings": {
"terminal.integrated.shell.linux": "/bin/bash",
"python.pythonPath": ".venv/bin/python",
"python.defaultInterpreterPath": ".venv/bin/python",
"python.typeChecking": "basic",
"terminal.integrated.env.linux": {
"PATH": "/home/vscode/.local/bin:${env:PATH}"
}
Expand Down
34 changes: 16 additions & 18 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,6 @@ jobs:
with:
persist-credentials: false

- uses: ./.github/actions/setup-node-tooling

- name: Set up uv
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
Expand Down Expand Up @@ -160,8 +158,6 @@ jobs:
- name: Use this job's Playwright installation
run: printf 'PLAYWRIGHT_BROWSERS_PATH=%s/openai-python-playwright\n' "$RUNNER_TEMP" >> "$GITHUB_ENV"

- uses: ./.github/actions/setup-node-tooling

- name: Set up uv
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
Expand All @@ -170,11 +166,9 @@ jobs:
enable-cache: true

- name: Check the optional Playwright demo
env:
PYRIGHT_PROJECT: scripts/pyrightconfig.browser-demo.json
run: >-
uv run --locked --all-extras --group browser-demo python -c
'import os, sys; os.execv("./scripts/run-pyright", ["./scripts/run-pyright", "--pythonpath", sys.executable])'
uv run --locked --all-extras --group browser-demo
ty check --extra-search-path examples/browser examples/browser-use-demo

- name: Install Chromium for local demo security checks
run: uv run --locked --all-extras --group browser-demo playwright install --with-deps chromium
Expand All @@ -195,7 +189,11 @@ jobs:
strategy:
fail-fast: false
matrix:
provider: [browser, computer]
include:
- provider: browser
directory: examples/browser
- provider: computer
directory: examples/computer-desktop
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
Expand All @@ -204,8 +202,6 @@ jobs:
- name: Use this job's Playwright installation
run: printf 'PLAYWRIGHT_BROWSERS_PATH=%s/openai-python-playwright\n' "$RUNNER_TEMP" >> "$GITHUB_ENV"

- uses: ./.github/actions/setup-node-tooling

- name: Set up uv
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
Expand All @@ -214,13 +210,9 @@ jobs:
enable-cache: true

- name: Check optional provider types against this checkout
env:
PYRIGHT_PROJECT: scripts/pyrightconfig.${{ matrix.provider }}-example.json
run: >-
uv run --locked --all-extras --no-default-groups --group dev --group provider-${{ matrix.provider }} python -c
'import os, pathlib, sys; import openai;
assert pathlib.Path(openai.__file__).resolve() == pathlib.Path("src/openai/__init__.py").resolve(), "Provider check replaced this OpenAI checkout";
os.execv("./scripts/run-pyright", ["./scripts/run-pyright", "--pythonpath", sys.executable])'
uv run --locked --all-extras --no-default-groups --group dev --group provider-${{ matrix.provider }}
ty check ${{ matrix.directory }}

- name: Install Chromium for local browser security checks
if: matrix.provider == 'browser'
Expand Down Expand Up @@ -253,7 +245,10 @@ jobs:
with:
persist-credentials: false

- uses: ./.github/actions/setup-node-tooling
- name: Set up Node for the mock-server tooling
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version-file: .node-version

- name: Set up Python ${{ matrix.python-version }}
id: setup-python
Expand All @@ -271,6 +266,9 @@ jobs:
- name: Bootstrap
run: ./scripts/bootstrap

- name: Install the pinned mock-server tooling
run: ./scripts/steady/install

- name: Check concurrent Steady publication
run: node scripts/steady/publication.test.cjs

Expand Down
49 changes: 29 additions & 20 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,39 +27,42 @@ must include a release note naming the final compatible SDK release.
### With uv

We use [uv](https://docs.astral.sh/uv/) to manage Python, dependencies, and builds.
Install uv 0.12.1 or newer, Node.js at the version in `.node-version`, and
the exact pnpm version in `package.json` (`npm install --global pnpm@11.19.0`).
The Node toolchain is for repository development only, not SDK users or builds.
Then run:
Install uv 0.12.1 or newer. To run the runtime tests, also install Node.js at
the version in `.node-version` for the mock-server tooling. Then run:

```sh
$ ./scripts/bootstrap
```

This installs all SDK extras and Python development tools from `uv.lock`, plus
the contributor tools from `pnpm-lock.yaml`. Bootstrap requires an already
installed Node.js and pnpm; it does not download or switch those tools for you.
Run a
command with `uv run --locked --all-extras`, or activate `.venv` in your shell.
This installs all SDK extras and Python development tools from `uv.lock`.
Run a command with `uv run --locked --all-extras`, or activate `.venv` in your shell.
Use `uv lock` after changing dependencies, and commit the updated `uv.lock`.
It is the authoritative lockfile; `uv export` can produce a requirements file
for tools that need one.

For Node tooling updates, edit `package.json`, run
`pnpm install --lockfile-only --ignore-scripts`, review the dependency and
integrity changes, and commit `pnpm-lock.yaml`. Normal bootstrap uses a frozen
lockfile. pnpm rejects releases that have not cleared the eight-day cooldown,
including packages whose registry metadata omits their publication date.
Pyright runs the local Microsoft package and never invokes npm or
downloads a runtime. Do not use `npx`, `pnpm dlx`, or Corepack auto-downloads.
The separate breaking-change CI job checks previous-version callers with Pyright.
To run that compatibility gate locally, install Node.js at the version in
`.node-version` and the exact pnpm version in `package.json`, then run
`./scripts/bootstrap-node`. Pyright and pnpm are not required for normal bootstrap or lint. Runtime tests
still require Node.js for the Steady mock-server tooling. Before running them, install
the pinned mock server and runtime:

```sh
$ ./scripts/steady/install
```

Node tooling uses the frozen `pnpm-lock.yaml`, an eight-day release cooldown,
and disabled lifecycle scripts. Update it with `pnpm install --lockfile-only
--ignore-scripts` and review the integrity changes. Do not use `npx`, `pnpm dlx`,
or Corepack auto-downloads to run compatibility checks.

The default environment uses Pydantic v2. `./scripts/test` also runs the
Pydantic-v1 suite in a separate, locked environment. To run that lane alone,
use `./scripts/test-pydantic-v1`.

### Dependency update policy

Routine Python, Node-tooling, and GitHub Actions updates have an eight-day cooldown. uv also
Routine Python, compatibility-checker, and GitHub Actions updates have an eight-day cooldown. uv also
excludes Python distribution artifacts uploaded within the last eight days.
Dependabot security updates remain enabled and are exempt from Dependabot's
version-update cooldown.
Expand Down Expand Up @@ -209,9 +212,9 @@ installation, integrity checks, and mock-server lifecycle.

Most tests require you to [set up a mock server](https://github.com/openai-oss-forks/steady) against the OpenAPI spec to run the tests.

`./scripts/bootstrap` installs the pinned Steady source and Deno runtime. `./scripts/test` starts it automatically when no mock server is running.
`./scripts/steady/install` installs the pinned Steady source and Deno runtime. `./scripts/test` starts it automatically when no mock server is running.
Neither `./scripts/test` nor `./scripts/mock` downloads mock tooling. If the local
tool is missing, rerun bootstrap. `TEST_API_BASE_URL` and an already-running
tool is missing, rerun `./scripts/steady/install`. `TEST_API_BASE_URL` and an already-running
mock server remain supported. To start the server yourself:

```sh
Expand All @@ -224,7 +227,13 @@ $ ./scripts/test

## Linting and formatting

This repository uses [Ruff](https://github.com/astral-sh/ruff) to lint and format Python code.
This repository uses [Ruff](https://github.com/astral-sh/ruff) to lint and format Python code,
and [ty](https://docs.astral.sh/ty/) to typecheck it. These are development tools;
installing the SDK does not install a typechecker or require consumers to switch checkers.
Consumers use their own checker and configuration. Keep public annotations, overloads,
exports, and `py.typed` compatible across checkers; repository-only checker settings
are not part of that contract. Preserve compatibility with both supported Pydantic
major versions and the Python versions covered by CI.

To lint:

Expand Down
2 changes: 1 addition & 1 deletion examples/audio.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def main() -> None:
# Legacy translation endpoint: shuts down February 26, 2027.
# New integrations should use Realtime translations instead:
# https://developers.openai.com/api/docs/deprecations
translation = openai.audio.translations.create( # pyright: ignore[reportDeprecated]
translation = openai.audio.translations.create( # ty: ignore[deprecated]
model="whisper-1",
file=speech_file_path,
)
Expand Down
3 changes: 2 additions & 1 deletion examples/browser-use-demo/booking.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,9 @@ async def read_page(self, args: ReadPageInput, *, cancelled: Callable[[], bool]
await handle.dispose()
continue
ref = secrets.token_hex(12)
item: BrowserReadElement
if role == "textbox":
item: BrowserReadElement = {
item = {
"ref": ref,
"role": "textbox",
"label": label,
Expand Down
4 changes: 2 additions & 2 deletions examples/browser-use-demo/exercise.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ async def hold_verified(*, cancelled: Callable[[], bool] | None = None) -> None:
await asyncio.wait_for(release.wait(), timeout=5)
await original_capture(cancelled=cancelled)

current.capture = hold_verified
current.capture = hold_verified # ty: ignore[invalid-assignment] # Test replaces a callable with a controlled fixture.
try:
await post("/api/approval", {**binding(offered), "approvalId": offered["approval"]["id"], "approved": True})
await asyncio.wait_for(entered.wait(), timeout=5)
Expand Down Expand Up @@ -277,7 +277,7 @@ async def hold_verified(*, cancelled: Callable[[], bool] | None = None) -> None:
assert stopped["result"] == "unverified", "The old agent result survived human editing and Stop"
finally:
release.set()
current.capture = original_capture
current.capture = original_capture # ty: ignore[invalid-assignment] # Test replaces a callable with a controlled fixture.
await post("/api/start", {"taskId": "reservation", "api": "decisions", "mode": "rehearsal"})
offered = await until(lambda s: s["approval"] is not None)
await post("/api/take-control", binding(offered))
Expand Down
4 changes: 2 additions & 2 deletions examples/computer-desktop/guest/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,8 @@ def install(root: Path) -> None:
b"BP22-LATE-EFI-PASSED",
b"BP22-LATE-COMPLETE",
)
if reason or process.returncode or not all(value in outputs["stdout"] for value in checks):
raise RuntimeError(reason or "Installer did not complete its fixed guest checks.")
if process.returncode or not all(value in outputs["stdout"] for value in checks):
raise RuntimeError("Installer did not complete its fixed guest checks.")
finally:
if process is not None:
if process.poll() is None:
Expand Down
10 changes: 5 additions & 5 deletions examples/computer-desktop/guest/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@
import subprocess
import urllib.parse
import urllib.request
from typing import NamedTuple
from typing import IO, NamedTuple
from pathlib import Path
from http.client import HTTPMessage

HERE = Path(__file__).resolve().parent
# This trusted checkout path also works when the verifier is started with python -I -S.
Expand Down Expand Up @@ -90,9 +91,8 @@ class Media(NamedTuple):


class OfficialRedirects(urllib.request.HTTPRedirectHandler):
# This Python 3.10 bootstrap uses only the standard library, which has no override decorator.
def redirect_request( # pyright: ignore[reportImplicitOverride]
self, req: urllib.request.Request, fp: object, code: int, msg: str, headers: object, newurl: str
def redirect_request(
self, req: urllib.request.Request, fp: IO[bytes], code: int, msg: str, headers: HTTPMessage, newurl: str
) -> urllib.request.Request | None:
url = urllib.parse.urlsplit(newurl)
if (
Expand All @@ -103,7 +103,7 @@ def redirect_request( # pyright: ignore[reportImplicitOverride]
or url.password is not None
):
raise ValueError("Debian redirected outside the fixed official download hosts.")
return super().redirect_request(req, fp, code, msg, headers, newurl) # type: ignore[arg-type]
return super().redirect_request(req, fp, code, msg, headers, newurl)


def digest(path: Path, algorithm: str = "sha256") -> str:
Expand Down
4 changes: 2 additions & 2 deletions examples/computer-desktop/guest/test_action_admission.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ def controlled_reply(request: object) -> dict[str, object]:
raise TimeoutError("Fixture read was not released.")
return {"ok": True}

action.validate = recorded_validate
action.reply = controlled_reply
action.validate = recorded_validate # ty: ignore[invalid-assignment] # Test replaces a callable with a controlled fixture.
action.reply = controlled_reply # ty: ignore[invalid-assignment] # Test replaces a callable with a controlled fixture.
action.main()


Expand Down
2 changes: 1 addition & 1 deletion examples/computer-desktop/guest/test_receipt.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ def guarded_popen(args: list[str], *rest: object, **kwargs: object) -> object:
if Path(args[0]) == self.binary:
attempted.append(args)
raise AssertionError("Test blocked guest spawn")
return original(args, *rest, **kwargs) # type: ignore[call-overload]
return original(args, *rest, **kwargs) # ty: ignore[no-matching-overload]

with patch("subprocess.Popen", guarded_popen):
with self.assertRaises((ComputerUseError, ValueError, OSError)):
Expand Down
2 changes: 2 additions & 0 deletions examples/demo.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env -S uv run python

from openai import OpenAI
from openai.types.chat import ChatCompletion

# gets API Key from environment variable OPENAI_API_KEY
client = OpenAI()
Expand Down Expand Up @@ -49,5 +50,6 @@
],
)
completion = response.parse()
assert isinstance(completion, ChatCompletion)
print(response.request_id)
print(completion.choices[0].message.content)
2 changes: 1 addition & 1 deletion examples/image_stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def main() -> None:
print(f" 💾 Saved to: {Path(filename).resolve()}")

else:
print(f"❓ Unknown event: {event}") # type: ignore[unreachable]
print(f"❓ Unknown event: {event}")


if __name__ == "__main__":
Expand Down
2 changes: 0 additions & 2 deletions examples/realtime/audio_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@
FORMAT = pyaudio.paInt16
CHANNELS = 1

# pyright: reportUnknownMemberType=false, reportUnknownVariableType=false, reportUnknownArgumentType=false


def audio_to_pcm16_base64(audio_bytes: bytes) -> bytes:
# load the audio file from the byte stream
Expand Down
14 changes: 7 additions & 7 deletions examples/realtime/push_to_talk_app.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
#!/usr/bin/env -S uv run
####################################################################

# Sample TUI app with a push to talk interface to the Realtime API #
# If you have `uv` installed and the `OPENAI_API_KEY` #
# environment variable set, you can run this example with just #
# #

# `./examples/realtime/push_to_talk_app.py` #
# #

# On Mac, you'll also need `brew install portaudio ffmpeg` #
####################################################################
#


# /// script
# requires-python = ">=3.10"
# dependencies = [
Expand Down Expand Up @@ -363,7 +363,7 @@ async def on_key(self, event: events.Key) -> None:
):
# The default in the API is that the model will automatically detect when the user has
# stopped talking and then start responding itself.
#

# However if we're in manual `turn_detection` mode then we need to
# manually tell the model to commit the audio buffer and start responding.
# Reserve the shared server buffer before the next K can send.
Expand All @@ -372,7 +372,7 @@ async def on_key(self, event: events.Key) -> None:
partial(self._finish_manual_recording, self._recording), group="realtime-finalization"
)
else:
import sounddevice as sd # type: ignore
import sounddevice as sd

self._recording = Recording()
self._awaiting_new_input = True
Expand Down
Loading
Loading