Skip to content

feat(llm): apikey_header auth scheme for Kong AI Gateway key-auth#303

Merged
initializ-mk merged 2 commits into
mainfrom
feat/llm-apikey-header-auth-scheme
Jul 13, 2026
Merged

feat(llm): apikey_header auth scheme for Kong AI Gateway key-auth#303
initializ-mk merged 2 commits into
mainfrom
feat/llm-apikey-header-auth-scheme

Conversation

@initializ-mk

Copy link
Copy Markdown
Contributor

Closes #302.

Problem

Kong AI Gateway's key-auth plugin reads the consumer key from a fixed apikey header (its default key_names) and ignores provider-native auth headers. Forge's LLM clients send only the provider-native scheme — Authorization: Bearer <key> (OpenAI-shape) or x-api-key: <key> (Anthropic-shape) — so an agent pointed at a Kong-fronted OPENAI_BASE_URL / ANTHROPIC_BASE_URL fails every LLM call:

401 { "message": "No API key found in request" }

The platform's own gateway clients were already fixed to send the key in both headers; agents were the remaining consumer, and the header logic lives here in forge's provider clients.

Change

Extend the existing AuthScheme extension point (added for aws_sigv4 in #202) with a new scheme:

  • auth_scheme: apikey_header — sends the API key in a gateway header in addition to the provider-native header. Additive by design: the gateway consumes apikey, Kong's ai-proxy replaces/injects the upstream provider header, and OpenAI-compatible endpoints ignore the extra header — so the scheme is safe to enable against non-Kong gateways too.
  • auth_header_name (optional) — overrides the header name for gateways with custom key_names (e.g. x-gateway-key). Defaults to apikey.
model:
  provider: openai                                 # or anthropic — symmetric
  name: gpt-4o
  base_url: https://kong-gateway.internal/openai
  auth_scheme: apikey_header
  # auth_header_name: x-gateway-key                # optional; default: apikey

The key comes from the usual OPENAI_API_KEY / ANTHROPIC_API_KEY env var.

Implementation

  • llm.ClientConfig / types.ModelRef: new AuthHeaderName field; introduced scheme constants (AuthSchemeAWSSigV4 / AuthSchemeAPIKeyHeader / DefaultAPIKeyHeaderName) that also replace the scattered "aws_sigv4" literals.
  • providers/auth_scheme.go: shared setGatewayAPIKeyHeader helper, wired into both providers' setHeaders — additive, and a no-op off-path or when APIKey is empty.
  • runtime/config.go: carries ModelRef.auth_header_name onto the client config (mirrors the auth_scheme / aws_region carry-over).

Tests

  • Both providers send both the native header and the gateway apikey header under apikey_header.
  • auth_header_name override is honored (and the default apikey header isn't also set).
  • No-op for the default scheme and for an empty API key.
  • ResolveModelConfig carries auth_scheme + auth_header_name from forge.yaml onto the client.

All pass with the full forge-core suite; gofmt + golangci-lint clean.

Docs

docs/core-concepts/runtime-engine.md, docs/reference/forge-yaml-schema.md, and the knowledge skill document the scheme, the additive semantics, and the auth_header_name override.

Follow-up (not in this PR)

Platform wiring in agent-builder — an env (e.g. AI_ROUTER_AUTH_SCHEME=apikey_header) written into the generated forge.yaml model block at build, so operators configure it once per install — matching the SigV4 precedent. Tracked separately.

…oses #302)

Kong AI Gateway's key-auth plugin reads the consumer key from a fixed
`apikey` header and ignores provider-native headers, so an agent pointed
at a Kong-fronted OPENAI_BASE_URL/ANTHROPIC_BASE_URL 401s every LLM call
with "No API key found in request".

Extend the existing AuthScheme extension point (added for aws_sigv4 in
#202) with auth_scheme: apikey_header — sends the API key in a gateway
header IN ADDITION TO the provider-native header (additive, so safe
against non-gateway endpoints; Kong's ai-proxy replaces/injects the
upstream provider header anyway). Header name defaults to "apikey"
(Kong key-auth's default key_names) and is overridable via
model.auth_header_name for gateways with custom key names.

- llm.ClientConfig / types.ModelRef: new AuthHeaderName field; scheme
  constants (AuthSchemeAWSSigV4 / AuthSchemeAPIKeyHeader /
  DefaultAPIKeyHeaderName) replace the scattered "aws_sigv4" literals.
- providers/auth_scheme.go: shared setGatewayAPIKeyHeader helper, wired
  into both anthropic and openai setHeaders (additive, no-op off-path or
  when APIKey is empty).
- runtime/config.go: carry ModelRef.auth_header_name onto the client.
- Tests: both providers send both headers; custom header name; no-op for
  the default scheme and empty key; config carry-over.
- Docs: runtime-engine.md, forge-yaml-schema.md, knowledge skill.

Platform wiring (agent-builder env → generated forge.yaml) is a
follow-up, matching the SigV4 precedent.

@initializ-mk initializ-mk left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: apikey_header auth scheme

The core change is exactly how an extension point should be extended: scheme constants replace the scattered "aws_sigv4" literals, a single shared setGatewayAPIKeyHeader helper with correct no-op semantics is wired into both providers, and the config carry-over mirrors the established aws_region pattern. Tests are exact — both providers assert BOTH headers, the custom-name test also asserts the default header ISN'T set, off-path no-ops are pinned, and the yaml→client mapping is covered. Docs updated in all three places including the knowledge skill. The additive design is correctly reasoned (Kong consumes apikey, ai-proxy manages the upstream header, OpenAI-compatible endpoints ignore unknown headers). CI fully green.

Three minors, all inline:

  1. auth_scheme values are validated nowhere (verified — no code outside the config mapping touches it): a typo silently reproduces the exact 401 this PR fixes. Recommended ride-along.
  2. Fallbacks can't use the schemeModelFallback has no scheme fields and resolveFallbacks builds bare ClientConfig{APIKey, Model}; worth a documenting sentence or the symmetric fields.
  3. Header collision + upstream key transitauth_header_name: Authorization overwrites the native Bearer header; Kong key-auth defaults to hide_credentials: false so the key transits upstream in an unconventional header.

Verdict

Merge-ready for the happy path; finding 1 strongly recommended as a ride-along — it's a few lines against constants this PR itself introduces, and it converts this PR's target failure mode from "silent 401s" into a config error. Findings 2–3 are a docs sentence each or small follow-ups.

// AuthScheme == "apikey_header"; carried unconditionally so the client
// owns the default-name fallback.
if cfg.Model.AuthHeaderName != "" {
mc.Client.AuthHeaderName = cfg.Model.AuthHeaderName

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor (recommended ride-along): auth_scheme values are validated nowhere — a typo reproduces the exact 401 this PR fixes, silently.

Verified: no code outside this mapping touches auth_scheme — no validate-package check, no warning. auth_scheme: apikey_headr parses fine, matches no branch, degrades to native-headers-only → every call against Kong fails with No API key found in request (the precise #302 symptom) with zero diagnostics pointing at the typo. Same failure class as #297's capability-name typo, which got parse-time validation — the precedent exists, and this PR's new constants make the check trivial: validate against the known set ("", x_api_key, bearer, aws_sigv4, apikey_header) here (warn) or in forge validate (error).

A warn for unsupported provider+scheme combos fits the same check: gemini/ollama + apikey_header is silently ignored today (only the openai/anthropic clients are wired).

// scheme (issue #302). Empty → "apikey" (Kong key-auth's default
// key_names). Set it for a gateway with custom key_names, e.g.
// "x-gateway-key". Ignored for every other AuthScheme.
AuthHeaderName string `yaml:"auth_header_name,omitempty"`

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: fallbacks can't use this scheme — worth one documenting sentence.

ModelFallback carries only provider/name/base_url, and resolveFallbacks builds bare ClientConfig{APIKey, Model} — no AuthScheme/AuthHeaderName. A fallback routed through the same Kong gateway (a base_url on the fallback entry) 401s exactly like the primary did before this PR. The shape is pre-existing (#202's aws_sigv4 has the same gap), but this PR's docs actively encourage gateway-fronted deployments — so either add the two fields to ModelFallback (small, symmetric) or state the limitation in the docs: "auth_scheme applies to the primary model only."

if headerName == "" {
headerName = llm.DefaultAPIKeyHeaderName
}
req.Header.Set(headerName, apiKey)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor (security): guard against auth_header_name colliding with a native auth header.

This runs AFTER the provider's native header set and uses Header.Set — so auth_header_name: Authorization silently overwrites the native Bearer header with the raw key (no prefix), breaking auth in a maximally confusing way; auth_header_name: x-api-key on Anthropic is a harmless same-value overwrite but equally nonsensical. Reject or warn when the custom name case-insensitively equals Authorization or x-api-key — fits naturally into the scheme validation suggested on config.go.


Some API gateways authenticate with a fixed header name rather than the provider-native scheme. Kong AI Gateway's `key-auth` plugin, for example, reads the consumer key from an `apikey` header and ignores `Authorization` / `x-api-key`, so an agent pointed at a Kong-fronted `OPENAI_BASE_URL` / `ANTHROPIC_BASE_URL` otherwise fails every LLM call with `401 {"message":"No API key found in request"}`.

`model.auth_scheme: apikey_header` sends the API key in the gateway header **in addition to** the provider-native header — additive, so it stays safe against non-gateway endpoints (the gateway consumes `apikey`; Kong's ai-proxy replaces/injects the upstream provider header). The header name defaults to `apikey` (Kong `key-auth`'s default `key_names`) and is overridable via `auth_header_name` for gateways with custom key names:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor (docs): worth one sentence on upstream key transit — Kong key-auth defaults to hide_credentials: false, meaning the gateway forwards the apikey header upstream, so the credential transits in a header conventional redaction tooling (which knows Authorization/x-api-key) won't scrub. Recommend hide_credentials: true on the Kong plugin here. (Forge's own tracing redactor matches key values by shape, so opt-in content capture is already covered.)

Address the PR #303 review's three minors:

1. auth_scheme is now validated (forge-core/validate/forge_config.go): an
   unrecognized value errors (a typo previously degraded silently to
   native-headers-only, reproducing the exact 401 apikey_header fixes),
   auth_scheme on a non-openai/anthropic provider warns (only those
   clients honor it), and a stray auth_header_name warns.

3. Header collision: an auth_header_name that case-insensitively equals a
   native auth header (Authorization / x-api-key) errors in validation,
   and setGatewayAPIKeyHeader refuses to overwrite a native header as
   defense-in-depth for the programmatic path.

2. Fallbacks: documented that auth_scheme applies to the PRIMARY model
   only (ModelFallback godoc + schema doc + knowledge skill) — matches
   the pre-existing aws_sigv4 shape; per-fallback fields tracked as a
   follow-up (the FORGE_MODEL_FALLBACKS env source needs a parallel
   encoding).

4. Docs: note Kong key-auth hide_credentials: false forwards the apikey
   header upstream; recommend hide_credentials: true.

Tests cover unknown scheme, collision (incl. lowercase), provider-combo
and stray-header warnings, and the runtime no-clobber guard.
@initializ-mk

Copy link
Copy Markdown
Contributor Author

Thanks — all three addressed in 8e262f2.

1 (recommended ride-along) — auth_scheme validation. Added to ValidateForgeConfig (forge-core/validate/forge_config.go), against the constants this PR introduced:

  • Unrecognized auth_schemeerror (known: x_api_key, bearer, aws_sigv4, apikey_header), so a typo like apikey_headr is a config error at forge validate / startup instead of the silent 401 it would otherwise reproduce.
  • auth_scheme set on a non-openai/anthropic provider → warning (only those two clients are wired), covering the gemini/ollama + apikey_header (or aws_sigv4) silent-ignore case you flagged.
  • Stray auth_header_name without apikey_headerwarning (ignored).

3 (security) — header collision. An auth_header_name that case-insensitively equals Authorization / x-api-keyerror in validation. Belt-and-suspenders: setGatewayAPIKeyHeader now also refuses to overwrite a native header on the programmatic path (test pins the lowercase authorization case → native Bearer survives).

2 (fallbacks) — documented the limitation. auth_scheme applies to the primary model only; a fallback authenticates with its provider-native header. Called out in the ModelFallback godoc, forge-yaml-schema.md, and runtime-engine.md. I went with the doc route rather than adding the fields because the FORGE_MODEL_FALLBACKS env source (Source 2 in resolveFallbacks) has no per-fallback encoding for a scheme, so doing it properly is its own small follow-up — noted in the struct comment.

4 (docs) — Kong upstream key transit. Added the hide_credentials note recommending hide_credentials: true so the apikey header is stripped before proxying upstream.

Tests: unknown scheme, collision (incl. lowercase), provider-combo + stray-header warnings, and the runtime no-clobber guard. Full forge-core suite + lint clean across modules.

@initializ-mk initializ-mk left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review of 8e262f2 — all findings resolved; one loose end (file the promised follow-up issue)

Finding 1 (validation) — fixed beyond the ask. ValidateForgeConfig now carries four checks: unknown auth_schemeerror naming the known set (built from this PR's own llm constants, so it can't drift), scheme on an unsupported provider → warning (covering the gemini/ollama silent-ignore for both aws_sigv4 and apikey_header), native-header collision → error, and a stray auth_header_name without the scheme → warning — that last one wasn't even asked for and catches the "set the header but forgot the scheme" half-configuration. All five cases table-tested, including the literal apikey_headr typo from the review.

Finding 3a (collision) — belt and braces. The validation error is backed by defense-in-depth in setGatewayAPIKeyHeader itself (case-insensitive refusal to touch authorization/x-api-key), protecting programmatic ClientConfig construction that never passes through forge validate. The test pins the lowercase variant and asserts the native Bearer survives.

Finding 3b — docs note added recommending hide_credentials: true on the Kong plugin.

Finding 2 (fallbacks) — documented as a limitation with an honest deferral reason (the FORGE_MODEL_FALLBACKS env source needs a parallel encoding first): godoc, schema doc, and knowledge skill all state auth_scheme applies to the primary model only. Reasonable decline-with-tracking — except the tracking doesn't exist yet: the commit says "tracked as a follow-up," but no such issue is in the tracker (checked). Please file it so the commit message is true.

Verdict

All findings resolved — merge-ready once the in-flight Lint/Test checks go green and the per-fallback auth_scheme follow-up issue is filed (a one-minute action).

@initializ-mk initializ-mk merged commit 9ac17a3 into main Jul 13, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

LLM client: apikey-header auth scheme for Kong AI Gateway (key-auth)

1 participant