Goal
Promote response-side OpenAPI/AsyncAPI spec drift detection from UserWarning to hard-fail. Closes the regression path that allowed Balance.balance_dollars to ship missing in v2.1.0 despite five rounds of review.
This is the single highest-leverage open item on ROADMAP.md (lines 50–54). v2.1's contract tests cover request body drift (hard-fail) but only warn for response model drift, which is why the missing required field on the Balance response model slipped through.
Current state (as of v2.1.0)
Running uv run pytest tests/ -W error::UserWarning --ignore=tests/integration produces 60 failing assertions:
| Bucket |
Count |
Disposition |
| Additive drift on REST response models |
17 models, 46 fields |
42 to backfill, 4 to allowlist |
| Required-mismatch (spec required, SDK Optional) on REST |
18 models, 126 fields |
Out of scope — see PR5 |
| WS additive drift |
11 payloads, 34 fields |
33 to backfill, 1 to allowlist |
| WS required-mismatch |
11 payloads, 78 fields |
Out of scope |
| WS payload without contract map entry |
1 (ErrorPayload) |
Register in PR5 |
Plus a related ROADMAP item: WS envelope models (FillMessage, TickerMessage, etc.) and helpers (ErrorMessage, SubscribedMessage, …) still use extra="ignore". #143 made WS payloads extra="allow" but missed envelopes. Closed in PR5.
Plan — 6 PRs
| # |
Issue |
Title |
Depends on |
Effort |
| PR0 |
#158 |
Wire response-side drift through EXCLUSIONS map (no behavior change) |
— |
~80 lines test infra |
| PR1 |
#159 |
Backfill v3.18.0 fields on Market / Order / Fill (+23 fields) |
#158 |
~80 model + ~30 tests |
| PR2 |
#160 |
Backfill v3.18.0 fields on events / portfolio / historical / incentive (+9 fields) |
#158 |
~50 model + ~20 tests |
| PR3 |
#161 |
Backfill v3.18.0 fields on communications + order_groups (+8 fields) |
#158 |
~30 model + ~15 tests |
| PR4 |
#162 |
Backfill v0.14+ fields on WS payloads (+33 fields including ms timestamps) |
#158 |
~90 model + ~30 tests |
| PR5 |
#163 |
Promote additive drift to hard-fail; register ErrorPayload; WS envelope extra="allow" |
#159–#162 |
~50 lines |
Tracking checklist
PR0 is the load-bearing prerequisite. PR1–PR4 are independent and parallelizable (different files, different models). PR5 is the final flip and pulls the trigger only once the backfills are in place.
Design decisions captured
-
Reuse EXCLUSIONS for response drift. No parallel RESPONSE_EXCLUSIONS — the existing (fqn, name) → Exclusion(reason, kind) shape already supports it. Existing kind values (spec_deprecated, wire_normalization) match the semantics; no new kinds needed.
-
Required-but-Optional axis stays as warning-only. It's a 204-entry policy question, not a v2.1-style hole. The SDK is intentionally permissive because demo has been known to omit spec-required fields. Separate decision; file a follow-up if/when the team wants to tighten.
-
*_ms timestamp fields are surfaced as int, not datetime. The AsyncAPI spec promotes them as the primary form going forward; latency-sensitive consumers want the integer form directly. Existing ts: datetime fields stay.
-
outcome_side / book_side are typed as Literal. Existing SideLiteral / BookSideLiteral aliases in kalshi/models/orders.py already define the values; reuse them.
-
Allowlist is small and reasoned. 5 REST + 1 WS entries cover spec-deprecated fields and one wire-normalization case (Orderbook.orderbook_fp — SDK reshapes the nested object into yes/no level lists at the resource layer).
Allowlist (lands in PR0)
REST:
Market.response_price_units spec_deprecated
Order.action spec_deprecated
CreateRFQRequest.target_cost_centi_cents spec_deprecated
CreateOrderRequest.sell_position_floor spec_deprecated
Orderbook.orderbook_fp wire_normalization
CreateRFQRequest.contracts_fp wire_normalization
WS:
TickerPayload.time spec_deprecated
Verification at each step
- After PR0:
uv run pytest tests/test_contracts.py -W error::UserWarning should show ~53 failures, not 60 (60 baseline minus 7 allowlisted).
- After each backfill PR: a specific subset of
test_additive_drift[*] cases pass without warnings.
- After PR5:
uv run pytest tests/test_contracts.py -v passes with zero UserWarning on additive checks. Deleting a backfilled field locally causes a real failure.
Related
Out of scope
Goal
Promote response-side OpenAPI/AsyncAPI spec drift detection from
UserWarningto hard-fail. Closes the regression path that allowedBalance.balance_dollarsto ship missing in v2.1.0 despite five rounds of review.This is the single highest-leverage open item on
ROADMAP.md(lines 50–54). v2.1's contract tests cover request body drift (hard-fail) but only warn for response model drift, which is why the missing required field on theBalanceresponse model slipped through.Current state (as of v2.1.0)
Running
uv run pytest tests/ -W error::UserWarning --ignore=tests/integrationproduces 60 failing assertions:ErrorPayload)Plus a related ROADMAP item: WS envelope models (
FillMessage,TickerMessage, etc.) and helpers (ErrorMessage,SubscribedMessage, …) still useextra="ignore". #143 made WS payloadsextra="allow"but missed envelopes. Closed in PR5.Plan — 6 PRs
Market/Order/Fill(+23 fields)ErrorPayload; WS envelopeextra="allow"Tracking checklist
PR0 is the load-bearing prerequisite. PR1–PR4 are independent and parallelizable (different files, different models). PR5 is the final flip and pulls the trigger only once the backfills are in place.
Design decisions captured
Reuse
EXCLUSIONSfor response drift. No parallelRESPONSE_EXCLUSIONS— the existing(fqn, name) → Exclusion(reason, kind)shape already supports it. Existingkindvalues (spec_deprecated,wire_normalization) match the semantics; no new kinds needed.Required-but-Optional axis stays as warning-only. It's a 204-entry policy question, not a v2.1-style hole. The SDK is intentionally permissive because demo has been known to omit spec-required fields. Separate decision; file a follow-up if/when the team wants to tighten.
*_mstimestamp fields are surfaced asint, notdatetime. The AsyncAPI spec promotes them as the primary form going forward; latency-sensitive consumers want the integer form directly. Existingts: datetimefields stay.outcome_side/book_sideare typed asLiteral. ExistingSideLiteral/BookSideLiteralaliases inkalshi/models/orders.pyalready define the values; reuse them.Allowlist is small and reasoned. 5 REST + 1 WS entries cover spec-deprecated fields and one wire-normalization case (
Orderbook.orderbook_fp— SDK reshapes the nested object intoyes/nolevel lists at the resource layer).Allowlist (lands in PR0)
Verification at each step
uv run pytest tests/test_contracts.py -W error::UserWarningshould show ~53 failures, not 60 (60 baseline minus 7 allowlisted).test_additive_drift[*]cases pass without warnings.uv run pytest tests/test_contracts.py -vpasses with zeroUserWarningon additive checks. Deleting a backfilled field locally causes a real failure.Related
extra="allow"(closed by PR5)extra="allow"policy (this stack extends it to WS envelopes)Balance.balance_dollarsregression that motivated this workOut of scope
$refresolution in body-schema drift (Resolve nested $ref pointers in request-body schema drift check #53)json={}workaround under prod credentials (Verify json={} workaround under prod credentials (subaccounts.create, communications.confirm_quote, order_groups.reset/trigger) #45)