v0.3.0 — real-world test surfaces 9 fixes + nested resource layout
Nine fixes surfaced by generating a working SDK from the real
openai-openapi spec (162 paths, 983 schemas) — each oracle-verified
against the actual Stainless-generated openai-python SDK. Plus the
nested resource directory layout that matches openai-python's
structure (resources/chat/completions/messages.py etc.) — drop-in
import paths.
Fixed
- Silent leaf-name collision (correctness bug). With the previous
flatresources/*.pylayout, two subresources sharing a leaf name
(e.g.chat.completions.messagesANDthreads.messages) wrote to
the same file; last-wins meantc.chat.completions.messages.list()
would silently call/threads/{thread_id}/messages(wrong
endpoint). Now: nested directories matchingopenai-python
(resources/chat/completions/messages.py,
resources/threads/messages.py). Cross-cutting imports inside
resource files switched from..to absolute<pkg>._core.Xso the
same template works at any depth. New regression test
(test_subresource_with_shared_leaf_name_calls_right_endpoint). - Discriminated union with an
Anyvariant (real openai spec
pattern:oneOf: [{type:object, properties:{type,...}}, {}]where
the empty branch lowers toAnyType). Pydantic rejectsobjectas
a discriminated-union variant at import time. Now: drop the AnyType
variants from a discriminated union before applying the
discriminator. - Field-name shadows Python builtin used as a type annotation.
Real example:ConversationResourcehas
object: Literal['conversation']ANDmetadata: object. Mypy's
class-scope name resolution finds the field, not the builtin →
error. Now: when a field is named after a Python builtin AND that
name is used as a type annotation by another field, the
builtin-named field is reordered to come AFTER any such use (within
the same required-vs-optional cohort, so pydantic field ordering
stays valid). Oracle:openai-python'sConversationlays out
id, metadata, object, ...for exactly this reason. allOfmerger was too strict at spec scale. Rejected
real-worldallOfbranches that differed only in (a) cosmetic
fields (description / title / example), (b)$refvs inlined-same-
schema, (c) enum membership where Stainless takes the union
(oracle:openai-pythonemitsLiteral["completed", "incomplete", "failed", "in_progress"]— the union of two branches), or (d) a
T-or-nullvsTlattice (oracle:openai-pythonemits
Optional[int]when one branch is nullable and the other isn't).
Now: cosmetic-strip + one-hop$refresolve + enum-union +
nullable-lattice; remaining structural divergence falls through to
lenient last-wins. Honest trade-off: strict raise was right for
in-repo fixtures, wrong at real-spec scale.cast_to: type | Nonewas too narrow. The
Annotated[Union[V0, V1], Field(discriminator="task")]shape for a
discriminated-union response (real example: openai
audio.transcriptions.create) failed mypy AND silently fell
through to raw dict at runtime. Now: signature widened toAny;
_process_response_dataswitched to a TypeAdapter-first pattern
(trypydantic.TypeAdapter(cast_to), fall back to raw data only on
PydanticSchemaGenerationError) — passes mypy, validates discriminated
unions, andbytes/Noneshort-circuits still work.
Added
- Real-world test:
examples/openai/— the public Stainless-
processedopenai-openapispec (162 paths, 983 schemas, OpenAPI
3.1) + a hand-writtenstainless.ymlcovering ~22 resources (chat,
embeddings, files, audio, fine_tuning, models, moderations, images,
batches, assistants, uploads, vector_stores, conversations,
threads, …). Generates a mypy-clean 172-file SDK that exercises
every capability stainful supports: streaming, JSON, multipart,
binary download, raw binary upload, cursor pagination,
discriminated-union responses, nested subresources, webhook unwrap.
Generated SDK is gitignored; spec committed (content-pinned via the
Stainless CDN SHA in the source URL). - Pydantic-reserved field-name mangling. Field names that clash
withpydantic.BaseModelattributes (schema,copy,dict,
json,model_dump, ...) are now mangled to<name>_and aliased
viaField(alias="<wire>")— same pattern as the existing
Python-keyword mangle. Oracle:openai-pythonemits
schema_: Optional[Dict[str, object]] = FieldInfo(alias="schema", default=None). - Compound brand name override.
openai→OpenAI(not
Openai),openapi→OpenAPI,anthropic→Anthropic,
cloudflare→Cloudflare. Hardcoded table (no risky auto-split
on initialism suffixes — would over-match real words likechai,
tai). Stainless'scustom_casingsblock is the long-term answer
(v1.1 backlog). - Adoption polish. New
docs/migrating-from-stainless.md:
step-by-step migration guide for Stainless customers (after the
Anthropic acquisition wind-down), exhaustive symbol-compat table,
honest gap list with workarounds. README leads with a migration
callout; OpenAI proof point in the Status section. Quickstart
trimmed to the essential install + generate; contributor commands
moved out of the user flow.
Verified
- 84 tests green (was 77 at v0.2.0; 7 new regression tests for
the cross-spec bugs). - mypy 0 on 250 combined source files (172 generated openai SDK +
92 dogfood + 9 emitter). - Drop-in import parity:
from openai.resources.chat.completions import CompletionsResourceresolves against stainful's output. - No regressions on the OneBusAway dogfood: 29/29 of Stainless's
own test files still import unchanged.