Skip to content
Closed
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
29 changes: 29 additions & 0 deletions .github/api-breakage-allowlist.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
API breakage: accessor Components.Schemas.CreateTranscriptionRequest.ChunkingStrategyPayload.body.Get() has return type change from OpenAI.Components.Schemas.CreateTranscriptionRequest.ChunkingStrategyPayload.BodyPayload? to OpenAI.Components.Schemas.CreateTranscriptionRequest.ChunkingStrategyPayload.BodyPayload
API breakage: accessor Components.Schemas.CreateTranscriptionRequest.ChunkingStrategyPayload.body.Set() has parameter 0 type change from OpenAI.Components.Schemas.CreateTranscriptionRequest.ChunkingStrategyPayload.BodyPayload? to OpenAI.Components.Schemas.CreateTranscriptionRequest.ChunkingStrategyPayload.BodyPayload
API breakage: constructor Components.Schemas.CreateTranscriptionRequest.ChunkingStrategyPayload.init(body:) has parameter 0 type change from OpenAI.Components.Schemas.CreateTranscriptionRequest.ChunkingStrategyPayload.BodyPayload? to OpenAI.Components.Schemas.CreateTranscriptionRequest.ChunkingStrategyPayload.BodyPayload
API breakage: constructor Components.Schemas.CreateTranscriptionRequest.ChunkingStrategyPayload.init(body:) has removed default argument from parameter 0
API breakage: enumelement Components.Schemas.CreateTranscriptionRequest.stream has been added as a new enum case
API breakage: typealias Components.Schemas.AutoCodeInterpreterToolParam.ContainerMemoryLimit has been removed
API breakage: typealias Components.Schemas.ComputerCallOutputItemParam.ComputerCallSafetyCheckParam has been removed
API breakage: typealias Components.Schemas.ComputerCallOutputItemParam.FunctionCallItemStatus has been removed
API breakage: typealias Components.Schemas.ContainerAutoParam.ContainerMemoryLimit has been removed
API breakage: typealias Components.Schemas.CreateResponse.Value3Payload.ContextManagementParam has been removed
API breakage: typealias Components.Schemas.CreateResponse.Value3Payload.ConversationParam has been removed
API breakage: typealias Components.Schemas.CreateResponse.Value3Payload.IncludeEnum has been removed
API breakage: typealias Components.Schemas.EasyInputMessage.MessagePhase has been removed
API breakage: typealias Components.Schemas.FileSearchTool.Filters has been removed
API breakage: typealias Components.Schemas.FunctionCallOutputItemParam.FunctionCallItemStatus has been removed
API breakage: typealias Components.Schemas.FunctionShellCallItemParam.FunctionShellCallItemStatus has been removed
API breakage: typealias Components.Schemas.FunctionShellCallOutputItemParam.FunctionShellCallItemStatus has been removed
API breakage: typealias Components.Schemas.FunctionToolParam.EmptyModelParam has been removed
API breakage: typealias Components.Schemas.ImageGenTool.InputFidelity has been removed
API breakage: typealias Components.Schemas.InputImageContentParamAutoParam.DetailEnum has been removed
API breakage: typealias Components.Schemas.MCPListToolsTool.OpenAPIObjectContainer has been removed
API breakage: typealias Components.Schemas.OutputMessage.MessagePhase has been removed
API breakage: typealias Components.Schemas.Response.Value3Payload.Conversation2 has been removed
API breakage: typealias Components.Schemas.ResponseProperties.Reasoning has been removed
API breakage: typealias Components.Schemas.ToolSearchCallItemParam.FunctionCallItemStatus has been removed
API breakage: typealias Components.Schemas.ToolSearchOutputItemParam.FunctionCallItemStatus has been removed
API breakage: typealias Components.Schemas.ToolSearchToolParam.EmptyModelParam has been removed
API breakage: typealias Components.Schemas.WebSearchPreviewTool.ApproximateLocation has been removed
API breakage: var Components.Schemas.CreateTranscriptionRequest.ChunkingStrategyPayload.body has declared type change from OpenAI.Components.Schemas.CreateTranscriptionRequest.ChunkingStrategyPayload.BodyPayload? to OpenAI.Components.Schemas.CreateTranscriptionRequest.ChunkingStrategyPayload.BodyPayload
48 changes: 48 additions & 0 deletions .github/workflows/generation.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Regenerates Components.swift with `make generate` and fails when the committed file differs, so the
# generated schemas can never drift from what the pipeline in the repository produces.

