Prompt Interceptor Extension¶
An extension that prepends a tone directive to every generation, configurable from the frontend.
spindle.json¶
{
"version": "1.0.0",
"name": "Tone Adjuster",
"identifier": "tone_adjuster",
"author": "Dev",
"github": "https://github.com/dev/tone-adjuster",
"homepage": "https://github.com/dev/tone-adjuster",
"permissions": ["interceptor"],
"entry_backend": "dist/backend.js"
}
src/backend.ts¶
declare const spindle: import('lumiverse-spindle-types').SpindleAPI
// Load saved tone from storage, default to 'neutral'
let currentTone = 'neutral'
;(async () => {
try {
currentTone = await spindle.storage.read('tone.txt')
} catch {
// File doesn't exist yet, use default
}
})()
spindle.registerInterceptor(async (messages, context) => {
if (currentTone === 'neutral') return messages
const injected = {
role: 'system' as const,
content: `[Tone directive] Respond in a ${currentTone} tone.`,
}
// Prepend a system instruction about tone and expose it in Prompt Breakdown
return {
messages: [injected, ...messages],
breakdown: [
{
messageIndex: 0,
name: 'Tone Directive',
},
],
}
}, 10) // Low priority = runs early
// Listen for tone changes from the frontend
spindle.onFrontendMessage(async (payload: any, userId) => {
if (payload.type === 'set_tone') {
currentTone = payload.tone
await spindle.storage.write('tone.txt', currentTone)
spindle.sendToFrontend({ type: 'tone_updated', tone: currentTone }, userId)
spindle.log.info(`Tone set to: ${currentTone}`)
}
})
spindle.log.info('Tone Adjuster loaded!')
How It Works¶
- Loads the saved tone preference from storage on startup
- Registers an interceptor with priority
10(runs early in the chain) - If tone is not
neutral, prepends a system message with the tone directive - Marks that injected message as a Prompt Breakdown block so users can see it in dry-run and saved breakdown views
- Listens for
set_tonemessages from the frontend to update the tone - Persists the tone choice to storage so it survives restarts
Structured Prefill Interceptor¶
An extension that injects response_format into the user's normal chat generation to enforce structured output via an interceptor.
spindle.json¶
{
"version": "1.0.0",
"name": "Structured Prefill",
"identifier": "structured_prefill",
"author": "Dev",
"github": "https://github.com/dev/structured-prefill",
"homepage": "https://github.com/dev/structured-prefill",
"permissions": ["interceptor", "generation_parameters"],
"entry_backend": "dist/backend.js"
}
src/backend.ts¶
declare const spindle: import('lumiverse-spindle-types').SpindleAPI
spindle.registerInterceptor(async (messages, context) => {
// Look for an assistant prefill at the end of the prompt
const last = messages[messages.length - 1]
if (!last || last.role !== 'assistant') {
// No prefill — pass through unchanged
return messages
}
const prefillText = last.content
// Remove the prefill message from the prompt
const filtered = messages.slice(0, -1)
// Inject a json_schema that forces the model to start with the prefill text
return {
messages: filtered,
parameters: {
response_format: {
type: 'json_schema',
json_schema: {
name: 'prefilled_response',
strict: true,
schema: {
type: 'object',
properties: {
text: {
type: 'string',
pattern: `^${escapeRegex(prefillText)}`,
},
},
required: ['text'],
},
},
},
},
}
}, 200) // High priority number = runs late, after other interceptors
function escapeRegex(str: string): string {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}
spindle.log.info('Structured Prefill loaded!')
How It Works¶
- Registers an interceptor with priority
200(runs late so other interceptors process first) - Checks if the last message is an assistant-role prefill
- If found, removes the prefill message and returns an
InterceptorResultDTOwith aresponse_formatparameter that forces the LLM output to begin with the prefill text - The
generation_parameterspermission allows theparametersfield to be merged into the outgoing request - Works with any OpenAI-compatible provider that supports
response_format: { type: "json_schema" }