Skip to content

Fix Anthropic custom provider Bearer authentication - #402

Merged
m-aebrer merged 4 commits into
masterfrom
feature/issue-392-anthropic-custom-provider-auth
Jul 27, 2026
Merged

Fix Anthropic custom provider Bearer authentication#402
m-aebrer merged 4 commits into
masterfrom
feature/issue-392-anthropic-custom-provider-auth

Conversation

@m-aebrer

Copy link
Copy Markdown
Collaborator

Closes #392

Make Anthropic-compatible custom provider authentication deterministic: explicit Bearer mode sends only Authorization, API-key mode sends only x-api-key, and SDK environment fallback cannot inject ambient credentials.

Implementation plan posted as a comment below.

@m-aebrer

Copy link
Copy Markdown
Collaborator Author

Implementation Plan

Problem and design

authHeader: true already expresses the correct user intent, so this change will keep that public configuration rather than introduce a second credential field with conflicting resolution rules. The fix will carry Bearer intent as non-secret model metadata to the provider layer, where the already-resolved request credential is available.

For the built-in Anthropic Messages implementation:

  • explicit Bearer mode will construct the SDK with apiKey: null and authToken: <request-time credential>;
  • default API-key mode will construct it with apiKey: <request-time credential> and authToken: null;
  • model-level Authorization materialized by the existing config path will not be allowed to override the request-time Bearer credential;
  • caller-supplied request headers will retain their existing final-override precedence;
  • Copilot and first-party Anthropic OAuth handling will remain special cases with their current identity/header behavior.

The model registry will continue materializing the documented Authorization header for backward compatibility with custom streamSimple implementations that consume model.headers, while the built-in Anthropic provider will use the new auth-mode signal as authoritative. This avoids breaking arbitrary custom APIs while removing stale credentials and dual auth from the affected built-in path.

Deliverables

  1. Represent explicit auth intent on models

    • Add optional, non-secret Bearer/API-key auth-mode metadata to the shared Model type; absence preserves current provider defaults and OAuth heuristics.
    • Map the existing provider-level authHeader: true setting to Bearer mode without adding a new models.json or extension config field.
  2. Apply auth configuration consistently in the model registry

    • Centralize the repeated auth-header/auth-mode transformation so models.json and extension registration cannot drift.
    • Cover custom-model definitions, built-in provider overrides, dynamic providers with models, and dynamic override-only providers.
    • Preserve existing header value resolution for environment-variable, literal, and command-backed credentials.
    • Make override-only authHeader configurations effective instead of silently ignoring them.
  3. Make Anthropic SDK construction deterministic

    • Select exactly one SDK credential channel from the model auth mode and the request-time resolved credential.
    • Explicitly null the unused SDK credential channel so ANTHROPIC_AUTH_TOKEN and ANTHROPIC_API_KEY SDK defaults cannot add ambient credentials.
    • In Bearer mode, prevent the load-time model Authorization header from overriding the selected request-time credential, using case-insensitive header handling while preserving explicit per-request overrides.
    • Preserve x-api-key defaults for existing third-party providers and preserve Copilot/Anthropic OAuth behavior.
    • Harden the first-party API-key branch against the same implicit authToken fallback.
  4. Add regression coverage at both configuration and wire levels

    • Extend constructor-option tests for third-party x-api-key and Bearer modes.
    • Add a non-network test using the real pinned Anthropic SDK with intercepted fetch to assert the final request headers, including when an unrelated ANTHROPIC_AUTH_TOKEN is present.
    • Extend model-registry tests for models.json and registerProvider() custom-model and override-only paths, including arbitrary env-var names, literal credentials, absent/false authHeader, and refresh behavior.
    • Verify a request-time credential wins over any load-time materialized header.
  5. Document and demonstrate the supported configuration

    • Add concrete Anthropic-compatible Bearer examples for models.json and extension registration.
    • State that authHeader: true sends Bearer without x-api-key for the built-in anthropic-messages implementation, while the default remains x-api-key.
    • Document support for custom-model and override-only provider configuration and all existing credential value formats.
    • Update the custom Anthropic extension example so every direct SDK constructor path explicitly selects or nulls both auth channels and cannot demonstrate the ambient fallback bug.

