diff options
Diffstat (limited to 'src/auth_manager.zig')
| -rw-r--r-- | src/auth_manager.zig | 52 |
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); |
