Integrate
OpenAI-compatible API
Tracon exposes the two OpenAI request styles most application clients already understand:
POST {prefix}/v1/responsesPOST {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.
Responses API: recommended for stateful clients
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 osfrom 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:
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.
Stream Responses events
Section titled “Stream Responses events”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.
Conversation state
Section titled “Conversation state”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:
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:SessionOwnershipis on — includingRefuseUnownedSessions. See Sessions: session ownership. - Your registered
IRunAuthorizationHandler, withSessionAccess.Readon the two reads andSessionAccess.Deleteon 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: stateless history
Section titled “Chat Completions: stateless history”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.
A message’s content can be a string or a list of parts. Tracon hands each part to the
model or refuses the request with 400 and the part’s location; it never drops a part:
| Part | Reaches the model as |
|---|---|
text, refusal |
Text |
image_url with a data: URL |
Image bytes; detail is kept |
image_url with an http(s) URL |
A link the provider fetches; detail is kept |
input_audio (wav, mp3) |
Audio bytes |
file with file_data |
File bytes, with filename as the name |
file with file_id, or any other type |
Refused: Tracon keeps no OpenAI file store |
Embedded bytes go through the same check as an upload: the type is read from the bytes,
not from the declared media type, must be listed in Attachments:AllowedMediaTypes and
must fit in Attachments:MaxBytes (configuration). The data is stored as an attachment of the tenant with no session, so
retention removes it later. Whether a model can read a given image, audio clip or file
depends on its provider.
The roles are system, developer (read as system), user and assistant. A tool
or function message, and an assistant message with tool_calls or function_call,
answer 400: Tracon runs the agent’s tools on the server, so there is no client-side
tool call to report.
The agent definition owns the model settings, so a request setting is never silently
ignored. These fields answer 400 (invalid_request_error), and the message names every
one the request sets: temperature, top_p, max_tokens, max_completion_tokens,
frequency_penalty, presence_penalty, seed, stop, logit_bias, reasoning_effort,
response_format, tools, tool_choice, functions, function_call, n, logprobs,
top_logprobs, modalities, audio, and web_search_options. A value that asks for the
default passes: n: 1, response_format: {"type": "text"}, logprobs: false, an empty
tools list, modalities: ["text"], or null. Fields that carry no setting the agent uses
(user, store, metadata, stream_options) pass too. /v1/responses refuses its own
setting fields (temperature, top_p, max_output_tokens, instructions, tools,
tool_choice) the same way. To change a setting, change the agent.
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);}Compatibility matrix
Section titled “Compatibility matrix”| 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 |
| Image, audio and file input | input_image, input_file |
image_url, input_audio, file |
Embedded data passes the attachment type check and is stored as an attachment of the tenant |
| Client tool-call history | no: 400 |
no: 400 |
Tools run on the server; send earlier turns as text |
| Agent tool loop | yes | yes | Tools are defined and executed server-side |
| Pending approval | yes, in output |
no: 409 |
Chat Completions carries no session, so an approval can never be answered |
| Usage | when provider reports it | when provider reports it | Missing usage remains unknown, not zero |
| Cut-off answer | no: status stays completed |
finish_reason: "length" |
See below |
Request model settings (temperature, tools, response_format, …) |
no: 400 |
no: 400 |
The agent definition sets them |
| 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 |
A Chat Completions request whose agent asks to call a tool that needs approval ends
there. The answer to an approval is the next turn of a session, and this endpoint has
none, so nobody could ever answer it. The run closes as Failed, and the caller gets
409 with the error type approval_required — or, on a stream that already answered
200, an error frame of that type and no [DONE]. Run such an agent through
/v1/responses with a conversation, or through POST /api/agents/{name}/run with a
sessionId.
Chat Completions sets finish_reason from the answer’s final model call, on the JSON
response and on the last chunk of a stream (data: [DONE] still follows). An answer
the model cut off at its output token limit or its context window is "length"; an
answer a content filter cut off after it produced text is "content_filter"; anything
else is "stop". Tools run on the server, so a tool turn’s tool_calls reason is never
reported. The value does not depend on run recording. The Responses endpoint does not
mark a cut-off answer incomplete yet: its status reads completed, and the run’s
tracon.output.truncated event is the only signal there
(see Truncated output).
A field that carries no model setting can be accepted without becoming a Tracon
feature. Do not assume every OpenAI storage flag, service tier, or retrieval surface
is forwarded to the agent’s provider. The server-side definition is authoritative.
POST /v1/conversations takes metadata only: a request with initial items answers
400, and the first turn goes to /v1/responses with the conversation’s id.
Authentication and authorization
Section titled “Authentication and authorization”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.
Failure behavior
Section titled “Failure behavior”| 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.
Read next
Section titled “Read next”- HTTP API conventions — the management API, which is a different surface with different rules
- Model providers — what actually answers the request behind the compatible endpoint
- Attachments and multimodal input — how non-text content arrives through the same endpoints