Skip to content

Commit 60c2c94

Browse files
Asaif AliAsaif Ali
authored andcommitted
Gateway Fixes
1 parent 86e94fd commit 60c2c94

4 files changed

Lines changed: 103 additions & 89 deletions

File tree

agent_service/app/infrastructure/agents_backend/model_provider.py

Lines changed: 40 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,15 @@
2020
# Environment Variables
2121
# --------------------------------------------------
2222
MODEL_TYPE = os.getenv("MODEL_TYPE", "OpenAI")
23-
OPENAI_MODEL_ID = os.getenv("OPENAI_MODEL_ID", "gemini-2.5-flash")
24-
OPENAI_BASE_URL = os.getenv("LLM_BASE_URL") or os.getenv("OPENAI_BASE_URL")
23+
OPENAI_MODEL_ID = os.getenv("LLM_MODEL") or os.getenv("OPENAI_MODEL_ID", "gateway-managed")
2524
LLM_GATEWAY_URL = os.getenv("LLM_GATEWAY_URL", "https://portfolio-llm-gateway.onrender.com/v1").strip()
2625
LLM_GATEWAY_TIMEOUT = float(os.getenv("LLM_GATEWAY_TIMEOUT", "180"))
2726
# Demo/portfolio gateways commonly rate-limit bursts. Avoid immediate SDK retries
2827
# that amplify a 429; make the value configurable for production.
29-
LLM_GATEWAY_MAX_RETRIES = max(0, int(os.getenv("LLM_GATEWAY_MAX_RETRIES", "2")))
28+
LLM_GATEWAY_MAX_RETRIES = max(0, int(os.getenv("LLM_GATEWAY_MAX_RETRIES", "0")))
3029
VLLM_BASE_URL = os.getenv("VLLM_BASE_URL")
3130
VLLM_CHAT_MODEL_ID = os.getenv("VLLM_CHAT_MODEL_ID")
3231
VLLM_API_KEY = os.getenv("VLLM_API_KEY") or "local"
33-
DIRECT_PROVIDER = os.getenv("LLM_DIRECT_PROVIDER", "google").strip().lower()
34-
GOOGLE_OPENAI_BASE_URL = os.getenv("GOOGLE_OPENAI_BASE_URL", "https://generativelanguage.googleapis.com/v1beta/openai/").strip()
35-
GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY", "").strip()
3632
# --------------------------------------------------
3733
# Message Normalization (vLLM compatibility)
3834
# --------------------------------------------------
@@ -92,80 +88,47 @@ def _gateway_config(self):
9288

