Skip to content

feat: tag CLI requests with the command that made them - #132

Merged
PennyroyalTea merged 2 commits into
mainfrom
feat/cli-command-telemetry
Aug 26, 2026
Merged

feat: tag CLI requests with the command that made them#132
PennyroyalTea merged 2 commits into
mainfrom
feat/cli-command-telemetry

Conversation

@PaulAsjes

Copy link
Copy Markdown
Collaborator

Nothing on the backend can currently tell that a request came from the CLI, let alone which product it belongs to. request_log.py identifies clients by X-Fern-SDK-Name; the CLI's entire header set is accept, accept-encoding, authorization, host, user-agent.

path_pattern is already logged, so once CLI traffic is identifiable most products fall out of the path. The one thing paths cannot express is agents-as-code: agents push and agents create hit the same convai endpoints. This adds that missing dimension.

Each workflow handler opens a CommandScope, which appends cmd/<command> to the User-Agent for the duration of the command:

User-Agent: elevenlabs-cli/1.0.0 cmd/agents.push

Pairs with the backend change in xi (feat/log-cli-traffic), which parses this into client_kind, sdk_name, sdk_version and cli_command.

Why the User-Agent and not a header

RequestOptions.additional_headers never reaches the wire. HttpClient::send_request only calls apply_custom_headers on its non-executor branch, and this CLI always uses the executor:

if let Some(executor) = &self.executor {
    executor.execute(req).await          // no apply_custom_headers
} else {
    self.apply_custom_headers(&mut req, options)?;
    ...
}

That is also why the X-Source: agents-cli we already set is silently dropped — and why v0's attribution via is_agents_cli_x_source (xi_shared_workspace/generation_attribution.py:144, constants.py:43) stopped firing on v1. Worth reporting upstream alongside the parse_response status-check fix they shipped in 0.38.0, since it is the same structural gap.

Tags are static, never derived from argv

crate::cli_args::extract_subcommand_path is public and would have been convenient, but it returns positional tokensagents test my-agent would put the agent's name in the User-Agent and from there into request logs. Every tag here is a static string chosen per handler, and the &'static str signature enforces it.

Verified on the wire

agents pull                       ->  elevenlabs-cli/1.0.0 cmd/agents.pull
tools pull                        ->  elevenlabs-cli/1.0.0 cmd/tools.pull
models list (generated)           ->  elevenlabs-cli/1.0.0
ELEVENLABS_VIA=envtoken/2.0 ...   ->  elevenlabs-cli/1.0.0 envtoken/2.0 cmd/agents.pull
--via partner/9.9                 ->  elevenlabs-cli/1.0.0 partner/9.9

Plus a leak check: agents test "my secret agent name" puts nothing user-supplied in any User-Agent.

The last row is a deliberate gap. --via sets an override that beats the env var wholesale, so an explicit --via drops our tag. Losing telemetry is the right side to fail on versus clobbering a consumer's own token — but it means --via users are invisible in the command dimension. Closing that needs framework support for composing the two.

Scope

Covers the 15 hand-written workflow handlers, which is exactly the set path_pattern cannot distinguish. Generated commands (TTS, dubbing, …) are already product-identifiable from the path. Command-level data for those needs Fern to emit it at dispatch, where op_path is known.

Tests

4 new unit tests covering the tag format, append-not-clobber, restore-on-drop, and that the env key tracks the configured flag name (so a userAgentSuffixFlag rename upstream cannot silently detach this). Full suite green: 1903 + 68 + 2 + 755 + doc.

All changes are under cli/elevenlabs/workflow/, which is fernignored.

PaulAsjes and others added 2 commits August 25, 2026 12:22
Nothing on the backend can currently tell which product a CLI request belongs
to, or that it came from the CLI at all. `request_log.py` identifies clients by
`X-Fern-SDK-Name`, which the CLI does not send — its entire header set is
accept, accept-encoding, authorization, host, user-agent.

`path_pattern` is already logged, so once CLI traffic is identifiable most
products fall out of the path. The one thing paths cannot express is
agents-as-code: `agents push` and `agents create` hit the same convai
endpoints. This adds that missing dimension.

Each workflow handler opens a CommandScope, which appends `cmd/<command>` to
the User-Agent for the duration of the command:

  User-Agent: elevenlabs-cli/1.0.0 cmd/agents.push

The User-Agent rather than a header because `RequestOptions.additional_headers`
never reaches the wire: `HttpClient::send_request` only calls
`apply_custom_headers` on its non-executor branch and this CLI always uses the
executor. That is the same reason the `X-Source: agents-cli` we set is silently
dropped, and why v0's attribution via `is_agents_cli_x_source` no longer fires.

Tags are static strings chosen per handler, never derived from argv.
`extract_subcommand_path` would have been convenient but returns positional
tokens, so `agents test my-agent` would put the agent's name in the User-Agent
and from there into request logs.

Verified against a local server:

  agents pull                        -> elevenlabs-cli/1.0.0 cmd/agents.pull
  models list (generated)            -> elevenlabs-cli/1.0.0
  ELEVENLABS_VIA=envtoken/2.0 ...    -> elevenlabs-cli/1.0.0 envtoken/2.0 cmd/agents.pull
  --via partner/9.9                  -> elevenlabs-cli/1.0.0 partner/9.9

The last case is a deliberate gap: the flag sets an override that beats the env
var wholesale, so an explicit --via drops the tag. Losing telemetry is the right
side to fail on versus clobbering a consumer's own token.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Patch release carrying the command tag, so the backend can attribute CLI
traffic to a product. cargo-dist reads the release version from the crate, so
the tag has to match: v1.0.1.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@PennyroyalTea
PennyroyalTea merged commit 147b7f4 into main Aug 26, 2026
12 checks passed
@fern-api fern-api Bot mentioned this pull request Aug 26, 2026
PaulAsjes added a commit that referenced this pull request Aug 26, 2026
* [fern-generated] Update SDK

Generated by Fern
CLI Version: unknown
Generators:
  - fernapi/fern-cli-generator: 0.38.4

* [fern-replay] Applied customizations

Patches applied (1):
  - patch-3324bcc8: build: publish prereleases to homebrew and scoop

Patches with unresolved conflicts (4):
  - patch-809b383f: fix(deps): patch two advisories; keep the verification script
  - patch-590ac6e0: Alpha build
  - patch-composite-7dd8e976: Customer customizations (composite)
  - patch-147b7f4d: feat: tag CLI requests with the command that made them (#132)
    Run `fern-replay resolve` to apply these customizations.

* [fern-replay] Resolved conflicts

Keep publish-prereleases = false. A tap has no prerelease channel, so
publishing one would serve an RC to every stable brew/scoop user; the
restored customization dates from the alpha, before 1.0.0 shipped.

Cargo.toml/Cargo.lock resolved to the generated 1.1.0: 1.0.1 was never
released, so 1.1.0 carries the unreleased telemetry patch plus this
generation's multipart and anyOf fixes.

* chore: ignore locally generated skills and verify-project

`fern replay resolve` stages with `git add -A`, so these local artifacts
were swept into its resolution commit twice.

---------

Co-authored-by: fern-api[bot] <115122769+fern-api[bot]@users.noreply.github.com>
Co-authored-by: Paul Asjes <paul.asjes@elevenlabs.io>
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.

2 participants