Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
90c93c1
Chore: Auto-download latest spec in make generate, sync generated types
nezhyborets Sep 14, 2026
e60255d
Merge branch 'main' into chore/sync-latest-openapi-spec
nezhyborets Sep 14, 2026
8c98fcb
test: fix annotation-added SSE fixture for stricter Annotation schema
nezhyborets Sep 14, 2026
1f78fb9
feat: handle new shell-call streaming events from the latest spec
nezhyborets Sep 15, 2026
751f0a0
docs: instruct agents to also build the Demo app for relevant changes
nezhyborets Sep 15, 2026
a9ef059
fix: add missing Tool/stream-event cases found in OpenAPI sync review
nezhyborets Sep 15, 2026
d0599fb
fix: download openapi.yaml atomically to avoid a truncated spec
nezhyborets Sep 15, 2026
2a766e8
docs: accept the ResponseObject.instructions breaking type change
nezhyborets Sep 15, 2026
3d41aff
docs: accept remaining API breaks from the OpenAPI spec regen
nezhyborets Sep 15, 2026
5921cae
docs: note why we expose generated Components.Schemas publicly
nezhyborets Sep 16, 2026
14fc29b
docs: call out ResponseObject.toolChoice becoming non-optional
nezhyborets Sep 16, 2026
f70b735
test: add fixtures for the shell-call streaming events
nezhyborets Sep 16, 2026
c393556
fix: match item_reference discriminator and align InputItem facade
nezhyborets Sep 17, 2026
c5d14f7
chore: allowlist the InputItem facade's new enum cases
nezhyborets Sep 17, 2026
55e5cf4
fix: keep generate as the Makefile's default goal
nezhyborets Sep 17, 2026
282bbea
fix: use a unique temp file when downloading the OpenAPI spec
nezhyborets Sep 17, 2026
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
325 changes: 325 additions & 0 deletions .github/api-breakage-allowlist.txt

Large diffs are not rendered by default.

18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,30 @@ Compatibility promise: the public API is additive-only. Anything `public` is dep
## [Unreleased]

### Added
- `ResponseStreamEvent.shellCall`, covering the new shell-call streaming events (`response.shell_call_command.added/delta/done`, `response.shell_call_output_content.delta/done`) added by the latest OpenAPI spec.
- `Tool.programmaticToolCallingTool`, the tool case paired with the `OutputItem.program`/`.programOutput` response items; without it, decoding a response whose `tools` array contains a `programmatic_tool_calling` tool would throw.
- `ResponseStreamEvent.customToolCallInput`, covering the previously unhandled `response.custom_tool_call_input.delta/done` streaming events; without it, a custom-tool streaming response would fail the whole stream with an `unknownEventType` error.
- `InputItem.compactionTriggerItemParam`, `.programItemParam`, and `.programOutputItemParam`, the item cases added by the latest OpenAPI spec; without them, decoding an input item with `type: "compaction_trigger"`, `"program"`, or `"program_output"` would throw `unknownOneOfDiscriminator`.
- CI: an *API Breakage* workflow fails a pull request that changes the public API compared with the latest release tag. Consciously accepted breaks are listed in `.github/api-breakage-allowlist.txt` with a matching changelog entry.
- 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.

### Changed
- **Breaking:** Regenerated `Components.Schemas` from the latest OpenAPI spec. Most of the reported breaks are mechanical (new cases added to generated enums, and generated memberwise initializers gaining parameters for new optional fields) and are allowlisted in `.github/api-breakage-allowlist.txt` without individual call-outs, matching how the `0.5.1` regeneration was documented. The changes worth knowing about if you read or construct these types directly:
- `ResponseObject.instructions` changed from `String?` to `ResponseObject.Instructions?`, letting `instructions` be either a plain string or a list of input items. `Instructions` is a generated two-case enum (`.case1(String)` for the string form, `.case2([InputItem])` for the list form); code that read `instructions` as a `String` needs to switch over it instead. This mirrors how the official Python SDK models the same field (`Union[str, List[ResponseInputItem], None]`) rather than adding a second property under a new name.
- `Components.Schemas.ServiceTier` was renamed to `ServiceTierResponses`.
- `Components.Schemas.Conversation2` was removed; `Response.conversation` and `ResponseProperties`-derived types now use `ResponseConversation` instead.
- `MCPToolCall.error` changed from `String?` to `MCPToolCallError?`.
- `ResponseOutputTextAnnotationAddedEvent.annotation` changed from `OpenAPIObjectContainer` to `Annotation?`, matching the stricter `Annotation` schema.
- `FunctionToolCallOutput.callId` and `FunctionCallOutputItemParam.callId` changed from `String` to `String?`.
- `ResponseStreamEvent.reasoning` was renamed to `.reasoningText`, with its payload changing from `ReasoningEvent` to `ReasoningTextEvent`.
- `ResponseObject.toolChoice` changed from `Schemas.ToolChoiceParam?` to `Schemas.ToolChoiceParam`. The generated `ResponseProperties.toolChoice` is optional only because that schema is shared with request bodies, where `tool_choice` may be omitted; an actual `Response` always includes it, which we verified against the API docs and the official Python SDK (`Response.tool_choice: ToolChoice`, non-optional). This corrects a facade type that didn't match the real response shape, but is still a source-breaking type change for callers who read `toolChoice` as optional.

