Bring your own copilot to OpenBB Workspace, on any model.

Updated 2026-07-16

OpenBB Workspace lets you register a custom agent backend by base URL: it reads your agents.json and streams conversations through your query endpoint. Inside that backend the LLM call is a standard OpenAI-style client, so pointing it at https://api.apisrouter.com/v1 puts every catalog model behind your copilot.

Quick answer: your copilot backend owns the LLM call.

OpenBB Workspace integrates custom agents through two HTTP endpoints you host: GET /agents.json, which describes your agent, and a POST query endpoint that receives the conversation and streams the reply back as server-sent events. You register the whole thing in the Workspace UI by entering your service's base URL; Workspace fetches /agents.json from it automatically and routes chats to the query endpoint it declares. The part that matters for model choice: OpenBB does not dictate which LLM your agent uses. The official examples build the reply with openai.AsyncOpenAI() and client.chat.completions.create(stream=True), and that client accepts a base_url. Construct it against the gateway, and your OpenBB copilot answers with claude-opus-4-7, gpt-5.5, deepseek-v4-pro, or any other catalog id, switchable per request.

import openai

client = openai.AsyncOpenAI(
    base_url="https://api.apisrouter.com/v1",
    api_key=os.environ["APISROUTER_API_KEY"],
)
# model can be any catalog id
stream = await client.chat.completions.create(
    model="claude-sonnet-4-6", messages=openai_messages, stream=True)

How OpenBB Workspace talks to a custom agent.

OpenBB (OpenBB-finance on GitHub, roughly 71K stars) pairs its open-source platform with OpenBB Workspace, the analyst UI where dashboards, widgets, and the copilot live. The built-in copilot is hosted by OpenBB, but the documented bring-your-own-copilot path hands the conversation to a service you run, which is the integration this page covers. The contract is small. Your /agents.json returns a JSON object keyed by agent id, with name, description, image, an endpoints object whose query field points at your conversation route (a relative path like /v1/query or a full URL), and a features object with flags such as streaming and widget-dashboard-select. Your query endpoint accepts a QueryRequest holding the message history plus any widget context the analyst attached, and responds with an SSE stream of message chunks. The official openbb-ai Python SDK covers the wire format: QueryRequest and MessageChunkSSE models, a message_chunk() helper for streaming text, and get_widget_data() for requesting the contents of dashboard widgets mid-conversation. The recommended stack in the docs is FastAPI with EventSourceResponse from sse_starlette, and the agents-for-openbb example repo has runnable agents from a minimal chat responder up to citations, charts, tables, and PDF handling. In every one of them, the LLM is a plain OpenAI-client call you are free to point anywhere.

Full setup: a minimal copilot on the gateway.

