Skip to content

Commit 26b6dd0

Browse files
authored
feat (providers/gateway): include deployment and request id (#6591)
## Background For o11y it is useful to have a developer's deployment and request id. The deployment id variable name was incorrect, and we don't yet include logic to look up the request id. ## Summary Corrected the environment variable name to look up the deployment id, and added logic to look for the request id in the headers as `x-vercel-id`. ## Verification Added verbose logging and deployed to a preview deployment to observe correct data being logged. ## Tasks - [x] Tests have been added / updated (for bug fixes / features) - [ ] Documentation has been added / updated (for bug fixes / features) - [x] A _patch_ changeset for relevant packages has been added (for bug fixes / features - run `pnpm changeset` in the project root) - [x] Formatting issues have been fixed (run `pnpm prettier-fix` in the project root)
1 parent 819fe61 commit 26b6dd0

File tree

6 files changed

+121
-28
lines changed

6 files changed

+121
-28
lines changed

.changeset/chilly-chairs-press.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@ai-sdk/gateway': patch
3+
---
4+
5+
feat (providers/gateway): include deployment and request id

packages/gateway/src/gateway-language-model.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
postJsonToApi,
1313
resolve,
1414
type ParseResult,
15+
type Resolvable,
1516
} from '@ai-sdk/provider-utils';
1617
import { z } from 'zod';
1718
import type { GatewayConfig } from './gateway-config';
@@ -20,7 +21,7 @@ import { asGatewayError } from './errors';
2021

2122
type GatewayChatConfig = GatewayConfig & {
2223
provider: string;
23-
o11yHeaders: Record<string, string>;
24+
o11yHeaders: Resolvable<Record<string, string>>;
2425
};
2526

