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
2 changes: 1 addition & 1 deletion chat/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def is_disabled(value: str | None) -> bool:
ENABLED = not is_disabled(MODE)

#: The base path the Open WebUI subpath build serves at. Must match the
#: WEBUI_SUBPATH baked into the image (0.12.4.9-subpath -> /chat). Defined
#: WEBUI_SUBPATH baked into the image (0.12.4.10-subpath -> /chat). Defined
#: first: UPSTREAM below bakes it into the default.
SUBPATH = os.environ.get("SIMPLEAUDIT_CHAT_SUBPATH", "/chat")
if not SUBPATH.startswith("/"):
Expand Down
2 changes: 1 addition & 1 deletion chat/proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -585,7 +585,7 @@ def start_open_webui(studio_port: int | None = None) -> subprocess.Popen:
OWUI_WHEEL_URL = os.environ.get(
"SIMPLEAUDIT_CHAT_WHEEL",
"https://github.com/SushantGautam/open-webui/releases/download/"
"v0.12.4.9-subpath/open_webui-0.12.4.9-py3-none-any.whl",
"v0.12.4.10-subpath/open_webui-0.12.4.10-py3-none-any.whl",
)
OWUI_PACKAGE = os.environ.get("SIMPLEAUDIT_CHAT_PACKAGE", "open-webui")

Expand Down
4 changes: 2 additions & 2 deletions chat/tests/test_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -381,8 +381,8 @@ class WheelMarkerTests(SimpleTestCase):
def test_default_wheel_is_content_capture_release(self):
# Pinned exactly: a version bump must change this assertion deliberately,
# not pass silently via a substring of an older tag.
self.assertIn("v0.12.4.9-subpath", proxy.OWUI_WHEEL_URL)
self.assertIn("open_webui-0.12.4.9-py3-none-any.whl", proxy.OWUI_WHEEL_URL)
self.assertIn("v0.12.4.10-subpath", proxy.OWUI_WHEEL_URL)
self.assertIn("open_webui-0.12.4.10-py3-none-any.whl", proxy.OWUI_WHEEL_URL)

def test_install_requirement_includes_observability_extra(self):
requirement = proxy._owui_install_requirement()
Expand Down
2 changes: 1 addition & 1 deletion deploy/openwebui-subpath/Caddyfile.subpath
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
#
# The routing shape enabled by the subpath fork built via
# deploy/openwebui-subpath/build.sh (published as
# ghcr.io/sushantgautam/open-webui:v0.12.4.9-subpath). It replaces the old
# ghcr.io/sushantgautam/open-webui:v0.12.4.10-subpath). It replaces the old
# cross-origin iframe model (Caddyfile.chat on :8801) with one origin / one
# port:
#
Expand Down
2 changes: 1 addition & 1 deletion deploy/openwebui-subpath/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ pip caveats): [`DESIGN.md`](./DESIGN.md).
publishing to ghcr.io (root on push, `-subpath` on `webui_subpath` dispatch).

