mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-06 06:52:20 +02:00
228 lines
9.4 KiB
Python
228 lines
9.4 KiB
Python
"""Tests for Model Picker Endpoint Grouping and ChatGPT Reasoning Effort Control.
|
|
|
|
Covers:
|
|
1. Endpoint/account grouping in modelPicker.js (keyed on endpoint_id, distinct routes, collapsible headers, search auto-expansion, keyboard nav).
|
|
2. ChatGPT reasoning effort (validation, catalog metadata, session thinking_mode persistence, payload shaping with zero provider tools).
|
|
3. Composer UI elements and styles.
|
|
"""
|
|
|
|
import json
|
|
from pathlib import Path
|
|
import subprocess
|
|
import pytest
|
|
|
|
from src import chatgpt_subscription, llm_core
|
|
from tests.helpers.stylesheets import app_css
|
|
|
|
ROOT = Path(__file__).parents[1]
|
|
|
|
|
|
# ============================================================
|
|
# PART 1: FRONTEND MODEL PICKER GROUPING & UI
|
|
# ============================================================
|
|
|
|
def test_model_picker_endpoint_grouping_logic_in_node():
|
|
"""Verify in node that modelPicker groups models by endpoint_id and maintains distinct routes."""
|
|
source = (ROOT / "static/js/modelPicker.js").read_text(encoding="utf-8")
|
|
key_start = source.index("function _pickerModelKey(m)")
|
|
key_end = source.index("// ── Shared keyboard nav")
|
|
key_helper = source[key_start:key_end]
|
|
|
|
group_start = source.index("const _endpointGroupNames =")
|
|
group_end = source.index("const _collapsedProviders =")
|
|
group_snippet = source[group_start:group_end]
|
|
|
|
test_js = f"""
|
|
const _PROVIDER_NAMES = {{}};
|
|
const _PROVIDER_ALIAS = {{}};
|
|
{key_helper}
|
|
{group_snippet}
|
|
|
|
const codex00_model = {{
|
|
mid: 'gpt-5.5',
|
|
display: 'gpt-5.5',
|
|
endpointId: 'chatgpt-codex00',
|
|
epName: 'ChatGPT · codex00',
|
|
category: 'chatgpt_subscription',
|
|
url: 'https://chatgpt.com/backend-api/codex'
|
|
}};
|
|
|
|
const codex01_model = {{
|
|
mid: 'gpt-5.5',
|
|
display: 'gpt-5.5',
|
|
endpointId: 'chatgpt-codex01',
|
|
epName: 'ChatGPT · codex01',
|
|
category: 'chatgpt_subscription',
|
|
url: 'https://chatgpt.com/backend-api/codex'
|
|
}};
|
|
|
|
const k0 = _pickerModelKey(codex00_model);
|
|
const k1 = _pickerModelKey(codex01_model);
|
|
if (k0 === k1) throw new Error('Same model across accounts must have distinct route keys');
|
|
|
|
const g0 = _providerGroupKey(codex00_model);
|
|
const g1 = _providerGroupKey(codex01_model);
|
|
if (g0 === g1) throw new Error('Groups must be separate per account endpoint');
|
|
|
|
const name0 = _providerGroupName(g0);
|
|
const name1 = _providerGroupName(g1);
|
|
if (name0 !== 'ChatGPT · codex00') throw new Error('Unexpected group name for codex00: ' + name0);
|
|
if (name1 !== 'ChatGPT · codex01') throw new Error('Unexpected group name for codex01: ' + name1);
|
|
|
|
console.log(JSON.stringify({{ k0, k1, g0, g1, name0, name1 }}));
|
|
"""
|
|
proc = subprocess.run(["node", "-e", test_js], check=True, capture_output=True, text=True)
|
|
res = json.loads(proc.stdout)
|
|
assert res["k0"] != res["k1"]
|
|
assert res["g0"] != res["g1"]
|
|
assert res["name0"] == "ChatGPT · codex00"
|
|
assert res["name1"] == "ChatGPT · codex01"
|
|
|
|
|
|
def test_model_picker_source_invariants():
|
|
"""Verify modelPicker.js contains required UI grouping structures."""
|
|
src = (ROOT / "static/js/modelPicker.js").read_text(encoding="utf-8")
|
|
# Grouping keyed on endpoint_id
|
|
assert "m.endpointId || m.url" in src
|
|
# Header class and chevron
|
|
assert "mp-provider-header" in src
|
|
assert "mp-provider-chevron" in src
|
|
assert "mp-provider-name" in src
|
|
assert "mp-provider-count" in src
|
|
# Group container
|
|
assert "mp-provider-group" in src
|
|
# Search mode groups matches
|
|
assert "isSearch" in src
|
|
# TextContent used to prevent XSS
|
|
assert "nameSpan.textContent = _providerGroupName(provider)" in src
|
|
# Collapsed persistence key
|
|
assert "odysseus-model-collapsed" in src
|
|
|
|
|
|
def test_composer_reasoning_effort_ui_markup():
|
|
"""Verify static/index.html and the app stylesheet cascade include reasoning effort controls."""
|
|
html = (ROOT / "static/index.html").read_text(encoding="utf-8")
|
|
css = app_css()
|
|
# HTML elements
|
|
assert 'id="reasoning-effort-wrap"' in html
|
|
assert 'id="reasoning-effort-btn"' in html
|
|
assert 'id="reasoning-effort-current"' in html
|
|
assert 'id="reasoning-effort-menu"' in html
|
|
assert 'title="Reasoning effort"' in html
|
|
assert 'class="reasoning-effort-prefix">Effort: </span>' in html
|
|
# CSS classes
|
|
assert ".reasoning-effort-wrap" in css
|
|
assert ".reasoning-effort-btn" in css
|
|
assert ".reasoning-effort-menu" in css
|
|
assert ".reasoning-effort-option" in css
|
|
# Responsive hide of prefix
|
|
assert ".reasoning-effort-prefix { display: none; }" in css
|
|
|
|
|
|
def test_chat_submit_includes_reasoning_effort():
|
|
"""Verify static/js/chat.js sends reasoning_effort when active."""
|
|
src = (ROOT / "static/js/chat.js").read_text(encoding="utf-8")
|
|
assert "window.__odysseusGetReasoningEffort" in src
|
|
assert "fd.append('reasoning_effort', effort)" in src
|
|
|
|
|
|
# ============================================================
|
|
# PART 2: BACKEND REASONING EFFORT CATALOG & VALIDATION
|
|
# ============================================================
|
|
|
|
def test_chatgpt_reasoning_effort_validation():
|
|
"""Test validate_reasoning_effort against catalog definitions."""
|
|
# gpt-6-astra supports low, medium, high, xhigh, max, ultra
|
|
assert chatgpt_subscription.validate_reasoning_effort("gpt-6-astra", "high") == "high"
|
|
assert chatgpt_subscription.validate_reasoning_effort("gpt-6-astra", "LOW") == "low"
|
|
assert chatgpt_subscription.validate_reasoning_effort("gpt-6-astra", "medium") == "medium"
|
|
assert chatgpt_subscription.validate_reasoning_effort("gpt-6-astra", "xhigh") == "xhigh"
|
|
assert chatgpt_subscription.validate_reasoning_effort("gpt-6-astra", "unsupported_level") is None
|
|
assert chatgpt_subscription.validate_reasoning_effort("gpt-6-astra", "default") is None
|
|
assert chatgpt_subscription.validate_reasoning_effort("gpt-6-astra", None) is None
|
|
|
|
# Model metadata catalog
|
|
meta_astra = chatgpt_subscription.get_chatgpt_model_metadata("gpt-6-astra")
|
|
assert meta_astra["default_reasoning_level"] == "low"
|
|
levels = meta_astra["supported_reasoning_levels"]
|
|
assert "low" in levels
|
|
assert "medium" in levels
|
|
assert "high" in levels
|
|
|
|
meta_55 = chatgpt_subscription.get_chatgpt_model_metadata("gpt-5.5")
|
|
assert meta_55["default_reasoning_level"] == "medium"
|
|
|
|
|
|
def test_models_metadata_attached_for_chatgpt_catalog():
|
|
"""Verify get_chatgpt_model_metadata populates metadata for ChatGPT models."""
|
|
from src.chatgpt_subscription import get_chatgpt_model_metadata
|
|
models = ["gpt-6-astra", "gpt-5.5", "gpt-5.6-sol", "llama3"]
|
|
models_metadata = {}
|
|
for mid in models:
|
|
meta = get_chatgpt_model_metadata(mid)
|
|
if meta:
|
|
models_metadata[mid] = meta
|
|
assert "gpt-6-astra" in models_metadata
|
|
assert "gpt-5.5" in models_metadata
|
|
assert "gpt-5.6-sol" in models_metadata
|
|
assert "llama3" not in models_metadata
|
|
assert models_metadata["gpt-6-astra"]["default_reasoning_level"] == "low"
|
|
assert "high" in models_metadata["gpt-6-astra"]["supported_reasoning_levels"]
|
|
|
|
|
|
def test_model_routes_contains_metadata_population():
|
|
"""Verify routes/model_routes.py populates models_metadata."""
|
|
src = (ROOT / "routes/model_routes.py").read_text(encoding="utf-8")
|
|
assert "from src.chatgpt_subscription import get_chatgpt_model_metadata" in src
|
|
assert "models_metadata[mid] = meta" in src
|
|
assert '"models_metadata": models_metadata' in src
|
|
|
|
|
|
def test_history_and_chat_routes_support_reasoning_effort():
|
|
"""Verify history and chat routes support reasoning_effort and thinking_mode persistence."""
|
|
hist_src = (ROOT / "routes/history/history_routes.py").read_text(encoding="utf-8")
|
|
assert "reasoning_effort" in hist_src
|
|
assert 'mode = f"effort:{clean_effort}"' in hist_src
|
|
|
|
chat_src = (ROOT / "routes/chat_routes.py").read_text(encoding="utf-8")
|
|
assert "validate_reasoning_effort(sess.model, reasoning_effort)" in chat_src
|
|
assert "session_mode.startswith(\"effort:\")" in chat_src
|
|
|
|
sess_src = (ROOT / "routes/session_routes.py").read_text(encoding="utf-8")
|
|
assert "session.thinking_mode = \"off\"" in sess_src
|
|
|
|
|
|
def test_responses_payload_zero_tools_with_reasoning():
|
|
"""Verify that reasoning effort does NOT permit any native tool surfaces."""
|
|
forbidden_tools = [
|
|
{"type": "function", "function": {"name": "test"}},
|
|
{"type": "web_search_preview"},
|
|
]
|
|
payload = llm_core._build_chatgpt_responses_payload(
|
|
model="gpt-6-astra",
|
|
messages=[{"role": "user", "content": "hello"}],
|
|
temperature=0.7,
|
|
max_tokens=4096,
|
|
stream=True,
|
|
reasoning_effort="high",
|
|
tools=forbidden_tools,
|
|
)
|
|
assert payload["reasoning"] == {"effort": "high"}
|
|
assert "tools" not in payload
|
|
assert "tool_choice" not in payload
|
|
for k in llm_core.CHATGPT_FORBIDDEN_PAYLOAD_KEYS:
|
|
assert k not in payload
|
|
assert set(payload.keys()) <= llm_core.CHATGPT_ALLOWED_PAYLOAD_KEYS
|
|
|
|
|
|
def test_session_model_change_revalidates_effort_logic():
|
|
"""Switching to an unsupported model resets effort to off."""
|
|
meta_astra = chatgpt_subscription.get_chatgpt_model_metadata("gpt-6-astra")
|
|
supported = [lvl.lower() for lvl in meta_astra.get("supported_reasoning_levels", [])]
|
|
|
|
current_effort = "high"
|
|
assert current_effort in supported # retained for gpt-6-astra
|
|
|
|
meta_local = chatgpt_subscription.get_chatgpt_model_metadata("llama3.2")
|
|
assert meta_local is None # unsupported -> resets to off
|