From ddab3652a47edf5ebaa842447864a3dc91b812e5 Mon Sep 17 00:00:00 2001 From: Luke Parke Date: Tue, 25 Aug 2026 21:10:55 +0000 Subject: [PATCH] feat(agent): identify SDK traffic in user agent Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .changeset/tidy-agents-smile.md | 13 ++++++ packages/agent/src/lib/user-agent.ts | 6 +++ packages/agent/src/openrouter.ts | 13 +++--- packages/agent/tests/unit/openrouter.test.ts | 43 ++++++++++++++++++++ packages/agent/tests/unit/user-agent.test.ts | 12 ++++++ 5 files changed, 82 insertions(+), 5 deletions(-) create mode 100644 .changeset/tidy-agents-smile.md create mode 100644 packages/agent/src/lib/user-agent.ts create mode 100644 packages/agent/tests/unit/user-agent.test.ts diff --git a/.changeset/tidy-agents-smile.md b/.changeset/tidy-agents-smile.md new file mode 100644 index 00000000..7c441799 --- /dev/null +++ b/.changeset/tidy-agents-smile.md @@ -0,0 +1,13 @@ +--- +'@openrouter/agent': patch +--- + +Identify Agent SDK requests with an Agent SDK user-agent suffix that includes the package version. + +```ts +import { OpenRouter } from '@openrouter/agent'; + +const client = new OpenRouter({ apiKey: process.env.OPENROUTER_API_KEY }); +// Requests use the Agent SDK user agent by default. +// Pass userAgent to override the default identification. +``` diff --git a/packages/agent/src/lib/user-agent.ts b/packages/agent/src/lib/user-agent.ts new file mode 100644 index 00000000..d9424f61 --- /dev/null +++ b/packages/agent/src/lib/user-agent.ts @@ -0,0 +1,6 @@ +import { SDK_METADATA } from '@openrouter/sdk/lib/config'; + +import { PACKAGE_VERSION } from '../mcp/version.js'; + +/** Keeps the base Speakeasy string byte-identical and appends one token for openrouter-web analytics. */ +export const AGENT_USER_AGENT = `${SDK_METADATA.userAgent} @openrouter/agent/${PACKAGE_VERSION}`; diff --git a/packages/agent/src/openrouter.ts b/packages/agent/src/openrouter.ts index 5aee752a..20cf5d37 100644 --- a/packages/agent/src/openrouter.ts +++ b/packages/agent/src/openrouter.ts @@ -15,6 +15,7 @@ import { callModel } from './inner-loop/call-model.js'; import type { CallModelInput } from './lib/async-params.js'; import type { ModelResult } from './lib/model-result.js'; import type { Tool } from './lib/tool-types.js'; +import { AGENT_USER_AGENT } from './lib/user-agent.js'; export type { SDKOptions } from '@openrouter/sdk/lib/config'; @@ -74,13 +75,15 @@ export class OpenRouter extends OpenRouterCore { constructor(options?: OpenRouterOptions) { const { hooks: hooksInput, ...rest } = options ?? {}; const hooks = buildSDKHooks(hooksInput); - super( - hooks === undefined - ? rest - : Object.assign({}, rest, { + super({ + ...rest, + userAgent: rest.userAgent ?? AGENT_USER_AGENT, + ...(hooks === undefined + ? {} + : { hooks, }), - ); + }); } callModel = < diff --git a/packages/agent/tests/unit/openrouter.test.ts b/packages/agent/tests/unit/openrouter.test.ts index 53ab985b..cc78d3f9 100644 --- a/packages/agent/tests/unit/openrouter.test.ts +++ b/packages/agent/tests/unit/openrouter.test.ts @@ -5,8 +5,10 @@ import type { BeforeRequestContext, BeforeRequestHook, } from '@openrouter/sdk/hooks/types'; +import { HTTPClient } from '@openrouter/sdk/lib/http'; import { describe, expect, it } from 'vitest'; import { ModelResult } from '../../src/lib/model-result.js'; +import { AGENT_USER_AGENT } from '../../src/lib/user-agent.js'; import { OpenRouter } from '../../src/openrouter.js'; describe('OpenRouter', () => { @@ -53,6 +55,45 @@ describe('OpenRouter', () => { expect(openrouter).toBeInstanceOf(OpenRouter); }); + it('should default to the Agent SDK user agent', () => { + const openrouter = new OpenRouter({ + apiKey: 'test-key', + }); + expect(openrouter._options.userAgent).toBe(AGENT_USER_AGENT); + }); + + it('should preserve an explicitly configured user agent', () => { + const openrouter = new OpenRouter({ + apiKey: 'test-key', + userAgent: 'custom/1.0', + }); + expect(openrouter._options.userAgent).toBe('custom/1.0'); + }); + + it('should send the Agent SDK user agent on outgoing requests', async () => { + let capturedUserAgent: string | null = null; + const httpClient = new HTTPClient(); + httpClient.request = async (request: Request): Promise => { + capturedUserAgent = request.headers.get('user-agent'); + throw new Error('captured request'); + }; + + const openrouter = new OpenRouter({ + apiKey: 'test-key', + httpClient, + }); + + await expect( + openrouter + .callModel({ + model: 'openai/gpt-4o', + input: 'Hello', + }) + .getText(), + ).rejects.toThrow('captured request'); + expect(capturedUserAgent).toBe(AGENT_USER_AGENT); + }); + it('should be an instance of OpenRouterCore', () => { const openrouter = new OpenRouter({ apiKey: 'test-key', @@ -113,11 +154,13 @@ describe('OpenRouter', () => { it('should wrap a single hook object into an SDKHooks instance', () => { const openrouter = new OpenRouter({ apiKey: 'test-key', + userAgent: 'custom/1.0', hooks: noopBeforeRequest, }); expect(openrouter._options.hooks).toBeInstanceOf(SDKHooks); expect(openrouter._options.hooks?.beforeRequestHooks).toHaveLength(1); expect(openrouter._options.hooks?.beforeRequestHooks[0]).toBe(noopBeforeRequest); + expect(openrouter._options.userAgent).toBe('custom/1.0'); }); it('should wrap an array of hook objects into an SDKHooks instance', () => { diff --git a/packages/agent/tests/unit/user-agent.test.ts b/packages/agent/tests/unit/user-agent.test.ts new file mode 100644 index 00000000..1ff48895 --- /dev/null +++ b/packages/agent/tests/unit/user-agent.test.ts @@ -0,0 +1,12 @@ +import { SDK_METADATA } from '@openrouter/sdk/lib/config'; +import { describe, expect, it } from 'vitest'; +import { AGENT_USER_AGENT } from '../../src/lib/user-agent.js'; +import { PACKAGE_VERSION } from '../../src/mcp/version.js'; + +describe('AGENT_USER_AGENT', () => { + it('composes the SDK and Agent package versions', () => { + expect(AGENT_USER_AGENT).toBe(`${SDK_METADATA.userAgent} @openrouter/agent/${PACKAGE_VERSION}`); + expect(AGENT_USER_AGENT.startsWith('speakeasy-sdk/typescript ')).toBe(true); + expect(AGENT_USER_AGENT).toMatch(/ @openrouter\/agent\/\d+\.\d+\.\d+$/u); + }); +});