v0.2.0 — seven product-quality fixes
Seven product-quality fixes verified vs the real Stainless SDKs (openai-
python / anthropic-sdk-python / onebusaway-python-sdk) — each its own
CI-gated commit. Quality bar: resource-method recall 1.00,
method-signature match 1.00, model-name recall 0.95, generated SDK
mypy-clean, 29/29 of Stainless's own OneBusAway test files import
unchanged, end-to-end behavioral conformance for streaming + cursor
pagination + multipart + binary download + binary upload + webhook
unwrap. Drop-in for stainless.yml; not affiliated with Stainless or
Anthropic.
Added
- Webhook unwrap with typed discriminated event union (Standard Webhooks
scheme). Astainless.ymlmethod withtype: webhook_unwrapnow
generatesclient.webhooks.unwrap(payload, headers, *, secret, tolerance=300)returning the typed event variant (not just a parsed
dict) — pydantic-discriminatedAnnotated[Union[…], PropertyInfo( discriminator=…)]built from the configevent_types:block. Sibling
client.webhooks.verify_signature(...)for verify-without-parse.
Signature scheme = Standard Webhooks (standardwebhooks.com): HMAC-
SHA256 overf"{webhook_id}.{timestamp}.{body}", base64, multi-v1,<sig>
header,whsec_base64-decoded secret, constant-time compare, ±tolerance
replay window. Algorithm verified against the openai-python oracle
(resources/webhooks/webhooks.py); the verification helper is vendored
once into<pkg>/_core/_webhooks.py(single source). New
InvalidWebhookSignatureErroris exported from the package root for
drop-inexceptcompatibility. Previously the emitter ignored
type: webhook_unwrapand would have generated a brokenself._post( f"")(latent: no fixture triggered it). IR change:event_types$refs
are now resolved into IRModelRefs so the emitter can render the typed
union. Newtests/fixtures/webhooks/+ 10-casetests/test_webhook_unwrap.py
(typed variant discrimination,whsec_-decode, bad sig, stale timestamp,
missing header, root-symbol export). Known scope boundary: the
secret=kwarg is required (no client-levelwebhook_secret/env-var
fallback yet). - Raw binary request bodies (
application/octet-streamuploads).
S3-stylePUT object, GitHub release-asset upload, and similar raw
uploads now work end-to-end: the emitter generatesbody: bytes,
the runtime sends it verbatim with
Content-Type: application/octet-stream, sync + async. Fixes a
latent codegen flaw — previously the emitter wrapped the bytes in
_body = {"None": body}and calledto_jsonableon it; no fixture
or dogfood exercised the path, so nothing in the wild was hitting
it. Newtests/fixtures/upload_binary/+tests/test_binary_upload.py
asserts the wire body is the exact bytes and the content-type is
octet-stream. Known scope boundary: endpoints declaring multiple
request content types on the same op (JSON OR multipart OR octet-
stream on the same op) still pick the first match; the
alternative-content case has no public Stainless oracle and is
deferred — separate work. - Binary responses. Non-JSON 200s (audio/, octet-stream, image/, …) used to get no type and be JSON-parsed (mangled). Now download endpoints (e.g. OpenAI
audio.speech) return rawbytes, sync + async. Fixture + conformance test; mypy-clean. - Multipart / file upload (RESEARCH §4 #10).
multipart/form-data
bodies were emitted as one opaque param and sent as JSON (broken for
real uploads, e.g. OpenAI files/audio). Now: fields are expanded, binary
fields typedFileTypes, and the runtime sends real multipart (file in
files, scalars indata), sync + async. New fixture + conformance
test; generated SDK stays mypy-clean. - Auto-pagination (RESEARCH §4 #1). Paginated
listendpoints now
generateSync/AsyncCursorPage[Item]returns;for x in client.x.list():
(andasync for) transparently walks every page over the vendored
runtime. Was entirely missing (the emitter ignoredpaginated:). New
conformance fixture + sync/async behavioral test. Generated SDK stays
mypy-clean.
Fixed
- Cursor pagination wire param is now config-driven. Previously the
runtime hard-coded?cursor=<id>on next-page requests — silently broken
against any real openai-/anthropic-style API expecting?after=<last_id>
(the actual StainlessSyncCursorPageshape). Now the IR derives the
cursor param name and the response cursor field from the stainless config
request:/response:blocks; the emitter passes them through; the
runtime uses them with safe defaults (after, thennext_cursor, then
last-itemid). One generic_CursorPagenow covers the five forward-
only cursor variants in the openai+anthropic oracles (SyncCursorPage,
SyncConversationCursorPage,SyncNextCursorPage,SyncTokenPage,
SyncPageCursor) — same algorithm, different wire names. New fixture
(tests/fixtures/paginated_after/) + conformance test asserting the
wire request actually says?after=2from alast_idresponse;
originalcursorfixture still passes. Known scope boundary:
anthropic's bi-directionalSyncPage(choosesbefore_idvsafter_id
from the initial request) is still forward-only here — separate algorithm,
separate work.
Improved (model fidelity vs the real Stainless SDK)
- Resource→type-name prefix now singularizes the last word
(trip-details→TripDetailRetrieveResponse;client.chat.completions
→CompletionCreateResponse, like OpenAI'sChatCompletion) while the
resource class stays plural. Narrowly scoped —agencies_with_coverage
et al. unchanged. - Result: model-name recall 0.90 → 0.95, method-signature match
0.99 → 1.00, and 29/29 (100%) of Stainless's own test files now
import unchanged against stainful's output (was 28/29).