The cutover is done: `docker-compose.yml` pulls
`ghcr.io/sushantgautam/open-webui:v0.12.4.9-subpath` (set `WEBUI_SUBPATH=/chat`),
`ghcr.io/sushantgautam/open-webui:v0.12.4.10-subpath` (set `WEBUI_SUBPATH=/chat`),
`chat-proxy` (Caddy, no profile) is the **only published port** and runs
`Caddyfile.subpath`, Studio's wrapper page moved to `/playground/`, and
`Caddyfile.chat` + the separate cross-origin port are retired. Embedded
Expand Down
1 change: 1 addition & 0 deletions deploy/openwebui-subpath/UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,4 +131,5 @@ half-port; staying on the proven base is the fallback.
| 2026-11-04 | v0.12.4.7 | **published ✓** | Schema-shaped GenAI spans (fork #8). `-subpath` image and wheel release `v0.12.4.7-subpath`. |
| 2026-11-04 | v0.12.4.8 | **published ✓** | Agentic knowledge search traced as a GenAI `retrieval` span with `gen_ai.retrieval.documents` (fork #9). |
| 2026-11-04 | v0.12.4.9 | **published ✓** | Compact GenAI content by default, each piece recorded once per trace (fork #10; `OTEL_GENAI_CONTENT_MODE=full` for standard spans). Studio pins `v0.12.4.9-subpath`. |
| 2026-11-05 | v0.12.4.10 | **published ✓** | GenAI spans on every content, tool and model path (fork #11): retrieval spans for every content-reading built-in tool (knowledge, chat files, notes, chats, channels, memories, web) and memory recall, `execute_tool` spans in default function calling, `chat` spans for pipe models. Studio pins `v0.12.4.10-subpath`. |
| | | | |
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ services:
# forward /chat/* WITHOUT stripping the prefix (see the Caddyfile header).
open-webui:
profiles: ["chat"]
image: ${OPEN_WEBUI_IMAGE:-ghcr.io/sushantgautam/open-webui:v0.12.4.9-subpath}
image: ${OPEN_WEBUI_IMAGE:-ghcr.io/sushantgautam/open-webui:v0.12.4.10-subpath}
# Derives the OTLP password, then drops the raw key and password from the
# environment before Open WebUI starts, so Open WebUI never holds the Django key.
# (`$$` is compose's escape for a literal `$`.) Command is the image's own CMD.
Expand Down
69 changes: 57 additions & 12 deletions infra/tests/test_engine_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,46 @@ def send(history, trace="1"):
self.assertNotIn("chat_id", posts[2])
self.assertIsNone(posts[2]["parent_id"])

@mock.patch("chat.config.UPSTREAM", "http://127.0.0.1:8080/chat")
@mock.patch("chat.config.ENABLED", True)
def test_model_not_found_reloads_open_webui_models_and_retries_once(self):
"""Right after Open WebUI starts its model registry may miss the agent: reload, retry once."""
import asyncio
from types import SimpleNamespace

from infra import trace_target
from infra.trace_target import TraceContextModelTarget

trace_target._owui_conversations.clear()
posts, refreshes = [], []

class RawClient:
base_url = "http://127.0.0.1:8080/chat/api/v1/"
api_key = "jwt"

async def post(self, path, *, cast_to, body, options):
posts.append(body)
self.message_id = body["message_ids"][0]["message_id"]
if len(posts) == 1:
raise RuntimeError("Error code: 400 - {'detail': 'Model not found'}")
return {"status": True, "chat_id": "chat-1"}

async def get(self, path, *, cast_to, options):
return {"chat": {"history": {"messages": {self.message_id: {
"id": self.message_id, "role": "assistant", "done": True, "content": "ok"}}}}}

async def refresh(origin, token, *, every=60.0):
refreshes.append((origin, token, every))

target = TraceContextModelTarget(client=SimpleNamespace(client=RawClient(), acompletion=None),
model="studio.agent-1")
with mock.patch.object(trace_target, "_owui_refresh_models", refresh), \
mock.patch.object(trace_target, "_owui_get_jwt", mock.AsyncMock(return_value="jwt")):
response = asyncio.run(target.send(user="hi", history=[{"role": "user", "content": "hi"}]))
self.assertEqual(response.content, "ok")
self.assertEqual(len(posts), 2)
self.assertEqual(refreshes, [("http://127.0.0.1:8080/chat", "jwt", 0.0)])

def test_streaming_call_never_forwards_studio_only_params_to_openai_client(self):
"""extra_headers (W3C trace context) must not leak into the OpenAI request."""
import asyncio
Expand Down Expand Up @@ -1466,19 +1506,24 @@ async def run_scenario_reps(self, **_kwargs):
classes = {"a": type("AuditorA", (), {}), "b": type("AuditorB", (), {})}

def run(label):
with mock.patch("infra.engine.decision_target_for", return_value=object()), \
mock.patch("infra.trace_target.decision_auditor_class", return_value=classes[label]), \
mock.patch("infra.engine.snapshot_api_key", return_value=""):
engine.run_scenario_repeated(
name=label, description="d", expected_behavior=None, test_prompt=None,
target=_snap(label), auditor=_snap("x"), judge=_snap("y"), n_repetitions=2, trace_config=None,
)
engine.run_scenario_repeated(
name=label, description="d", expected_behavior=None, test_prompt=None,
target=_snap(label), auditor=_snap("x"), judge=_snap("y"), n_repetitions=2, trace_config=None,
)

threads = [threading.Thread(target=run, args=(k,)) for k in classes]
for t in threads:
t.start()
for t in threads:
t.join(timeout=30)
# Patched once, here: mock.patch is not thread-safe, and two threads patching
# and restoring the same attribute leave one thread's mock installed for every
# later test in this worker. Each thread's class follows from its own target.
with monkeypatch.context() as patched:
patched.setattr(engine, "decision_target_for",
lambda target, **_kw: ("decision", target["model_id"]))
patched.setattr("infra.trace_target.decision_auditor_class", lambda factory: classes[factory()[1]])
patched.setattr(engine, "snapshot_api_key", lambda _snapshot: "")
threads = [threading.Thread(target=run, args=(k,)) for k in classes]
for t in threads:
t.start()
for t in threads:
t.join(timeout=30)
assert {label.split(" ")[0] for label in seen} == {"a", "b"}
for label, cls in seen.items():
assert cls is classes[label.split(" ")[0]]
Expand Down
12 changes: 11 additions & 1 deletion infra/trace_target.py
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,17 @@ async def _openwebui_saved_chat_call(
if chat_id_to_continue:
body["chat_id"] = chat_id_to_continue
options = {"headers": trace_headers} if trace_headers else {}
created = await raw_client.post("/chat/completions", cast_to=dict, body=body, options=options)
try:
created = await raw_client.post("/chat/completions", cast_to=dict, body=body, options=options)
except Exception as exc:
# Right after Open WebUI starts, its model registry can still miss the agent
# (the throttled refresh may have run before its connections loaded). Reload
# the registry now and retry once.
if "not found" not in str(exc).lower() or "model" not in str(exc).lower():
raise
origin = base_url.rsplit("/api/v1", 1)[0].rstrip("/")
await _owui_refresh_models(origin, str(getattr(raw_client, "api_key", "") or ""), every=0.0)
created = await raw_client.post("/chat/completions", cast_to=dict, body=body, options=options)
chat_id = created.get("chat_id") if isinstance(created, dict) else None
if not chat_id:
raise RuntimeError("Open WebUI saved-chat request returned no chat_id")
Expand Down
Loading