Status: Developer preview.If you’re calling the OpenAI or Anthropic SDKs directly today — with your own model-switch logic, your own retry ladder, your own ad hoc “pause and ask a human” hooks around specific tool calls — this guide maps what you have to the Execute API’s v1 contract, and is honest about what changes and what you give up.
1. Map the request
- OpenAI:
openai.chat.completions.create({ model, messages, tools, stream: true }) - Anthropic:
anthropic.messages.stream({ model, messages, tools }) - Praxa:
POST /v1/executewithmode: "turn", sending atask(plus optionalcontext,tools, andpolicy) instead of amodel. Routing picks the model — and can fail over between models — on your behalf; there is nomodelfield to get right up front.
2. Map the stream
Both direct APIs and Praxa stream over SSE, but the event vocabulary and what executes where are different. This table lines up the closest equivalents:finish_reason / stop_reason → finishReason
3. tool_calls becomes a server-side lifecycle, not a round-trip
Today, you get tool_calls, execute them yourself against your own function
implementations, and send a new request with the results appended to
messages. That loop — and everything you built to make it safe (a
confirmation prompt before a destructive call, retry-on-429, your own tool
registry) — is Praxa’s job now:
tool.proposed (what’s about to run, with a rendered summary) → (if your
policy gates it) approval.required → tool.started → tool.completed
(with the result) or tool.failed (a structured code and retryable
flag).
You still see every tool call and its result in your event stream, for
logging or UI — you just don’t execute it yourself anymore.
4. What you delete
- Model-switch / failover logic. Routing and degrade floors live
server-side; you send a
task, not amodel. - Retry ladders around transient provider failures. These surface to
you as
retryevents (attempt,reason,delayMs) if you want to show progress — not exceptions you catch and re-drive yourself. - Hand-rolled approval plumbing. Whatever you built to intercept
specific tool names or arguments and pause for a human is replaced by the
policyrequest field (risk ceiling, budget caps, tool allowlist, approval mode) plus theapproval.requiredprotocol event. Delete the special-casing; express it as policy instead. - Per-provider billing reconciliation. One
/v1/usagereadback against your reservation, instead of stitching together separate provider dashboards for whatever you’re funding today.
5. What changes conceptually
- Approvals are a first-class protocol event, not an app-level convention you maintain yourself. Render == record == gate-match: what a person approved is exactly, byte for byte, what runs — see build-an-approval-ui.md.
- Every run returns a receipt: what ran, which policy version was applied, who approved what, and what it was verified against — not just a completion you have to take on faith.
- You stop being the tool executor. Your integration surface shrinks to: submit a task, render events, answer approvals, read the receipt.
6. What you give up — honestly
- Provider beta features arrive on a lag. A new experimental endpoint or provider-exclusive preview has to be integrated and routed by Praxa before it’s reachable through the fabric — you’re not first in line for it the way a direct SDK integration would be.
- You don’t pin the exact model or SDK version for a call. Routing picks
for you, constrained by
policy/modelPreference— or, under BYO funding, by which providers you’ve funded (see bring-your-own-keys.md). If pinning an exact model and version matters more to you than routing, approvals, and receipts, direct integration still has a place. - One more moving part sits between you and the model. Praxa’s routing/policy layer is in the path — that’s the point, but it also means a provider-side incident and a Praxa-side incident both need to be ruled out when something looks wrong.
7. Migration checklist
- Replace your model-selection config with a
taskand apolicy(risk ceiling, budget cap, approval mode) — stop picking a model per call. - Delete your tool-execution loop. Keep your tool definitions, register
them via
tools, and update your UI to rendertool.proposed/tool.completed/tool.failedinstead of calling functions yourself. - Replace any hand-built “pause and ask a human” logic with
approval.requiredhandling. - Swap your
finish_reason/stop_reasonbranches forresponse.final.finishReason, using the mapping table in §2. - Point your billing reconciliation at
/v1/usageand drop the per-provider invoice stitching. - If you have committed spend with a provider you don’t want to walk away from, read bring-your-own-keys.md before assuming bundled is your only option.