Skip to content

Support auth-less custom Anthropic providers via apiKeyOptional - #109

Merged
wch merged 2 commits into
mainfrom
authless-provider
Sep 8, 2026
Merged

wch merged 2 commits into
mainfrom
authless-provider

Conversation

@wch

@wch wch commented Sep 8, 2026 •

Copy link
Copy Markdown
Collaborator

This PR lets a custom Anthropic provider in providers.json be used without an API key, via a new optional apiKeyOptional: true field. The motivating case is an endpoint that handles authentication itself — for example, a local enterprise proxy that injects SSO credentials — so asking the user for a key is unnecessary (reported in posit-dev/assistant-feedback#91).

Custom OpenAI-compatible entries already worked this way. Custom Anthropic entries always required a key, which made this kind of proxy impossible to configure.

How it works

Deciding whether a key is required. A provider is key-optional if its provider type is inherently key-optional (as OpenAI-compatible already is) OR the individual entry sets apiKeyOptional: true. Like other providers.json settings, the field can come from the user's config file or from admin-managed default/enforced config, so an administrator can pin the policy fleet-wide.

Each resolved catalog entry records both the effective policy and where it came from (built-in default, admin default, user config, or admin-enforced). Consumers need this to tell an overridable default apart from an enforced value, even when the two happen to be equal. Editing only this field in providers.json triggers a live reload: the file watcher treats it as a connection change, so the provider is re-registered and its credentials are re-resolved without a restart.

What "no key" means on the wire. When the policy is on, an empty API key string is the signal for "send no credentials":

  • Model discovery sends only the anthropic-version header — no auth headers.
  • Chat needs care because the Anthropic SDK falls back to the ANTHROPIC_API_KEY environment variable when given an empty key. We hand the SDK a non-empty placeholder to prevent that, then strip both x-api-key and Authorization from the request before it is sent (and before raw HTTP logging captures it). Neither a real key nor the placeholder ever reaches the endpoint or the log files.

Changes

  • providers.json schema: the custom Anthropic variant, and the admin default/enforced fragment shape, now accept apiKeyOptional; the generated JSON schema is regenerated.
  • Resolved catalog entries now carry the effective key-optionality policy and its source; the catalog builder copies both for custom entries.
  • The config file watcher treats a policy-only edit as a connection change, triggering a live reload.
  • New auth-descriptor helper that combines the provider-type default with the entry's authored value; the existing type-level map is unchanged for back-compat.
  • Anthropic model discovery and chat accept an empty key as described above.

Testing

  • Wire-level tests assert that no auth header reaches the endpoint or the raw request logs, including with ANTHROPIC_API_KEY set in the test process to prove it isn't inherited.
  • The generated-schema equality test passes.

This supports the auth-less custom provider feature in posit-dev/assistant; the companion Positron change (teaching the host's own provider management about the new field) is specced separately.

Custom anthropic entries in providers.json can now declare
apiKeyOptional: true for endpoints that need no API key (e.g. a
localhost SSO-authenticating enterprise proxy):

- ai-config: the field on the custom anthropic variant and the
  custom-fragment shape; ResolvedProvider carries the resolved policy
  (kind default OR authored value) with its provenance
  (kind-default | default | user | enforced); the catalog watcher
  classifies an auth-policy-only change as connectionChanged.
- ai-credentials: resolveCustomAuthMapping() applies kind default OR
  entry value; the kind-level map stays for back-compat. A shape-guard
  assertion pins ai-config's mirrored kind defaults to the descriptors.
- ai-provider-bridge: apiKey === "" is the canonical anonymous signal.
  Anthropic discovery proceeds keyless (only anthropic-version is sent);
  chat passes the SDK a non-empty sentinel (preventing ANTHROPIC_API_KEY
  fallback) and strips x-api-key/Authorization in fetch middleware
  before raw HTTP logging, so neither sentinel nor key reaches the wire
  or the logs.
The per-key mapped-type union absorbed `never` mismatch branches
(`true | never` collapses to `true`, and `[never] extends [true]`
passes), so the guard type-checked even when every mirrored default
disagreed. Mismatch branches now produce `false`, so any divergence
turns the union into `boolean` and fails the final assertion.
@wch
wch merged commit ff42c1b into main Sep 8, 2026
4 checks passed
@wch
wch deleted the authless-provider branch September 8, 2026 22:34
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.

1 participant