Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
4921950
feat: generate-spec command
sobanieca-redocly Jun 24, 2026
6b62b28
tests: generate-spec e2e
sobanieca-redocly Jul 6, 2026
e40ff10
chore: remove plugin support
sobanieca-redocly Jul 7, 2026
73ff400
chore: pr comments
sobanieca-redocly Jul 7, 2026
956bf00
chore: pr comments
sobanieca-redocly Jul 7, 2026
bcd25c2
chore: refined ai usage
sobanieca-redocly Jul 7, 2026
3a11b20
chore: improve generator
sobanieca-redocly Jul 7, 2026
d7ee556
chore: smarter inference
sobanieca-redocly Jul 9, 2026
2ae80d8
Merge branch 'main' into feat/generate-spec-command
sobanieca-redocly Jul 10, 2026
e731b9c
chore: adjust codeowners
sobanieca-redocly Jul 10, 2026
c025fe0
chore: improve ai performance for large input files
sobanieca-redocly Jul 10, 2026
90a2052
chore: apply pr comments
sobanieca-redocly Jul 10, 2026
45980fa
chore: improve performance for ai spec generate
sobanieca-redocly Jul 10, 2026
b3b9a11
chore: performance improvements
sobanieca-redocly Jul 14, 2026
22674ba
fix: keep unobserved list envelopes inline and infer multipart form r…
sobanieca-redocly Jul 14, 2026
b3ce51a
docs: generate-spec docs
sobanieca-redocly Jul 15, 2026
e387bc5
docs: update generate spec docs
sobanieca-redocly Jul 15, 2026
b464f23
docs: simplify docs
sobanieca-redocly Jul 15, 2026
896fd97
docs(cli): format readme with semantic line breaks
JLekawa Jul 16, 2026
f7f5917
Apply suggestions from code review
JLekawa Jul 16, 2026
918faf5
chore: export singularize
sobanieca-redocly Jul 17, 2026
2d41e65
fix: export singularize from separate file
sobanieca-redocly Jul 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/generate-spec-command.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@redocly/cli': minor
---

Added an experimental `generate-spec` command that infers an OpenAPI description from recorded HTTP traffic.
5 changes: 5 additions & 0 deletions .changeset/singularize-core-export.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@redocly/openapi-core': minor
---

Added a `singularize` utility that returns the singular form of an English word.
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ docs/ @Redocly/technical-writers
packages/cli/src/utils/otel.ts @Redocly/hot-dogs
packages/cli/src/utils/telemetry.ts @Redocly/hot-dogs
packages/cli/src/commands/drift/ @Redocly/cyberpunks
packages/cli/src/commands/generate-spec/ @Redocly/cyberpunks
packages/cli/src/commands/proxy/ @Redocly/cyberpunks
1 change: 1 addition & 0 deletions docs/@v2/commands/drift.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,4 @@ redocly drift ./traffic.har --api ./openapi.yaml --format json -o ./drift-report
## Related commands

- [`proxy`](./proxy.md) captures live HTTP traffic into a HAR file that can be replayed through `drift`.
- [`generate-spec`](./generate-spec.md) infers an OpenAPI description from the same traffic formats.
119 changes: 119 additions & 0 deletions docs/@v2/commands/generate-spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# `generate-spec`

The `generate-spec` command infers an OpenAPI description from recorded HTTP traffic.
The command reads a traffic log (or a folder of logs), builds a deterministic baseline description from the observed exchanges, and can optionally refine that baseline with an AI provider.

{% admonition type="warning" name="Experimental" %}
This is an experimental feature.
Its behavior, command, flags, and output may change in future releases.

The `generate-spec` command generates OpenAPI 3.1 descriptions only.
{% /admonition %}

## Supported traffic formats

The traffic input can be provided in any of the following formats.
By default the format is detected automatically from the file contents:

- HAR
- Kong
- Nginx JSON
- Apache JSON
- NDJSON

Traffic parsing is shared with the [`drift`](./drift.md) command, so any log that works with `drift` works here too.

## How it works

The baseline description is inferred directly from the recorded exchanges: identifier-like path segments become named path parameters, request and response schemas are merged across all observations, repeated object shapes are extracted into `components/schemas`, and consistently formatted string values get `format` or `enum` hints.

