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.

0.5.0tsx examples/…node >=22

Example 1

Predictstring signature

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

Predictnumberboolean

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

Predictstring[]optional

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

ChainOfThought2 calls

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

RespActtoolstracing

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

streamingno validation

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

portability3 providers

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

ValidationErrorLMErrorproduction

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'
Reading this

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

vitestno network

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