summaryrefslogtreecommitdiff
path: root/libpanto
diff options
context:
space:
mode:
Diffstat (limited to 'libpanto')
-rw-r--r--libpanto/src/config.zig6
-rw-r--r--libpanto/src/provider_anthropic_messages.zig56
2 files changed, 55 insertions, 7 deletions
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",