A description inferred from traffic alone is a hypothesis: types are coarse and there are no descriptions, enums, or examples beyond what was observed.
With `--with-ai`, the command sends each operation together with sample exchanges to an AI provider, which narrows types and adds formats, enums, descriptions, and examples.

The AI's answer is never trusted blindly: a refined operation is only accepted when it keeps the operation's path, method, and observed response status codes, and passes validation with the `spec` ruleset.
Rejected refinements keep their baseline version, and if refinement fails entirely the command falls back to the baseline description.
The validation uses the built-in `spec` ruleset, not your project's `redocly.yaml` — lint the generated description with your own configuration afterward.

{% admonition type="warning" name="Data sharing" %}
`--with-ai` sends samples of the recorded traffic (URLs, query strings, request and response bodies) to the selected AI provider.
Comment thread
DmitryAnansky marked this conversation as resolved.
Make sure the traffic contains no secrets or personal data you are not allowed to share.
{% /admonition %}

### AI providers

Refinement runs a locally installed AI CLI in non-interactive mode: `claude` (Claude Code), `codex` (Codex CLI), or `cursor` (Cursor CLI).
The selected CLI must be installed and authenticated on the machine running the command.

The provider runs in isolation: project context the CLIs normally load (such as `CLAUDE.md`, `AGENTS.md`, or `.cursor/rules`) and settings like a configured model do not apply.
Use `--ai-model` to choose a model, or the provider's default is used.

## Usage

```bash
redocly generate-spec <traffic>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we call it traffic-log or traffic-record to make it more clear?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The description already says "traffic log file or folder", I would keep it short.

redocly generate-spec <traffic> [--traffic-format=<option>] [--server=<url>]
redocly generate-spec <traffic> [--title=<string>] [--output=<file>]
redocly generate-spec <traffic> --with-ai [--ai-provider=<option>] [--ai-model=<string>] [--ai-concurrency=<number>]
```

## Options

| Option | Type | Description |
| ---------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| traffic | string | **REQUIRED.** Path to a traffic log file or folder (HAR, Kong, Nginx/Apache JSON, NDJSON). |
| --type | string | Target API description type.<br/>**Possible values:** `openapi`. Default value is `openapi`. |
| --traffic-format | string | Traffic input format.<br/>**Possible values:** `auto`, `har`, `kong`, `nginx-json`, `apache-json`, `ndjson`. Default value is `auto`. |
| --server | string | Server URL the traffic was captured against (host, host + base path, or a path-only prefix like `/api`). Only requests under it are considered, the rest of their URL is treated as the API path, and it becomes the `servers` URL of the generated description. |
| --title | string | Title for the generated API description. Default value is `Generated API`. |
| --with-ai | boolean | Refine the inferred description with an AI provider. Default value is `false`. |
| --ai-provider | string | AI provider used with `--with-ai`. Runs the corresponding CLI in non-interactive mode.<br/>**Possible values:** `claude`, `codex`, `cursor`. Default value is `claude`. |
| --ai-model | string | Model passed to the selected AI provider. If not set, the provider's default model is used. |
| --ai-concurrency | number | Number of operations refined in parallel with `--with-ai`. Default value is `4`. |
| --output, -o | string | Write the generated description to this file instead of stdout. |
| --config | string | Specify path to the [configuration file](../configuration/index.md). |
| --help | boolean | Display help. |
| --version | boolean | Display version number. |

## Examples

### Generate a description from a HAR capture

```bash
redocly generate-spec ./traffic.har -o ./openapi.yaml
```

### Scope the traffic to a server

When the capture contains traffic for more than one host, or the API lives behind a base path (for example, a gateway that adds `/api`), use `--server` to declare the server the traffic was captured against.
Only requests under it are considered, and the remaining path becomes the API path:

```bash
redocly generate-spec ./traffic.har --server https://api.example.com/api -o ./openapi.yaml
```

### Refine the description with AI

```bash
redocly generate-spec ./traffic.har --with-ai --ai-provider claude -o ./openapi.yaml
```

### Speed up refinement for large APIs

For descriptions with many operations, raise the concurrency and pick a faster model to shorten the run:

```bash
redocly generate-spec ./traffic-logs/ --with-ai --ai-concurrency 12 --ai-model claude-sonnet-5 -o ./openapi.yaml
```

