Spaces:
Running
Running
File size: 6,196 Bytes
26d7b76 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 | """
Unit and integration tests for FrugalGPT cascading router and prompt adaptation.
"""
import json
import pytest
from fastapi.testclient import TestClient
from inferroute.main import app
from inferroute.prompt_adapter import adapt_prompt, compress_few_shot_examples
from inferroute.validator import ReliabilityScorer
client = TestClient(app)
HEADERS = {"Authorization": "Bearer sk-inferroute-demo"}
# ββ Test 1: Prompt Adaptation Trimming βββββββββββββββββββββββββββββββββββββββββ
def test_prompt_adaptation_trimming():
"""
Ensure prompt adapter trims few-shot examples for cheap local backends (ollama, vllm)
but preserves them for premium cloud models (openai, gemini).
"""
few_shot_prompt = (
"Classify the sentiment of these headlines:\n\n"
"Example 1:\nHeadline: Gold prices surge amid inflation fears.\nSentiment: positive\n\n"
"Example 2:\nHeadline: Retail sales plummet in Q3.\nSentiment: negative\n\n"
"Example 3:\nHeadline: Central bank holds interest rates steady.\nSentiment: neutral\n\n"
"Headline: Stock index hits record high today.\nSentiment:"
)
messages = [{"role": "user", "content": few_shot_prompt}]
# Cheap backend -> should compress few-shot examples to keep max 1 example
compressed = adapt_prompt(messages, "ollama")
assert len(compressed[0]["content"]) < len(few_shot_prompt)
assert "Example 1:" in compressed[0]["content"]
assert "Example 2:" not in compressed[0]["content"]
assert "Stock index hits record high today." in compressed[0]["content"]
# Premium backend -> should preserve full few-shots
preserved = adapt_prompt(messages, "openai")
assert len(preserved[0]["content"]) == len(few_shot_prompt)
assert "Example 2:" in preserved[0]["content"]
assert "Example 3:" in preserved[0]["content"]
# ββ Test 2: Reliability Scorer Math Classification βββββββββββββββββββββββββββ
def test_reliability_scorer_math():
"""
Test that ReliabilityScorer correctly matches known prompt templates
and evaluates math answer correctness.
"""
scorer = ReliabilityScorer()
req = {
"messages": [{"role": "user", "content": "Solve for x: 5x - 15 = 20. Output ONLY the final numeric value of x as an integer."}]
}
# Correct response (7) should score 1.0
assert scorer.evaluate_reliability(req, "7") == 1.0
assert scorer.evaluate_reliability(req, "The answer is 7") == 1.0
# Incorrect response (5) should score 0.0
assert scorer.evaluate_reliability(req, "5") == 0.0
assert scorer.evaluate_reliability(req, "x = 10") == 0.0
# ββ Test 3: Server-side Cascade Blocking Integration βββββββββββββββββββββββββ
def test_cascade_blocking_integration():
"""
Test cascade routing logic on a math prompt.
Since Ollama returns incorrect math answers, the router should escalate
and eventually accept OpenAI/Gemini response.
"""
payload = {
"model": "edge/auto",
"messages": [{"role": "user", "content": "Solve for x: 5x - 15 = 20. Output ONLY the final numeric value of x as an integer."}],
"stream": False,
"routing": {
"policy": "cascade",
"acceptance_threshold": 0.8,
"cascade_chain": ["ollama", "vllm", "openai"]
}
}
response = client.post("/v1/chat/completions", headers=HEADERS, json=payload)
assert response.status_code == 200
data = response.json()
# The final accepted model should be openai (since ollama and vllm return wrong math results)
assert data["model"] == "openai"
assert data["choices"][0]["message"]["content"].strip() == "7"
# Verify cascade routing trace metadata
route = data.get("route", {})
assert route["policy"] == "cascade"
assert route["fallback_count"] > 0
assert len(route["cascade_steps"]) > 0
# Confirm the first step failed quality but last step accepted
assert route["cascade_steps"][0]["backend"] == "ollama"
assert route["cascade_steps"][0]["accepted"] is False
assert route["cascade_steps"][-1]["backend"] == "openai"
assert route["cascade_steps"][-1]["accepted"] is True
# ββ Test 4: Server-side Cascade Streaming Integration ββββββββββββββββββββββββ
def test_cascade_streaming_integration():
"""
Test cascade streaming execution flow.
Ensures that stream chunks are correctly buffered and pumped only when accepted,
and returns a final end-of-stream stats chunk.
"""
payload = {
"model": "edge/auto",
"messages": [{"role": "user", "content": "Solve for x: 5x - 15 = 20. Output ONLY the final numeric value of x as an integer."}],
"stream": True,
"routing": {
"policy": "cascade",
"acceptance_threshold": 0.8,
"cascade_chain": ["ollama", "vllm", "openai"]
}
}
response = client.post("/v1/chat/completions", headers=HEADERS, json=payload)
assert response.status_code == 200
# Parse SSE stream chunks
lines = response.text.split("\n")
events = []
for line in lines:
if line.startswith("data:"):
data_str = line[5:].strip()
if data_str != "[DONE]":
events.append(json.loads(data_str))
assert len(events) > 0
# Find the final stats chunk
stats_chunk = next((e for e in events if e.get("id") == "inferroute-stream-end"), None)
assert stats_chunk is not None
route = stats_chunk.get("route", {})
assert route["policy"] == "cascade"
assert route["selected_backend"] == "openai"
assert route["fallback_count"] > 0
cascade_steps = route.get("cascade_steps", [])
assert cascade_steps[0]["backend"] == "ollama"
assert cascade_steps[0]["accepted"] is False
assert cascade_steps[-1]["backend"] == "openai"
assert cascade_steps[-1]["accepted"] is True
|