Pointing Studio at your own model
Settings → Model takes a base URL alongside the key. Set it and every run goes there instead of the provider's own host: your server, your hardware, your domain, no code change.
This is what Studio sends, taken from packages/control-plane/src/providers/openai.ts rather than from memory. If your server satisfies it, Studio can drive it.
The one call
POST {your base URL}/chat/completionsThe base URL is used verbatim with /chat/completions appended, so include the version segment if your server has one, so https://models.example.com/v1, not https://models.example.com.
Headers
content-type: application/json
authorization: Bearer <your key> ← only when a key is setA missing key means no header at all, not an empty one. A local server that needs no authentication should not have to ignore Bearer .
Body
{
"model": "whatever-you-named-it",
"messages": [ { "role": "system" | "user" | "assistant" | "tool", "content": "…" } ],
// Present only when the automation uses them:
"tools": [ { "type": "function", "function": { "name", "description", "parameters" } } ],
"tool_choice": "auto" | { "type": "function", "function": { "name": "…" } },
"temperature": 0.7,
"max_tokens": 4096
}Nothing else is sent. No streaming: Studio waits for the whole response, because a run is a batch job and a half-written answer is not useful to a workflow step.
Response
{
"choices": [
{
"message": {
"role": "assistant",
"content": "…",
// Required if you accepted `tools`; this is how an agent calls one.
"tool_calls": [
{ "id": "call_1", "type": "function",
"function": { "name": "app.collect_items", "arguments": "{\"since_hours\":24}" } }
]
},
"finish_reason": "stop" | "tool_calls" | "length"
}
],
"usage": { "prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0 }
}**usage is optional.** Studio fills in zeros when it is missing, so a server that does not count tokens works. You simply get no token analytics for those runs, rather than a failure.
**arguments is a JSON string**, not an object. That is the OpenAI shape, and an object there is the most common reason a working server drives agents that never call a tool.
What has to work for an agent, specifically
A tool-less workflow needs only content. An agent step needs real tool calling: it is a loop, and Studio ends it when the model calls claritty_finish. A server that ignores tools and answers in prose will loop until the step's maxIterations and then fail. The automation is not broken, the endpoint is.
Test it with tools before trusting it with an agent.
Anything else
A non-2xx response is surfaced with your server's own body, uncut, because the reason is usually in it. If Studio ever hides that, the bug is Studio's.
pnpm proof:byom is the gate that a configured base URL actually receives the run's call. It stands up a server, points Studio at it, fires a workflow and asserts the request arrived. If you change this contract, that is what should fail first.