Skip to content
skyl

POST /v1/chat

A completion. Returns the assistant turn in the same shape a request takes.

The completion endpoint. Its response carries message — the assistant's turn in the same shape a request takes — which is what makes a tool-calling loop expressible over HTTP.

Reference#

POST /v1/chat

Requires Authorization: Bearer <SKYL_AUTH_TOKEN>. Returns application/json.

Request body — ChatRequest#

FieldTypeJSONDescription
ProviderstringproviderWhich registered provider to use.
Zero value: the gateway’s default provider
ModelstringmodelThe provider’s model identifier, passed through untouched.
Zero value: rejected — the model is required
SystemstringsystemThe system prompt. The gateway places it where each provider expects.
Zero value: no system prompt
Messages[]ChatMessagemessagesThe conversation so far. Must not be empty.
Zero value: rejected
MaxTokensintmax_tokensCaps the response length.
Zero value: the provider's default, which for Anthropic means skyl supplies 4096
Temperature*float64temperatureSampling temperature. Sent only when present.
Zero value: omitted
TopP*float64top_pNucleus sampling. Sent only when present.
Zero value: omitted
Stop[]stringstopSequences that end generation.
Zero value: none
Tools[]ChatTooltoolsTools the model may call.
Zero value: no tools offered
ToolChoice*ChatToolChoicetool_choiceConstrains tool use: auto, none, required, or a named tool.
Zero value: auto
Thinking*ChatThinkingthinkingRequests reasoning. Support varies sharply by provider.
Zero value: the provider's default
ProviderOptionsmap[string]anyprovider_optionsVendor-specific fields merged into the outbound payload, overriding anything skyl set.
Zero value: nothing extra sent

ChatMessage#

FieldTypeJSONDescription
RolestringroleOne of user, assistant, or tool.
TextstringtextShorthand for a turn whose content is a single run of text. Use it instead of Content for the common case.
Zero value: no shorthand text; Content is used
Content[]ChatPartcontentTyped content parts. This is what makes a tool-calling loop expressible: an assistant turn containing tool calls can be sent back verbatim.
Zero value: no parts; Text is used

Use text for a plain turn and content for anything typed. Do not set both.

ChatPart#

FieldTypeJSONDescription
TypestringtypeOne of text, image, tool_call, or tool_result.
TextstringtextFor type: text.
MediaTypestringmedia_typeFor type: image with inline data — the IANA media type, e.g. image/png.
DatastringdataFor type: image — base64-encoded image content.
URLstringurlFor type: image — a remotely hosted image. Gemini rejects this form.
IDstringidFor type: tool_call — the call identifier the provider generated.
NamestringnameFor type: tool_call — the tool the model wants to run.
Argumentsjson.RawMessageargumentsFor type: tool_call — the JSON object the model produced.
ToolCallIDstringtool_call_idFor type: tool_result — must match the tool_call it answers.
ContentstringcontentFor type: tool_result — the tool's output, rendered as text.
IsErrorboolis_errorFor type: tool_result — reports that the tool failed. Reaches the model faithfully only on Anthropic.

Response body — ChatResponse#

FieldTypeJSONDescription
IDstringidThe provider's response identifier.
ProviderstringproviderThe adapter that produced this response.
ModelstringmodelThe model that actually served the request, read from the response rather than echoed.
TextstringtextEvery text part concatenated — the convenience field for the common case.
MessageChatMessagemessageThe assistant's turn, in the same shape a request takes, so it can be appended to your conversation and replayed verbatim. This is what makes the tool loop work over HTTP.
StopReasonstringstop_reasonWhy generation ended.
ToolCalls[]ChatToolCalltool_callsThe tool calls the model requested, lifted out of Message for convenience.
UsageChatUsageusageToken consumption.
Rawjson.RawMessagerawThe provider's untouched body. Present only when SKYL_INCLUDE_RAW is on.

Usage#

A simple completion

curl -sS localhost:8080/v1/chat \
  -H "Authorization: Bearer $SKYL_AUTH_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "provider": "anthropic",
    "model": "claude-opus-5",
    "max_tokens": 512,
    "messages": [{"role": "user", "text": "Hello"}]
  }'
curl -sS localhost:8080/v1/chat \
  -H "Authorization: Bearer $SKYL_AUTH_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "provider": "anthropic",
    "model": "claude-opus-5",
    "max_tokens": 512,
    "messages": [{"role": "user", "text": "Hello"}]
  }'
{
  "id": "msg_01...",
  "provider": "anthropic",
  "model": "claude-opus-5",
  "text": "Hello! How can I help?",
  "message": {
    "role": "assistant",
    "content": [{"type": "text", "text": "Hello! How can I help?"}]
  },
  "stop_reason": "end_turn",
  "usage": {"input_tokens": 9, "output_tokens": 8}
}
{
  "id": "msg_01...",
  "provider": "anthropic",
  "model": "claude-opus-5",
  "text": "Hello! How can I help?",
  "message": {
    "role": "assistant",
    "content": [{"type": "text", "text": "Hello! How can I help?"}]
  },
  "stop_reason": "end_turn",
  "usage": {"input_tokens": 9, "output_tokens": 8}
}

A system prompt and history

