Skip to content
Tracon

OpenAI-compatible API

Tracon exposes the two OpenAI request styles most application clients already understand:

  • POST {prefix}/v1/responses
  • POST {prefix}/v1/chat/completions
  • /v1/conversations/* for Responses conversation state
flowchart TD
    accTitle: The two compatible surfaces and who owns the conversation
    accDescr: Responses keeps conversation state on the server, reached either by previous_response_id or by an explicitly reserved conversation. Chat Completions carries its history in the request. Both resolve the model field to a Tracon agent, which then chooses its own provider model.
    RESP["POST /v1/responses"] --> SRV["Tracon owns the session"]
    CONV["POST /v1/conversations"] --> SRV
    PREV["previous_response_id"] --> SRV
    CHAT["POST /v1/chat/completions"] --> CLI["The client owns the message list"]
    SRV --> PICK["model selects a Tracon agent"]
    CLI --> PICK
    PICK --> AGENT["Agent: instructions · tools · skills<br/>memory · guards · budgets"]
    AGENT --> MODEL["The provider model the agent binds to"]

This is an agent surface, not a transparent model proxy. The request’s model selects a Tracon agent. That agent then selects its provider model, instructions, tools, skills, memory, guards, and budgets on the server.

Section titled “Responses API: recommended for stateful clients”

The official OpenAI SDK uses client.responses.create(model=..., input=...); point the same client shape at Tracon and use a Tracon credential:

import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["TRACON_API_KEY"],
base_url="https://agents.example.com/tracon/v1",
)
response = client.responses.create(
model="support",
input="Where is order 4182?",
)
print(response.output_text)

If your generated SDK version expects the base URL without /v1, follow that SDK’s base-URL rule. The request that reaches Tracon must end at {prefix}/v1/responses.

The same call without an SDK is unambiguous:

Terminal window
curl -sS https://agents.example.com/tracon/v1/responses \
-H "Authorization: Bearer $TRACON_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"model":"support","input":"Where is order 4182?"}'

The response has the familiar response object, resp_... identifier, output items, status, and usage when the underlying provider reports it. Errors use the OpenAI {"error": { ... }} envelope so an OpenAI client can parse them.

Set stream=true. The server returns SSE with OpenAI event names, including response.created, response.output_text.delta, and response.completed:

stream = client.responses.create(
model="support",
input="Summarize the escalation in three bullets.",
stream=True,
)
for event in stream:
if event.type == "response.output_text.delta":
print(event.delta, end="", flush=True)

This follows the official Responses streaming model. Tracon still creates its own run and ordered event history behind the compatible stream.

Use either mechanism supported by Responses:

{
"model": "support",
"input": "Second turn",
"previous_response_id": "resp_..."
}

Tracon stores the session under the first response identifier, so previous_response_id finds its history. Or reserve and reuse an explicit conversation:

Terminal window
CONVERSATION_ID=$(curl -sS -X POST \
https://agents.example.com/tracon/v1/conversations \
-H "Authorization: Bearer $TRACON_API_KEY" \
-H 'Content-Type: application/json' \
-d '{}' | jq -r .id)
curl -sS https://agents.example.com/tracon/v1/responses \
-H "Authorization: Bearer $TRACON_API_KEY" \
-H 'Content-Type: application/json' \
-d "{\"model\":\"support\",\"conversation\":\"$CONVERSATION_ID\",\"input\":\"Hello\"}"

Creating a conversation reserves an identifier; the session is born on the first response call. Tenant ownership is checked on every reference. An identifier owned by another tenant returns 404, not evidence that the resource exists.

The conversation routes go through your own gates

Section titled “The conversation routes go through your own gates”

GET, DELETE and GET …/items reach the same sessions /api/sessions/{id} reaches, so they pass the same two checks before answering:

  • Session ownership, when Tracon:SessionOwnership is on — including RefuseUnownedSessions. See Sessions: session ownership.
  • Your registered IRunAuthorizationHandler, with SessionAccess.Read on the two reads and SessionAccess.Delete on the delete.

If your deployment does not use these routes at all, leave them unmapped:

app.MapTracon("/tracon", options => options.MapOpenAIConversations = false);

The four paths then answer 404 and disappear from the OpenAPI document. /v1/responses and /v1/chat/completions are unaffected.

Chat Completions remains useful for clients that carry their own message list:

completion = client.chat.completions.create(
model="support",
messages=[
{"role": "user", "content": "Where is order 4182?"},
],
)
print(completion.choices[0].message.content)

stream=True returns chat.completion.chunk frames and ends with [DONE]. Tracon does not open a server session for this endpoint. Send the full history on every call; otherwise the next turn has no previous context.

Calling these endpoints from a typed client

Section titled “Calling these endpoints from a typed client”

Both endpoints answer with either JSON or SSE, and the stream flag in the request body decides which — at request time. An OpenAPI document cannot describe a response shape chosen that way, so a generated client cannot pick one signature that covers both. Each endpoint therefore has two methods, and you choose by calling one of them:

You want .NET method Returns
One complete answer TraconOpenAIResponsesAsync Task<JsonElement>
Frames as they arrive TraconOpenAIResponsesStreamAsync IAsyncEnumerable<string>
var body = JsonSerializer.SerializeToElement(
new { model = "support", input = "Where is order 4182?", stream = true });
await foreach (var frame in client.TraconOpenAIResponsesStreamAsync(body))
{
// One raw SSE frame per element, e.g.
// "event: response.output_text.delta\ndata: {...}"
Console.WriteLine(frame);
}

The frame is handed over as the server flushes it, so a break or a cancelled token stops the read and releases the connection. Comment-only keep-alive blocks are skipped. The element is the raw frame rather than a parsed event type: the payload follows OpenAI’s schema, not Tracon’s, so parsing it is yours to control.

TraconRunAgentStreamAsync and the other *StreamAsync methods give the management API’s streaming endpoints the same shape.

In TypeScript no special method is needed — ask for the raw stream and decode it with the reader the package ships:

import { createTraconClient, readSse } from '@tracon/client';
const client = createTraconClient({
baseUrl: 'https://agents.example.com/tracon',
token: process.env.TRACON_API_KEY,
});
const { response } = await client.POST('/v1/responses', {
body: { model: 'support', input: 'Where is order 4182?', stream: true },
parseAs: 'stream',
});
for await (const frame of readSse(response)) {
console.log(frame.event, frame.data);
}
Capability Responses Chat Completions Tracon behavior
Text input and output yes yes Runs the selected agent and records the run
SSE streaming yes yes Uses surface-specific OpenAI frame names
Server conversation conversation or previous_response_id no Maps to Tracon sessions
Client-carried history input items messages Resolved attachments reach the provider
Agent tool loop yes yes Tools are defined and executed server-side
Pending approval visibility yes provider-shaped output The call cannot supply an interactive approval turn
Usage when provider reports it when provider reports it Missing usage remains unknown, not zero
OpenAI hosted tools not a pass-through not a pass-through Configure Tracon tools/MCP instead
Background Responses mode no no Use Prefer: respond-async on the management run endpoint

Fields understood by the OpenAI parser can be accepted without becoming a Tracon feature. Do not assume every OpenAI hosted tool, storage flag, service tier, or retrieval surface is forwarded to the agent’s provider. The server-side definition is authoritative.

Use the bearer token configured on MapTracon, or a tenant-bound Tracon API key with RunsWrite — these routes start runs, so they take the same scope the management run endpoints take. GET /v1/conversations/{id} and its items route take RunsRead instead. ExternalInvoke is the scope for the MCP and A2A surfaces and does not authorize these routes: a key issued with only ExternalInvoke is rejected here. The base URL, credential, and model semantics all change; test them explicitly when moving an existing client.

For a browser or untrusted device, do not embed a long-lived control-plane key. Terminate user authentication in your application and issue the narrowest credential your architecture permits.

Symptom Meaning
400 with listed agents No valid model or metadata.entity_id selected an agent
401 Bearer token or API key is missing, invalid, revoked, or expired
403 Role, scope, loopback, or tenant rule refused the call
404 model_not_found The selected Tracon agent does not resolve
422 A Tracon content guard or idempotency contract rejected the request
429 A rate or quota boundary was reached
502 The configured upstream provider failed

Use the management run API when you need Tracon-specific controls such as queued execution, idempotent non-streaming responses, explicit replay, cancellation, or complete run diagnostics.

The client method and streaming examples follow the official OpenAI Responses documentation. The behavior and compatibility limits above are Tracon’s own contract.