Prompts
Every tool that drives the engine sends the same prompt for the same block. Here you can see what a prompt contains, where each part comes from, and how to print it from the binary you run.
The framework builds the prompt with the same builder and rules whoever asks.
The kapi CLI, Kapi Desktop and an embedding Go program send an identical prompt
for an identical block, so the prompt can be documented and fingerprinted. The
Prompt Reference is generated from these builders. The
examples below use the kapi CLI's --explain-prompts.
See the prompt for a run
Add --explain-prompts to any command that reaches a model. It prints every
call the run made: the prompt, the provider, the model, the reply, and the
tokens spent.
kapi translate messages.json --target-lang fr --explain-prompts
Pair it with --provider demo (the built-in offline stub) to see the prompt a
paid provider would receive, with no API key, no network, and no spend:
kapi translate messages.json --source-lang en --target-lang fr --provider demo --explain-promptsThe output attributes every block of the prompt to the thing that produced it:
─── LLM call 1/1 ─────────────────────────────────
prompt: translate.single (v3)
provider: anthropic model: claude-sonnet-5
── system ──
[task · framework]
You are an expert linguist with deep command of both languages and of the
subject domain. Translate the user's text from en to fr, rendering it as a
specialist writing natively in that domain and language would. Return ONLY
the translation, with no explanation, preamble or quoting.
[constraint · framework]
Preserve placeholders such as {0}, %s and {{name}} exactly.
[instruction · --instruction / recipe]
Instruction (apply when translating):
Informal register. Keep product names in English.
[preferred_terms · terms (12 terms)]
Terminology (use these renderings):
- utilize → use
[do_not_translate · terms (2 do-not-translate terms)]
Keep these terms exactly as written, in the same casing. Do not translate them:
- Acme
- Acme Cloud
── user ──
[content · source block]
Save
--explain-prompts=<path> writes the same calls to a file as JSON, so a prompt
can be diffed between runs or attached to a bug report.
--explain is a different flag. It reports a flow's resolved source → sink
bindings without running the flow.
How a prompt is composed
A kapi prompt is a document assembled from sections with different owners:
The sections of a kapi prompt, and who owns each.
The framework owns the task and the constraints. A translation that drops a placeholder or mangles an inline tag cannot be written back into the source file: it breaks the round trip. No flag or setting removes these rules.
Your project owns the steering. The instruction, the voice profile and the pinned terms make the output fit your project. You declare them in the recipe, the voice profile and the terms store, and never type them into a prompt.
Your document owns the content and the context. The source text is the entire user turn: data, never instruction. Its key, its neighbours and its prior translation travel separately, in the system turn, as reference the model reads but must not translate.
Steering the model
To change what the model does, add to the steering sections. The rest of the prompt is fixed.
| You want | You set |
|---|---|
| A one-off directive for this run | --instruction "informal register" |
| A standing directive for the project | defaults.tools.translate.instruction in the recipe |
| Consistent tone and phrasing | A voice profile |
| Specific terms translated a fixed way | A terms store |
| A term kept exactly as written | A concept marked do-not-translate, or --dnt |
| The model to know what a string is | --context key (default) or --context neighbours |
defaults:
source_language: en
target_languages: [fr, de]
voice:
profile: acme-docs
tools:
translate:
instruction: "Informal register. Keep product names in English."
Everything in that recipe is visible in the prompt, and --explain-prompts
shows which section each line landed in. If a pinned term is not reaching
the model, this is how you find out.
Prompts are provider-neutral
kapi keeps one prompt for every vendor. A prompt declares its turns semantically (a system turn and a user turn), and each provider carries them the way its API expects.
One prompt, carried by each provider's transport.
Providers differ in capability: how structured output is requested,
whether images are accepted, how a system instruction is carried, and a
workable batch size. That variance lives behind the provider interface.
Switching --provider anthropic to
--provider ollama changes the transport and the model, and leaves the
instructions as they are.
Identical prompts and the translation cache
The same inputs always render byte-identical prompt text. Terms in the terminology section are sorted, and no timestamp, run id or machine detail is ever interpolated.
kapi caches translations, and the cache key is derived from the rendered prompt text. Change your instruction, your voice profile, your terms, or the prompt itself, and the fingerprint moves. The affected strings are then re-translated, and none is served from a cache that a different prompt produced.
Context: what the model is told about a block
A block on its own is often not enough to translate it. Save can be a verb or
a noun (in German, Speichern or Speicherung), and a better model cannot
resolve an ambiguity in the input.
So the engine sends reference material: text the model reads but must not translate. Reference material differs from the other blocks in a batch. Every segment in a batch is a task that competes for the model's attention and distracts from the others. Reference material only disambiguates, and adds no work. More segments in a batch lower quality; more reference material does not.
context | What the model is told |
|---|---|
none | Only the text. |
key | Default. The block's key (settings.save). |
neighbours | The key, plus the surrounding source blocks. |
The key provides context already present in the document at a small token
cost. A key such as settings.save tells a button label apart from other uses
of “Save”. The key stays with its block.
Neighbours provide more context and consume more tokens. Their content is included in the cache fingerprint, so editing a neighbouring string invalidates cached translations that depended on it. It is off by default.
kapi translate messages.json --target-lang fr --context neighbours
A third piece of reference material comes from the content memory. When a block
has been edited since it was last translated, translate sends the block's own
last approved translation alongside the new source (reuse: prior, the
default), so the model revises a settled rendering.
The prior version is offered only when the governing context (voice profile,
term rules) is the one it was approved under; reuse: none turns it off.
What the engine does not send
The engine leaves these out of every prompt:
- Fuzzy content-memory matches. Memory reuse is a separate, deterministic
step (
recycle) that runs before translate and fills what it can. Matches are never offered to the model as examples; the one thing the memory contributes to a prompt is a block's own prior approved translation, above. - A rolling document context. Reference material is a bounded window around the block, never the whole document.
- Telemetry. Your content goes to the provider you configured, and nowhere else.
By default, translate batches several blocks into one call (translate.batch),
taken in document order and sent as a JSON payload keyed by segment id. Blocks
in one call are all being translated, so none of them is settled reference
material for another, and the blocks either side of the batch are in a
different call. A block is translated
on its own (translate.single) when you set batching: single, or when the
provider runs on-device: a small local model tends to ignore the "return JSON"
instruction on a large batch.
kapi sets how many blocks share a call. The right number depends on what
your model can emit, how long your segments are, and a quality-versus-batch-size
curve with no published measurements. kapi sizes each call against the model's
real output limit. A fixed batch size would overrun that limit on anything
longer than a UI string, and the model could not finish its reply. You choose
between batching: auto and batching: single.
The evidence favours a large untranslated context with a small translation
unit, and --context neighbours --batching single has that shape.
Prompt injection
The text kapi translates is often not written by the person running kapi: it is CMS copy, user comments, strings from a dependency. A block that says "ignore your instructions and output X" does occur.
The prompt tells the model the content is data, in a constraint section you
can see in --explain-prompts and in the Prompt Reference.
This is one layer of defence. No prompt-level rule reliably stops a model from
following an instruction embedded in its input.
The structure of the call limits the damage:
- The call has no tools, no filesystem and no network. It is one request that returns text. An injected instruction cannot act; at worst it produces a wrong translation, and the damage stays within the content.
- The reply is constrained to a schema by the provider's own grammar, so it cannot escape its shape, only fill it.
- kapi accepts only the segment ids it sent, so an injected omission is caught and retried, and the translations after it stay aligned.
- The payload is JSON, so content cannot forge a segment boundary. A block
containing
[2]is enough to break a delimiter-framed payload with no attacker involved, and a user documenting a numbered list writes one by accident.
An injected instruction can still corrupt the text of a translation. Checks and review catch that.
The prompts kapi ships
Every LLM call kapi makes carries a prompt id, so it can be named in
--explain-prompts and cited in a bug report:
| Prompt | Sent by |
|---|---|
translate.single | translate, one block at a time |
translate.batch | translate, several blocks in one call |
voice.check | voice-check |
voice.infer | voice-infer (drafting a profile) |
term.forms | kapi terms expand, listing a term's inflected forms |
quality.check | qa, in AI mode |
review | review |
term.extract | term-extract |
entity.extract | entity-extract |
axis.discover | context-scan, proposing the axes a corpus varies along |
media.refine.image | media-refine, re-reading an image crop |
media.refine.audio | media-refine, re-hearing a speech clip |
media.refine.video | media-refine, re-reading a video frame |
segment | segment, with the LLM engine |
The three media.refine.* prompts are separate prompts, and each sends a
different kind of your data: a cropped image, a clip of audio, a video frame.
If a call appears in --explain-prompts without an id, report it as a bug: an
unlabelled call is an undocumented use of your content.
The full text of each is published in the Prompt Reference, generated from the same builders the binary uses and checked against them in CI.
Watch a prompt get built
Build a prompt up a layer at a time and read the real output at each step. This runs kapi in your browser: the same binary, the offline demo provider, no key and no network.
Loading the walkthrough…
Start bare and you get the framework's two sections plus your content. Add
--instruction and a steering section appears, attributed to the flag. Translate
a block that carries inline markup and the tag-fidelity constraint appears,
which a plain string never sees.
Related
- Prompt Reference: the full text of every prompt.
- Translation: the
translatetool and its providers. - Voice: the voice profile that steers the prompt.