build

OpenAI-compatible HTTP

Connect an existing chat or Responses API integration to Kestrel without rewriting its request format first.

SDKintermediate0.7.0 Stable
Verified 2026-07-20View sourceReport a docs issue

If your application already uses Chat Completions or Responses API requests, you can point its server-side integration at Kestrel first and adopt the richer SDK capabilities later. Your existing request format stays familiar while Kestrel adds durable runs, session identifiers, and user attribution.

When to choose this path

Choose the OpenAI-compatible path when:

  • your app already has OpenAI-style client code
  • you only need chat or responses semantics
  • you want server-owned auth, actor metadata, and durable run IDs
  • you are not yet using operator inbox, task graph, or project snapshot APIs directly

Use the SDK and runner APIs instead when the application needs direct access to operator controls, project state, or background subscriptions.

Integration pathBest fitWhat you gainWhat you do not get directly
OpenAI-compatible HTTPchat-first or responses-first appsfamiliar request bodies, simple proxy routes, standard SSEoperator control, task graph, project snapshot APIs
@kestrel-agents/sdkserver-owned product logictyped agent API, session memory, streaming, subscriptionsno drop-in OpenAI client compatibility
runner APIsadvanced operational toolscomplete operator and event APIsmore app-side routing and payload work

Available endpoints

The compatibility layer exposes:

  • GET /v1/models
  • POST /v1/chat/completions
  • POST /v1/responses

All three endpoints use the same runner and produce the same durable run history as SDK requests. The compatibility surface is intentionally fixed to the Reference agent; it is not a listing of every CLI profile.

Models example

Use GET /v1/models to verify that the compatibility surface is reachable:

Bash
curl -sS \
  -H "Authorization: Bearer $KESTREL_RUNNER_SERVICE_TOKEN" \
  "$KESTREL_RUNNER_SERVICE_URL/v1/models"

The response lists reference-react. Requests may also use the accepted compatibility alias reference-web; both select the same Reference-agent implementation. Other configured profile IDs are rejected.

Example response:

JSON
{
  "data": [
    {
      "id": "reference-react",
      "object": "model"
    }
  ],
  "object": "list"
}

Required auth and actor headers

Your Next.js server should forward bearer auth and actor context on every request:

Text
Authorization: Bearer <service-token>
x-kestrel-actor-id: user-123
x-kestrel-actor-type: end_user
x-kestrel-actor-name: Taylor Example
x-kestrel-tenant-id: acme

The browser should not know the service token. Keep the proxy route in your app and attach actor metadata from your own auth system.

Full Next.js proxy example

This is the simplest useful pattern for a browser app that already uses OpenAI-style chat. The browser posts to your Next.js route. The route keeps authentication and correlation on the server, then sends the request to its configured Local Core bridge or remote runner service. The shared Execution Protocol turns that request into a durable Kestrel run.

TypeScript
import "server-only";
 
export async function POST(request: Request) {
  const body = await request.text();
  const user = await requireAuthenticatedUser(request);
 
  const upstream = await fetch(
    `${process.env.KESTREL_RUNNER_SERVICE_URL}/v1/chat/completions`,
    {
      method: "POST",
      headers: {
        "content-type": "application/json",
        authorization: `Bearer ${process.env.KESTREL_RUNNER_SERVICE_TOKEN}`,
        "x-kestrel-actor-id": user.id,
        "x-kestrel-actor-type": "end_user",
        "x-kestrel-actor-name": user.name,
        "x-kestrel-tenant-id": user.tenantId,
      },
      body,
    },
  );
 
  return new Response(upstream.body, {
    status: upstream.status,
    headers: upstream.headers,
  });
}

Responses proxy example

If the app uses the Responses API instead of chat completions, keep the same server-owned pattern:

TypeScript
import "server-only";
 
