feat: more on step 6

This commit is contained in:
2026-04-28 22:03:29 +02:00
parent 0929a4e798
commit 92ae2b601c
5 changed files with 227 additions and 24 deletions

View File

@@ -318,14 +318,7 @@ defmodule BDS.AI do
end
def available_chat_models(current_model \\ nil) do
endpoint_models =
[:online, :airplane]
|> Enum.flat_map(fn kind ->
case get_endpoint(kind) do
{:ok, %{model: model}} when is_binary(model) and model != "" -> [model]
_other -> []
end
end)
endpoint_models = configured_chat_models()
preference_models =
[:chat, :airplane_chat]
@@ -336,10 +329,19 @@ defmodule BDS.AI do
end
end)
[current_model | endpoint_models ++ preference_models]
provider_names = catalog_provider_name_map()
endpoint_provider_map = Map.new(endpoint_models, &{&1.id, &1.provider})
[current_model | Enum.map(endpoint_models, & &1.id) ++ preference_models]
|> Enum.filter(&(is_binary(&1) and String.trim(&1) != ""))
|> Enum.uniq()
|> Enum.map(&%{id: &1, name: &1})
|> Enum.map(&build_available_chat_model(&1, endpoint_provider_map, provider_names))
|> Enum.sort_by(fn model ->
{
String.downcase(to_string(model.provider_name || model.provider || "")),
String.downcase(to_string(model.name || model.id))
}
end)
end
def set_conversation_model(conversation_id, model_id)
@@ -479,6 +481,81 @@ defmodule BDS.AI do
}
end
defp configured_chat_models do
[:online, :airplane]
|> Enum.flat_map(fn kind ->
case get_endpoint(kind) do
{:ok, %{model: model, url: url}} when is_binary(model) and model != "" ->
[%{id: model, provider: infer_endpoint_provider(kind, url)}]
_other ->
[]
end
end)
end
defp build_available_chat_model(model_id, endpoint_provider_map, provider_names) do
case get_catalog_model(model_id) do
{:ok, model} ->
provider = model.provider || Map.get(endpoint_provider_map, model_id, "other")
%{
id: model.model_id,
name: model.name || model.model_id,
provider: provider,
provider_name: Map.get(provider_names, provider, fallback_provider_name(provider)),
context_window: model.context_window,
max_output_tokens: model.max_output_tokens
}
{:error, :not_found} ->
provider = Map.get(endpoint_provider_map, model_id, "other")
%{
id: model_id,
name: model_id,
provider: provider,
provider_name: Map.get(provider_names, provider, fallback_provider_name(provider)),
context_window: nil,
max_output_tokens: nil
}
end
end
defp catalog_provider_name_map do
Repo.all(from provider in CatalogProvider, select: {provider.id, provider.name})
|> Map.new()
end
defp infer_endpoint_provider(:online, _url), do: "generic-openai"
defp infer_endpoint_provider(:airplane, url) when is_binary(url) do
normalized_url = String.downcase(url)
cond do
String.contains?(normalized_url, "11434") or String.contains?(normalized_url, "ollama") -> "ollama"
String.contains?(normalized_url, "1234") or String.contains?(normalized_url, "lmstudio") -> "lmstudio"
true -> "generic-openai"
end
end
defp infer_endpoint_provider(:airplane, _url), do: "generic-openai"
defp fallback_provider_name("generic-openai"), do: "Generic OpenAI"
defp fallback_provider_name("lmstudio"), do: "LM Studio"
defp fallback_provider_name("mistral"), do: "Mistral"
defp fallback_provider_name("ollama"), do: "Ollama"
defp fallback_provider_name("openai"), do: "OpenAI"
defp fallback_provider_name(provider) when is_binary(provider) and provider != "" do
provider
|> String.split(["-", "_"], trim: true)
|> Enum.map(&String.capitalize/1)
|> Enum.join(" ")
end
defp fallback_provider_name(_provider), do: "Other"
defp run_one_shot(operation, payload, opts, formatter) do
runtime = Keyword.get(opts, :runtime, OpenAICompatibleRuntime)

View File

@@ -201,12 +201,14 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
%ChatConversation{} = conversation ->
messages = AI.list_chat_messages(conversation.id)
request = Map.get(assigns.chat_editor_requests, conversation.id)
available_models = AI.available_chat_models(conversation.model)
%{
id: conversation.id,
title: conversation.title || translated("chat.newChat"),
model: conversation.model,
available_models: AI.available_chat_models(conversation.model),
available_models: available_models,
available_model_groups: group_available_models(available_models),
model_selector_open?: Map.get(assigns.chat_model_selectors_open, conversation.id, false),
input: Map.get(assigns.chat_editor_inputs, conversation.id, ""),
messages: build_entries(messages, assigns),
@@ -250,6 +252,24 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
def markdown_html(_content), do: ""
defp group_available_models(models) when is_list(models) do
models
|> Enum.group_by(&Map.get(&1, :provider, "other"))
|> Enum.map(fn {provider, entries} ->
%{
provider: provider,
label: provider_group_label(entries, provider),
models: Enum.sort_by(entries, &String.downcase(to_string(Map.get(&1, :name) || Map.get(&1, :id))))
}
end)
|> Enum.sort_by(&String.downcase(to_string(&1.label)))
end
defp provider_group_label([%{provider_name: name} | _entries], _provider) when is_binary(name) and name != "",
do: name
defp provider_group_label(_entries, provider) when is_binary(provider), do: provider
def payload_json(nil), do: "{}"
def payload_json(payload) when is_map(payload), do: Jason.encode!(payload)

View File

@@ -22,18 +22,28 @@
<%= if @chat_editor.model_selector_open? and @chat_editor.available_models != [] do %>
<div class="chat-model-selector-menu">
<%= for model <- @chat_editor.available_models do %>
<button
class={[
"chat-model-selector-option",
if(model.id == @chat_editor.model, do: "active")
]}
type="button"
phx-click="select_chat_model"
phx-value-model={model.id}
>
<%= model.name %>
</button>
<%= for group <- @chat_editor.available_model_groups do %>
<section class="chat-model-provider-group" data-testid="chat-model-provider-group" data-provider={group.provider}>
<%= if length(@chat_editor.available_model_groups) > 1 do %>
<div class="chat-model-provider-header"><%= group.label %></div>
<% end %>
<%= for model <- group.models do %>
<button
class={[
"chat-model-selector-option",
if(model.id == @chat_editor.model, do: "active")
]}
type="button"
phx-click="select_chat_model"
phx-value-model={model.id}
data-testid="chat-model-selector-option"
data-provider={group.provider}
>
<span class="chat-model-selector-option-name"><%= model.name || model.id %></span>
</button>
<% end %>
</section>
<% end %>
</div>
<% end %>