🧪 killBottleneck is in public beta — cloud and self-host.🧪 killBottleneck is in beta.Beta on GitHub →
Skip to content

Custom AI endpoint (custom)

KB_AI_PROVIDER has a custom position — your own service speaking the same language as killBottleneck. Until now that "language" was only written down in our source code, which is not good enough: anyone wanting to plug in their own service had to read it there. Here it is in full.

Most people do not need this page

If you have a key from OpenAI, OpenRouter, Groq and friends, pick openai — it speaks the common shape and there is nothing to build. custom is for when you want your own layer between killBottleneck and the model (your own prompts, routing across several models, request logging, content filtering).

The “Custom endpoint” mode in Administration → AI features. The contract below describes what such a service must do.

How it works

killBottleneck sends a POST with a JSON body to KB_AI_URL. The body always carries mode (which job this is) and lang (cs or en — the language to answer in; the server decides it from the signed-in user, a client cannot spoof it).

MethodPOST
HeadersContent-Type: application/json, X-KB-Token: <KB_AI_TOKEN>
Timeout120 s (600 s for transcribe)
ResponseJSON, HTTP 2xx

You may include "schema_version": 1 in the response. If you send a different number, killBottleneck rejects the answer with a clear error instead of breaking silently. Without the field nothing is checked.

Jobs (mode)

questions — clarifying questions about a goal

json
// request
{ "mode": "questions", "goal": "Throw a company party", "scope": "detailní", "lang": "en" }
// response
{ "questions": ["How many people?", "By when?", "What budget?"] }

Return 1–5 questions; killBottleneck takes the first five.

generate — a plan tree from a goal

json
// request
{ "mode": "generate", "goal": "Throw a company party", "scope": "detailní",
  "answers": ["40 people", "before Christmas", "80k"], "lang": "en" }
// response
{ "nodes": [
  { "id": "root", "title": "Throw a company party", "description": "", "parentId": null },
  { "id": "n1", "title": "Book a venue", "description": "By the end of October.", "parentId": "root" }
] }

Rules killBottleneck enforces on the answer:

  • exactly one root — the node with parentId: null;
  • parentId must point at an id present in the same list (otherwise the node is attached to the root);
  • title is trimmed to 120 characters;
  • answers holds the replies to the previous step's questions — it may be absent.

scope is one of three values: "stručná", "detailní", "hloubková" (they are values, not user-facing text — they are never translated or changed).

from_text — a plan tree from free text

json
{ "mode": "from_text", "text": "Meeting notes…", "scope": "detailní", "lang": "en" }

The response has the same shape as generate. killBottleneck trims the text to 8,000 characters before sending.

expand — expand a single node

json
// request
{ "mode": "expand", "goal": "Throw a company party",
  "path": ["Throw a company party", "Book a venue"],
  "node": { "id": "n1", "title": "Book a venue", "description": "By the end of October." },
  "action": "subgoals", "count": 3, "lang": "en" }
// response
{ "nodes": [ { "title": "Visit three places", "description": "Compare price and capacity." } ] }

Here the nodes have no id and no parentId — they hang under the node from the request.

actionWhat is expected
subgoalsconcrete sub-steps for completing the node
milestonesmeasurable milestones along the way
kpimetrics by which success is measured
risksrisks (in title) and how to mitigate them (in description)
rewriteone node — a better wording of the same thing (count is 1)

chat — a conversation over the map

json
// request
{ "mode": "chat", "message": "What is missing from my plan?",
  "map": { "title": "Party", "nodes": [
    { "id": "root", "title": "Party", "description": "", "status": "todo", "parentId": null } ] },
  "lang": "en" }
// response
{ "reply": "You are missing a budget and a date.", "operations": [] }

When the conversation should also change the map, return operations. Anything other than these four is discarded:

json
{ "op": "add",    "parentId": "root", "title": "…", "description": "…" }
{ "op": "update", "id": "n1", "title": "…", "description": "…", "status": "todo|in_progress|done" }
{ "op": "delete", "id": "n1" }
{ "op": "move",   "id": "n1", "newParentId": "root" }

transcribe — transcribe a recording

Goes to KB_AI_TRANSCRIBE_URL. When that is empty it is derived from KB_AI_URL (a trailing kb-advisor becomes kb-transcribe).

json
// request
{ "mode": "transcribe", "audio_base64": "…", "filename": "recording.webm", "lang": "en" }
// response
{ "text": "the transcribed text" }

Note this is not the OpenAI shape (which wants multipart with a file). If your transcription is OpenAI-compatible, pick the openai provider — it builds the multipart request itself.

Errors

What you returnWhat the user sees
HTTP 2xx with the expected shapethe result
HTTP 403 or 429 with { "error": "…", "code": "…" }your message (quota refusals are passed through unchanged)
any other error status"The AI service returned an error (HTTP …)"
unreachable, timeout"Could not connect to the AI service"
schema_version ≠ 1"Incompatible AI contract version"

Things to watch

  • Answer in the language you were asked for. lang is not decoration — the user gets your text as it is.
  • Return valid JSON, not text with JSON inside it. killBottleneck can strip a ```json wrapper, but there is no reason to rely on that.
  • An empty answer is an error, not an empty result — prefer an error status with an explanation, so the user learns more than "nothing happened".
  • On a hosted instance the address must not point into a private network (otherwise the instance could be used to scan the provider's network). On your own server a private address is perfectly fine.

fair-code — self-hosting and internal use are free, reselling as a hosted service is not.