Skip to content

feat(cli-generator): send X-Fern platform-identification headers - #17516

Open
devin-ai-integration[bot] wants to merge 2 commits into
mainfrom
devin/1787659021-cli-fern-platform-headers
Open

feat(cli-generator): send X-Fern platform-identification headers#17516
devin-ai-integration[bot] wants to merge 2 commits into
mainfrom
devin/1787659021-cli-fern-platform-headers

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Description

Generated CLIs previously sent only a User-Agent. They now send the same Fern platform-identification headers the SDK generators use, plus the invoked command, on every HTTP request and WebSocket handshake:

X-Fern-SDK-Name: elevenlabs-cli
X-Fern-SDK-Version: 1.0.0
X-Fern-Language: Rust
X-Fern-CLI-Command: text-to-speech.convert

Requested in Slack ("I noticed that the CLI doesn't send the X-Fern-SDK-Name header, can we get that included?").

Changes Made

  • sdk/src/http.rs: new HttpConfig::platform_headers() (X-Fern-Language: Rust, X-Fern-SDK-Name: <binary>-cli via the existing user_agent_product normalization, X-Fern-SDK-Version: CARGO_PKG_VERSION) and with_cli_command(Option<String>) which adds X-Fern-CLI-Command when the dispatch path knows the invoked command (blank/header-invalid values are dropped). build_client() installs them as reqwest default headers alongside the User-Agent.
  • REST (openapi/binding.rs) and GraphQL (graphql/binding.rs) dispatch set the command from the invoked op path (_op_path.join(".")); AsyncAPI (asyncapi/binding.rs) sets it from channel.sdk_group_name ++ leaf_command_name, mirroring resolve_channel. Programmatic consumers without a known command send no X-Fern-CLI-Command.
  • WebSocket handshakes don't go through reqwest, so build_handshake_request now takes the platform headers and inserts them before auth headers (same override semantics as User-Agent).
  • Changelog entry at generators/cli/changes/unreleased/add-fern-platform-headers.yml.
  • Regenerated seed/cli (second commit). Note: most of that diff is catch-up — the committed seed/cli snapshots were stale relative to main's generator (the Update Seed workflow has been failing on main), so the regeneration also pulls in unrelated drift (e.g. jsonl/raw/http format docs, Cargo.lock, types-crate output).
  • Updated README.md generator (if applicable)

Testing

  • Unit tests added/updated: platform_headers_identify_the_cli, platform_headers_include_the_invoked_command_when_set, with_cli_command_ignores_blank_or_invalid_values, a wire-level build_client_sends_platform_headers_on_the_wire (wiremock, asserts all four headers received), and handshake_request_sets_platform_headers for the WS path.
  • Manual testing completed: cargo build --locked --all-features --tests and cargo test --locked --all-features in generators/cli/sdk (all 1908 tests pass); pnpm seed test --generator cli --skip-scripts — 151/151 fixtures pass.

Link to Devin session: https://app.devin.ai/sessions/5b7fd815c5074c19a4a85820accf7f56


Open in Devin Review

rishabh-fern and others added 2 commits August 25, 2026 11:57
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

Tested the platform headers end-to-end with real generated CLI binaries (no mocked runtime): built regenerated seed fixtures with cargo build --locked and ran them against local header-logging servers.

HTTPallof-composition entities get-entity --base-url http://127.0.0.1:4010 (fixture seed/cli/allof): the mock server received x-fern-sdk-name: allof-composition-cli, x-fern-sdk-version: 0.0.0, x-fern-language: Rust, x-fern-cli-command: entities.get-entity. A second command (users list-users) correctly switched the command header to users.list-users.

HTTP requests: all four X-Fern headers, per-invocation CLI command

WebSocket handshake (null-type fixture + AsyncApiBinding test harness)

Temporarily wired the sdk's agent.asyncapi.yaml into the null-type fixture's main.rs (reverted after testing) and ran null-type agent-messages --message '{"type":"pong"}' --base-url ws://127.0.0.1:4011 against a local WS echo server — the handshake carried all four headers, inserted next to the Upgrade headers:

WebSocket handshake: all four X-Fern headers present

Full demo recording in the Devin session: https://app.devin.ai/sessions/5b7fd815c5074c19a4a85820accf7f56

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

Also tested against a customer's real ElevenLabs specs (their openapi.json — 268 paths — plus openapi-overrides.yml and generators.yml): generated with this branch's generator via docker-mode seed run, built with cargo build --locked, and ran the generated elevenlabs binary against a local header-logging mock server.

elevenlabs text-to-speech convert --voice-id testvoice --json '{"text":"hello"}' --base-url http://127.0.0.1:4010POST /v1/text-to-speech/testvoice received all four headers, and a 3-level command (conversational-ai agents list) correctly produced x-fern-cli-command: conversational-ai.agents.list:

Real ElevenLabs-generated CLI: all four X-Fern headers, per-invocation dot-joined command path

  • x-fern-sdk-name: elevenlabs-cli (from binaryName: elevenlabs)
  • x-fern-sdk-version: 0.0.0 (generated crate's CARGO_PKG_VERSION)
  • x-fern-language: Rust
  • x-fern-cli-command: text-to-speech.convert / conversational-ai.agents.list (per invocation)
Note on WebSocket coverage for this binary

The customer's generators.yml references an AsyncAPI spec, but copySpecs.ts/renderMainRs currently only emits OpenApiBinding, so the generated CLI exposes no WS channel commands — WS handshake headers couldn't be exercised through this binary. They were verified separately with a hand-wired AsyncApiBinding harness (see earlier comment/evidence).

Full demo recording in the Devin session: https://app.devin.ai/sessions/5b7fd815c5074c19a4a85820accf7f56

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant