Docs / SDKs

SDKs

Generated TypeScript and Python clients.

Source: docs/initiatives/memory-api/SDKS.md — this page is rendered from it, not copied. Generated from the contract, never written by hand.

Date: 2026-07-29 · Backlog item S3 · Script: scripts/generate_sdks.sh

Two SDKs — TypeScript and Python — are functions of openapi.yaml. Nobody writes one. Nothing generated is committed. One command produces both, and the command fails if the contract is invalid or either SDK does not build.


Run it

cURL
cd ~/Documents/brainComputer/brain-platform
scripts/generate_sdks.sh

Real output, re-run 2026-07-29 from a clean generated tree:

code
── toolchain
── validating docs/initiatives/memory-api/openapi.yaml
docs/initiatives/memory-api/openapi.yaml: OK
   every $ref resolves
   contract version 0.1.0
── python
Generating dist/sdk/python/brain-memory-api-client
   python sdk imports: the two calls that are the whole product are present
── typescript
@hey-api/openapi-ts v0.99.0
[Job 1] ✓ dist/sdk/typescript/src · 4 files · 92ms
   typescript sdk typechecks under --strict
── postcondition
   all 15 operations present in both SDKs

generated v0.1.0 from docs/initiatives/memory-api/openapi.yaml
  dist/sdk/python/brain-memory-api-client   88 files
  dist/sdk/typescript                       16 files

The dated 2026-07-27 timing snapshot was 10.5 s cold with warm pip/pnpm caches and 4.1 s warm; the 2026-07-29 run above re-verifies contract shape, imports, typechecking, operation presence, and file counts rather than claiming a new benchmark. The TypeScript side needs pnpm or npx on PATH — this machine has only pnpm, so the npx branch is written but unproven and CI is where it gets its first real run. Python needs nothing but an interpreter ≥3.11: /usr/bin/python3 on macOS is 3.9 and silently resolves the generator to a years-old release instead of failing, so the script picks the interpreter rather than inheriting it.


What you get

code
dist/sdk/                     gitignored, disposable, NEVER hand-edited
  .toolchain/py/              the pinned generators, in their own venv
  python/brain-memory-api-client/   88 Python files · 8,277 lines · installable package
  typescript/                 16 TypeScript files · 3,291 lines · package.json + tsconfig + src/

1,202 lines of contract → 11,568 lines of client, in two languages. That ratio is the whole argument: it is the volume of code nobody has to review, keep in sync, or be wrong in.

All 15 operations are present in both, named by operationIdcreateBrain, getBrain, forgetBrain, getProfile, getCensus, listTraces, getTrace, rememberRecord, getRecord, forgetRecord, listMemories, getMemory, acceptObject, getObject, and createContext.

The TypeScript package has zero runtime dependencies — the fetch client is emitted inline, so there is no third-party package between a caller and the contract.

The Python package builds, installs and imports as part of the run. A generator that emits a tree of files that does not import is a generator that failed quietly; this one is checked.

The knob budget lands in the type system

This is the property that makes generation more than convenience. ContextRequest in the generated Python, unedited:

Python
query: str
purpose: str | Unset = UNSET
as_of: datetime.datetime | Unset = UNSET
budget: ContextRequestBudget | Unset = UNSET
include: list[ContextRequestIncludeItem] | Unset = UNSET
schema: ContextRequestSchema | Unset = UNSET
filter_: ContextRequestFilter | Unset = UNSET
mode: ContextRequestMode | Unset = ContextRequestMode.FAST

Seven read knobs, and mode defaults to fast because the contract says so. An eighth knob appears here, and in types.gen.ts, in the same commit that adds it — visible to every reviewer, in two languages, before check_api_simplicity.py even runs.


The gotcha this built: all three tools fail silently

Found while checking that the validator was not a no-op, 2026-07-27. Point createBrain's requestBody at #/components/schemas/NoSuchSchema and:

Result on a dangling $refExit code
openapi-spec-validatoropenapi.yaml: OK0
openapi-python-clientWARNING … Endpoint will not be generated — the endpoint is dropped0
@hey-api/openapi-tsSkipping unresolvable $ref — the operation is emitted with a hole0