{
  "provider": "openai",
  "model": "gpt-5.6",
  "system": "You are a terse Go expert.",
  "max_tokens": 256,
  "messages": [
    {"role": "user", "text": "What is a nil map?"},
    {"role": "assistant", "text": "A map that is declared but not allocated."},
    {"role": "user", "text": "Can I read from one?"}
  ]
}
{
  "provider": "openai",
  "model": "gpt-5.6",
  "system": "You are a terse Go expert.",
  "max_tokens": 256,
  "messages": [
    {"role": "user", "text": "What is a nil map?"},
    {"role": "assistant", "text": "A map that is declared but not allocated."},
    {"role": "user", "text": "Can I read from one?"}
  ]
}

A tool-calling round trip

Declare the tool and send the turn:

{
  "provider": "anthropic",
  "model": "claude-opus-5",
  "max_tokens": 512,
  "messages": [{"role": "user", "text": "Weather in Kampala?"}],
  "tools": [{
    "name": "get_weather",
    "description": "Get the current weather for a city.",
    "parameters": {
      "type": "object",
      "properties": {"city": {"type": "string"}},
      "required": ["city"]
    }
  }]
}
{
  "provider": "anthropic",
  "model": "claude-opus-5",
  "max_tokens": 512,
  "messages": [{"role": "user", "text": "Weather in Kampala?"}],
  "tools": [{
    "name": "get_weather",
    "description": "Get the current weather for a city.",
    "parameters": {
      "type": "object",
      "properties": {"city": {"type": "string"}},
      "required": ["city"]
    }
  }]
}

The response carries the calls and the replayable turn:

{
  "stop_reason": "tool_use",
  "message": {
    "role": "assistant",
    "content": [{"type": "tool_call", "id": "c1", "name": "get_weather",
                 "arguments": {"city": "Kampala"}}]
  },
  "tool_calls": [{"id": "c1", "name": "get_weather", "arguments": {"city": "Kampala"}}]
}
{
  "stop_reason": "tool_use",
  "message": {
    "role": "assistant",
    "content": [{"type": "tool_call", "id": "c1", "name": "get_weather",
                 "arguments": {"city": "Kampala"}}]
  },
  "tool_calls": [{"id": "c1", "name": "get_weather", "arguments": {"city": "Kampala"}}]
}

Send message back verbatim, then the result:

{
  "messages": [
    {"role": "user", "text": "Weather in Kampala?"},
    {"role": "assistant", "content": [{"type": "tool_call", "id": "c1",
      "name": "get_weather", "arguments": {"city": "Kampala"}}]},
    {"role": "tool", "content": [{"type": "tool_result", "tool_call_id": "c1",
      "content": "22C and sunny"}]}
  ]
}
{
  "messages": [
    {"role": "user", "text": "Weather in Kampala?"},
    {"role": "assistant", "content": [{"type": "tool_call", "id": "c1",
      "name": "get_weather", "arguments": {"city": "Kampala"}}]},
    {"role": "tool", "content": [{"type": "tool_result", "tool_call_id": "c1",
      "content": "22C and sunny"}]}
  ]
}

Echoing message back is exactly why it exists: every provider rejects a tool result that does not follow the call it answers.

An image

{
  "messages": [{
    "role": "user",
    "content": [
      {"type": "image", "media_type": "image/png", "data": "<base64>"},
      {"type": "text", "text": "What does this chart show?"}
    ]
  }]
}
{
  "messages": [{
    "role": "user",
    "content": [
      {"type": "image", "media_type": "image/png", "data": "<base64>"},
      {"type": "text", "text": "What does this chart show?"}
    ]
  }]
}

Over HTTP, data is base64 — unlike the Go API, where Image.Data is raw bytes.

Errors#

skyl errorHTTPkindNote
ErrAuth502authThe gateway's own provider credential was rejected. Not 401 — the caller did nothing wrong.
ErrRateLimit429rate_limitReturned only after skyl exhausted its retries upstream.
ErrNotFound404not_foundNo such model for this account — usually a typo, since model IDs are not validated.
ErrBadRequest400bad_requestMalformed request, caught by local validation or by the provider.
ErrUnsupported400unsupportedThe provider cannot express part of the request — an image URL on Gemini, say.
ErrRefusal422refusalThe model or its safety classifiers declined. Do not retry the same request.
ErrServer502serverThe provider failed on its side.
(anything else)502unknownThe default. 502 is the fallback for an unclassified upstream failure.

And before any upstream call happens:

HTTPkindWhen
401authThe caller presented a missing or wrong bearer token.
400bad_requestInvalid JSON, or an unknown field. DisallowUnknownFields is on, so a newer client against an older gateway gets a 400 rather than a silent ignore — upgrade gateways first.
404not_foundThe named provider is not registered on this gateway.

The provider's raw error body is never forwarded: it can echo request content back to a caller who should not see it.

Troubleshooting#

400 on a request that used to work

The wire format changed: "content": "hi" became "text": "hi", and DisallowUnknownFields is on. Update the client.

502 with kind “auth”

The gateway's own provider key was rejected. This is an operator problem, not a caller one — check the ANTHROPIC_API_KEY / OPENAI_API_KEY in the gateway's environment.

raw is missing

SKYL_INCLUDE_RAW defaults to false, because the body crosses a trust boundary.

422 with kind “refusal”

The model or its classifiers declined. The request was well-formed and understood — do not retry it unchanged.

Edit this page on GitHub