-
Notifications
You must be signed in to change notification settings - Fork 234
Description
✨ Disclaimer: Debugged and authored with Claude.
Summary
When using createOpenRouter() from ai-gateway-provider without passing an API key (expecting AI Gateway BYOK to handle authentication), requests fail with a 401 error from OpenRouter: "No cookie auth credentials found".
Root cause: The header removal logic uses lowercase "authorization" but the OpenRouter SDK sends "Authorization" (capital A). Since JavaScript object keys are case-sensitive, the deletion silently fails.
Environment
ai-gateway-provider: 3.0.2@openrouter/ai-sdk-provider: 1.5.4ai(Vercel AI SDK): 6.0.24
Steps to Reproduce
- Configure AI Gateway with OpenRouter BYOK credentials in the Cloudflare dashboard
- Use the following code:
import { createAiGateway } from "ai-gateway-provider";
import { createOpenRouter } from "ai-gateway-provider/providers/openrouter";
import { generateText } from "ai";
const aigateway = createAiGateway({
accountId: "your-account-id",
gateway: "default",
apiKey: "your-ai-gateway-token",
});
// No API key passed - expecting BYOK to handle it
const openrouter = createOpenRouter();
const model = openrouter.chat("perplexity/sonar-pro");
const { text } = await generateText({
model: aigateway(model),
prompt: "Hello",
});- Observe the error:
APICallError [AI_APICallError]: No cookie auth credentials found
statusCode: 401,
responseBody: '{"error":{"message":"No cookie auth credentials found","code":401}}'
Root Cause Analysis
Step 1: CF_TEMP_TOKEN placeholder is set correctly
The authWrapper in src/auth.ts correctly sets CF_TEMP_TOKEN as a placeholder when no API key is provided.
Step 2: OpenRouter SDK uses capital "Authorization"
The @openrouter/ai-sdk-provider sets the header with a capital A:
// From @openrouter/ai-sdk-provider
headers: () => ({
Authorization: `Bearer ${...}` // Capital A!
})Step 3: ai-gateway-provider looks for lowercase "authorization"
In src/index.ts, the header removal logic uses lowercase:
const authHeader = providerConfig.headerKey ?? "authorization"; // lowercase!
const authValue = "get" in req.request.headers
? req.request.headers.get(authHeader)
: req.request.headers[authHeader]; // Looking for "authorization"
if (authValue?.indexOf(CF_TEMP_TOKEN) !== -1) {
// ...
delete req.request.headers[authHeader]; // Deleting "authorization"
}Step 4: Case mismatch causes silent failure
Since JavaScript object keys are case-sensitive:
const headers = { "Authorization": "Bearer CF_TEMP_TOKEN" };
headers["authorization"] // undefined - key doesn't exist!
delete headers["authorization"]; // No-op - deletes nothing
headers["Authorization"] // Still "Bearer CF_TEMP_TOKEN"Step 5: The condition passes incorrectly
const authValue = undefined; // Because "authorization" doesn't exist
authValue?.indexOf("CF_TEMP_TOKEN") // undefined
undefined !== -1 // true! Condition passes but deletion targets wrong keyProof of Concept
// Simulating the bug
const headers = { "Authorization": "Bearer CF_TEMP_TOKEN", "Content-Type": "application/json" };
const authHeader = "authorization"; // lowercase
console.log("Before:", headers);
// { Authorization: 'Bearer CF_TEMP_TOKEN', 'Content-Type': 'application/json' }
const authValue = headers[authHeader]; // undefined
// Condition check - passes incorrectly!
console.log("undefined !== -1:", undefined !== -1); // true
delete headers[authHeader]; // Deletes nothing - wrong key!
console.log("After:", headers);
// { Authorization: 'Bearer CF_TEMP_TOKEN', 'Content-Type': 'application/json' }
// Header is STILL there!Verification via curl
Works (empty headers):
curl -X POST "https://gateway.ai.cloudflare.com/v1/{account_id}/default" \
-H "Content-Type: application/json" \
-H "cf-aig-authorization: Bearer {token}" \
-d '[{
"provider": "openrouter",
"endpoint": "v1/chat/completions",
"headers": {},
"query": {"model": "perplexity/sonar-pro", "messages": [{"role": "user", "content": "Hi"}]}
}]'
# Returns successful responseFails (with Authorization header):
curl -X POST "https://gateway.ai.cloudflare.com/v1/{account_id}/default" \
-H "Content-Type: application/json" \
-H "cf-aig-authorization: Bearer {token}" \
-d '[{
"provider": "openrouter",
"endpoint": "v1/chat/completions",
"headers": {"Authorization": "Bearer CF_TEMP_TOKEN"},
"query": {"model": "perplexity/sonar-pro", "messages": [{"role": "user", "content": "Hi"}]}
}]'
# Returns: {"error":{"message":"No cookie auth credentials found","code":401}}Workaround
Use the unified provider with the compat endpoint:
import { createAiGateway } from "ai-gateway-provider";
import { createUnified } from "ai-gateway-provider/providers/unified";
import { generateText } from "ai";
const aigateway = createAiGateway({
accountId: "your-account-id",
gateway: "default",
apiKey: "your-ai-gateway-token",
});
const unified = createUnified();
const model = unified.chatModel("openrouter/perplexity/sonar-pro");
const { text } = await generateText({
model: aigateway(model),
prompt: "Hello",
});
// Works correctly!Suggested Fix
Use case-insensitive header key matching:
// Find the actual key in the headers object (case-insensitive)
const findHeaderKey = (headers: Record<string, string>, targetKey: string): string | undefined => {
const lowerTarget = targetKey.toLowerCase();
return Object.keys(headers).find(key => key.toLowerCase() === lowerTarget);
};
const authHeaderKey = providerConfig.headerKey ?? "authorization";
const actualKey = findHeaderKey(req.request.headers, authHeaderKey);
if (actualKey) {
const authValue = req.request.headers[actualKey];
if (authValue?.includes(CF_TEMP_TOKEN)) {
delete req.request.headers[actualKey];
}
}Impact
This bug likely affects all providers using BYOK, since most SDKs use Authorization (capital A) which is the standard HTTP convention. HTTP headers are case-insensitive per RFC 7230, so the code should handle both cases.