Files to modify or create

  • packages/ai/src/types.ts — add optional auth-mode metadata to Model.
  • packages/ai/src/providers/anthropic.ts — deterministic SDK credential selection, ambient-fallback hardening, and header precedence handling.
  • packages/ai/test/third-party-anthropic.test.ts — update the existing x-api-key invariant and add Bearer constructor coverage.
  • packages/ai/test/anthropic-auth-headers.test.ts — new real-SDK/fake-fetch final-header regression tests.
  • packages/coding-agent/src/core/model-registry.ts — propagate auth mode through all config paths and support override-only auth.
  • packages/coding-agent/src/core/extensions/types.ts — align public authHeader documentation with its deterministic semantics.
  • packages/coding-agent/test/model-registry.test.ts — cover models.json and dynamic provider auth configuration.
  • packages/coding-agent/docs/models.md — models.json Bearer configuration and guarantees.
  • packages/coding-agent/docs/custom-provider.md — extension configuration and Anthropic-specific behavior.
  • packages/coding-agent/docs/extensions.md — align the provider config reference and link to the detailed guide.
  • packages/coding-agent/examples/extensions/custom-provider-anthropic/index.ts — demonstrate explicit SDK auth-channel selection.

The root README, coding-agent README, and provider overview already route readers to the dedicated model/custom-provider guides without describing authHeader; they do not need duplicated configuration text.

Acceptance criteria

  • A third-party anthropic-messages provider with authHeader: true works with an arbitrary environment-variable name, literal key, or existing command-backed key resolution.
  • Bearer mode sends one Authorization: Bearer <resolved credential> header and no SDK-generated x-api-key.
  • Default/false mode sends x-api-key and no ambient Bearer header, including when ANTHROPIC_AUTH_TOKEN is set in the process.
  • The credential selected at request time through the existing AuthStorage priority chain is the credential sent on the wire.
  • authHeader works for both custom-model definitions and override-only provider configurations from models.json and registerProvider().
  • Existing Copilot, first-party Anthropic OAuth/API-key, and known third-party x-api-key behavior remains covered and unchanged except for removal of undocumented ambient SDK fallback.
  • Dedicated docs clearly show the supported Bearer configuration and single-header guarantee.

Testing and validation

  • Run Biome on every touched source, test, documentation, and example file.
  • Run targeted Vitest suites for Anthropic auth and model registry behavior.
  • Run npx tsgo --noEmit so the shared model type and examples are checked across workspaces.
  • Run npm run build before any compiled-binary smoke test, then npm run verify-workspace-links.
  • Run bash test.sh for CI-equivalent, API-key-isolated validation.
  • Smoke the compiled CLI with a sanitized custom-provider configuration; final wire-header assertions remain in deterministic fake-fetch tests and require no live credential or external endpoint.

Risks and resolved questions

  • Existing third-party compatibility: Bearer remains opt-in; unconfigured Kimi, MiniMax, OpenCode, Vercel AI Gateway, and other x-api-key endpoints retain their current mode.
  • Header casing/precedence: Model-level Authorization must be removed case-insensitively only when built-in Bearer mode owns auth; explicit per-request headers remain highest precedence.
  • Custom stream compatibility: Existing header materialization remains available to custom stream implementations even though the built-in Anthropic provider switches to request-time SDK auth.
  • SDK env behavior: Any flow that accidentally depended on an unrelated ANTHROPIC_AUTH_TOKEN being injected will stop doing so by design; users must opt into Bearer auth explicitly.
  • Schema choice: No new authToken config field is planned. Reusing authHeader avoids duplicate credential sources and undefined precedence.
  • Scope choice: Override-only provider configurations are included because the public schema already accepts authHeader there and silently ignoring it is unsafe.

Plan created by mach6

@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown

Vitest coverage

