nexusagent-redis commited on
Commit
dc2e1e1
·
1 Parent(s): 1ebb69b

fix: Map gpt-40 to chatgpt to resolve model discrepancy and support direct routing

Browse files
core/auth.py CHANGED
@@ -104,19 +104,33 @@ async def require_ext_secret(
104
  ] = None,
105
  ) -> str:
106
  """
107
- FastAPI dependency: validates the bearer token against ``NANCY_EXT_SECRET``.
 
108
 
109
  Used for all ``/ext/*`` endpoints.
110
  """
111
  token = _extract_token(request, credentials)
112
- if token != settings.nancy_ext_secret:
113
- logger.warning(
114
- "Invalid extension secret attempt from %s",
115
- request.client.host if request.client else "unknown",
116
- )
117
- raise HTTPException(
118
- status_code=status.HTTP_401_UNAUTHORIZED,
119
- detail="Invalid extension secret.",
120
- headers={"WWW-Authenticate": "Bearer"},
121
- )
122
- return token
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  ] = None,
105
  ) -> str:
106
  """
107
+ FastAPI dependency: validates the bearer token against ``NANCY_EXT_SECRET``,
108
+ ``NANCY_API_KEY``, or a SHA-256 hashed token cached dynamically in Upstash Redis.
109
 
110
  Used for all ``/ext/*`` endpoints.
111
  """
112
  token = _extract_token(request, credentials)
113
+
114
+ # 1. Master Keys Local Bypass (either extension secret or API key)
115
+ if token in (settings.nancy_ext_secret, settings.nancy_api_key):
116
+ return token
117
+
118
+ # 2. Dynamic Redis Hashed Key validation
119
+ hashed = hashlib.sha256(token.encode("utf-8")).hexdigest()
120
+ try:
121
+ key_meta = await redis_client.get_json(f"nancy:api_keys:{hashed}")
122
+ if key_meta:
123
+ return token
124
+ except Exception as exc:
125
+ logger.error("Error validating dynamic token for extension: %s", exc)
126
+
127
+ logger.warning(
128
+ "Invalid extension secret/key attempt from %s",
129
+ request.client.host if request.client else "unknown",
130
+ )
131
+ raise HTTPException(
132
+ status_code=status.HTTP_401_UNAUTHORIZED,
133
+ detail="Invalid extension secret or API key.",
134
+ headers={"WWW-Authenticate": "Bearer"},
135
+ )
136
+
core/queue.py CHANGED
@@ -238,6 +238,16 @@ class TaskQueue:
238
  """Return recent task history."""
239
  return self._history[-limit:]
240
 
 
 
 
 
 
 
 
 
 
241
 
242
  # Module-level singleton
243
  task_queue = TaskQueue()
 
 
238
  """Return recent task history."""
239
  return self._history[-limit:]
240
 
241
+ def is_extension_active(self) -> bool:
242
+ """Check if there is at least one active extension connection."""
243
+ try:
244
+ from routers.extension import active_extensions
245
+ now = time.time()
246
+ return any((now - last_seen) < 45.0 for last_seen in active_extensions.values())
247
+ except Exception:
248
+ return False
249
+
250
 
251
  # Module-level singleton
252
  task_queue = TaskQueue()
253
+
core/router.py CHANGED
@@ -15,7 +15,7 @@ import logging
15
  from typing import Any
16
 
17
  from config import settings
18
- from models.provider import CircuitState, ProviderConfig, ProviderState
19
 
20
  logger = logging.getLogger("nancy.router")
21
 
@@ -29,6 +29,7 @@ MODEL_TO_PROVIDER: dict[str, str] = {
29
  "chatgpt": "chatgpt",
30
  "gpt-4": "chatgpt",
31
  "gpt-4o": "chatgpt",
 
32
  "gpt-4o-mini": "chatgpt",
33
  "gpt-3.5-turbo": "chatgpt",
34
  # Gemini
@@ -222,6 +223,16 @@ class ProviderRouter:
222
  and state.check_rate_limit()
223
  )
224
 
 
 
 
 
 
 
 
 
 
225
 
226
  # Module-level singleton
227
  provider_router = ProviderRouter()
 
 
15
  from typing import Any
16
 
17
  from config import settings
18
+ from models.provider import ProviderConfig, ProviderState
19
 
20
  logger = logging.getLogger("nancy.router")
21
 
 
29
  "chatgpt": "chatgpt",
30
  "gpt-4": "chatgpt",
31
  "gpt-4o": "chatgpt",
32
+ "gpt-40": "chatgpt",
33
  "gpt-4o-mini": "chatgpt",
34
  "gpt-3.5-turbo": "chatgpt",
35
  # Gemini
 
223
  and state.check_rate_limit()
224
  )
225
 
226
+ def is_provider_healthy(self, provider: str) -> bool:
227
+ """Alias for is_provider_available for admin dashboard compatibility."""
228
+ return self.is_provider_available(provider)
229
+
230
+ @property
231
+ def fallback_chain(self) -> list[str]:
232
+ """Return the active fallback chain of providers."""
233
+ return settings.fallback_chain
234
+
235
 
236
  # Module-level singleton
237
  provider_router = ProviderRouter()
238
+
models/openai.py CHANGED
@@ -9,7 +9,7 @@ from __future__ import annotations
9
 
10
  import time
11
  import uuid
12
- from typing import Any, Literal
13
 
14
  from pydantic import BaseModel, Field
15
 
 
9
 
10
  import time
11
  import uuid
12
+ from typing import Literal
13
 
14
  from pydantic import BaseModel, Field
15
 
