Skip to content

Commit eac0fc8

Browse files
committed
feat: route LLM calls through LiteLLM for multi-provider support
1 parent d084a3c commit eac0fc8

5 files changed

Lines changed: 54 additions & 34 deletions

File tree

backend/app/condense.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -184,12 +184,11 @@ def _condense_with_claude(section: dict, max_chars: int) -> Optional[dict]:
184184
return None
185185

186186
try:
187-
from anthropic import Anthropic
187+
import litellm
188188

189189
allowed_ids = set(_union_memory_ids(elements))
190190
by_id = _elements_by_id(elements)
191191

192-
client = Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
193192
model = os.environ.get(
194193
"CORTEX_CONDENSE_MODEL",
195194
os.environ.get("CORTEX_EXTRACTION_MODEL", "claude-opus-4-5"),
@@ -203,14 +202,18 @@ def _condense_with_claude(section: dict, max_chars: int) -> Optional[dict]:
203202
)
204203
user = _build_user_prompt(section, elements)
205204

206-
response = client.messages.create(
205+
response = litellm.completion(
207206
model=model,
208207
max_tokens=300,
209-
system=system,
210-
messages=[{"role": "user", "content": user}],
208+
messages=[
209+
{"role": "system", "content": system},
210+
{"role": "user", "content": user},
211+
],
212+
# Drop provider-unsupported params so one config works across providers.
213+
drop_params=True,
211214
)
212215

213-
text = response.content[0].text.strip()
216+
text = (response.choices[0].message.content or "").strip()
214217
text = re.sub(r"^```(?:json)?\s*", "", text)
215218
text = re.sub(r"\s*```$", "", text)
216219
data = json.loads(text)

backend/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ uvicorn>=0.20.0
33
pydantic>=2.0.0
44
python-dotenv>=1.0.0
55
anthropic>=0.40.0
6+
litellm>=1.89.0,<2.0.0
67
sqlite-vec>=0.1.9
78
argon2-cffi>=23.1.0
89
cryptography>=42.0.0

ingest.py

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@
1919
from instrumentation import setup_tracing, get_tracer
2020
setup_tracing(project_name="cortex")
2121

22-
from anthropic import Anthropic
22+
import litellm
2323

24-
client = Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
24+
# Model routed through LiteLLM (Claude by default). Set CORTEX_MODEL to use any
25+
# other provider LiteLLM supports; credentials come from that provider's env var.
26+
DEFAULT_MODEL = os.environ.get("CORTEX_MODEL", "claude-opus-4-5")
2527
_tracer = get_tracer()
2628

2729

@@ -89,23 +91,27 @@ def extract_context(raw_text: str, source: str = "unknown") -> dict:
8991
now = datetime.now().isoformat()
9092
source_id = make_id("src_", source + now[:16])
9193

92-
response = client.messages.create(
93-
model="claude-opus-4-5",
94+
response = litellm.completion(
95+
model=DEFAULT_MODEL,
9496
max_tokens=3000,
95-
system=EXTRACTION_SYSTEM_PROMPT,
96-
messages=[{
97-
"role": "user",
98-
"content": (
99-
f"Source: {source}\n"
100-
f"Source ID: {source_id}\n"
101-
f"Captured: {now}\n\n"
102-
f"---\n\n{truncated}\n\n---\n\n"
103-
f"Extract context as JSON:"
104-
)
105-
}]
97+
messages=[
98+
{"role": "system", "content": EXTRACTION_SYSTEM_PROMPT},
99+
{
100+
"role": "user",
101+
"content": (
102+
f"Source: {source}\n"
103+
f"Source ID: {source_id}\n"
104+
f"Captured: {now}\n\n"
105+
f"---\n\n{truncated}\n\n---\n\n"
106+
f"Extract context as JSON:"
107+
),
108+
},
109+
],
110+
# Drop provider-unsupported params so one config works across providers.
111+
drop_params=True,
106112
)
107113

108-
raw_output = response.content[0].text.strip()
114+
raw_output = (response.choices[0].message.content or "").strip()
109115
extracted = _parse_json_response(raw_output)
110116

111117
# Ensure required fields

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
anthropic>=0.40.0
2+
litellm>=1.89.0,<2.0.0
23
mcp>=1.0.0
34
redis[hiredis]>=5.0.0
45
voyageai>=0.2.0

ui.py

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,14 @@
1616
from instrumentation import setup_tracing
1717
setup_tracing(project_name="cortex")
1818

19-
from anthropic import Anthropic
19+
import litellm
2020
from redis_store import search_context, get_recent_context
2121

22-
client = Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
22+
# Model routed through LiteLLM: keep Claude as the default, but any provider
23+
# LiteLLM supports works by setting CORTEX_MODEL (e.g. "openai/gpt-4o",
24+
# "gemini/gemini-2.5-pro", "bedrock/..."). Credentials come from that
25+
# provider's own env var (ANTHROPIC_API_KEY by default).
26+
DEFAULT_MODEL = os.environ.get("CORTEX_MODEL", "claude-opus-4-5")
2327

2428
# ── Page config ───────────────────────────────────────────────────────────────
2529

@@ -119,24 +123,29 @@ def ask_cortex(question: str, context_chunks: list[dict]) -> str:
119123
for c in context_chunks
120124
])
121125

122-
response = client.messages.create(
123-
model="claude-opus-4-5",
124-
max_tokens=1000,
125-
system="""You are Cortex — a personal AI that knows everything about the user based on their captured context.
126+
system_prompt = """You are Cortex — a personal AI that knows everything about the user based on their captured context.
126127
127128
You have access to the user's second brain: notes, decisions, insights, and memories captured from their AI chats, Slack, iMessage, and other apps.
128129
129130
Answer questions directly and personally, as if you are their most knowledgeable assistant.
130131
- Reference specific details from the context (dates, sources, exact decisions)
131132
- Be concise but complete
132133
- If the context is partial, say so and answer with what you have
133-
- Never say "based on the provided context" — just answer naturally""",
134-
messages=[{
135-
"role": "user",
136-
"content": f"Context from my second brain:\n\n{context_text}\n\n---\n\nQuestion: {question}"
137-
}]
134+
- Never say "based on the provided context" — just answer naturally"""
135+
response = litellm.completion(
136+
model=DEFAULT_MODEL,
137+
max_tokens=1000,
138+
messages=[
139+
{"role": "system", "content": system_prompt},
140+
{
141+
"role": "user",
142+
"content": f"Context from my second brain:\n\n{context_text}\n\n---\n\nQuestion: {question}",
143+
},
144+
],
145+
# Drop provider-unsupported params so one config works across providers.
146+
drop_params=True,
138147
)
139-
return response.content[0].text
148+
return response.choices[0].message.content or ""
140149

141150

142151
# ── Sidebar ───────────────────────────────────────────────────────────────────

0 commit comments

Comments
 (0)