fix(streaming): SSE heartbeat alignment, repair grace period, local-server model id preservation (#1623, #1624, #1625)
Closes #1623 — Lower SSE app heartbeat from 30s to 5s at every long-lived
handler (main agent, terminal, gateway-watcher, approval-poller, clarify-poller).
Kernel TCP keepalive declares peer dead at 25s worst-case (10s KEEPIDLE +
5s KEEPINTVL * 3 KEEPCNT, added v0.50.289 #1581). 30s app heartbeat let the
kernel tear sockets down on flaky networks before the app sent its first
keepalive byte — drops at ~10s during long thinking phases. New named
constant _SSE_HEARTBEAT_INTERVAL_SECONDS=5; regression test pins the
inequality (app_heartbeat * 2 <= kernel_window) so future tuning can't
re-introduce the misalignment.
Closes #1624 — Add 30s grace period to _repair_stale_pending() trigger.
Without it, any narrow race between the streaming thread clearing
pending_user_message and STREAMS.pop(stream_id) produces a false-positive
'Previous turn did not complete.' marker on a turn that finished correctly
(reproducible after every command-approval turn). Defense-in-depth, not
the root-cause fix — the actual streaming-thread leak path is tracked
separately. Falsy pending_started_at (legacy sidecars) treated as
'old enough' so legitimate legacy-data recovery still works. Plus
logger.warning telemetry on every legitimate repair so the next batch of
user reports tells us whether the underlying race still fires.
Closes #1625 — Local model servers (LM Studio, Ollama, llama.cpp, vLLM,
TabbyAPI, koboldcpp, textgen-webui) now keep the full HuggingFace-style
model id (e.g. 'qwen/qwen3.6-27b' instead of stripped 'qwen3.6-27b'). New
_LOCAL_SERVER_PROVIDERS set + _base_url_points_at_local_server() loopback/
RFC1918 heuristic — either signal triggers no-strip. Backward compat
preserved for OpenAI-compatible proxies on public hosts (LiteLLM at
litellm.example.com still strips openai/gpt-5.4 -> gpt-5.4). Updated the
existing #230/#433 test to reflect that #1625 supersedes the strip-on-custom
rule for loopback hosts (see api/config.py and test_model_resolver.py
docstring update). Reported by @akarichan8231 in Discord on 2026-05-04.
42 regression tests across:
tests/test_issue1623_sse_heartbeat_alignment.py (3)
tests/test_issue1624_repair_stale_pending_grace.py (9)
tests/test_issue1625_local_server_model_id_preservation.py (30)
4142 -> 4184 passing. 0 regressions.
2026-05-04 16:31:32 +00:00
|
|
|
"""Tests for #1625: resolve_model_provider must NOT strip provider prefix on local servers.
|
|
|
|
|
|
|
|
|
|
Bug shape: with `model.provider: lmstudio`, `model.base_url: http://localhost:1234/v1`,
|
|
|
|
|
`model.default: qwen/qwen3.6-27b`, resolve_model_provider() stripped the
|
|
|
|
|
"qwen/" prefix because "qwen" matches an entry in _PROVIDER_MODELS — sending
|
|
|
|
|
the request to LM Studio with model name "qwen3.6-27b". LM Studio (and Ollama,
|
|
|
|
|
llama.cpp, vLLM, TabbyAPI) register models under their full HuggingFace-style
|
|
|
|
|
id, so the stripped name didn't match the loaded model and a fresh instance
|
|
|
|
|
loaded with default settings, ignoring the user's tuned context length /
|
|
|
|
|
parallel slots.
|
|
|
|
|
|
|
|
|
|
Fix: explicit no-strip path for known local-server providers AND any
|
|
|
|
|
base_url pointing at a loopback/private host. OpenAI-compatible proxies
|
|
|
|
|
(LiteLLM, OpenRouter relays) on public URLs continue to get prefix-stripping.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
from api import config as cfg_mod
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── Helpers ───────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
2026-05-08 00:41:55 +00:00
|
|
|
def _patch_cfg(monkeypatch, custom_providers=None, **model_overrides):
|
fix(streaming): SSE heartbeat alignment, repair grace period, local-server model id preservation (#1623, #1624, #1625)
Closes #1623 — Lower SSE app heartbeat from 30s to 5s at every long-lived
handler (main agent, terminal, gateway-watcher, approval-poller, clarify-poller).
Kernel TCP keepalive declares peer dead at 25s worst-case (10s KEEPIDLE +
5s KEEPINTVL * 3 KEEPCNT, added v0.50.289 #1581). 30s app heartbeat let the
kernel tear sockets down on flaky networks before the app sent its first
keepalive byte — drops at ~10s during long thinking phases. New named
constant _SSE_HEARTBEAT_INTERVAL_SECONDS=5; regression test pins the
inequality (app_heartbeat * 2 <= kernel_window) so future tuning can't
re-introduce the misalignment.
Closes #1624 — Add 30s grace period to _repair_stale_pending() trigger.
Without it, any narrow race between the streaming thread clearing
pending_user_message and STREAMS.pop(stream_id) produces a false-positive
'Previous turn did not complete.' marker on a turn that finished correctly
(reproducible after every command-approval turn). Defense-in-depth, not
the root-cause fix — the actual streaming-thread leak path is tracked
separately. Falsy pending_started_at (legacy sidecars) treated as
'old enough' so legitimate legacy-data recovery still works. Plus
logger.warning telemetry on every legitimate repair so the next batch of
user reports tells us whether the underlying race still fires.
Closes #1625 — Local model servers (LM Studio, Ollama, llama.cpp, vLLM,
TabbyAPI, koboldcpp, textgen-webui) now keep the full HuggingFace-style
model id (e.g. 'qwen/qwen3.6-27b' instead of stripped 'qwen3.6-27b'). New
_LOCAL_SERVER_PROVIDERS set + _base_url_points_at_local_server() loopback/
RFC1918 heuristic — either signal triggers no-strip. Backward compat
preserved for OpenAI-compatible proxies on public hosts (LiteLLM at
litellm.example.com still strips openai/gpt-5.4 -> gpt-5.4). Updated the
existing #230/#433 test to reflect that #1625 supersedes the strip-on-custom
rule for loopback hosts (see api/config.py and test_model_resolver.py
docstring update). Reported by @akarichan8231 in Discord on 2026-05-04.
42 regression tests across:
tests/test_issue1623_sse_heartbeat_alignment.py (3)
tests/test_issue1624_repair_stale_pending_grace.py (9)
tests/test_issue1625_local_server_model_id_preservation.py (30)
4142 -> 4184 passing. 0 regressions.
2026-05-04 16:31:32 +00:00
|
|
|
"""Patch api.config.cfg to a synthetic config dict for the duration of a test."""
|
2026-05-08 00:41:55 +00:00
|
|
|
fake_cfg = {
|
|
|
|
|
"model": dict(model_overrides),
|
|
|
|
|
"custom_providers": list(custom_providers or []),
|
|
|
|
|
}
|
fix(streaming): SSE heartbeat alignment, repair grace period, local-server model id preservation (#1623, #1624, #1625)
Closes #1623 — Lower SSE app heartbeat from 30s to 5s at every long-lived
handler (main agent, terminal, gateway-watcher, approval-poller, clarify-poller).
Kernel TCP keepalive declares peer dead at 25s worst-case (10s KEEPIDLE +
5s KEEPINTVL * 3 KEEPCNT, added v0.50.289 #1581). 30s app heartbeat let the
kernel tear sockets down on flaky networks before the app sent its first
keepalive byte — drops at ~10s during long thinking phases. New named
constant _SSE_HEARTBEAT_INTERVAL_SECONDS=5; regression test pins the
inequality (app_heartbeat * 2 <= kernel_window) so future tuning can't
re-introduce the misalignment.
Closes #1624 — Add 30s grace period to _repair_stale_pending() trigger.
Without it, any narrow race between the streaming thread clearing
pending_user_message and STREAMS.pop(stream_id) produces a false-positive
'Previous turn did not complete.' marker on a turn that finished correctly
(reproducible after every command-approval turn). Defense-in-depth, not
the root-cause fix — the actual streaming-thread leak path is tracked
separately. Falsy pending_started_at (legacy sidecars) treated as
'old enough' so legitimate legacy-data recovery still works. Plus
logger.warning telemetry on every legitimate repair so the next batch of
user reports tells us whether the underlying race still fires.
Closes #1625 — Local model servers (LM Studio, Ollama, llama.cpp, vLLM,
TabbyAPI, koboldcpp, textgen-webui) now keep the full HuggingFace-style
model id (e.g. 'qwen/qwen3.6-27b' instead of stripped 'qwen3.6-27b'). New
_LOCAL_SERVER_PROVIDERS set + _base_url_points_at_local_server() loopback/
RFC1918 heuristic — either signal triggers no-strip. Backward compat
preserved for OpenAI-compatible proxies on public hosts (LiteLLM at
litellm.example.com still strips openai/gpt-5.4 -> gpt-5.4). Updated the
existing #230/#433 test to reflect that #1625 supersedes the strip-on-custom
rule for loopback hosts (see api/config.py and test_model_resolver.py
docstring update). Reported by @akarichan8231 in Discord on 2026-05-04.
42 regression tests across:
tests/test_issue1623_sse_heartbeat_alignment.py (3)
tests/test_issue1624_repair_stale_pending_grace.py (9)
tests/test_issue1625_local_server_model_id_preservation.py (30)
4142 -> 4184 passing. 0 regressions.
2026-05-04 16:31:32 +00:00
|
|
|
monkeypatch.setattr(cfg_mod, "cfg", fake_cfg)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── Local-server providers preserve full model id ──────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("provider_name", [
|
|
|
|
|
"lmstudio",
|
2026-05-04 16:48:22 +00:00
|
|
|
"lm-studio", # Opus pre-release NIT
|
fix(streaming): SSE heartbeat alignment, repair grace period, local-server model id preservation (#1623, #1624, #1625)
Closes #1623 — Lower SSE app heartbeat from 30s to 5s at every long-lived
handler (main agent, terminal, gateway-watcher, approval-poller, clarify-poller).
Kernel TCP keepalive declares peer dead at 25s worst-case (10s KEEPIDLE +
5s KEEPINTVL * 3 KEEPCNT, added v0.50.289 #1581). 30s app heartbeat let the
kernel tear sockets down on flaky networks before the app sent its first
keepalive byte — drops at ~10s during long thinking phases. New named
constant _SSE_HEARTBEAT_INTERVAL_SECONDS=5; regression test pins the
inequality (app_heartbeat * 2 <= kernel_window) so future tuning can't
re-introduce the misalignment.
Closes #1624 — Add 30s grace period to _repair_stale_pending() trigger.
Without it, any narrow race between the streaming thread clearing
pending_user_message and STREAMS.pop(stream_id) produces a false-positive
'Previous turn did not complete.' marker on a turn that finished correctly
(reproducible after every command-approval turn). Defense-in-depth, not
the root-cause fix — the actual streaming-thread leak path is tracked
separately. Falsy pending_started_at (legacy sidecars) treated as
'old enough' so legitimate legacy-data recovery still works. Plus
logger.warning telemetry on every legitimate repair so the next batch of
user reports tells us whether the underlying race still fires.
Closes #1625 — Local model servers (LM Studio, Ollama, llama.cpp, vLLM,
TabbyAPI, koboldcpp, textgen-webui) now keep the full HuggingFace-style
model id (e.g. 'qwen/qwen3.6-27b' instead of stripped 'qwen3.6-27b'). New
_LOCAL_SERVER_PROVIDERS set + _base_url_points_at_local_server() loopback/
RFC1918 heuristic — either signal triggers no-strip. Backward compat
preserved for OpenAI-compatible proxies on public hosts (LiteLLM at
litellm.example.com still strips openai/gpt-5.4 -> gpt-5.4). Updated the
existing #230/#433 test to reflect that #1625 supersedes the strip-on-custom
rule for loopback hosts (see api/config.py and test_model_resolver.py
docstring update). Reported by @akarichan8231 in Discord on 2026-05-04.
42 regression tests across:
tests/test_issue1623_sse_heartbeat_alignment.py (3)
tests/test_issue1624_repair_stale_pending_grace.py (9)
tests/test_issue1625_local_server_model_id_preservation.py (30)
4142 -> 4184 passing. 0 regressions.
2026-05-04 16:31:32 +00:00
|
|
|
"ollama",
|
|
|
|
|
"llamacpp",
|
|
|
|
|
"llama-cpp",
|
|
|
|
|
"vllm",
|
|
|
|
|
"tabby",
|
|
|
|
|
"tabbyapi",
|
2026-05-04 16:48:22 +00:00
|
|
|
"koboldcpp",
|
|
|
|
|
"textgen",
|
|
|
|
|
"localai", # Opus pre-release NIT
|
fix(streaming): SSE heartbeat alignment, repair grace period, local-server model id preservation (#1623, #1624, #1625)
Closes #1623 — Lower SSE app heartbeat from 30s to 5s at every long-lived
handler (main agent, terminal, gateway-watcher, approval-poller, clarify-poller).
Kernel TCP keepalive declares peer dead at 25s worst-case (10s KEEPIDLE +
5s KEEPINTVL * 3 KEEPCNT, added v0.50.289 #1581). 30s app heartbeat let the
kernel tear sockets down on flaky networks before the app sent its first
keepalive byte — drops at ~10s during long thinking phases. New named
constant _SSE_HEARTBEAT_INTERVAL_SECONDS=5; regression test pins the
inequality (app_heartbeat * 2 <= kernel_window) so future tuning can't
re-introduce the misalignment.
Closes #1624 — Add 30s grace period to _repair_stale_pending() trigger.
Without it, any narrow race between the streaming thread clearing
pending_user_message and STREAMS.pop(stream_id) produces a false-positive
'Previous turn did not complete.' marker on a turn that finished correctly
(reproducible after every command-approval turn). Defense-in-depth, not
the root-cause fix — the actual streaming-thread leak path is tracked
separately. Falsy pending_started_at (legacy sidecars) treated as
'old enough' so legitimate legacy-data recovery still works. Plus
logger.warning telemetry on every legitimate repair so the next batch of
user reports tells us whether the underlying race still fires.
Closes #1625 — Local model servers (LM Studio, Ollama, llama.cpp, vLLM,
TabbyAPI, koboldcpp, textgen-webui) now keep the full HuggingFace-style
model id (e.g. 'qwen/qwen3.6-27b' instead of stripped 'qwen3.6-27b'). New
_LOCAL_SERVER_PROVIDERS set + _base_url_points_at_local_server() loopback/
RFC1918 heuristic — either signal triggers no-strip. Backward compat
preserved for OpenAI-compatible proxies on public hosts (LiteLLM at
litellm.example.com still strips openai/gpt-5.4 -> gpt-5.4). Updated the
existing #230/#433 test to reflect that #1625 supersedes the strip-on-custom
rule for loopback hosts (see api/config.py and test_model_resolver.py
docstring update). Reported by @akarichan8231 in Discord on 2026-05-04.
42 regression tests across:
tests/test_issue1623_sse_heartbeat_alignment.py (3)
tests/test_issue1624_repair_stale_pending_grace.py (9)
tests/test_issue1625_local_server_model_id_preservation.py (30)
4142 -> 4184 passing. 0 regressions.
2026-05-04 16:31:32 +00:00
|
|
|
])
|
|
|
|
|
def test_known_local_server_provider_preserves_full_model_id(provider_name, monkeypatch):
|
|
|
|
|
"""Known local-server provider names must preserve the slashed model id
|
|
|
|
|
even when the prefix matches _PROVIDER_MODELS."""
|
|
|
|
|
_patch_cfg(monkeypatch, provider=provider_name, base_url="http://localhost:1234/v1")
|
|
|
|
|
model, provider, base_url = cfg_mod.resolve_model_provider("qwen/qwen3.6-27b")
|
|
|
|
|
assert model == "qwen/qwen3.6-27b", (
|
|
|
|
|
f"Local-server provider {provider_name!r} must preserve the full model id; "
|
|
|
|
|
f"stripping it makes LM Studio/Ollama/etc. load a fresh instance with "
|
|
|
|
|
f"default settings (#1625)."
|
|
|
|
|
)
|
|
|
|
|
assert provider == provider_name
|
|
|
|
|
assert base_url == "http://localhost:1234/v1"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_lmstudio_with_huggingface_namespace_preserved(monkeypatch):
|
|
|
|
|
"""The reporter's exact case: lmstudio + qwen/qwen3.6-27b + localhost."""
|
|
|
|
|
_patch_cfg(monkeypatch, provider="lmstudio", base_url="http://localhost:1234/v1",
|
|
|
|
|
default="qwen/qwen3.6-27b")
|
|
|
|
|
model, provider, base_url = cfg_mod.resolve_model_provider("qwen/qwen3.6-27b")
|
|
|
|
|
assert model == "qwen/qwen3.6-27b"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_lmstudio_with_openai_prefix_preserved(monkeypatch):
|
|
|
|
|
"""openai/gpt-oss-120b on LM Studio is a real HuggingFace id; the namespace
|
|
|
|
|
is part of the registry key. Must not be stripped on local servers."""
|
|
|
|
|
_patch_cfg(monkeypatch, provider="lmstudio", base_url="http://localhost:1234/v1")
|
|
|
|
|
model, provider, base_url = cfg_mod.resolve_model_provider("openai/gpt-oss-120b")
|
|
|
|
|
assert model == "openai/gpt-oss-120b", (
|
|
|
|
|
"openai/gpt-oss-120b on LM Studio must preserve the full id (#1625)"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-05-08 00:41:55 +00:00
|
|
|
@pytest.mark.parametrize("provider_name", [
|
|
|
|
|
"ollama",
|
|
|
|
|
"lmstudio",
|
|
|
|
|
"lm-studio",
|
|
|
|
|
"vllm",
|
|
|
|
|
"tabby",
|
|
|
|
|
])
|
|
|
|
|
def test_named_custom_local_server_provider_preserves_full_model_id_on_lan_host(
|
|
|
|
|
provider_name,
|
|
|
|
|
monkeypatch,
|
|
|
|
|
):
|
|
|
|
|
"""#1830: custom:<local-server> slugs must keep local-server no-strip semantics.
|
|
|
|
|
|
|
|
|
|
Non-loopback hostnames like ollama.lan do not trigger the base_url local
|
|
|
|
|
heuristic, so the provider-id check must recognize custom:<slug> directly.
|
|
|
|
|
"""
|
|
|
|
|
_patch_cfg(
|
|
|
|
|
monkeypatch,
|
|
|
|
|
provider=provider_name,
|
|
|
|
|
base_url="http://lan-host:1234/v1",
|
|
|
|
|
default="qwen/qwen3.6-27b",
|
|
|
|
|
custom_providers=[
|
|
|
|
|
{
|
|
|
|
|
"name": provider_name,
|
|
|
|
|
"base_url": "http://lan-host:1234/v1",
|
|
|
|
|
"api_key": "local-key",
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
model, provider, base_url = cfg_mod.resolve_model_provider("qwen/qwen3.6-27b")
|
|
|
|
|
assert model == "qwen/qwen3.6-27b"
|
|
|
|
|
assert provider == f"custom:{provider_name}"
|
|
|
|
|
assert base_url == "http://lan-host:1234/v1"
|
|
|
|
|
|
|
|
|
|
|
fix(streaming): SSE heartbeat alignment, repair grace period, local-server model id preservation (#1623, #1624, #1625)
Closes #1623 — Lower SSE app heartbeat from 30s to 5s at every long-lived
handler (main agent, terminal, gateway-watcher, approval-poller, clarify-poller).
Kernel TCP keepalive declares peer dead at 25s worst-case (10s KEEPIDLE +
5s KEEPINTVL * 3 KEEPCNT, added v0.50.289 #1581). 30s app heartbeat let the
kernel tear sockets down on flaky networks before the app sent its first
keepalive byte — drops at ~10s during long thinking phases. New named
constant _SSE_HEARTBEAT_INTERVAL_SECONDS=5; regression test pins the
inequality (app_heartbeat * 2 <= kernel_window) so future tuning can't
re-introduce the misalignment.
Closes #1624 — Add 30s grace period to _repair_stale_pending() trigger.
Without it, any narrow race between the streaming thread clearing
pending_user_message and STREAMS.pop(stream_id) produces a false-positive
'Previous turn did not complete.' marker on a turn that finished correctly
(reproducible after every command-approval turn). Defense-in-depth, not
the root-cause fix — the actual streaming-thread leak path is tracked
separately. Falsy pending_started_at (legacy sidecars) treated as
'old enough' so legitimate legacy-data recovery still works. Plus
logger.warning telemetry on every legitimate repair so the next batch of
user reports tells us whether the underlying race still fires.
Closes #1625 — Local model servers (LM Studio, Ollama, llama.cpp, vLLM,
TabbyAPI, koboldcpp, textgen-webui) now keep the full HuggingFace-style
model id (e.g. 'qwen/qwen3.6-27b' instead of stripped 'qwen3.6-27b'). New
_LOCAL_SERVER_PROVIDERS set + _base_url_points_at_local_server() loopback/
RFC1918 heuristic — either signal triggers no-strip. Backward compat
preserved for OpenAI-compatible proxies on public hosts (LiteLLM at
litellm.example.com still strips openai/gpt-5.4 -> gpt-5.4). Updated the
existing #230/#433 test to reflect that #1625 supersedes the strip-on-custom
rule for loopback hosts (see api/config.py and test_model_resolver.py
docstring update). Reported by @akarichan8231 in Discord on 2026-05-04.
42 regression tests across:
tests/test_issue1623_sse_heartbeat_alignment.py (3)
tests/test_issue1624_repair_stale_pending_grace.py (9)
tests/test_issue1625_local_server_model_id_preservation.py (30)
4142 -> 4184 passing. 0 regressions.
2026-05-04 16:31:32 +00:00
|
|
|
# ── Loopback / private-IP heuristic ───────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("loopback_url", [
|
|
|
|
|
"http://localhost:11434",
|
|
|
|
|
"http://127.0.0.1:1234/v1",
|
|
|
|
|
"http://127.0.0.1:8080/openai",
|
|
|
|
|
"http://10.0.0.5:8080/v1", # private RFC1918
|
|
|
|
|
"http://192.168.1.50:1234/v1", # private RFC1918
|
|
|
|
|
"http://172.16.0.10:8000/v1", # private RFC1918
|
|
|
|
|
"http://[::1]:1234/v1", # IPv6 loopback
|
|
|
|
|
])
|
|
|
|
|
def test_loopback_base_url_preserves_full_model_id(loopback_url, monkeypatch):
|
|
|
|
|
"""Even with a generic `provider: custom` (or any non-local-server name),
|
|
|
|
|
a base_url pointing at a loopback or private IP must preserve the model id —
|
|
|
|
|
almost certainly a local model server."""
|
|
|
|
|
_patch_cfg(monkeypatch, provider="custom", base_url=loopback_url)
|
|
|
|
|
model, _, _ = cfg_mod.resolve_model_provider("qwen/qwen3.6-27b")
|
|
|
|
|
assert model == "qwen/qwen3.6-27b", (
|
|
|
|
|
f"Loopback/private base_url {loopback_url!r} must preserve the full model id (#1625)"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── Backward compat: OpenAI-compatible proxies keep prefix-stripping ─────
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_public_openai_proxy_still_strips_prefix(monkeypatch):
|
|
|
|
|
"""OpenAI-compatible proxies (LiteLLM, public OpenRouter relays) still get
|
|
|
|
|
the strip behavior so 'openai/gpt-5.4' → 'gpt-5.4'."""
|
|
|
|
|
_patch_cfg(monkeypatch, provider="openai", base_url="https://litellm.example.com/v1")
|
|
|
|
|
model, provider, base_url = cfg_mod.resolve_model_provider("openai/gpt-5.4")
|
|
|
|
|
assert model == "gpt-5.4", (
|
|
|
|
|
"Public-host openai/* on a non-loopback proxy must continue to strip prefix"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_unknown_prefix_on_public_proxy_preserved(monkeypatch):
|
|
|
|
|
"""Unknown prefix (zai-org/GLM-5.1) on public proxy passes through full
|
|
|
|
|
(the existing contract — stripping unknown prefixes caused model_not_found)."""
|
|
|
|
|
_patch_cfg(monkeypatch, provider="openai", base_url="https://litellm.example.com/v1")
|
|
|
|
|
model, _, _ = cfg_mod.resolve_model_provider("zai-org/GLM-5.1")
|
|
|
|
|
assert model == "zai-org/GLM-5.1"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_openrouter_passes_full_unaffected(monkeypatch):
|
|
|
|
|
"""OpenRouter always needs the full provider/model path — pre-existing
|
|
|
|
|
contract that the local-server fix must not disturb."""
|
|
|
|
|
_patch_cfg(monkeypatch, provider="openrouter")
|
|
|
|
|
model, provider, _ = cfg_mod.resolve_model_provider("anthropic/claude-sonnet-4.6")
|
|
|
|
|
assert model == "anthropic/claude-sonnet-4.6"
|
|
|
|
|
assert provider == "openrouter"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── Helper unit tests ─────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("url, expected", [
|
|
|
|
|
("http://localhost:1234", True),
|
|
|
|
|
("http://127.0.0.1:1234", True),
|
|
|
|
|
("http://10.0.0.5", True),
|
|
|
|
|
("http://192.168.1.1:8080", True),
|
|
|
|
|
("http://[::1]:1234", True),
|
|
|
|
|
("http://example.com", False),
|
|
|
|
|
("https://api.openai.com/v1", False),
|
|
|
|
|
("https://litellm.example.com/v1", False),
|
|
|
|
|
("", False),
|
|
|
|
|
(None, False),
|
|
|
|
|
("not-a-url", False),
|
|
|
|
|
])
|
|
|
|
|
def test_base_url_points_at_local_server_helper(url, expected):
|
|
|
|
|
assert cfg_mod._base_url_points_at_local_server(url) is expected
|