name: Generation

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
regenerate:
name: Components.swift matches make generate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: "3.12"
- name: Toolchain
id: toolchain
run: |
swift --version
echo "generator=$(make -s generator-version)" >> "$GITHUB_OUTPUT"
echo "swift=$(swift --version 2>&1 | head -n 1 | tr -c 'A-Za-z0-9.\n' '-')" >> "$GITHUB_OUTPUT"
- name: Cache the built generator
uses: actions/cache@v6
with:
path: .build/openapi-generator/swift-openapi-generator-${{ steps.toolchain.outputs.generator }}
key: swift-openapi-generator-${{ steps.toolchain.outputs.generator }}-${{ steps.toolchain.outputs.swift }}
- name: Regenerate
run: make generate
- name: Committed file must match the pipeline output
run: |
if ! git diff --exit-code --stat -- Sources/OpenAI/Public/Schemas/Generated/Components.swift; then
echo "::error::Components.swift is out of date. Run 'make generate' and commit the result."
exit 1
fi
echo "Components.swift matches make generate."
1 change: 1 addition & 0 deletions .github/workflows/swift.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ on:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
workflow_dispatch:

permissions:
contents: read
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ Compatibility promise: the public API is additive-only. Anything `public` is dep
- CI: the *Swift Build* workflow now builds and tests on Linux with Swift 5.10, 6.0 and 6.3 containers, tests on macOS and the iOS Simulator, and builds for tvOS, watchOS and visionOS with Xcode. Tests written with Swift Testing only exist on toolchains that ship it (Swift 6); the XCTest suite runs everywhere.
- CONTRIBUTING.md: API stability policy, including how new endpoint groups are added as namespaces and how generated `Components.Schemas` types are treated.
- This changelog.
- CI: a *Generation* workflow runs `make generate` and fails when the committed `Components.swift` differs from the pipeline's output.

### Changed
- Code generation no longer needs a private fork of Swift OpenAPI Generator. `make generate` clones and builds the stock generator (1.13.1) under `.build/`, and two new scripts replace the fork's patches: `Scripts/transform_openapi.py` collapses OpenAI's `anyOf: [X, {type: 'null'}]` nullability into optional properties and records discriminator wire values, and `Scripts/postprocess_components.py` applies them to the generated Swift and adds the fallback for the `message` value shared by `InputMessage` and `OutputMessage`. `Scripts/fix_recursive_reference.py` now handles any number of `$recursiveRef` occurrences.
- Regenerated `Components.Schemas` with that pipeline from the unchanged vendored spec. Generated-type changes, all listed in `.github/api-breakage-allowlist.txt`: 23 nested typealiases that were artifacts of the fork's nullable handling are gone (for example `OutputMessage.MessagePhase` and `CreateResponse.Value3Payload.IncludeEnum`; the top-level `Components.Schemas` types they aliased are unchanged), `CreateTranscriptionRequest.ChunkingStrategyPayload.body` is no longer optional, and `CreateTranscriptionRequest` gained a `stream` part. Decoding improvements: `ItemResource` accepts input messages, `InputItem` accepts `item_reference`, and nested unions such as `WebSearchToolCall.action` decode by wire value.

### Fixed
- Build warning in `ModelResponseEventsStreamInterpreter` when logging a failed stream event decode in debug builds.
Expand Down
39 changes: 24 additions & 15 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,11 @@ uses handwritten `CreateModelResponseQuery`, `ResponseObject`, and
`ResponseStreamEvent` types, while their supporting schemas come from
`Components.Schemas`.

