Skip to content

OpenAI-compatible chat: Chat completion

Sends one prompt to any API that speaks OpenAI’s /chat/completions (OpenAI, Mistral, Ollama, vLLM, LM Studio, LiteLLM) and returns the reply. Between them only base_url, model and the credential change.

  • Kind: openai-compatible.chat
  • Palette: Integrations, under OpenAI-compatible chat
  • Credential: optional; a credential of kind bearer, named in credential
  • Calling a hosted model other than Claude, or one you run yourself.
  • A local model with no authentication: leave credential out, since this connector’s credential is optional.
  • Getting an answer in a fixed shape: give output_schema and branch on nodes.<id>.output.data.

The request is made by whichever agent claims the step, so localhost in base_url means that agent’s machine. To reach a model at home, run an agent next to it that advertises connector plus a capability of your own, and name that capability in the step’s requires. Under spool dev the embedded agent is on your machine, so localhost works there as it is.

Field Required Description
base_url yes OpenAI-compatible API base, including the version segment. OpenAI: https://api.openai.com/v1 · Mistral: https://api.mistral.ai/v1 · Ollama: http://localhost:11434/v1 · vLLM / LM Studio / LiteLLM: your endpoint. For example: https://api.openai.com/v1.
model yes Model id for the target provider (gpt-4o-mini, mistral-small-latest, llama3.2, …). For example: gpt-4o-mini.
Field Type Required Description
prompt string yes The user message. Reference upstream data with {{ … }}. Written in a markdown editor; the rendered HTML is stored beside it as <name>_html.
system string no Optional system prompt to steer the model (persona, format, constraints).
max_tokens integer no The most tokens the reply may use, sent as max_tokens. Unset leaves it to the provider. A model that thinks spends from the same budget, and a reply cut off at it fails a step that gives output_schema, with no retry. From 1 to 131072.
output_schema object no A JSON Schema for the reply. When set, the model is asked to answer in this shape, and the parsed reply is the step’s data. The top must be an object; strict providers also want every object to list all its properties in “required” and set “additionalProperties”: false.
output_name string no The name the schema goes by in the request. Default "result".
output_mode string no How the shape is asked for: json_schema sends it as a strict response_format; json_object asks only for JSON and puts the schema in the system prompt, for providers without json_schema. One of "json_schema", "json_object". Default "json_schema".

Beside with and input in the node’s config.

Field Type Required Description
credential string no Name of the project credential the action authenticates with.
fail_on_error_status boolean no Treat any 4xx/5xx as a failure. On by default: an action either did its job or did not. Default true.
optional_when_untrusted boolean no Run without the credential when the run is untrusted (a fork pull request), instead of failing. The credential is still withheld.
retry_on array no Status codes that fail the node instead of returning, so its retry: policy applies (e.g. [429, 503]).

The HTTP response. The reply is under body: nodes.<id>.output.body.<field>.

Field Type Description
status integer The HTTP status code.
headers object The response headers.
body object The response body, parsed as JSON when it is JSON.
body.choices array The replies; choices[0].message.content is the text.
body.id string
body.model string
body.usage object prompt_tokens, completion_tokens and total_tokens.
data object The reply as JSON, checked against output_schema. Set only when the step gives one.
  • On an agent with the connector capability. The managed environments have one, so this needs no agent of your own.
  • One POST request to an address built from with.
  • Any 4xx or 5xx fails the step, unless fail_on_error_status is false. retry: asks again after a 5xx, a 401, 408, 409, 423, 425 or 429, or a 4xx with Retry-After, and not after any other 4xx, which would get the same answer.
  • With output_schema set, the reply is parsed and checked against it and returned as data. A reply that does not fit fails the step, quoting what the model said; the step’s retry: sends the request again, except after a refusal or a cut-off reply.
version: hyperspool-graph/v1
nodes:
- id: summarize
type: openai-compatible.chat
config:
credential: openai
with:
base_url: https://api.openai.com/v1
model: gpt-4o-mini
input:
system: Answer in at most two sentences.
prompt: { $template: "Summarise this changelog: {{ input.changelog }}" }
- id: post
type: shell
config:
run: { $template: "echo '{{ nodes.summarize.output.body.choices[0].message.content }}'" }
edges:
- { from: summarize, to: post }

The reply is at nodes.<id>.output.body.choices[0].message.content.

With output_schema, classify an email and route on the category:

version: hyperspool-graph/v1
nodes:
- id: classify
type: openai-compatible.chat
config:
credential: openai
with: { base_url: https://api.openai.com/v1, model: gpt-4o-mini }
input:
prompt: { $template: "Classify this support email. {{ input.email }}" }
output_schema:
type: object
properties:
category: { type: string, enum: [billing, bug, other] }
urgent: { type: boolean }
required: [category, urgent]
additionalProperties: false
retry: { attempts: 3 }
- id: route
type: switch
config:
cases:
- { name: billing, when: "nodes.classify.output.data.category == 'billing'" }
- { name: bug, when: "nodes.classify.output.data.category == 'bug'" }
default: other
edges:
- { from: classify, to: route }

The schema goes out as a strict response_format of type json_schema, which OpenAI, Mistral, Ollama and vLLM accept; on OpenRouter it depends on which provider serves the model. Strict mode wants every property listed in required and "additionalProperties": false on every object. For a provider that only offers JSON mode, set output_mode: json_object: the request asks for any JSON object and puts the schema in the system prompt. Either way the reply is checked against the schema here, so a provider that ignores the request still cannot hand a wrong shape to the next step. A reply that does not fit fails the step with the model’s text in the error, and retry: asks again. See Connectors.

A strict response_format makes the model answer in the schema’s shape, so an enum gets one of its values even when none of them is right. When the answer might not be on the list, add a value such as unknown or other and give it a branch.

max_tokens caps the reply, from 1 to 131072. Unset, the request leaves it out and the provider’s own default applies. A model that thinks before it answers spends from the same budget. A reply cut off at the cap ends with finish_reason: length; with output_schema set, that fails the step, and retry: does not ask again, since the same cap would cut it off again. Some OpenAI reasoning models take only max_completion_tokens and refuse max_tokens; leave it unset for those.

A reasoning model (GLM, DeepSeek R1, Qwen with thinking on) can spend 300 to 1500 tokens thinking before it writes a word of the answer, so a max_tokens of 1024 cuts it off on anything tricky: give it 4096 or more. Its thinking is at nodes.<id>.output.body.choices[0].message.reasoning_content (reasoning on some servers), apart from the answer in content. Cut off before the thinking ends, some servers copy the unfinished thinking into content as well; with output_schema set that is not read as the answer, and the step fails saying the model gave no answer after its reasoning.

See Connectors for credentials and versions, and the node reference for every other node.