Search

↑↓ to move↵ to openesc to closetop 6 documentation results

Book a demoTry TokenTriage

In development · Explore the product preview. Join the launch list or book a walkthrough.

Browse docs

Python ↔ TypeScript parity

What is identical across the two bindings, what differs, and the reason each difference was kept rather than fixed.

ReferenceLast updated

On this page

Most of this section teaches both languages at once, which is only defensible if the two bindings genuinely behave the same. This page is where that claim is checked, and where it stops.

Three behaviours are byte-identical by construction. Everything else is listed below, with the reason it was kept rather than made uniform.

What is identical

Behaviour Held identical by
the emitted header map 27 emission vectors, compared byte for byte
the parsed receipt 19 receipt vectors, compared as a canonical mapping
money rendering 12 render vectors, compared character for character
the outcome and feedback request bodies 12 more vectors, compared byte for byte
every diagnostic code frozen; assertions match on the code, not the prose

Both bindings execute all 70 vectors on every build, so those rows are checked rather than asserted. The contract covers how.

A demonstration rather than a claim: the same request through both SDKs, against the same proxy, printing the same cost string.

# Python                                    # TypeScript
agent      : planner                        agent      : planner
step class : user_turn                      step class : user_turn
cost       : $0.000027 (snapshot            cost       : $0.000027 (snapshot
             litellm@2026-07-14#179affb)                 litellm@2026-07-14#179affb)

What differs

Synchronous versus async

Python’s ledger and check calls are synchronous. TypeScript’s return promises.

row = ledger.fetch(rcpt, wait_s=3)
const row = await tt.ledger.fetch(rcpt, { waitS: 3 });

An idiom choice per language rather than a capability difference. Emission and receipt parsing are synchronous in both, because neither touches the network.

Scope form

Python’s scopes are context managers and decorators. TypeScript’s take a callback.

with tt.step(agent="planner"):
    ...

@tt.observe(agent="planner")
def plan(): ...
await tt.step({ agent: "planner" }, async () => { ... });

TypeScript has no decorator form and no observe. The callback form is the only one that can hold an AsyncLocalStorage frame, so this difference is imposed by the runtime rather than chosen.

Reaching a cost

TypeScript models Cost as a discriminated union, so cost.usd is a compile error until you narrow on cost.state === "priced". Python keeps one class whose off-state fields are present and None.

Python’s invariant is that usd, estimated and pricing_snapshot are None when the state is not priced. Making them raise instead would add a new runtime failure mode inside the host application, to buy a guarantee Python’s type system cannot enforce statically anyway.

Numbers in ledger rows

Money is safe in both, by different means, and the difference is visible on every other number.

TypeScript decodes ledger payloads preserving every number as its exact source literal, which means as a string. JavaScript has no float-parsing hook, and a reviver only ever sees an already-lossy float, so preserving the literal is the only way to keep a price exact.

Python decodes floats as Decimal, which touches floats only and leaves integers alone.

# the same row, fetched by each binding
Python      row.usage['total_tokens'] == 120        row.cost.usd == Decimal('0.000027')
TypeScript  row.usage.total_tokens === "120"        row.cost.usd === "0.000027"

Money is identical either way, which is the field this protects. A caller reading a non-money number out of a raw block has to expect the difference.

A failed request through Python’s httpx adapter

The implicit per-request step closes with an error status everywhere except Python’s httpx adapter, where the record is absent instead.

Adapter On a transport error
Python requests a step record with status=error
TypeScript fetch a step record with status=error
Python httpx no step record

httpx’s documented hook interface has a request hook and a response hook and no error hook. Closing the gap would mean reaching into a private transport attribute, contradicting the rule the adapter is built on. What it means for you is on adapters.

A guardrail refusal: raised in Python, narrowed in TypeScript

Binding What a guardrail refusal looks like
Python raise GuardrailRefusal from the SDK’s transport; except GuardrailRefusal
TypeScript the official PermissionDeniedError / InternalServerError, read with guardrailRefusal(error)

The fields, the recognition rule and the written sentence are identical, and both bindings are pinned against one shared fixture file, so neither can drift into a different reading of the same wire. What differs is how you reach them.

The provider packages force it. openai-python wraps only httpx.RequestError and re-raises everything else unwrapped, so a distinct class survives the trip. openai-node converts anything thrown from a custom fetch into APIConnectionError, so a class thrown there would reach you as “connection error”, strictly worse than the status error it replaced. TypeScript instead rewrites the refusal body so the official error carries the reason, and exports a narrower.

See Python and TypeScript.

Closed vocabularies are stricter in TypeScript

TypeScript narrows Mode, StepClass, StepStatus and the check-finding level to literal unions, so a typo in a comparison fails to compile. The new Python scope and lifecycle APIs also use literal types.

The field sets are at full parity — the record, report, acknowledgement and receipt types were compared member by member and match, including members that are properties rather than fields. Only the strictness differs. These values are read-only projections of server bytes, so narrowing buys a typo check on the comparison side at the cost of changing a frozen typed surface.

Origin matching normalises differently

Python’s adapter origin check collapses case, a trailing dot, and internationalised names to punycode, but not percent-encoded hosts or alternative IP-literal spellings. TypeScript uses the platform URL object, which normalises more.

The direction matters. For this check, over-normalising is the hazard: a false match injects identity somewhere it should not go. The residual failure mode is a skipped injection and lost telemetry rather than a leak.

Naming, and one unshielded call

Python is snake_case, TypeScript is camelCase, applied consistently: fetch_trace and fetchTrace, last_receipt and lastReceipt.

ledger.feedback() is the one ledger call that is not shielded in either binding. It raises or rejects on failure, because a silently dropped write is worse than a loud failure. That is parity, not a difference, and it is listed here because it surprises people.

What is not different, despite looking it

Looks like a difference Is not
Python has more adapters TypeScript’s fetch covers what httpx and requests cover separately
TypeScript’s package has subpaths, Python has extras the same opt-in shape, spelled per ecosystem
the two diagnostic texts differ for two codes a bounded substitution: the API name and the absence literal
init() takes different argument shapes both carry the same fields, including the default dimensions

That last row was a genuine gap once. TypeScript’s init() had no dimension fields, so a program ported from Python silently lost its process-wide defaults. It was closed; both now take agent, user, tenant, feature and tags.

Where this list comes from, and how to check it is current

The divergences here are drawn from docs/SDK_PARITY.md in the product repository, which carries a per-row cite into the shipped code or the test that pins it. The ones on this page were also reproduced against both bindings while it was written.

If you need to know whether a difference still exists, the test names in that document are the place to look, not this page. Documentation of a moving surface is a snapshot; the tests are the gate.