export async function POST(request: Request) {
  const body = await request.text();
  const user = await requireAuthenticatedUser(request);
 
  const upstream = await fetch(
    `${process.env.KESTREL_RUNNER_SERVICE_URL}/v1/responses`,
    {
      method: "POST",
      headers: {
        "content-type": "application/json",
        authorization: `Bearer ${process.env.KESTREL_RUNNER_SERVICE_TOKEN}`,
        "x-kestrel-actor-id": user.id,
        "x-kestrel-actor-type": "end_user",
        "x-kestrel-actor-name": user.name,
        "x-kestrel-tenant-id": user.tenantId,
      },
      body,
    },
  );
 
  return new Response(upstream.body, {
    status: upstream.status,
    headers: upstream.headers,
  });
}

Request examples

Chat completions

JSON
{
  "model": "reference-react",
  "messages": [
    {
      "role": "user",
      "content": "Summarize the latest deployment issues."
    }
  ],
  "stream": true
}

Streamed response headers worth preserving:

Text
x-kestrel-session-id: reference-session-001
x-kestrel-run-id: run_01workspacecopilot
x-kestrel-thread-id: thread_01workspacecopilot
x-kestrel-model-id: reference-react

Responses API

JSON
{
  "model": "reference-react",
  "input": "Return machine readable status",
  "text": {
    "format": {
      "type": "json_schema",
      "json_schema": {
        "name": "compat_status",
        "schema": {
          "type": "object",
          "properties": {
            "status": {
              "type": "string"
            }
          },
          "required": ["status"],
          "additionalProperties": false
        }
      }
    }
  }
}

POST /v1/responses returns a response object and includes the Kestrel run id in its response metadata.

Example Responses API payload:

JSON
{
  "id": "resp_01workspacecopilot",
  "object": "response",
  "created_at": 1710000000,
  "model": "reference-react",
  "status": "completed",
  "output": [
    {
      "type": "message",
      "id": "msg_01workspacecopilot",
      "role": "assistant",
      "content": [
        {
          "type": "output_text",
          "text": "{\"status\":\"needs-review\"}"
        }
      ]
    }
  ],
  "output_text": "{\"status\":\"needs-review\"}",
  "metadata": {
    "kestrel": {
      "session_id": "reference-session-001",
      "run_id": "run_01workspacecopilot"
    }
  }
}

Model selection and migration path

Start with compatibility mode when preserving the existing client is the priority. Move to the SDK when you need more of Kestrel's session and operator capabilities.

StageUse this
existing OpenAI-style chat UIproxy /v1/chat/completions
responses-first or structured output appproxy /v1/responses
app now needs session memory, subscriptions, or typed routesmove to @kestrel-agents/sdk and @kestrel-agents/next
app now needs operator control, task graph, or project reviewadd the runner client or custom protocol

Response headers worth preserving

If your frontend needs continuity or debugging breadcrumbs, forward these response headers from the configured Kestrel execution target:

  • x-kestrel-session-id
  • x-kestrel-run-id
  • x-kestrel-thread-id
  • x-kestrel-model-id

x-kestrel-request-id and x-kestrel-correlation-id are generated by the @kestrel-agents/next route helpers. They are application-route correlation headers, not OpenAI-compatibility response headers from the runner service.

Those headers make it possible to connect a chat completion back to later operator inspection or replay evidence.

Keep the existing client

Instead of writing typed agent routes first, put Kestrel behind a thin proxy route and keep the client request format unchanged.

Adopt Kestrel without rewriting the client

Compatibility mode lets you keep familiar HTTP while gaining durable runs, actor attribution, and request correlation. Your runner credential remains on the application server.

When to move beyond compatibility mode

Move to the SDK or runner protocol when you need any of these:

  • operator.inbox, operator.thread, or operator.control
  • task.graph.get or task.graph.update
  • mission_control.project.get or mission_control.action.execute
  • project.snapshot.get or Git-only project.action
  • filtered background subscriptions over /events/stream
  • direct evaluation and quality workflows

Those capabilities are what separate Kestrel from a thin compat proxy.

Continue when you need more control