Examples
Nine things
you might build.
Each one runs as written, given an API key in the environment. They start simple and end at the parts most libraries leave out: failure handling, provider swapping, and tests.
Example 1
Question answering
The shortest useful program. A string signature needs no decorators.
import { Predict, configure } from '@ts-dspy/core'
import { AnthropicLM } from '@ts-dspy/anthropic'
configure({ lm: new AnthropicLM({ apiKey: process.env.ANTHROPIC_API_KEY! }) })
const qa = new Predict('question -> answer')
const out = await qa.forward({ question: 'What is a B-tree used for?' })
console.log(out.answer)
Output
B-trees keep sorted data on disk with a high branching factor, so a lookup touches few blocks. Databases and filesystems use them for indexes.
Example 2
Classification with a confidence score
Declaring type: 'number' is what makes confidence
arithmetic-safe. Without it you get a string that looks like a number until
you multiply by it.
import { Signature, InputField, OutputField, Predict } from '@ts-dspy/core'
class TriageTicket extends Signature {
static description = 'Classify a support ticket.'
@InputField({ description: 'the ticket body' })
ticket!: string
@OutputField({ description: 'one of: billing, bug, feature, other' })
category!: string
@OutputField({ description: 'confidence from 0 to 1', type: 'number' })
confidence!: number
@OutputField({ description: 'true if a human should see this today', type: 'boolean' })
urgent!: boolean
}
const out = await new Predict(TriageTicket).forward({
ticket: 'Charged twice this month and support never replied.',
})
if (out.urgent && out.confidence > 0.7) page(out.category)
Output
{ category: 'billing', confidence: 0.93, urgent: true }
string number boolean
Example 3
Structured extraction
Arrays and optional fields. required: false makes a field
optional in both the schema and the inferred type, so a missing value is
not a failure.
class ExtractInvoice extends Signature {
static description = 'Pull structured fields out of an invoice email.'
@InputField({ description: 'raw email text' })
email!: string
@OutputField({ description: 'vendor name' })
vendor!: string
@OutputField({ description: 'total in dollars', type: 'number' })
total!: number
@OutputField({ description: 'each line item description', type: 'string[]' })
lineItems!: string[]
@OutputField({ description: 'due date if stated', required: false })
dueDate?: string
}
const out = await new Predict(ExtractInvoice).forward({ email })
Output — note "$1,240.00" coerced to a number
{
vendor: 'Northwind Traders',
total: 1240,
lineItems: [ 'Annual licence', 'Priority support' ],
dueDate: undefined
}
Example 4
Chain of thought
Reasons first, answers second. You get the reasoning alongside the validated fields, which is useful to log even when you do not show it.
import { ChainOfThought } from '@ts-dspy/core'
class SolveWordProblem extends Signature {
@InputField({ description: 'a word problem' })
problem!: string
@OutputField({ description: 'the numeric answer', type: 'number' })
answer!: number
}
const out = await new ChainOfThought(SolveWordProblem).forward({
problem: 'A train leaves at 09:40 and arrives at 13:15. How many minutes?',
})
logger.debug({ reasoning: out.reasoning })
console.log(out.answer) // 215, as a number
Example 5
A tool loop
The model decides which tool to call and when it has enough to answer. Tool descriptions are the whole interface — write them as instructions, and say what the input should be.
import { RespAct } from '@ts-dspy/core'
const support = new RespAct('question -> answer', {
tools: {
orderStatus: {
description: 'Look up an order. Input: the order ID, e.g. A-4182.',
fn: async (id) => JSON.stringify(await orders.find(id.trim())),
},
shippingPolicy: {
description: 'Return the shipping policy. Input: ignored.',
fn: () => policies.shipping,
},
},
maxSteps: 8,
onEvent: (e) => logger.debug({ respact: e }),
})
const out = await support.forward({
question: 'Order A-4182 has not arrived. Is it late?',
})
console.log(out.answer, 'in', out.steps, 'steps')
Trace
Thought: Need the order's ship date before judging lateness.
Action: orderStatus(A-4182)
Observation: {"shipped":"2026-08-14","carrier":"DHL","eta":"2026-08-18"}
Thought: ETA has passed. Check the policy for what counts as late.
Action: shippingPolicy()
Observation: Orders are late 2 business days past ETA.
Final Answer: Yes - it is three days past the 18 Aug ETA.
Example 6
Streaming to a UI
Streaming skips signature validation by design: there is no complete reply to check yet. Stream for display, then use a module when you need the parsed object.
const lm = new AnthropicLM({ apiKey: process.env.ANTHROPIC_API_KEY! })
for await (const chunk of lm.generateStream('Explain WAL in two paragraphs.')) {
res.write(chunk.content)
if (chunk.done) logger.info({ usage: chunk.usage })
}
res.end()
Example 7
Swapping providers
Signatures and modules name no vendor, so the provider is a runtime choice. This is also how you run the same evaluation across models.
import { AnthropicLM } from '@ts-dspy/anthropic'
import { OpenAILM } from '@ts-dspy/openai'
import { GeminiLM } from '@ts-dspy/gemini'
const providers = {
claude: () => new AnthropicLM({ apiKey: process.env.ANTHROPIC_API_KEY! }),
gpt: () => new OpenAILM({ apiKey: process.env.OPENAI_API_KEY! }),
gemini: () => new GeminiLM({ apiKey: process.env.GEMINI_API_KEY! }),
}
for (const [name, make] of Object.entries(providers)) {
const triage = new Predict(TriageTicket, make())
const out = await triage.forward({ ticket })
console.log(name, out.category, out.confidence)
}
Example 8
Handling failure
The case worth writing code for. A validation failure tells you which field the model got wrong and what it actually said, which is usually enough to fix the field description.
import { ValidationError, LMError } from '@ts-dspy/core'
async function triageWithFallback(ticket: string) {
try {
return await triage.forward({ ticket }, { timeout: 20_000 })
} catch (err) {
if (err instanceof ValidationError) {
// The model answered, but not in the declared shape.
logger.warn({
fields: err.issues.map((i) => i.field),
raw: err.rawOutput,
})
return { category: 'other', confidence: 0, urgent: true }
}
if (err instanceof LMError) {
// The call itself failed. The SDK already retried.
logger.error({ cause: err.cause })
throw err
}
throw err
}
}
A caught ValidationError
ValidationError: Output did not match TriageTicket
issues: [ { field: 'confidence',
expected: 'number',
received: 'very high' } ]
rawOutput: 'category: billing\nconfidence: very high\nurgent: yes'
urgent: yes passed — "yes" is accepted for a
boolean. "very high" is not a number and no amount of
coercion makes it one, so the whole reply is rejected rather than handing
back a partly-wrong object. Tightening the description to
'confidence from 0 to 1' is the fix.
Example 9
Testing without a network
Pass a stub model and your tests are deterministic, free, and fast. Test the shapes you fear, not the one you hope for.
import { describe, it, expect } from 'vitest'
import { Predict, ValidationError } from '@ts-dspy/core'
import { MockLM } from '@ts-dspy/core/testing'
describe('TriageTicket', () => {
it('coerces a numeric string', async () => {
const lm = new MockLM({ responses: ['category: bug\nconfidence: 0.82\nurgent: yes'] })
const out = await new Predict(TriageTicket, lm).forward({ ticket: 'x' })
expect(out.confidence).toBe(0.82)
expect(typeof out.confidence).toBe('number')
expect(out.urgent).toBe(true)
})
it('rejects prose where a number was declared', async () => {
const lm = new MockLM({ responses: ['category: bug\nconfidence: very high\nurgent: yes'] })
await expect(new Predict(TriageTicket, lm).forward({ ticket: 'x' }))
.rejects.toBeInstanceOf(ValidationError)
})
})
Running these
The repository ships runnable versions under examples/. They
read their key from the environment and fail with a clear message when it is
missing.
git clone https://github.com/ardada2468/LLMTypeSafe.git
cd LLMTypeSafe && npm ci && npm run build
ANTHROPIC_API_KEY=... npm run example:anthropic
OPENAI_API_KEY=... npm run example:openai
GEMINI_API_KEY=... npm run example:gemini