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
9 changes: 9 additions & 0 deletions .changeset/gateway-eve-version-ua.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"eve": patch
---

Send a versioned `User-Agent` on AI Gateway model calls. eve now sets a leading
`eve/<version> (<agent name>)` product token, and the AI SDK appends its own
`ai/<version> ai-sdk/provider-utils/<version> runtime/node.js/<version>` tokens
after it, so gateway traffic is attributable to both the eve and SDK versions
alongside the existing `x-title` and `http-referer` attribution headers.
37 changes: 37 additions & 0 deletions packages/eve/src/harness/tool-loop.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8019,6 +8019,7 @@ describe("createToolLoopHarness", () => {

const agentCall = vi.mocked(ToolLoopAgent).mock.calls[0]?.[0];
expect(agentCall?.headers).toEqual({
"user-agent": "eve/0.0.0 (Weather Agent)",
"x-title": "Weather Agent",
"http-referer": "https://my-agent.vercel.app",
});
Expand All @@ -8031,6 +8032,39 @@ describe("createToolLoopHarness", () => {
}
});

it("sets a versioned user-agent suffixed with the agent name", async () => {
setupStopResultForAttribution();
const originalProductionUrl = process.env.VERCEL_PROJECT_PRODUCTION_URL;
delete process.env.VERCEL_PROJECT_PRODUCTION_URL;
const originalUrl = process.env.VERCEL_URL;
delete process.env.VERCEL_URL;
try {
const config: ToolLoopHarnessConfig = {
mode: "conversation",
resolveModel: vi.fn().mockResolvedValue("anthropic/claude-sonnet-4-5"),
runtimeIdentity: {
agentId: "weather-agent",
agentName: "Weather Agent",
eveVersion: "9.9.9",
modelId: "anthropic/claude-sonnet-4-5",
},
tools: new Map(),
};
const runStep = createToolLoopHarness(config);
await runStep(createTestSession(), { message: "hi" });

const agentCall = vi.mocked(ToolLoopAgent).mock.calls[0]?.[0];
expect(agentCall?.headers?.["user-agent"]).toBe("eve/9.9.9 (Weather Agent)");
} finally {
if (originalProductionUrl !== undefined) {
process.env.VERCEL_PROJECT_PRODUCTION_URL = originalProductionUrl;
}
if (originalUrl !== undefined) {
process.env.VERCEL_URL = originalUrl;
}
}
});

it("falls back to agentId when agentName is not set", async () => {
setupStopResultForAttribution();
const originalProductionUrl = process.env.VERCEL_PROJECT_PRODUCTION_URL;
Expand All @@ -8053,6 +8087,7 @@ describe("createToolLoopHarness", () => {

const agentCall = vi.mocked(ToolLoopAgent).mock.calls[0]?.[0];
expect(agentCall?.headers).toEqual({
"user-agent": "eve/0.0.0 (weather-agent)",
"x-title": "weather-agent",
});
} finally {
Expand Down Expand Up @@ -8088,6 +8123,7 @@ describe("createToolLoopHarness", () => {

const agentCall = vi.mocked(ToolLoopAgent).mock.calls[0]?.[0];
expect(agentCall?.headers).toEqual({
"user-agent": "eve/0.0.0 (My Agent)",
"x-title": "My Agent",
"http-referer": "https://preview-123.vercel.app",
});
Expand Down Expand Up @@ -8183,6 +8219,7 @@ describe("createToolLoopHarness", () => {

const compactCall = vi.mocked(compactMessages).mock.calls[0];
expect(compactCall?.[5]).toEqual({
"user-agent": "eve/0.0.0 (Weather Agent)",
"x-title": "Weather Agent",
"http-referer": "https://my-agent.vercel.app",
});
Expand Down
24 changes: 17 additions & 7 deletions packages/eve/src/harness/tool-loop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
} from "ai";
import { isScheduleAppAuth } from "#channel/schedule-auth.js";
import { resolveInstalledPackageInfo } from "#internal/application/package.js";
import { EVE_PACKAGE_NAME } from "#internal/package-name.js";
import {
createErrorId,
createLogger,
Expand Down Expand Up @@ -263,8 +264,15 @@ function enrichTelemetry(
* Builds AI Gateway app attribution headers when the model is gateway-routed.
*
* Gateway routing is detected by `typeof model === "string"` — the same
* condition used for the `gateway-auto` cache path. Returns `undefined`
* for non-gateway models or when no meaningful attribution is available.
* condition used for the `gateway-auto` cache path. Sets a leading
* `user-agent` product token (`eve/<version> (<agent>)`); the AI SDK then
* appends its own `ai/<v> ai-sdk/provider-utils/<v> runtime/node.js/<v>`
* tokens after it (via `withUserAgentSuffix`), so the gateway sees both the
* eve and SDK versions without eve reconstructing the SDK's. The agent name
* rides in a UA comment so a spaced display name stays a valid User-Agent.
* Also sets the OpenRouter-style `x-title` (agent name) and `http-referer`
* (deployment host). Returns `undefined` for non-gateway models or when no
* meaningful attribution is available.
*/
function buildGatewayAttributionHeaders(
model: LanguageModel,
Expand All @@ -274,18 +282,20 @@ function buildGatewayAttributionHeaders(
return undefined;
}

const version = runtimeIdentity?.eveVersion;
const title = runtimeIdentity?.agentName ?? runtimeIdentity?.agentId;
const deploymentHost = process.env.VERCEL_PROJECT_PRODUCTION_URL || process.env.VERCEL_URL;
const referer = deploymentHost ? `https://${deploymentHost}` : undefined;

if (!title && !referer) {
return undefined;
}

const headers: Record<string, string> = {};
if (version) {
headers["user-agent"] = title
? `${EVE_PACKAGE_NAME}/${version} (${title})`
: `${EVE_PACKAGE_NAME}/${version}`;
}
if (title) headers["x-title"] = title;
if (referer) headers["http-referer"] = referer;
return headers;
return Object.keys(headers).length > 0 ? headers : undefined;
}

async function resolveActiveRuntimeModel(input: {
Expand Down
Loading