Skip to content

Commit 950d530

Browse files
Asaif AliAsaif Ali
authored andcommitted
Stabilize LegacyLens gateway and low-memory embeddings
1 parent a48fd27 commit 950d530

2 files changed

Lines changed: 90 additions & 16 deletions

File tree

agent_service/app/infrastructure/agents_backend/model_provider.py

Lines changed: 80 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
from agno.knowledge.embedder.google import GeminiEmbedder
1212
from agno.knowledge.embedder.ollama import OllamaEmbedder
1313
from agno.knowledge.embedder.fastembed import FastEmbedEmbedder
14+
import hashlib
15+
import math
16+
1417
# from agno.models.vllm import VLLM
1518
from openai import AsyncOpenAI, OpenAI
1619
# --------------------------------------------------
@@ -26,6 +29,9 @@
2629
OPENAI_MODEL_ID = os.getenv("LLM_MODEL") or os.getenv("OPENAI_MODEL_ID", "gpt-4o")
2730
OPENAI_BASE_URL = os.getenv("LLM_BASE_URL") or os.getenv("OPENAI_BASE_URL")
2831
LLM_GATEWAY_URL = os.getenv("LLM_GATEWAY_URL", "https://portfolio-llm-gateway.onrender.com/v1").strip()
32+
LLM_GATEWAY_TIMEOUT = float(os.getenv("LLM_GATEWAY_TIMEOUT", "180"))
33+
EMBED_MODEL_TYPE = os.getenv("EMBED_MODEL_TYPE", "fastembed").strip().lower()
34+
EMBED_DIMENSIONS = int(os.getenv("EMBED_DIMENSIONS", "384"))
2935
EMBEDDING_BASE_URL = os.getenv("EMBEDDING_BASE_URL")
3036
VLLM_BASE_URL = os.getenv("VLLM_BASE_URL")
3137
VLLM_CHAT_MODEL_ID = os.getenv("VLLM_CHAT_MODEL_ID")
@@ -128,11 +134,15 @@ class GatewayAwareOpenAIChat(OpenAIChat):
128134
def _gateway_config(self):
129135
token = get_llm_gateway_token().strip()
130136
gateway_url = LLM_GATEWAY_URL.strip()
137+
if gateway_url.endswith("/"):
138+
gateway_url = gateway_url[:-1]
131139
return token, gateway_url
132140