### Capture traffic and generate a description

```bash
redocly proxy --target https://api.example.com --har ./capture.har
# point your client at http://127.0.0.1:4040, then press Ctrl+C to stop
redocly generate-spec ./capture.har -o ./openapi.yaml
```

## Related commands

- [`proxy`](./proxy.md) captures live HTTP traffic into a HAR file that can be fed into `generate-spec`.
- [`drift`](./drift.md) detects drift between recorded HTTP traffic and an existing OpenAPI description.
1 change: 1 addition & 0 deletions docs/@v2/commands/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Testing commands:
- [`generate-arazzo`](generate-arazzo.md) Generate an Arazzo description from an OpenAPI description.
- [`drift`](drift.md) Detect drift between recorded HTTP traffic and an OpenAPI description [experimental feature].
- [`proxy`](proxy.md) Capture live HTTP traffic through a reverse proxy into a HAR file [experimental feature].
- [`generate-spec`](generate-spec.md) Infer an OpenAPI description from recorded HTTP traffic [experimental feature].

Redocly platform commands:

Expand Down
1 change: 1 addition & 0 deletions docs/@v2/commands/proxy.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,4 @@ redocly drift ./capture.har --api ./openapi.yaml
## Related commands

- [`drift`](./drift.md) replays a recorded HAR (or other traffic log) against an OpenAPI description.
- [`generate-spec`](./generate-spec.md) infers an OpenAPI description from a captured HAR.
2 changes: 2 additions & 0 deletions docs/@v2/v2.sidebars.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
page: commands/eject.md
- label: generate-arazzo
page: commands/generate-arazzo.md
- label: generate-spec
page: commands/generate-spec.md
- label: join
page: commands/join.md
- label: lint
Expand Down
105 changes: 105 additions & 0 deletions packages/cli/src/commands/generate-spec/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# generate-spec [experimental]

Infer an OpenAPI description from recorded HTTP traffic, optionally refined with AI.

Traffic alone can only ever produce a _hypothesis_ about an API's real shape: types are coarse, every observed property looks required, and there are no descriptions, enums, or formats.
`generate-spec` first builds that hypothesis deterministically, then can hand it to an AI provider together with real sample exchanges to narrow it toward the true definition.

## Usage

```bash
# Deterministic inference only
redocly generate-spec ./traffic.har

# Refine the hypothesis with AI
redocly generate-spec ./traffic.har --with-ai --ai-provider claude -o openapi.yaml
```

The `<traffic>` argument is a file or folder of recorded traffic.
Supported formats: HAR, Kong, Nginx/Apache JSON, and NDJSON (auto-detected, or forced with `--traffic-format`).
The traffic parsing infrastructure is shared with the `drift` command.
The command generates OpenAPI 3.1 descriptions only.

## Deterministic inference

The baseline is derived from every exchange in the traffic:

- Identifier-like path segments (numeric, UUID, ULID, CUID, prefixed and opaque tokens) become named path parameters.
- Body schemas are merged across all observations.
A property becomes optional as soon as one sample omits it.
- Alternative body shapes for the same operation are preserved as `oneOf` variants instead of being collapsed, and values observed as `null` produce type unions such as `["string", "null"]`.
- Object shapes that repeat across the document are extracted into `components/schemas` and referenced with `$ref`.
The same entity is recognized when it appears as a list item and as a single resource, with different `required` sets, or with near-identical properties (at least 75% shared, with compatible types).
Components are named from the path entity, the enclosing property name, or `Error` for error responses.
A shape repeated only because its parent shape repeats stays inline.
- Observed string values are analyzed conservatively: when every value of a property (or parameter) matches the same well-known pattern it gets a `format` (`uuid`, `date-time`, `date`, `email`, `uri`, `ipv4`).
When a string only ever takes a small set of identifier-like values with enough repetition (at least 4 observations, at most 5 distinct values, each seen twice on average) it becomes an `enum`.
Enums apply to body properties and query parameters.
Path parameters and nullable unions get formats only.
Evidence is pooled across all operations a shared component was observed in.
- Responses that were never received (status `0` in HAR captures) are ignored.

## AI refinement

The description is refined one operation at a time, so prompt and response stay small no matter how large the recorded traffic or the resulting description is.
Each prompt carries a single operation from the baseline, the component schemas it references, the names of the other components (reserved against collisions), and a capped, shape-diverse sample of the real exchanges recorded for that operation (grouped by status and body shape, selected round-robin so every observed payload variant is represented).
The AI is instructed to narrow types, add formats, enums, descriptions and examples, refine or add `components/schemas`, and model alternative payloads explicitly with `oneOf` (plus `discriminator`) and `allOf` composition.

Up to `--ai-concurrency` operations (default 4) are refined in parallel, and every accepted refinement is merged back as it arrives, so operations prompted later see already-refined shared components.
When two concurrent refinements touch the same shared component, the one merged last wins.
The final whole-document lint still guards the result.
Set `--ai-concurrency 1` to process operations strictly sequentially.
Progress is reported per operation as refinements complete.

The AI's answer is never trusted blindly.
A refined operation is only accepted when it:

- keeps the exact path template and method — the AI cannot invent, drop, or rename operations because only the requested operation is merged back
- keeps every response status code observed in the traffic
- passes validation with the `spec` ruleset (checked against the description's full component set)

An operation whose refinement is rejected keeps its deterministic baseline (reported with the reason), components no operation references anymore are pruned, and the final document is linted again as a whole.
If refinement fails for every operation, the command falls back to the deterministic baseline.
Each provider call times out after 5 minutes.

The acceptance lint is intentionally pinned to the `spec` ruleset and does not use the project's `redocly.yaml`: a stricter governance config would reject refinements for problems the baseline itself has, and a looser one could let structurally broken output through.
Lint the generated description with your own config afterward.

> **Warning:** `--with-ai` sends samples of the recorded traffic (URLs, query strings, request and response bodies) to the selected AI provider.
> Make sure the traffic contains no secrets or personal data you are not allowed to share.

## Options

| Option | Description |
| ------------------ | ----------------------------------------------------------------------------------------------------- |
| `--type` | Target description type. Only `openapi` is supported. |
| `--traffic-format` | Traffic input format (`auto`, `har`, `kong`, `nginx-json`, `apache-json`, `ndjson`). |
| `--server` | Server URL the traffic was captured against. Scopes which requests are considered and sets `servers`. |
| `--title` | Title for the generated description. |
| `--with-ai` | Refine the inferred description with an AI provider. |
| `--ai-provider` | `claude`, `codex`, or `cursor` (default `claude`). |
| `--ai-model` | Model passed to the selected provider. |
| `--ai-concurrency` | Number of operations refined in parallel (default `4`). |
| `-o, --output` | Write the result to a file instead of stdout. |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also mentioned in README the limitation of producing only OAS v3.1.0 description?


## AI providers

Every provider CLI is spawned in an empty temporary directory, so project context the CLIs normally auto-load from the working directory (`CLAUDE.md`, `AGENTS.md`, `.cursor/rules`) never enters the prompts — it would slow every call down and could steer the refinement.

- **`claude`** — runs the local `claude` CLI in headless mode (`claude -p`), with built-in tools, MCP servers, settings (hooks, skills, plugins), and session persistence disabled so every call is a plain completion.
Because settings are not loaded, a model configured there does not apply: pass `--ai-model`, or the CLI's built-in default model is used.
- **`codex`** — runs the local `codex` CLI in non-interactive mode (`codex exec`) with MCP servers and `AGENTS.md` discovery disabled and a read-only sandbox.
Other settings from `~/.codex/config.toml`, such as `model_reasoning_effort`, still apply.
- **`cursor`** — runs the local Cursor CLI in print mode: `cursor-agent -p`.
The renamed `agent` binary is tried as a fallback).

All providers require the respective CLI to be installed and authenticated on the machine running the command.

## Performance for large APIs

Total time is roughly the per-operation AI time multiplied by the number of operations and divided by `--ai-concurrency`.
For descriptions with many operations:

- Raise `--ai-concurrency` (for example to `12`) — calls are network-bound and the provider CLIs retry rate-limit responses themselves.
- Pick a faster model with `--ai-model`.
Smaller models roughly halve the per-operation time compared to the largest ones at some cost in refinement depth.
Loading
Loading