summaryrefslogtreecommitdiff
path: root/src/auth_manager.zig
diff options
context:
space:
mode:
authort <t@tjp.lol>2026-06-15 17:08:00 -0600
committert <t@tjp.lol>2026-06-15 17:18:20 -0600
commit9f8719929dbd1326d9b327885c4da319c6d2673c (patch)
treecb7cd72f0be46c099e9831364ba7d5cbedebeb09 /src/auth_manager.zig
parentd9d4131cb321a9ce29b000cd7aed8ced9a18efc1 (diff)
anthropic credentials in oauth providers (eg copilot)
Diffstat (limited to 'src/auth_manager.zig')
-rw-r--r--src/auth_manager.zig52
1 files changed, 51 insertions, 1 deletions
diff --git a/src/auth_manager.zig b/src/auth_manager.zig
index 5504807..e91339b 100644
--- a/src/auth_manager.zig
+++ b/src/auth_manager.zig
@@ -166,6 +166,31 @@ pub const AuthManager = struct {
}
};
+/// When an auth exchange returns only an origin/base host but the provider's
+/// configured base_url carries an API path segment (e.g. Anthropic providers
+/// configured as `.../v1`), preserve that path on the override. This keeps
+/// dynamic-host exchanges from accidentally dropping a required version prefix.
+fn mergeBaseUrlOverride(
+ arena: Allocator,
+ style: panto.APIStyle,
+ provider_base_url: []const u8,
+ override_base_url: ?[]const u8,
+) ![]const u8 {
+ const over = override_base_url orelse return provider_base_url;
+ if (style != .anthropic_messages) return over;
+
+ const prov_uri = std.Uri.parse(provider_base_url) catch return over;
+ const over_uri = std.Uri.parse(over) catch return over;
+ const prov_path = prov_uri.path.percent_encoded;
+ const over_path = over_uri.path.percent_encoded;
+
+ const prov_has_path = prov_path.len > 0 and !std.mem.eql(u8, prov_path, "/");
+ const over_has_path = over_path.len > 0 and !std.mem.eql(u8, over_path, "/");
+ if (!prov_has_path or over_has_path) return over;
+
+ return try std.fmt.allocPrint(arena, "{s}{s}", .{ over, prov_path });
+}
+
/// Patch a resolved credential into the live provider config: set the bearer/
/// key, override base_url when the credential supplies one, and merge the
/// provider's static `extra_headers` with the auth-derived ones (into `arena`).
@@ -181,7 +206,7 @@ fn patchCredential(
@memcpy(merged[0..prov.extra_headers.len], prov.extra_headers);
@memcpy(merged[prov.extra_headers.len..], cred.extra_headers);
- const base_url = cred.base_url_override orelse prov.base_url;
+ const base_url = try mergeBaseUrlOverride(arena, prov.style, prov.base_url, cred.base_url_override);
switch (live.provider) {
.openai_chat => |*c| {
c.api_key = cred.api_key;
@@ -239,6 +264,31 @@ test "patchCredential: injects key, base_url override, merged headers" {
try t.expectEqualStrings("chatgpt-account-id", live.provider.openai_chat.extra_headers[1].name);
}
+test "patchCredential: anthropic preserves provider path on base_url override" {
+ var aa = std.heap.ArenaAllocator.init(t.allocator);
+ defer aa.deinit();
+ const arena = aa.allocator();
+
+ const prov: config_file.Provider = .{
+ .name = "copilot-anthropic",
+ .style = .anthropic_messages,
+ .base_url = "https://api.individual.githubcopilot.com/v1",
+ .auth_name = "github_copilot",
+ };
+ var live: panto.Config = .{ .provider = .{ .anthropic_messages = .{
+ .api_key = "",
+ .base_url = prov.base_url,
+ .model = "claude-sonnet-4-5",
+ } } };
+ const cred: panto.ResolvedCredential = .{
+ .api_key = "exchanged-token",
+ .base_url_override = "https://api.individual.githubcopilot.com",
+ };
+ try patchCredential(&live, &prov, cred, arena);
+
+ try t.expectEqualStrings("https://api.individual.githubcopilot.com/v1", live.provider.anthropic_messages.base_url);
+}
+
test "resolveInto: api_key auth is a no-op" {
const a = t.allocator;
var env = std.process.Environ.Map.init(a);