Skip to content

Commit dcac47b

Browse files
committed
chore: include quick task 260706-h8b router changes
1 parent 61fde53 commit dcac47b

5 files changed

Lines changed: 115 additions & 36 deletions

File tree

.coveragerc

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[run]
2+
branch = true
3+
source = maf_starter,entities,main.py
4+
omit =
5+
autogen_dashboard/*
6+
autogen_starter/*
7+
*/__pycache__/*
8+
tests/*
9+
.venv/*
10+
11+
[report]
12+
exclude_lines =
13+
pragma: no cover
14+
if __name__ == .__main__.:
15+
raise NotImplementedError
16+
if TYPE_CHECKING:

maf_starter/config.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010

1111

1212
PROJECT_ROOT = Path(__file__).resolve().parents[1]
13-
DEFAULT_GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/openai/"
14-
DEFAULT_MODEL = "gemini-2.5-flash"
13+
DEFAULT_GEMINI_BASE_URL = "http://localhost:20128/v1"
14+
DEFAULT_MODEL = "openrouter/free"
1515
DEFAULT_MODEL_CANDIDATES = (
1616
"gemini-2.5-flash",
1717
"gemini-2.5-flash-lite",

maf_starter/routing_policy.py

Lines changed: 50 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,23 @@
4040
"fallback",
4141
)
4242

43+
TRIVIAL_KEYWORDS = (
44+
"lint",
45+
"test retry",
46+
"file search",
47+
"summary",
48+
"summaries",
49+
"classification",
50+
"simple refactor",
51+
)
52+
53+
ESCALATION_KEYWORDS = (
54+
"architecture",
55+
"security risk",
56+
"stuck",
57+
"escalate",
58+
)
59+
4360

4461
@dataclass(frozen=True)
4562
class RoutingPlan:
@@ -104,6 +121,7 @@ def build_routing_plan(
104121
requested_model=requested_model,
105122
default_provider=primary_provider,
106123
default_model=primary_model,
124+
messages=messages,
107125
)
108126
return RoutingPlan(
109127
mode="auto" if lane == "auto" and routing_mode == "auto" else "fixed",
@@ -129,8 +147,9 @@ def _build_lane_chain(
129147
requested_model: str | None,
130148
default_provider: str,
131149
default_model: str,
150+
messages: Iterable[Message] = (),
132151
) -> tuple[ChainStep, tuple[ChainStep, ...]]:
133-
lane_primary, lane_fallbacks = _lane_default_chain(settings, lane=lane, tier=tier)
152+
lane_primary, lane_fallbacks = _lane_default_chain(settings, lane=lane, tier=tier, messages=messages)
134153
primary = ChainStep(
135154
requested_provider or lane_primary.provider,
136155
requested_model or lane_primary.model,
@@ -155,39 +174,36 @@ def _build_lane_chain(
155174
return primary, tuple(fallback_chain)
156175

157176

158-
def _lane_default_chain(settings: Settings, *, lane: RouteLane, tier: str) -> tuple[ChainStep, tuple[ChainStep, ...]]:
159-
normalized_tier = tier if lane == "auto" else _tier_for_lane(lane)[0]
160-
if settings.ollama_base_url:
161-
ollama_primary = ChainStep("ollama", settings.ollama_model)
162-
gemini_fallbacks = _gemini_fallbacks_for_tier(settings, normalized_tier)
163-
return ollama_primary, gemini_fallbacks
164-
if normalized_tier == "simple":
165-
primary = ChainStep("gemini", "gemini-2.5-flash-lite")
166-
fallbacks = [
167-
ChainStep("gemini", "gemini-2.5-flash"),
168-
ChainStep("gemini", "gemini-2.5-pro"),
169-
ChainStep("claude-cli", settings.claude_cli_model or "haiku"),
170-
ChainStep("codex-cli", settings.codex_cli_model or "gpt-5-mini"),
171-
ChainStep("gemini-cli", settings.gemini_cli_model or "gemini-2.5-flash"),
172-
]
173-
elif normalized_tier == "deep":
174-
primary = ChainStep("gemini", "gemini-2.5-pro")
175-
fallbacks = [
176-
ChainStep("gemini", "gemini-2.5-flash"),
177-
ChainStep("gemini", "gemini-2.5-flash-lite"),
178-
ChainStep("claude-cli", settings.claude_cli_model or "sonnet"),
179-
ChainStep("codex-cli", settings.codex_cli_model or "gpt-5-codex"),
180-
ChainStep("gemini-cli", settings.gemini_cli_model or "gemini-2.5-pro"),
181-
]
182-
else:
183-
primary = ChainStep("gemini", "gemini-2.5-flash")
184-
fallbacks = [
185-
ChainStep("gemini", "gemini-2.5-pro"),
186-
ChainStep("gemini", "gemini-2.5-flash-lite"),
187-
ChainStep("claude-cli", settings.claude_cli_model or "sonnet"),
188-
ChainStep("codex-cli", settings.codex_cli_model or "gpt-5"),
189-
ChainStep("gemini-cli", settings.gemini_cli_model or "gemini-2.5-flash"),
190-
]
177+
def _is_escalation_allowed(messages: Iterable[Message]) -> bool:
178+
full_text = "\n".join(_extract_last_user_text([m]) for m in messages).lower()
179+
180+
if any(keyword in full_text for keyword in TRIVIAL_KEYWORDS):
181+
return False
182+
183+
error_count = full_text.count("build failed") + full_text.count("test failed")
184+
if error_count >= 3:
185+
return True
186+
187+
if any(keyword in full_text for keyword in ESCALATION_KEYWORDS):
188+
return True
189+
190+
return False
191+
192+
def _lane_default_chain(settings: Settings, *, lane: RouteLane, tier: str, messages: Iterable[Message] = ()) -> tuple[ChainStep, tuple[ChainStep, ...]]:
193+
primary = ChainStep("openai", settings.requested_model or "gemini-free")
194+
fallbacks = [
195+
ChainStep("openai", "groq-fast-free"),
196+
ChainStep("openai", "openrouter/free"),
197+
ChainStep("openai", "github-models"),
198+
ChainStep("openai", "cerebras-free"),
199+
ChainStep("openai", "mistral-free"),
200+
ChainStep("openai", "huggingface-free"),
201+
ChainStep("openai", "qwen2.5-coder:7b"),
202+
]
203+
204+
if _is_escalation_allowed(messages):
205+
fallbacks.append(ChainStep("openai", "claude-paid-escalation"))
206+
191207
return primary, tuple(fallbacks)
192208

193209

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@ jsonschema>=4.23,<5.0
99
openapi-spec-validator>=0.7,<1.0
1010
pydantic>=2.0,<3.0
1111
pytest>=8.0,<10.0
12+
pytest-cov>=5.0,<8.0
1213
python-dotenv>=1.0,<2.0
1314
uvicorn>=0.30,<1.0

scripts/9router_proxy.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import os
2+
from fastapi import FastAPI, Request, Response
3+
import httpx
4+
import uvicorn
5+
6+
app = FastAPI()
7+
8+
# Mappings for logical models to physical providers
9+
PROVIDER_URLS = {
10+
"openrouter/free": "https://openrouter.ai/api/v1/chat/completions",
11+
"gemini-free": "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions",
12+
"groq-fast-free": "https://api.groq.com/openai/v1/chat/completions",
13+
"github-models": "https://models.inference.ai.azure.com/chat/completions",
14+
"claude-paid-escalation": "https://api.anthropic.com/v1/messages",
15+
}
16+
17+
API_KEYS = {
18+
"openrouter/free": os.getenv("OPENROUTER_API_KEY"),
19+
"gemini-free": os.getenv("GEMINI_API_KEY"),
20+
"groq-fast-free": os.getenv("GROQ_API_KEY"),
21+
"github-models": os.getenv("GITHUB_TOKEN"),
22+
"claude-paid-escalation": os.getenv("ANTHROPIC_API_KEY"),
23+
}
24+
25+
@app.post("/v1/chat/completions")
26+
async def proxy_completions(request: Request):
27+
body = await request.json()
28+
model = body.get("model", "openrouter/free")
29+
30+
target_url = PROVIDER_URLS.get(model, "https://openrouter.ai/api/v1/chat/completions")
31+
api_key = API_KEYS.get(model)
32+
33+
headers = {
34+
"Content-Type": "application/json",
35+
"Authorization": f"Bearer {api_key}" if api_key else ""
36+
}
37+
38+
# Forward the request
39+
async with httpx.AsyncClient() as client:
40+
proxy_resp = await client.post(target_url, json=body, headers=headers)
41+
42+
return Response(content=proxy_resp.content, status_code=proxy_resp.status_code, media_type="application/json")
43+
44+
if __name__ == "__main__":
45+
print("Starting 9Router Proxy at http://localhost:20128/v1")
46+
uvicorn.run(app, host="127.0.0.1", port=20128)

0 commit comments

Comments
 (0)