Metric Covered Total Coverage
Statements 25868 42992 60.16%
Branches 13963 26400 52.89%
Functions 4902 7817 62.7%
Lines 22085 37023 59.65%

View full coverage run

@m-aebrer

Copy link
Copy Markdown
Collaborator Author

Progress Update

Implemented deterministic authentication for Anthropic-compatible custom providers:

  • authHeader: true now carries explicit Bearer intent through custom-model and override-only models.json and extension provider paths.
  • The Anthropic SDK receives exactly one credential channel: Bearer mode uses authToken, while default API-key mode uses apiKey; the unused channel is explicitly nulled to prevent ambient SDK environment fallback.
  • Request-time credentials replace stale materialized Authorization headers for the built-in Anthropic provider, while custom stream implementations retain compatibility through model headers and auth metadata.
  • Added constructor-level, real-SDK wire-header, configuration, refresh, and dynamic reconfiguration regressions.
  • Updated root/package READMEs, dedicated provider/model docs, extension API documentation, and the custom Anthropic example.

Validation completed:

  • targeted Anthropic/model-registry tests: 63 passed
  • npx tsgo --noEmit
  • npm run build
  • npm run verify-workspace-links
  • bash test.sh --no-live-api: 5,088 passed, 708 skipped
  • compiled dreb -p local-server smoke: Bearer header present, x-api-key absent, ambient ANTHROPIC_AUTH_TOKEN not leaked

The live-inclusive suite was also attempted; configured ChatGPT enterprise tests reached the account usage limit. The complete credential-isolated suite is green.

Commit: 86d01a0


Progress tracked by mach6

@m-aebrer
m-aebrer marked this pull request as ready for review July 27, 2026 17:08
@m-aebrer

Copy link
Copy Markdown
Collaborator Author

Code Review

Critical

None.

Important

Finding 1 — Caller Authorization override is not verified on the final wire

Severity: high
Confidence: 97
Location: packages/ai/test/third-party-anthropic.test.ts:80-111

The Bearer-mode precedence test checks options.headers.Authorization only in the mocked SDK constructor's defaultHeaders. It does not verify the final HTTP request generated by the pinned Anthropic SDK. The scoped guarantee that caller-supplied request headers retain final-override precedence therefore lacks a wire-level regression.

Add a real-SDK intercepted-fetch test in anthropic-auth-headers.test.ts using a Bearer model with a stale, differently cased model Authorization header and headers: { Authorization: "Bearer caller-token" }. Assert that the final request contains exactly the caller token and no x-api-key.

Finding 2 — AuthStorage runtime credential precedence is not exercised end to end

Severity: high
Confidence: 93
Location: packages/coding-agent/test/model-registry.test.ts:470-629

Registry tests validate load-time auth metadata and credential resolution, while the wire test directly supplies a literal options.apiKey. No test connects the normal registry/AuthStorage path to real Anthropic request dispatch when a runtime or saved credential supersedes the materialized load-time Bearer header. That leaves the acceptance-critical AuthStorage priority-chain behavior split across layers rather than verified end to end.

Add an integration test that configures authHeader: true with a load-time credential, stores a different runtime credential in AuthStorage, resolves the model and request credential normally, intercepts the real SDK fetch, and asserts that only Authorization: Bearer <runtime credential> is sent.

Suggestions

Finding 3 — Identical withoutHeader helpers are duplicated

Severity: low
Confidence: 82
Locations: packages/ai/src/providers/anthropic.ts:206-213, packages/coding-agent/src/core/model-registry.ts:169-176

The helpers have identical names, signatures, and implementations. Consider sharing one utility from @dreb/ai to prevent future divergence. This is optional if keeping a trivial helper private in each abstraction layer is preferred over expanding the package API.

Finding 4 — Credential-channel selection is repeated in two SDK constructors

Severity: low
Confidence: 88
Location: packages/ai/src/providers/anthropic.ts:595-596,639-640

The same apiKey/authToken ternaries appear in the third-party and first-party explicit-auth branches. A local sdkCredentials object derived once from bearerAuth would name the invariant and ensure the paths cannot diverge.

