Skip to content
Merged
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
111 changes: 104 additions & 7 deletions src/backends/claude/claude_agent_sdk.erl
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@
-export([discover_models_from_claude_config/0,
discover_models_from_claude_config/1,
collect_claude_model_ids/1,
discoverable_claude_model_ids/1,
expand_claude_model_entries/1,
normalize_claude_model_id/1,
claude_config_path/0]).
-endif.
Expand Down Expand Up @@ -231,7 +233,7 @@ supported_commands(Session) ->
supported_models(Session) ->
case extract_init_field(Session, models, models, []) of
{ok, Models} when is_list(Models), Models =/= [] ->
{ok, Models};
{ok, expand_claude_model_entries(Models)};
{ok, _} ->
discover_models_from_claude_config();
{error, _} = Err ->
Expand Down Expand Up @@ -829,9 +831,11 @@ collect_claude_model_ids(#{<<"lastModelUsage">> := Usage} = Map)
lists:flatmap(fun collect_claude_model_ids/1, maps:values(Map));
collect_claude_model_ids(#{<<"model">> := Model} = Map) when is_binary(Model) ->
MaybeModel =
case normalize_claude_model_id(Model) of
<<"claude-", _/binary>> = ClaudeModel -> [ClaudeModel];
_ -> []
case Model of
<<"claude-", _/binary>> ->
discoverable_claude_model_ids(Model);
_ ->
[]
end,
MaybeModel ++ lists:flatmap(fun collect_claude_model_ids/1,
maps:values(Map));
Expand All @@ -844,11 +848,11 @@ collect_claude_model_ids(_) ->

-spec usage_model_ids(map()) -> [binary()].
usage_model_ids(Usage) ->
[Normalized
[Model
|| ModelId <- maps:keys(Usage),
is_binary(ModelId),
Normalized <- [normalize_claude_model_id(ModelId)],
case Normalized of
Model <- discoverable_claude_model_ids(ModelId),
case Model of
<<"claude-", _/binary>> -> true;
_ -> false
end].
Expand All @@ -857,6 +861,99 @@ usage_model_ids(Usage) ->
normalize_claude_model_id(ModelId) when is_binary(ModelId) ->
hd(binary:split(ModelId, <<"[">>)).

-spec discoverable_claude_model_ids(binary()) -> [binary()].
discoverable_claude_model_ids(ModelId) when is_binary(ModelId) ->
BaseModelId = normalize_claude_model_id(ModelId),
case supports_context_variant(BaseModelId) of
true ->
ordsets:from_list([BaseModelId, context_variant_model_id(BaseModelId)]);
false when BaseModelId =:= <<>> ->
[];
false ->
[BaseModelId]
end.

-spec supports_context_variant(binary()) -> boolean().
supports_context_variant(<<"claude-sonnet-4-6">>) -> true;
supports_context_variant(<<"claude-opus-4-6">>) -> true;
supports_context_variant(_) -> false.

-spec expand_claude_model_entries([map()]) -> [map()].
expand_claude_model_entries(Models) when is_list(Models) ->
lists:reverse(
lists:foldl(
fun expand_claude_model_entry/2,
[],
Models)).

-spec expand_claude_model_entry(map(), [map()]) -> [map()].
expand_claude_model_entry(Model, Acc) when is_map(Model) ->
case claude_model_entry_ids(Model) of
[] ->
[Model | Acc];
ModelIds ->
lists:foldl(
fun(ModelId, InnerAcc) ->
put_unique_claude_model_entry(clone_claude_model_entry(Model, ModelId), InnerAcc)
end,
Acc,
ModelIds)
end;
expand_claude_model_entry(Model, Acc) ->
[Model | Acc].

-spec put_unique_claude_model_entry(map(), [map()]) -> [map()].
put_unique_claude_model_entry(Model, Acc) ->
case claude_model_entry_id(Model) of
undefined ->
[Model | Acc];
ModelId ->
case lists:any(
fun(Existing) ->
claude_model_entry_id(Existing) =:= ModelId
end,
Acc) of
true -> Acc;
false -> [Model | Acc]
end
end.

-spec claude_model_entry_ids(map()) -> [binary()].
claude_model_entry_ids(Model) when is_map(Model) ->
case claude_model_entry_id(Model) of
undefined -> [];
ModelId -> discoverable_claude_model_ids(ModelId)
end.

-spec claude_model_entry_id(map()) -> binary() | undefined.
claude_model_entry_id(#{<<"modelId">> := ModelId}) when is_binary(ModelId) ->
ModelId;
claude_model_entry_id(#{modelId := ModelId}) when is_binary(ModelId) ->
ModelId;
claude_model_entry_id(#{model_id := ModelId}) when is_binary(ModelId) ->
ModelId;
claude_model_entry_id(_) ->
undefined.

-spec clone_claude_model_entry(map(), binary()) -> map().
clone_claude_model_entry(Model, ModelId) when is_map(Model), is_binary(ModelId) ->
Model1 =
case Model of
#{<<"modelId">> := _} -> Model#{<<"modelId">> => ModelId};
#{modelId := _} -> Model#{modelId => ModelId};
#{model_id := _} -> Model#{model_id => ModelId};
_ -> Model
end,
case Model1 of
#{<<"name">> := _} -> Model1#{<<"name">> => ModelId};
#{name := _} -> Model1#{name => ModelId};
_ -> Model1
end.

-spec context_variant_model_id(binary()) -> binary().
context_variant_model_id(BaseModelId) when is_binary(BaseModelId) ->
<<BaseModelId/binary, "[1m]">>.

-spec claude_model_entry(binary()) -> map().
claude_model_entry(ModelId) ->
#{<<"modelId">> => ModelId,
Expand Down
25 changes: 24 additions & 1 deletion src/backends/copilot/copilot_client.erl
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,17 @@ supported_models(Session) ->
%% Query the CLI via the native models.list RPC command. If the ACP
%% transport cannot answer, fall back to a direct non-interactive CLI
%% probe so model discovery still works.
case health(Session) of
ready ->
supported_models_from_native_or_prompt(Session);
active_query ->
supported_models_from_native_or_prompt(Session);
_ ->
prompt_or_init_models(Session)
end.

-spec supported_models_from_native_or_prompt(pid()) -> {ok, list()} | {error, term()}.
supported_models_from_native_or_prompt(Session) ->
case catch model_list(Session) of
{ok, #{<<"models">> := Models}} when is_list(Models) ->
{ok, Models};
Expand Down Expand Up @@ -1248,7 +1259,19 @@ model_entry(ModelId) ->
-spec run_model_cli(string(), [string(), ...]) ->
{ok, [string()]} | {error, term()}.
run_model_cli(Program, Args) ->
beam_agent_auth_core:run_capture_cli(Program, Args, 30000).
beam_agent_auth_core:run_capture_cli(Program, Args, 60000,
neutral_probe_run_opts()).

-spec neutral_probe_run_opts() -> map().
neutral_probe_run_opts() ->
case os:getenv("HOME") of
false ->
#{};
[] ->
#{};
Home ->
#{cwd => Home}
end.

%%====================================================================
%% beam_agent_adapter callbacks
Expand Down
70 changes: 43 additions & 27 deletions src/backends/opencode/opencode_client.erl
Original file line number Diff line number Diff line change
Expand Up @@ -184,10 +184,10 @@
{thread_realtime_append_text, 3},
{thread_realtime_stop, 2},
{review_start, 2},
{with_adapter_source, 2},
{maybe_include_thread_read, 4},
{discover_cli_models, 1},
{run_model_cli, 2}]}).
{with_adapter_source, 2},
{maybe_include_thread_read, 4},
{discover_cli_models, 1},
{run_model_cli, 3}]}).
-dialyzer({no_underspecs,
[{send_control, 3},
{fork_session, 2},
Expand Down Expand Up @@ -891,22 +891,30 @@ supported_commands(Session) ->
extract_init_field(Session, commands, slash_commands, []).
-spec supported_models(pid()) -> {ok, list()} | {error, term()}.
supported_models(Session) ->
%% Opencode does not populate models during init. Query the
%% provider endpoint which returns the full model catalog.
case provider_list(Session) of
{ok, #{<<"models">> := Models}} when is_list(Models) ->
%% Prefer the CLI-backed model catalog because it reflects the local
%% Opencode provider wiring even when the session transport is unhealthy
%% or the HTTP provider endpoint only exposes a partial view.
case cli_or_init_models(Session) of
{ok, Models} when is_list(Models), Models =/= [] ->
{ok, Models};
{ok, #{<<"providers">> := Provs}} when is_list(Provs) ->
%% Flatten models across all providers into a single list.
Models = lists:flatmap(
fun(#{<<"models">> := Ms}) when is_list(Ms) -> Ms;
(_) -> []
end, Provs),
{ok, Models};
{ok, _} ->
cli_or_init_models(Session);
{error, _} ->
cli_or_init_models(Session)
_ ->
%% Fall back to the provider endpoint when the CLI path does not
%% yield any models (for example if the binary is unavailable).
case provider_list(Session) of
{ok, #{<<"models">> := Models}} when is_list(Models) ->
{ok, Models};
{ok, #{<<"providers">> := Provs}} when is_list(Provs) ->
%% Flatten models across all providers into a single list.
Models = lists:flatmap(
fun(#{<<"models">> := Ms}) when is_list(Ms) -> Ms;
(_) -> []
end, Provs),
{ok, Models};
{ok, _} ->
extract_init_field(Session, models, models, []);
{error, _} ->
extract_init_field(Session, models, models, [])
end
end.
-spec supported_agents(pid()) -> {ok, list()} | {error, term()}.
supported_agents(Session) ->
Expand Down Expand Up @@ -1014,20 +1022,26 @@ cli_or_init_models(Session) ->
extract_init_field(Session, models, models, [])
end.

-spec session_cli_opts(pid()) -> #{cli_path := string() | binary()}.
-spec session_cli_opts(pid()) -> map().
session_cli_opts(Session) ->
case session_info(Session) of
{ok, Info} ->
#{cli_path => maps:get(cli_path, Info, "opencode")};
case maps:find(cli_path, Info) of
{ok, CliPath} ->
#{cli_path => CliPath};
error ->
#{}
end;
{error, _} ->
#{cli_path => "opencode"}
#{}
end.

-spec discover_cli_models(#{cli_path := string() | binary()}) ->
-spec discover_cli_models(map()) ->
{ok, [discovered_model()]} | {error, term()}.
discover_cli_models(Opts) when is_map(Opts) ->
Cli = beam_agent_auth_core:resolve_cli(opencode, Opts),
case run_model_cli(Cli, ["models"]) of
ExplicitCliPath = maps:is_key(cli_path, Opts),
case run_model_cli(Cli, ["models"], ExplicitCliPath) of
{ok, Lines} ->
{ok, parse_cli_model_lines(Lines)};
{error, _} = Err ->
Expand All @@ -1053,10 +1067,12 @@ model_entry(ModelId) ->
#{<<"modelId">> => ModelId,
<<"name">> => ModelId}.

-spec run_model_cli(string(), [string(), ...]) ->
-spec run_model_cli(string(), [string(), ...], boolean()) ->
{ok, [string()]} | {error, term()}.
run_model_cli(Program, Args) ->
beam_agent_auth_core:run_capture_cli(Program, Args, 10000).
run_model_cli(Program, Args, true) ->
beam_agent_auth_core:run_capture_cli(Program, Args, 60000);
run_model_cli(Program, Args, false) ->
beam_agent_auth_core:run_capture_login_shell(Program, Args, 60000).

-spec with_adapter_source(pid(), map()) -> session_view().
with_adapter_source(_Session, Result) ->
Expand Down
Loading