Skip to content

Commit 61fedc6

Browse files
authored
fix(models): relax subaccount-number request constraint (drop le=32) (#164) (#169)
Nightly integration test `test_transfer_between_subaccounts` failed with: pydantic_core._pydantic_core.ValidationError: 1 validation error for ApplySubaccountTransferRequest to_subaccount Input should be less than or equal to 32 [type=less_than_equal, input_value=41, input_type=int] Root cause ---------- The demo server created an ephemeral subaccount and returned `subaccount_number=41`. The test then tried to use that value as `to_subaccount` in a transfer, but the SDK refused client-side because 7 request-side model fields carried `le=32`. The `le=32` bound was added in v0.11.0 from a code-review pass that read the spec's prose description ("0 for primary, 1-32 for numbered subaccounts") as a hard limit. The actual OpenAPI schema defines no `minimum`/`maximum` constraints on any of these fields. Demo has clearly extended past 32 in practice, and response-side subaccount numbers were already unbounded — the asymmetry was the bug. Fix --- Drop `le=32` from 7 request-side model fields, keep `ge=0`: ApplySubaccountTransferRequest.{from,to}_subaccount UpdateSubaccountNettingRequest.subaccount_number CreateOrderRequest.subaccount BatchCancelOrdersV2RequestOrder.subaccount CreateRFQRequest.subaccount CreateQuoteRequest.subaccount Server is the source of truth on the upper bound. Negative values still fail-fast at the SDK boundary; out-of-range positives now round-trip cleanly. Docstrings on ApplySubaccountTransferRequest and SubaccountsResource updated to drop the "1-32" claim and explain the constraint asymmetry. Tests ----- Replaced `test_transfer_request_rejects_out_of_range_subaccount` (asserted `to_subaccount=33` failed) with `test_transfer_request_accepts_subaccount_above_32` (asserts `41` round-trips). The existing `test_transfer_request_rejects_negative_subaccount` keeps the `ge=0` coverage; the new test locks in the new contract so a future code- review pass can't re-introduce the bound from prose alone. Verification ------------ uv run pytest tests/test_subaccounts.py -> 43 passed uv run pytest tests/ --ignore=tests/integration -> 2023 passed (unchanged) uv run mypy kalshi/ -> Success uv run ruff check . -> All checks passed The failing integration test cannot be reproduced locally without hitting demo with KALSHI_KEY_ID, but the unit-test regression guard (`accepts_subaccount_above_32`) directly mirrors the failure mode from the nightly run. Closes #164.
1 parent 8f98e63 commit 61fedc6

6 files changed

Lines changed: 48 additions & 19 deletions

File tree

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,23 @@ v3.18.0 / v0.14 fields and promotes additive drift to a hard CI failure.
3535
- Required-but-Optional drift stays as warning-only (~204 entries;
3636
separate policy decision).
3737

38+
### Fixed
39+
40+
- Subaccount-number request fields no longer cap at 32. Demo allocates
41+
ephemeral subaccount numbers above 32 (observed: 41), but the SDK was
42+
rejecting them client-side with a ``ValidationError`` because seven
43+
request-side model fields carried a ``le=32`` bound derived from spec
44+
prose. The actual OpenAPI schema defines no upper bound; only the
45+
description text mentions ``1-32``. Affected fields:
46+
``ApplySubaccountTransferRequest.{from,to}_subaccount``,
47+
``UpdateSubaccountNettingRequest.subaccount_number``,
48+
``CreateOrderRequest.subaccount``,
49+
``BatchCancelOrdersV2RequestOrder.subaccount``,
50+
``CreateRFQRequest.subaccount``,
51+
``CreateQuoteRequest.subaccount``.
52+
The ``ge=0`` lower bound is unchanged. Unblocks the
53+
``test_transfer_between_subaccounts`` nightly integration test (#164).
54+
3855
## 2.1.0 — 2026-05-18
3956

4057
OpenAPI spec sync from v3.13.0 → v3.18.0. Adds the V2 event-market

kalshi/models/communications.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ class CreateRFQRequest(BaseModel):
158158
)
159159
replace_existing: bool | None = None
160160
subtrader_id: str | None = None
161-
subaccount: int | None = Field(default=None, ge=0, le=32)
161+
subaccount: int | None = Field(default=None, ge=0)
162162

163163
model_config = {"extra": "forbid"}
164164

@@ -200,7 +200,7 @@ class CreateQuoteRequest(BaseModel):
200200
yes_bid: DollarDecimal
201201
no_bid: DollarDecimal
202202
rest_remainder: bool
203-
subaccount: int | None = Field(default=None, ge=0, le=32)
203+
subaccount: int | None = Field(default=None, ge=0)
204204
post_only: bool | None = None
205205

206206
model_config = {"extra": "forbid"}

kalshi/models/orders.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,7 @@ class CreateOrderV2Request(BaseModel):
414414
post_only: bool | None = None
415415
cancel_order_on_pause: bool | None = None
416416
reduce_only: bool | None = None
417-
subaccount: int | None = Field(default=None, ge=0, le=32)
417+
subaccount: int | None = Field(default=None, ge=0)
418418
order_group_id: str | None = None
419419
exchange_index: int | None = None
420420

@@ -557,7 +557,7 @@ class BatchCancelOrdersV2RequestOrder(BaseModel):
557557
"""Single entry in BatchCancelOrdersV2Request.orders."""
558558

559559
order_id: str
560-
subaccount: int | None = Field(default=None, ge=0, le=32)
560+
subaccount: int | None = Field(default=None, ge=0)
561561
exchange_index: int | None = None
562562

563563
model_config = {"extra": "forbid"}

kalshi/models/subaccounts.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,17 @@ class ApplySubaccountTransferRequest(BaseModel):
2222
2323
``amount_cents`` is integer cents per spec (matches the ``buy_max_cost``
2424
convention on ``CreateOrderRequest``). Pass ``500`` for $5.00, never
25-
a Decimal. ``from_subaccount`` and ``to_subaccount`` use ``0`` for
26-
the primary account and ``1-32`` for numbered subaccounts.
25+
a Decimal. ``from_subaccount`` / ``to_subaccount`` use ``0`` for the
26+
primary account and a positive integer for numbered subaccounts. The
27+
server is the source of truth for the upper bound: spec describes
28+
``1-32`` in prose but defines no JSON-schema maximum, and demo has
29+
been observed allocating values above 32. The SDK validates only the
30+
lower bound (``ge=0``) so server-assigned numbers always round-trip.
2731
"""
2832

2933
client_transfer_id: UUID
30-
from_subaccount: int = Field(ge=0, le=32)
31-
to_subaccount: int = Field(ge=0, le=32)
34+
from_subaccount: int = Field(ge=0)
35+
to_subaccount: int = Field(ge=0)
3236
amount_cents: int = Field(gt=0)
3337

3438
model_config = {"extra": "forbid"}
@@ -79,7 +83,7 @@ class SubaccountTransfer(BaseModel):
7983
class UpdateSubaccountNettingRequest(BaseModel):
8084
"""Body for PUT /portfolio/subaccounts/netting."""
8185

82-
subaccount_number: int = Field(ge=0, le=32)
86+
subaccount_number: int = Field(ge=0)
8387
enabled: bool
8488

8589
model_config = {"extra": "forbid"}

kalshi/resources/subaccounts.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,9 @@ def _build_update_netting_body(
9090
class SubaccountsResource(SyncResource):
9191
"""Sync subaccounts API.
9292
93-
Subaccount 0 is the primary account; 1-32 are numbered subaccounts.
93+
Subaccount 0 is the primary account; positive integers identify numbered
94+
subaccounts (spec prose says ``1-32`` but defines no JSON-schema upper
95+
bound, and demo has been observed allocating numbers above 32).
9496
POST /portfolio/subaccounts spins up the next subaccount with an
9597
empty body (spec takes no request payload).
9698
"""

tests/test_subaccounts.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -190,15 +190,21 @@ def test_transfer_request_rejects_zero_amount(self) -> None:
190190
amount_cents=0,
191191
)
192192

193-
def test_transfer_request_rejects_out_of_range_subaccount(self) -> None:
194-
with pytest.raises(ValidationError):
195-
ApplySubaccountTransferRequest(
196-
client_transfer_id=_TEST_XFER_ID,
197-
from_subaccount=0,
198-
to_subaccount=33,
199-
amount_cents=100,
200-
)
201-
193+
def test_transfer_request_accepts_subaccount_above_32(self) -> None:
194+
"""Regression guard for #164: demo allocates subaccount numbers above 32.
195+
196+
Spec describes ``1-32`` in prose but defines no JSON-schema maximum,
197+
and an integration test caught the SDK rejecting a server-assigned 41
198+
before the request could leave the client. The SDK validates only the
199+
lower bound (``ge=0``); the server is the source of truth on the upper.
200+
"""
201+
req = ApplySubaccountTransferRequest(
202+
client_transfer_id=_TEST_XFER_ID,
203+
from_subaccount=0,
204+
to_subaccount=41,
205+
amount_cents=100,
206+
)
207+
assert req.to_subaccount == 41
202208
def test_update_netting_request_serializes(self) -> None:
203209
req = UpdateSubaccountNettingRequest(subaccount_number=2, enabled=True)
204210
body = req.model_dump(exclude_none=True, by_alias=True, mode="json")

0 commit comments

Comments
 (0)