Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
16 changes: 16 additions & 0 deletions src/builtins/providers.zig
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ 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 openai_compatible = @import("../gateway/openai_compatible.zig");
const openai_compatible_models = @import("../gateway/openai_compatible_models.zig");
const provider_catalog = @import("../core/auth/provider_catalog.zig");

pub const native = provider_set.Set{
Expand All @@ -26,4 +30,16 @@ 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,
},
.openai_compatible = .{
.presentation = provider_catalog.find(.openai_compatible),
.agent_stream = openai_compatible.agent_stream_provider,
.cli_model_catalog = openai_compatible_models.cli_model_catalog_provider,
.model_catalog = openai_compatible_models.model_catalog_provider,
},
};
8 changes: 8 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,14 @@ 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,
},
.openai_compatible = .{
.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
5 changes: 4 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,8 @@ pub fn Runtime(comptime App: type) type {
const required_source: credentials.Source = switch (provider) {
.codex => .chatgpt_subscription,
.grok => .grok_subscription,
.anthropic => .anthropic_api_key,
.openai_compatible => .stored_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 +780,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 +1400,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, .openai_compatible => unreachable,
}

try std.testing.expectEqual(@as(usize, 0), app.auth.start_count);
Expand Down
37 changes: 35 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,23 @@ 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, .openai_compatible => switchModelFromEnv() orelse switch (provider) {
.codex => return error.CodexModelNotSelected,
.grok => return error.GrokModelNotSelected,
.anthropic => return error.AnthropicModelNotSelected,
.openai_compatible => return error.OpenAiModelNotSelected,
.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 +1173,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
19 changes: 19 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,24 @@ pub const Runtime = struct {
self,
loadRuntimeCredentialSource,
),
.anthropic => if (self.credentialSource() == .anthropic_api_key)
false
else
self.selectSourceWithLoader(
alloc,
.anthropic_api_key,
self,
loadRuntimeCredentialSource,
),
.openai_compatible => if (self.credentialSource() == .stored_key)
false
else
self.selectSourceWithLoader(
alloc,
.stored_key,
self,
loadRuntimeCredentialSource,
),
.gateway => if (self.credentialSource() != .chatgpt_subscription and self.credentialSource() != .grok_subscription)
false
else
Expand Down
2 changes: 2 additions & 0 deletions src/core/auth/auth_transition.zig
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ pub fn signInCompletion(
.{ .switch_provider = .grok }
else
.{ .activate_source = .grok_subscription },
.anthropic => .{ .activate_source = .anthropic_api_key },
.openai_compatible => .{ .activate_source = .stored_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