fix: fix airplane mode for AI usage and qwen 3.6 one-shot parsing

This commit is contained in:
2026-06-11 22:28:44 +02:00
parent d8b24c9b72
commit 8546080a3d
12 changed files with 269 additions and 24 deletions

View File

@@ -3,7 +3,6 @@ defmodule BDS.AITest do
import ExUnit.CaptureLog
import Ecto.Query
require Logger
alias BDS.Media.Media
alias BDS.Persistence
@@ -366,6 +365,31 @@ defmodule BDS.AITest do
assert Repo.get(Setting, "__encrypted_ai.online.api_key") == nil
end
test "airplane_endpoint_configured? reflects the airplane endpoint url and model" do
refute BDS.AI.airplane_endpoint_configured?()
assert {:ok, _endpoint} =
BDS.AI.put_endpoint(
:airplane,
%{url: "http://localhost:11434/v1", api_key: nil, model: ""},
secret_backend: FakeSecretBackend
)
refute BDS.AI.airplane_endpoint_configured?()
assert {:ok, _endpoint} =
BDS.AI.put_endpoint(
:airplane,
%{url: "http://localhost:11434/v1", api_key: nil, model: "llama3.3"},
secret_backend: FakeSecretBackend
)
assert BDS.AI.airplane_endpoint_configured?()
assert :ok = BDS.AI.delete_endpoint(:airplane)
refute BDS.AI.airplane_endpoint_configured?()
end
test "refresh_model_catalog stores providers, models, modalities, and etag metadata" do
assert {:ok, result} =
BDS.AI.refresh_model_catalog(http_client: FakeHttpClient)
@@ -611,6 +635,95 @@ defmodule BDS.AITest do
assert log =~ "This is not valid JSON"
end
test "analyze_image accepts JSON wrapped in markdown fences from local models" do
assert {:ok, _endpoint} =
BDS.AI.put_endpoint(
:airplane,
%{
url: "http://localhost:11434/v1",
api_key: nil,
model: "llama-default"
},
secret_backend: FakeSecretBackend
)
assert :ok = BDS.AI.set_airplane_mode(true)
assert :ok = BDS.AI.put_model_preference(:airplane_image_analysis, "qwen-vision")
assert :ok =
BDS.AI.put_model_capabilities("qwen-vision", %{
supports_attachment: true,
supports_tool_calls: false,
disables_reasoning: false
})
defmodule FencedJsonContentRuntime do
def generate(_endpoint, _request, _opts) do
content = """
```json
{
"title": "Ahornblätter im Herbstlicht",
"alt": "Nahaufnahme von Ahornblättern",
"caption": "Einige Ahornblätter verfärben sich."
}
```
"""
{:ok,
%{
content: content,
json: nil,
tool_calls: [],
usage: %{input_tokens: 4, output_tokens: 2}
}}
end
end
assert {:ok, result} =
BDS.AI.analyze_image(
%{
mime_type: "image/png",
image_url: "data:image/png;base64,abc123"
},
runtime: FencedJsonContentRuntime,
test_pid: self(),
secret_backend: FakeSecretBackend
)
assert result.title == "Ahornblätter im Herbstlicht"
assert result.alt == "Nahaufnahme von Ahornblättern"
assert result.caption == "Einige Ahornblätter verfärben sich."
end
test "one-shot system prompts demand raw JSON without markdown fences" do
assert {:ok, _endpoint} =
BDS.AI.put_endpoint(
:airplane,
%{
url: "http://localhost:11434/v1",
api_key: nil,
model: "llama-default"
},
secret_backend: FakeSecretBackend
)
assert :ok = BDS.AI.set_airplane_mode(true)
assert {:ok, _result} =
BDS.AI.analyze_post(
%{title: "Title", excerpt: "Excerpt", content: "Content"},
runtime: FakeRuntime,
test_pid: self(),
secret_backend: FakeSecretBackend
)
assert_received {:runtime_request, _endpoint, request}
system_content = get_in(request.messages, [Access.at(0), "content"])
assert system_content =~ "raw JSON only"
assert system_content =~ "without markdown code fences"
end
test "airplane mode routes title tasks to airplane endpoint and offline title model" do
assert {:ok, _endpoint} =
BDS.AI.put_endpoint(

View File

@@ -3230,6 +3230,129 @@ defmodule BDS.Desktop.ShellLiveTest do
assert html =~ "Automatic AI actions stay gated by airplane mode"
end
test "ai suggestions overlay uses the local model in airplane mode for media", %{
project: project
} do
Application.put_env(:bds, :test_pid, self())
server =
start_supervised!({Bandit, plug: AiSuggestionsServer, port: 0, startup_log: false})
{:ok, {_address, port}} = ThousandIsland.listener_info(server)
assert :ok = AI.set_airplane_mode(true)
assert {:ok, _endpoint} =
AI.put_endpoint(:airplane, %{
url: "http://127.0.0.1:#{port}/v1",
api_key: nil,
model: "llava-local"
})
assert :ok = AI.put_model_preference(:airplane_image_analysis, "llava-local")
assert :ok = AI.put_model_capabilities("llava-local", %{supports_attachment: true})
temp_dir =
Path.join(System.tmp_dir!(), "bds-shell-live-#{System.unique_integer([:positive])}")
File.mkdir_p!(temp_dir)
media_source_path = Path.join(temp_dir, "airplane-media.jpg")
File.write!(media_source_path, "fake image body")
{:ok, media} =
Media.import_media(%{
project_id: project.id,
source_path: media_source_path,
title: "Airplane Media"
})
{:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
html =
render_click(view, "pin_sidebar_item", %{
"route" => "media",
"id" => media.id,
"title" => media.title,
"subtitle" => "draft"
})
assert html =~ ~s(data-testid="media-editor")
html =
view
|> element("[data-testid='media-editor'] .quick-actions-btn")
|> render_click()
assert html =~ "quick-actions-menu"
html =
view
|> element("[phx-click='open_overlay'][phx-value-kind='ai_suggestions']")
|> render_click()
assert html =~ "ai-suggestions-modal"
assert_receive {:ai_suggestions_request, request}, 2_000
assert request["model"] == "llava-local"
Process.sleep(200)
html = render(view)
assert html =~ "AI Image Title"
assert html =~ "AI Alt Text"
assert html =~ "AI Caption"
end
test "chat editor sends messages to the local model in airplane mode" do
Application.put_env(:bds, :test_pid, self())
server =
start_supervised!({Bandit, plug: TitleChatServer, port: 0, startup_log: false})
{:ok, {_address, port}} = ThousandIsland.listener_info(server)
assert :ok = AI.set_airplane_mode(true)
assert {:ok, _endpoint} =
AI.put_endpoint(:airplane, %{
url: "http://127.0.0.1:#{port}/v1",
api_key: nil,
model: "llama-local"
})
assert {:ok, conversation} = AI.start_chat(%{title: "New Chat"})
{:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
html =
render_click(view, "pin_sidebar_item", %{
"route" => "chat",
"id" => conversation.id,
"title" => conversation.title,
"subtitle" => "chat"
})
assert html =~ ~s(data-testid="chat-send-button")
_html =
view
|> element(".chat-input-wrapper")
|> render_change(%{"message" => "Beschreibe das neueste Bild"})
_html =
view
|> element("[data-testid='chat-send-button']")
|> render_click()
assert_receive {:title_chat_request, request}, 2_000
assert request["model"] == "llama-local"
Process.sleep(350)
html = render(view)
assert html =~ "Ich habe die Posts pro Monat ermittelt."
end
test "ai suggestions overlay fetches async results for media when online", %{project: project} do
Application.put_env(:bds, :test_pid, self())