mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-21 06:12:21 +02:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
166a6e9cf6 | ||
|
|
43682d4e2e | ||
|
|
032967af4b | ||
|
|
0e03aea134 |
@@ -1204,6 +1204,41 @@ def _safe_env_prefix(ep: str | None) -> str | None:
|
||||
return f'[ -f "{path}" ] && source "{path}" || true'
|
||||
|
||||
|
||||
def _local_windows_bash_env_prefix(ep: str | None) -> str | None:
|
||||
"""Convert a frontend PowerShell venv prefix for the local Git Bash runner."""
|
||||
if not ep:
|
||||
return ep
|
||||
|
||||
prefix = ep.strip()
|
||||
if not prefix.startswith("&"):
|
||||
return ep
|
||||
|
||||
raw_path = prefix[1:].lstrip()
|
||||
if not raw_path:
|
||||
return ep
|
||||
if raw_path.startswith("'"):
|
||||
if len(raw_path) < 2 or not raw_path.endswith("'"):
|
||||
return ep
|
||||
quoted_path = raw_path[1:-1]
|
||||
if "'" in quoted_path.replace("''", ""):
|
||||
return ep
|
||||
path = quoted_path.replace("''", "'")
|
||||
else:
|
||||
path = raw_path.rstrip()
|
||||
if "'" in path or '"' in path:
|
||||
return ep
|
||||
if any(c in path for c in "\r\n;&|`$<>"):
|
||||
return ep
|
||||
if not path.replace("\\", "/").casefold().endswith("/scripts/activate.ps1"):
|
||||
return ep
|
||||
|
||||
bash_path = _git_bash_path(path)
|
||||
if "\\" in bash_path:
|
||||
return ep
|
||||
bash_path = bash_path[: -len("Activate.ps1")] + "activate"
|
||||
return "source " + shlex.quote(bash_path)
|
||||
|
||||
|
||||
def _ssh_ps(host, script_path, port=None):
|
||||
"""Build SSH command to run a PowerShell script on a Windows remote."""
|
||||
pf = f"-p {port} " if port and port != "22" else ""
|
||||
|
||||
@@ -50,7 +50,7 @@ from routes.cookbook_helpers import (
|
||||
_SESSION_ID_RE, _validate_repo_id, _validate_serve_model_id, _validate_include, _validate_token,
|
||||
_validate_local_dir, _validate_gpus, _shell_path,
|
||||
_ps_squote, _bash_squote, _validate_serve_cmd, _parse_serve_phase, OLLAMA_MISSING_HINT,
|
||||
_safe_env_prefix, _local_tooling_path_export, _append_serve_preflight_exit_lines,
|
||||
_safe_env_prefix, _local_windows_bash_env_prefix, _local_tooling_path_export, _append_serve_preflight_exit_lines,
|
||||
_append_serve_exit_code_lines, _append_llama_cpp_linux_accel_build_lines, _cached_model_scan_script,
|
||||
load_stored_hf_token,
|
||||
_append_vllm_linux_preflight_lines, _ollama_bind_from_cmd, _pip_install_fallback_chain,
|
||||
@@ -1336,7 +1336,7 @@ def setup_cookbook_routes() -> APIRouter:
|
||||
# Local: run hf download in the background (tmux on POSIX, a detached
|
||||
# process + logfile on Windows where tmux doesn't exist).
|
||||
if req.env_prefix:
|
||||
lines.append(_safe_env_prefix(req.env_prefix))
|
||||
lines.append(_safe_env_prefix(_local_windows_bash_env_prefix(req.env_prefix) if local_windows else req.env_prefix))
|
||||
else:
|
||||
lines.append("deactivate 2>/dev/null; hash -r")
|
||||
# Show whether the HF token reached this run (masked) — tells a gated
|
||||
@@ -2166,7 +2166,7 @@ def setup_cookbook_routes() -> APIRouter:
|
||||
if req.gpus:
|
||||
runner_lines.append(f"export CUDA_VISIBLE_DEVICES='{req.gpus}'")
|
||||
if req.env_prefix:
|
||||
runner_lines.append(_safe_env_prefix(req.env_prefix))
|
||||
runner_lines.append(_safe_env_prefix(_local_windows_bash_env_prefix(req.env_prefix) if local_windows else req.env_prefix))
|
||||
else:
|
||||
runner_lines.append("deactivate 2>/dev/null; hash -r")
|
||||
_append_venv_nvidia_library_path_lines(runner_lines, cmd=req.cmd)
|
||||
|
||||
@@ -1351,14 +1351,14 @@ def _legacy_visible_api_models(ep) -> List[str]:
|
||||
def _picker_models_for_endpoint(ep, base_url: str, kind: str):
|
||||
"""Return model IDs that should appear in the picker for an endpoint.
|
||||
|
||||
API providers expose remote inventory from /v1/models. Treat that cache as
|
||||
inventory, not approval: only manually pinned API models should appear in
|
||||
the picker. Local/self-hosted endpoints keep the older hide-list behavior.
|
||||
API providers expose remote inventory from /v1/models. Default to that
|
||||
visible inventory until an explicit pinned-model allow-list is saved.
|
||||
Local/self-hosted endpoints keep the older hide-list behavior.
|
||||
"""
|
||||
pinned = _normalize_model_ids(getattr(ep, "pinned_models", None))
|
||||
if _picker_requires_pinning(base_url, kind):
|
||||
if not _has_explicit_pinned_models(ep):
|
||||
pinned = _legacy_visible_api_models(ep) if _hidden_model_ids(ep) else []
|
||||
pinned = _legacy_visible_api_models(ep)
|
||||
return pinned, pinned
|
||||
return _visible_models(
|
||||
_cached_model_ids(ep),
|
||||
@@ -2342,9 +2342,7 @@ def setup_model_routes(model_discovery):
|
||||
else:
|
||||
response.headers["X-Model-Refresh-Status"] = "failed"
|
||||
response.headers["X-Model-Refresh-Warning"] = "Model refresh failed or returned no models; kept cached models."
|
||||
pinned = _normalize_model_ids(getattr(ep, "pinned_models", None))
|
||||
if picker_requires_pinning and not _has_explicit_pinned_models(ep):
|
||||
pinned = _legacy_visible_api_models(ep)
|
||||
_, pinned = _picker_models_for_endpoint(ep, base, kind)
|
||||
pinned_set = set(pinned)
|
||||
return [
|
||||
{
|
||||
|
||||
+11
-4
@@ -3081,10 +3081,17 @@ def _append_tool_results(
|
||||
messages.append(result_message)
|
||||
else:
|
||||
tool_output_text = "\n\n".join(tool_results)
|
||||
msg = {"role": "assistant", "content": round_response}
|
||||
if round_reasoning:
|
||||
msg["reasoning_content"] = round_reasoning
|
||||
messages.append(msg)
|
||||
# An approved-action replay injects the sealed tool result with no
|
||||
# assistant prose for that round, which used to append an assistant turn
|
||||
# whose content was "". Anthropic's Messages API rejects a non-final
|
||||
# assistant message with empty content (HTTP 400), so the resumed turn
|
||||
# died before the model saw the result. A turn carrying neither prose nor
|
||||
# reasoning has nothing to say to any provider, so skip it entirely.
|
||||
if round_response.strip() or round_reasoning:
|
||||
msg = {"role": "assistant", "content": round_response}
|
||||
if round_reasoning:
|
||||
msg["reasoning_content"] = round_reasoning
|
||||
messages.append(msg)
|
||||
# Tool output (shell/python stdout, file reads, fetched pages, email
|
||||
# bodies, MCP results) is sourced from outside the server. Wrap it as
|
||||
# untrusted data so prompt-injection inside a tool result is treated as
|
||||
|
||||
@@ -15,6 +15,7 @@ let _getPlatform;
|
||||
let _serverByVal;
|
||||
let _isWindows;
|
||||
let _buildEnvPrefix;
|
||||
let _psQuote;
|
||||
let _buildServeCmd;
|
||||
let _detectBackend;
|
||||
let _detectToolParser;
|
||||
@@ -538,7 +539,7 @@ export async function _runModelDownload(panel, model, backend, hostOverride) {
|
||||
if (srv.downloadDir) payload.local_dir = srv.downloadDir;
|
||||
if (isWin) {
|
||||
if (env === 'venv' && envPath) {
|
||||
payload.env_prefix = '& ' + (envPath.endsWith('\\Scripts\\Activate.ps1') ? envPath : envPath + '\\Scripts\\Activate.ps1');
|
||||
payload.env_prefix = '& ' + _psQuote(envPath.endsWith('\\Scripts\\Activate.ps1') ? envPath : envPath + '\\Scripts\\Activate.ps1');
|
||||
} else if (env === 'conda' && envPath) {
|
||||
payload.env_prefix = 'conda activate ' + envPath;
|
||||
}
|
||||
@@ -652,6 +653,7 @@ export function initDownload(shared) {
|
||||
_serverByVal = shared._serverByVal;
|
||||
_isWindows = shared._isWindows;
|
||||
_buildEnvPrefix = shared._buildEnvPrefix;
|
||||
_psQuote = shared._psQuote;
|
||||
_buildServeCmd = shared._buildServeCmd;
|
||||
_detectBackend = shared._detectBackend;
|
||||
_detectToolParser = shared._detectToolParser;
|
||||
|
||||
@@ -338,6 +338,7 @@ let _sshPrefix;
|
||||
let _getPlatform;
|
||||
let _isWindows;
|
||||
let _buildEnvPrefix;
|
||||
let _psQuote;
|
||||
let _loadPresets;
|
||||
let _savePresets;
|
||||
let _copyText;
|
||||
@@ -1971,7 +1972,7 @@ export async function _launchServeTask(shortName, repo, cmd, fields, hostOverrid
|
||||
let envPrefix = '';
|
||||
if (_isWindows()) {
|
||||
if (_envState.env === 'venv' && _envState.envPath) {
|
||||
envPrefix = '& ' + (_envState.envPath.endsWith('\\Scripts\\Activate.ps1') ? _envState.envPath : _envState.envPath + '\\Scripts\\Activate.ps1');
|
||||
envPrefix = '& ' + _psQuote(_envState.envPath.endsWith('\\Scripts\\Activate.ps1') ? _envState.envPath : _envState.envPath + '\\Scripts\\Activate.ps1');
|
||||
} else if (_envState.env === 'conda' && _envState.envPath) {
|
||||
envPrefix = 'conda activate ' + _envState.envPath;
|
||||
}
|
||||
@@ -4402,6 +4403,7 @@ export function initRunning(shared) {
|
||||
_getPlatform = shared._getPlatform;
|
||||
_isWindows = shared._isWindows;
|
||||
_buildEnvPrefix = shared._buildEnvPrefix;
|
||||
_psQuote = shared._psQuote;
|
||||
_loadPresets = shared._loadPresets;
|
||||
_savePresets = shared._savePresets;
|
||||
_copyText = shared._copyText;
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
"""Regression coverage for the assistant turn an approved-action replay appends.
|
||||
|
||||
Anthropic's Messages API rejects a non-final assistant message whose content is
|
||||
empty, so the resumed turn after a tool approval used to fail before the model
|
||||
ever saw the sealed result. The replay injects its result with no assistant
|
||||
prose for that round, which is the only path that produced such a turn.
|
||||
"""
|
||||
|
||||
import src.llm_core as llm_core
|
||||
from src.agent_loop import _append_tool_results
|
||||
|
||||
_RESULT = "bash: ok\nhello"
|
||||
_RECORD = {
|
||||
"tool_name": "bash",
|
||||
"content": "printf hello",
|
||||
"result": {"output": "hello", "exit_code": 0},
|
||||
"text": _RESULT,
|
||||
}
|
||||
|
||||
|
||||
def _replay_messages(round_response="", round_reasoning=""):
|
||||
"""Mirror the approved-action injection in stream_agent_loop."""
|
||||
messages = [
|
||||
{"role": "system", "content": "system preface"},
|
||||
{"role": "user", "content": "run the command and summarise it"},
|
||||
]
|
||||
_append_tool_results(
|
||||
messages,
|
||||
round_response,
|
||||
[],
|
||||
[_RESULT],
|
||||
[_RESULT],
|
||||
False,
|
||||
0,
|
||||
round_reasoning=round_reasoning,
|
||||
tool_result_records=[_RECORD],
|
||||
)
|
||||
return messages
|
||||
|
||||
|
||||
def _empty_assistant_turns(messages):
|
||||
return [
|
||||
index
|
||||
for index, message in enumerate(messages)
|
||||
if message.get("role") == "assistant"
|
||||
and not str(message.get("content") or "").strip()
|
||||
and not message.get("tool_calls")
|
||||
]
|
||||
|
||||
|
||||
def test_replay_appends_no_empty_assistant_turn():
|
||||
assert _empty_assistant_turns(_replay_messages()) == []
|
||||
|
||||
|
||||
def test_replay_payload_is_a_single_non_empty_user_turn_for_anthropic():
|
||||
# Asserting the whole sequence rather than scanning a slice: once the empty
|
||||
# spacer is gone the payload is one message, so a "no offenders in
|
||||
# chat[:-1]" check would pass without inspecting anything.
|
||||
sanitized = llm_core._sanitize_llm_messages(_replay_messages())
|
||||
payload = llm_core._build_anthropic_payload(
|
||||
"claude-sonnet-5", sanitized, 0.2, 512
|
||||
)
|
||||
chat = payload["messages"]
|
||||
assert [message["role"] for message in chat] == ["user"]
|
||||
assert all(str(message.get("content") or "").strip() for message in chat)
|
||||
|
||||
|
||||
def test_replay_merges_tool_output_after_the_request_with_its_fence_intact():
|
||||
# Dropping the empty assistant turn leaves two adjacent user messages, which
|
||||
# the sanitizer merges. Pin that shape: the tool output must still sit
|
||||
# behind its untrusted fence and must not precede the operator's request.
|
||||
sanitized = llm_core._sanitize_llm_messages(_replay_messages())
|
||||
user_turns = [m for m in sanitized if m.get("role") == "user"]
|
||||
assert len(user_turns) == 1
|
||||
merged = user_turns[0]["content"]
|
||||
assert merged.index("run the command") < merged.index("UNTRUSTED SOURCE DATA")
|
||||
assert merged.index("UNTRUSTED SOURCE DATA") < merged.index("hello")
|
||||
|
||||
|
||||
def test_a_round_with_prose_still_appends_its_assistant_turn():
|
||||
messages = _replay_messages(round_response="running that now")
|
||||
assistants = [m for m in messages if m.get("role") == "assistant"]
|
||||
assert [m["content"] for m in assistants] == ["running that now"]
|
||||
|
||||
|
||||
def test_reasoning_only_round_keeps_its_carrier_and_its_known_empty_content():
|
||||
"""Characterises the one empty-content turn this change deliberately leaves.
|
||||
|
||||
A round with reasoning and no prose still appends its carrier, and that
|
||||
carrier's content is still "", which Anthropic would still reject. Dropping
|
||||
it would lose the reasoning DeepSeek thinking mode requires on the next
|
||||
request, and the approval replay never takes this path because it passes no
|
||||
reasoning. Left alone on purpose; this test makes the gap visible instead of
|
||||
silent, and should be updated by whoever closes it.
|
||||
"""
|
||||
messages = _replay_messages(round_reasoning="thinking about it")
|
||||
assistants = [m for m in messages if m.get("role") == "assistant"]
|
||||
assert len(assistants) == 1
|
||||
assert assistants[0].get("reasoning_content") == "thinking about it"
|
||||
assert assistants[0]["content"] == ""
|
||||
@@ -16,6 +16,7 @@ from routes.cookbook_helpers import (
|
||||
_llama_cpp_rebuild_cmd,
|
||||
_append_vllm_linux_preflight_lines,
|
||||
_local_tooling_path_export,
|
||||
_local_windows_bash_env_prefix,
|
||||
_pip_install_attempt,
|
||||
_pip_install_fallback_chain,
|
||||
_ollama_bind_from_cmd,
|
||||
@@ -107,6 +108,70 @@ def test_safe_env_prefix_accepts_powershell_activation_path():
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("prefix", "expected"),
|
||||
[
|
||||
("& 'C:\\Users\\me\\venv\\Scripts\\Activate.ps1'", "source /c/Users/me/venv/Scripts/activate"),
|
||||
(r"& C:\Users\me\venv\Scripts\Activate.ps1", "source /c/Users/me/venv/Scripts/activate"),
|
||||
(
|
||||
r"& C:\Users\me\My Envs\venv\Scripts\Activate.ps1",
|
||||
"source '/c/Users/me/My Envs/venv/Scripts/activate'",
|
||||
),
|
||||
(
|
||||
"& 'C:\\Users\\me\\My Envs\\venv\\Scripts\\Activate.ps1'",
|
||||
"source '/c/Users/me/My Envs/venv/Scripts/activate'",
|
||||
),
|
||||
(r"& D:/Envs/venv/Scripts/Activate.ps1", "source /d/Envs/venv/Scripts/activate"),
|
||||
],
|
||||
)
|
||||
def test_local_windows_bash_env_prefix_converts_powershell_venv_activation(prefix, expected):
|
||||
converted = _local_windows_bash_env_prefix(prefix)
|
||||
|
||||
assert converted == expected
|
||||
assert _safe_env_prefix(converted).startswith('[ -f "')
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"prefix",
|
||||
[
|
||||
None,
|
||||
"",
|
||||
"source /home/me/venv/bin/activate",
|
||||
"conda activate qwen35",
|
||||
'eval "$(conda shell.bash hook)" && conda activate qwen35',
|
||||
r"& \\server\share\venv\Scripts\Activate.ps1",
|
||||
],
|
||||
)
|
||||
def test_local_windows_bash_env_prefix_leaves_other_prefixes_unchanged(prefix):
|
||||
assert _local_windows_bash_env_prefix(prefix) == prefix
|
||||
|
||||
|
||||
def test_local_windows_bash_env_prefix_handles_long_whitespace_input():
|
||||
prefix = "\t" * 100_000
|
||||
|
||||
assert _local_windows_bash_env_prefix(prefix) == prefix
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"relative_path",
|
||||
["static/js/cookbookRunning.js", "static/js/cookbookDownload.js"],
|
||||
)
|
||||
def test_primary_windows_venv_emitters_quote_activation_path(relative_path):
|
||||
source = (Path(__file__).resolve().parents[1] / relative_path).read_text(encoding="utf-8")
|
||||
|
||||
assert "'& ' + _psQuote(" in source
|
||||
assert "_psQuote = shared._psQuote;" in source
|
||||
|
||||
|
||||
def test_windows_venv_conversion_stays_scoped_to_local_git_bash_runners():
|
||||
source = (Path(__file__).resolve().parents[1] / "routes/cookbook_routes.py").read_text(encoding="utf-8")
|
||||
guarded_conversion = (
|
||||
"_local_windows_bash_env_prefix(req.env_prefix) if local_windows else req.env_prefix"
|
||||
)
|
||||
|
||||
assert source.count(guarded_conversion) == 2
|
||||
|
||||
|
||||
def test_validate_local_dir_accepts_external_drive_paths_with_spaces():
|
||||
path = "/Volumes/T7 2TB/AI Models/llamacpp"
|
||||
|
||||
|
||||
@@ -1107,6 +1107,47 @@ def test_get_api_models_marks_picker_as_pinned_only(monkeypatch):
|
||||
assert by_id["anthropic/claude-sonnet-4"]["is_pinned"] is False
|
||||
|
||||
|
||||
def test_get_fresh_api_models_marks_cached_inventory_as_pinned(monkeypatch):
|
||||
ep = _make_endpoint(
|
||||
base_url="https://api.example.test/v1",
|
||||
cached_models=json.dumps(["openai/gpt-image-1", "anthropic/claude-sonnet-4"]),
|
||||
pinned_models=None,
|
||||
)
|
||||
db = _PinnedFakeDb([ep])
|
||||
monkeypatch.setattr(model_routes, "SessionLocal", lambda: db)
|
||||
monkeypatch.setattr(model_routes, "require_admin", lambda request: None)
|
||||
endpoint = _get_route("/api/model-endpoints/{ep_id}/models", "GET")
|
||||
|
||||
result = endpoint("ep1", _PinnedFakeRequest(), SimpleNamespace(headers={}))
|
||||
|
||||
assert [row["id"] for row in result] == [
|
||||
"openai/gpt-image-1",
|
||||
"anthropic/claude-sonnet-4",
|
||||
]
|
||||
assert all(row["picker_requires_pinning"] is True for row in result)
|
||||
assert all(row["is_pinned"] is True for row in result)
|
||||
|
||||
|
||||
def test_get_legacy_api_models_preserves_hidden_selection_as_pinned(monkeypatch):
|
||||
ep = _make_endpoint(
|
||||
base_url="https://api.example.test/v1",
|
||||
cached_models=json.dumps(["m1", "m2", "m3"]),
|
||||
hidden_models=json.dumps(["m2"]),
|
||||
pinned_models=None,
|
||||
)
|
||||
db = _PinnedFakeDb([ep])
|
||||
monkeypatch.setattr(model_routes, "SessionLocal", lambda: db)
|
||||
monkeypatch.setattr(model_routes, "require_admin", lambda request: None)
|
||||
endpoint = _get_route("/api/model-endpoints/{ep_id}/models", "GET")
|
||||
|
||||
result = endpoint("ep1", _PinnedFakeRequest(), SimpleNamespace(headers={}))
|
||||
|
||||
by_id = {row["id"]: row for row in result}
|
||||
assert by_id["m1"]["is_pinned"] is True
|
||||
assert by_id["m2"]["is_pinned"] is False
|
||||
assert by_id["m3"]["is_pinned"] is True
|
||||
|
||||
|
||||
def test_reprobe_preserves_pinned_models(monkeypatch):
|
||||
ep = _make_endpoint(pinned_models=json.dumps(["deploy-1"]))
|
||||
db = _PinnedFakeDb([ep])
|
||||
@@ -1266,7 +1307,7 @@ def test_list_model_endpoints_returns_key_fingerprint(monkeypatch):
|
||||
assert result[1]["api_key_fingerprint"] == ""
|
||||
|
||||
|
||||
def test_list_api_endpoint_reports_inventory_count_when_none_pinned(monkeypatch):
|
||||
def test_list_api_endpoint_defaults_inventory_to_visible_when_none_pinned(monkeypatch):
|
||||
ep = _make_endpoint(
|
||||
base_url="https://api.example.test/v1",
|
||||
cached_models=json.dumps(["openai/gpt-image-1", "anthropic/claude-sonnet-4"]),
|
||||
@@ -1279,12 +1320,31 @@ def test_list_api_endpoint_reports_inventory_count_when_none_pinned(monkeypatch)
|
||||
|
||||
result = endpoint(_PinnedFakeRequest())
|
||||
|
||||
assert result[0]["models"] == []
|
||||
assert result[0]["models"] == [
|
||||
"openai/gpt-image-1",
|
||||
"anthropic/claude-sonnet-4",
|
||||
]
|
||||
assert result[0]["pinned_models"] == result[0]["models"]
|
||||
assert result[0]["model_count"] == 2
|
||||
assert result[0]["picker_requires_pinning"] is True
|
||||
assert result[0]["status"] == "online"
|
||||
|
||||
|
||||
def test_api_picker_preserves_explicit_empty_selection():
|
||||
ep = _make_endpoint(
|
||||
base_url="https://api.example.test/v1",
|
||||
cached_models=json.dumps(["openai/gpt-image-1", "anthropic/claude-sonnet-4"]),
|
||||
pinned_models=json.dumps([]),
|
||||
)
|
||||
|
||||
models, pinned = model_routes._picker_models_for_endpoint(
|
||||
ep, ep.base_url, ep.endpoint_kind
|
||||
)
|
||||
|
||||
assert models == []
|
||||
assert pinned == []
|
||||
|
||||
|
||||
def test_list_api_endpoint_returns_pinned_picker_models(monkeypatch):
|
||||
ep = _make_endpoint(
|
||||
base_url="https://api.example.test/v1",
|
||||
@@ -1767,7 +1827,7 @@ def test_api_models_openrouter_uses_pinned_models_not_hidden(monkeypatch):
|
||||
assert result["items"][0]["models"] == ["openai/gpt-image-1"]
|
||||
|
||||
|
||||
def test_api_models_openrouter_derives_legacy_visible_models(monkeypatch):
|
||||
def test_api_models_openrouter_defaults_cached_models_visible(monkeypatch):
|
||||
row = _route_ep(
|
||||
"openrouter",
|
||||
"https://openrouter.ai/api/v1",
|
||||
@@ -1775,7 +1835,6 @@ def test_api_models_openrouter_derives_legacy_visible_models(monkeypatch):
|
||||
pinned_models=None,
|
||||
api_key="fake-key",
|
||||
)
|
||||
row.hidden_models = json.dumps(["m2"])
|
||||
db = _RouteDb([row])
|
||||
router = model_routes.setup_model_routes(model_discovery=None)
|
||||
|
||||
@@ -1788,7 +1847,7 @@ def test_api_models_openrouter_derives_legacy_visible_models(monkeypatch):
|
||||
result = _route_endpoint(router, "/api/models")(_route_request())
|
||||
|
||||
assert result["items"][0]["endpoint_name"] == "openrouter"
|
||||
assert result["items"][0]["models"] == ["m1", "m3"]
|
||||
assert result["items"][0]["models"] == ["m1", "m2", "m3"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
Reference in New Issue
Block a user