Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/tidy-agents-smile.md
Original file line number Diff line number Diff line change
@@ -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.
```
6 changes: 6 additions & 0 deletions packages/agent/src/lib/user-agent.ts
Original file line number Diff line number Diff line change
@@ -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}`;
13 changes: 8 additions & 5 deletions packages/agent/src/openrouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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 = <
Expand Down
43 changes: 43 additions & 0 deletions packages/agent/tests/unit/openrouter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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<Response> => {
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',
Expand Down Expand Up @@ -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', () => {
Expand Down
12 changes: 12 additions & 0 deletions packages/agent/tests/unit/user-agent.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
Loading