9389
def get_client(self):
9490
token, gateway_url = self._gateway_config()
95-
if token:
96-
if not gateway_url:
97-
raise RuntimeError("LLM gateway token present but LLM_GATEWAY_URL is not configured")
98-
logger.info(
99-
"Using request-scoped Portfolio LLM Gateway for model=%s base_url=%s",
100-
self.id,
101-
gateway_url,
91+
if not token:
92+
raise RuntimeError(
93+
"LLM gateway session token is required. Pass X-LLM-Gateway-Token from the portfolio session."
10294
)
103-
return OpenAI(
104-
api_key=token,
105-
base_url=gateway_url,
106-
timeout=max(float(self.timeout or 0), LLM_GATEWAY_TIMEOUT),
107-
max_retries=LLM_GATEWAY_MAX_RETRIES if self.max_retries in (None, 0) else int(self.max_retries),
108-
default_headers=self.default_headers,
109-
default_query=self.default_query,
110-
)
111-
# Standalone LegacyLens deployments do not have a portfolio session token.
112-
# Use a remote OpenAI-compatible free-tier provider instead of silently
113-
# falling through to an unauthenticated OpenRouter endpoint.
114-
if DIRECT_PROVIDER == "google":
115-
if not GOOGLE_API_KEY:
116-
raise RuntimeError(
117-
"No LLM gateway session token and GOOGLE_API_KEY is not configured for direct Gemini fallback"
118-
)
119-
logger.info(
120-
"No gateway token; using direct Google Gemini OpenAI-compatible endpoint for model=%s",
121-
self.id,
122-
)
123-
return OpenAI(
124-
api_key=GOOGLE_API_KEY,
125-
base_url=GOOGLE_OPENAI_BASE_URL,
126-
timeout=max(float(self.timeout or 0), LLM_GATEWAY_TIMEOUT),
127-
max_retries=LLM_GATEWAY_MAX_RETRIES if self.max_retries in (None, 0) else int(self.max_retries),
128-
default_headers=self.default_headers,
129-
default_query=self.default_query,
130-
)
131-
return super().get_client()
95+
if not gateway_url:
96+
raise RuntimeError("LLM gateway token present but LLM_GATEWAY_URL is not configured")
97+
logger.info(
98+
"Using request-scoped Portfolio LLM Gateway for model=%s base_url=%s",
99+
self.id,
100+
gateway_url,
101+
)
102+
return OpenAI(
103+
api_key=token,
104+
base_url=gateway_url,
105+
timeout=max(float(self.timeout or 0), LLM_GATEWAY_TIMEOUT),
106+
max_retries=LLM_GATEWAY_MAX_RETRIES if self.max_retries in (None, 0) else int(self.max_retries),
107+
default_headers=self.default_headers,
108+
default_query=self.default_query,
109+
)
132110

133111
def get_async_client(self):
134112
token, gateway_url = self._gateway_config()
135-
if token:
136-
if not gateway_url:
137-
raise RuntimeError("LLM gateway token present but LLM_GATEWAY_URL is not configured")
138-
logger.info(
139-
"Using request-scoped Portfolio LLM Gateway (async) for model=%s base_url=%s",
140-
self.id,
141-
gateway_url,
113+
if not token:
114+
raise RuntimeError(
115+
"LLM gateway session token is required. Pass X-LLM-Gateway-Token from the portfolio session."
142116
)
143-
return AsyncOpenAI(
144-
api_key=token,
145-
base_url=gateway_url,
146-
timeout=max(float(self.timeout or 0), LLM_GATEWAY_TIMEOUT),
147-
max_retries=LLM_GATEWAY_MAX_RETRIES if self.max_retries in (None, 0) else int(self.max_retries),
148-
default_headers=self.default_headers,
149-
default_query=self.default_query,
150-
)
151-
if DIRECT_PROVIDER == "google":
152-
if not GOOGLE_API_KEY:
153-
raise RuntimeError(
154-
"No LLM gateway session token and GOOGLE_API_KEY is not configured for direct Gemini fallback"
155-
)
156-
logger.info(
157-
"No gateway token; using direct Google Gemini OpenAI-compatible endpoint (async) for model=%s",
158-
self.id,
159-
)
160-
return AsyncOpenAI(
161-
api_key=GOOGLE_API_KEY,
162-
base_url=GOOGLE_OPENAI_BASE_URL,
163-
timeout=max(float(self.timeout or 0), LLM_GATEWAY_TIMEOUT),
164-
max_retries=LLM_GATEWAY_MAX_RETRIES if self.max_retries in (None, 0) else int(self.max_retries),
165-
default_headers=self.default_headers,
166-
default_query=self.default_query,
167-
)
168-
return super().get_async_client()
117+
if not gateway_url:
118+
raise RuntimeError("LLM gateway token present but LLM_GATEWAY_URL is not configured")
119+
logger.info(
120+
"Using request-scoped Portfolio LLM Gateway (async) for model=%s base_url=%s",
121+
self.id,
122+
gateway_url,
123+
)
124+
return AsyncOpenAI(
125+
api_key=token,
126+
base_url=gateway_url,
127+
timeout=max(float(self.timeout or 0), LLM_GATEWAY_TIMEOUT),
128+
max_retries=LLM_GATEWAY_MAX_RETRIES if self.max_retries in (None, 0) else int(self.max_retries),
129+
default_headers=self.default_headers,
130+
default_query=self.default_query,
131+
)
169132

170133

171134

@@ -177,8 +140,8 @@ def create_model():
177140
if MODEL_TYPE == "OpenAI":
178141
model = GatewayAwareOpenAIChat(
179142
id=OPENAI_MODEL_ID,
180-
api_key=os.getenv("OPENAI_API_KEY"),
181-
base_url=OPENAI_BASE_URL,
143+
api_key="gateway-session",
144+
base_url=LLM_GATEWAY_URL,
182145
temperature=0.1,
183146
)
184147
logger.info("Using OpenAI-compatible model with request-scoped gateway support")

agent_service/app/presentation/routes/agent_router.py

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ async def upload_team_files(
179179

180180
async def execute_agent_team(task_id: str, request: RunTeamRequest, owner_user_id: str, llm_gateway_token: str = ""):
181181
logger.info("[TASK %s] STARTED | source_path=%r owner=%r", task_id, request.source_path, owner_user_id)
182-
update_task(task_id, status=AgentConstants.TASK_STATUS_RUNNING)
182+
create_task(task_id, user_id=owner_user_id, status=AgentConstants.TASK_STATUS_RUNNING)
183183
# Connect workflow progress events to this persisted task so the UI
184184
# receives live updates through GET /v1/tasks/{task_id}.
185185
bind_task(task_id)
@@ -337,6 +337,21 @@ async def download_migration_v1(migration_name: str, user=Depends(get_current_us
337337
)
338338

339339

340+
def _require_llm_gateway_token(token: str | None) -> str:
341+
"""Require the portfolio-issued short-lived LLM gateway JWT."""
342+
value = (token or "").strip()
343+
if not value:
344+
raise HTTPException(
345+
status_code=401,
346+
detail={
347+
"code": "LLM_GATEWAY_SESSION_REQUIRED",
348+
"message": "A Portfolio LLM Gateway session token is required for AI operations.",
349+
"header": "X-LLM-Gateway-Token",
350+
},
351+
)
352+
return value
353+
354+
340355
@router.post("/v1/teams/run", response_model=TaskAcceptedResponse)
341356
async def run_agent_team(
342357
request: RunTeamRequest,
@@ -345,16 +360,14 @@ async def run_agent_team(
345360
user=Depends(get_current_user),
346361
):
347362
current_user.set(user)
363+
gateway_token = _require_llm_gateway_token(x_llm_gateway_token)
348364
task_id = str(uuid.uuid4())
349365
logger.info("POST /v1/teams/run | task_id=%s", task_id)
350-
# Persist the task before returning 202 so an immediate UI poll cannot
351-
# race the BackgroundTasks startup and receive a false 404.
352-
create_task(task_id, user_id=str(user.id), status=AgentConstants.TASK_STATUS_ACCEPTED)
353366

354367
# Stack detection uses an LLM and can take longer than the UI's request
355368
# timeout. Do it inside execute_agent_team, where it belongs, after this
356369
# endpoint has returned a task ID.
357-
background_tasks.add_task(execute_agent_team, task_id, request, str(user.id), (x_llm_gateway_token or "").strip())
370+
background_tasks.add_task(execute_agent_team, task_id, request, str(user.id), gateway_token)
358371
return TaskAcceptedResponse(
359372
task_id=task_id,
360373
message=AgentConstants.AGENT_TEAM_EXECUTION_QUEUED,
@@ -403,6 +416,7 @@ async def health_check():
403416
@router.post("/v1/chat/ask", response_model=ChatAskResponse)
404417
async def chat_ask(
405418
request: ChatAskRequest,
419+
x_llm_gateway_token: str | None = Header(default=None, alias="X-LLM-Gateway-Token"),
406420
user=Depends(get_current_user),
407421
) -> ChatAskResponse:
408422
try:
@@ -411,6 +425,7 @@ async def chat_ask(
411425
except Exception:
412426
pass
413427
_set_migration_context(request.migration_name, user)
428+
set_llm_gateway_token(_require_llm_gateway_token(x_llm_gateway_token))
414429
from app.application.agents.chat.chat_tools import ask_kb_impl
415430

416431
ask_kb_callable = _resolve_tool_callable(ask_kb_impl)
@@ -428,6 +443,7 @@ async def chat_ask(
428443
@router.post("/v1/report/migration")
429444
async def migration_report(
430445
request: MigrationReportRequest,
446+
x_llm_gateway_token: str | None = Header(default=None, alias="X-LLM-Gateway-Token"),
431447
user=Depends(get_current_user),
432448
):
433449
try:
@@ -436,6 +452,7 @@ async def migration_report(
436452
except Exception:
437453
pass
438454
_set_migration_context(request.migration_name, user)
455+
set_llm_gateway_token(_require_llm_gateway_token(x_llm_gateway_token))
439456
from app.infrastructure.utils.reporting_manager import generate_migration_comparison_report
440457

441458
report_callable = _resolve_tool_callable(generate_migration_comparison_report)
@@ -454,6 +471,7 @@ async def migration_report(
454471
@router.post("/v1/showcase/migration")
455472
async def migration_showcase(
456473
request: MigrationShowcaseRequest,
474+
x_llm_gateway_token: str | None = Header(default=None, alias="X-LLM-Gateway-Token"),
457475
user=Depends(get_current_user),
458476
):
459477
try:
@@ -462,6 +480,7 @@ async def migration_showcase(
462480
except Exception:
463481
pass
464482
_set_migration_context(request.migration_name, user)
483+
set_llm_gateway_token(_require_llm_gateway_token(x_llm_gateway_token))
465484
from app.infrastructure.utils.showcase_manager import generate_showcase_bundle
466485

467486
showcase_callable = _resolve_tool_callable(generate_showcase_bundle)
@@ -477,6 +496,7 @@ def _run_post_migration_background(
477496
migrated_code_path,
478497
persist: bool,
479498
user,
499+
llm_gateway_token: str = "",
480500
) -> None:
481501
"""Background task: sets context vars and runs the full post-migration workflow."""
482502
try:
@@ -485,6 +505,7 @@ def _run_post_migration_background(
485505
pass
486506
try:
487507
_set_migration_context(migration_name, user)
508+
set_llm_gateway_token((llm_gateway_token or "").strip())
488509
except Exception:
489510
pass
490511
try:
@@ -505,18 +526,21 @@ def _run_post_migration_background(
505526
async def run_post_migration(
506527
request: PostMigrationRunRequest,
507528
background_tasks: BackgroundTasks,
529+
x_llm_gateway_token: str | None = Header(default=None, alias="X-LLM-Gateway-Token"),
508530
user=Depends(get_current_user),
509531
):
510532
try:
511533
current_user.set(user)
512534
except Exception:
513535
pass
536+
gateway_token = _require_llm_gateway_token(x_llm_gateway_token)
514537
background_tasks.add_task(
515538
_run_post_migration_background,
516539
request.migration_name,
517540
request.migrated_code_path,
518541
bool(request.persist),
519542
user,
543+
gateway_token,
520544
)
521545
return {
522546
"status": "accepted",

render.yaml

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,14 @@ services:
1515
value: "10000"
1616
- key: MODEL_TYPE
1717
value: "OpenAI"
18-
- key: OPENAI_BASE_URL
19-
value: "https://openrouter.ai/api/v1"
2018
- key: OPENAI_MODEL_ID
21-
value: "openai/gpt-4o-mini"
19+
value: "gateway-managed"
2220
- key: LLM_GATEWAY_URL
2321
value: "https://portfolio-llm-gateway.onrender.com/v1"
2422
- key: LLM_GATEWAY_TIMEOUT
2523
value: "180"
26-
- key: LLM_MODEL
27-
value: "gemini-3.5-flash-lite"
24+
- key: LLM_GATEWAY_REQUIRED
25+
value: "true"
2826
- key: QDRANT_URL
2927
sync: false
3028
- key: QDRANT_API_KEY
@@ -39,8 +37,6 @@ services:
3937
value: "qdrant/bm25"
4038
- key: QDRANT_DENSE_DIMENSIONS
4139
value: "384"
42-
- key: OPENAI_API_KEY
43-
sync: false
4440
- key: DATABASE_URL
4541
sync: false
4642
- key: ALLOWED_ORIGINS
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from pathlib import Path
2+
3+
ROOT = Path(__file__).resolve().parents[1]
4+
MODEL_PROVIDER = ROOT / "agent_service/app/infrastructure/agents_backend/model_provider.py"
5+
ROUTER = ROOT / "agent_service/app/presentation/routes/agent_router.py"
6+
RENDER = ROOT / "render.yaml"
7+
8+
9+
def test_render_is_gateway_only():
10+
text = RENDER.read_text(encoding="utf-8")
11+
assert 'LLM_GATEWAY_URL' in text
12+
assert 'LLM_GATEWAY_REQUIRED' in text
13+
assert 'OPENAI_BASE_URL' not in text
14+
assert 'OPENAI_API_KEY' not in text
15+
assert 'LLM_MODEL' not in text
16+
17+
18+
def test_model_provider_has_no_direct_openai_fallback():
19+
text = MODEL_PROVIDER.read_text(encoding="utf-8")
20+
assert 'return super().get_client()' not in text
21+
assert 'return super().get_async_client()' not in text
22+
assert 'LLM gateway session token is required' in text
23+
assert 'api_key="gateway-session"' in text
24+
25+
26+
def test_ai_routes_require_and_propagate_gateway_token():
27+
text = ROUTER.read_text(encoding="utf-8")
28+
assert text.count('alias="X-LLM-Gateway-Token"') >= 5
29+
assert '_require_llm_gateway_token' in text
30+
assert 'background_tasks.add_task(execute_agent_team, task_id, request, str(user.id), gateway_token)' in text
31+
assert 'gateway_token,' in text

0 commit comments

Comments
 (0)