Three tools, three green lights, one SDK missing an operation the contract promises. A naive "validate then generate" script reports success. openapi-spec-validator checks the document against the OpenAPI JSON Schema; it does not resolve local pointers, and neither generator treats an unresolvable one as fatal.

So the script does not trust any of them:

  1. Every $ref is resolved before anything is generated, and an unresolvable one is fatal with the exact JSON path. External (http://…) refs are refused outright — a contract that is only complete over the network is not reproducible.
  2. --fail-on-warning on openapi-python-client, which is off by default. A partial SDK is not a green build.
  3. A postcondition: every operationId in the contract must exist as a module in the Python tree and as an export const in sdk.gen.ts. This is the one check that catches the TypeScript case, because there the file count does not change.

All three were verified to fire, not assumed to (see Verification).

The general form is one the repo already knows: a check that cannot express failure is not a check. "It validated" and "it generated" are both satisfiable by a tool that quietly did less than you asked.


Why generated and not hand-written

A hand-written SDK is a second implementation of the contract, and its failure mode is silent. It keeps compiling after the server changes, because it encodes its author's belief about the API rather than the API. The divergence surfaces at a customer, months later, in the one path nobody wrote a test for. This is the same class of error the repo already spends gates on: a generated FastAPI schema can only ever agree with itself, which is why check_api_conformance.sh fuzzes against the checked-in openapi.yaml. An SDK written by hand reintroduces exactly the problem that check exists to prevent, one layer further out — and further out is worse, because it is the layer the customer touches.

Four consequences that are worth more than the typing they save:

  1. The contract cannot lie about the client. The only way for the SDK to be wrong is for the contract to be wrong — and the contract has two gates on it already.
  2. An ungeneratable spec is a spec bug, caught at build time. Validation and $ref resolution run before generation and the script exits non-zero. A contract that no generator can read is a contract no customer can implement either; better to learn that here.
  3. Surface is visible. See above. Quiet API growth is the thing the simplicity law exists to stop, and generation makes the growth show up as a diff in a typed file.
  4. Marginal cost per language is ~zero. Go, Rust, Kotlin are one more line in the script, not one more artifact to keep honest.

The rule that follows: hand-written code never lives inside dist/sdk/. If an ergonomic wrapper is ever wanted (brain.remember(...) over rememberRecord(client, ...)), it is a separate, committed package that depends on the generated one. The generated tree stays a pure function of the contract, or it is not one.


The generators, and one we declined

ToolPinWhy
Pythonopenapi-python-client0.29.0Reads OpenAPI 3.1 natively. httpx + attrs, typed, py.typed, sync and async.
TypeScript@hey-api/openapi-ts0.99.0Reads 3.1 natively, emits a self-contained fetch client, no runtime deps.
ruff0.15.0openapi-python-client shells out to it to format its output. Pinned into the toolchain so the format step cannot silently skip.
openapi-spec-validator0.7.2The fail-fast. Dev-only, toolchain-only.
typescript5.9.3Peer of the TS generator, and the --strict typecheck of its output.

openapi-generator was evaluated and declined, for two reasons — the first checked here, the second on the record and not verifiable here:

  • It is a JVM tool and this machine has no JVM — checked: java -version fails, /usr/libexec/java_home finds nothing, no JDK under /Library/Java/JavaVirtualMachines. It could not be run, so a build step using it could not be verified — and an unverified build step is how a "generated" SDK quietly becomes a hand-written one, the first time somebody cannot run the script.
  • Its OpenAPI 3.1 support is partial and works by down-conversion to 3.0. This one is the project's own documented position, not something measured here — with no JVM there was no way to test it. If it is out of date, the check is cheap: install a JDK and diff the generated types against the shapes this contract leans on — type: [string, 'null'] (valid_to, text, abstained, next_cursor, if_version), const (the object discriminator on every resource), and 3.1 examples. A generator that has to lower the contract to read it produces a client for a different contract.

Both chosen tools are open source and read 3.1 as written. If a JVM ever lands on the build host and a language is wanted that only openapi-generator covers, add it as a third generator; do not replace these.

The pins are load-bearing: a generator upgrade is an SDK change. Bumping any version above is its own commit, reviewed against the regenerated diff.


Release policy

  1. The SDK version is the contract version. package.json and the Python pyproject.toml both take info.version from openapi.yaml. There is no field anywhere to type a different number into. An SDK cannot claim a version the contract does not have.
  2. Nothing generated is committed. dist/sdk/ is gitignored twice over (.gitignore: dist/ and dist/sdk/). Thousands of generated lines in a pull request is how a real ten-line diff gets missed, and a generated file in the tree is a file somebody eventually edits.
  3. A release is a contract release. Bump info.versioncheck_api_simplicity.py GREEN → check_api_conformance.sh GREEN → generate_sdks.sh GREEN → publish → tag sdk-v<version>. The gates come first; an SDK is downstream of a contract that passed.
  4. A change that fails the simplicity law never ships, in any language. The generator will happily emit an eighth knob. The gate is what refuses it.
  5. Regenerate on every contract change, not on demand. The correct time to discover the spec no longer generates is the commit that broke it.

Not yet automated, stated plainly: the publish step itself. The npm scope @brain is not claimed and the PyPI name is not registered, so generate_sdks.sh stops at a built, typechecked, importable package in dist/sdk/ and does not push anywhere. Wiring npm publish / twine upload into CI is a follow-up to S3, not part of it.


Known warts, not patched

Two name collisions the contract's own nouns cause. Both are real; neither was silently worked around, because working around them means editing the contract to suit a generator:

  • TypeScript exports Record, Object and Error as types — all three shadow globals. A consumer writing import type { Record } from '@brain/memory-api' loses TypeScript's Record<K,V> utility type in that file. Namespace import (import type * as Brain from …) avoids it.
  • Python renames the filter knob to filter_ (ContextRequest.filter_), the generator's standard escape for a builtin.

Neither is worth renaming a resource over. Both belong in the quickstart (S10) so a stranger meets them in the docs rather than in a compiler error.


Verification, 2026-07-27

The contract validates as OpenAPI 3.1 — no errors, nothing patched to make it pass. generate_sdks.sh runs this before it generates anything; to run it alone (after one generate_sdks.sh has built the toolchain):

code
$ dist/sdk/.toolchain/py/bin/python -m openapi_spec_validator docs/initiatives/memory-api/openapi.yaml
docs/initiatives/memory-api/openapi.yaml: OK

That "OK" was itself checked. Against a copy with operationId set to an integer and responses to a string, the same validator exits 1 with Validation Error: 12345 is not of type 'string' and the failing instance path — so the green above is a result, not a default.

And each of the three added guards was made to fire, by running the shipped code against a deliberately broken copy of the contract (#/components/schemas/BrainCreate#/components/schemas/NoSuchSchema) and against an amputated pair of SDKs:

code
$ <shipped $ref check> broken.yaml
   UNRESOLVABLE $ref  #/components/schemas/NoSuchSchema  at  $.paths./brains.post.requestBody.content.application/json.schema
exit 1

$ openapi-python-client generate --path broken.yaml --fail-on-warning …
WARNING parsing POST /brains within brains. Endpoint will not be generated.
exit 1                                              # exit 0 without the flag

$ <shipped postcondition> openapi.yaml <sdk tree with create_brain.py deleted and createContext renamed>
   MISSING OPERATION  python: createBrain (create_brain.py)
   MISSING OPERATION  typescript: createContext
exit 1

The simplicity law is unaffected by this work — no resource, verb, or knob was added, because an SDK is not surface:

code
$ .venv/bin/python scripts/check_api_simplicity.py
  resources        5/5   Brain, Context, Memory, Object, Record
  verbs            6/6   accept, create, forget, get, list, remember
  read knobs       7/7
  write knobs      3/3
GREEN — the simplicity law holds.

The generators live in dist/sdk/.toolchain/py, not in the project .venv. openapi-spec-validator pins referencing<0.37.0 and installing it into .venv downgrades a package the conformance gate resolves through. An SDK build must not be able to perturb a gate, so the toolchain is isolated and the project venv is left exactly as found (pip check: no broken requirements).