The workflow is automated by [`make generate`](Makefile). The Makefile is the
source of truth for prerequisites and the exact commands; in particular, it
documents the required sibling checkout of the project's Swift OpenAPI Generator
fork and the generator changes that fork must contain.
The workflow is automated by [`make generate`](Makefile). It needs a Swift
toolchain, `python3` with `venv`, and network access on the first run. No fork
of the generator and no sibling checkout are required: the Makefile clones and
builds the pinned Swift OpenAPI Generator release under `.build/` and installs
the Python dependency (PyYAML) into a virtualenv there.

Before running generation, update
[`openapi-generator-config.yaml`](openapi-generator-config.yaml) with every path
Expand All @@ -136,17 +137,25 @@ make generate

The command:

1. prepares a generator-compatible copy of `openapi.yaml` under `.build/`;
2. applies the narrowly scoped workarounds documented in [`Scripts/`](Scripts/);
3. runs Swift OpenAPI Generator with the repository's configuration; and
4. extracts the generated `Components` enum into
`Sources/OpenAI/Public/Schemas/Generated/Components.swift` while preserving
that file's imports and header.

The source specification is not modified during this process. The final
preparation diff is written to `.build/openapi-generator/openapi.patch`; review
it along with the generated Swift diff. Build the package and run the relevant
tests before submitting the change.
1. applies the conditional, line-based spec fixes in [`Scripts/`](Scripts/)
(`prepare_openapi.py`, `remove_required_properties.py`) and writes their diff
to `.build/openapi-generator/openapi.patch`;
2. runs `Scripts/transform_openapi.py`, which collapses OpenAI's
`anyOf: [X, {type: 'null'}]` nullability into optional properties (the
generator does not support that form, see apple/swift-openapi-generator#906)
and records the wire values of every discriminated union, because the spec's
discriminators have no `mapping` (openai/openai-openapi#542);
3. runs Swift OpenAPI Generator (types only) with the repository's configuration;
4. runs `Scripts/postprocess_components.py`, which re-wraps the generated schemas
under the existing header of `Components.swift`, appends the wire values to
each union's decoder, and adds a fallback for the one value that names two
schemas (`message` in `Item` and `ItemResource`).

Review `.build/openapi-generator/openapi.patch` and the generated Swift diff, run
`swift package diagnose-api-breaking-changes` against the latest tag (every
regeneration is a public API change, see *API stability*), build the package and
run the tests. The `Generation` workflow in CI runs `make generate` and fails
when the committed `Components.swift` does not match the pipeline's output.

Do not edit `Components.swift` by hand. It is deliberately replaceable output,
so a later generation would discard such edits.
Expand Down
128 changes: 69 additions & 59 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,59 +1,54 @@
# Requires a local fork of swift-openapi-generator to be checked out as a
# sibling directory named `swift-openapi-generator` (i.e. ../swift-openapi-generator).
# See https://github.com/apple/swift-openapi-generator for the upstream repo.
# Regenerates Sources/OpenAI/Public/Schemas/Generated/Components.swift from openapi.yaml.
#
# The fork must include these changes, which are not available in the official
# generator at the time of writing:
# No fork of Swift OpenAPI Generator and no sibling checkout are required. The pinned generator release is
# cloned and built under .build/, and the Python dependency (PyYAML) is installed into a virtualenv there.
# Prerequisites: a Swift toolchain, python3 with venv, and network access on the first run.
#
# - Handle OpenAPI 3.1 nullable schemas expressed as
# `anyOf: [<schema>, { type: null }]`. The generator must ignore the null
# branch while assigning the Swift type, then make the resulting type
# optional. Without this change, nullable properties are unsupported or are
# generated as an anyOf wrapper instead of the expected optional Swift type.
# Pipeline (details in CONTRIBUTING.md, "Implementing using Code Generation"):
# 1. Scripts/prepare_openapi.py conditional, line-based spec fixes (each script documents its removal
# condition); the combined diff is written to .build/openapi-generator/openapi.patch
# 2. Scripts/remove_required_properties.py `required` entries the live API does not honour (see below)
# 3. Scripts/transform_openapi.py nullable `anyOf` -> optional properties; discriminator wire values recorded
# (replaces the patches that used to live in a private generator fork)
# 4. swift-openapi-generator types only, paths and schemas from openapi-generator-config.yaml
# 5. Scripts/postprocess_components.py re-wraps the output under the existing header, appends the wire values,
# adds the collision fallbacks -> Components.swift
#
# - When a oneOf discriminator has no explicit mapping, also match the string
# enum values declared by the referenced schemas' discriminator property.
# The OpenAI spec uses runtime values such as `input_text`, which do not match
# schema names such as `InputTextContent`; without this change, decoding a
# valid response throws unknownOneOfDiscriminator. See
# https://github.com/openai/openai-openapi/issues/542 for the spec issue.
#
# - When inferred discriminator values collide across multiple oneOf schemas,
# fall back to structural decoding for the colliding value instead of
# generating duplicate switch patterns. The OpenAI spec uses `message` for
# both InputMessage and OutputMessage, so the discriminator alone cannot
# select the correct schema.
#
# Expected diagnostic:
# The generator warns that `InputMessageResource/value2` requires `type` even
# though that property is declared by the sibling `InputMessage` schema in the
# same `allOf`. JSON Schema applies both members to the same object, while the
# generator validates each generated allOf payload independently. The property
# remains available through the generated `InputMessage` payload, so this
# warning is intentionally ignored.
GENERATOR_DIR := $(shell dirname $(abspath $(lastword $(MAKEFILE_LIST))))/../swift-openapi-generator
PROJECT_DIR := $(shell dirname $(abspath $(lastword $(MAKEFILE_LIST))))
TYPES_SWIFT := $(GENERATOR_DIR)/Types.swift
COMPONENTS_SWIFT := $(PROJECT_DIR)/Sources/OpenAI/Public/Schemas/Generated/Components.swift
PREPARED_OPENAPI := $(PROJECT_DIR)/.build/openapi-generator/openapi.yaml
OPENAPI_DIFF := $(PROJECT_DIR)/.build/openapi-generator/openapi.patch
# Expected generator diagnostics: "A property name only appears in the required list, but not in the properties
# map" for InputMessageResource/value2/type and Response/value3/{metadata, model, temperature, tool_choice, tools,
# top_p}. Those properties are declared by a sibling allOf member; the generator validates each member alone and
# the properties remain available through the sibling payload. Harmless.

PROJECT_DIR := $(shell dirname $(abspath $(lastword $(MAKEFILE_LIST))))
BUILD_DIR := $(PROJECT_DIR)/.build/openapi-generator

GENERATOR_VERSION := 1.13.1
GENERATOR_REPO := https://github.com/apple/swift-openapi-generator
GENERATOR_DIR := $(BUILD_DIR)/swift-openapi-generator-$(GENERATOR_VERSION)
# Override on the command line to use an already built generator: make generate GENERATOR_BIN=/path/to/binary
GENERATOR_BIN ?= $(GENERATOR_DIR)/.build/release/swift-openapi-generator

VENV := $(BUILD_DIR)/venv
PYTHON := $(VENV)/bin/python

SPEC := $(PROJECT_DIR)/openapi.yaml
CONFIG := $(PROJECT_DIR)/openapi-generator-config.yaml
COMPONENTS_SWIFT := $(PROJECT_DIR)/Sources/OpenAI/Public/Schemas/Generated/Components.swift
PREPARED_OPENAPI := $(BUILD_DIR)/openapi.prepared.yaml
TRANSFORMED_OPENAPI := $(BUILD_DIR)/openapi.transformed.yaml
DISCRIMINATORS := $(BUILD_DIR)/discriminators.json
OPENAPI_DIFF := $(BUILD_DIR)/openapi.patch
GENERATED_DIR := $(BUILD_DIR)/generated

.PHONY: generate
generate:
# Prepare a working copy with conditional, documented upstream-spec fixes.
# See the scripts called by prepare_openapi.py for each error and its fix.
python3 -B "$(PROJECT_DIR)/Scripts/prepare_openapi.py" \
"$(PROJECT_DIR)/openapi.yaml" \
"$(PREPARED_OPENAPI)"
# The LocalShellToolCallOutput, MCP approval response, and response audio
# event removals are required-list entries without matching schema properties.
# They otherwise produce swift-openapi-generator warnings that the names are
# likely typos and will be skipped.
#
# WebSearchActionSearch/query is different: the property is declared, but the
# live API can omit the deprecated singular query and return queries instead.
# It must be optional so valid web-search response items decode successfully.
python3 -B "$(PROJECT_DIR)/Scripts/remove_required_properties.py" \
.PHONY: generate generator-version clean-generation

generate: $(PYTHON) $(GENERATOR_BIN)
$(PYTHON) -B "$(PROJECT_DIR)/Scripts/prepare_openapi.py" "$(SPEC)" "$(PREPARED_OPENAPI)"
# LocalShellToolCallOutput, the MCP approval responses and the response audio events list properties as
# required that they never declare; the generator would otherwise warn and skip them.
# WebSearchActionSearch/query is declared but no longer sent by the live API (fixed upstream in July 2026,
# openai/openai-openapi#544); drop this entry when the vendored spec is updated past that fix.
$(PYTHON) -B "$(PROJECT_DIR)/Scripts/remove_required_properties.py" \
"$(PREPARED_OPENAPI)" \
"$(PREPARED_OPENAPI)" \
--remove-required "LocalShellToolCallOutput" "call_id" \
Expand All @@ -63,11 +58,26 @@ generate:
--remove-required "ResponseAudioTranscriptDeltaEvent" "response_id" \
--remove-required "ResponseAudioTranscriptDoneEvent" "response_id" \
--remove-required "WebSearchActionSearch" "query" \
--diff-source "$(PROJECT_DIR)/openapi.yaml" \
--diff-source "$(SPEC)" \
--diff-output "$(OPENAPI_DIFF)"
cd "$(GENERATOR_DIR)" && swift run swift-openapi-generator generate \
--config "$(PROJECT_DIR)/openapi-generator-config.yaml" \
"$(PREPARED_OPENAPI)"
python3 -B "$(PROJECT_DIR)/Scripts/extract_components.py" \
"$(TYPES_SWIFT)" \
"$(COMPONENTS_SWIFT)"
$(PYTHON) -B "$(PROJECT_DIR)/Scripts/transform_openapi.py" "$(PREPARED_OPENAPI)" "$(TRANSFORMED_OPENAPI)" "$(DISCRIMINATORS)"
rm -rf "$(GENERATED_DIR)" && mkdir -p "$(GENERATED_DIR)"
"$(GENERATOR_BIN)" generate --config "$(CONFIG)" --output-directory "$(GENERATED_DIR)" "$(TRANSFORMED_OPENAPI)"
$(PYTHON) -B "$(PROJECT_DIR)/Scripts/postprocess_components.py" "$(GENERATED_DIR)/Types+Components+Schemas.swift" "$(DISCRIMINATORS)" "$(COMPONENTS_SWIFT)"

$(PYTHON): $(PROJECT_DIR)/Scripts/requirements.txt
python3 -m venv "$(VENV)"
"$(PYTHON)" -m pip install --quiet --disable-pip-version-check -r "$(PROJECT_DIR)/Scripts/requirements.txt"
touch "$(PYTHON)"

$(GENERATOR_DIR)/.build/release/swift-openapi-generator:
rm -rf "$(GENERATOR_DIR)"
git clone --quiet --depth 1 --branch "$(GENERATOR_VERSION)" "$(GENERATOR_REPO)" "$(GENERATOR_DIR)"
cd "$(GENERATOR_DIR)" && swift build -c release --product swift-openapi-generator

# Used by the Generation workflow to key its cache of the built generator.
generator-version:
@echo $(GENERATOR_VERSION)

clean-generation:
rm -rf "$(BUILD_DIR)"
Loading