ZKRINET Docs
Building blocks

AI with your own key

On Zkrinet: add the block under Building blocks in the panel — one click, your own code is untouched and a snapshot is taken first. The installation steps below apply if you run PocketBase yourself, for example after an export.

Let your app talk to an AI model — with your own API key, which never leaves the server. The app calls a single route; which provider, model and system prompt are used is decided in the admin panel. With a quota per user, a monthly budget and a log.

Supports Anthropic (Claude), OpenAI, Google Gemini and OpenAI-compatible services — e.g. Mistral (EU) or your own model in Ollama/vLLM.

Tested against PocketBase 0.40.4.

Installation

  1. Copy pb_hooks/ and pb_migrations/ to your PocketBase and restart.
  2. Create one row in zk_ai_installningar:
Field
leverantoranthropic, openai, gemini or kompatibel
nyckelYour API key (hidden — only superusers see it). Alternatively the environment variable ZK_AI_NYCKEL, which wins. Can be left empty for a local model without a key.
modellRequired for OpenAI, Gemini and compatible. For Anthropic the default is claude-opus-5-5.
bas_urlOnly for kompatibel (e.g. https://api.mistral.ai or http://localhost:11434), or if you go via a proxy
max_tokensCap on the length of the response. Anthropic: default 16,000
anstrangningAnthropic only: low/medium/high — lower = faster and cheaper
dagsgrans_per_anvandareQuestions per user per day (UTC). Default 50
manadsgrans_tokensStop all calls when the month's tokens (in + out) reach the limit. Empty = no limit — set it, it is your key that pays
spara_innehallOff by default: the log stores only the number of tokens, not what was asked. Only turn it on if you need it, and mention it in your privacy policy.
  1. Tasks: zk_ai_uppdrag already contains assistent. Add more (e.g. sammanfatta, oversatt) with their own system prompt, and if needed their own model and max_tokens. The system prompts stay on the server — the app can neither read nor change them.

Usage from the app

const r = await pb.send("/api/zk/ai/chatt", { method: "POST", body: {
  uppdrag: "assistent",                     // optional, default "assistent"
  meddelanden: [
    { roll: "anvandare", text: "How much is shipping?" },
    { roll: "assistent", text: "Shipping costs 49 kr." },
    { roll: "anvandare", text: "And to Norway?" },
  ],
}})
// r = { svar: "…", vagrad: false, modell: "…", tokens: { in: 412, ut: 38 }, kvar_idag: 47 }

// Show how many questions are left
const { kvar_idag } = await pb.send("/api/zk/ai/kvot")

The app sends the whole conversation every time (max 50 messages / 50,000 characters). It must begin and end with the user's message. Login is required.

ResponseMeans
200, vagrad: falseThe answer is in svar
200, vagrad: trueThe model declined the question. Show a friendly message.
400Error in the request (unknown task, invalid conversation)
429Today's quota or the month's budget has run out
502The provider responded with an error — the real error is in zk_ai_logg, the app gets a generic message
503Not configured

The user can read their own rows in zk_ai_logg (e.g. to show usage).

Good to know

Limitations

Test

./bank.py ai

14 tests against a fake provider (nothing reaches the internet): correct format for all four providers, fallback model only where it applies, refusals, that provider errors do not leak to the app, invalid conversations, daily quota, monthly budget, content logging off/on, permissions and key from the environment. The protections are mutant-verified.

Who sees your requests

The block sends the text you process to the provider you chose, with your own key: Anthropic, OpenAI, Google Gemini or your own compatible service. That provider is then your provider, not ours. If the requests contain personal data you are responsible for having a data processing agreement with the provider and for where the data ends up. Zkrinet does not see the content of the requests; the log only stores token counts unless you turn on more.