DSTS is a minimal, AI SDK–aligned prompt optimizer for TypeScript. It optimizes prompts for both generateObject (with Zod schemas) and generateText with the latest GEPA optimizer (soft G like “giraffe”). GEPA evolves a prompt along a Pareto frontier across multiple objectives that matter in practice: task performance, latency, and cost.
- AI Gateway–aligned: pass model ids as strings; generateObject with a Zod schema for a structured object or generateText with a string.
- Minimal abstractions: one default adapter to call
generateObject/generateTextand one optimizer. - Multi‑objective first: correctness + latency (and cost) tracked per iteration; Pareto front and hyper‑volume (2D) reported.
- Persistence & budgets: checkpoint/resume, per‑call cost estimation (via tokenlens) and budget caps, seeded minibatching.
npm i @currentai/dsts zodimport { z } from "zod";
import { optimize, DefaultAdapterTask } from "@currentai/dsts";
// Define schema (generateObject)
const Item = z.object({ title: z.string(), url: z.string().url() });
// Training data: use schema ⇒ generateObject; provide expectedOutput and/or a scorer
const trainset: DefaultAdapterTask<z.infer<typeof Item>>[] = [
{
input: "link to TS docs",
expectedOutput: {
title: "TypeScript",
url: "https://www.typescriptlang.org",
},
schema: Item,
},
];
const valset = trainset;
const result = await optimize({
seedCandidate: { system: "Extract a title and a valid URL from the text." },
trainset,
// Explicit held-out set used for Pareto selection
valset,
taskLM: {
model: "openai/gpt-5-nano",
// Any extra fields are passed through to AI SDK calls
temperature: 0.2,
providerOptions: { openai: { service_tier: "default" } },
},
reflectionLM: {
model: "openai/gpt-5.2",
providerOptions: { openai: { reasoningEffort: "high" } },
},
// Simple way to steer reflection without templates
reflectionHint: "Prioritize exact field correctness; avoid hallucinating properties.",
maxIterations: 5,
maxMetricCalls: 200,
maxBudgetUSD: 50,
reflectionMinibatchSize: 3,
candidateSelectionStrategy: "pareto",
componentSelector: "round_robin",
logger: {
log: (lvl, msg, data) => {
if (lvl === "info") console.log(`[${lvl}] ${msg}`, data || "");
},
},
persistence: {
dir: "runs/quickstart",
checkpointEveryIterations: 1,
resume: true,
},
});
console.log("Best system prompt:", result.bestCandidate.system);dsts currently works best as a prompt refiner, not a prompt inventor.
For heavy structured tasks, the seed prompt should already encode the stable protocol of the task:
- output shape and schema expectations,
- allowed labels or field values,
- evidence or citation format,
- claim granularity,
- edge-case handling rules.
If the seed prompt is weak, early GEPA iterations tend to spend budget rediscovering schema constraints instead of improving domain behavior. In practice, the best workflow is:
- Hand-write a seed prompt that is already structurally competent.
- Make sure it can satisfy the basic schema and rubric.
- Use GEPA to optimize domain judgments, tradeoffs, and prioritization.
- Email extraction to a rich object schema:
examples/email-extraction.ts - Message spam classification:
examples/message-spam.ts - Optimize-anything for structured ticket extraction:
examples/optimize-anything-email-extraction.ts - Optimize-anything using a legacy GEPAAdapter bridge (message triage):
examples/optimize-anything-legacy-bridge.ts
Each example:
- Loads
.envlocally (AI Gateway by default), - Prints total iterations, metric calls, cost (USD), and duration (ms),
- Enables persistence to
runs/....
Run:
npm run example # email extraction
npm run example:message-spam # spam classification
npm run example:optimize-anything
npm run example:optimize-anything-legacyTask-specific or private experiments can live under local-examples/, which is gitignored.
optimize() remains the stable, AI SDK-native prompt optimization path.
For generalized optimization (including non-text state and seedless bootstrapping), use optimizeAnything().
import { optimizeAnything } from "@currentai/dsts";
const result = await optimizeAnything<{ system: string }, Task>({
initialState: {
system: "Extract {title, assignee?, priority} as strict JSON.",
},
trainset,
evaluator: async (batch, state, captureTraces) => {
const evalResult = await adapter.evaluate(batch, state, captureTraces);
return {
instanceScores: evalResult.scores,
metrics: evalResult.metrics,
trajectories: evalResult.trajectories,
objectives: {
correctness: avg(evalResult.scores),
latency: avg(evalResult.metrics?.map((m) => m.latency_ms ?? 0) ?? []),
},
metricCalls: batch.length,
};
},
mutator: async (state, context) => {
// mutate prompt text using failed examples in context.evaluation
return { ...state, system: state.system + "\nBe stricter with priority mapping." };
},
objectives: { latency: { direction: "minimize" } },
scalarObjective: "correctness",
maxIterations: 5,
maxMetricCalls: 100,
});
console.log("Best prompt:", result.bestState.system);To reuse an existing GEPAAdapter with optimizeAnything, use fromLegacyAdapter:
import { fromLegacyAdapter, optimizeAnything } from "@currentai/dsts";
// const adapter = new MyGEPAAdapter(...)
// const { evaluator, mutator } = fromLegacyAdapter(adapter)- Default adapter decides generateObject vs generateText based on
schemapresence in each task; collects per‑instance scores, latency_ms, and cost_usd (via tokenlens when usage is available). - GEPA optimizer maintains a candidate archive, runs minibatch reflection, and accepts improving children. It computes per‑candidate metrics:
- correctness = average(score[])
- latency = −avg(latency_ms) (stored negative so higher is better)
- cost is tracked cumulatively and enforced via
maxBudgetUSD.
- Pareto front and 2D hyper‑volume (when exactly two objectives) are logged per iteration and at the end.
Pass the object variant for taskLM to forward extra options directly into AI SDK calls (both generateObject and generateText). All unknown fields are spread through to the adapter and then to the SDK:
optimize({
// ...
taskLM: {
model: "openai/gpt-5-nano",
temperature: 0.2,
providerOptions: { openai: { service_tier: "default" } },
// tools, stopWhen, toolChoice, maxToolRoundtrips, experimentalTelemetry, etc. are also supported
},
})Use reflectionHint to insert a short guidance string at the top of the reflection prompt. Keep it concise:
optimize({
// ...
reflectionHint: "Prioritize exact field correctness; avoid hallucinating properties.",
})Key files:
- Optimizer:
src/gepa.ts - Adapter:
src/adapters/default-adapter.ts(default maxConcurrency = 10) - Pareto utilities:
src/pareto-utils.ts - Types:
src/types.ts - Persistence:
src/persistence.ts
- No custom LLM classes: pass model ids as strings (Gateway format, e.g.,
openai/gpt-5-nano). The adapter uses AI SDK directly. - Minimal knobs: set budgets (
maxMetricCalls,maxBudgetUSD), minibatch size, and selectors. Concurrency defaults to 10. - Multi‑objective by default: we optimise for correctness and latency together; add cost as an explicit objective later if desired.
- AI Gateway by default. Set
AI_GATEWAY_API_KEYin.envor export it in your shell. - If you prefer provider‑direct, swap to
@ai-sdk/openaimodels and pass model objects; the adapter will forward them.
- Extend hyper‑volume and objectives (e.g., cost as a third dimension) with explicit reference points.
- Reflection concurrency (optional) and parent/child evaluation parallelism.