2627
export class GatewayLanguageModel implements LanguageModelV2 {
@@ -51,7 +52,7 @@ export class GatewayLanguageModel implements LanguageModelV2 {
5152
await resolve(this.config.headers()),
5253
options.headers,
5354
this.getModelConfigHeaders(this.modelId, false),
54-
this.config.o11yHeaders,
55+
await resolve(this.config.o11yHeaders),
5556
),
5657
body: this.maybeEncodeFileParts(body),
5758
successfulResponseHandler: createJsonResponseHandler(z.any()),
@@ -86,7 +87,7 @@ export class GatewayLanguageModel implements LanguageModelV2 {
8687
await resolve(this.config.headers()),
8788
options.headers,
8889
this.getModelConfigHeaders(this.modelId, true),
89-
this.config.o11yHeaders,
90+
await resolve(this.config.o11yHeaders),
9091
),
9192
body: this.maybeEncodeFileParts(body),
9293
successfulResponseHandler: createEventSourceResponseHandler(z.any()),

packages/gateway/src/gateway-provider.test.ts

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
} from './gateway-provider';
77
import { GatewayFetchMetadata } from './gateway-fetch-metadata';
88
import { NoSuchModelError } from '@ai-sdk/provider';
9-
import { getVercelOidcToken } from './get-vercel-oidc-token';
9+
import { getVercelOidcToken, getVercelRequestId } from './vercel-environment';
1010
import { resolve } from '@ai-sdk/provider-utils';
1111
import { GatewayLanguageModel } from './gateway-language-model';
1212
import {
@@ -23,14 +23,16 @@ vi.mock('./gateway-fetch-metadata', () => ({
2323
GatewayFetchMetadata: vi.fn(),
2424
}));
2525

26-
vi.mock('./get-vercel-oidc-token', () => ({
26+
vi.mock('./vercel-environment', () => ({
2727
getVercelOidcToken: vi.fn(),
28+
getVercelRequestId: vi.fn(),
2829
}));
2930

3031
describe('GatewayProvider', () => {
3132
beforeEach(() => {
3233
vi.clearAllMocks();
3334
vi.mocked(getVercelOidcToken).mockResolvedValue('mock-oidc-token');
35+
vi.mocked(getVercelRequestId).mockResolvedValue('mock-request-id');
3436
if ('AI_GATEWAY_API_KEY' in process.env) {
3537
Reflect.deleteProperty(process.env, 'AI_GATEWAY_API_KEY');
3638
}
@@ -76,7 +78,7 @@ describe('GatewayProvider', () => {
7678
};
7779

7880
const provider = createGatewayProvider(options);
79-
await provider('test-model');
81+
provider('test-model');
8082

8183
const constructorCall = vi.mocked(GatewayLanguageModel).mock.calls[0];
8284
const config = constructorCall[1];
@@ -218,10 +220,11 @@ describe('GatewayProvider', () => {
218220
const originalEnv = process.env;
219221
process.env = {
220222
...originalEnv,
221-
DEPLOYMENT_ID: 'test-deployment',
223+
VERCEL_DEPLOYMENT_ID: 'test-deployment',
222224
VERCEL_ENV: 'test',
223225
VERCEL_REGION: 'iad1',
224226
};
227+
vi.mocked(getVercelRequestId).mockResolvedValue('test-request-id');
225228

226229
try {
227230
const provider = createGatewayProvider({
@@ -230,18 +233,25 @@ describe('GatewayProvider', () => {
230233
});
231234
provider('test-model');
232235

233-
expect(GatewayLanguageModel).toHaveBeenCalledWith(
234-
'test-model',
236+
const constructorCall = vi.mocked(GatewayLanguageModel).mock.calls[0];
237+
const config = constructorCall[1];
238+
239+
expect(config).toEqual(
235240
expect.objectContaining({
236241
provider: 'gateway',
237242
baseURL: 'https://api.example.com',
238-
o11yHeaders: {
239-
'ai-o11y-deployment-id': 'test-deployment',
240-
'ai-o11y-environment': 'test',
241-
'ai-o11y-region': 'iad1',
242-
},
243+
o11yHeaders: expect.any(Function),
243244
}),
244245
);
246+
247+
// Test that the o11yHeaders function returns the expected result
248+
const o11yHeaders = await resolve(config.o11yHeaders);
249+
expect(o11yHeaders).toEqual({
250+
'ai-o11y-deployment-id': 'test-deployment',
251+
'ai-o11y-environment': 'test',
252+
'ai-o11y-region': 'iad1',
253+
'ai-o11y-request-id': 'test-request-id',
254+
});
245255
} finally {
246256
process.env = originalEnv;
247257
}
@@ -254,21 +264,30 @@ describe('GatewayProvider', () => {
254264
process.env.VERCEL_ENV = undefined;
255265
process.env.VERCEL_REGION = undefined;
256266

267+
vi.mocked(getVercelRequestId).mockResolvedValue(undefined);
268+
257269
try {
258270
const provider = createGatewayProvider({
259271
baseURL: 'https://api.example.com',
260272
apiKey: 'test-api-key',
261273
});
262274
provider('test-model');
263275

264-
expect(GatewayLanguageModel).toHaveBeenCalledWith(
265-
'test-model',
276+
// Get the constructor call to check o11yHeaders
277+
const constructorCall = vi.mocked(GatewayLanguageModel).mock.calls[0];
278+
const config = constructorCall[1];
279+
280+
expect(config).toEqual(
266281
expect.objectContaining({
267282
provider: 'gateway',
268283
baseURL: 'https://api.example.com',
269-
o11yHeaders: {},
284+
o11yHeaders: expect.any(Function),
270285
}),
271286
);
287+
288+
// Test that the o11yHeaders function returns empty object
289+
const o11yHeaders = await resolve(config.o11yHeaders);
290+
expect(o11yHeaders).toEqual({});
272291
} finally {
273292
process.env = originalEnv;
274293
}
@@ -650,7 +669,7 @@ describe('GatewayProvider', () => {
650669
baseURL: 'https://api.example.com',
651670
headers: expect.any(Function),
652671
fetch: undefined,
653-
o11yHeaders: expect.any(Object),
672+
o11yHeaders: expect.any(Function),
654673
}),
655674
);
656675
});
@@ -701,7 +720,7 @@ describe('GatewayProvider', () => {
701720
baseURL: 'https://api.example.com',
702721
headers: expect.any(Function),
703722
fetch: undefined,
704-
o11yHeaders: expect.any(Object),
723+
o11yHeaders: expect.any(Function),
705724
}),
706725
);
707726

@@ -725,7 +744,7 @@ describe('GatewayProvider', () => {
725744
baseURL: 'https://api.example.com',
726745
headers: expect.any(Function),
727746
fetch: undefined,
728-
o11yHeaders: expect.any(Object),
747+
o11yHeaders: expect.any(Function),
729748
}),
730749
);
731750

packages/gateway/src/gateway-provider.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
} from './gateway-fetch-metadata';
1313
import { GatewayLanguageModel } from './gateway-language-model';
1414
import type { GatewayModelId } from './gateway-language-model-settings';
15-
import { getVercelOidcToken } from './get-vercel-oidc-token';
15+
import { getVercelOidcToken, getVercelRequestId } from './vercel-environment';
1616

1717
export interface GatewayProvider extends ProviderV2 {
1818
(modelId: GatewayModelId): LanguageModelV2;
@@ -101,7 +101,7 @@ export function createGatewayProvider(
101101
const createLanguageModel = (modelId: GatewayModelId) => {
102102
const deploymentId = loadOptionalSetting({
103103
settingValue: undefined,
104-
environmentVariableName: 'DEPLOYMENT_ID',
104+
environmentVariableName: 'VERCEL_DEPLOYMENT_ID',
105105
});
106106
const environment = loadOptionalSetting({
107107
settingValue: undefined,
@@ -116,10 +116,14 @@ export function createGatewayProvider(
116116
baseURL,
117117
headers: getHeaders,
118118
fetch: options.fetch,
119-
o11yHeaders: {
120-
...(deploymentId && { 'ai-o11y-deployment-id': deploymentId }),
121-
...(environment && { 'ai-o11y-environment': environment }),
122-
...(region && { 'ai-o11y-region': region }),
119+
o11yHeaders: async () => {
120+
const requestId = await getVercelRequestId();
121+
return {
122+
...(deploymentId && { 'ai-o11y-deployment-id': deploymentId }),
123+
...(environment && { 'ai-o11y-environment': environment }),
124+
...(region && { 'ai-o11y-region': region }),
125+
...(requestId && { 'ai-o11y-request-id': requestId }),
126+
};
123127
},
124128
});
125129
};

packages/gateway/src/get-vercel-oidc-token.test.ts renamed to packages/gateway/src/vercel-environment.test.ts

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2-
import { getVercelOidcToken } from './get-vercel-oidc-token';
3-
import { GatewayAuthenticationError } from './errors';
2+
import { getVercelOidcToken, getVercelRequestId } from './vercel-environment';
43

54
const SYMBOL_FOR_REQ_CONTEXT = Symbol.for('@vercel/request-context');
65

@@ -91,3 +90,64 @@ describe('getVercelOidcToken', () => {
9190
expect(token).toBe('env-token-value');
9291
});
9392
});
93+
94+
describe('getVercelRequestId', () => {
95+
const originalSymbolValue = (globalThis as any)[SYMBOL_FOR_REQ_CONTEXT];
96+
97+
beforeEach(() => {
98+
(globalThis as any)[SYMBOL_FOR_REQ_CONTEXT] = undefined;
99+
});
100+
101+
afterEach(() => {
102+
if (originalSymbolValue) {
103+
(globalThis as any)[SYMBOL_FOR_REQ_CONTEXT] = originalSymbolValue;
104+
} else {
105+
(globalThis as any)[SYMBOL_FOR_REQ_CONTEXT] = undefined;
106+
}
107+
});
108+
109+
it('should get request ID from request headers when available', async () => {
110+
(globalThis as any)[SYMBOL_FOR_REQ_CONTEXT] = {
111+
get: () => ({
112+
headers: {
113+
'x-vercel-id': 'req_1234567890abcdef',
114+
},
115+
}),
116+
};
117+
118+
const requestId = await getVercelRequestId();
119+
expect(requestId).toBe('req_1234567890abcdef');
120+
});
121+
122+
it('should return undefined when request ID header is not available', async () => {
123+
(globalThis as any)[SYMBOL_FOR_REQ_CONTEXT] = {
124+
get: () => ({ headers: {} }),
125+
};
126+
127+
const requestId = await getVercelRequestId();
128+
expect(requestId).toBeUndefined();
129+
});
130+
131+
it('should return undefined when no headers are available', async () => {
132+
(globalThis as any)[SYMBOL_FOR_REQ_CONTEXT] = {
133+
get: () => ({}),
134+
};
135+
136+
const requestId = await getVercelRequestId();
137+
expect(requestId).toBeUndefined();
138+
});
139+
140+
it('should handle missing request context gracefully', async () => {
141+
(globalThis as any)[SYMBOL_FOR_REQ_CONTEXT] = undefined;
142+
143+
const requestId = await getVercelRequestId();
144+
expect(requestId).toBeUndefined();
145+
});
146+
147+
it('should handle missing get method in request context', async () => {
148+
(globalThis as any)[SYMBOL_FOR_REQ_CONTEXT] = {};
149+
150+
const requestId = await getVercelRequestId();
151+
expect(requestId).toBeUndefined();
152+
});
153+
});

packages/gateway/src/get-vercel-oidc-token.ts renamed to packages/gateway/src/vercel-environment.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ The token is expected to be provided via the 'VERCEL_OIDC_TOKEN' environment var
2020
return token;
2121
}
2222

23+
export async function getVercelRequestId(): Promise<string | undefined> {
24+
return getContext().headers?.['x-vercel-id'];
25+
}
26+
2327
type Context = {
2428
headers?: Record<string, string>;
2529
};

0 commit comments

Comments
 (0)