ai-gateway-provider: BYOK fails for OpenRouter due to case-sensitive header key mismatch
- Dominant language
- TypeScript
- Stars
- 1.2k
- Forks
- 345
- Avg merge
- 13h 31m
- Merged PRs (30d)
- 1
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.4
- `ai` (Vercel AI SDK): 6.0.24
Steps to Reproduce
1. Configure AI Gateway with OpenRouter BYOK credentials in the Cloudflare dashboard
2. Use the following code:
```typescript
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",
});
```
3. 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:
```typescript
// 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:
```typescript
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:
```javascript
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
```javascript
const authValue = undefined; // Because "authorization" doesn't exist
authValue?.indexOf("CF_TEMP_TOKEN") // undefined
undefined !== -1 // true! Condition passes but deletion targets wrong key
```
Proof of Concept
```javascript
// 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):**
```bash
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 response
```
**Fails (with Authorization header):**
```bash
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:
```typescript
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:
```typescript
// Find the actual key in the headers object (case-insensitive)
const findHeaderKey = (headers: Record, 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.
Contributor guide
Research direction
Start in src/index.ts at the header removal logic described in the issue, then reproduce the BYOK request with an Authorization header from the OpenRouter provider. Verify that temporary authentication is removed regardless of header-key casing and that the resulting OpenRouter request no longer returns the 401 error.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- api, authentication
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Stale
- Clarity
- Clearly specified
- Newbie friendliness
- 50/100