The skeleton below is the shape of the official vanilla-agent example with the client constructed against the gateway. It answers chat messages and streams tokens back to Workspace; widget data, citations, and charts layer on top of the same loop. To register it: run the service somewhere Workspace can reach (the examples allow CORS from https://pro.openbb.co), then in the Workspace copilot picker choose to add a custom agent and enter your service's base URL. Workspace pulls /agents.json, shows your agent by name, and routes conversations to the query endpoint it declared.

import os, openai
from fastapi import FastAPI
from fastapi.responses import JSONResponse
from openbb_ai import message_chunk
from openbb_ai.models import QueryRequest
from sse_starlette.sse import EventSourceResponse

app = FastAPI()
client = openai.AsyncOpenAI(
    base_url="https://api.apisrouter.com/v1",
    api_key=os.environ["APISROUTER_API_KEY"],
)

@app.get("/agents.json")
def describe():
    return JSONResponse({
        "my_copilot": {
            "name": "My Copilot",
            "description": "Financial copilot on gateway-routed models.",
            "image": "https://example.com/logo.png",
            "endpoints": {"query": "/v1/query"},
            "features": {"streaming": True,
                         "widget-dashboard-select": True},
        }
    })

@app.post("/v1/query")
async def query(request: QueryRequest) -> EventSourceResponse:
    messages = [{"role": "user" if m.role == "human" else "assistant",
                 "content": m.content}
                for m in request.messages if isinstance(m.content, str)]
    async def loop():
        stream = await client.chat.completions.create(
            model="claude-sonnet-4-6", messages=messages, stream=True)
        async for event in stream:
            if chunk := event.choices[0].delta.content:
                yield message_chunk(chunk).model_dump()
    return EventSourceResponse(loop(), media_type="text/event-stream")

Choosing models for an analyst copilot.

The comparison loop is unusually easy here: your backend is the only consumer, so flip the model string, ask the same dashboard the same questions, and diff the streams. The per-key usage log prices each candidate conversation, which turns the model debate into a line item.

  • Widget-heavy questions are long-context questions. An analyst attaching a few dashboard widgets can push tens of thousands of tokens into one turn, so the per-input-token price of the id you hardcode matters more here than in a plain chatbot.
  • claude-sonnet-4-6 is a strong default for the conversational loop: fast enough to stream comfortably, strong enough to reason over attached tables and filings.
  • Escalate synthesis to a frontier id. A clean pattern is two agents in one agents.json, a fast one for lookups on deepseek-v4-pro and a deep one for theses on claude-opus-4-7, since the file is keyed by agent id and Workspace lists both.
  • gemini-3.1-pro-preview earns a test when your widgets carry very large tables or long documents in a single turn.
  • Because the model is one string in your backend, you can also read it from an environment variable and repoint your copilot without redeploying Workspace anything.

Pay-as-you-go · transparent per-model pricing

Selected models are priced below official list prices. Exact input, output, cache, and per-request prices are shown for each model.

ModelOfficial PriceOur Price
Claude Sonnet 4.6$3.00 / $15.00 per M$2.40 / $12.00 per M
Claude Opus 4.7$5.00 / $25.00 per M$4.00 / $20.00 per M
GPT-5.6 Terra$2.50 / $15.00 per M$2.00 / $12.00 per M
Gemini 3.1 Pro Preview$2.00 / $12.00 per M$1.60 / $9.60 per M
DeepSeek V4 Pro$0.43 / $0.87 per M$0.39 / $0.78 per M

What this setup is not.

Two boundaries keep expectations straight. First, this integrates with OpenBB Workspace, the analyst UI. It is not a setting inside the open-source OpenBB Platform's Python API, which is a data layer and does not proxy LLM calls. The copilot contract (agents.json plus a query endpoint) is a Workspace feature, documented under the developer section of docs.openbb.co. Second, the hosted default copilot's own model choice stays OpenBB's. Registering a custom agent does not reroute the built-in copilot; it adds yours next to it in the picker. Everything the analyst sends to your agent, including widget data they attach, flows through your backend and your gateway key, which is exactly what makes usage attributable: one key per copilot shows what the copilot costs, separated from any other LLM traffic your team runs. On the wire format, note that Workspace talks SSE to your service while your service talks standard chat completions to the gateway. The openbb-ai SDK handles the first half; the OpenAI client handles the second. Keeping those two halves separate in your head makes every debugging session shorter.

Who runs an OpenBB copilot through a gateway.

  • Analyst teams that want Claude-quality synthesis inside Workspace dashboards without waiting on the hosted copilot's model roadmap.
  • Quant and research desks with compliance reasons to keep copilot traffic on infrastructure they control, where the gateway gives one auditable billing surface.
  • Developers shipping one agent backend to several Workspace users, metering each deployment with its own key.
  • Teams comparing model families on real analyst workflows: same dashboards, same questions, one model string per experiment.
  • Developers without access to a given vendor's billing. Top-up based access with no card requirement removes the per-provider sign-up dependency.

Verify the endpoint and debug registration.

Two things fail independently: Workspace reaching your backend, and your backend reaching the gateway. Test them separately. For the gateway half, list the models your key can address and confirm the id your code sends is served. A 401 in your backend logs means the key does not match the endpoint; a model-not-found error is an id typo against /v1/models. For the Workspace half: if your agent never appears after you enter the base URL, fetch /agents.json yourself from a browser, since a JSON error or a wrong endpoints.query path fails silently in the picker. If the agent appears but conversations hang, check that your query route actually streams (EventSourceResponse with text/event-stream) rather than returning one JSON body, and that CORS allows https://pro.openbb.co. If chats work but widget questions come back empty, your agents.json features flags likely do not declare the widget capabilities your code expects. Once conversations flow, the APIsRouter console shows per-request model, token counts, and spend, which for widget-heavy analyst sessions is the honest measure of what a copilot turn costs.

# gateway half
curl -s https://api.apisrouter.com/v1/models \
  -H "Authorization: Bearer $APISROUTER_API_KEY" | head -50

# workspace half
curl -s https://your-agent.example.com/agents.json

FAQ

Can the OpenBB Workspace copilot use Claude or DeepSeek models?

Through a custom agent, yes. Workspace hands the conversation to your backend, and your backend's OpenAI-style client decides the model. With base_url on the gateway, any catalog id works, Claude and DeepSeek included.

Where do I register a custom copilot endpoint in OpenBB Workspace?

In the copilot picker inside Workspace, add a custom agent and enter your service's base URL. Workspace fetches /agents.json from that URL and routes conversations to the query endpoint declared in its endpoints.query field.

Does my agent backend have to expose an OpenAI-compatible API to Workspace?

No. Workspace speaks its own contract: agents.json metadata plus a query endpoint streaming SSE message chunks. The OpenAI-compatible call happens inside your backend, from your client to the gateway.

Do I need the openbb-ai SDK?

It is the documented path and covers QueryRequest parsing, message_chunk streaming helpers, and widget-data requests. Any framework that can serve the same JSON and SSE shapes works, but the SDK plus FastAPI and sse_starlette is what the official examples use.

Can one backend serve multiple copilots on different models?

Yes. agents.json is keyed by agent id, so one service can declare several agents, each with its own query route and its own model string, for example a fast lookup agent on deepseek-v4-pro and a deep-analysis agent on claude-opus-4-7.

Does this change the built-in OpenBB copilot?

No. Registering a custom agent adds it alongside the hosted copilot in the picker. Only conversations the analyst directs at your agent flow through your backend and your gateway key.