Strengths

  • SDK construction structurally selects one credential channel and explicitly nulls the other, preventing ambient SDK fallback.
  • Real-SDK fake-fetch tests cover the core header behavior and ambient-token suppression rather than relying only on mocks.
  • Registry auth-mode propagation covers custom models, built-in overrides, dynamic registration, refresh, and override-only paths.
  • Existing Copilot, first-party OAuth/API-key, and third-party x-api-key behavior is preserved.
  • Documentation and the extension example clearly describe Bearer-only configuration and explicit SDK channel selection.
  • The code reviewer, error auditor, and completeness checker found no correctness, runtime-safety, or scope-completeness defects.

Agents run: code-reviewer, error-auditor, test-reviewer, completeness-checker, simplifier


Reviewed by mach6

@m-aebrer

Copy link
Copy Markdown
Collaborator Author

Review Assessment

Review comment

Classifications

Finding Classification Reasoning
Finding 1 — caller Authorization override lacks final-wire coverage genuine Factual: The mock-based test checks constructor defaultHeaders, while the real-SDK wire suite never supplies caller options.headers.Authorization. The current SDK behavior appears correct, but the final-wire contract is untested. Scope: The authoritative plan explicitly requires caller-supplied request headers to retain final-override precedence. This newly added, testable auth behavior needs regression coverage.
Finding 2 — AuthStorage runtime credential precedence lacks end-to-end coverage genuine Factual: Existing tests separately prove AuthStorage priority, registry Bearer metadata, and wire dispatch with a directly supplied key, but no test joins AuthStorage through ModelRegistry to real SDK dispatch and proves replacement of the stale materialized header. Scope: Acceptance explicitly requires the credential selected at request time through the existing AuthStorage priority chain to be the credential sent.
Finding 3 — duplicate withoutHeader helpers deferred Factual: Identical private helpers exist in the AI provider and coding-agent registry. Scope: The duplication does not affect auth correctness or security; sharing a trivial helper is optional cleanup and could unnecessarily expand the AI package API. No tracking issue is needed unless maintainers want broader helper cleanup.
Finding 4 — repeated SDK credential ternaries deferred Factual: The same apiKey/authToken selection pair appears in the third-party and first-party constructors. Scope: Both branches currently enforce the correct invariant. Extracting an object is optional maintainability work, not required for safe delivery. No tracking issue is needed.

Action Plan

  1. Add an SDK-backed fake-fetch test proving caller Authorization overrides both stale differently-cased model authorization and the generated request-time Bearer value, with no x-api-key.
  2. Add an end-to-end AuthStorage/ModelRegistry/real-SDK fake-fetch test proving a runtime or saved credential supersedes the load-time materialized Bearer header and is the only credential sent.

Assessment by mach6

@m-aebrer

Copy link
Copy Markdown
Collaborator Author

Progress Update

Fixed review findings 1 and 2:

  • Added real-SDK wire coverage proving a caller-supplied Authorization header overrides generated and stale Bearer credentials without sending x-api-key.
  • Added an end-to-end AuthStorage → ModelRegistry → generic AI dispatch → real Anthropic SDK test proving the runtime override is the only credential sent on the wire.

Validation completed:

  • Biome on both changed test files
  • 63 targeted Anthropic and registry tests
  • npx tsgo --noEmit
  • npm run build
  • npm run verify-workspace-links
  • bash test.sh --no-live-api: 5,090 passed, 709 skipped
  • Commit hook rerun: 5,090 passed, 709 skipped

Commit: 435dd32


Progress tracked by mach6

@m-aebrer
m-aebrer merged commit 23d7ec8 into master Jul 27, 2026
3 checks passed
@m-aebrer
m-aebrer deleted the feature/issue-392-anthropic-custom-provider-auth branch July 27, 2026 18:05
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.

Anthropic custom provider only authenticates when credential env var is named ANTHROPIC_AUTH_TOKEN (401 otherwise)

1 participant