v0.5.0  /  TypeScript  /  MIT

You declared a number.
The model said “high”.

Declare the shape you want back. TS-DSPy checks the model’s reply against it and hands you typed values — or throws, naming the field that failed. What it never does is give you a string wearing a number’s type.

Predict(AnswerQuestion).forward() idle
answerstring
scorenumber
sourcesstring[]

The difference

Types that are checked, not assumed

Without validation

const { score } = await predict({ question })

// TypeScript says: number
// Actually holds: "high"
score.toFixed(2)  // throws, far from the cause

The failure surfaces somewhere else entirely — a chart, a sum, a database write — long after the reply that caused it.

With TS-DSPy

try {
  const { score } = await predict({ question })
} catch (e) {
  // ValidationError, at the boundary
  e.issues     // [{ field: 'score', expected: 'number' }]
  e.rawOutput  // what the model actually said
}

You find out at the parse boundary, with the field name and the raw reply in hand.

Get started

Declare the shape. Get it back, checked.

Install

npm install @ts-dspy/core @ts-dspy/anthropic

Requirements

Node.js 22 or newer. Ships ESM and CommonJS builds with types for both.

Write it

import { Signature, InputField, OutputField, Predict, configure } from '@ts-dspy/core'
import { AnthropicLM } from '@ts-dspy/anthropic'

configure({ lm: new AnthropicLM({ apiKey: process.env.ANTHROPIC_API_KEY }) })

class RateAnswer extends Signature {
  @InputField({ description: 'the question' })
  question!: string

  @OutputField({ description: 'confidence 0-1', type: 'number' })
  score!: number
}

const { score } = await new Predict(RateAnswer).forward({ question })
// score is a number, or this threw

Providers

One interface, three vendors

Each provider wraps its vendor’s official SDK and uses the native structured-output mode when the model has one. Swap the provider; the signature and the module stay put.

  • @ts-dspy/anthropic

    claude-opus-5

    Structured outputs, streaming, and refusals surfaced as a typed error rather than empty text.

    npm i @ts-dspy/anthropic
  • @ts-dspy/openai

    gpt-5.2

    Chat Completions on the official SDK, JSON-schema structured outputs, and any OpenAI-compatible base URL.

    npm i @ts-dspy/openai
  • @ts-dspy/gemini

    gemini-3.5-flash

    Built on @google/genai with response schemas, streaming, and Vertex AI support.

    npm i @ts-dspy/gemini

Concepts

Three pieces

Signature
A class whose decorated fields declare what goes in and what must come back, with a type per output field. It is the contract the reply is measured against, and the source of the JSON schema sent to providers that accept one.
Module
The strategy that fills the contract. Predict asks once. ChainOfThought reasons first, then answers. RespAct runs a tool loop until it can.
Validation
Every reply is parsed and checked against the signature before it reaches you. Coercion stays lenient, because models emit text — "42" satisfies a number. Failure does not: it throws ValidationError naming the field.

How it works

One declaration, three consumers

The signature you write drives the prompt, the JSON Schema sent to the provider, and the schema your reply is validated against. Both routes through the middle end at the same gate, so nothing reaches your code unchecked.

declared field types class AnswerQuestion extends Signature @InputField / @OutputField → static field maps inputs buildPrompt() description + labelled inputs + field list prompt lm.getCapabilities() .supportsStructuredOutput true false generateStructured() buildOutputJsonSchema(signature) provider constrains decoding generate() parseOutput() scrapes "field: value" BaseLM fallback asks for JSON in prose candidate object buildOutputSchema().safeParse() one zod schema, built from the declared types success issues[] Prediction<T> direct field access ValidationError issues + rawOutput
Fig. 1 — The request path. The dashed rail is the point of the library: one declaration feeds the prompt, the JSON Schema, and the validator. Both branches end at the same safeParse, so nothing reaches your code untyped. All three shipped providers report supportsStructuredOutput: true, so the right-hand branch is what a custom ILanguageModel gets — and what RespAct always uses.

Read the architecture