### Fixed
- `Components.Schemas.InputItem` and the `InputItem` facade failed to decode a valid `{ "type": "item_reference", "id": "..." }` payload with `unknownOneOfDiscriminator`. The generator only matched discriminator values declared as a plain string enum, but `ItemReferenceParam.type` is declared as a nullable `anyOf: [<string enum>, {type: null}]`, which it didn't unwrap; the `InputItem` facade also fixed the same bug independently, plus a stale `"ItemReference"` discriminator string (missing the `Param` suffix) and a missing `"message"` match for `EasyInputMessage`.
- `ResponseStreamEvent.reasoningText` streaming events never decoded: `ModelResponseStreamEventType` listened for `response.reasoning.delta`/`.done`, but the API sends `response.reasoning_text.delta`/`.done`, so reasoning-text deltas always failed with an `unknownEventType` error.
- `make download-spec` could leave the tracked `openapi.yaml` truncated if `curl` was interrupted mid-transfer; it now downloads to a temp file and moves it into place only on success.
- Build warning in `ModelResponseEventsStreamInterpreter` when logging a failed stream event decode in debug builds.
- The test target compiles for the package's minimum iOS deployment target again; it used `Regex`, which requires iOS 16.
- Building on Linux with Swift 5.10 works again. swift-corelibs-foundation gained the async `URLSession` APIs only in Swift 6, so the async client now bridges the completion-handler API on older Linux toolchains.
Expand Down
21 changes: 21 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Agent instructions

## Always build the Demo app too

Whenever a change touches anything the `Demo` app could depend on (public API
in `Sources/OpenAI`, generated `Components.Schemas`, the `Edited`/`Facade`
types, streaming events, etc.), don't stop at `swift build` / `swift test`
passing for the package. Also build the Demo app:

```sh
xcodebuild -project Demo/Demo.xcodeproj -scheme Demo \
-destination "generic/platform=iOS Simulator" build
```

`Demo` is not covered by any CI workflow, so nothing else catches it when it
falls behind — e.g. an exhaustive `switch` over a `Facade` enum (like
`ResponseStreamEvent`) that doesn't get a new case added, or `DemoChat` code
that still calls a removed/renamed API. The goal is for Demo to stay complete
and up to date with the package, not silently lag behind. Only skip this
build when the change clearly cannot affect Demo (docs, CI config, tests-only
changes, etc.).
34 changes: 27 additions & 7 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,19 @@ The public API of this package is additive-only:
- A result type changes only when the API itself changed shape, and even then
prefer keeping the old member as a deprecated computed property over removing it.

