Skip to content
3 changes: 3 additions & 0 deletions src/acp/server.zig
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,7 @@ pub fn selectCredentialForProvider(
.refresh_if_needed,
provider,
state.credential_source,
null,
);
break :blk resolution.credential orelse return false;
};
Expand Down Expand Up @@ -1370,6 +1371,7 @@ fn handleInitialize(state: *ServerState, alloc: Allocator, msg: *jsonrpc.Message
.refresh_if_needed,
state.provider,
preferred,
null,
);
routed_credential = resolution.credential;
if (routed_credential == null) {
Expand Down Expand Up @@ -1651,6 +1653,7 @@ fn handleSetConfigOption(state: *ServerState, alloc: Allocator, msg: *jsonrpc.Me
.refresh_if_needed,
target,
null,
null,
);
break :credential resolution.credential orelse
return state.writer.writeError(alloc, msg.id, .{
Expand Down
8 changes: 8 additions & 0 deletions src/builtins/providers.zig
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ const openai_codex_permission_reviewer = @import("../gateway/openai_codex_permis
const xai_grok = @import("../gateway/xai_grok.zig");
const xai_grok_models = @import("../gateway/xai_grok_models.zig");
const xai_grok_permission_reviewer = @import("../gateway/xai_grok_permission_reviewer.zig");
const anthropic = @import("../gateway/anthropic.zig");
const anthropic_models = @import("../gateway/anthropic_models.zig");
const provider_catalog = @import("../core/auth/provider_catalog.zig");

pub const native = provider_set.Set{
Expand All @@ -26,4 +28,10 @@ pub const native = provider_set.Set{
.model_catalog = xai_grok_models.model_catalog_provider,
.permission_reviewer = xai_grok_permission_reviewer.provider,
},
.anthropic = .{
.presentation = provider_catalog.find(.anthropic),
.agent_stream = anthropic.agent_stream_provider,
.cli_model_catalog = anthropic_models.cli_model_catalog_provider,
.model_catalog = anthropic_models.model_catalog_provider,
},
};
4 changes: 4 additions & 0 deletions src/core/app/app_agent_runtime.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1064,6 +1064,10 @@ pub fn Runtime(comptime App: type) type {
.agent_stream = tool_context.agent_stream_provider,
.permission_reviewer = tool_context.permission_reviewer_provider,
},
.anthropic = .{
.agent_stream = tool_context.agent_stream_provider,
.permission_reviewer = null,
},
};
return subagent_agent_adapter.run(.{
.host = app_session_runtime.Runtime(App).subagentHost(app) orelse
Expand Down
4 changes: 3 additions & 1 deletion src/core/app/app_auth_runtime.zig
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ pub fn Runtime(comptime App: type) type {
const required_source: credentials.Source = switch (provider) {
.codex => .chatgpt_subscription,
.grok => .grok_subscription,
.anthropic => .anthropic_api_key,
.gateway => app.auth.credentialSource() orelse .fx_login,
};
const route_change = app.auth.selectForProvider(app.alloc, provider) catch |err| switch (err) {
Expand Down Expand Up @@ -778,6 +779,7 @@ pub fn Runtime(comptime App: type) type {
.refresh_if_needed,
target,
null,
null,
) catch |err| {
debug_trace.logf("provider", "credential preparation failed provider={t} err={s}", .{ target, @errorName(err) });
try app.writeDomainNotice(.{
Expand Down Expand Up @@ -1397,7 +1399,7 @@ test "interactive subscription sign-in rejects active and queued work before OAu
switch (provider) {
.codex => try Runtime(BusySignInApp).beginChatGptSignIn(&app),
.grok => try Runtime(BusySignInApp).beginGrokSignIn(&app),
.gateway => unreachable,
.gateway, .anthropic => unreachable,
}

try std.testing.expectEqual(@as(usize, 0), app.auth.start_count);
Expand Down
36 changes: 34 additions & 2 deletions src/core/app/app_lifecycle.zig
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,13 @@ pub const StartupState = struct {
notification_attention_required: bool = false,
notification_max: bool = false,
theme_monitor_enabled: bool = false,
profile_anthropic_api_key: []u8 = &.{},

pub fn takeProfileAnthropicApiKey(self: *StartupState) []u8 {
const value = self.profile_anthropic_api_key;
self.profile_anthropic_api_key = &.{};
return value;
}

pub fn deinit(self: *StartupState, alloc: Allocator) void {
self.workspace_access.deinit(alloc);
Expand Down Expand Up @@ -400,6 +407,9 @@ fn loadStartupStateFromOwnedWorkspace(
state.model_source = detailed.model_source orelse .compiled_default;
state.selected_model = try loadInitialModel(alloc, configured_selection.model, null);
if (hasProcessModelOverride()) state.model_source = .process_override;
if (settings.anthropic_api_key) |profile_key| {
state.profile_anthropic_api_key = try alloc.dupe(u8, profile_key);
}
state.config_diagnostics = detailed.diagnostics;
detailed.diagnostics = &.{};
state.prompt_history_enabled = settings.prompt_history_enabled orelse true;
Expand All @@ -412,6 +422,7 @@ fn loadStartupStateFromOwnedWorkspace(
mode,
state.provider,
settings.credential_source,
settings.anthropic_api_key,
);
state.credential = resolution.credential;
state.stored_key_status = resolution.stored_key_status;
Expand Down Expand Up @@ -1113,12 +1124,22 @@ fn configuredProviderSelection(
const provider = settings.provider orelse .gateway;
const model = settings.models.get(provider) orelse switch (provider) {
.gateway => default_model,
.codex => return error.CodexModelNotSelected,
.grok => return error.GrokModelNotSelected,
.codex, .grok, .anthropic => switchModelFromEnv() orelse switch (provider) {
.codex => return error.CodexModelNotSelected,
.grok => return error.GrokModelNotSelected,
.anthropic => return error.AnthropicModelNotSelected,
.gateway => unreachable,
},
};
return .{ .provider = provider, .model = model };
}

fn switchModelFromEnv() ?[]const u8 {
const raw = io_mod.getenv("FX_MODEL") orelse return null;
const trimmed = std.mem.trim(u8, raw, " \t\r\n");
return if (trimmed.len > 0) trimmed else null;
}

fn initialModelId(default_model: []const u8, configured: ?[]const u8) []const u8 {
const model = io_mod.getenv("FX_MODEL") orelse return configured orelse default_model;
const trimmed = std.mem.trim(u8, model, " \t\r\n");
Expand Down Expand Up @@ -1151,6 +1172,17 @@ test "startup provider chooses only its provider-scoped model" {
const grok = try configuredProviderSelection("default/model", &grok_settings);
try std.testing.expectEqual(model_provider.ProviderId.grok, grok.provider);
try std.testing.expectEqualStrings("grok-model", grok.model);
var anthropic_settings = config_runtime.Settings{ .provider = .anthropic };
anthropic_settings.models.values[@intFromEnum(model_provider.ProviderId.anthropic)] = @constCast("claude-opus-5");
const anthropic = try configuredProviderSelection("default/model", &anthropic_settings);
try std.testing.expectEqual(model_provider.ProviderId.anthropic, anthropic.provider);
try std.testing.expectEqualStrings("claude-opus-5", anthropic.model);

const missing_anthropic = config_runtime.Settings{ .provider = .anthropic };
try std.testing.expectError(
error.AnthropicModelNotSelected,
configuredProviderSelection("default/model", &missing_anthropic),
);
}

fn loadInitialModel(alloc: Allocator, default_model: []const u8, configured: ?[]const u8) ![]u8 {
Expand Down
10 changes: 10 additions & 0 deletions src/core/auth/auth_runtime.zig
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,7 @@ pub fn loadStatusSnapshotForProvider(
.stored,
selected_provider,
preferred,
null,
)
else
credentials.resolvePreferring(
Expand Down Expand Up @@ -1636,6 +1637,15 @@ pub const Runtime = struct {
self,
loadRuntimeCredentialSource,
),
.anthropic => if (self.credentialSource() == .anthropic_api_key)
false
else
self.selectSourceWithLoader(
alloc,
.anthropic_api_key,
self,
loadRuntimeCredentialSource,
),
.gateway => if (self.credentialSource() != .chatgpt_subscription and self.credentialSource() != .grok_subscription)
false
else
Expand Down
1 change: 1 addition & 0 deletions src/core/auth/auth_transition.zig
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ pub fn signInCompletion(
.{ .switch_provider = .grok }
else
.{ .activate_source = .grok_subscription },
.anthropic => .{ .activate_source = .anthropic_api_key },
};
}

Expand Down
1 change: 1 addition & 0 deletions src/core/auth/credential_authority.zig
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ pub fn derive(
.ai_gateway_api_key,
.fx_login,
.stored_key,
.anthropic_api_key,
=> hash.update("\x00slot\x00"),
.chatgpt_subscription,
.grok_subscription,
Expand Down
Loading