Skip to content

Commit c67d80d

Browse files
authored
feat(api): generate REST models from pinned OpenAPI spec (#690)
### AI Summary Implements step 1 of #683. This does not add public REST methods. It adds two type namespaces: - `braintrust.api._generated.models`: private generated `TypedDict`s and type aliases - `braintrust.api.types`: the public REST type namespace, empty until resource APIs are added The generated module is derived from committed inputs: ``` openapi/spec.json + openapi/config.json | v validation | v datamodel-code-generator + Ruff | v braintrust/api/_generated/models.py ``` `make generate-api-client` runs that pipeline. `make check-api-client-codegen` runs it in a temporary directory and fails if the committed output differs, so CI catches hand edits and stale models. Builds use the committed models and do not run code generation. Spec updates are manual for now: update the pinned upstream commit and spec SHA-256 in `openapi/config.json`, fetch the snapshot, regenerate, and review the spec and model diffs together. Automated update PRs are deferred to step 7.
1 parent f3f2f8d commit c67d80d

23 files changed

Lines changed: 42430 additions & 12 deletions

‎.gitattributes‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
py/src/braintrust/api/_generated/** linguist-generated=true

‎.github/actions/setup-python-env/action.yml‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ description: "Checkout, configure mise, and install dev dependencies for a given
33

44
inputs:
55
python-version:
6-
description: "Python version to install (e.g. 3.12)"
7-
required: true
6+
description: "Python version to install (e.g. 3.12). Defaults to the .tool-versions pin."
7+
required: false
8+
default: ""
89

910
runs:
1011
using: "composite"
@@ -20,8 +21,8 @@ runs:
2021
with:
2122
cache: true
2223
experimental: true
23-
install_args: python@${{ inputs.python-version }}
24+
install_args: ${{ inputs.python-version && format('python@{0}', inputs.python-version) || '' }}
2425
- name: Install dependencies
2526
shell: bash
2627
run: |
27-
mise exec python@${{ inputs.python-version }} -- make -C py install-dev
28+
mise exec ${{ inputs.python-version && format('python@{0}', inputs.python-version) || '' }} -- make -C py install-dev

‎.github/workflows/checks.yaml‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,17 @@ jobs:
3232
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
3333
- run: bash scripts/ensure-pinned-actions.sh
3434

35+
api-codegen:
36+
runs-on: ubuntu-24.04
37+
timeout-minutes: 10
38+
steps:
39+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
40+
- name: Setup Python environment
41+
uses: ./.github/actions/setup-python-env
42+
- name: Test generator and check committed output
43+
run: |
44+
mise exec -- make -C py test-api-codegen check-api-client-codegen
45+
3546
static_checks:
3647
runs-on: ubuntu-24.04
3748
timeout-minutes: 20
@@ -142,7 +153,8 @@ jobs:
142153
shell: bash
143154
run: |
144155
mise exec python@${{ matrix.python-version }} -- python ./py/scripts/nox-matrix.py ${{ matrix.shard }} 6 \
145-
--exclude-static-checks
156+
--exclude-static-checks \
157+
--exclude-session test_api_codegen
146158
147159
adk-py:
148160
uses: ./.github/workflows/adk-py-test.yaml
@@ -178,6 +190,7 @@ jobs:
178190
needs:
179191
- lint
180192
- ensure-pinned-actions
193+
- api-codegen
181194
- static_checks
182195
- smoke
183196
- nox
@@ -204,6 +217,7 @@ jobs:
204217
205218
check_result "lint" "${{ needs.lint.result }}"
206219
check_result "ensure-pinned-actions" "${{ needs['ensure-pinned-actions'].result }}"
220+
check_result "api-codegen" "${{ needs['api-codegen'].result }}"
207221
check_result "static_checks" "${{ needs.static_checks.result }}"
208222
check_result "smoke" "${{ needs.smoke.result }}"
209223
check_result "nox" "${{ needs.nox.result }}"

‎.github/workflows/update-session-weights.yaml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ jobs:
3131
run: |
3232
mise exec python@3.10 -- python ./py/scripts/nox-matrix.py ${{ matrix.shard }} 4 \
3333
--exclude-static-checks \
34+
--exclude-session test_api_codegen \
3435
--output-durations measured-durations-${{ matrix.shard }}.json
3536
- name: Upload measured durations
3637
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1

‎.pre-commit-config.yaml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ exclude: >
22
(?x)^(
33
py/src/braintrust/_generated_types\.py
44
|py/src/braintrust/generated_types\.py
5+
|py/src/braintrust/api/_generated/.*
56
)$
67
78
repos:

‎openapi/README.md‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Pinned Braintrust OpenAPI specification
2+
3+
`spec.json` is a committed snapshot of the public specification from
4+
[`braintrustdata/braintrust-openapi`](https://github.com/braintrustdata/braintrust-openapi).
5+
`config.json` pins the full upstream commit, snapshot SHA-256, generator tools, generator flags, and
6+
explicit endpoint exclusions. The generator scripts live in `py/scripts/`. Builds and package installation use the committed generated source and never fetch or run
7+
code generation.
8+
9+
From `py/`, validate and regenerate the private models offline with:
10+
11+
```bash
12+
make generate-api-client
13+
make check-api-client-codegen
14+
```
15+
16+
The check regenerates into a temporary directory and does not modify the worktree.
17+
18+
To fetch the configured upstream commit explicitly:
19+
20+
```bash
21+
make fetch-openapi-spec
22+
```
23+
24+
For an existing local checkout, set `BRAINTRUST_OPENAPI_ROOT` to its root. The checkout must be at the
25+
commit pinned in `config.json`, and its specification must have the pinned hash:
26+
27+
```bash
28+
BRAINTRUST_OPENAPI_ROOT=../../braintrust-openapi make fetch-openapi-spec
29+
```
30+
31+
To update the snapshot, first update the commit and SHA-256 in `config.json`, then fetch, regenerate,
32+
and review both the upstream spec diff and generated model diff. Fix specification defects upstream
33+
rather than adding Python-side normalization beyond CORS `OPTIONS` removal and configured exclusions.

‎openapi/config.json‎

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
{
2+
"schema_version": 1,
3+
"spec": {
4+
"repository": "braintrustdata/braintrust-openapi",
5+
"path": "openapi/spec.json",
6+
"commit": "9daf27f19d9e0340304d7a3e7d0edb28380b94c6",
7+
"sha256": "5ec753c0263c0c44cd04f741edfc7e8bad491cc25a2113d029e84edc076520f0"
8+
},
9+
"tools": {
10+
"datamodel-code-generator": "0.72.4",
11+
"ruff": "0.15.21",
12+
"python": "3.14"
13+
},
14+
"model_generator": {
15+
"flags": [
16+
"--input-file-type=openapi",
17+
"--output-model-type=typing.TypedDict",
18+
"--target-python-version=3.10",
19+
"--use-union-operator",
20+
"--enum-field-as-literal=all",
21+
"--use-generic-container-types",
22+
"--use-field-description",
23+
"--strict-nullable",
24+
"--parent-scoped-naming",
25+
"--no-use-closed-typed-dict",
26+
"--disable-future-imports",
27+
"--formatters=ruff-format"
28+
]
29+
},
30+
"endpoint_generator": {
31+
"schema_version": 1,
32+
"skip_tags": {
33+
"Proxy": {
34+
"reason": "Proxy endpoints stream provider-specific payloads and remain on the specialized proxy path.",
35+
"operation_ids": [
36+
"proxychatCompletions",
37+
"proxycompletions",
38+
"proxyauto",
39+
"proxyembeddings",
40+
"proxycredentials",
41+
"proxy{path+}"
42+
]
43+
}
44+
},
45+
"supported_success_statuses": [
46+
"200",
47+
"201",
48+
"202",
49+
"204"
50+
],
51+
"supported_request_media_types": [
52+
"application/json"
53+
],
54+
"supported_response_media_types": [
55+
"application/json",
56+
"text/plain"
57+
]
58+
}
59+
}

0 commit comments

Comments
 (0)