fix: OpenRouter model routing regression + project input validation
- resolve_model_provider: fix regression where OpenRouter model IDs like
openai/gpt-5.4-mini had their prefix stripped, causing AIAgent to look
for OPENAI_API_KEY (direct API) instead of routing through OpenRouter.
All chats returned Connection lost for OpenRouter users. Fix: only strip
prefix and use direct-API when config.provider explicitly matches that
provider; pass full provider/model string through for openrouter.
- Project name: cap at 128 chars, reject empty after strip on create/rename
- Project color: validate ^#[0-9a-fA-F]{3,8}$ to prevent CSS injection
via dot.style.background in sessions.js
- Remove 2 redundant sys.path.insert() calls in cron handlers
Tests: 214 passed, 23 pre-existing failures, 0 regressions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -343,12 +343,14 @@ def resolve_model_provider(model_id: str):
|
|||||||
|
|
||||||
if '/' in model_id:
|
if '/' in model_id:
|
||||||
prefix, bare = model_id.split('/', 1)
|
prefix, bare = model_id.split('/', 1)
|
||||||
# If prefix matches config provider, strip it
|
# If prefix matches config provider, strip it and use that provider directly
|
||||||
if config_provider and prefix == config_provider:
|
if config_provider and prefix == config_provider:
|
||||||
return bare, config_provider, config_base_url
|
return bare, config_provider, config_base_url
|
||||||
# If prefix is a known direct-API provider, use it
|
# If the config provider is openrouter (or unset/None), pass the full
|
||||||
# (base_url only applies when matching config provider)
|
# provider/model string through -- OpenRouter uses this as its model ID.
|
||||||
if prefix in _PROVIDER_MODELS:
|
# Only strip the prefix and switch to a direct-API provider when the
|
||||||
|
# config is explicitly set to that direct provider.
|
||||||
|
if config_provider and config_provider != 'openrouter' and prefix in _PROVIDER_MODELS:
|
||||||
return bare, prefix, None
|
return bare, prefix, None
|
||||||
|
|
||||||
return model_id, config_provider, config_base_url
|
return model_id, config_provider, config_base_url
|
||||||
|
|||||||
@@ -140,7 +140,6 @@ def handle_get(handler, parsed):
|
|||||||
|
|
||||||
# ── Cron API (GET) ──
|
# ── Cron API (GET) ──
|
||||||
if parsed.path == '/api/crons':
|
if parsed.path == '/api/crons':
|
||||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
||||||
from cron.jobs import list_jobs
|
from cron.jobs import list_jobs
|
||||||
return j(handler, {'jobs': list_jobs(include_disabled=True)})
|
return j(handler, {'jobs': list_jobs(include_disabled=True)})
|
||||||
|
|
||||||
@@ -345,8 +344,14 @@ def handle_post(handler, parsed):
|
|||||||
if parsed.path == '/api/projects/create':
|
if parsed.path == '/api/projects/create':
|
||||||
try: require(body, 'name')
|
try: require(body, 'name')
|
||||||
except ValueError as e: return bad(handler, str(e))
|
except ValueError as e: return bad(handler, str(e))
|
||||||
|
import re as _re
|
||||||
|
name = body['name'].strip()[:128]
|
||||||
|
if not name: return bad(handler, 'name required')
|
||||||
|
color = body.get('color')
|
||||||
|
if color and not _re.match(r'^#[0-9a-fA-F]{3,8}$', color):
|
||||||
|
return bad(handler, 'Invalid color format')
|
||||||
projects = load_projects()
|
projects = load_projects()
|
||||||
proj = {'project_id': uuid.uuid4().hex[:12], 'name': body['name'], 'color': body.get('color'), 'created_at': time.time()}
|
proj = {'project_id': uuid.uuid4().hex[:12], 'name': name, 'color': color, 'created_at': time.time()}
|
||||||
projects.append(proj)
|
projects.append(proj)
|
||||||
save_projects(projects)
|
save_projects(projects)
|
||||||
return j(handler, {'ok': True, 'project': proj})
|
return j(handler, {'ok': True, 'project': proj})
|
||||||
@@ -354,11 +359,16 @@ def handle_post(handler, parsed):
|
|||||||
if parsed.path == '/api/projects/rename':
|
if parsed.path == '/api/projects/rename':
|
||||||
try: require(body, 'project_id', 'name')
|
try: require(body, 'project_id', 'name')
|
||||||
except ValueError as e: return bad(handler, str(e))
|
except ValueError as e: return bad(handler, str(e))
|
||||||
|
import re as _re
|
||||||
projects = load_projects()
|
projects = load_projects()
|
||||||
proj = next((p for p in projects if p['project_id'] == body['project_id']), None)
|
proj = next((p for p in projects if p['project_id'] == body['project_id']), None)
|
||||||
if not proj: return bad(handler, 'Project not found', 404)
|
if not proj: return bad(handler, 'Project not found', 404)
|
||||||
proj['name'] = body['name']
|
proj['name'] = body['name'].strip()[:128]
|
||||||
if 'color' in body: proj['color'] = body['color']
|
if 'color' in body:
|
||||||
|
color = body['color']
|
||||||
|
if color and not _re.match(r'^#[0-9a-fA-F]{3,8}$', color):
|
||||||
|
return bad(handler, 'Invalid color format')
|
||||||
|
proj['color'] = color
|
||||||
save_projects(projects)
|
save_projects(projects)
|
||||||
return j(handler, {'ok': True, 'project': proj})
|
return j(handler, {'ok': True, 'project': proj})
|
||||||
|
|
||||||
@@ -594,7 +604,6 @@ def _handle_cron_recent(handler, parsed):
|
|||||||
qs = parse_qs(parsed.query)
|
qs = parse_qs(parsed.query)
|
||||||
since = float(qs.get('since', ['0'])[0])
|
since = float(qs.get('since', ['0'])[0])
|
||||||
try:
|
try:
|
||||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
||||||
from cron.jobs import list_jobs
|
from cron.jobs import list_jobs
|
||||||
jobs = list_jobs(include_disabled=True)
|
jobs = list_jobs(include_disabled=True)
|
||||||
completions = []
|
completions = []
|
||||||
|
|||||||
Reference in New Issue
Block a user