133141
def get_client(self):
134142
token, gateway_url = self._gateway_config()
135-
if token and gateway_url:
143+
if token:
144+
if not gateway_url:
145+
raise RuntimeError("LLM gateway token present but LLM_GATEWAY_URL is not configured")
136146
logger.info(
137147
"Using request-scoped Portfolio LLM Gateway for model=%s base_url=%s",
138148
self.id,
@@ -141,16 +151,18 @@ def get_client(self):
141151
return OpenAI(
142152
api_key=token,
143153
base_url=gateway_url,
144-
timeout=self.timeout,
145-
max_retries=self.max_retries,
154+
timeout=max(float(self.timeout or 0), LLM_GATEWAY_TIMEOUT),
155+
max_retries=max(int(self.max_retries or 0), 2),
146156
default_headers=self.default_headers,
147157
default_query=self.default_query,
148158
)
149159
return super().get_client()
150160

151161
def get_async_client(self):
152162
token, gateway_url = self._gateway_config()
153-
if token and gateway_url:
163+
if token:
164+
if not gateway_url:
165+
raise RuntimeError("LLM gateway token present but LLM_GATEWAY_URL is not configured")
154166
logger.info(
155167
"Using request-scoped Portfolio LLM Gateway (async) for model=%s base_url=%s",
156168
self.id,
@@ -159,13 +171,69 @@ def get_async_client(self):
159171
return AsyncOpenAI(
160172
api_key=token,
161173
base_url=gateway_url,
162-
timeout=self.timeout,
163-
max_retries=self.max_retries,
174+
timeout=max(float(self.timeout or 0), LLM_GATEWAY_TIMEOUT),
175+
max_retries=max(int(self.max_retries or 0), 2),
164176
default_headers=self.default_headers,
165177
default_query=self.default_query,
166178
)
167179
return super().get_async_client()
168180

181+
182+
class LowMemoryHashEmbedder:
183+
"""Dependency-free fallback embedder for small Render instances.
184+
185+
Produces deterministic, normalized vectors using hashed token/character
186+
features. It avoids loading an ONNX embedding model into the service.
187+
For this demo-oriented deployment, LanceDB can still perform vector/
188+
lexical retrieval without the memory spike caused by FastEmbed startup.
189+
"""
190+
191+
def __init__(self, dimensions: int = 384):
192+
self.id = "portfolio-hash-embedder"
193+
self.dimensions = dimensions
194+
195+
@staticmethod
196+
def _tokens(text: str):
197+
text = str(text or "").lower()
198+
tokens = []
199+
cur = []
200+
for ch in text:
201+
if ch.isalnum() or ch == "_":
202+
cur.append(ch)
203+
elif cur:
204+
tokens.append("".join(cur))
205+
cur = []
206+
if cur:
207+
tokens.append("".join(cur))
208+
return tokens
209+
210+
def get_embedding(self, text):
211+
vec = [0.0] * self.dimensions
212+
tokens = self._tokens(text)
213+
if not tokens:
214+
return vec
215+
for token in tokens:
216+
for feature in (token, token[:3], token[-3:]):
217+
digest = hashlib.blake2b(feature.encode("utf-8"), digest_size=8).digest()
218+
idx = int.from_bytes(digest, "little") % self.dimensions
219+
sign = 1.0 if (digest[0] & 1) else -1.0
220+
vec[idx] += sign
221+
norm = math.sqrt(sum(x * x for x in vec)) or 1.0
222+
return [x / norm for x in vec]
223+
224+
def get_embedding_and_usage(self, text):
225+
return self.get_embedding(text), {"prompt_tokens": 0, "total_tokens": 0}
226+
227+
def embed_documents(self, texts):
228+
return [self.get_embedding(t) for t in texts]
229+
230+
231+
def create_embedder():
232+
if EMBED_MODEL_TYPE in {"hash", "low_memory", "low-memory"}:
233+
logger.warning("Using low-memory deterministic embeddings for constrained deployment")
234+
return LowMemoryHashEmbedder(dimensions=EMBED_DIMENSIONS)
235+
return FastEmbedEmbedder(id=os.getenv("FASTEMBED_MODEL", "BAAI/bge-small-en-v1.5"), dimensions=EMBED_DIMENSIONS)
236+
169237
# --------------------------------------------------
170238
# Model Factory
171239
# --------------------------------------------------
@@ -178,8 +246,8 @@ def create_model_and_embedder():
178246
base_url=OPENAI_BASE_URL,
179247
temperature=0.1
180248
)
181-
embedder = FastEmbedEmbedder()
182-
logger.info("Using OpenAI-compatible model with FastEmbed local embeddings")
249+
embedder = create_embedder()
250+
logger.info("Using OpenAI-compatible model with constrained local embeddings")
183251
elif MODEL_TYPE == "VLLM":
184252
if not VLLM_CHAT_MODEL_ID or not VLLM_BASE_URL:
185253
raise ValueError("VLLM config missing")
@@ -189,15 +257,15 @@ def create_model_and_embedder():
189257
api_key=VLLM_API_KEY,
190258
temperature=0.1
191259
)
192-
embedder = FastEmbedEmbedder()
260+
embedder = create_embedder()
193261
logger.info(f"✅ Using vLLM model: {VLLM_CHAT_MODEL_ID}")
194262
elif MODEL_TYPE == "Gemini":
195263
model = Gemini(
196264
id=os.getenv("GEMINI_MODEL_ID"),
197265
api_key=os.getenv("GOOGLE_API_KEY"),
198266
temperature=0.1
199267
)
200-
embedder = FastEmbedEmbedder()
268+
embedder = create_embedder()
201269
logger.info("✅ Using Gemini model")
202270
elif MODEL_TYPE == "Ollama":
203271
model = Ollama(
@@ -216,8 +284,8 @@ def create_model_and_embedder():
216284
supports_native_structured_outputs=True,
217285
supports_json_schema_outputs=True,
218286
)
219-
embedder = FastEmbedEmbedder()
220-
logger.info("✅ Using FastEmbed local embeddings")
287+
embedder = create_embedder()
288+
logger.info("✅ Using configured local embeddings")
221289
else:
222290
raise ValueError(f"Unsupported MODEL_TYPE: {MODEL_TYPE}")
223291
return model, embedder

render.yaml

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,20 @@ services:
1414
value: "10000"
1515
- key: MODEL_TYPE
1616
value: "OpenAI"
17-
- key: LLM_GATEWAY_URL
18-
value: "https://portfolio-llm-gateway.onrender.com/v1"
1917
- key: OPENAI_BASE_URL
2018
value: "https://openrouter.ai/api/v1"
2119
- key: OPENAI_MODEL_ID
2220
value: "openai/gpt-4o-mini"
21+
- key: LLM_GATEWAY_URL
22+
value: "https://portfolio-llm-gateway.onrender.com/v1"
23+
- key: LLM_GATEWAY_TIMEOUT
24+
value: "180"
25+
- key: LLM_MODEL
26+
value: "gemini-3.5-flash-lite"
27+
- key: EMBED_MODEL_TYPE
28+
value: "hash"
29+
- key: EMBED_DIMENSIONS
30+
value: "384"
2331
- key: OPENAI_API_KEY
2432
sync: false
2533
- key: DATABASE_URL
@@ -30,8 +38,6 @@ services:
3038
value: "true"
3139
- key: STACK_ANALYZER_VERSION
3240
value: "1.27.6"
33-
- key: EMBED_MODEL_TYPE
34-
value: "fastembed"
3541
- key: OTEL_ENABLED
3642
value: "false"
3743
- key: PUBLIC_API_URL

0 commit comments

Comments
 (0)