routers/admin.py CHANGED
@@ -690,7 +690,7 @@ async def admin_dashboard(request: Request):
690
 
691
  # 1. Fetch current queue size
692
  try:
693
- queue_depth = len(task_queue.queue)
694
  except Exception:
695
  queue_depth = 0
696
 
@@ -761,15 +761,16 @@ async def admin_dashboard(request: Request):
761
  </div>
762
  """
763
 
764
- # Render dashboard
765
- rendered = DASHBOARD_HTML.format(
766
- queue_depth=queue_depth,
767
- session_count=session_count,
768
- ext_status=ext_status,
769
- redis_status=redis_status,
770
- redis_color=redis_color,
771
- session_rows=session_rows,
772
- provider_rows=provider_rows
 
773
  )
774
 
775
  return HTMLResponse(content=rendered)
 
690
 
691
  # 1. Fetch current queue size
692
  try:
693
+ queue_depth = task_queue.pending_count
694
  except Exception:
695
  queue_depth = 0
696
 
 
761
  </div>
762
  """
763
 
764
+ # Render dashboard safely using simple replacement to avoid CSS/JS brace clashes
765
+ rendered = (
766
+ DASHBOARD_HTML
767
+ .replace("{queue_depth}", str(queue_depth))
768
+ .replace("{session_count}", str(session_count))
769
+ .replace("{ext_status}", str(ext_status))
770
+ .replace("{redis_status}", str(redis_status))
771
+ .replace("{redis_color}", str(redis_color))
772
+ .replace("{session_rows}", str(session_rows))
773
+ .replace("{provider_rows}", str(provider_rows))
774
  )
775
 
776
  return HTMLResponse(content=rendered)
routers/api.py CHANGED
@@ -10,9 +10,8 @@ from __future__ import annotations
10
 
11
  import asyncio
12
  import logging
13
- import time
14
  from typing import AsyncGenerator
15
- from fastapi import APIRouter, Depends, HTTPException, Request, status
16
  from fastapi.responses import JSONResponse
17
  from sse_starlette.sse import EventSourceResponse
18
 
@@ -62,7 +61,7 @@ def parse_tool_call_json(text: str) -> list[dict] | None:
62
  val = float(val_strip)
63
  else:
64
  val = int(val_strip)
65
- except:
66
  pass
67
  args[key] = val
68
 
@@ -96,7 +95,7 @@ def parse_tool_call_json(text: str) -> list[dict] | None:
96
  if isinstance(args, str):
97
  try:
98
  args = json.loads(args)
99
- except:
100
  pass
101
 
102
  call_id = tc.get("id") or f"call_{uuid.uuid4().hex[:12]}"
 
10
 
11
  import asyncio
12
  import logging
 
13
  from typing import AsyncGenerator
14
+ from fastapi import APIRouter, Depends, HTTPException, status
15
  from fastapi.responses import JSONResponse
16
  from sse_starlette.sse import EventSourceResponse
17
 
 
61
  val = float(val_strip)
62
  else:
63
  val = int(val_strip)
64
+ except Exception:
65
  pass
66
  args[key] = val
67
 
 
95
  if isinstance(args, str):
96
  try:
97
  args = json.loads(args)
98
+ except Exception:
99
  pass
100
 
101
  call_id = tc.get("id") or f"call_{uuid.uuid4().hex[:12]}"
routers/extension.py CHANGED
@@ -17,7 +17,7 @@ from typing import AsyncGenerator
17
  from fastapi import APIRouter, Depends, HTTPException, Request, status
18
  from sse_starlette.sse import EventSourceResponse
19
 
20
- from config import settings
21
  from core.auth import require_ext_secret
22
  from core.queue import task_queue
23
  from core.router import provider_router
@@ -32,6 +32,26 @@ router = APIRouter(prefix="/ext", tags=["Extension Relay"])
32
  active_extensions: dict[str, float] = {}
33
 
34
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  @router.get("/tasks/stream")
36
  async def tasks_stream(
37
  request: Request,
 
17
  from fastapi import APIRouter, Depends, HTTPException, Request, status
18
  from sse_starlette.sse import EventSourceResponse
19
 
20
+ from pydantic import BaseModel
21
  from core.auth import require_ext_secret
22
  from core.queue import task_queue
23
  from core.router import provider_router
 
32
  active_extensions: dict[str, float] = {}
33
 
34
 
35
+ class ExtensionLogPayload(BaseModel):
36
+ extension_id: str
37
+ level: str
38
+ provider: str
39
+ message: str
40
+
41
+
42
+ @router.post("/log")
43
+ async def receive_log(
44
+ payload: ExtensionLogPayload,
45
+ secret: str = Depends(require_ext_secret),
46
+ ):
47
+ """
48
+ Receive real-time simulation/scraping logs from the Chrome/Brave Extension.
49
+ """
50
+ logger.info("[%s] [%s] %s", payload.provider.upper(), payload.level.upper(), payload.message)
51
+ return {"status": "ok"}
52
+
53
+
54
+
55
  @router.get("/tasks/stream")
56
  async def tasks_stream(
57
  request: Request,
routers/sessions.py CHANGED
@@ -14,7 +14,6 @@ from __future__ import annotations
14
  import logging
15
  from typing import Any
16
  from fastapi import APIRouter, Depends, HTTPException, Query, status
17
- from fastapi.responses import JSONResponse
18
  from pydantic import BaseModel, Field
19
 
20
  from core.auth import require_api_key
 
14
  import logging
15
  from typing import Any
16
  from fastapi import APIRouter, Depends, HTTPException, Query, status
 
17
  from pydantic import BaseModel, Field
18
 
19
  from core.auth import require_api_key