From 49b3ff788cea9938f78e055e5ae287b4b9e30e2b Mon Sep 17 00:00:00 2001 From: Glenn Jocher Date: Fri, 11 Sep 2026 15:12:02 +0800 Subject: [PATCH 01/10] Expand AGENTS.md with verified architecture map, commands, consumer contract, and where-to-look guidance --- AGENTS.md | 113 ++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 98 insertions(+), 15 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 1622933..82f6f73 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,6 +2,8 @@ This file provides guidance to AI coding agents (Claude Code, etc.) when working with code in this repository. CLAUDE.md is a symlink to this file. +Ultralytics OpenAPI (`@ultralytics/openapi`, AGPL-3.0, `private: true` — never published to npm, consumed only from Git) turns one OpenAPI 3.0–3.2 contract into two outputs: an interactive static documentation site (Next.js 16 App Router, React 19, TypeScript strict, Tailwind CSS v4, shadcn `base-nova` components over Base UI) and a typed Python SDK (sync + async `httpx` clients). Everything runs on Bun; `uv`/`uvx` run Ruff over generated Python and are required for `bun run generate` and `bun run test`. There is no build step for the library code: `package.json` exports `./openapi` → `lib/openapi.ts` as raw TypeScript. + ## Core Principles (CRITICAL) **Less is more. The simplest solution is the best solution.** The action hierarchy for every change: **Delete > Replace > Add**. @@ -29,19 +31,42 @@ After opening a PR: ## Commands ```bash -bun install # install application and generator dependencies -bun run dev # run the documentation application -bun run sync # fetch the configured OpenAPI contract -bun run generate # generate and format all SDK outputs -bun run typecheck # type-check TypeScript -bun run lint # check formatting and lint rules -bun run knip # find unused files, exports, and dependencies -bun run test # run focused generator tests -bun run build # build the static documentation application +bun install --frozen-lockfile # Bun + uv are the whole toolchain; CI also installs Python 3.11 and 3.14 +bun run sync # scripts/sync.ts: read `source` from openapi.config.json → public/openapi.json (gitignored) +bun run generate # sync → scripts/generate.ts → generated/python (deleted and rewritten) → uvx ruff@0.16.2 format → scripts/headers.ts +bun run dev # sync → next dev (docs at http://localhost:3000; rewrites the nextjs-agent-rules block at the end of this file) +bun run build # sync → next build (static export to out/) → scripts/headers.ts out +bun run typecheck # next typegen && tsgo --noEmit (works without public/openapi.json) +bun run lint # biome check . (lint:fix writes); generated/, public/openapi.json, .next, out are excluded in biome.json +bun run knip # knip --exclude binaries (no knip.json; exports of lib/openapi.ts are public via package.json "exports") +bun run test # bun test — the single test file lib/generators/python.test.ts (20 tests, ~2 s); needs uv + python3 +bun test lib/generators/python.test.ts -t "multipart" # filter tests by name + +# Generated-Python checks that CI runs after `bun run generate` (Python 3.11 and 3.14) +python3 -m compileall -q generated/python/src +uv pip install --system ./generated/python # CI then imports . and Async +uvx ruff@0.16.2 check generated/python + +# Generate for a consumer's configuration (relative paths inside it resolve against that file; output still lands in generated/python) +OPENAPI_CONFIG=../sdk/openapi.config.json bun run generate ``` Run checks through the package scripts. Generated Python additionally supports `python3 -m compileall -q generated/python/src` and `uvx ruff@0.16.2 check generated/python`. +- `dev`, `build`, and `generate` always sync first; `typecheck`, `lint`, `knip`, and `test` do not touch the contract. Never run `next build` directly: both `next.config.ts` (`generateBuildId` hashes `public/openapi.json`) and `app/page.tsx` (`readFileSync("public/openapi.json")`) fail with `ENOENT` without a prior sync. +- Workflows in `.github/workflows/`: + - `ci.yml` (CI) — on PRs, pushes to `main`, a daily cron, and manual dispatch; one `Test` job per Python version (3.11, 3.14), 15-minute timeout. Steps in order: `bun install --frozen-lockfile`; `bun run generate` twice with `diff --recursive` between the two outputs (determinism gate); `bun run typecheck && bun run lint && bun run knip && bun run test && bun run build` (lint IS in CI here); `python -m compileall -q generated/python/src`; `uv pip install --system ./generated/python` followed by importing the configured client and `Async` client; `uvx ruff@0.16.2 check generated/python`. + - `format.yml` (Ultralytics Actions) — on PR open/sync/close/review-request and new issues; runs Ruff on Python, Prettier on YAML/JSON/Markdown/CSS (`.prettierignore` excludes `public/openapi.json`), codespell, Lychee link checks, AI labels and PR summary, and pushes auto-format commits to the PR branch. `python_docstrings: false` is deliberate — generated Google-style docstrings are owned by the generator. + - `cla.yml` — CLA check on PR open/reopen/sync and on `recheck`/signature comments. + - `.github/dependabot.yml` — weekly npm and GitHub Actions update PRs labeled `dependencies`. + +## Conventions + +- Ultralytics-owned PyPI packages use `MAJOR.MINOR.PATCH` versions only; no suffixes. +- License headers (`# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license`) are added automatically by Ultralytics Actions — don't add or revert them manually. +- Generated output must be deterministic and is validated in CI. +- Google-style docstrings, modern type hints, and a 120-character Python line length are formatted by Ruff. + ## Product Boundary (CRITICAL) This repository is a standalone, general-purpose OpenAPI-to-SDK and API documentation product, intended to compete with products such as Stainless and Scalar. Third-party users must be able to generate SDKs and documentation for their own APIs without inheriting Ultralytics application behavior. @@ -52,6 +77,14 @@ This repository is a standalone, general-purpose OpenAPI-to-SDK and API document - Keep consumer customizations reproducible through generation and synchronization. Never hand-edit generated output or make the converter depend on a consumer repository. - Review every change against this boundary. Relocate application-specific work to its owner instead of teaching the converter about one application. +## Downstream Consumers (what a merge to `main` ships immediately) + +Both consumers track this repository's `main` with no pin, so every merged commit is live in their next CI run or build. Treat these surfaces as public API: + +- **`ultralytics/portal`** (Platform docs and contract). `apps/alpha/package.json` script `openapi:main` is exactly `bun add --dev --no-save @ultralytics/openapi@github:ultralytics/openapi#main`; alpha's `dev`, `build`, `api-docs`, and `openapi` scripts run it, and portal CI asserts that string before refreshing. Portal's `scripts/generate-openapi.ts` then calls `(await import("@ultralytics/openapi/openapi")).addPythonCodeSamples(document, { client, environment, package })` on an `openapi: "3.2.0"` document that portal generated itself, and alpha's `next.config.mjs` lists the package in `transpilePackages`. Contract: the `./openapi` export path, the `addPythonCodeSamples(document, PythonCodeSampleConfig)` signature, the `OpenApiDocument` type, `x-codeSamples` entries labeled `"Python SDK"`, and acceptance of 3.2.0 documents. `lib/openapi.ts` must stay import-free and isomorphic — it runs in the browser (`components/api-reference.tsx`), in Bun scripts here, and inside portal's generator and Next build. +- **`ultralytics/sdk`** (`ultralytics-platform` on PyPI). Its `ci.yml` checks out `ultralytics/openapi` at `ref: main` into `.generator/`, runs `bun install --frozen-lockfile` and `OPENAPI_CONFIG=/openapi.config.json bun run generate`, then `diff --recursive sdk/python .generator/generated/python`. Any byte of changed Python output fails every sdk PR until sdk's `Live` job (main push, daily, or dispatch) regenerates and auto-merges an "Update Platform API contract" PR whose branch hash includes this repository's `main` SHA. sdk's config exercises `header`, `python.cli` (`ul` from `cli.py`), `python.authProvider` (`auth.py`), `python.readme`, omits `python.version` (so the version is the contract's `info.version`), and carries an sdk-only `upstream` key that `getConfig` neither validates nor reads. Contract: every key in `lib/config.ts` `OpenApiConfig`, the `generate` script name, the `generated/python` output path, `bun.lock` installing with `--frozen-lockfile`, and deterministic output. +- Before merging a generator change, reproduce the sdk drift check locally: `OPENAPI_CONFIG=/path/to/sdk/openapi.config.json bun run generate && diff -r generated/python /path/to/sdk/sdk/python` — a non-empty diff is expected only when the change intends to alter the published SDK, and it means sdk CI goes red until its `Live` job runs. + ## Architecture - Downstream API docs and SDK consumers must track this repository's `main` branch. Never introduce a commit SHA or tag pin for `ultralytics/openapi` in Portal, SDK, or related automation. @@ -62,6 +95,29 @@ This repository is a standalone, general-purpose OpenAPI-to-SDK and API document - `components/api-reference.tsx` renders the interactive reference from the same shared operation model. API keys remain in browser memory and never appear in copied examples. - The documentation uses shadcn's `base-nova` style with Base UI primitives and Ultralytics design tokens. +Paths below are relative to the repository root. + +**Data flow.** `lib/config.ts` `getConfig()` reads `OPENAPI_CONFIG` or `openapi.config.json`, throws on a missing `source`, `name`, `apiKey.environment`, `python.client`, `python.package`, or `python.project`, defaults `python.install` to `pip install ` and `license` to this repository's AGPL-3.0 `LICENSE`, and resolves relative `source`, `license.file`, `python.readme`, `python.authProvider`, and `python.cli.source` against the config file's directory. `scripts/sync.ts` loads `source` (local file, or `https://` with `redirect: "error"`; `http://` is rejected), requires `openapi`, `info`, and `paths` keys, and writes `public/openapi.json`. Two consumers read that one file: `app/page.tsx` (Server Component, `readFileSync`) hands the document plus `apiKey.environment`, `python`, and `specUrl` to the client `ApiReference`, and `scripts/generate.ts` calls `generatePython(document, config, "generated/python")`. Both go through `lib/openapi.ts` `getOperations(document)`, which flattens `paths` into `ApiOperation[]`: `id` (`operationId`, else `-`, de-duplicated with `-2`, `-3`, …), `tag` (first tag, else `"Other"`), `resource` (`sdkIdentifier(tag)`), `sdkMethod`, parameters merged from path-item and operation level with `$ref`s resolved, resolved `requestBody`/`responses`, and `server` (operation → path item → document). + +- `lib/config.ts` — `OpenApiConfig` (the full configuration schema; README "Configure" documents it for users) and `getConfig()`; `configPath` is also hashed by `next.config.ts`. +- `lib/openapi.ts` (no imports; browser-safe) — + - Types: `JsonSchema`, `Parameter`, `OpenApiDocument`, `ApiOperation`, `SdkArgument`, `PythonCodeSampleConfig`, `ApiAuthentication`. + - Identifiers: `sdkIdentifier` (camelCase and punctuation become `snake_case`, possessives are stripped, Python keywords get a trailing underscore, leading digits get a leading underscore), `allocateSdkIdentifiers` (collision suffixes `_` then `_2`, `_3`, …; reserves `self`, `extra_headers`, `timeout`). + - Method naming: `sdkMethodCandidates` + `SDK_VERBS` (`get`→`retrieve`/`list`, `post`→`create`, `put`/`patch`→`update`, `delete`→`delete`) — `x-sdk-method` wins (validated as a lowercase identifier, unique per resource), else the static path leaf after the resource segment, else the verb; a leafless GET with a path parameter is `retrieve` unless its summary starts with list/view/search. `getOperations` resolves collisions per resource (canonical names first, GET keeps the bare leaf, then `_` / `_`, then `_2` suffixes). + - Arguments: `sdkArguments` — path params are positional-required, everything else keyword; a JSON/form/multipart object body is flattened into per-property keyword arguments unless it contains `oneOf`/`anyOf`, incompatible closed `allOf` branches, `additionalProperties` schemas, or `minProperties`/`maxProperties`, in which case a single `body` argument (`wholeBody: true`) is emitted. `readOnly` properties are skipped. + - Auth: `getAuthentication` accepts only single-scheme security requirements that are `apiKey` in `header` or `http` `bearer` (returns `{ header, prefix }`); `getAuthenticationMode` → `none`/`optional`/`required`. + - Servers: `resolveServerUrl(document, origin, operation?)`, `expandServerUrl` (substitutes `variables[*].default`); relative server URLs resolve against `http://localhost:3000` in the generator and `window.location.origin` in the browser. + - Schemas: `resolveSchema` (local `#/components/schemas/` only, sibling keys and `properties`/`required` merged over the target, cycle-safe), `objectSchema` (merges `allOf` and object-shaped `anyOf`/`oneOf` into one object schema), `schemaExample`/`stringExample`/`schemaMatches` (example synthesis: `example` → `default` → `const` → first matching `enum` value → union/variant walk → format-aware strings), `schemaLabel`, `schemaConstraints`, `schemaFields`. + - Responses: `successMediaEntries`/`successMedia`/`successSchema` (exact `2xx` codes first, else `2XX`; distinct shapes become `oneOf`), `requestMedia` (`preferredMedia` order: `application/json`, `+json`, form/multipart, `text/*`, first listed). + - Samples and requests: `pythonCodeSample`, `addPythonCodeSamples` (rewrites the `"Python SDK"` entry of each operation's `x-codeSamples`), `curlCodeSample` (uses the `YOUR_API_KEY` placeholder), `requestBodyExample`, `buildApiRequest` (URL, headers, and `BodyInit` for the docs "Send" button; refuses cookie parameters and missing required parameters). The docs UI does not render `x-codeSamples`; it regenerates samples live from the current form values. +- `lib/generators/python.ts` — `generatePython(document, config, output)` → `prepare` (calls `getOperations`, then `validateOperation`, which throws `Unsupported …` for: security requirements with no supported scheme, non-`simple` path styles, `allowReserved` query params, array/object header or cookie params, schema-less request bodies, request `encoding`, and mixed JSON/text/binary success media) → `rm -rf output` → writes files. Rendering: `pythonType` (JSON Schema → `str`/`int`/`float`/`bool`/`Sequence[...]` for arguments and `list[...]` for responses/`dict[str, Any]`/`BinaryIO`/`Literal[...]`/`| None`), `docstring` (Google style, `Args:` types in parentheses), `methodSource` (one `self._client.request(...)` call with `params=[*_query_parameter(...)]`, `json=`/`data=_form_data(...)`/`files=`/`content=`, `auth=(header, prefix)`, `text=True` for text responses), `resourceSource`, `modelSource` (functional `TypedDict`s in `types.py` named `Response`, nested `…Item`, `NotRequired` for optional fields, wire keys preserved), `clientSource`/`apiClientSource` (the runtime: `_resolve_api_key`, `NotGiven`/`NOT_GIVEN`, `_path_parameter`, `_query_parameter`, `_form_data`, `_retry_delay`, `SyncAPIClient`/`AsyncAPIClient.request`), `EXCEPTIONS_SOURCE` (`APIError(status_code, body, request_id)` with `.json`, `APIConnectionError`), `publicClientSource` (``/`Async` with `api_key`, `base_url`, `timeout=60.0`, `max_retries=2`, `http_client`, context managers). All Python lives inside TypeScript template literals — escape `\`, `${`, and backticks, and remember Ruff reformats the output afterwards. + - Generated layout: `pyproject.toml` (`uv_build` backend, `httpx>=0.28,<1`, `[tool.ruff] line-length = 120`, `[project.scripts]` only with `python.cli`), `README.md`, `LICENSE`, `src//{__init__.py,_client.py,_exceptions.py,client.py,async_client.py,types.py,py.typed}`, `src//resources/{__init__.py,.py}`, plus optional `_auth.py` (copied `python.authProvider`), `cli.py` (copied `python.cli.source`), and `_cli_metadata.py` (`MULTIPART_FILES` mapping `resource.method` → binary field names of whole-body multipart operations). + - Runtime behavior worth knowing before "fixing" a client bug: the auth header is added only when a key resolved (explicit → `apiKey.environment` → provider; `""` disables) and the operation has auth; retries apply to GET/HEAD/OPTIONS on connection errors and 408/409/429/5xx, plus 429 for JSON-bodied requests, honoring `Retry-After` (≤ 60 s) else `0.5·2^attempt` capped at 8 s; 204/empty → `None`, JSON media → parsed, `text=True` or `text/*` → `str`, else `bytes`. +- `components/api-reference.tsx` (`"use client"`) — `ApiReference` (API key held in React state only, ⌘/Ctrl+K focuses search, selection via URL hash `#operation=`), `OperationNavigation`, `OverviewPanel` (version badges, "Download OpenAPI contract" from `specUrl`, servers, security schemes, Python quick start), `OperationPanel` (parameter inputs, body textarea seeded by `requestBodyExample`, file inputs for binary multipart fields, "Send" = `buildApiRequest` + browser `fetch`, so the target API must allow CORS), `codeExamples` (live cURL + Python), `Markdown` (react-markdown). `components/ui/*` are shadcn `base-nova` copies over `@base-ui/react` — compose with `render={}`, never Radix `asChild`. `components/logo.tsx` + `app/icon.svg` + `--brand-gradient-*` in `app/globals.css` are the only branding hooks. +- `app/` — `layout.tsx` (title `${config.name} Reference`, Geist fonts, skip link), `page.tsx`, `globals.css` (Tailwind v4 CSS-first with oklch tokens; no tailwind config), `icon.svg`. `next.config.ts`: `output: "export"`, `basePath` from `docs.basePath`, and `generateBuildId` = SHA-256 of `app/`, `components/`, `lib/`, `bun.lock`, `package.json`, `next.config.ts`, `postcss.config.mjs`, the config file, and `public/openapi.json`, so `out/` is byte-stable for the same inputs. +- `scripts/` — `sync.ts`, `generate.ts` (Python only; a new language is wired here), `headers.ts` (prepends `header` to `.html`, `.css`, `.js`, `.py`, `.toml` files under the given directories; a no-op when `header` is unset, which is the case for this repository's own config). +- `examples/openapi.json` — OpenAPI 3.1 fixture (7 paths, 8 operations) covering server variables, bearer auth, `allOf`/`anyOf`/`oneOf`, nullable, `writeOnly`, binary multipart, text and empty responses. It is both the `source` of the repository's `openapi.config.json` and the test fixture. + ## Python Output - Follow the OpenAI client shape: one client, grouped resources, keyword arguments, and environment-based authentication. @@ -69,12 +125,39 @@ This repository is a standalone, general-purpose OpenAPI-to-SDK and API document - Generate Google-style docstrings. Types are parenthesized in `Args:`, `Returns:`, and `Raises:` sections. - Generated SDK packages default to AGPL-3.0 and use the license configured in `openapi.config.json`. -## Conventions - -- Ultralytics-owned PyPI packages use `MAJOR.MINOR.PATCH` versions only; no suffixes. -- License headers (`# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license`) are added automatically by Ultralytics Actions — don't add or revert them manually. -- Generated output must be deterministic and is validated in CI. -- Google-style docstrings, modern type hints, and a 120-character Python line length are formatted by Ruff. +## Where to look + +- Wrong resource or method name in the SDK, docs sample, or `x-codeSamples` → `sdkIdentifier`, `sdkMethodCandidates`, and the collision pass in `getOperations` (`lib/openapi.ts`); contracts can pin a name with `x-sdk-method`. +- Wrong keyword arguments or a body that should/shouldn't be flattened → `sdkArguments`; Python-side rendering of those arguments → `methodSource`. +- Wrong Python type, `TypedDict`, or nullability → `pythonType`, `isNullable`, `literalValues`, `modelSource` in `lib/generators/python.ts`. +- `bun run generate` throws `Unsupported …` / `Unknown … reference` → `validateOperation` (python.ts) or `componentName`/`resolveParameter`/`resolveRequestBody`/`resolveResponse` (openapi.ts). Add opt-in, contract-driven support or fix the contract; never special-case one API (Product Boundary). +- Wrong example values in docs or samples → `schemaExample`, `stringExample`, `authoredSchemaExample`, `requestBodyExampleValue`, `pythonCodeSample`, `curlCodeSample`. +- HTTP behavior of generated clients (auth header, retries, encoding, response decoding) → `clientSource`/`apiClientSource`; errors → `EXCEPTIONS_SOURCE`; constructor surface → `publicClientSource`. +- Package metadata (`pyproject.toml`, README, LICENSE, CLI entry point, version validation) → the tail of `generatePython`. +- Docs page bug → `components/api-reference.tsx`; shell/metadata → `app/layout.tsx`; theme tokens → `app/globals.css`; static export/base path → `next.config.ts`. +- New configuration key → `OpenApiConfig` + `getConfig` in `lib/config.ts`, consume it in the generator or page, document it in the "Configure" section of `README.md` and `README.zh-CN.md`. +- New language → `lib/generators/.ts` exporting `generate(document, config, output)` built on `getOperations`/`sdkArguments`, a `` block in `OpenApiConfig`, a call in `scripts/generate.ts`, a sibling `.test.ts`, and matching CI validation steps in `.github/workflows/ci.yml`. +- Header/licensing of emitted files → `scripts/headers.ts` and the `header`/`license` config keys. + +## Gotchas + +- `generatePython` deletes the output directory before writing. Never point `output` at a directory you care about; CI and the sdk repo rely on this to drop stale files. +- Determinism is enforced (CI generates twice and diffs). Resource order follows the contract's path order via `Map` insertion; exports are sorted explicitly (`resourceExports.sort()`). Avoid timestamps, randomness, or unordered iteration in generator code. +- `openapi.config.json` and `examples/openapi.json` are the test fixtures: `python.test.ts` imports the config, expects 8 operations, package `example_api`, client `Example`, the `Repository` URL, authors, and classifiers. Editing either file changes test expectations. +- `bun run test` executes Python: the CLI test runs `uv run --no-project --python python3 --with ` twice. Without `uv` and `python3` on `PATH` that test fails. +- Ruff reformats generated Python after generation, so formatting inside the template literals is irrelevant, but syntax errors only surface in CI's `compileall`/import step — run those locally after touching `python.ts`. +- `python.version` omitted → the contract's `info.version` is used and must match `^\d+(\.\d+)*((a|b|rc)\d+)?(\.post\d+)?(\.dev\d+)?$`, otherwise generation throws `Set python.version …`. +- Every `sync` overwrites `public/openapi.json` (gitignored) from `source`; `source` must be a local path or `https://` (no `http://`, no redirects). A stale `public/openapi.json` is the usual reason docs and `generated/` disagree — re-run `bun run sync`. +- `tsconfig.json` excludes `generated/`, and Biome/Prettier ignore `generated/` and `public/openapi.json`; `tsconfig.tsbuildinfo`, `.next/`, `out/`, `.idea/` are gitignored — never commit them. +- `format.yml` reformats JSON/Markdown/YAML with Prettier and pushes to your branch; `git pull --rebase` before every push. +- `next dev` rewrites the `nextjs-agent-rules` block at the bottom of this file (see the block's own note); commit it rather than reverting it. +- `knip` has no config file: anything exported from `lib/openapi.ts` counts as public API (package `exports`), while an unused export elsewhere fails `bun run knip`. + +## Tests + +- One test file: `lib/generators/python.test.ts` (`describe("Python generator")`). `beforeAll` generates into a `mkdtemp` directory from `examples/openapi.json` with the repository `openapi.config.json`, after planting `stale.py` to prove the output is wiped. Tests clone the fixture with `structuredClone`, mutate schemas to hit edge cases, and assert on generated source text, thrown messages, `getOperations` naming, `sdkArguments`, `schemaExample`, `buildApiRequest`, and `curlCodeSample`. Separate temp directories cover `python.cli` (runs the generated CLI through `uv run`) and `python.readme`/`python.authProvider`. +- CI is the only place the generated package is installed and imported (`uv pip install --system ./generated/python`, then `importlib.import_module(package)` and `getattr(m, client)`/`getattr(m, "Async" + client)`), compiled, and Ruff-checked on Python 3.11 and 3.14. +- There are no browser or E2E tests; `bun run typecheck`, `bun run lint`, and `bun run build` are the only checks on `app/` and `components/`. Verify UI changes in `bun run dev`. From e541aabc40c09ea7e0cb6423c9ba7a2d4cfbf694 Mon Sep 17 00:00:00 2001 From: UltralyticsAssistant Date: Fri, 11 Sep 2026 07:13:03 +0000 Subject: [PATCH 02/10] Auto-format by https://ultralytics.com/actions --- AGENTS.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 82f6f73..1cb8efb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -31,20 +31,20 @@ After opening a PR: ## Commands ```bash -bun install --frozen-lockfile # Bun + uv are the whole toolchain; CI also installs Python 3.11 and 3.14 -bun run sync # scripts/sync.ts: read `source` from openapi.config.json → public/openapi.json (gitignored) -bun run generate # sync → scripts/generate.ts → generated/python (deleted and rewritten) → uvx ruff@0.16.2 format → scripts/headers.ts -bun run dev # sync → next dev (docs at http://localhost:3000; rewrites the nextjs-agent-rules block at the end of this file) -bun run build # sync → next build (static export to out/) → scripts/headers.ts out -bun run typecheck # next typegen && tsgo --noEmit (works without public/openapi.json) -bun run lint # biome check . (lint:fix writes); generated/, public/openapi.json, .next, out are excluded in biome.json -bun run knip # knip --exclude binaries (no knip.json; exports of lib/openapi.ts are public via package.json "exports") -bun run test # bun test — the single test file lib/generators/python.test.ts (20 tests, ~2 s); needs uv + python3 -bun test lib/generators/python.test.ts -t "multipart" # filter tests by name +bun install --frozen-lockfile # Bun + uv are the whole toolchain; CI also installs Python 3.11 and 3.14 +bun run sync # scripts/sync.ts: read `source` from openapi.config.json → public/openapi.json (gitignored) +bun run generate # sync → scripts/generate.ts → generated/python (deleted and rewritten) → uvx ruff@0.16.2 format → scripts/headers.ts +bun run dev # sync → next dev (docs at http://localhost:3000; rewrites the nextjs-agent-rules block at the end of this file) +bun run build # sync → next build (static export to out/) → scripts/headers.ts out +bun run typecheck # next typegen && tsgo --noEmit (works without public/openapi.json) +bun run lint # biome check . (lint:fix writes); generated/, public/openapi.json, .next, out are excluded in biome.json +bun run knip # knip --exclude binaries (no knip.json; exports of lib/openapi.ts are public via package.json "exports") +bun run test # bun test — the single test file lib/generators/python.test.ts (20 tests, ~2 s); needs uv + python3 +bun test lib/generators/python.test.ts -t "multipart" # filter tests by name # Generated-Python checks that CI runs after `bun run generate` (Python 3.11 and 3.14) python3 -m compileall -q generated/python/src -uv pip install --system ./generated/python # CI then imports . and Async +uv pip install --system ./generated/python # CI then imports . and Async uvx ruff@0.16.2 check generated/python # Generate for a consumer's configuration (relative paths inside it resolve against that file; output still lands in generated/python) From 97707e549785b0d504c49ecbc9448567462c72e0 Mon Sep 17 00:00:00 2001 From: Glenn Jocher Date: Fri, 11 Sep 2026 15:29:07 +0800 Subject: [PATCH 03/10] Correct AGENTS.md toolchain, body flattening, server, retry, and validation details --- AGENTS.md | 57 +++++++++++++++++++++++++++++-------------------------- 1 file changed, 30 insertions(+), 27 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 1cb8efb..aca4c9a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,7 +2,7 @@ This file provides guidance to AI coding agents (Claude Code, etc.) when working with code in this repository. CLAUDE.md is a symlink to this file. -Ultralytics OpenAPI (`@ultralytics/openapi`, AGPL-3.0, `private: true` — never published to npm, consumed only from Git) turns one OpenAPI 3.0–3.2 contract into two outputs: an interactive static documentation site (Next.js 16 App Router, React 19, TypeScript strict, Tailwind CSS v4, shadcn `base-nova` components over Base UI) and a typed Python SDK (sync + async `httpx` clients). Everything runs on Bun; `uv`/`uvx` run Ruff over generated Python and are required for `bun run generate` and `bun run test`. There is no build step for the library code: `package.json` exports `./openapi` → `lib/openapi.ts` as raw TypeScript. +Ultralytics OpenAPI (`@ultralytics/openapi`, AGPL-3.0, `private: true` — never published to npm, consumed only from Git) turns one OpenAPI 3.0–3.2 contract into two outputs: an interactive static documentation site (Next.js 16 App Router, React 19, TypeScript strict, Tailwind CSS v4, shadcn `base-nova` components over Base UI) and a typed Python SDK (sync + async `httpx` clients). Bun manages dependencies and runs the package scripts and tests; `next`, `knip`, and `tsgo` are Node-shebang binaries, so Node is required as well; `uvx` runs Ruff inside `bun run generate`, and the CLI test runs generated Python through `uv run` with `python3`. There is no build step for the library code: `package.json` exports `./openapi` → `lib/openapi.ts` as raw TypeScript. ## Core Principles (CRITICAL) @@ -31,29 +31,30 @@ After opening a PR: ## Commands ```bash -bun install --frozen-lockfile # Bun + uv are the whole toolchain; CI also installs Python 3.11 and 3.14 +bun install --frozen-lockfile # needs Bun, Node (next/knip/tsgo shebangs), uv, python3; CI uses Python 3.11 and 3.14 bun run sync # scripts/sync.ts: read `source` from openapi.config.json → public/openapi.json (gitignored) bun run generate # sync → scripts/generate.ts → generated/python (deleted and rewritten) → uvx ruff@0.16.2 format → scripts/headers.ts -bun run dev # sync → next dev (docs at http://localhost:3000; rewrites the nextjs-agent-rules block at the end of this file) +bun run dev # sync → next dev (docs at http://localhost:3000; re-adds the nextjs-agent-rules block at the end of this file when it is missing or outdated) bun run build # sync → next build (static export to out/) → scripts/headers.ts out bun run typecheck # next typegen && tsgo --noEmit (works without public/openapi.json) bun run lint # biome check . (lint:fix writes); generated/, public/openapi.json, .next, out are excluded in biome.json bun run knip # knip --exclude binaries (no knip.json; exports of lib/openapi.ts are public via package.json "exports") -bun run test # bun test — the single test file lib/generators/python.test.ts (20 tests, ~2 s); needs uv + python3 +bun run test # bun test — the single test file lib/generators/python.test.ts (20 tests); the CLI test needs uv + python3 bun test lib/generators/python.test.ts -t "multipart" # filter tests by name # Generated-Python checks that CI runs after `bun run generate` (Python 3.11 and 3.14) python3 -m compileall -q generated/python/src -uv pip install --system ./generated/python # CI then imports . and Async +uv pip install --system ./generated/python # CI then imports the configured package and checks its and Async exports uvx ruff@0.16.2 check generated/python -# Generate for a consumer's configuration (relative paths inside it resolve against that file; output still lands in generated/python) -OPENAPI_CONFIG=../sdk/openapi.config.json bun run generate +# Generate for a consumer's configuration (relative paths inside it resolve against that file; public/openapi.json and generated/python stay in this repository) +OPENAPI_CONFIG=/absolute/path/to/sdk/openapi.config.json bun run generate ``` Run checks through the package scripts. Generated Python additionally supports `python3 -m compileall -q generated/python/src` and `uvx ruff@0.16.2 check generated/python`. - `dev`, `build`, and `generate` always sync first; `typecheck`, `lint`, `knip`, and `test` do not touch the contract. Never run `next build` directly: both `next.config.ts` (`generateBuildId` hashes `public/openapi.json`) and `app/page.tsx` (`readFileSync("public/openapi.json")`) fail with `ENOENT` without a prior sync. +- Configuration and contract are parsed with `JSON.parse` — no YAML. `getConfig` checks only the six required keys and the `python.cli` shape (`OpenApiConfig` is a TypeScript interface, not a runtime validator), and `sync` only checks that `openapi`, `info`, and `paths` exist. `bun run test` reads the repository `openapi.config.json` and `examples/openapi.json` directly; `OPENAPI_CONFIG` does not change its fixtures. - Workflows in `.github/workflows/`: - `ci.yml` (CI) — on PRs, pushes to `main`, a daily cron, and manual dispatch; one `Test` job per Python version (3.11, 3.14), 15-minute timeout. Steps in order: `bun install --frozen-lockfile`; `bun run generate` twice with `diff --recursive` between the two outputs (determinism gate); `bun run typecheck && bun run lint && bun run knip && bun run test && bun run build` (lint IS in CI here); `python -m compileall -q generated/python/src`; `uv pip install --system ./generated/python` followed by importing the configured client and `Async` client; `uvx ruff@0.16.2 check generated/python`. - `format.yml` (Ultralytics Actions) — on PR open/sync/close/review-request and new issues; runs Ruff on Python, Prettier on YAML/JSON/Markdown/CSS (`.prettierignore` excludes `public/openapi.json`), codespell, Lychee link checks, AI labels and PR summary, and pushes auto-format commits to the PR branch. `python_docstrings: false` is deliberate — generated Google-style docstrings are owned by the generator. @@ -77,13 +78,13 @@ This repository is a standalone, general-purpose OpenAPI-to-SDK and API document - Keep consumer customizations reproducible through generation and synchronization. Never hand-edit generated output or make the converter depend on a consumer repository. - Review every change against this boundary. Relocate application-specific work to its owner instead of teaching the converter about one application. -## Downstream Consumers (what a merge to `main` ships immediately) +## Downstream Consumers and Compatibility Surfaces Both consumers track this repository's `main` with no pin, so every merged commit is live in their next CI run or build. Treat these surfaces as public API: - **`ultralytics/portal`** (Platform docs and contract). `apps/alpha/package.json` script `openapi:main` is exactly `bun add --dev --no-save @ultralytics/openapi@github:ultralytics/openapi#main`; alpha's `dev`, `build`, `api-docs`, and `openapi` scripts run it, and portal CI asserts that string before refreshing. Portal's `scripts/generate-openapi.ts` then calls `(await import("@ultralytics/openapi/openapi")).addPythonCodeSamples(document, { client, environment, package })` on an `openapi: "3.2.0"` document that portal generated itself, and alpha's `next.config.mjs` lists the package in `transpilePackages`. Contract: the `./openapi` export path, the `addPythonCodeSamples(document, PythonCodeSampleConfig)` signature, the `OpenApiDocument` type, `x-codeSamples` entries labeled `"Python SDK"`, and acceptance of 3.2.0 documents. `lib/openapi.ts` must stay import-free and isomorphic — it runs in the browser (`components/api-reference.tsx`), in Bun scripts here, and inside portal's generator and Next build. - **`ultralytics/sdk`** (`ultralytics-platform` on PyPI). Its `ci.yml` checks out `ultralytics/openapi` at `ref: main` into `.generator/`, runs `bun install --frozen-lockfile` and `OPENAPI_CONFIG=/openapi.config.json bun run generate`, then `diff --recursive sdk/python .generator/generated/python`. Any byte of changed Python output fails every sdk PR until sdk's `Live` job (main push, daily, or dispatch) regenerates and auto-merges an "Update Platform API contract" PR whose branch hash includes this repository's `main` SHA. sdk's config exercises `header`, `python.cli` (`ul` from `cli.py`), `python.authProvider` (`auth.py`), `python.readme`, omits `python.version` (so the version is the contract's `info.version`), and carries an sdk-only `upstream` key that `getConfig` neither validates nor reads. Contract: every key in `lib/config.ts` `OpenApiConfig`, the `generate` script name, the `generated/python` output path, `bun.lock` installing with `--frozen-lockfile`, and deterministic output. -- Before merging a generator change, reproduce the sdk drift check locally: `OPENAPI_CONFIG=/path/to/sdk/openapi.config.json bun run generate && diff -r generated/python /path/to/sdk/sdk/python` — a non-empty diff is expected only when the change intends to alter the published SDK, and it means sdk CI goes red until its `Live` job runs. +- Before merging a generator change, reproduce the sdk drift check locally: `OPENAPI_CONFIG=/path/to/sdk/openapi.config.json bun run generate && diff -r generated/python /path/to/sdk/sdk/python` — a non-empty diff is expected only when the change intends to alter the published SDK, and it means sdk CI stays red until the `Live` job's contract-update PR merges. ## Architecture @@ -99,22 +100,22 @@ Paths below are relative to the repository root. **Data flow.** `lib/config.ts` `getConfig()` reads `OPENAPI_CONFIG` or `openapi.config.json`, throws on a missing `source`, `name`, `apiKey.environment`, `python.client`, `python.package`, or `python.project`, defaults `python.install` to `pip install ` and `license` to this repository's AGPL-3.0 `LICENSE`, and resolves relative `source`, `license.file`, `python.readme`, `python.authProvider`, and `python.cli.source` against the config file's directory. `scripts/sync.ts` loads `source` (local file, or `https://` with `redirect: "error"`; `http://` is rejected), requires `openapi`, `info`, and `paths` keys, and writes `public/openapi.json`. Two consumers read that one file: `app/page.tsx` (Server Component, `readFileSync`) hands the document plus `apiKey.environment`, `python`, and `specUrl` to the client `ApiReference`, and `scripts/generate.ts` calls `generatePython(document, config, "generated/python")`. Both go through `lib/openapi.ts` `getOperations(document)`, which flattens `paths` into `ApiOperation[]`: `id` (`operationId`, else `-`, de-duplicated with `-2`, `-3`, …), `tag` (first tag, else `"Other"`), `resource` (`sdkIdentifier(tag)`), `sdkMethod`, parameters merged from path-item and operation level with `$ref`s resolved, resolved `requestBody`/`responses`, and `server` (operation → path item → document). -- `lib/config.ts` — `OpenApiConfig` (the full configuration schema; README "Configure" documents it for users) and `getConfig()`; `configPath` is also hashed by `next.config.ts`. +- `lib/config.ts` — `OpenApiConfig` (the configuration interface; defaults and required-key checks live in `getConfig()`; the README "Configure" section documents it for users); `configPath` is also hashed by `next.config.ts`. - `lib/openapi.ts` (no imports; browser-safe) — - Types: `JsonSchema`, `Parameter`, `OpenApiDocument`, `ApiOperation`, `SdkArgument`, `PythonCodeSampleConfig`, `ApiAuthentication`. - - Identifiers: `sdkIdentifier` (camelCase and punctuation become `snake_case`, possessives are stripped, Python keywords get a trailing underscore, leading digits get a leading underscore), `allocateSdkIdentifiers` (collision suffixes `_` then `_2`, `_3`, …; reserves `self`, `extra_headers`, `timeout`). + - Identifiers: `sdkIdentifier` (camelCase and punctuation become `snake_case`, possessives are stripped, Python keywords get a trailing underscore, leading digits get a leading underscore), `allocateSdkIdentifiers` (collision suffixes `_` then `_2`, `_3`, …; always reserves `self`, and `sdkArguments` passes `extra_headers` and `timeout` as extra reserved names while `modelSource` field allocation passes none). - Method naming: `sdkMethodCandidates` + `SDK_VERBS` (`get`→`retrieve`/`list`, `post`→`create`, `put`/`patch`→`update`, `delete`→`delete`) — `x-sdk-method` wins (validated as a lowercase identifier, unique per resource), else the static path leaf after the resource segment, else the verb; a leafless GET with a path parameter is `retrieve` unless its summary starts with list/view/search. `getOperations` resolves collisions per resource (canonical names first, GET keeps the bare leaf, then `_` / `_`, then `_2` suffixes). - - Arguments: `sdkArguments` — path params are positional-required, everything else keyword; a JSON/form/multipart object body is flattened into per-property keyword arguments unless it contains `oneOf`/`anyOf`, incompatible closed `allOf` branches, `additionalProperties` schemas, or `minProperties`/`maxProperties`, in which case a single `body` argument (`wholeBody: true`) is emitted. `readOnly` properties are skipped. + - Arguments: `sdkArguments` — path arguments are required (positional or keyword), every other argument is keyword-only; a JSON/form/multipart object body is flattened into per-property keyword arguments unless the body schema (or one of its `allOf` branches) is a `oneOf`/`anyOf` union, has incompatible closed `allOf` branches, carries an object-valued `additionalProperties` inside `allOf` (`constrainedComposedBody`), or sets `minProperties`/`maxProperties`; then a single `body` argument (`wholeBody: true`) is emitted. Unions inside individual properties do not prevent flattening, and a top-level object with its own object-valued `additionalProperties` still flattens. `readOnly` properties are skipped. - Auth: `getAuthentication` accepts only single-scheme security requirements that are `apiKey` in `header` or `http` `bearer` (returns `{ header, prefix }`); `getAuthenticationMode` → `none`/`optional`/`required`. - - Servers: `resolveServerUrl(document, origin, operation?)`, `expandServerUrl` (substitutes `variables[*].default`); relative server URLs resolve against `http://localhost:3000` in the generator and `window.location.origin` in the browser. + - Servers: `resolveServerUrl(document, origin, operation?)`, `expandServerUrl` (substitutes `variables[*].default`); the generated `base_url` default is the first document server (a relative URL is resolved against `http://localhost:3000`); a relative operation- or path-level server override is emitted as `server=` and joined onto the client's `base_url` at runtime, an absolute override is baked into the request URL, and the browser resolves relative servers against `window.location.origin`. - Schemas: `resolveSchema` (local `#/components/schemas/` only, sibling keys and `properties`/`required` merged over the target, cycle-safe), `objectSchema` (merges `allOf` and object-shaped `anyOf`/`oneOf` into one object schema), `schemaExample`/`stringExample`/`schemaMatches` (example synthesis: `example` → `default` → `const` → first matching `enum` value → union/variant walk → format-aware strings), `schemaLabel`, `schemaConstraints`, `schemaFields`. - Responses: `successMediaEntries`/`successMedia`/`successSchema` (exact `2xx` codes first, else `2XX`; distinct shapes become `oneOf`), `requestMedia` (`preferredMedia` order: `application/json`, `+json`, form/multipart, `text/*`, first listed). - - Samples and requests: `pythonCodeSample`, `addPythonCodeSamples` (rewrites the `"Python SDK"` entry of each operation's `x-codeSamples`), `curlCodeSample` (uses the `YOUR_API_KEY` placeholder), `requestBodyExample`, `buildApiRequest` (URL, headers, and `BodyInit` for the docs "Send" button; refuses cookie parameters and missing required parameters). The docs UI does not render `x-codeSamples`; it regenerates samples live from the current form values. -- `lib/generators/python.ts` — `generatePython(document, config, output)` → `prepare` (calls `getOperations`, then `validateOperation`, which throws `Unsupported …` for: security requirements with no supported scheme, non-`simple` path styles, `allowReserved` query params, array/object header or cookie params, schema-less request bodies, request `encoding`, and mixed JSON/text/binary success media) → `rm -rf output` → writes files. Rendering: `pythonType` (JSON Schema → `str`/`int`/`float`/`bool`/`Sequence[...]` for arguments and `list[...]` for responses/`dict[str, Any]`/`BinaryIO`/`Literal[...]`/`| None`), `docstring` (Google style, `Args:` types in parentheses), `methodSource` (one `self._client.request(...)` call with `params=[*_query_parameter(...)]`, `json=`/`data=_form_data(...)`/`files=`/`content=`, `auth=(header, prefix)`, `text=True` for text responses), `resourceSource`, `modelSource` (functional `TypedDict`s in `types.py` named `Response`, nested `…Item`, `NotRequired` for optional fields, wire keys preserved), `clientSource`/`apiClientSource` (the runtime: `_resolve_api_key`, `NotGiven`/`NOT_GIVEN`, `_path_parameter`, `_query_parameter`, `_form_data`, `_retry_delay`, `SyncAPIClient`/`AsyncAPIClient.request`), `EXCEPTIONS_SOURCE` (`APIError(status_code, body, request_id)` with `.json`, `APIConnectionError`), `publicClientSource` (``/`Async` with `api_key`, `base_url`, `timeout=60.0`, `max_retries=2`, `http_client`, context managers). All Python lives inside TypeScript template literals — escape `\`, `${`, and backticks, and remember Ruff reformats the output afterwards. + - Samples and requests: `pythonCodeSample`, `addPythonCodeSamples` (rewrites the `"Python SDK"` entry of each operation's `x-codeSamples`), `curlCodeSample` (uses the `YOUR_API_KEY` placeholder), `requestBodyExample`, `buildApiRequest` (URL, headers, and `BodyInit` for the docs "Send" button; refuses cookie parameters and missing required parameters). The docs UI does not render `x-codeSamples`: the cURL sample reflects the current parameter, file, and body inputs, while the Python sample takes only the parsed body and synthesizes required non-body arguments from schema examples. +- `lib/generators/python.ts` — `generatePython(document, config, output)` → `prepare` (calls `getOperations`, then `validateOperation`, which throws `Unsupported …` for: security requirements whose alternatives are all non-empty and none is a supported scheme (an anonymous `{}` alternative permits generation), non-`simple` path styles, `allowReserved` query params, array/object header or cookie params, schema-less request bodies, a non-empty request `encoding` object, and mixed JSON/text/binary modes across the preferred media of the success responses) → `rm -rf output` → writes files. Rendering: `pythonType` (JSON Schema → `str`/`int`/`float`/`bool`/`Sequence[...]` for arguments and `list[...]` for responses/`dict[str, Any]`/`BinaryIO`/`Literal[...]`/`| None`), `docstring` (Google style, `Args:` types in parentheses), `methodSource` (one `self._client.request(...)` call with `params=[*_query_parameter(...)]`, `json=`/`data=_form_data(...)`/`files=`/`content=`, `auth=(header, prefix)`, `text=True` for text responses), `resourceSource`, `modelSource` (functional `TypedDict`s in `types.py` named `Response`, nested `…Item`, `NotRequired` for optional fields, wire keys preserved, response `writeOnly` fields dropped; nested helpers `possibleTypes`, `objectUnion`, `modelType`, `addModel`), `clientSource`/`apiClientSource` (the runtime: `_resolve_api_key`, `NotGiven`/`NOT_GIVEN`, `_path_parameter`, `_query_parameter`, `_form_data`, `_retry_delay`, `SyncAPIClient`/`AsyncAPIClient.request`), `EXCEPTIONS_SOURCE` (`APIError(status_code, body, request_id)` with `.json`, `APIConnectionError`), `publicClientSource` (``/`Async` with `api_key`, `base_url`, `timeout=60.0`, `max_retries=2`, `http_client`, context managers). Generator-owned Python lives inside TypeScript template literals (consumer `authProvider`/`cli.source` files are copied verbatim) — escape `\`, `${`, and backticks; Ruff reformats the output afterwards, and its parser is the first thing that fails on a Python syntax error. - Generated layout: `pyproject.toml` (`uv_build` backend, `httpx>=0.28,<1`, `[tool.ruff] line-length = 120`, `[project.scripts]` only with `python.cli`), `README.md`, `LICENSE`, `src//{__init__.py,_client.py,_exceptions.py,client.py,async_client.py,types.py,py.typed}`, `src//resources/{__init__.py,.py}`, plus optional `_auth.py` (copied `python.authProvider`), `cli.py` (copied `python.cli.source`), and `_cli_metadata.py` (`MULTIPART_FILES` mapping `resource.method` → binary field names of whole-body multipart operations). - - Runtime behavior worth knowing before "fixing" a client bug: the auth header is added only when a key resolved (explicit → `apiKey.environment` → provider; `""` disables) and the operation has auth; retries apply to GET/HEAD/OPTIONS on connection errors and 408/409/429/5xx, plus 429 for JSON-bodied requests, honoring `Retry-After` (≤ 60 s) else `0.5·2^attempt` capped at 8 s; 204/empty → `None`, JSON media → parsed, `text=True` or `text/*` → `str`, else `bytes`. -- `components/api-reference.tsx` (`"use client"`) — `ApiReference` (API key held in React state only, ⌘/Ctrl+K focuses search, selection via URL hash `#operation=`), `OperationNavigation`, `OverviewPanel` (version badges, "Download OpenAPI contract" from `specUrl`, servers, security schemes, Python quick start), `OperationPanel` (parameter inputs, body textarea seeded by `requestBodyExample`, file inputs for binary multipart fields, "Send" = `buildApiRequest` + browser `fetch`, so the target API must allow CORS), `codeExamples` (live cURL + Python), `Markdown` (react-markdown). `components/ui/*` are shadcn `base-nova` copies over `@base-ui/react` — compose with `render={}`, never Radix `asChild`. `components/logo.tsx` + `app/icon.svg` + `--brand-gradient-*` in `app/globals.css` are the only branding hooks. -- `app/` — `layout.tsx` (title `${config.name} Reference`, Geist fonts, skip link), `page.tsx`, `globals.css` (Tailwind v4 CSS-first with oklch tokens; no tailwind config), `icon.svg`. `next.config.ts`: `output: "export"`, `basePath` from `docs.basePath`, and `generateBuildId` = SHA-256 of `app/`, `components/`, `lib/`, `bun.lock`, `package.json`, `next.config.ts`, `postcss.config.mjs`, the config file, and `public/openapi.json`, so `out/` is byte-stable for the same inputs. + - Runtime behavior worth knowing before "fixing" a client bug: the auth header is added only when a key resolved (explicit → `apiKey.environment` → provider; `""` disables) and the operation has auth; retries apply to GET/HEAD/OPTIONS on any `httpx.HTTPError` and on 408/409/429/5xx responses, plus 429 for requests that passed `json=`; a numeric `Retry-After` is honored (clamped to 0–60 s), otherwise — including HTTP-date values — the delay is `0.5·2^attempt` capped at 8 s; 204/empty → `None`, JSON media → parsed, `text=True` or `text/*` → `str`, else `bytes`. Optional arguments default to `NOT_GIVEN`, not `None`: `_json_value` drops `NotGiven` fields recursively but keeps explicit `None` and turns sequences into lists, `_without_none` drops both from headers and cookies, and `_query_parameter` skips both. Response types are `typing.cast` declarations only — the runtime returns decoded JSON, `str`, `bytes`, or `None` with no validation. A supplied `http_client` keeps its own timeout (the constructor `timeout` only configures the client the SDK creates) and is closed by `close()`/context exit either way. `python.authProvider` must export a synchronous `get_api_key() -> str | None`, called once per client construction; `python.cli.source` must define a zero-argument `main()` (console script `.cli:main`) and its own `python -m` launcher. +- `components/api-reference.tsx` (`"use client"`) — `ApiReference` (API key held in React state only, ⌘/Ctrl+K focuses search, selection via URL hash `#operation=`), `OperationNavigation`, `OverviewPanel` (version badges, "Download OpenAPI contract" from `specUrl`, servers, security schemes, Python quick start), `OperationPanel` (parameter inputs, body textarea seeded by `requestBodyExample`, file inputs for binary multipart fields, "Send" = `buildApiRequest` + browser `fetch`, so the target API must allow CORS), `codeExamples` (live cURL + Python), `Markdown` (react-markdown). `components/ui/*` are shadcn `base-nova` copies over `@base-ui/react` — compose with `render={}`, never Radix `asChild`. `components/logo.tsx` + `app/icon.svg` + `--brand-gradient-*` in `app/globals.css` are the brand mark, favicon, and gradient hooks; visible titles come from the contract's `info.title` and the config `name`. `OperationPanel` is keyed by `selected.id`, so switching operations remounts it and resets parameters, body, files, and result; the selection is read from the hash inside an effect, so the first render is always the overview. `SchemaFields` uses the shared `schemaFields` (hides request `readOnly` and response `writeOnly` fields, stops at depth 4), and `.operation-list-item` in `app/globals.css` uses `content-visibility: auto` to keep long navigation lists cheap. +- `app/` — `layout.tsx` (title `${config.name} Reference`, Geist fonts, skip link), `page.tsx`, `globals.css` (Tailwind v4 CSS-first with oklch tokens; no tailwind config), `icon.svg`. `next.config.ts`: `output: "export"`, `basePath` from `docs.basePath`, and `generateBuildId` = SHA-256 of `app/`, `components/`, `lib/`, `bun.lock`, `package.json`, `next.config.ts`, `postcss.config.mjs`, the config file, and `public/openapi.json` (first 20 hex characters), so the build id is stable for the same inputs; `scripts/headers.ts` runs after the build and is not part of that hash. - `scripts/` — `sync.ts`, `generate.ts` (Python only; a new language is wired here), `headers.ts` (prepends `header` to `.html`, `.css`, `.js`, `.py`, `.toml` files under the given directories; a no-op when `header` is unset, which is the case for this repository's own config). - `examples/openapi.json` — OpenAPI 3.1 fixture (7 paths, 8 operations) covering server variables, bearer auth, `allOf`/`anyOf`/`oneOf`, nullable, `writeOnly`, binary multipart, text and empty responses. It is both the `source` of the repository's `openapi.config.json` and the test fixture. @@ -129,7 +130,9 @@ Paths below are relative to the repository root. - Wrong resource or method name in the SDK, docs sample, or `x-codeSamples` → `sdkIdentifier`, `sdkMethodCandidates`, and the collision pass in `getOperations` (`lib/openapi.ts`); contracts can pin a name with `x-sdk-method`. - Wrong keyword arguments or a body that should/shouldn't be flattened → `sdkArguments`; Python-side rendering of those arguments → `methodSource`. -- Wrong Python type, `TypedDict`, or nullability → `pythonType`, `isNullable`, `literalValues`, `modelSource` in `lib/generators/python.ts`. +- Wrong Python type, `TypedDict`, or nullability → `pythonType`, `isNullable`, `literalValues` in `lib/generators/python.ts`; wrong response union, nested model name, or recursive reference → `modelSource`'s nested `possibleTypes`, `objectUnion`, `modelType`, `addModel`. +- Wrong path/query/form encoding → browser/cURL: `serializeSimplePath`, `serializeQueryParameter`, `parameterValue`, `formEntries` in `lib/openapi.ts`; Python: `_path_parameter`, `_query_parameter`, `_form_data` in `clientSource` — two implementations that must agree. +- A `$ref` is not resolved → `resolveSchema` silently returns the input for missing or non-local schema references, while `resolveParameter`/`resolveRequestBody`/`resolveResponse` throw `Unknown … reference`. - `bun run generate` throws `Unsupported …` / `Unknown … reference` → `validateOperation` (python.ts) or `componentName`/`resolveParameter`/`resolveRequestBody`/`resolveResponse` (openapi.ts). Add opt-in, contract-driven support or fix the contract; never special-case one API (Product Boundary). - Wrong example values in docs or samples → `schemaExample`, `stringExample`, `authoredSchemaExample`, `requestBodyExampleValue`, `pythonCodeSample`, `curlCodeSample`. - HTTP behavior of generated clients (auth header, retries, encoding, response decoding) → `clientSource`/`apiClientSource`; errors → `EXCEPTIONS_SOURCE`; constructor surface → `publicClientSource`. @@ -145,19 +148,19 @@ Paths below are relative to the repository root. - Determinism is enforced (CI generates twice and diffs). Resource order follows the contract's path order via `Map` insertion; exports are sorted explicitly (`resourceExports.sort()`). Avoid timestamps, randomness, or unordered iteration in generator code. - `openapi.config.json` and `examples/openapi.json` are the test fixtures: `python.test.ts` imports the config, expects 8 operations, package `example_api`, client `Example`, the `Repository` URL, authors, and classifiers. Editing either file changes test expectations. - `bun run test` executes Python: the CLI test runs `uv run --no-project --python python3 --with ` twice. Without `uv` and `python3` on `PATH` that test fails. -- Ruff reformats generated Python after generation, so formatting inside the template literals is irrelevant, but syntax errors only surface in CI's `compileall`/import step — run those locally after touching `python.ts`. +- Ruff reformats generated Python after generation, so formatting inside the template literals is irrelevant. Python syntax errors fail `bun run generate` at the Ruff step and the CLI test at `uv run`; import errors and 3.11/3.14 differences only surface in CI's `compileall`/install/import steps — run those locally after touching `python.ts`. - `python.version` omitted → the contract's `info.version` is used and must match `^\d+(\.\d+)*((a|b|rc)\d+)?(\.post\d+)?(\.dev\d+)?$`, otherwise generation throws `Set python.version …`. -- Every `sync` overwrites `public/openapi.json` (gitignored) from `source`; `source` must be a local path or `https://` (no `http://`, no redirects). A stale `public/openapi.json` is the usual reason docs and `generated/` disagree — re-run `bun run sync`. -- `tsconfig.json` excludes `generated/`, and Biome/Prettier ignore `generated/` and `public/openapi.json`; `tsconfig.tsbuildinfo`, `.next/`, `out/`, `.idea/` are gitignored — never commit them. +- Every `sync` overwrites `public/openapi.json` (gitignored) from `source`; `source` must be a local path or `https://` (no `http://`, no redirects). After changing the contract or config, re-run `bun run generate` and rebuild or restart the docs so both outputs come from the same `public/openapi.json`. +- `tsconfig.json` excludes `generated/`; Biome excludes `generated/`, `public/openapi.json`, `.next`, and `out`; `.prettierignore` lists only `public/openapi.json`; `tsconfig.tsbuildinfo`, `.next/`, `out/`, `.idea/` are gitignored — never commit them. - `format.yml` reformats JSON/Markdown/YAML with Prettier and pushes to your branch; `git pull --rebase` before every push. -- `next dev` rewrites the `nextjs-agent-rules` block at the bottom of this file (see the block's own note); commit it rather than reverting it. -- `knip` has no config file: anything exported from `lib/openapi.ts` counts as public API (package `exports`), while an unused export elsewhere fails `bun run knip`. +- `next dev` re-adds or updates the `nextjs-agent-rules` block at the bottom of this file when it detects a coding agent and the block is missing or outdated (see the block's own note); commit it rather than reverting it. +- `knip` has no config file: exports of entry files are not reported (`lib/openapi.ts` via package `exports`, `app/` files via the Next plugin), while unused exports in other modules and unused files or dependencies fail `bun run knip`. ## Tests -- One test file: `lib/generators/python.test.ts` (`describe("Python generator")`). `beforeAll` generates into a `mkdtemp` directory from `examples/openapi.json` with the repository `openapi.config.json`, after planting `stale.py` to prove the output is wiped. Tests clone the fixture with `structuredClone`, mutate schemas to hit edge cases, and assert on generated source text, thrown messages, `getOperations` naming, `sdkArguments`, `schemaExample`, `buildApiRequest`, and `curlCodeSample`. Separate temp directories cover `python.cli` (runs the generated CLI through `uv run`) and `python.readme`/`python.authProvider`. -- CI is the only place the generated package is installed and imported (`uv pip install --system ./generated/python`, then `importlib.import_module(package)` and `getattr(m, client)`/`getattr(m, "Async" + client)`), compiled, and Ruff-checked on Python 3.11 and 3.14. -- There are no browser or E2E tests; `bun run typecheck`, `bun run lint`, and `bun run build` are the only checks on `app/` and `components/`. Verify UI changes in `bun run dev`. +- One test file: `lib/generators/python.test.ts` (`describe("Python generator")`). `beforeAll` generates into a `mkdtemp` directory from `examples/openapi.json` with the repository `openapi.config.json`, after planting `stale.py` to prove the output is wiped. Tests clone the fixture with `structuredClone`, mutate schemas to hit edge cases, and assert on generated source text, thrown messages, `getOperations` naming, `sdkArguments`, `schemaExample`, `buildApiRequest`, and `curlCodeSample`. Separate temp directories cover `python.cli` (runs the generated CLI through `uv run`) and `python.readme`/`python.authProvider`. Tests call `generatePython` directly, so assertions see output before Ruff formatting and the header pass; they assert on source text and never exercise HTTP retries or serialization through a transport. Useful `-t` filters: `multipart`, `successful response`, `mixed parse modes`, `generic string examples`, `consumer CLI`. +- CI is the only place the generated package is installed with `uv pip install --system ./generated/python`, imported (`importlib.import_module(package)` and `getattr(m, client)`/`getattr(m, "Async" + client)`), compiled, and Ruff-checked on both Python 3.11 and 3.14; locally, only the CLI test installs and runs a generated package. +- There are no browser or E2E tests; `bun run typecheck`, `bun run lint`, and `bun run build` are the only checks on `app/` and `components/`. Verify UI changes in `bun run dev`. CSS linting and formatting are disabled in `biome.json`; dark mode follows `prefers-color-scheme` in `app/globals.css`. From 6de06b7da884abfd274cd1ff059ee72bba5b91ce Mon Sep 17 00:00:00 2001 From: Glenn Jocher Date: Fri, 11 Sep 2026 15:33:28 +0800 Subject: [PATCH 04/10] Qualify AGENTS.md flattening, retry, credential, reference, and validation claims --- AGENTS.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index aca4c9a..9b86ea4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -105,7 +105,7 @@ Paths below are relative to the repository root. - Types: `JsonSchema`, `Parameter`, `OpenApiDocument`, `ApiOperation`, `SdkArgument`, `PythonCodeSampleConfig`, `ApiAuthentication`. - Identifiers: `sdkIdentifier` (camelCase and punctuation become `snake_case`, possessives are stripped, Python keywords get a trailing underscore, leading digits get a leading underscore), `allocateSdkIdentifiers` (collision suffixes `_` then `_2`, `_3`, …; always reserves `self`, and `sdkArguments` passes `extra_headers` and `timeout` as extra reserved names while `modelSource` field allocation passes none). - Method naming: `sdkMethodCandidates` + `SDK_VERBS` (`get`→`retrieve`/`list`, `post`→`create`, `put`/`patch`→`update`, `delete`→`delete`) — `x-sdk-method` wins (validated as a lowercase identifier, unique per resource), else the static path leaf after the resource segment, else the verb; a leafless GET with a path parameter is `retrieve` unless its summary starts with list/view/search. `getOperations` resolves collisions per resource (canonical names first, GET keeps the bare leaf, then `_` / `_`, then `_2` suffixes). - - Arguments: `sdkArguments` — path arguments are required (positional or keyword), every other argument is keyword-only; a JSON/form/multipart object body is flattened into per-property keyword arguments unless the body schema (or one of its `allOf` branches) is a `oneOf`/`anyOf` union, has incompatible closed `allOf` branches, carries an object-valued `additionalProperties` inside `allOf` (`constrainedComposedBody`), or sets `minProperties`/`maxProperties`; then a single `body` argument (`wholeBody: true`) is emitted. Unions inside individual properties do not prevent flattening, and a top-level object with its own object-valued `additionalProperties` still flattens. `readOnly` properties are skipped. + - Arguments: `sdkArguments` — path arguments are required (positional or keyword), every other contract argument is keyword-only (`*` is emitted only when such arguments exist); a JSON/form/multipart object body with at least one named property is flattened into per-property keyword arguments unless the body schema (or one of its `allOf` branches) is a `oneOf`/`anyOf` union, has incompatible closed `allOf` branches, carries an object-valued `additionalProperties` inside `allOf` (`constrainedComposedBody`), or sets `minProperties`/`maxProperties`; then a single `body` argument (`wholeBody: true`) is emitted. Unions inside individual properties do not prevent flattening, a top-level object with named properties and its own object-valued `additionalProperties` still flattens, and a body with only `additionalProperties` and no named properties becomes a whole `body`. `readOnly` properties are skipped. - Auth: `getAuthentication` accepts only single-scheme security requirements that are `apiKey` in `header` or `http` `bearer` (returns `{ header, prefix }`); `getAuthenticationMode` → `none`/`optional`/`required`. - Servers: `resolveServerUrl(document, origin, operation?)`, `expandServerUrl` (substitutes `variables[*].default`); the generated `base_url` default is the first document server (a relative URL is resolved against `http://localhost:3000`); a relative operation- or path-level server override is emitted as `server=` and joined onto the client's `base_url` at runtime, an absolute override is baked into the request URL, and the browser resolves relative servers against `window.location.origin`. - Schemas: `resolveSchema` (local `#/components/schemas/` only, sibling keys and `properties`/`required` merged over the target, cycle-safe), `objectSchema` (merges `allOf` and object-shaped `anyOf`/`oneOf` into one object schema), `schemaExample`/`stringExample`/`schemaMatches` (example synthesis: `example` → `default` → `const` → first matching `enum` value → union/variant walk → format-aware strings), `schemaLabel`, `schemaConstraints`, `schemaFields`. @@ -113,7 +113,7 @@ Paths below are relative to the repository root. - Samples and requests: `pythonCodeSample`, `addPythonCodeSamples` (rewrites the `"Python SDK"` entry of each operation's `x-codeSamples`), `curlCodeSample` (uses the `YOUR_API_KEY` placeholder), `requestBodyExample`, `buildApiRequest` (URL, headers, and `BodyInit` for the docs "Send" button; refuses cookie parameters and missing required parameters). The docs UI does not render `x-codeSamples`: the cURL sample reflects the current parameter, file, and body inputs, while the Python sample takes only the parsed body and synthesizes required non-body arguments from schema examples. - `lib/generators/python.ts` — `generatePython(document, config, output)` → `prepare` (calls `getOperations`, then `validateOperation`, which throws `Unsupported …` for: security requirements whose alternatives are all non-empty and none is a supported scheme (an anonymous `{}` alternative permits generation), non-`simple` path styles, `allowReserved` query params, array/object header or cookie params, schema-less request bodies, a non-empty request `encoding` object, and mixed JSON/text/binary modes across the preferred media of the success responses) → `rm -rf output` → writes files. Rendering: `pythonType` (JSON Schema → `str`/`int`/`float`/`bool`/`Sequence[...]` for arguments and `list[...]` for responses/`dict[str, Any]`/`BinaryIO`/`Literal[...]`/`| None`), `docstring` (Google style, `Args:` types in parentheses), `methodSource` (one `self._client.request(...)` call with `params=[*_query_parameter(...)]`, `json=`/`data=_form_data(...)`/`files=`/`content=`, `auth=(header, prefix)`, `text=True` for text responses), `resourceSource`, `modelSource` (functional `TypedDict`s in `types.py` named `Response`, nested `…Item`, `NotRequired` for optional fields, wire keys preserved, response `writeOnly` fields dropped; nested helpers `possibleTypes`, `objectUnion`, `modelType`, `addModel`), `clientSource`/`apiClientSource` (the runtime: `_resolve_api_key`, `NotGiven`/`NOT_GIVEN`, `_path_parameter`, `_query_parameter`, `_form_data`, `_retry_delay`, `SyncAPIClient`/`AsyncAPIClient.request`), `EXCEPTIONS_SOURCE` (`APIError(status_code, body, request_id)` with `.json`, `APIConnectionError`), `publicClientSource` (``/`Async` with `api_key`, `base_url`, `timeout=60.0`, `max_retries=2`, `http_client`, context managers). Generator-owned Python lives inside TypeScript template literals (consumer `authProvider`/`cli.source` files are copied verbatim) — escape `\`, `${`, and backticks; Ruff reformats the output afterwards, and its parser is the first thing that fails on a Python syntax error. - Generated layout: `pyproject.toml` (`uv_build` backend, `httpx>=0.28,<1`, `[tool.ruff] line-length = 120`, `[project.scripts]` only with `python.cli`), `README.md`, `LICENSE`, `src//{__init__.py,_client.py,_exceptions.py,client.py,async_client.py,types.py,py.typed}`, `src//resources/{__init__.py,.py}`, plus optional `_auth.py` (copied `python.authProvider`), `cli.py` (copied `python.cli.source`), and `_cli_metadata.py` (`MULTIPART_FILES` mapping `resource.method` → binary field names of whole-body multipart operations). - - Runtime behavior worth knowing before "fixing" a client bug: the auth header is added only when a key resolved (explicit → `apiKey.environment` → provider; `""` disables) and the operation has auth; retries apply to GET/HEAD/OPTIONS on any `httpx.HTTPError` and on 408/409/429/5xx responses, plus 429 for requests that passed `json=`; a numeric `Retry-After` is honored (clamped to 0–60 s), otherwise — including HTTP-date values — the delay is `0.5·2^attempt` capped at 8 s; 204/empty → `None`, JSON media → parsed, `text=True` or `text/*` → `str`, else `bytes`. Optional arguments default to `NOT_GIVEN`, not `None`: `_json_value` drops `NotGiven` fields recursively but keeps explicit `None` and turns sequences into lists, `_without_none` drops both from headers and cookies, and `_query_parameter` skips both. Response types are `typing.cast` declarations only — the runtime returns decoded JSON, `str`, `bytes`, or `None` with no validation. A supplied `http_client` keeps its own timeout (the constructor `timeout` only configures the client the SDK creates) and is closed by `close()`/context exit either way. `python.authProvider` must export a synchronous `get_api_key() -> str | None`, called once per client construction; `python.cli.source` must define a zero-argument `main()` (console script `.cli:main`) and its own `python -m` launcher. + - Runtime behavior worth knowing before "fixing" a client bug: the auth header is added only when a key resolved (explicit → `apiKey.environment` → provider; `""` disables) and the operation has auth; retries apply to GET/HEAD/OPTIONS on any `httpx.HTTPError` and on 408/409/429/5xx responses, plus 429 when the request passed a non-`None` `json=` payload; a numeric `Retry-After` is honored (clamped to 0–60 s), otherwise — including HTTP-date values — the delay is `0.5·2^attempt` capped at 8 s; 204/empty → `None`, JSON media → parsed, `text=True` or `text/*` → `str`, else `bytes`. Optional contract arguments default to `NOT_GIVEN`, not `None` (`timeout` and `extra_headers` default to `None`): `_json_value` drops `NotGiven` fields recursively but keeps explicit `None` and turns sequences into lists, `_without_none` drops both from headers and cookies, and `_query_parameter` skips both. Response types are `typing.cast` declarations only — the runtime returns decoded JSON, `str`, `bytes`, or `None` with no validation. A supplied `http_client` keeps its own timeout (the constructor `timeout` only configures the client the SDK creates) and is closed by `close()`/context exit either way. `python.authProvider` must export a synchronous `get_api_key() -> str | None`, called at most once per client construction and only when neither an explicit key nor `apiKey.environment` resolves; `python.cli.source` must define a zero-argument `main()` (console script `.cli:main`) and its own `python -m` launcher. - `components/api-reference.tsx` (`"use client"`) — `ApiReference` (API key held in React state only, ⌘/Ctrl+K focuses search, selection via URL hash `#operation=`), `OperationNavigation`, `OverviewPanel` (version badges, "Download OpenAPI contract" from `specUrl`, servers, security schemes, Python quick start), `OperationPanel` (parameter inputs, body textarea seeded by `requestBodyExample`, file inputs for binary multipart fields, "Send" = `buildApiRequest` + browser `fetch`, so the target API must allow CORS), `codeExamples` (live cURL + Python), `Markdown` (react-markdown). `components/ui/*` are shadcn `base-nova` copies over `@base-ui/react` — compose with `render={}`, never Radix `asChild`. `components/logo.tsx` + `app/icon.svg` + `--brand-gradient-*` in `app/globals.css` are the brand mark, favicon, and gradient hooks; visible titles come from the contract's `info.title` and the config `name`. `OperationPanel` is keyed by `selected.id`, so switching operations remounts it and resets parameters, body, files, and result; the selection is read from the hash inside an effect, so the first render is always the overview. `SchemaFields` uses the shared `schemaFields` (hides request `readOnly` and response `writeOnly` fields, stops at depth 4), and `.operation-list-item` in `app/globals.css` uses `content-visibility: auto` to keep long navigation lists cheap. - `app/` — `layout.tsx` (title `${config.name} Reference`, Geist fonts, skip link), `page.tsx`, `globals.css` (Tailwind v4 CSS-first with oklch tokens; no tailwind config), `icon.svg`. `next.config.ts`: `output: "export"`, `basePath` from `docs.basePath`, and `generateBuildId` = SHA-256 of `app/`, `components/`, `lib/`, `bun.lock`, `package.json`, `next.config.ts`, `postcss.config.mjs`, the config file, and `public/openapi.json` (first 20 hex characters), so the build id is stable for the same inputs; `scripts/headers.ts` runs after the build and is not part of that hash. - `scripts/` — `sync.ts`, `generate.ts` (Python only; a new language is wired here), `headers.ts` (prepends `header` to `.html`, `.css`, `.js`, `.py`, `.toml` files under the given directories; a no-op when `header` is unset, which is the case for this repository's own config). @@ -132,7 +132,7 @@ Paths below are relative to the repository root. - Wrong keyword arguments or a body that should/shouldn't be flattened → `sdkArguments`; Python-side rendering of those arguments → `methodSource`. - Wrong Python type, `TypedDict`, or nullability → `pythonType`, `isNullable`, `literalValues` in `lib/generators/python.ts`; wrong response union, nested model name, or recursive reference → `modelSource`'s nested `possibleTypes`, `objectUnion`, `modelType`, `addModel`. - Wrong path/query/form encoding → browser/cURL: `serializeSimplePath`, `serializeQueryParameter`, `parameterValue`, `formEntries` in `lib/openapi.ts`; Python: `_path_parameter`, `_query_parameter`, `_form_data` in `clientSource` — two implementations that must agree. -- A `$ref` is not resolved → `resolveSchema` silently returns the input for missing or non-local schema references, while `resolveParameter`/`resolveRequestBody`/`resolveResponse` throw `Unknown … reference`. +- A `$ref` is not resolved → `resolveSchema` and `resolveParameter` silently pass through non-local references (and `resolveSchema` also passes through missing local ones); `resolveRequestBody`/`resolveResponse` throw `Unsupported reference` for non-local targets, and all three component resolvers throw `Unknown … reference` for a missing local target. - `bun run generate` throws `Unsupported …` / `Unknown … reference` → `validateOperation` (python.ts) or `componentName`/`resolveParameter`/`resolveRequestBody`/`resolveResponse` (openapi.ts). Add opt-in, contract-driven support or fix the contract; never special-case one API (Product Boundary). - Wrong example values in docs or samples → `schemaExample`, `stringExample`, `authoredSchemaExample`, `requestBodyExampleValue`, `pythonCodeSample`, `curlCodeSample`. - HTTP behavior of generated clients (auth header, retries, encoding, response decoding) → `clientSource`/`apiClientSource`; errors → `EXCEPTIONS_SOURCE`; constructor surface → `publicClientSource`. @@ -148,7 +148,7 @@ Paths below are relative to the repository root. - Determinism is enforced (CI generates twice and diffs). Resource order follows the contract's path order via `Map` insertion; exports are sorted explicitly (`resourceExports.sort()`). Avoid timestamps, randomness, or unordered iteration in generator code. - `openapi.config.json` and `examples/openapi.json` are the test fixtures: `python.test.ts` imports the config, expects 8 operations, package `example_api`, client `Example`, the `Repository` URL, authors, and classifiers. Editing either file changes test expectations. - `bun run test` executes Python: the CLI test runs `uv run --no-project --python python3 --with ` twice. Without `uv` and `python3` on `PATH` that test fails. -- Ruff reformats generated Python after generation, so formatting inside the template literals is irrelevant. Python syntax errors fail `bun run generate` at the Ruff step and the CLI test at `uv run`; import errors and 3.11/3.14 differences only surface in CI's `compileall`/install/import steps — run those locally after touching `python.ts`. +- Ruff reformats generated Python after generation, so formatting inside the template literals is irrelevant. Python syntax errors fail `bun run generate` at the Ruff step and the CLI test at `uv run`; the CLI test also imports the generated package, but the full `compileall`/install/import/Ruff pass on both 3.11 and 3.14 runs only in CI — run those steps locally after touching `python.ts`. Python indentation stays significant inside the templates. - `python.version` omitted → the contract's `info.version` is used and must match `^\d+(\.\d+)*((a|b|rc)\d+)?(\.post\d+)?(\.dev\d+)?$`, otherwise generation throws `Set python.version …`. - Every `sync` overwrites `public/openapi.json` (gitignored) from `source`; `source` must be a local path or `https://` (no `http://`, no redirects). After changing the contract or config, re-run `bun run generate` and rebuild or restart the docs so both outputs come from the same `public/openapi.json`. - `tsconfig.json` excludes `generated/`; Biome excludes `generated/`, `public/openapi.json`, `.next`, and `out`; `.prettierignore` lists only `public/openapi.json`; `tsconfig.tsbuildinfo`, `.next/`, `out/`, `.idea/` are gitignored — never commit them. @@ -160,7 +160,7 @@ Paths below are relative to the repository root. - One test file: `lib/generators/python.test.ts` (`describe("Python generator")`). `beforeAll` generates into a `mkdtemp` directory from `examples/openapi.json` with the repository `openapi.config.json`, after planting `stale.py` to prove the output is wiped. Tests clone the fixture with `structuredClone`, mutate schemas to hit edge cases, and assert on generated source text, thrown messages, `getOperations` naming, `sdkArguments`, `schemaExample`, `buildApiRequest`, and `curlCodeSample`. Separate temp directories cover `python.cli` (runs the generated CLI through `uv run`) and `python.readme`/`python.authProvider`. Tests call `generatePython` directly, so assertions see output before Ruff formatting and the header pass; they assert on source text and never exercise HTTP retries or serialization through a transport. Useful `-t` filters: `multipart`, `successful response`, `mixed parse modes`, `generic string examples`, `consumer CLI`. - CI is the only place the generated package is installed with `uv pip install --system ./generated/python`, imported (`importlib.import_module(package)` and `getattr(m, client)`/`getattr(m, "Async" + client)`), compiled, and Ruff-checked on both Python 3.11 and 3.14; locally, only the CLI test installs and runs a generated package. -- There are no browser or E2E tests; `bun run typecheck`, `bun run lint`, and `bun run build` are the only checks on `app/` and `components/`. Verify UI changes in `bun run dev`. CSS linting and formatting are disabled in `biome.json`; dark mode follows `prefers-color-scheme` in `app/globals.css`. +- There are no browser or E2E tests; `bun run typecheck`, `bun run lint`, and `bun run build` are the only checks on `app/` and `components/`. Verify UI changes in `bun run dev`. `app/layout.tsx` loads Geist through `next/font/google`, so `bun run build` fetches from Google Fonts even though the contract is local (`NEXT_FONT_GOOGLE_MOCKED_RESPONSES` is Next's offline escape hatch). CSS linting and formatting are disabled in `biome.json`; dark mode follows `prefers-color-scheme` in `app/globals.css`. From 79b5585f3f36ac9d79326d6378e115cf38c53729 Mon Sep 17 00:00:00 2001 From: Glenn Jocher Date: Fri, 11 Sep 2026 15:36:30 +0800 Subject: [PATCH 05/10] List knip among the checks that cover the documentation UI in AGENTS.md --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 9b86ea4..06d429c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -160,7 +160,7 @@ Paths below are relative to the repository root. - One test file: `lib/generators/python.test.ts` (`describe("Python generator")`). `beforeAll` generates into a `mkdtemp` directory from `examples/openapi.json` with the repository `openapi.config.json`, after planting `stale.py` to prove the output is wiped. Tests clone the fixture with `structuredClone`, mutate schemas to hit edge cases, and assert on generated source text, thrown messages, `getOperations` naming, `sdkArguments`, `schemaExample`, `buildApiRequest`, and `curlCodeSample`. Separate temp directories cover `python.cli` (runs the generated CLI through `uv run`) and `python.readme`/`python.authProvider`. Tests call `generatePython` directly, so assertions see output before Ruff formatting and the header pass; they assert on source text and never exercise HTTP retries or serialization through a transport. Useful `-t` filters: `multipart`, `successful response`, `mixed parse modes`, `generic string examples`, `consumer CLI`. - CI is the only place the generated package is installed with `uv pip install --system ./generated/python`, imported (`importlib.import_module(package)` and `getattr(m, client)`/`getattr(m, "Async" + client)`), compiled, and Ruff-checked on both Python 3.11 and 3.14; locally, only the CLI test installs and runs a generated package. -- There are no browser or E2E tests; `bun run typecheck`, `bun run lint`, and `bun run build` are the only checks on `app/` and `components/`. Verify UI changes in `bun run dev`. `app/layout.tsx` loads Geist through `next/font/google`, so `bun run build` fetches from Google Fonts even though the contract is local (`NEXT_FONT_GOOGLE_MOCKED_RESPONSES` is Next's offline escape hatch). CSS linting and formatting are disabled in `biome.json`; dark mode follows `prefers-color-scheme` in `app/globals.css`. +- There are no browser or E2E tests; `bun run typecheck`, `bun run lint`, `bun run knip`, and `bun run build` are the only checks on `app/` and `components/`. Verify UI changes in `bun run dev`. `app/layout.tsx` loads Geist through `next/font/google`, so `bun run build` fetches from Google Fonts even though the contract is local (`NEXT_FONT_GOOGLE_MOCKED_RESPONSES` is Next's offline escape hatch). CSS linting and formatting are disabled in `biome.json`; dark mode follows `prefers-color-scheme` in `app/globals.css`. From 84d090e996a01f490453a04ac8f53b17a8776bbd Mon Sep 17 00:00:00 2001 From: Glenn Jocher Date: Fri, 11 Sep 2026 18:40:19 +0800 Subject: [PATCH 06/10] Open multipart file fields in generated Python code samples instead of passing the path string --- lib/generators/python.test.ts | 8 ++++++++ lib/openapi.ts | 27 +++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/lib/generators/python.test.ts b/lib/generators/python.test.ts index 5b7a821..0860c4b 100644 --- a/lib/generators/python.test.ts +++ b/lib/generators/python.test.ts @@ -13,6 +13,7 @@ import { getOperations, type OpenApiDocument, objectSchema, + pythonCodeSample, requestBodyExample, requestMedia, resolveServerUrl, @@ -279,6 +280,13 @@ describe("Python generator", () => { origin: "https://docs.example.com", }); expect(curl).toContain("-F 'file=@path/to/file'"); + expect( + pythonCodeSample(document, upload, { + client: config.python.client, + environment: config.apiKey.environment, + package: config.python.package, + }), + ).toContain('file=open("path/to/file", "rb")'); expect(curl).toStartWith("curl https://api.example.com/v1/uploads"); expect(curl).not.toContain("--request"); expect(curl).not.toContain("--url"); diff --git a/lib/openapi.ts b/lib/openapi.ts index 2980db4..e326239 100644 --- a/lib/openapi.ts +++ b/lib/openapi.ts @@ -531,7 +531,15 @@ export function sdkArguments(document: OpenApiDocument, operation: ApiOperation) return parameters; } +class PythonExpression { + readonly source: string; + constructor(source: string) { + this.source = source; + } +} + function pythonLiteral(value: unknown): string { + if (value instanceof PythonExpression) return value.source; if (value === null) return "None"; if (value === true) return "True"; if (value === false) return "False"; @@ -560,6 +568,25 @@ export function pythonCodeSample( ? requestBodyExample(document, operation) : requestBodyExampleValue(document, request); } + if ( + request?.[0].startsWith("multipart/") && + exampleBody && + typeof exampleBody === "object" && + !Array.isArray(exampleBody) + ) { + // The SDK passes multipart file fields to httpx as file content, so samples must open the file rather than pass its path + const properties = resolveSchema(document, request[1].schema)?.properties ?? {}; + exampleBody = Object.fromEntries( + Object.entries(exampleBody as Record).map(([name, value]) => [ + name, + isBinarySchema(document, properties[name]) + ? new PythonExpression( + `open(${JSON.stringify(typeof value === "string" && value && value !== "..." ? value : "path/to/file")}, "rb")`, + ) + : value, + ]), + ); + } const bodyValues = exampleBody && typeof exampleBody === "object" && !Array.isArray(exampleBody) ? (exampleBody as Record) From 837e903bcfd360e48bd68c4471050f8b053d3309 Mon Sep 17 00:00:00 2001 From: Glenn Jocher Date: Fri, 11 Sep 2026 18:43:49 +0800 Subject: [PATCH 07/10] Convert multipart file fields at render time for composed schemas and flattened arguments; keep explicit null --- lib/openapi.ts | 63 ++++++++++++++++++++++++++++++++++---------------- 1 file changed, 43 insertions(+), 20 deletions(-) diff --git a/lib/openapi.ts b/lib/openapi.ts index e326239..2cf8da3 100644 --- a/lib/openapi.ts +++ b/lib/openapi.ts @@ -552,6 +552,44 @@ function pythonLiteral(value: unknown): string { return JSON.stringify(value); } +function pythonFile(value: unknown): PythonExpression { + return new PythonExpression( + `open(${JSON.stringify(typeof value === "string" && value && value !== "..." ? value : "path/to/file")}, "rb")`, + ); +} + +function multipartPropertySchema( + document: OpenApiDocument, + input: JsonSchema | undefined, + name: string, + depth = 0, +): JsonSchema | undefined { + const schema = resolveSchema(document, input); + if (!schema || depth > 8) return undefined; + const direct = objectSchema(document, schema)?.properties?.[name]; + if (direct) return direct; + for (const branch of [...(schema.anyOf ?? []), ...(schema.oneOf ?? [])]) { + const found = multipartPropertySchema(document, branch, name, depth + 1); + if (found) return found; + } + return undefined; +} + +// The SDK sends multipart file fields to httpx as file content, so Python samples open the file instead of passing its path +function pythonMultipartValue(document: OpenApiDocument, schema: JsonSchema | undefined, value: unknown): unknown { + if (value === null || value === undefined) return value; + if (isBinarySchema(document, schema)) return pythonFile(value); + if (typeof value !== "object" || Array.isArray(value)) return value; + return Object.fromEntries( + Object.entries(value as Record).map(([name, item]) => [ + name, + item !== null && item !== undefined && isBinarySchema(document, multipartPropertySchema(document, schema, name)) + ? pythonFile(item) + : item, + ]), + ); +} + export function pythonCodeSample( document: OpenApiDocument, operation: ApiOperation, @@ -568,25 +606,6 @@ export function pythonCodeSample( ? requestBodyExample(document, operation) : requestBodyExampleValue(document, request); } - if ( - request?.[0].startsWith("multipart/") && - exampleBody && - typeof exampleBody === "object" && - !Array.isArray(exampleBody) - ) { - // The SDK passes multipart file fields to httpx as file content, so samples must open the file rather than pass its path - const properties = resolveSchema(document, request[1].schema)?.properties ?? {}; - exampleBody = Object.fromEntries( - Object.entries(exampleBody as Record).map(([name, value]) => [ - name, - isBinarySchema(document, properties[name]) - ? new PythonExpression( - `open(${JSON.stringify(typeof value === "string" && value && value !== "..." ? value : "path/to/file")}, "rb")`, - ) - : value, - ]), - ); - } const bodyValues = exampleBody && typeof exampleBody === "object" && !Array.isArray(exampleBody) ? (exampleBody as Record) @@ -602,7 +621,11 @@ export function pythonCodeSample( : bodyValues[argument.name] !== undefined ? bodyValues[argument.name] : schemaExample(document, argument.schema, 0, argument.name); - return { source: `${argument.pythonName}=${pythonLiteral(value)}`, value }; + const sample = + argument.location === "body" && request?.[0].startsWith("multipart/") + ? pythonMultipartValue(document, argument.wholeBody ? request[1].schema : argument.schema, value) + : value; + return { source: `${argument.pythonName}=${pythonLiteral(sample)}`, value }; }); return [ `from ${config.package} import ${config.client}`, From 8f988e6ff02f80254e0ff8a7cee4203df9c90cda Mon Sep 17 00:00:00 2001 From: Glenn Jocher Date: Fri, 11 Sep 2026 18:48:32 +0800 Subject: [PATCH 08/10] Limit nested file conversion to whole-body arguments and detect binary properties across schema unions --- lib/openapi.ts | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/lib/openapi.ts b/lib/openapi.ts index 2cf8da3..c78835b 100644 --- a/lib/openapi.ts +++ b/lib/openapi.ts @@ -558,32 +558,34 @@ function pythonFile(value: unknown): PythonExpression { ); } -function multipartPropertySchema( +function isMultipartFileProperty( document: OpenApiDocument, input: JsonSchema | undefined, name: string, depth = 0, -): JsonSchema | undefined { +): boolean { const schema = resolveSchema(document, input); - if (!schema || depth > 8) return undefined; - const direct = objectSchema(document, schema)?.properties?.[name]; - if (direct) return direct; - for (const branch of [...(schema.anyOf ?? []), ...(schema.oneOf ?? [])]) { - const found = multipartPropertySchema(document, branch, name, depth + 1); - if (found) return found; - } - return undefined; + if (!schema || depth > 8) return false; + if (isBinarySchema(document, objectSchema(document, schema)?.properties?.[name])) return true; + return [...(schema.allOf ?? []), ...(schema.anyOf ?? []), ...(schema.oneOf ?? [])].some((branch) => + isMultipartFileProperty(document, branch, name, depth + 1), + ); } // The SDK sends multipart file fields to httpx as file content, so Python samples open the file instead of passing its path -function pythonMultipartValue(document: OpenApiDocument, schema: JsonSchema | undefined, value: unknown): unknown { +function pythonMultipartValue( + document: OpenApiDocument, + argument: SdkArgument, + bodySchema: JsonSchema | undefined, + value: unknown, +): unknown { if (value === null || value === undefined) return value; - if (isBinarySchema(document, schema)) return pythonFile(value); + if (!argument.wholeBody) return isBinarySchema(document, argument.schema) ? pythonFile(value) : value; if (typeof value !== "object" || Array.isArray(value)) return value; return Object.fromEntries( Object.entries(value as Record).map(([name, item]) => [ name, - item !== null && item !== undefined && isBinarySchema(document, multipartPropertySchema(document, schema, name)) + item !== null && item !== undefined && isMultipartFileProperty(document, bodySchema, name) ? pythonFile(item) : item, ]), @@ -623,7 +625,7 @@ export function pythonCodeSample( : schemaExample(document, argument.schema, 0, argument.name); const sample = argument.location === "body" && request?.[0].startsWith("multipart/") - ? pythonMultipartValue(document, argument.wholeBody ? request[1].schema : argument.schema, value) + ? pythonMultipartValue(document, argument, request[1].schema, value) : value; return { source: `${argument.pythonName}=${pythonLiteral(sample)}`, value }; }); From 03f2e8672c125357e82fdaaea555b059734eb6a7 Mon Sep 17 00:00:00 2001 From: Glenn Jocher Date: Fri, 11 Sep 2026 20:26:31 +0800 Subject: [PATCH 09/10] Trim agent guidance to policies, commands, and owner pointers --- AGENTS.md | 148 ++++++++++--------------------------------------- lib/openapi.ts | 22 ++------ 2 files changed, 34 insertions(+), 136 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 06d429c..4552319 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,8 +1,6 @@ # AGENTS.md -This file provides guidance to AI coding agents (Claude Code, etc.) when working with code in this repository. CLAUDE.md is a symlink to this file. - -Ultralytics OpenAPI (`@ultralytics/openapi`, AGPL-3.0, `private: true` — never published to npm, consumed only from Git) turns one OpenAPI 3.0–3.2 contract into two outputs: an interactive static documentation site (Next.js 16 App Router, React 19, TypeScript strict, Tailwind CSS v4, shadcn `base-nova` components over Base UI) and a typed Python SDK (sync + async `httpx` clients). Bun manages dependencies and runs the package scripts and tests; `next`, `knip`, and `tsgo` are Node-shebang binaries, so Node is required as well; `uvx` runs Ruff inside `bun run generate`, and the CLI test runs generated Python through `uv run` with `python3`. There is no build step for the library code: `package.json` exports `./openapi` → `lib/openapi.ts` as raw TypeScript. +Repository guidance for coding agents. `CLAUDE.md` is a symlink to this file. ## Core Principles (CRITICAL) @@ -28,38 +26,37 @@ After opening a PR: 4. Never fight other commits: Ultralytics Actions pushes auto-format and header commits, and multiple users may work on the same PR. `git pull --rebase` before pushing; never reset or revert commits you did not author. 5. After the PR merges, clean up: remove local worktrees and branches for it, then `git checkout main && git pull`. -## Commands +## Product Boundary (CRITICAL) + +This repository is a standalone, general-purpose OpenAPI-to-SDK and API documentation product, intended to compete with products such as Stainless and Scalar. Third-party users must be able to generate SDKs and documentation for their own APIs without inheriting Ultralytics application behavior. + +- Never add Ultralytics ML-package or Platform-specific integrations, endpoint knowledge, credential stores, filesystem conventions, business rules, or dependencies to the converter or its generated defaults. Configurable product names do not make application-specific policy generic. +- Ultralytics-specific SDK behavior belongs in `ultralytics/sdk`, which owns the Python SDK and future language SDKs. Platform API behavior and contracts belong in the Platform repository. +- Extend the converter only with reusable, opt-in capabilities that make sense for independent API providers. Keep language-specific customization under that language's configuration; default generation must remain independent of any consumer. +- Keep consumer customizations reproducible through generation and synchronization. Never hand-edit generated output or make the converter depend on a consumer repository. +- Review every change against this boundary. Relocate application-specific work to its owner instead of teaching the converter about one application. + +## Commands and validation ```bash -bun install --frozen-lockfile # needs Bun, Node (next/knip/tsgo shebangs), uv, python3; CI uses Python 3.11 and 3.14 -bun run sync # scripts/sync.ts: read `source` from openapi.config.json → public/openapi.json (gitignored) -bun run generate # sync → scripts/generate.ts → generated/python (deleted and rewritten) → uvx ruff@0.16.2 format → scripts/headers.ts -bun run dev # sync → next dev (docs at http://localhost:3000; re-adds the nextjs-agent-rules block at the end of this file when it is missing or outdated) -bun run build # sync → next build (static export to out/) → scripts/headers.ts out -bun run typecheck # next typegen && tsgo --noEmit (works without public/openapi.json) -bun run lint # biome check . (lint:fix writes); generated/, public/openapi.json, .next, out are excluded in biome.json -bun run knip # knip --exclude binaries (no knip.json; exports of lib/openapi.ts are public via package.json "exports") -bun run test # bun test — the single test file lib/generators/python.test.ts (20 tests); the CLI test needs uv + python3 -bun test lib/generators/python.test.ts -t "multipart" # filter tests by name - -# Generated-Python checks that CI runs after `bun run generate` (Python 3.11 and 3.14) -python3 -m compileall -q generated/python/src -uv pip install --system ./generated/python # CI then imports the configured package and checks its and Async exports -uvx ruff@0.16.2 check generated/python - -# Generate for a consumer's configuration (relative paths inside it resolve against that file; public/openapi.json and generated/python stay in this repository) -OPENAPI_CONFIG=/absolute/path/to/sdk/openapi.config.json bun run generate +bun install --frozen-lockfile +bun run generate +bun run typecheck +bun run lint +bun run knip +bun run test +bun run build ``` -Run checks through the package scripts. Generated Python additionally supports `python3 -m compileall -q generated/python/src` and `uvx ruff@0.16.2 check generated/python`. +Use package scripts: `dev`, `build`, and `generate` synchronize the contract first. Bun, Node, uv, and Python are required; the CLI test runs generated Python. Generator changes also require CI's deterministic regeneration, Python compile/import checks, and Ruff checks (`.github/workflows/ci.yml`). `generatePython` deletes its output directory before writing; use only a disposable output path. + +## Where to look -- `dev`, `build`, and `generate` always sync first; `typecheck`, `lint`, `knip`, and `test` do not touch the contract. Never run `next build` directly: both `next.config.ts` (`generateBuildId` hashes `public/openapi.json`) and `app/page.tsx` (`readFileSync("public/openapi.json")`) fail with `ENOENT` without a prior sync. -- Configuration and contract are parsed with `JSON.parse` — no YAML. `getConfig` checks only the six required keys and the `python.cli` shape (`OpenApiConfig` is a TypeScript interface, not a runtime validator), and `sync` only checks that `openapi`, `info`, and `paths` exist. `bun run test` reads the repository `openapi.config.json` and `examples/openapi.json` directly; `OPENAPI_CONFIG` does not change its fixtures. -- Workflows in `.github/workflows/`: - - `ci.yml` (CI) — on PRs, pushes to `main`, a daily cron, and manual dispatch; one `Test` job per Python version (3.11, 3.14), 15-minute timeout. Steps in order: `bun install --frozen-lockfile`; `bun run generate` twice with `diff --recursive` between the two outputs (determinism gate); `bun run typecheck && bun run lint && bun run knip && bun run test && bun run build` (lint IS in CI here); `python -m compileall -q generated/python/src`; `uv pip install --system ./generated/python` followed by importing the configured client and `Async` client; `uvx ruff@0.16.2 check generated/python`. - - `format.yml` (Ultralytics Actions) — on PR open/sync/close/review-request and new issues; runs Ruff on Python, Prettier on YAML/JSON/Markdown/CSS (`.prettierignore` excludes `public/openapi.json`), codespell, Lychee link checks, AI labels and PR summary, and pushes auto-format commits to the PR branch. `python_docstrings: false` is deliberate — generated Google-style docstrings are owned by the generator. - - `cla.yml` — CLA check on PR open/reopen/sync and on `recheck`/signature comments. - - `.github/dependabot.yml` — weekly npm and GitHub Actions update PRs labeled `dependencies`. +- Shared contract interpretation and samples → `lib/openapi.ts`. +- Python output → `lib/generators/python.ts`, `lib/generators/python.test.ts`. +- Generation and synchronization → `scripts/generate.ts`, `scripts/sync.ts`. +- Configuration and fixtures → `openapi.config.json`, `examples/openapi.json`. +- Documentation UI → `app/`, `components/`. ## Conventions @@ -68,99 +65,12 @@ Run checks through the package scripts. Generated Python additionally supports ` - Generated output must be deterministic and is validated in CI. - Google-style docstrings, modern type hints, and a 120-character Python line length are formatted by Ruff. -## Product Boundary (CRITICAL) - -This repository is a standalone, general-purpose OpenAPI-to-SDK and API documentation product, intended to compete with products such as Stainless and Scalar. Third-party users must be able to generate SDKs and documentation for their own APIs without inheriting Ultralytics application behavior. +## Pitfalls -- Never add Ultralytics ML-package or Platform-specific integrations, endpoint knowledge, credential stores, filesystem conventions, business rules, or dependencies to the converter or its generated defaults. Configurable product names do not make application-specific policy generic. -- Ultralytics-specific SDK behavior belongs in `ultralytics/sdk`, which owns the Python SDK and future language SDKs. Platform API behavior and contracts belong in the Platform repository. -- Extend the converter only with reusable, opt-in capabilities that make sense for independent API providers. Keep language-specific customization under that language's configuration; default generation must remain independent of any consumer. -- Keep consumer customizations reproducible through generation and synchronization. Never hand-edit generated output or make the converter depend on a consumer repository. -- Review every change against this boundary. Relocate application-specific work to its owner instead of teaching the converter about one application. - -## Downstream Consumers and Compatibility Surfaces - -Both consumers track this repository's `main` with no pin, so every merged commit is live in their next CI run or build. Treat these surfaces as public API: - -- **`ultralytics/portal`** (Platform docs and contract). `apps/alpha/package.json` script `openapi:main` is exactly `bun add --dev --no-save @ultralytics/openapi@github:ultralytics/openapi#main`; alpha's `dev`, `build`, `api-docs`, and `openapi` scripts run it, and portal CI asserts that string before refreshing. Portal's `scripts/generate-openapi.ts` then calls `(await import("@ultralytics/openapi/openapi")).addPythonCodeSamples(document, { client, environment, package })` on an `openapi: "3.2.0"` document that portal generated itself, and alpha's `next.config.mjs` lists the package in `transpilePackages`. Contract: the `./openapi` export path, the `addPythonCodeSamples(document, PythonCodeSampleConfig)` signature, the `OpenApiDocument` type, `x-codeSamples` entries labeled `"Python SDK"`, and acceptance of 3.2.0 documents. `lib/openapi.ts` must stay import-free and isomorphic — it runs in the browser (`components/api-reference.tsx`), in Bun scripts here, and inside portal's generator and Next build. -- **`ultralytics/sdk`** (`ultralytics-platform` on PyPI). Its `ci.yml` checks out `ultralytics/openapi` at `ref: main` into `.generator/`, runs `bun install --frozen-lockfile` and `OPENAPI_CONFIG=/openapi.config.json bun run generate`, then `diff --recursive sdk/python .generator/generated/python`. Any byte of changed Python output fails every sdk PR until sdk's `Live` job (main push, daily, or dispatch) regenerates and auto-merges an "Update Platform API contract" PR whose branch hash includes this repository's `main` SHA. sdk's config exercises `header`, `python.cli` (`ul` from `cli.py`), `python.authProvider` (`auth.py`), `python.readme`, omits `python.version` (so the version is the contract's `info.version`), and carries an sdk-only `upstream` key that `getConfig` neither validates nor reads. Contract: every key in `lib/config.ts` `OpenApiConfig`, the `generate` script name, the `generated/python` output path, `bun.lock` installing with `--frozen-lockfile`, and deterministic output. -- Before merging a generator change, reproduce the sdk drift check locally: `OPENAPI_CONFIG=/path/to/sdk/openapi.config.json bun run generate && diff -r generated/python /path/to/sdk/sdk/python` — a non-empty diff is expected only when the change intends to alter the published SDK, and it means sdk CI stays red until the `Live` job's contract-update PR merges. - -## Architecture - -- Downstream API docs and SDK consumers must track this repository's `main` branch. Never introduce a commit SHA or tag pin for `ultralytics/openapi` in Portal, SDK, or related automation. -- `openapi.config.json` points to the sole local or remote API contract. Never duplicate or patch endpoint definitions in a generator. -- `lib/openapi.ts` owns parsing, schema normalization, examples, and operation names shared by documentation and every SDK. -- `lib/generators/` contains language-specific renderers. Add another language only when its implementation is ready; do not add placeholder abstractions. -- `generated/` contains ignored local SDK output and is never committed or edited manually. Change the contract, shared representation, or renderer, then regenerate. -- `components/api-reference.tsx` renders the interactive reference from the same shared operation model. API keys remain in browser memory and never appear in copied examples. -- The documentation uses shadcn's `base-nova` style with Base UI primitives and Ultralytics design tokens. - -Paths below are relative to the repository root. - -**Data flow.** `lib/config.ts` `getConfig()` reads `OPENAPI_CONFIG` or `openapi.config.json`, throws on a missing `source`, `name`, `apiKey.environment`, `python.client`, `python.package`, or `python.project`, defaults `python.install` to `pip install ` and `license` to this repository's AGPL-3.0 `LICENSE`, and resolves relative `source`, `license.file`, `python.readme`, `python.authProvider`, and `python.cli.source` against the config file's directory. `scripts/sync.ts` loads `source` (local file, or `https://` with `redirect: "error"`; `http://` is rejected), requires `openapi`, `info`, and `paths` keys, and writes `public/openapi.json`. Two consumers read that one file: `app/page.tsx` (Server Component, `readFileSync`) hands the document plus `apiKey.environment`, `python`, and `specUrl` to the client `ApiReference`, and `scripts/generate.ts` calls `generatePython(document, config, "generated/python")`. Both go through `lib/openapi.ts` `getOperations(document)`, which flattens `paths` into `ApiOperation[]`: `id` (`operationId`, else `-`, de-duplicated with `-2`, `-3`, …), `tag` (first tag, else `"Other"`), `resource` (`sdkIdentifier(tag)`), `sdkMethod`, parameters merged from path-item and operation level with `$ref`s resolved, resolved `requestBody`/`responses`, and `server` (operation → path item → document). - -- `lib/config.ts` — `OpenApiConfig` (the configuration interface; defaults and required-key checks live in `getConfig()`; the README "Configure" section documents it for users); `configPath` is also hashed by `next.config.ts`. -- `lib/openapi.ts` (no imports; browser-safe) — - - Types: `JsonSchema`, `Parameter`, `OpenApiDocument`, `ApiOperation`, `SdkArgument`, `PythonCodeSampleConfig`, `ApiAuthentication`. - - Identifiers: `sdkIdentifier` (camelCase and punctuation become `snake_case`, possessives are stripped, Python keywords get a trailing underscore, leading digits get a leading underscore), `allocateSdkIdentifiers` (collision suffixes `_` then `_2`, `_3`, …; always reserves `self`, and `sdkArguments` passes `extra_headers` and `timeout` as extra reserved names while `modelSource` field allocation passes none). - - Method naming: `sdkMethodCandidates` + `SDK_VERBS` (`get`→`retrieve`/`list`, `post`→`create`, `put`/`patch`→`update`, `delete`→`delete`) — `x-sdk-method` wins (validated as a lowercase identifier, unique per resource), else the static path leaf after the resource segment, else the verb; a leafless GET with a path parameter is `retrieve` unless its summary starts with list/view/search. `getOperations` resolves collisions per resource (canonical names first, GET keeps the bare leaf, then `_` / `_`, then `_2` suffixes). - - Arguments: `sdkArguments` — path arguments are required (positional or keyword), every other contract argument is keyword-only (`*` is emitted only when such arguments exist); a JSON/form/multipart object body with at least one named property is flattened into per-property keyword arguments unless the body schema (or one of its `allOf` branches) is a `oneOf`/`anyOf` union, has incompatible closed `allOf` branches, carries an object-valued `additionalProperties` inside `allOf` (`constrainedComposedBody`), or sets `minProperties`/`maxProperties`; then a single `body` argument (`wholeBody: true`) is emitted. Unions inside individual properties do not prevent flattening, a top-level object with named properties and its own object-valued `additionalProperties` still flattens, and a body with only `additionalProperties` and no named properties becomes a whole `body`. `readOnly` properties are skipped. - - Auth: `getAuthentication` accepts only single-scheme security requirements that are `apiKey` in `header` or `http` `bearer` (returns `{ header, prefix }`); `getAuthenticationMode` → `none`/`optional`/`required`. - - Servers: `resolveServerUrl(document, origin, operation?)`, `expandServerUrl` (substitutes `variables[*].default`); the generated `base_url` default is the first document server (a relative URL is resolved against `http://localhost:3000`); a relative operation- or path-level server override is emitted as `server=` and joined onto the client's `base_url` at runtime, an absolute override is baked into the request URL, and the browser resolves relative servers against `window.location.origin`. - - Schemas: `resolveSchema` (local `#/components/schemas/` only, sibling keys and `properties`/`required` merged over the target, cycle-safe), `objectSchema` (merges `allOf` and object-shaped `anyOf`/`oneOf` into one object schema), `schemaExample`/`stringExample`/`schemaMatches` (example synthesis: `example` → `default` → `const` → first matching `enum` value → union/variant walk → format-aware strings), `schemaLabel`, `schemaConstraints`, `schemaFields`. - - Responses: `successMediaEntries`/`successMedia`/`successSchema` (exact `2xx` codes first, else `2XX`; distinct shapes become `oneOf`), `requestMedia` (`preferredMedia` order: `application/json`, `+json`, form/multipart, `text/*`, first listed). - - Samples and requests: `pythonCodeSample`, `addPythonCodeSamples` (rewrites the `"Python SDK"` entry of each operation's `x-codeSamples`), `curlCodeSample` (uses the `YOUR_API_KEY` placeholder), `requestBodyExample`, `buildApiRequest` (URL, headers, and `BodyInit` for the docs "Send" button; refuses cookie parameters and missing required parameters). The docs UI does not render `x-codeSamples`: the cURL sample reflects the current parameter, file, and body inputs, while the Python sample takes only the parsed body and synthesizes required non-body arguments from schema examples. -- `lib/generators/python.ts` — `generatePython(document, config, output)` → `prepare` (calls `getOperations`, then `validateOperation`, which throws `Unsupported …` for: security requirements whose alternatives are all non-empty and none is a supported scheme (an anonymous `{}` alternative permits generation), non-`simple` path styles, `allowReserved` query params, array/object header or cookie params, schema-less request bodies, a non-empty request `encoding` object, and mixed JSON/text/binary modes across the preferred media of the success responses) → `rm -rf output` → writes files. Rendering: `pythonType` (JSON Schema → `str`/`int`/`float`/`bool`/`Sequence[...]` for arguments and `list[...]` for responses/`dict[str, Any]`/`BinaryIO`/`Literal[...]`/`| None`), `docstring` (Google style, `Args:` types in parentheses), `methodSource` (one `self._client.request(...)` call with `params=[*_query_parameter(...)]`, `json=`/`data=_form_data(...)`/`files=`/`content=`, `auth=(header, prefix)`, `text=True` for text responses), `resourceSource`, `modelSource` (functional `TypedDict`s in `types.py` named `Response`, nested `…Item`, `NotRequired` for optional fields, wire keys preserved, response `writeOnly` fields dropped; nested helpers `possibleTypes`, `objectUnion`, `modelType`, `addModel`), `clientSource`/`apiClientSource` (the runtime: `_resolve_api_key`, `NotGiven`/`NOT_GIVEN`, `_path_parameter`, `_query_parameter`, `_form_data`, `_retry_delay`, `SyncAPIClient`/`AsyncAPIClient.request`), `EXCEPTIONS_SOURCE` (`APIError(status_code, body, request_id)` with `.json`, `APIConnectionError`), `publicClientSource` (``/`Async` with `api_key`, `base_url`, `timeout=60.0`, `max_retries=2`, `http_client`, context managers). Generator-owned Python lives inside TypeScript template literals (consumer `authProvider`/`cli.source` files are copied verbatim) — escape `\`, `${`, and backticks; Ruff reformats the output afterwards, and its parser is the first thing that fails on a Python syntax error. - - Generated layout: `pyproject.toml` (`uv_build` backend, `httpx>=0.28,<1`, `[tool.ruff] line-length = 120`, `[project.scripts]` only with `python.cli`), `README.md`, `LICENSE`, `src//{__init__.py,_client.py,_exceptions.py,client.py,async_client.py,types.py,py.typed}`, `src//resources/{__init__.py,.py}`, plus optional `_auth.py` (copied `python.authProvider`), `cli.py` (copied `python.cli.source`), and `_cli_metadata.py` (`MULTIPART_FILES` mapping `resource.method` → binary field names of whole-body multipart operations). - - Runtime behavior worth knowing before "fixing" a client bug: the auth header is added only when a key resolved (explicit → `apiKey.environment` → provider; `""` disables) and the operation has auth; retries apply to GET/HEAD/OPTIONS on any `httpx.HTTPError` and on 408/409/429/5xx responses, plus 429 when the request passed a non-`None` `json=` payload; a numeric `Retry-After` is honored (clamped to 0–60 s), otherwise — including HTTP-date values — the delay is `0.5·2^attempt` capped at 8 s; 204/empty → `None`, JSON media → parsed, `text=True` or `text/*` → `str`, else `bytes`. Optional contract arguments default to `NOT_GIVEN`, not `None` (`timeout` and `extra_headers` default to `None`): `_json_value` drops `NotGiven` fields recursively but keeps explicit `None` and turns sequences into lists, `_without_none` drops both from headers and cookies, and `_query_parameter` skips both. Response types are `typing.cast` declarations only — the runtime returns decoded JSON, `str`, `bytes`, or `None` with no validation. A supplied `http_client` keeps its own timeout (the constructor `timeout` only configures the client the SDK creates) and is closed by `close()`/context exit either way. `python.authProvider` must export a synchronous `get_api_key() -> str | None`, called at most once per client construction and only when neither an explicit key nor `apiKey.environment` resolves; `python.cli.source` must define a zero-argument `main()` (console script `.cli:main`) and its own `python -m` launcher. -- `components/api-reference.tsx` (`"use client"`) — `ApiReference` (API key held in React state only, ⌘/Ctrl+K focuses search, selection via URL hash `#operation=`), `OperationNavigation`, `OverviewPanel` (version badges, "Download OpenAPI contract" from `specUrl`, servers, security schemes, Python quick start), `OperationPanel` (parameter inputs, body textarea seeded by `requestBodyExample`, file inputs for binary multipart fields, "Send" = `buildApiRequest` + browser `fetch`, so the target API must allow CORS), `codeExamples` (live cURL + Python), `Markdown` (react-markdown). `components/ui/*` are shadcn `base-nova` copies over `@base-ui/react` — compose with `render={}`, never Radix `asChild`. `components/logo.tsx` + `app/icon.svg` + `--brand-gradient-*` in `app/globals.css` are the brand mark, favicon, and gradient hooks; visible titles come from the contract's `info.title` and the config `name`. `OperationPanel` is keyed by `selected.id`, so switching operations remounts it and resets parameters, body, files, and result; the selection is read from the hash inside an effect, so the first render is always the overview. `SchemaFields` uses the shared `schemaFields` (hides request `readOnly` and response `writeOnly` fields, stops at depth 4), and `.operation-list-item` in `app/globals.css` uses `content-visibility: auto` to keep long navigation lists cheap. -- `app/` — `layout.tsx` (title `${config.name} Reference`, Geist fonts, skip link), `page.tsx`, `globals.css` (Tailwind v4 CSS-first with oklch tokens; no tailwind config), `icon.svg`. `next.config.ts`: `output: "export"`, `basePath` from `docs.basePath`, and `generateBuildId` = SHA-256 of `app/`, `components/`, `lib/`, `bun.lock`, `package.json`, `next.config.ts`, `postcss.config.mjs`, the config file, and `public/openapi.json` (first 20 hex characters), so the build id is stable for the same inputs; `scripts/headers.ts` runs after the build and is not part of that hash. -- `scripts/` — `sync.ts`, `generate.ts` (Python only; a new language is wired here), `headers.ts` (prepends `header` to `.html`, `.css`, `.js`, `.py`, `.toml` files under the given directories; a no-op when `header` is unset, which is the case for this repository's own config). -- `examples/openapi.json` — OpenAPI 3.1 fixture (7 paths, 8 operations) covering server variables, bearer auth, `allOf`/`anyOf`/`oneOf`, nullable, `writeOnly`, binary multipart, text and empty responses. It is both the `source` of the repository's `openapi.config.json` and the test fixture. - -## Python Output - -- Follow the OpenAI client shape: one client, grouped resources, keyword arguments, and environment-based authentication. -- Generate synchronous and asynchronous clients with the same resource tree. -- Generate Google-style docstrings. Types are parenthesized in `Args:`, `Returns:`, and `Raises:` sections. -- Generated SDK packages default to AGPL-3.0 and use the license configured in `openapi.config.json`. - -## Where to look - -- Wrong resource or method name in the SDK, docs sample, or `x-codeSamples` → `sdkIdentifier`, `sdkMethodCandidates`, and the collision pass in `getOperations` (`lib/openapi.ts`); contracts can pin a name with `x-sdk-method`. -- Wrong keyword arguments or a body that should/shouldn't be flattened → `sdkArguments`; Python-side rendering of those arguments → `methodSource`. -- Wrong Python type, `TypedDict`, or nullability → `pythonType`, `isNullable`, `literalValues` in `lib/generators/python.ts`; wrong response union, nested model name, or recursive reference → `modelSource`'s nested `possibleTypes`, `objectUnion`, `modelType`, `addModel`. -- Wrong path/query/form encoding → browser/cURL: `serializeSimplePath`, `serializeQueryParameter`, `parameterValue`, `formEntries` in `lib/openapi.ts`; Python: `_path_parameter`, `_query_parameter`, `_form_data` in `clientSource` — two implementations that must agree. -- A `$ref` is not resolved → `resolveSchema` and `resolveParameter` silently pass through non-local references (and `resolveSchema` also passes through missing local ones); `resolveRequestBody`/`resolveResponse` throw `Unsupported reference` for non-local targets, and all three component resolvers throw `Unknown … reference` for a missing local target. -- `bun run generate` throws `Unsupported …` / `Unknown … reference` → `validateOperation` (python.ts) or `componentName`/`resolveParameter`/`resolveRequestBody`/`resolveResponse` (openapi.ts). Add opt-in, contract-driven support or fix the contract; never special-case one API (Product Boundary). -- Wrong example values in docs or samples → `schemaExample`, `stringExample`, `authoredSchemaExample`, `requestBodyExampleValue`, `pythonCodeSample`, `curlCodeSample`. -- HTTP behavior of generated clients (auth header, retries, encoding, response decoding) → `clientSource`/`apiClientSource`; errors → `EXCEPTIONS_SOURCE`; constructor surface → `publicClientSource`. -- Package metadata (`pyproject.toml`, README, LICENSE, CLI entry point, version validation) → the tail of `generatePython`. -- Docs page bug → `components/api-reference.tsx`; shell/metadata → `app/layout.tsx`; theme tokens → `app/globals.css`; static export/base path → `next.config.ts`. -- New configuration key → `OpenApiConfig` + `getConfig` in `lib/config.ts`, consume it in the generator or page, document it in the "Configure" section of `README.md` and `README.zh-CN.md`. -- New language → `lib/generators/.ts` exporting `generate(document, config, output)` built on `getOperations`/`sdkArguments`, a `` block in `OpenApiConfig`, a call in `scripts/generate.ts`, a sibling `.test.ts`, and matching CI validation steps in `.github/workflows/ci.yml`. -- Header/licensing of emitted files → `scripts/headers.ts` and the `header`/`license` config keys. - -## Gotchas - -- `generatePython` deletes the output directory before writing. Never point `output` at a directory you care about; CI and the sdk repo rely on this to drop stale files. - Determinism is enforced (CI generates twice and diffs). Resource order follows the contract's path order via `Map` insertion; exports are sorted explicitly (`resourceExports.sort()`). Avoid timestamps, randomness, or unordered iteration in generator code. -- `openapi.config.json` and `examples/openapi.json` are the test fixtures: `python.test.ts` imports the config, expects 8 operations, package `example_api`, client `Example`, the `Repository` URL, authors, and classifiers. Editing either file changes test expectations. -- `bun run test` executes Python: the CLI test runs `uv run --no-project --python python3 --with ` twice. Without `uv` and `python3` on `PATH` that test fails. -- Ruff reformats generated Python after generation, so formatting inside the template literals is irrelevant. Python syntax errors fail `bun run generate` at the Ruff step and the CLI test at `uv run`; the CLI test also imports the generated package, but the full `compileall`/install/import/Ruff pass on both 3.11 and 3.14 runs only in CI — run those steps locally after touching `python.ts`. Python indentation stays significant inside the templates. -- `python.version` omitted → the contract's `info.version` is used and must match `^\d+(\.\d+)*((a|b|rc)\d+)?(\.post\d+)?(\.dev\d+)?$`, otherwise generation throws `Set python.version …`. -- Every `sync` overwrites `public/openapi.json` (gitignored) from `source`; `source` must be a local path or `https://` (no `http://`, no redirects). After changing the contract or config, re-run `bun run generate` and rebuild or restart the docs so both outputs come from the same `public/openapi.json`. -- `tsconfig.json` excludes `generated/`; Biome excludes `generated/`, `public/openapi.json`, `.next`, and `out`; `.prettierignore` lists only `public/openapi.json`; `tsconfig.tsbuildinfo`, `.next/`, `out/`, `.idea/` are gitignored — never commit them. - `format.yml` reformats JSON/Markdown/YAML with Prettier and pushes to your branch; `git pull --rebase` before every push. -- `next dev` re-adds or updates the `nextjs-agent-rules` block at the bottom of this file when it detects a coding agent and the block is missing or outdated (see the block's own note); commit it rather than reverting it. -- `knip` has no config file: exports of entry files are not reported (`lib/openapi.ts` via package `exports`, `app/` files via the Next plugin), while unused exports in other modules and unused files or dependencies fail `bun run knip`. - -## Tests -- One test file: `lib/generators/python.test.ts` (`describe("Python generator")`). `beforeAll` generates into a `mkdtemp` directory from `examples/openapi.json` with the repository `openapi.config.json`, after planting `stale.py` to prove the output is wiped. Tests clone the fixture with `structuredClone`, mutate schemas to hit edge cases, and assert on generated source text, thrown messages, `getOperations` naming, `sdkArguments`, `schemaExample`, `buildApiRequest`, and `curlCodeSample`. Separate temp directories cover `python.cli` (runs the generated CLI through `uv run`) and `python.readme`/`python.authProvider`. Tests call `generatePython` directly, so assertions see output before Ruff formatting and the header pass; they assert on source text and never exercise HTTP retries or serialization through a transport. Useful `-t` filters: `multipart`, `successful response`, `mixed parse modes`, `generic string examples`, `consumer CLI`. -- CI is the only place the generated package is installed with `uv pip install --system ./generated/python`, imported (`importlib.import_module(package)` and `getattr(m, client)`/`getattr(m, "Async" + client)`), compiled, and Ruff-checked on both Python 3.11 and 3.14; locally, only the CLI test installs and runs a generated package. -- There are no browser or E2E tests; `bun run typecheck`, `bun run lint`, `bun run knip`, and `bun run build` are the only checks on `app/` and `components/`. Verify UI changes in `bun run dev`. `app/layout.tsx` loads Geist through `next/font/google`, so `bun run build` fetches from Google Fonts even though the contract is local (`NEXT_FONT_GOOGLE_MOCKED_RESPONSES` is Next's offline escape hatch). CSS linting and formatting are disabled in `biome.json`; dark mode follows `prefers-color-scheme` in `app/globals.css`. +Consumers must follow this repository's `main`, never a SHA or tag. Keep contract definitions in the configured input, language output in its renderer, and generated files untracked. API keys stay in browser memory and out of copied examples. Compose the existing Base UI primitives with `render`, not Radix `asChild`. diff --git a/lib/openapi.ts b/lib/openapi.ts index c78835b..4f426ce 100644 --- a/lib/openapi.ts +++ b/lib/openapi.ts @@ -558,20 +558,6 @@ function pythonFile(value: unknown): PythonExpression { ); } -function isMultipartFileProperty( - document: OpenApiDocument, - input: JsonSchema | undefined, - name: string, - depth = 0, -): boolean { - const schema = resolveSchema(document, input); - if (!schema || depth > 8) return false; - if (isBinarySchema(document, objectSchema(document, schema)?.properties?.[name])) return true; - return [...(schema.allOf ?? []), ...(schema.anyOf ?? []), ...(schema.oneOf ?? [])].some((branch) => - isMultipartFileProperty(document, branch, name, depth + 1), - ); -} - // The SDK sends multipart file fields to httpx as file content, so Python samples open the file instead of passing its path function pythonMultipartValue( document: OpenApiDocument, @@ -580,12 +566,14 @@ function pythonMultipartValue( value: unknown, ): unknown { if (value === null || value === undefined) return value; - if (!argument.wholeBody) return isBinarySchema(document, argument.schema) ? pythonFile(value) : value; + if (!argument.wholeBody) + return resolveSchema(document, argument.schema)?.format === "binary" ? pythonFile(value) : value; if (typeof value !== "object" || Array.isArray(value)) return value; + const properties = objectSchema(document, bodySchema)?.properties; return Object.fromEntries( Object.entries(value as Record).map(([name, item]) => [ name, - item !== null && item !== undefined && isMultipartFileProperty(document, bodySchema, name) + item !== null && item !== undefined && resolveSchema(document, properties?.[name])?.format === "binary" ? pythonFile(item) : item, ]), @@ -624,7 +612,7 @@ export function pythonCodeSample( ? bodyValues[argument.name] : schemaExample(document, argument.schema, 0, argument.name); const sample = - argument.location === "body" && request?.[0].startsWith("multipart/") + argument.location === "body" && request?.[0] === "multipart/form-data" ? pythonMultipartValue(document, argument, request[1].schema, value) : value; return { source: `${argument.pythonName}=${pythonLiteral(sample)}`, value }; From 6f61703b0006cff6abbf0f5bd02af7d857a82ca0 Mon Sep 17 00:00:00 2001 From: Glenn Jocher Date: Fri, 11 Sep 2026 20:30:41 +0800 Subject: [PATCH 10/10] Clarify retained guidance and fix review findings --- AGENTS.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 4552319..dd514ab 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -72,6 +72,10 @@ Use package scripts: `dev`, `build`, and `generate` synchronize the contract fir Consumers must follow this repository's `main`, never a SHA or tag. Keep contract definitions in the configured input, language output in its renderer, and generated files untracked. API keys stay in browser memory and out of copied examples. Compose the existing Base UI primitives with `render`, not Radix `asChild`. +## Python output conventions + +Use one client with grouped resources, keyword arguments, and environment authentication. Sync and async clients share the same resource tree. Generate Google-style docstrings with parenthesized types in Args, Returns, and Raises. Generated packages use the configured license, defaulting to AGPL-3.0. + # This is NOT the Next.js you know