Skip to content

feat(agent): identify Agent SDK traffic in the user agent - #120

Open
LukasParke wants to merge 1 commit into
mainfrom
devin/1787692049-agent-user-agent
Open

feat(agent): identify Agent SDK traffic in the user agent#120
LukasParke wants to merge 1 commit into
mainfrom
devin/1787692049-agent-user-agent

Conversation

@LukasParke

@LukasParke LukasParke commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Summary

@openrouter/agent now identifies itself in the user-agent header, so OpenRouter can attribute traffic to the Agent SDK and to a specific agent package version instead of inferring it. Today an agent request is indistinguishable on the wire from a direct @openrouter/sdk request, apart from the x-openrouter-callmodel marker that only callModel sends.

The OpenRouter client defaults SDKOptions.userAgent, and an explicit caller value still wins:

super({ ...rest, userAgent: rest.userAgent ?? AGENT_USER_AGENT, ...hooks })

AGENT_USER_AGENT appends one conventional product/version token to the generated Speakeasy string, keeping the base byte-identical so anything already parsing it keeps working:

speakeasy-sdk/typescript 0.13.7 2.884.4 1.0.0 @openrouter/sdk @openrouter/agent/0.11.0

Both halves are composed at runtime from SDK_METADATA.userAgent and the generated PACKAGE_VERSION, so a dependency bump or a release moves them without another edit here.

Two design points worth stating, both of which are why the x-openrouter-callmodel header stays exactly as it was:

  1. The header is the only signal that survives browsers. @openrouter/sdk skips the user agent entirely under if (!isBrowserLike), because CORS forbids setting it.
  2. Construction is the only override point. The SDK sets the user agent after merging caller-supplied options.headers, reading client._options.userAgent per operation, so a per-request header cannot win. callModel also accepts any OpenRouterCore, and a caller who passes a plain SDK client is deliberately left alone rather than having their client's options mutated.

The matching analytics side is openrouter-web#37298, which parses this string into sdk_client, sdk_package, sdk_version and the agent version on generations.

How I verified

  • pnpm run lint, pnpm run typecheck, pnpm run build, pnpm run test and pnpm run verify:packages all pass. Full agent suite is 103 files and 1222 tests.
  • A wire-level unit test asserts the header on the outgoing request rather than the option value, by capturing the Request through an injected httpClient. That is the test that fails if the SDK ever changes its override order:
httpClient.request = async (request) => {
  capturedUserAgent = request.headers.get('user-agent');
  throw new Error('captured request');
};
// ...
expect(capturedUserAgent).toBe(AGENT_USER_AGENT);
  • The composition test pins the format that openrouter-web parses, asserting the speakeasy-sdk/typescript prefix and a trailing @openrouter/agent/<semver> token, so a reformat of the string breaks the test here rather than silently dropping attribution in production.
  • pnpm run test:e2e was not run, it requires a live OPENROUTER_API_KEY.

Link to Devin session: https://openrouter.devinenterprise.com/sessions/4694f6390dd142739b1941b796452585
Requested by: @LukasParke


Open in Devin Review

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor
Original prompt from Luke

SYSTEM:
<latest_message>
Luke Parke (U0BF2BKT52N) [ts=1787686596.650429]: @Devin I need metrics on API traffic coming from the Agent SDK
</latest_message>

=== BEGIN THREAD HISTORY (in #agents) ===
Luke Parke (U0BF2BKT52N) [ts=1787686596.650429]: @Devin I need metrics on API traffic coming from the Agent SDK
=== END THREAD HISTORY ===
Channel ID: C07UF9XLTFF
Thread URL: https://openrouter.slack.com/archives/C07UF9XLTFF/p1787686596650429?thread_ts=1787686596.650429&amp;cid=C07UF9XLTFF

The <latest_message> is the message that you should use to guide your goals + task for this session, and you should use the rest of the slack thread as context.
A [ts=...] marker on a Slack message is that message's timestamp. To act on a specific message with the slack tool (e.g. adding an emoji reaction via the reaction command), pass that value as timestamp along with the Channel ID — no extra lookup call is needed.

@devin-ai-integration

Copy link
Copy Markdown
Contributor

🤖 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 that start with 'DevinAI' or '@devin'.
  • 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 devin-ai-integration Bot left a comment

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.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Open in Devin Review

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