When using the Vercel AI SDK with Workflow DevKit, AI Gateway authentication fails with GatewayAuthenticationError even when VERCEL_OIDC_TOKEN is present in the environment. The root cause is that the @vercel/oidc package requires Node.js fs module for token validation/refresh, which is not available in the workflow sandbox.
- AI SDK Version: 6.0.50 (also tested with 5.0.76)
- @ai-sdk/gateway: 3.0.23
- @vercel/oidc: 3.0.5
- Workflow DevKit: 4.0.1-beta.44
- Date Discovered: January 26, 2026
GatewayAuthenticationError: Unauthenticated request to AI Gateway.
To authenticate, set the AI_GATEWAY_API_KEY environment variable with your API key.
Alternatively, you can use a provider module instead of the AI Gateway.
import { generateText } from 'ai';
import { fetch } from 'workflow';
export async function myWorkflow(input: string) {
'use workflow';
// This is the documented pattern but it FAILS
globalThis.fetch = fetch;
const { text } = await generateText({
model: 'openai/o4-mini',
prompt: input,
});
}| Check | Result |
|---|---|
process.env available in workflow |
✅ Yes (81 keys) |
VERCEL_OIDC_TOKEN in workflow env |
✅ SET (1195 chars) |
globalThis.fetch override works |
✅ Applied successfully |
| Token still set at error time | ✅ STILL SET |
| AI SDK call succeeds | ❌ FAILED |
The workflow sandbox DOES have access to process.env including VERCEL_OIDC_TOKEN. The issue is NOT missing environment variables.
The AI SDK's gateway authentication flows through these packages:
ai (generateText)
└── @ai-sdk/gateway (getGatewayAuthToken)
└── @vercel/oidc (getVercelOidcToken)
File: @vercel/oidc/dist/get-vercel-oidc-token.js
async function getVercelOidcToken() {
let token = "";
try {
token = getVercelOidcTokenSync(); // Gets token from process.env ✅
} catch (error) { /* ... */ }
try {
// Dynamic imports that use fs module
const [{ getTokenPayload, isExpired }, { refreshToken }] = await Promise.all([
await import("./token-util.js"), // ⚠️ Uses fs module
await import("./token.js")
]);
// Check if token needs refresh
if (!token || isExpired(getTokenPayload(token))) {
await refreshToken(); // ❌ FAILS - needs fs
}
} catch (error) {
throw new VercelOidcTokenError(`Failed to refresh OIDC token`, error);
}
}File: @vercel/oidc/dist/token-util.js
var fs = __toESM(require("fs")); // ❌ Not available in workflow sandbox
var path = __toESM(require("path"));
function findProjectInfo() {
const dir = findRootDir();
const prjPath = path.join(dir, ".vercel", "project.json");
if (!fs.existsSync(prjPath)) { // ❌ fs.existsSync fails in sandbox
throw new VercelOidcTokenError("project.json not found");
}
// ...
}
function loadToken(projectId) {
// ...
const tokenPath = path.join(dir, "com.vercel.token", `${projectId}.json`);
if (!fs.existsSync(tokenPath)) { // ❌ fs.existsSync fails in sandbox
return null;
}
// ...
}- ✅ Token is read from
process.env.VERCEL_OIDC_TOKEN - ✅
@vercel/oidcchecks if token is expired usingisExpired(getTokenPayload(token)) ⚠️ If expired (or validation needed),refreshToken()is called- ❌
refreshToken()→findProjectInfo()→ requiresfsmodule - ❌
fsmodule is NOT available in workflow sandbox (by design - for determinism) - ❌ Error is thrown → caught → wrapped as
GatewayAuthenticationError
Step functions run in the full Node.js context where fs is available:
import { generateText } from 'ai';
// Step function has full Node.js access
async function generateWithAI(prompt: string) {
'use step';
const { text } = await generateText({
model: 'openai/o4-mini',
prompt,
});
return text;
}
export async function myWorkflow(input: string) {
'use workflow';
// Call the step function - auth works
const result = await generateWithAI(input);
}Use AI_GATEWAY_API_KEY instead of OIDC tokens:
# .env.local
AI_GATEWAY_API_KEY=your_key_hereThis bypasses the OIDC token validation flow entirely.
The @vercel/oidc package should detect when running in a restricted environment (like workflow sandbox) and gracefully handle the case where fs is unavailable:
async function getVercelOidcToken() {
let token = "";
try {
token = getVercelOidcTokenSync();
} catch (error) { /* ... */ }
// If we have a token from env, try to use it directly first
if (token) {
try {
const { getTokenPayload, isExpired } = await import("./token-util.js");
if (!isExpired(getTokenPayload(token))) {
return token; // Token is valid, use it directly
}
} catch (error) {
// fs not available (e.g., workflow sandbox)
// Return the token anyway and let the server validate it
return token;
}
}
// Only attempt refresh if we can access fs
try {
const { refreshToken } = await import("./token.js");
await refreshToken();
token = getVercelOidcTokenSync();
} catch (error) {
if (token) {
// We have a token but can't refresh - use what we have
return token;
}
throw new VercelOidcTokenError(`Failed to refresh OIDC token`, error);
}
return token;
}Alternatively, the workflow runtime could:
- Pre-validate tokens before workflow execution starts
- Inject a mock
fsthat returns appropriate values for token operations - Provide a workflow-specific OIDC token getter that doesn't require filesystem access
- All AI SDK calls using gateway models in workflow functions fail
- Affects both
generateTextandgenerateObject - Users must restructure code to use step functions for all AI calls
- This is not documented and the documented
globalThis.fetch = fetchpattern does not work
@vercel/oidc/dist/get-vercel-oidc-token.js- Token retrieval@vercel/oidc/dist/token-util.js- Usesfsfor token validation@vercel/oidc/dist/token.js- Usesfsfor token refresh@ai-sdk/gateway/src/gateway-provider.ts- CallsgetVercelOidcTokenai/src/prompt/wrap-gateway-error.ts- Error wrapping
The documented pattern of using globalThis.fetch = fetch in workflow functions does not work for AI Gateway authentication because the underlying @vercel/oidc package requires filesystem access that is not available in the workflow sandbox. The workaround is to move all AI SDK calls to step functions which have full Node.js runtime access.
Report generated: January 26, 2026 Investigated by: Claude (Opus 4.5) with user verification