Skip to main content

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.

Documentation

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.

import cortexhub
cx = cortexhub.init("cxh_key_...")      # or set CORTEXHUB_API_KEY

# equivalently:
from cortexhub import Cortexhub
cx = Cortexhub(api_key="cxh_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, carrying an approval_id and the presentation (headline / sections / summary) of what will happen. Needs both extras: pip install 'cortexhub[ai,mcp]'.

How consent reaches your user. Your agent is headless, so you own the consent UI. When a call parks, read the approval_id + presentation from the result, show it to your end user however your app does UI, and when they decide submit it through the SDK:

s.decide(approval_id, "approve", note="ok by me")   # or "deny"

s.decide records the decision as that end user's consent -- no CortexHub account for the end user is required -- bound to the same agent + end_user_subject this session runs as. Then poll cortexhub_get_task (below): an approved call runs server-side and its result comes back. If you'd rather not build any UI, the parked result also carries a signed consent_url you can hand the user to approve in a browser instead. (A UI host that declares the MCP-UI extension, e.g. claude.ai, gets consent rendered inline and never reaches this path.)

import asyncio, json
from cortexhub import Cortexhub
from mcp import ClientSession

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")

    # s.mcp.open() opens the transport across mcp client versions.
    async with s.mcp.open() 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:
                        # Show task["presentation"] in YOUR UI and get the user's
                        # decision. Here we auto-approve to keep the example runnable.
                        s.decide(task["approval_id"], "approve", note="demo auto-approve")
                        text = await _await_decision(mcp, task["task_id"])   # resume + read result

                    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 an approval_id (and a
    # signed consent_url fallback). The presentation you passed to the tool call
    # rides alongside it, so you can render your own consent UI.
    def walk(node):
        if isinstance(node, dict):
            t = node.get("mcp_task")
            if isinstance(t, dict) and t.get("approval_id"):
                if "presentation" in node and "presentation" not in t:
                    t = {**t, "presentation": node["presentation"]}
                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_..., or CORTEXHUB_API_KEY) - backends whose end users do not have CortexHub accounts. Attest the end user per call with end_user_subject. Required for cx.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

cortexhub-3.2.0.tar.gz (15.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

cortexhub-3.2.0-py3-none-any.whl (19.2 kB view details)

Uploaded Python 3

File details

Details for the file cortexhub-3.2.0.tar.gz.

File metadata

  • Download URL: cortexhub-3.2.0.tar.gz
  • Upload date:
  • Size: 15.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.14.0

File hashes

Hashes for cortexhub-3.2.0.tar.gz
Algorithm Hash digest
SHA256 11d3c27f290fdc048fc34d6523f4d45a2a70ef86deed1db41276170cbceab9cb
MD5 6a21f8a09baed677dec78b889b0de55e
BLAKE2b-256 8de41d973afd94762fed8d09e8bbd7a0accc7f4bbaaf0c9a7877405da63c2e8e

See more details on using hashes here.

File details

Details for the file cortexhub-3.2.0-py3-none-any.whl.

File metadata

  • Download URL: cortexhub-3.2.0-py3-none-any.whl
  • Upload date:
  • Size: 19.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.14.0

File hashes

Hashes for cortexhub-3.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 53554eb8de7440a9b0c42810e6b4c46e6ba1e995ef4f491cbb8bd6b204afcbd2
MD5 39dfda9bd2fe2842a396dd6894e5980b
BLAKE2b-256 a0c6c09995df36e628d8f9778c7aef40e26e22ec476a84d1bb396374ac5e728b

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

3.2.0 This release

2 files

3.1.1

2 files

3.1.0

2 files

3.0.0

2 files

2.1.0

2 files

2.0.5

2 files

2.0.4

2 files

2.0.3

2 files

2.0.2

2 files

2.0.1

2 files

2.0.0

2 files

1.1.0

2 files

1.0.3

2 files

1.0.2

2 files

1.0.1

2 files

1.0.0

2 files

0.5.28

2 files

0.5.27

2 files

0.5.25

2 files

0.5.24

2 files

0.5.23

2 files

0.5.22

2 files

0.5.21

2 files

0.5.19

2 files

0.5.18

2 files

0.5.16

2 files

0.5.15

2 files

0.5.14

2 files

0.5.13

2 files

0.5.12

2 files

0.5.11

2 files

0.5.10

2 files

0.5.9

2 files

0.5.8

2 files

0.5.7

2 files

0.5.6

2 files

0.5.5

2 files

0.5.4

2 files

0.5.3

2 files

0.5.2

2 files

0.5.1

2 files

0.5.0

2 files

0.4.2

2 files

0.4.1

2 files

0.4.0

2 files

0.3.5

2 files

0.3.4

2 files

0.3.3

2 files

0.3.2

2 files

0.3.0

2 files

0.2.15

2 files

0.2.14

2 files

0.2.13

2 files

0.2.12

2 files

0.2.11

2 files

0.2.10

2 files

0.2.9

2 files

0.2.8

2 files

0.2.7

2 files

0.2.6

2 files

0.2.4

2 files

0.2.2

2 files

0.2.1

2 files

0.1.28

2 files

0.1.27

2 files

0.1.26

2 files

0.1.25

2 files

0.1.24

2 files

0.1.23

2 files

0.1.22

2 files

0.1.20

2 files

0.1.19

2 files

0.1.18

2 files

0.1.16

2 files

0.1.15

2 files

0.1.14

2 files

0.1.13

2 files

0.1.12

2 files

0.1.11

2 files

0.1.10

2 files

0.1.9

2 files

0.1.8

2 files

0.1.7

2 files

0.1.5

2 files

0.1.4

2 files

0.1.3

2 files

0.1.2

2 files

0.1.1

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page