swift-openapi-generator's own docs recommend against exposing generated code as
part of a package's public API, precisely because a `oneOf` schema gaining a
case, a response gaining a content type, and similar spec changes are breaking
in generated Swift even when they're additive in OpenAPI (see
[API stability of generated code](https://swiftpackageindex.com/apple/swift-openapi-generator/documentation/swift-openapi-generator/api-stability-of-generated-code)).
This package does it anyway, because generating and exposing `Components.Schemas`
directly is what makes it practical to track a spec as large and fast-moving as
OpenAI's: hand-writing and maintaining a wrapper type behind every generated
schema would make keeping up with new APIs far slower. This section, and the
*API Breakage* CI workflow described below, exist to make that trade-off safe:
every such break has to be noticed and consciously accepted rather than
silently shipped.

### Adding endpoints

New endpoint groups are added as namespaces, following the Responses API: one
Expand Down Expand Up @@ -136,18 +149,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
1. downloads the latest `openapi.yaml` from
[openai/openai-openapi](https://github.com/openai/openai-openapi), overwriting
the repository's copy;
2. prepares a generator-compatible copy of that spec under `.build/`;
3. applies the narrowly scoped workarounds documented in [`Scripts/`](Scripts/);
4. runs Swift OpenAPI Generator with the repository's configuration; and
5. 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
The downloaded `openapi.yaml` is committed as-is; only the working copy under
`.build/` receives the workarounds. The final preparation diff is written to
`.build/openapi-generator/openapi.patch`; review it, the `openapi.yaml` diff,
and the generated Swift diff together. Build the package and run the relevant
tests before submitting the change.

Run `make download-spec` on its own to refresh `openapi.yaml` without
regenerating types.

Do not edit `Components.swift` by hand. It is deliberately replaceable output,
so a later generation would discard such edits.

Expand Down
10 changes: 9 additions & 1 deletion Demo/DemoChat/Sources/ResponsesStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -585,7 +585,7 @@ public final class ResponsesStore: ObservableObject {
// TODO: Implement proper annotation handling when type conversion is resolved
print("Text annotation added: itemId=\(event.itemId), annotationIndex=\(event.annotationIndex)")
}
case .reasoning(let reasoningEvent):
case .reasoningText(let reasoningEvent):
// Handle reasoning events - could show reasoning in UI
switch reasoningEvent {
case .delta(let event):
Expand Down Expand Up @@ -621,6 +621,14 @@ public final class ResponsesStore: ObservableObject {
// Reasoning summary text events - not implemented yet
print("Reasoning summary text event received (not implemented)")
break
case .shellCall(_ /* let shellCallEvent */):
// Shell call events - not implemented yet
print("Shell call event received (not implemented)")
break
case .customToolCallInput(_ /* let customToolCallInputEvent */):
// Custom tool call input events - not implemented yet
print("Custom tool call input event received (not implemented)")
break
}
}

Expand Down
25 changes: 19 additions & 6 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,27 @@ 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
OPENAPI_SPEC_URL := https://raw.githubusercontent.com/openai/openai-openapi/main/openapi.yaml

# download-spec is defined first so `generate` can depend on it, but a plain
# `make` should still run the full pipeline, not just refresh the spec.
.DEFAULT_GOAL := generate

.PHONY: download-spec
download-spec:
# Refresh the vendored spec from upstream so `generate` always starts from
# the latest published OpenAPI document. Download to a unique temp file
# first and move it into place only on success, so an interrupted
# transfer -- or another concurrent `make download-spec`/`generate` using
# the same fixed temp name -- can't leave the tracked openapi.yaml
# truncated or corrupted.
tmp="$$(mktemp "$(PROJECT_DIR)/openapi.yaml.XXXXXX")"; \
trap 'rm -f "$$tmp"' EXIT; \
curl -fsSL "$(OPENAPI_SPEC_URL)" -o "$$tmp" && \
mv "$$tmp" "$(PROJECT_DIR)/openapi.yaml"

.PHONY: generate
generate:
generate: download-spec
# 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" \
Expand All @@ -49,10 +67,6 @@ generate:
# 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" \
"$(PREPARED_OPENAPI)" \
"$(PREPARED_OPENAPI)" \
Expand All @@ -62,7 +76,6 @@ generate:
--remove-required "ResponseAudioDoneEvent" "response_id" \
--remove-required "ResponseAudioTranscriptDeltaEvent" "response_id" \
--remove-required "ResponseAudioTranscriptDoneEvent" "response_id" \
--remove-required "WebSearchActionSearch" "query" \
--diff-source "$(PROJECT_DIR)/openapi.yaml" \
--diff-output "$(OPENAPI_DIFF)"
cd "$(GENERATOR_DIR)" && swift run swift-openapi-generator generate \
Expand Down
38 changes: 36 additions & 2 deletions Scripts/extract_components.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,33 @@
continue
filtered.append(line)

components_block = "".join(filtered)
# --- Drop duplicate typealiases within the same struct/enum scope ---
# The generator emits one `public typealias NAME = ...` immediately before each
# property that references an external component. When two sibling properties
# in the same type reference the same component, the identical typealias is
# emitted twice in the same scope, causing an "invalid redeclaration" build
# error. Keep only the first occurrence per scope.

deduped = []
scope_stack = [set()]
duplicates_removed = 0
for line in filtered:
for ch in line:
if ch == "{":
scope_stack.append(set())
elif ch == "}" and len(scope_stack) > 1:
scope_stack.pop()
m = TYPEALIAS_RE.match(line)
if m:
name = m.group(1)
seen = scope_stack[-1]
if name in seen:
duplicates_removed += 1
continue
seen.add(name)
deduped.append(line)

components_block = "".join(deduped)

# --- Read the existing header from Components.swift (up to and including #endif) ---

Expand All @@ -90,7 +116,7 @@
f.write(components_block)
f.write("\n")

print(f"Written {len(filtered)} lines of Components enum to {COMPONENTS_SWIFT}")
print(f"Written {len(deduped)} lines of Components enum to {COMPONENTS_SWIFT}")
if removed > 0:
print(
f"Note: stripped {removed} typealias line(s) that shadow Swift built-in type names "
Expand All @@ -100,3 +126,11 @@
f"Check https://github.com/apple/swift-openapi-generator/issues for a related bug report — "
f"if it has been fixed, this stripping step may no longer be necessary."
)
if duplicates_removed > 0:
print(
f"Note: dropped {duplicates_removed} duplicate typealias line(s) declared twice in "
f"the same type (e.g. two sibling properties referencing the same external "
f"component). swift-openapi-generator emits one typealias per reference, which "
f"collides when a scope has more than one reference to the same component, "
f"causing an 'invalid redeclaration' build error."
)
77 changes: 58 additions & 19 deletions Scripts/fix_recursive_reference.py
Original file line number Diff line number Diff line change
@@ -1,54 +1,93 @@
#!/usr/bin/env python3
"""Replace the unsupported CompoundFilter recursive reference.
"""Replace unsupported top-level-schema recursive references.

Symptom:
Validation warns that CompoundFilter's second item union member contains
Validation warns that a filter schema's recursive union member contains
nothing but unsupported attributes.

Cause:
Swift OpenAPI Generator does not support JSON Schema `$recursiveRef`.

Fix:
Replace the one known `$recursiveRef: '#'` with an ordinary component
reference to `CompoundFilter`, preserving the intended recursion.
Replace each `$recursiveRef: '#'` (or `"#"`) with an ordinary component
reference to the enclosing top-level component schema, preserving the
intended recursion. The upstream spec currently contains two such
occurrences: one inside `CompoundFilter`, one inside `BetaCompoundFilter`.

Semantic limitation:
This is not a general equivalent of `$recursiveRef`. `$recursiveRef` can
resolve through the active recursive-anchor scope, while `$ref` always
targets the named `CompoundFilter` component. They behave the same for this
self-contained document because CompoundFilter is the only recursive anchor
and the reference represents nested CompoundFilter values. Reassess this
workaround if the schema gains another recursive anchor, an external schema,
or recursive extension/composition.
targets the enclosing top-level component. They behave the same here
because each affected component is the only recursive anchor within
itself and the reference represents nested values of that same component.
Reassess this workaround if a `$recursiveRef` appears nested inside more
than one component schema, references an external schema, or is used with
recursive extension/composition.

Removal condition:
Remove this workaround when the generator supports `$recursiveRef`, or when
the upstream spec no longer contains this exact reference.
the upstream spec no longer contains any `$recursiveRef` occurrences.
"""

from __future__ import annotations

import argparse
import re
from pathlib import Path


OLD_REFERENCE = "$recursiveRef: '#'"
NEW_REFERENCE = "$ref: '#/components/schemas/CompoundFilter'"
COMPONENT_SCHEMA_RE = re.compile(r"^ (?P<name>[^\s][^:]*):(?:\r?\n)?$")
# The reference is typically a YAML sequence item (`- $recursiveRef: "#"`), so
# match everything before the key as an opaque prefix rather than assuming no
# `- ` marker.
RECURSIVE_REF_RE = re.compile(
r"^(?P<prefix>\s*(?:-\s+)?)\$recursiveRef:\s*(?P<quote>['\"])#(?P=quote)"
r"[ \t]*(?:#.*)?(?P<newline>\r?\n)?$"
)


def fix_recursive_reference(document: str) -> tuple[str, int]:
count = document.count(OLD_REFERENCE)
if count > 1:
raise ValueError(
"Expected at most one CompoundFilter recursive reference; "
f"found {count}."
lines = document.splitlines(keepends=True)
schema_starts = [
(index, match.group("name"))
for index, line in enumerate(lines)
if (match := COMPONENT_SCHEMA_RE.match(line)) is not None
]

replacement_count = 0
for index, line in enumerate(lines):
match = RECURSIVE_REF_RE.match(line)
if match is None:
continue

enclosing_schema = next(
(name for start, name in reversed(schema_starts) if start < index),
None,
)
return document.replace(OLD_REFERENCE, NEW_REFERENCE), count
if enclosing_schema is None:
raise ValueError(
f"Cannot resolve recursive reference on line {index + 1}: no "
"enclosing component schema found."
)

quote = match.group("quote")
prefix = match.group("prefix")
newline = match.group("newline") or ""
lines[index] = (
f"{prefix}$ref: {quote}#/components/schemas/{enclosing_schema}{quote}"
f"{newline}"
)
replacement_count += 1

return "".join(lines), replacement_count


def report_result(replacement_count: int) -> None:
if replacement_count:
print("Recursive reference workaround applied: 1 replacement.")
print(
"Recursive reference workaround applied: "
f"{replacement_count} replacement(s)."
)
else:
print(
"Recursive reference workaround not needed. The upstream spec or "
Expand Down
Loading
Loading