From 9f8719929dbd1326d9b327885c4da319c6d2673c Mon Sep 17 00:00:00 2001 From: t Date: Mon, 15 Jun 2026 17:08:00 -0600 Subject: anthropic credentials in oauth providers (eg copilot) --- libpanto/src/config.zig | 6 +++ libpanto/src/provider_anthropic_messages.zig | 56 ++++++++++++++++++++++++---- 2 files changed, 55 insertions(+), 7 deletions(-) (limited to 'libpanto/src') diff --git a/libpanto/src/config.zig b/libpanto/src/config.zig index 7c8a495..f2f04e7 100644 --- a/libpanto/src/config.zig +++ b/libpanto/src/config.zig @@ -122,6 +122,11 @@ pub const AnthropicMessagesConfig = struct { /// between tool calls. Ignored when `thinking == .adaptive` (interleaving /// is automatic there) or `.disabled`. thinking_interleaved: bool = false, + /// Use `Authorization: Bearer ...` instead of `x-api-key`. This is set + /// by the embedder from the configured auth family (not guessed from the + /// base URL): standard Anthropic uses `false`, while OAuth-backed + /// Anthropic-compatible providers such as Copilot set `true`. + use_bearer_auth: bool = false, /// Place one `cache_control` breakpoint on the last cacheable block of /// each request, replicating Anthropic's "automatic caching" (a single /// advancing breakpoint) via the broadly-supported per-block marker @@ -344,6 +349,7 @@ test "ProviderConfig - anthropic_messages variant" { try t.expectEqual(APIStyle.anthropic_messages, cfg.style()); try t.expectEqualStrings("2023-06-01", cfg.anthropic_messages.api_version); try t.expectEqual(@as(u32, 64_000), cfg.anthropic_messages.max_tokens); + try t.expectEqual(false, cfg.anthropic_messages.use_bearer_auth); try t.expectEqual(Thinking.disabled, cfg.anthropic_messages.thinking); try t.expectEqual(Effort.medium, cfg.anthropic_messages.effort); try t.expectEqual(@as(?u32, 32_000), cfg.anthropic_messages.thinking_budget_tokens); diff --git a/libpanto/src/provider_anthropic_messages.zig b/libpanto/src/provider_anthropic_messages.zig index 95013d1..5721e4a 100644 --- a/libpanto/src/provider_anthropic_messages.zig +++ b/libpanto/src/provider_anthropic_messages.zig @@ -85,16 +85,34 @@ pub const AnthropicMessagesRequest = struct { defer self.allocator.free(body); std.log.debug("anthropic_messages => {s}", .{body}); - // Build headers. The four base headers are always present; the - // interleaved-thinking beta header is added only when the config - // explicitly requests manual extended thinking with interleaving. - // It is intentionally NOT sent for `.adaptive` (interleaving is - // automatic there and the header causes 400s on some backends) or - // `.disabled`. + // Build headers. Standard Anthropic-compatible backends use + // `x-api-key`. OAuth-backed Anthropic-compatible providers (e.g. + // Copilot) opt into `Authorization: Bearer ...` via + // `config.use_bearer_auth`, which is derived from the configured auth + // family rather than guessed from the URL/headers. The four base + // headers are always present; the interleaved-thinking beta header is + // added only when the config explicitly requests manual extended + // thinking with interleaving. It is intentionally NOT sent for + // `.adaptive` (interleaving is automatic there and the header causes + // 400s on some backends) or `.disabled`. + const use_bearer_auth = self.config.use_bearer_auth; + const auth_value = if (use_bearer_auth) + try std.fmt.allocPrint( + self.allocator, + "Bearer {s}", + .{self.config.api_key}, + ) + else + ""; + defer if (use_bearer_auth) self.allocator.free(auth_value); + const auth_header: http.Header = if (use_bearer_auth) + .{ .name = "authorization", .value = auth_value } + else + .{ .name = "x-api-key", .value = self.config.api_key }; var headers_buf: [5]http.Header = .{ .{ .name = "content-type", .value = "application/json" }, .{ .name = "accept", .value = "text/event-stream" }, - .{ .name = "x-api-key", .value = self.config.api_key }, + auth_header, .{ .name = "anthropic-version", .value = self.config.api_version }, undefined, // slot reserved for the optional beta header }; @@ -1226,6 +1244,11 @@ test "two streamed turns persist assistant replies in the conversation" { ); } +/// Helper: whether this config sends Bearer auth instead of `x-api-key`. +fn headerSliceUsesBearerAuth(cfg: *const config_mod.AnthropicMessagesConfig) bool { + return cfg.use_bearer_auth; +} + /// Helper: build the header slice exactly as `open` does, given a config, /// and return whether the interleaved beta header is present. /// This lets us test the header-selection logic without a live HTTP connection. @@ -1234,6 +1257,25 @@ fn headerSliceIncludesInterleaved(cfg: *const config_mod.AnthropicMessagesConfig return send_interleaved; } +test "oauth-backed anthropic auth uses bearer auth" { + const cfg: config_mod.AnthropicMessagesConfig = .{ + .api_key = "k", + .base_url = "https://api.individual.githubcopilot.com", + .model = "claude-sonnet-4-5", + .use_bearer_auth = true, + }; + try testing.expect(headerSliceUsesBearerAuth(&cfg)); +} + +test "plain anthropic auth uses x-api-key" { + const cfg: config_mod.AnthropicMessagesConfig = .{ + .api_key = "k", + .base_url = "https://api.anthropic.com/v1", + .model = "claude-sonnet-4-5", + }; + try testing.expect(!headerSliceUsesBearerAuth(&cfg)); +} + test "interleaved beta header: enabled when thinking=.enabled and interleaved=true" { const cfg: config_mod.AnthropicMessagesConfig = .{ .api_key = "k", -- cgit v1.3