Skip to content

ai-gateway-provider: BYOK fails for OpenRouter due to case-sensitive header key mismatch #355

@martinklepsch

Description

@martinklepsch

✨ 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:
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",
});
  1. 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 key
Proof 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 response

Fails (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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions