ouroboros/tests/test_subscription_vision_capability.py
Ouroboros 627ee94471 Restore supervising model recovery and retain failed response evidence
Preserve free live-executor waiting before ordinary managed model recovery.
Configured-session supervisors use the same upstream-observation path as
other managed tasks; external start and patch custody guards stay in place.
Negotiate private response evidence and preserve known terminal usage when
local message normalization fails, without introducing another retry owner.

Keep unknown accounting and exact source custody explicit (P1), repair the
shared recovery boundary (P2), and reuse existing transport/result owners
(P7). Update coupled tests and architecture/development descriptions.
2026-09-15 01:40:05 +03:00

195 lines
9.8 KiB
Python

"""Subscription images reach the ordinary Main and registered VLM entrypoints."""
import base64
from copy import deepcopy
import json
from types import SimpleNamespace
import pytest
from ouroboros import provider_models
from ouroboros.gateways.claudexor import ClaudexorUnavailable
from ouroboros.llm import LLMClient
from ouroboros.model_wait import task_model_wait_scope
from ouroboros.subscription_install_presets import compile_model_settings
from ouroboros.tools import vision
from ouroboros.vision_routing import VisionRoutingContext, prepare_messages_for_send
from tests.test_llm_claudexor import MODEL, ledger, setup as _subscription_transport
from tests.test_vision_model_wait import child_fixture as _child_fixture, _events
subscription_transport = _subscription_transport
child_fixture = _child_fixture
@pytest.fixture
def catalog(monkeypatch):
calls = []
state = {"modalities": ["text", "image"], "error": None}
def read(source, profile=None, *, requested_model=None):
calls.append((source, profile, requested_model))
if state["error"]:
raise ClaudexorUnavailable(state["error"], "controlled metadata unavailable")
return {"source": source, "credentialProfileId": profile or "auto-account",
"accountFingerprint": "identity-" + (profile or "auto-account"),
"models": [{"id": requested_model, "inputModalities": state["modalities"]}]}
monkeypatch.setattr(LLMClient, "claudexor_model_catalog", staticmethod(read))
monkeypatch.setenv("OUROBOROS_MODEL_ACCOUNTS", json.dumps({"main": "main-a", "vision": "vision-b"}))
return state, calls
def image_messages():
return [{"role": "system", "content": "Own SYSTEM and BIBLE 🐍\r\n"},
{"role": "user", "content": [{"type": "text", "text": "Inspect this"},
{"type": "image_url", "image_url": {"url": "data:image/png;base64,AAAA"}}]}]
def install_preset(monkeypatch, model=MODEL):
proposed = compile_model_settings([{"value": model, "is_default": True,
"input_modalities": ["text", "image"]}], {})
for key, value in proposed.items():
monkeypatch.setenv(key, value)
assert proposed["OUROBOROS_MODEL"] == proposed["OUROBOROS_MODEL_VISION"] == model
return proposed
@pytest.mark.parametrize("metadata_error", [None, "daemon_not_discovered", "subscription_window_exhausted"])
def test_preset_images_reach_real_main_transport(subscription_transport, catalog, monkeypatch, metadata_error):
from ouroboros.loop_llm_call import call_llm_with_retry
root, gateway, client = subscription_transport
state, calls = catalog
state["error"] = metadata_error
proposed = install_preset(monkeypatch)
monkeypatch.setenv("OUROBOROS_IMAGE_INPUT_MODE", "auto")
messages = image_messages()
original = deepcopy(messages)
(root / "logs").mkdir(parents=True, exist_ok=True)
answer, _ = call_llm_with_retry(client, messages, proposed["OUROBOROS_MODEL"], [],
"high", 1, root / "logs", "task-one", 1, None, {},
model_role="main", model_account_override="explicit-main")
assert answer["content"] == "Ответ 🐍"
assert gateway.uploads[0][0]["messages"] == original
assert gateway.uploads[0][0]["account"] == {"mode": "pin", "profileId": "explicit-main"}
assert calls == [("codex", "explicit-main", "exact-model")]
assert messages == original
assert len(gateway.creates) == 1 and len(gateway.accepted_operations) == 1
assert [row["state"] for row in ledger(root)] == ["reserved", "dispatched", "settled"]
def test_image_capability_is_exact_role_account_and_not_a_global_overlay(catalog):
state, calls = catalog
before = dict(provider_models._VISION_OVERLAY)
assert provider_models.supports_vision(MODEL, model_role="main") is True
state["modalities"] = ["text"]
assert provider_models.supports_vision(MODEL, model_role="vision") is False
state["modalities"] = ["text", "image"]
assert provider_models.supports_vision(MODEL, model_role="vision", model_account_override="") is True
assert calls == [("codex", "main-a", "exact-model"), ("codex", "vision-b", "exact-model"),
("codex", None, "exact-model")]
assert provider_models._VISION_OVERLAY == before
@pytest.mark.parametrize("modalities", [None, []])
def test_absent_image_metadata_stays_unknown_and_does_not_erase_input(catalog, monkeypatch, modalities):
state, _ = catalog
state["modalities"] = modalities
monkeypatch.setenv("OUROBOROS_IMAGE_INPUT_MODE", "auto")
assert provider_models.supports_vision(MODEL, model_role="main") is None
messages = image_messages()
assert prepare_messages_for_send(messages, routing=VisionRoutingContext(MODEL, object(), {})) is messages
def test_metadata_cannot_borrow_another_account_or_source(monkeypatch):
monkeypatch.setattr(LLMClient, "claudexor_model_catalog", staticmethod(lambda *a, **k: {
"source": "codex", "credentialProfileId": "another-account",
"models": [{"id": "exact-model", "inputModalities": ["text"]}]}))
assert provider_models.supports_vision(MODEL, model_account_override="wanted") is None
assert provider_models.supports_vision("claudexor::another=exact-model") is None
def test_text_only_and_image_off_do_not_query_catalog(catalog, monkeypatch):
_, calls = catalog
monkeypatch.setenv("OUROBOROS_IMAGE_INPUT_MODE", "auto")
text = [{"role": "user", "content": "hello"}]
assert prepare_messages_for_send(text, routing=VisionRoutingContext(MODEL, object(), {})) is text
monkeypatch.setenv("OUROBOROS_IMAGE_INPUT_MODE", "off")
prepared = prepare_messages_for_send(image_messages(), routing=VisionRoutingContext(MODEL, object(), {}))
assert "image omitted" in prepared[1]["content"][1]["text"]
assert calls == []
def test_confirmed_nonvision_keeps_existing_capability_gap(catalog, monkeypatch):
state, _ = catalog
state["modalities"] = ["text"]
monkeypatch.setenv("OUROBOROS_IMAGE_INPUT_MODE", "inline")
assert vision._resolve_vlm_model(LLMClient(), MODEL) == ""
prepared = prepare_messages_for_send(image_messages(), routing=VisionRoutingContext(MODEL, object(), {}))
assert "image omitted" in prepared[1]["content"][1]["text"]
assert provider_models.supports_vision(MODEL + " (local)") is False
def test_caption_calls_use_vision_role_and_preserve_canonical_image(subscription_transport, catalog, monkeypatch):
root, gateway, client = subscription_transport
_, calls = catalog
install_preset(monkeypatch)
monkeypatch.setenv("OUROBOROS_IMAGE_INPUT_MODE", "caption")
messages = image_messages()
original = deepcopy(messages)
prepared = prepare_messages_for_send(messages, routing=VisionRoutingContext(
MODEL, client, {}, drive_root=root, task_id="task-one"))
assert "Ответ 🐍" in prepared[1]["content"][1]["text"]
assert messages == original
assert calls == [("codex", "vision-b", "exact-model")]
assert gateway.uploads[0][0]["account"] == {"mode": "pin", "profileId": "vision-b"}
assert gateway.uploads[0][0]["messages"][-1]["content"][1]["type"] == "image_url"
assert len(gateway.creates) == 1
def test_temporary_vision_role_uses_new_model_account_only(catalog, tmp_path):
_, calls = catalog
changed = "claudexor::another=new-image-model"
with task_model_wait_scope(task={"id": "image-task", "_attempt": 1}, drive_root=tmp_path,
event_queue=None, worker_slot_held=True) as wait:
wait.overrides["vision"] = {"model": changed, "model_account_override": "changed-profile", "use_local": False}
assert vision._resolve_vlm_model(LLMClient(), MODEL) == changed
from ouroboros.vision_routing import resolve_vision_caption_model
assert resolve_vision_caption_model(SimpleNamespace(model=MODEL), LLMClient()) == changed
assert calls == [("another", "changed-profile", "new-image-model")] * 2
def test_browser_screenshot_keeps_subscription_image(catalog, tmp_path, monkeypatch):
from ouroboros.tools.browser import _inject_native_screenshot
monkeypatch.setenv("OUROBOROS_IMAGE_INPUT_MODE", "auto")
ctx = SimpleNamespace(active_model=MODEL, task_metadata={}, messages=[], drive_root=tmp_path)
encoded = base64.b64encode(b"fixture-image").decode()
_inject_native_screenshot(ctx, encoded)
assert ctx.messages[-1]["content"][-1]["image_url"]["url"] == "data:image/png;base64," + encoded
assert prepare_messages_for_send(ctx.messages, routing=VisionRoutingContext(MODEL, object(), {})) is ctx.messages
@pytest.mark.serial
@pytest.mark.parametrize("name", ["vlm_query", "analyze_screenshot"])
def test_registered_vision_tool_reaches_real_child_from_preset(child_fixture, catalog, tmp_path, monkeypatch, name):
from ouroboros.tools.registry import ToolRegistry
from tests.test_vision_model_wait import MODEL as child_model
_, events, root = child_fixture
install_preset(monkeypatch, child_model)
registry = ToolRegistry(repo_dir=tmp_path, drive_root=root)
registry._ctx.task_id = "image-task"
registry._ctx.task_attempt = 1
# URL avoids image decoder fixtures; invalid base64 exercises the existing
# permissive screenshot fixture path. The controlled child does not inspect pixels.
registry._ctx.browser_state.last_screenshot_b64 = "fixture-not-base64"
args = {"prompt": "Describe only this image"}
if name == "vlm_query":
args["image_url"] = "https://example.invalid/fixture.png"
result = registry.execute_result(name, args)
assert result.text == "pixels read exactly once 🐍"
rows = _events(events)
assert sum(row["kind"] == "generation" for row in rows) == 1
payload = next(row["payload"] for row in rows if row["kind"] == "upload")
assert payload["account"] == {"mode": "pin", "profileId": "vision-b"}
assert payload["messages"][-1]["content"][1]["type"] == "image_url"