Skip to content

apiUrl can't reach an authenticated AI Gateway: the direct path has no way to send cf-aig-authorization #253

Description

@ishibashi-futos

AiModelConfig.apiUrl documents itself as the way to point a model at a proxy:

https://github.com/cloudflare/cloudflare-os/blob/main/packages/workshop-shared/src/api.ts#L1160-L1165

URL of the API. If not specified, use the default for the provider. Overriding the URL is
useful in order to use AI proxy products like Cloudflare's AI gateway, or even to use an
alternative provider that provides a compatible API.

But an AI Gateway with Authenticated Gateway turned on rejects a request that carries no cf-aig-authorization header, and the direct path has no way to send one. In ai-models.ts that header is only ever constructed in getModelViaUserGateway() and getModelViaGateway() — both reached from a connected Cloudflare account or the CF_AI_GATEWAY_* deployment env, never from the model config:

$ rg -n 'cf-aig-authorization' packages/workshop-backend/src/ai-models.ts
400:      "cf-aig-authorization": `Bearer ${userGateway.apiKey}`,   # getModelViaUserGateway
427:    "cf-aig-authorization": `Bearer ${gwConfig.apiToken}`,      # getModelViaGateway

getModelDirect() builds headers only for the keyless-Ollama {Authorization: null} case. AiModelConfig is {provider, model, apiToken, accountId?, apiUrl?} — there is no field that can carry it.

So the documented use case works only against an unauthenticated gateway. The same gap applies to any header-authenticated proxy (LiteLLM, Portkey, Helicone) and to private inference endpoints that route on a header such as X-Tenant-Id.

To be upfront: the fix is an added field, so this is capability-shaped rather than a pure bug, and it is over the automatic PR size limit — I'm filing it rather than sending a patch. Discussion #29 covers the adjacent request-shape problem, but nothing there or anywhere else in the tracker mentions headers (custom header, extraHeaders, defaultHeaders return zero results).

What would fix it

headers?: Record<string, string> on AiModelConfig, merged in getModelDirect(). pi already supports it — ProviderHeaders exists and all four API impls merge options.headers last so caller values beat the SDK's own; it is simply never populated from user config.

Two things that matter if anyone picks this up:

  • Merge order is load-bearing. The config's headers have to merge after the ones the provider client sends. The keyless Ollama path sends Authorization: null to strip the SDK's bearer token, so if config headers merge first, a user-supplied Authorization is deleted and the endpoint gets no credentials at all.
  • Direct path only, matching how apiToken/apiUrl are already ignored under AI Gateway mode — otherwise a config header could clobber cf-aig-authorization on the gateway paths.

I have this working on a fork if it's useful as a reference — about 40 lines of non-test change, plus unit tests and an end-to-end check against a local endpoint with a recording proxy in front of it, asserting on what actually reached the wire: ishibashi-futos#1

Happy to close this if you'd rather fold it into the custom-provider work in #29.

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