{"templateId":"../@theme/templates/BlogPost","sharedDataIds":{"blog-latest-posts":"blog-latest-posts"},"props":{"metadata":{"markdoc":{"tagList":["admonition"]},"redocly_category":"Blog","type":"markdown","template":"../@theme/templates/BlogPost","title":"Generate OpenAPI from real traffic (with AI)","description":"The new generate-spec command infers an OpenAPI description from recorded HTTP traffic - with AI refinement.","seo":{"title":"Generate OpenAPI from real traffic (with AI)","description":"The new generate-spec command infers an OpenAPI description from recorded HTTP traffic - with AI refinement."},"author":{"id":"adam-sobaniec","name":"Adam Sobaniec","authorBIO":"Software Engineer, Redocly","image":"/assets/sobaniec.d72cecde632c015ce4db05cc559e1f27ccae55d6e5977f816362afb23a1a56a7.978384e4.png"},"publishedDate":"2026-09-14","categories":[{"category":{"id":"redocly","label":"Redocly"},"subcategory":{"id":"redocly-cli","label":"Redocly CLI"}},{"category":{"id":"redocly","label":"Redocly"},"subcategory":{"id":"product-updates","label":"Product updates"}},{"category":{"id":"api-specifications","label":"API specifications"},"subcategory":{"id":"openapi","label":"OpenAPI"}}],"slug":"/blog/generate-openapi-from-traffic"},"seo":{"title":"Generate OpenAPI from real traffic (with AI)","description":"The new generate-spec command infers an OpenAPI description from recorded HTTP traffic - with AI refinement.","siteUrl":"https://redocly.com","image":"/assets/redocly-card.f670aae34a39545a5ea633a540cb3a4a333a1f23bb2ed3c4a1b17a5fbcf0ac85.db81178d.png","lang":"en-US"},"dynamicMarkdocComponents":[],"compilationErrors":[],"ast":{"$$mdtype":"Tag","name":"article","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Plenty of production APIs have no OpenAPI description at all."," ","That has always been a problem for documentation and SDKs."," ","In the AI era it is also an integration problem, because an OpenAPI description is the contract AI agents read to learn how to call an API."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["There are two usual fixes: write the description by hand, or ask an AI assistant to derive it from the source code."," ","Writing by hand is slow."," ","The source-code approach fails on large codebases: the model loses context, hallucinates API behavior, and generates convincing but subtly inaccurate descriptions that are impossible to verify."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Instead of guessing from source code, the new ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["redocly generate-spec"]}," command uses actual recorded HTTP traffic to build an accurate, initial API specification."," ","First, it automatically creates a base description from the traffic."," ","Then, it uses AI selectively - processing one endpoint at a time, grounding every change in real data, and verifying all output."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"from-traffic-to-description","__idx":0},"children":["From traffic to description"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The command accepts HAR files, Kong logs, Nginx/Apache JSON logs, and NDJSON - a single file or a whole folder of them."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["From the recorded exchanges it builds a baseline deterministically:"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Identifier-like path segments (numeric IDs, UUIDs, prefixed tokens like ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["prd_…"]},") become named path parameters, so a hundred URLs become one templated path."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Request and response schemas are merged across all observations; a property becomes optional as soon as one sample omits it."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Alternative body shapes for the same operation are preserved as ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["oneOf"]}," variants, and object shapes that repeat across the document are extracted into ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["components/schemas"]},"."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["String values are analyzed conservatively: strings that consistently match well-known patterns get a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["format"]}," (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["uuid"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["date-time"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["email"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["uri"]},"), and strings that only ever take a small set of repeated values become an ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["enum"]},"."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"example-the-cafe-api","__idx":1},"children":["Example: the Cafe API"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Here is what that looks like end to end, on Redocly Cafe - our public demo API, so you can follow along on the same traffic if you want."," ","Pretend for a moment that its OpenAPI description doesn't exist, and let's reconstruct it from traffic."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["First, record some."," ","The ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["proxy"]}," command starts a local reverse proxy that captures everything passing through into a HAR file:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"bash","header":{"controls":{"copy":{}}},"source":"redocly proxy --target https://api.cafe.redocly.com --har ./cafe.har\n","lang":"bash"},"children":[]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"sh","header":{"controls":{"copy":{}}},"source":"Proxy listening on http://127.0.0.1:4040 → forwarding to https://api.cafe.redocly.com/\nRecording traffic to ./cafe.har\nPress Ctrl+C to stop.\n","lang":"sh"},"children":[]},{"$$mdtype":"Tag","name":"Admonition","attributes":{"type":"warning","name":"Record in a sandboxed environment"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The proxy records complete exchanges - URLs, headers, cookies, and request and response bodies."," ","A capture taken against production contains whatever that traffic contained, including credentials and personal data."," ","Record against a test environment with synthetic data."," ","This matters in every mode, not only with AI: observed values end up in the generated description as enums and examples, so a description inferred from real user data is not safe to share either."]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Send a few requests through it, the way a real client would: browse the menu, filter it, then take menu item IDs from the response and download some photos:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"bash","header":{"controls":{"copy":{}}},"source":"curl http://127.0.0.1:4040/menu\ncurl \"http://127.0.0.1:4040/menu?category=dessert\"\ncurl \"http://127.0.0.1:4040/menu?category=beverage\"\nfor id in $(curl -s http://127.0.0.1:4040/menu | jq -r '.items[:3][].id'); do\n  curl -o \"$id.png\" \"http://127.0.0.1:4040/menu-item-images/$id\"\ndone\n","lang":"bash"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Press ",{"$$mdtype":"Tag","name":"kbd","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Ctrl"]}]}," + ",{"$$mdtype":"Tag","name":"kbd","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["C"]}]}," to stop the proxy - it reports how many exchanges it captured and writes the HAR file."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Now ask for a description:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"bash","header":{"controls":{"copy":{}}},"source":"redocly generate-spec ./cafe.har --title \"Cafe API\" -o cafe-openapi.yaml\n","lang":"bash"},"children":[]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"sh","header":{"controls":{"copy":{}}},"source":"Inferred a baseline OpenAPI description from traffic: 2 operation(s).\nWritten to: cafe-openapi.yaml\nDone in 0s.\n","lang":"sh"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["It outputs a valid OpenAPI 3.2 spec (~100 lines long) from just those few captured requests."," ","The file sets the server URL and outlines a path for each endpoint found:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"yaml","header":{"controls":{"copy":{}}},"source":"openapi: 3.2.0\ninfo:\n  title: Cafe API\n  version: 1.0.0\nservers:\n  - url: https://api.cafe.redocly.com\npaths:\n  /menu:\n    # …\n  /menu-item-images/{menu-item-imageId}:\n    # …\n","lang":"yaml"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Let's look at what the inference did."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Start with the paths: the photo URLs became one templated path, because the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["prd_…"]}," identifiers were recognized as IDs and turned into a required path parameter:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"yaml","header":{"controls":{"copy":{}}},"source":"/menu-item-images/{menu-item-imageId}:\n  get:\n    operationId: get-menu-item-images-menu-item-imageId\n    responses:\n      '200':\n        description: OK\n    parameters:\n      - name: menu-item-imageId\n        in: path\n        required: true\n        schema:\n          type: string\n","lang":"yaml"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Inside the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["/menu"]}," response schema, every menu item property got a type, and the observed values were analyzed for more detail:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"yaml","header":{"controls":{"copy":{}}},"source":"properties:\n  # …\n  price:\n    type: integer\n  category:\n    type: string\n    enum:\n      - beverage\n      - dessert\n  createdAt:\n    type: string\n    format: date-time\n  photoUrl:\n    type: string\n    format: uri\n","lang":"yaml"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["category"]}," became an enum because every observed value was one of the two, and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["createdAt"]}," and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["photoUrl"]}," matched well-known patterns in every sample."," ","The ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["category"]}," query parameter on the same operation stayed a plain string - two observations are not enough evidence, so the inference stays conservative:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"yaml","header":{"controls":{"copy":{}}},"source":"parameters:\n  - name: category\n    in: query\n    required: false\n    schema:\n      type: string\n","lang":"yaml"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The merge across samples also detected which properties are not always present."," ","Beverages have volume, desserts have calories, so ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["volume"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["containsCaffeine"]},", and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["calories"]}," are typed but absent from the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["required"]}," list:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"yaml","header":{"controls":{"copy":{}}},"source":"properties:\n  # …\n  volume:\n    type: integer\n  containsCaffeine:\n    type: boolean\n  calories:\n    type: integer\nrequired:\n  - id\n  - name\n  - price\n  - photoTextDescription\n  - category\n  - createdAt\n  - updatedAt\n  - object\n  - photoUrl\n","lang":"yaml"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The result is still only a hypothesis - the description knows only what the traffic showed."," ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["price"]}," is an integer because every observed price happened to be a whole number."," ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["name"]}," became enum of the handful of menu items in the capture."," ","Endpoints that nobody called are missing, there are no human-readable descriptions, and names like ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["{menu-item-imageId}"]}," are generated mechanically - rename them when you review."," ","More traffic makes the hypothesis stronger. You can record traffic in your e2e tests using Redocly CLI ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["proxy"]}," command and then feed it to ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["generate-spec"]}," ","command."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"refine-it-with-ai","__idx":2},"children":["Refine it with AI"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The baseline is structurally correct, but it can't explain anything."," ","That can be improved with AI. Let's explore with ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["--with-ai"]}," parameter:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"bash","header":{"controls":{"copy":{}}},"source":"redocly generate-spec ./cafe.har --title \"Cafe API\" --with-ai --ai-provider claude -o cafe-openapi.yaml\n","lang":"bash"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["As a result, the AI fills in everything the deterministic engine couldn't:"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Documentation"]}," - summaries and descriptions for every operation, parameter, and property."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Semantic types and constraints"]}," - business logic like ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["minimum: 0"]}," on prices, ID pattern matches, and data formats inferred from field meaning rather than repeated values."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Real API design"]}," - variant payloads modeled as ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["oneOf"]}," unions with discriminators, and shared structures extracted into ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["allOf"]}," components."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Over-fitting cleanup"]}," - overly restrictive enums converted into plain typed fields with realistic examples, preserving only true enums."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"built-to-keep-the-ai-on-track","__idx":3},"children":["Built to keep the AI on track"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["\"Ask AI for an OpenAPI description\" usually fails for one reason: context."," ","Give a model a whole codebase - or a whole traffic dump - and it loses track, then fills the gaps with plausible guesses."," ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["generate-spec"]}," structures the work so this cannot happen:"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["One operation per prompt."]}," ","Each prompt contains a single operation from the baseline, the component schemas it references, and a small sample of its recorded exchanges - a few real requests, picked so that every observed payload variant is included."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Determinism and AI work together, not against each other."]}," ","The AI does not rebuild anything from scratch - it refines the baseline."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Nothing is trusted blindly."]}," ","Each AI response is validated against baseline. If operations differ too much, the response is treated as rejected."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"runs-on-the-ai-you-already-have","__idx":4},"children":["Runs on the AI you already have"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Three providers are supported - ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["claude"]}," (Claude Code), ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["codex"]}," (Codex CLI), and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["cursor"]}," (Cursor CLI)."," ","Each one runs the locally installed CLI in non-interactive mode, so the subscription you already use and pay for does the work."," ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["--ai-provider"]}," is optional and defaults to ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["claude"]},"; pick a model with ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["--ai-model"]}," or let the provider use its default."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Operations are refined in parallel."," ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["--ai-concurrency"]}," (default 4) is the main way to make it faster."]},{"$$mdtype":"Tag","name":"Admonition","attributes":{"type":"warning","name":"Traffic leaves your machine"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["--with-ai"]}," sends captured traffic samples - URLs, query strings, and payloads to your AI provider."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Three built-in safeguards minimize data exposure:"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["headers are omitted to keep auth tokens and cookies strictly local;"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["environments are isolated by running the CLI in an empty directory, preventing local files or custom rules from leaking into the prompt;"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["secrets are scrubbed via model instructions that block credential-like values from ending up in examples."]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["These guardrails reduce risk, but they aren't foolproof."," ","Run captures in a sandbox and verify traffic is clean before sending data to an external provider."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"how-much-does---with-ai-actually-add","__idx":5},"children":["How much does ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["--with-ai"]}," actually add?"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["With Cafe API we can answer it precisely: its real, handwritten ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"https://cafe.redocly.com/openapi/cafe"},"children":["OpenAPI description"]}," exists - we only pretended it doesn't."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["We recorded a fuller session than the small capture above - one that covers every endpoint: the OAuth2 client registration flow, menu items created in both categories, orders placed, updated, and deleted, photo downloads, and the errors a real session produces along the way (a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["400"]},", a few ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["404"]},"s, even a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["409"]},")."," ","Then we generated a description twice from that one capture - once deterministically, once with ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["--with-ai"]}," - and scored both against the handwritten description."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Let's look at the results:"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["For response schemas:"]},{"$$mdtype":"Tag","name":"div","attributes":{"className":"md-table-wrapper"},"children":[{"$$mdtype":"Tag","name":"table","attributes":{"className":"md"},"children":[{"$$mdtype":"Tag","name":"thead","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Metric"},"children":["Metric"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Deterministic"},"children":["Deterministic"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"--with-ai"},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["--with-ai"]}]}]}]},{"$$mdtype":"Tag","name":"tbody","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Response properties recovered"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["97.5%"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["98.3%"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Correct types"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["100%"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["100%"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Correct ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["required"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["69.2%"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["72.2%"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Formats documented, recovered"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["53.1%"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["62.5%"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Enums documented, recovered"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["66.7%"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["66.7%"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["required"]}," documented, recovered"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["91.3%"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["94.2%"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Properties carrying a description"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["0%"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["97.5%"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Numeric and length constraints"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["0"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["21"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Run time"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["under 1s"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["1-15 min¹"]}]}]}]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["¹ Depends heavily on the model and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["--ai-concurrency"]}," - the largest models at the default concurrency are the slowest, while a rerun of the same capture with ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["--ai-concurrency 6"]}," finished in under a minute."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["What ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["--with-ai"]}," adds is what determinism cannot produce at all: descriptions on nearly every property, constraints, examples, and formats inferred from context rather than repetition."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["For request bodies we could see more improvements with AI:"]},{"$$mdtype":"Tag","name":"div","attributes":{"className":"md-table-wrapper"},"children":[{"$$mdtype":"Tag","name":"table","attributes":{"className":"md"},"children":[{"$$mdtype":"Tag","name":"thead","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Metric"},"children":["Metric"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Deterministic"},"children":["Deterministic"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"--with-ai"},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["--with-ai"]}]}]}]},{"$$mdtype":"Tag","name":"tbody","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Request properties recovered"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["55.9%"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["61.8%"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Correct types"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["78.9%"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["100%"]}]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Correct ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["required"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["81.8%"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["100%"]}]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Properties carrying a description"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["0%"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["85.7%"]}]}]}]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"what-actually-changed","__idx":6},"children":["What actually changed"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["POST /menu"]}," accepts ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["multipart/form-data"]},", and every value in a multipart form is sent as a string."," ","The deterministic baseline can only write down what it saw:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"yaml","header":{"controls":{"copy":{}}},"source":"requestBody:\n  content:\n    multipart/form-data:\n      schema:\n        type: object\n        properties:\n          name:\n            type: string\n          price:\n            type: string\n          category:\n            type: string\n          volume:\n            type: string\n          containsCaffeine:\n            type: string\n          calories:\n            type: string\n        required:\n          - name\n          - price\n          - category\n","lang":"yaml"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["This is the one place in the whole experiment where the baseline was ",{"$$mdtype":"Tag","name":"em","attributes":{},"children":["wrong"]}," rather than just incomplete - ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["price"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["volume"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["calories"]},", and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["containsCaffeine"]}," are not strings - and it is exactly what the AI fixed."," ","The same request body after ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["--with-ai"]},":"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"yaml","header":{"controls":{"copy":{}}},"source":"requestBody:\n  content:\n    multipart/form-data:\n      schema:\n        oneOf:\n          - $ref: '#/components/schemas/BeverageCreate'\n          - $ref: '#/components/schemas/DessertCreate'\n        discriminator:\n          propertyName: category\n          mapping:\n            beverage: '#/components/schemas/BeverageCreate'\n            dessert: '#/components/schemas/DessertCreate'\n","lang":"yaml"},"children":[]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"yaml","header":{"controls":{"copy":{}}},"source":"BeverageCreate:\n  description: Creation request for a beverage menu item.\n  allOf:\n    - $ref: '#/components/schemas/MenuItemCreateBase'\n    - type: object\n      properties:\n        category:\n          type: string\n          enum:\n            - beverage\n        volume:\n          type: integer\n          minimum: 0\n          description: Serving volume in millilitres.\n          example: 180\n        containsCaffeine:\n          type: boolean\n          description: Whether the beverage contains caffeine.\n          example: true\nMenuItemCreateBase:\n  type: object\n  description: Attributes shared by every menu item creation request.\n  properties:\n    name:\n      type: string\n      description: Human-readable name of the menu item.\n      example: flat-white\n    price:\n      type: integer\n      minimum: 0\n      description: Price in the smallest currency unit (for example cents).\n      example: 450\n    # …\n  required:\n    - name\n    - price\n    - category\n","lang":"yaml"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Every type is corrected, and constraints, descriptions, and examples appeared - but the bigger change is the shape itself."," ","The AI noticed from the samples that beverages and desserts carry different fields, and modeled the union explicitly: ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["allOf"]}," composition over the shared attributes, selected by a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["category"]}," discriminator."," ","The handwritten description models menu items exactly the same way - ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["oneOf"]}," beverage or dessert, discriminated by ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["category"]},"."," ","Traffic plus AI arrived at the same design the API team chose by hand; the baseline could only offer one merged object with everything optional."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["One caveat applies to every API: path parameters."," ","Every Cafe path parameter was recognized, because its identifiers are prefixed tokens (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["prd_…"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ord_…"]},") that the deterministic inference detects."," ","On APIs whose path segments are ordinary words - organization names, repository names, branches - those segments stay hardcoded, and AI refinement cannot fix them, because a refined operation must keep its path."," ","Reviewing paths by hand is the one step you cannot skip."]},{"$$mdtype":"Tag","name":"Admonition","attributes":{"type":"warning","name":"Experimental"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["generate-spec"]}," command is experimental."," ","Flags, output, and behavior may change - including breaking changes - in upcoming releases while we shape it with your feedback."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"get-started","__idx":7},"children":["Get started"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["generate-spec"]}," command is available now in the latest ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"https://redocly.com/docs/cli"},"children":["Redocly CLI"]}," - see the ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"https://redocly.com/docs/cli/commands/generate-spec"},"children":["command reference"]}," for all options."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Once you have your spec generated don't let it go stale. Use ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/blog/catch-api-drift"},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["drift"]}," command"]}," to ensure it stays up-to-date."]}]},"headings":[{"value":"From traffic to description","id":"from-traffic-to-description","depth":2},{"value":"Example: the Cafe API","id":"example-the-cafe-api","depth":2},{"value":"Refine it with AI","id":"refine-it-with-ai","depth":2},{"value":"Built to keep the AI on track","id":"built-to-keep-the-ai-on-track","depth":3},{"value":"Runs on the AI you already have","id":"runs-on-the-ai-you-already-have","depth":3},{"value":"How much does --with-ai actually add?","id":"how-much-does---with-ai-actually-add","depth":2},{"value":"What actually changed","id":"what-actually-changed","depth":2},{"value":"Get started","id":"get-started","depth":2}],"frontmatter":{"template":"../@theme/templates/BlogPost","title":"Generate OpenAPI from real traffic (with AI)","description":"The new generate-spec command infers an OpenAPI description from recorded HTTP traffic - with AI refinement.","seo":{"title":"Generate OpenAPI from real traffic (with AI)","description":"The new generate-spec command infers an OpenAPI description from recorded HTTP traffic - with AI refinement."},"author":"adam-sobaniec","publishedDate":"2026-09-14","categories":["redocly:redocly-cli","redocly:product-updates","api-specifications:openapi"]},"lastModified":"2026-09-14T11:24:36.000Z","pagePropGetterError":{"message":"","name":""}},"slug":"/blog/generate-openapi-from-traffic","userData":{"isAuthenticated":false,"teams":["anonymous"]},"isPublic":true}