mirror of
https://github.com/open-jarvis/OpenJarvis.git
synced 2026-07-28 14:07:55 +00:00
* feat: add query complexity analyzer with CLI and UI integration Classify incoming queries by difficulty (trivial→very_complex) to suggest appropriate token budgets for local vs. cloud routing. - Add score_complexity() with weighted signals (length, code, math, reasoning, multi-step, creative) and token tier mapping - Wire into `jarvis ask`: auto-suggest max_tokens when not set by user, show complexity in --profile output, log at DEBUG level - Add complexity metadata to /v1/chat/completions API response - Display complexity tier and score in frontend XRayFooter - Extend RoutingContext with complexity_score, suggested_max_tokens, has_reasoning fields - Update HeuristicRouter to route on complexity_score instead of raw query_length - Remove duplicated regex patterns from router.py (use complexity module as single source of truth) - Add 30 unit tests for complexity module - Fix existing router tests for new complexity-based routing Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: pass temperature and max_tokens from UI settings to backend The settings page stores temperature and max_tokens in the frontend store, but these values were never included in the chat API request. The backend Pydantic model defaults max_tokens to 1024 when the field is absent, which is too low for thinking models like qwen3.5 — they consume all tokens on reasoning and return empty content. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: pass UI settings to backend and auto-bump max_tokens from complexity - Pass temperature and max_tokens from frontend Settings store to the backend API (cherry-picked from fix/ui-max-tokens-passthrough) - Server-side: bump max_tokens when the complexity analyzer suggests a higher budget (e.g. for thinking models on complex queries), never reduce below the client-requested value - Fixes empty responses with thinking models (e.g. qwen3.5) that consumed all tokens on internal reasoning Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: raise token budget tiers to prevent empty responses on thinking models Double all tier budgets (trivial: 512→1024, simple: 1024→2048, etc.) so that thinking models like qwen3.5 have enough headroom for internal chain-of-thought plus visible output, even on simple queries. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: CI lint and test failures - Break long lines in routes.py to satisfy 88-char limit (E501) - Add intelligence.max_tokens and temperature to mocked config in test_ask_router.py so complexity analyzer can compare against int instead of MagicMock Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: line too long in test_complexity.py (E501) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
162 lines
4.9 KiB
Python
162 lines
4.9 KiB
Python
"""Tests for the heuristic model router (canonical location)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from openjarvis.core.registry import ModelRegistry
|
|
from openjarvis.core.types import ModelSpec
|
|
from openjarvis.learning._stubs import RoutingContext
|
|
from openjarvis.learning.routing.router import (
|
|
HeuristicRouter,
|
|
build_routing_context,
|
|
)
|
|
|
|
|
|
def _register_models() -> None:
|
|
"""Register a small set of models for testing."""
|
|
ModelRegistry.register_value(
|
|
"small",
|
|
ModelSpec(
|
|
model_id="small",
|
|
name="Small",
|
|
parameter_count_b=3.0,
|
|
context_length=4096,
|
|
),
|
|
)
|
|
ModelRegistry.register_value(
|
|
"large",
|
|
ModelSpec(
|
|
model_id="large",
|
|
name="Large",
|
|
parameter_count_b=70.0,
|
|
context_length=131072,
|
|
),
|
|
)
|
|
ModelRegistry.register_value(
|
|
"coder",
|
|
ModelSpec(
|
|
model_id="coder",
|
|
name="DeepSeek Coder",
|
|
parameter_count_b=16.0,
|
|
context_length=131072,
|
|
),
|
|
)
|
|
|
|
|
|
class TestBuildRoutingContext:
|
|
def test_code_detection(self) -> None:
|
|
ctx = build_routing_context("def hello():\n pass")
|
|
assert ctx.has_code is True
|
|
assert ctx.has_math is False
|
|
|
|
def test_math_detection(self) -> None:
|
|
ctx = build_routing_context("solve the integral of x^2")
|
|
assert ctx.has_math is True
|
|
assert ctx.has_code is False
|
|
|
|
def test_length(self) -> None:
|
|
ctx = build_routing_context("Hi")
|
|
assert ctx.query_length == 2
|
|
|
|
def test_urgency_default(self) -> None:
|
|
ctx = build_routing_context("test")
|
|
assert ctx.urgency == 0.5
|
|
|
|
|
|
class TestHeuristicRouter:
|
|
def test_short_query_prefers_small(self) -> None:
|
|
_register_models()
|
|
router = HeuristicRouter(
|
|
available_models=["small", "large", "coder"],
|
|
)
|
|
ctx = RoutingContext(query="Hi", query_length=2)
|
|
assert router.select_model(ctx) == "small"
|
|
|
|
def test_code_prefers_coder(self) -> None:
|
|
_register_models()
|
|
router = HeuristicRouter(
|
|
available_models=["small", "large", "coder"],
|
|
)
|
|
ctx = RoutingContext(
|
|
query="def foo():",
|
|
query_length=10,
|
|
has_code=True,
|
|
)
|
|
assert router.select_model(ctx) == "coder"
|
|
|
|
def test_math_prefers_large(self) -> None:
|
|
_register_models()
|
|
router = HeuristicRouter(
|
|
available_models=["small", "large", "coder"],
|
|
)
|
|
ctx = RoutingContext(
|
|
query="solve x",
|
|
query_length=7,
|
|
has_math=True,
|
|
)
|
|
assert router.select_model(ctx) == "large"
|
|
|
|
def test_high_complexity_prefers_large(self) -> None:
|
|
_register_models()
|
|
router = HeuristicRouter(
|
|
available_models=["small", "large", "coder"],
|
|
)
|
|
ctx = RoutingContext(query="x" * 501, query_length=501, complexity_score=0.7)
|
|
assert router.select_model(ctx) == "large"
|
|
|
|
def test_high_urgency_overrides_to_small(self) -> None:
|
|
_register_models()
|
|
router = HeuristicRouter(
|
|
available_models=["small", "large", "coder"],
|
|
)
|
|
ctx = RoutingContext(
|
|
query="x" * 501,
|
|
query_length=501,
|
|
urgency=0.9,
|
|
)
|
|
assert router.select_model(ctx) == "small"
|
|
|
|
def test_fallback_chain(self) -> None:
|
|
_register_models()
|
|
router = HeuristicRouter(
|
|
available_models=["small", "large"],
|
|
default_model="large",
|
|
fallback_model="small",
|
|
)
|
|
# Medium complexity, no code/math, no reasoning → falls to default
|
|
ctx = RoutingContext(
|
|
query="Tell me about cats",
|
|
query_length=60,
|
|
complexity_score=0.35,
|
|
)
|
|
assert router.select_model(ctx) == "large"
|
|
|
|
def test_no_available_models(self) -> None:
|
|
router = HeuristicRouter(
|
|
available_models=[],
|
|
default_model="fallback-model",
|
|
)
|
|
ctx = RoutingContext(query="test", query_length=4)
|
|
assert router.select_model(ctx) == "fallback-model"
|
|
|
|
def test_reasoning_keywords_prefer_large(self) -> None:
|
|
_register_models()
|
|
router = HeuristicRouter(available_models=["small", "large"])
|
|
query = (
|
|
"Please explain step by step how the process"
|
|
" of photosynthesis works in plants"
|
|
)
|
|
ctx = build_routing_context(query)
|
|
assert router.select_model(ctx) == "large"
|
|
|
|
def test_code_without_coder_falls_to_large(self) -> None:
|
|
_register_models()
|
|
router = HeuristicRouter(
|
|
available_models=["small", "large"],
|
|
)
|
|
ctx = RoutingContext(
|
|
query="def foo():",
|
|
query_length=10,
|
|
has_code=True,
|
|
)
|
|
assert router.select_model(ctx) == "large"
|