cortexhub
The CortexHub platform SDK. One API key points your existing agent at two governed planes:
- the LLM router (inference) - OpenAI-compatible, so any framework works, with routing, caching, compression, governance, and per-agent metering; and
- the MCP gateway (tools + brain + files) - governed, consent-gated, injection-defended.
You don't rewrite your agent. You change a base URL and drop in one key.
Install
pip install cortexhub # zero-dependency core (URLs + headers)
pip install 'cortexhub[ai]' # + a ready OpenAI-compatible client via cx.llm
pip install 'cortexhub[mcp]' # + the official MCP client for cx.mcp() sessions
The key
Onboard an external AI employee in the CortexHub dashboard, grant it
capabilities, and mint an API key bound to that agent (Settings ->
API keys, or the agent's page). The cxh_key_... is that agent's identity -
every call is attributed, governed, and metered to it. No agent argument needed.
from cortexhub import Cortexhub
cx = Cortexhub(api_key="cxh_key_...") # or set CORTEXHUB_API_KEY
Run a session (recommended)
Your agent runs in your own runtime, but every run should show up in CortexHub with the same visibility as a co-worker on CortexHub's runtime -- one Session in the Sessions tab, with its Trace, Spans, cost, and user attribution.
Use one cx.session(...) handle for the whole run. Pass the identity only your
app knows: a stable session id (your id for the conversation or autonomous
run) and the end user it acts for. The handle's .llm (inference) and .mcp
(tools) both land in that one session.
s = cx.session(
agent="support-bot",
mcp_session_id="conv-42", # your stable id -> one CortexHub session
end_user_subject="user_42", # who it acts for (None for an autonomous run)
)
# Inference (OpenAI-compatible). model="cortexhub/auto" lets CortexHub route per
# turn; or pass any model enabled on the platform.
s.llm.chat.completions.create(model="cortexhub/auto", messages=[{"role": "user", "content": "hi"}])
# Tools + brain + files, in the SAME session:
s.mcp.url # https://mcp.cortexhub.ai/v1/mcp
s.mcp.headers # wire into your MCP transport (see "Tools" below)
s.session_id # "conv-42" -- log or forward it to correlate
Reuse the same handle across the run's turns so they group. An autonomous agent
(e.g. a cron job) does the same with a per-run id and no end_user_subject
(extra for .llm: cortexhub[ai]).
Inference without a session
For a quick, standalone call, cx.llm is a ready OpenAI-compatible client and
cx.ai gives the raw base_url + api_key any framework needs. These are NOT
grouped into a session (no Sessions row) -- prefer cx.session(...) above when
you want the run visible in CortexHub.
cx.llm.chat.completions.create(model="cortexhub/auto", messages=[...])
cx.llm.responses.create(model="cortexhub/auto", input="hi")
cx.llm.models.list()
cx.ai.base_url # e.g. https://api.cortexhub.ai/v1 (override with CORTEXHUB_ROUTER_URL)
cx.ai.api_key # your cxh_key_...
Use your existing framework
The router speaks the OpenAI wire protocol, so every framework integrates the
same way: point its OpenAI-compatible provider at cx.ai.base_url, pass
cx.ai.api_key, and choose a model.
base, key = cx.ai.base_url, cx.ai.api_key
# OpenAI SDK / plain
from openai import OpenAI
OpenAI(base_url=base, api_key=key).chat.completions.create(model="cortexhub/auto", messages=[...])
# LangChain / LangGraph
from langchain_openai import ChatOpenAI
ChatOpenAI(base_url=base, api_key=key, model="cortexhub/auto")
# CrewAI (LiteLLM under the hood)
from crewai import LLM
LLM(model="openai/cortexhub/auto", base_url=base, api_key=key)
# AutoGen
from autogen_ext.models.openai import OpenAIChatCompletionClient
OpenAIChatCompletionClient(model="cortexhub/auto", base_url=base, api_key=key)
# Pydantic AI
from pydantic_ai.models.openai import OpenAIModel
from pydantic_ai.providers.openai import OpenAIProvider
OpenAIModel("cortexhub/auto", provider=OpenAIProvider(base_url=base, api_key=key))
Anthropic SDK: the router is OpenAI-compatible, not Anthropic-Messages, so
point the OpenAI client at CortexHub with a claude-* model (we route to
Claude) rather than the Anthropic SDK.
Tools, brain, and files (MCP gateway)
s.mcp (the session handle above) is the MCP connection your agent's loop
drives. Any MCP-capable framework registers it as a server; the cortexhub_*
tools (governed toolkit calls, brain recall/learn, files search) then appear
automatically -- and, on the session handle, group with that run's LLM turns.
conn = s.mcp # from cx.session(...) -> same session as .llm
conn.url # https://mcp.cortexhub.ai/v1/mcp
conn.headers # wire into your MCP transport
cx.mcp(agent=..., end_user_subject=...) is the standalone form when you only
need tools (no session grouping).
A full turn: reason, act, and approvals
One session, end to end: the model reasons with s.llm, acts through the
governed cortexhub_* tools on s.mcp, and when a governed action needs the end
user's approval it comes back parked with a signed consent_url -- you show
that to the user and poll until they decide in CortexHub. Needs both extras:
pip install 'cortexhub[ai,mcp]'.
How consent reaches your user. The gateway picks the consent surface from what your MCP client declares at initialize. A UI host that declares the MCP-UI / Tasks extension (e.g. claude.ai) gets consent rendered inline. A plain backend client like the one below declares neither, so the gateway hands it the
consent_urlfallback shown here -- a signed link you surface to your end user however your app does UI (a page, an email, a Slack DM), then pollcortexhub_get_taskfor the decision. This is the right pattern for a headless or autonomous agent (there is no host to render inline consent).
import asyncio, json
from cortexhub import Cortexhub
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client
cx = Cortexhub(api_key="cxh_key_...")
async def run_turn(user_text: str) -> str:
# One session for this end user's conversation -> LLM + tools share it.
s = cx.session(agent="support-bot", mcp_session_id="conv-42", end_user_subject="user_42")
async with streamablehttp_client(s.mcp.url, headers=s.mcp.headers) as (read, write, _):
async with ClientSession(read, write) as mcp:
await mcp.initialize()
# Offer the governed cortexhub_* tools to the model.
listed = await mcp.list_tools()
tools = [{
"type": "function",
"function": {"name": t.name, "description": t.description or "",
"parameters": t.inputSchema or {"type": "object", "properties": {}}},
} for t in listed.tools]
messages = [{"role": "user", "content": user_text}]
for _ in range(8): # a few reason -> act rounds
msg = s.llm.chat.completions.create(
model="cortexhub/auto", messages=messages, tools=tools,
).choices[0].message
if not msg.tool_calls:
return msg.content or "" # final answer
messages.append(msg.model_dump())
for call in msg.tool_calls:
args = json.loads(call.function.arguments or "{}")
result = await mcp.call_tool(call.function.name, args)
text = _text(result)
task = _parked_task(result) # governed action -> needs approval?
if task:
print("Approve to continue:", task["consent_url"]) # show the end user
text = await _await_decision(mcp, task["task_id"]) # wait for the decision
messages.append({"role": "tool", "tool_call_id": call.id, "content": text})
return ""
# --- small helpers over the MCP result ---
def _text(result) -> str:
return "\n".join(b.text for b in result.content if getattr(b, "text", None)) or "{}"
def _payload(result):
sc = getattr(result, "structuredContent", None)
if sc:
return sc
try:
return json.loads(_text(result))
except json.JSONDecodeError:
return {}
def _parked_task(result):
# A parked approval comes back as an `mcp_task` carrying a signed consent_url.
def walk(node):
if isinstance(node, dict):
t = node.get("mcp_task")
if isinstance(t, dict) and t.get("consent_url"):
return t
for v in node.values():
if (found := walk(v)):
return found
elif isinstance(node, list):
for item in node:
if (found := walk(item)):
return found
return None
return walk(_payload(result))
async def _await_decision(mcp, task_id: str) -> str:
# Poll until the human decides in CortexHub; an approval runs the action.
for _ in range(600): # ~10 min at 1s waits
res = await mcp.call_tool("cortexhub_get_task", {"task_id": task_id, "wait_ms": 1000})
if _payload(res).get("terminal"):
return _text(res)
return "approval timed out"
print(asyncio.run(run_turn("Email the Q3 report to the finance team")))
The whole turn is one CortexHub session (conv-42): open its Sessions tab to see
the model step, each tool call, and the approval as a single trace.
Authentication modes
- API key (
api_key=cxh_key_..., orCORTEXHUB_API_KEY) - backends whose end users do not have CortexHub accounts. Attest the end user per call withend_user_subject. Required forcx.ai/cx.llm. - MCP OAuth client (
client_id=/client_secret=) - for interactive CortexHub users, MCP gateway only.
The MCP gateway also accepts MCP OAuth from third-party clients (Claude, Cursor) independently of this SDK.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file cortexhub-3.1.0.tar.gz.
File metadata
- Download URL: cortexhub-3.1.0.tar.gz
- Upload date:
- Size: 13.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.14.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fc80325fdd3a5d10af364364d142f9690a940027f7b37572d15de4ce3a7a1f83
|
|
| MD5 |
910eb504a14b7e8de63e7b6455e8d97d
|
|
| BLAKE2b-256 |
a9fec427bee1e58aace63d9c0044bd55ddc3b2c898ea53135b2e90dfd631b5a5
|
File details
Details for the file cortexhub-3.1.0-py3-none-any.whl.
File metadata
- Download URL: cortexhub-3.1.0-py3-none-any.whl
- Upload date:
- Size: 16.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.14.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
698519954f10026142cd3a6cf820003c61690c35dd207c06f0881565113eaa11
|
|
| MD5 |
5f1e42dceb115b7418aa3a9376651a16
|
|
| BLAKE2b-256 |
bfd6d89c02492bc76b778a7a1b2cf1e064c4609e3360ca03e6ca0ed8965066c6
|