chore(release): version packages - #7
Merged
Merged
Conversation
github-actions
Bot
force-pushed
the
changeset-release/main
branch
16 times, most recently
from
August 22, 2026 22:23
e900ff4 to
5f3a4c2
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
from
August 22, 2026 22:34
5f3a4c2 to
ae0ff48
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.
Releases
@ts-dspy/anthropic@0.6.0
Minor Changes
c3f4ee8: Add
signaltoLLMCallOptionsso an in-flight provider call can be cancelled.timeoutbounds how long a call may take, but there was no way to drop one whoseanswer nobody is waiting for any more — a React component that unmounted, or a
server request whose client hung up.
LLMCallOptions.signaltakes anyAbortSignal; the call rejects as soon as it aborts. OpenAI and Anthropic pass itstraight to their SDK request options. Gemini exposes a single
abortSignalslot,so a caller-supplied signal and the timeout signal are combined with
AbortSignal.any(), built freshly per request.The Gemini provider also gains the reliability options the other two already had.
GeminiConfignow acceptstimeoutandmaxRetriesat construction, and per-callretriesis honoured instead of being silently ignored.@google/genaireads itsretry policy from client-level options only — a per-call
retriescannot beexpressed through it, and its wrapper replaces API errors with generic ones and
keeps retrying after an abort — so the provider runs the loop itself: exponential
backoff on 408/409/429/5xx and transport failures, never on an abort or a client
error, with the status code preserved on the resulting
LMError.maxRetriesdefaults to 2, as the other two SDKs do, so a Gemini instance built with no
options now rides out a transient failure the way the others already did.
Aborting mid-stream now rejects with an
LMErrorand incrementserrorCountonthe OpenAI and Gemini providers, matching Anthropic; previously the raw SDK error
escaped
generateStream/chatStreamuncounted.c7db627: Native tool calling, end to end.
All three providers reported
supportsFunctionCalling: truewhile implementingnothing, and
RespActran ReAct purely by text prompting — regex-extractingAction:/Action Input:from raw completions. That capped every tool at exactlyone string argument, ruled out parallel calls, and left the loop at the mercy of
the model formatting its output correctly. The flag is now honest.
LLMCallOptionsgainstoolsandtoolChoice, andILanguageModelgainschatWithTools, which returns text, tool calls, and a normalised finish reasonfrom one turn.
BaseLMsupplies a text-only default, so the capability flag —not feature detection — is what callers branch on. Each provider translates the
declarations into its own request shape (OpenAI
tools/tool_calls, Anthropicinput_schema/tool_use, GeminifunctionDeclarations/functionCall) andreads the calls back out.
RespActuses that path whenever the model supports it and tools are declared,and keeps the text-parsing loop as the fallback for local models and providers
without native tool calling — the same task completes either way. Tools can now
declare a JSON Schema or Zod schema for their arguments and receive a validated
object instead of a single string; bare functions and
{ description, function }keep working unchanged. Parallel tool calls in one turn are executed and reported
individually, and the whole
RespActEventsurface stays meaningful on bothpaths.
forceTextModepins a tool-capable model to the text loop.Breaking:
ToolCallis reshaped for cross-provider use. It was a copy ofOpenAI's encoding — a required
id, atype: 'function'literal, and a nestedfunction.argumentsJSON string — which no other provider can populatefaithfully. It is now
{ id?, name, arguments, rawArguments? }, whereargumentsis always a parsed object andidis optional because Gemini'sfunction calls have none. The dead
ChatMessage.functionCallfield is removed;ChatMessagegainstoolCallIdto correlate a tool result with its call.That correlation also fixes a silent role collapse in all three converters:
toolandfunctionturns were downgraded tousertext, and Anthropic couldthen merge a tool result into the preceding user turn. Anthropic additionally
dropped
tool_useblocks on the floor (textOfkeeps onlytextblocks) andignored
input_json_deltawhile streaming; both are now surfaced.ae35bd2: Close the gaps left where the 0.6 features met each other.
Images now reach the model through
PredictandChainOfThought. A signaturedeclaring an
imageinput previously had it flattened to an[image: …]placeholder before the request was built, so the model never saw the picture;
the prompt now travels as chat content whenever a field is declared
image,and as a plain string otherwise. Structured output over an image asks for the
schema in the prompt, since the provider methods that constrain decoding accept
only a string.
Every provider now overrides
cacheScope(). Two clients differing only inmaxTokens,safetySettings,baseURL, or declared capabilities hashed to thesame cache key, so one could be served a reply the other's configuration would
never have produced.
AnthropicRefusalErroris a subclass ofContentFilterErrorrather than analias of it. As an alias,
instanceof AnthropicRefusalErroralso matched OpenAIand Gemini content filters; as a subclass, a cross-provider
catchonContentFilterErrorstill works and narrowing to Anthropic means Anthropicagain.
9965514: Add a typed error taxonomy so callers can branch on a class instead of sniffing
HTTP status numbers.
RateLimitError,AuthError,ContextLengthError,ContentFilterErrorandTimeoutErrornow joinLMError, which they all extend — existingcatch (e) { if (e instanceof LMError) }handlers keep working unchanged. A sharedclassify(status, { type, code })helper in core picks the class, and eachprovider's
toLMError()delegates to it with whatever discriminators its SDKactually supplies: OpenAI's
code(the only dependable signal for acontext-length overflow), Anthropic's typed
error.typeunion, and, for Gemini,nothing but an HTTP status.
Content filtering is a 200-response condition on all three providers rather than
a thrown SDK error, so
ContentFilterErrorcomes from response inspection.AnthropicRefusalErroris now a deprecated alias ofContentFilterError. It isan alias of that class rather than a subclass of it, so two things change:
constructing one directly now takes
(provider, message, options)instead of(category, explanation), and aninstanceofcheck under the old name alsomatches an OpenAI or Gemini content filter. Test
error.providerto tell themapart.
Three bugs fixed along the way:
toLMError()coerced anystatuswithNumber(...), producingstatus: NaNfor errors carrying a non-numeric one, such as a Node systemerror.
finish_reason === 'content_filter', so a filteredcompletion was returned as an empty string with no error at all.
finishReason === 'MAX_TOKENS', so a truncatedstructured reply fell through to
JSON.parseand surfaced as a misleading"not valid JSON" error. It also never checked
candidates[].finishReason === 'SAFETY'.628deed: Send images, not just text.
ChatMessage.contentis widened fromstringtostring | ContentPart[], where aContentPartis either text or an imagecarried as an
https://URL, adata:URI, or base64 plus a media type. Plainstrings remain valid content and behave exactly as before, so text-only code —
generate(),generateStructured(), and every module — is untouched.Signature inputs can now be declared as images with
@ImageField(or theimagetype in a string signature), and the newbuildPromptContent()renderssuch a signature as content parts, returning a plain string when every input is
text.
buildPrompt()still returns a string, rendering an image input as an[image: image/png]placeholder.Each provider maps parts onto its own SDK shape: OpenAI
image_urlparts (onlyon user turns, since system and assistant messages accept text alone),
Anthropic
imageblocks with a base64 or URL source, and GeminiinlineDataorfileData. Anthropic's merging of consecutive same-role turns now concatenatesblock arrays rather than strings; it previously merged only when both turns were
strings, which silently skipped the merge for image turns and produced two
adjacent user messages that the Messages API rejects.
Widening
ChatMessage.contentis a breaking change to a public type — codethat treats it as a
stringwithout narrowing will need a narrowing step. Perthe pre-1.0 convention this ships as a minor.
supportsVisionis reported per model rather than hardcoded totrue: falsefor
gpt-3.5,o1-miniando3-mini, forclaude-3-5-haikuand older Claudemodels, and for Gemini embedding models.
Patch Changes
run —
zod,json-schema,structured-outputs,validation,type-safe,tool-calling, and per-provider terms likegpt,claude, andgemini-api.@ts-dspy/core@0.6.0
Minor Changes
c3f4ee8: Add
signaltoLLMCallOptionsso an in-flight provider call can be cancelled.timeoutbounds how long a call may take, but there was no way to drop one whoseanswer nobody is waiting for any more — a React component that unmounted, or a
server request whose client hung up.
LLMCallOptions.signaltakes anyAbortSignal; the call rejects as soon as it aborts. OpenAI and Anthropic pass itstraight to their SDK request options. Gemini exposes a single
abortSignalslot,so a caller-supplied signal and the timeout signal are combined with
AbortSignal.any(), built freshly per request.The Gemini provider also gains the reliability options the other two already had.
GeminiConfignow acceptstimeoutandmaxRetriesat construction, and per-callretriesis honoured instead of being silently ignored.@google/genaireads itsretry policy from client-level options only — a per-call
retriescannot beexpressed through it, and its wrapper replaces API errors with generic ones and
keeps retrying after an abort — so the provider runs the loop itself: exponential
backoff on 408/409/429/5xx and transport failures, never on an abort or a client
error, with the status code preserved on the resulting
LMError.maxRetriesdefaults to 2, as the other two SDKs do, so a Gemini instance built with no
options now rides out a transient failure the way the others already did.
Aborting mid-stream now rejects with an
LMErrorand incrementserrorCountonthe OpenAI and Gemini providers, matching Anthropic; previously the raw SDK error
escaped
generateStream/chatStreamuncounted.95eedf2: Add
Module.batch()and a bounded worker pool.Every module —
Predict,ChainOfThought,RespAct— now inheritsbatch(inputs, options), which runs a list of inputs with at mostconcurrencycalls in flight (eight by default). Results come back ininput order regardless of the order the calls finished in, which is the
detail hand-rolled loops get wrong:
Promise.allover fixed-size slicesstalls each slice on its slowest call, and a queue that pushes results as
they settle loses the correspondence between row and answer.
By default a per-input failure is captured rather than thrown, in the shape
of
Promise.allSettled—{ status: 'fulfilled', value }or{ status: 'rejected', reason }— so one bad row does not destroy aten-thousand-row job.
stopOnError: truerejects the whole batch on thefirst failure instead — with the lowest-indexed failure, not whichever one
landed first —
onProgressfires as inputs settle, and anAbortSignalstops new inputs from starting. Both of those reject rather than returning
the inputs that already finished. A
concurrencythat is not apositive integer throws
RangeErrorrather than hanging forever. Everyother option is passed through to each underlying call unchanged.
The pool underneath is exported as
mapWithConcurrency(items, worker, options)with the same guarantees, for rate-limited work that has nothingto do with a module.
8b13de5: Add
evaluate, a harness for measuring a program against a dataset, plus thebuilt-in metrics that grade it.
Until now there was no way to tell whether a signature or prompt change made
things better or worse, which also made optimisation impossible: an optimiser
is only as good as the number it is climbing.
evaluate(program, dataset, metric, options)runs the program overExamplerecords — the class has alwayssplit inputs from outputs via
withInputs(), which is exactly the split anevaluation needs — and returns a report carrying the aggregate score, the
per-example results, and the tokens and latency the run consumed.
Failures are recorded, not thrown: an example whose program or metric throws
comes back as a zero-score result with the error attached, and the run
continues. An evaluation that dies on row 40 of 500 tells you nothing. Examples
run with bounded concurrency, defaulting to four in flight.
Built-in metrics cover the usual grading shapes —
exactMatch,normalizedMatchfor case- and whitespace-insensitive text,numericMatchfora tolerance,
fieldAccuracyfor per-field partial credit on a multi-outputsignature, and
tokenF1for free-text answers — withmatchMetric,fieldAccuracyMetric, andtokenF1Metricas the configurable factories behindthem. A
Metricis just(example, prediction) => number | boolean, so ametric of your own is a one-line function.
Usage is obtained by diffing the language model's own counters around the run,
so it reflects the calls the evaluation made and nothing else. There is still no
cost figure: a built-in price table goes stale, and the last one reported numbers
wrong by more than an order of magnitude.
formatReportrenders a report asplain text for the caller to print, since the library itself never writes to a
console.
b13fe66: Add few-shot demos and optimizers, so a program can improve itself from data
rather than from prompt edits.
Predictnow accepts demos —new Predict(Sig, { demos }), orwithDemos()fora configured copy — and renders them into the prompt as worked examples before
the real input, in the same labelled
field: valueshape the parser reads back.A prompt built without demos is byte-for-byte what it was before.
Two optimizers turn a labelled trainset into those demos.
LabeledFewShotselects k of your own labels and makes no model calls at all.
BootstrapFewShotruns the module over the trainset, scores each attempt with a metric, and
promotes the runs that passed into demos; a
teacheroption generates them witha stronger model that the cheaper student then imitates, so you pay for the
strong model once, at compile time.
Both are deterministic given a seed, so a compiled program can be reproduced and
tested. Trainset runs use bounded concurrency, and an example whose attempt
throws is skipped rather than failing the whole compile. Progress is reported
through an optional callback.
Predictalso gainswithLM(), andrenderDemos()is exported for inspectingthe few-shot text a set of demos produces.
0232800: Add module-level streaming:
Predict.stream()(inherited and extended byChainOfThought) returns an async generator of progressively-filled outputfields, so a field can be rendered as its tokens arrive. Every provider already
implemented
chatStream, but nothing above the provider layer exposed it, whichleft the capability unreachable from a module.
Each yield is a snapshot of the fields parsed so far; the last yield is the
complete output, validated against the signature exactly as
forward()validates it, and the generator's return value is the
Predictionwrapper. Astream that ends in something the signature rejects still throws a
ValidationError, so streaming does not opt out of the runtime checks. Only thefinal snapshot is guaranteed to match the declared types, since coercion belongs
to validation, and snapshots are typed as
PartialOutput<T>to say so ratherthan claiming a field is a
numberwhile the model is still writing'0.'.Both of
complete()'s paths are covered. Providers with native structuredoutput stream JSON, read by a new dependency-free incremental parser exported as
parsePartialJson, which recovers the fields present in a document truncatedmid-string, mid-key, or after a comma without throwing. Everything else streams
labelled text through the existing
parseOutputheuristics over an accumulatingbuffer.
Models that do not support streaming, or that omit the optional
chatStream,fall back to a single non-streaming call yielded once rather than failing.
stream()also accepts anAbortSignal, and abandoning the generator earlycloses the underlying provider stream.
c7db627: Native tool calling, end to end.
All three providers reported
supportsFunctionCalling: truewhile implementingnothing, and
RespActran ReAct purely by text prompting — regex-extractingAction:/Action Input:from raw completions. That capped every tool at exactlyone string argument, ruled out parallel calls, and left the loop at the mercy of
the model formatting its output correctly. The flag is now honest.
LLMCallOptionsgainstoolsandtoolChoice, andILanguageModelgainschatWithTools, which returns text, tool calls, and a normalised finish reasonfrom one turn.
BaseLMsupplies a text-only default, so the capability flag —not feature detection — is what callers branch on. Each provider translates the
declarations into its own request shape (OpenAI
tools/tool_calls, Anthropicinput_schema/tool_use, GeminifunctionDeclarations/functionCall) andreads the calls back out.
RespActuses that path whenever the model supports it and tools are declared,and keeps the text-parsing loop as the fallback for local models and providers
without native tool calling — the same task completes either way. Tools can now
declare a JSON Schema or Zod schema for their arguments and receive a validated
object instead of a single string; bare functions and
{ description, function }keep working unchanged. Parallel tool calls in one turn are executed and reported
individually, and the whole
RespActEventsurface stays meaningful on bothpaths.
forceTextModepins a tool-capable model to the text loop.Breaking:
ToolCallis reshaped for cross-provider use. It was a copy ofOpenAI's encoding — a required
id, atype: 'function'literal, and a nestedfunction.argumentsJSON string — which no other provider can populatefaithfully. It is now
{ id?, name, arguments, rawArguments? }, whereargumentsis always a parsed object andidis optional because Gemini'sfunction calls have none. The dead
ChatMessage.functionCallfield is removed;ChatMessagegainstoolCallIdto correlate a tool result with its call.That correlation also fixes a silent role collapse in all three converters:
toolandfunctionturns were downgraded tousertext, and Anthropic couldthen merge a tool result into the preceding user turn. Anthropic additionally
dropped
tool_useblocks on the floor (textOfkeeps onlytextblocks) andignored
input_json_deltawhile streaming; both are now surfaced.ae35bd2: Close the gaps left where the 0.6 features met each other.
Images now reach the model through
PredictandChainOfThought. A signaturedeclaring an
imageinput previously had it flattened to an[image: …]placeholder before the request was built, so the model never saw the picture;
the prompt now travels as chat content whenever a field is declared
image,and as a plain string otherwise. Structured output over an image asks for the
schema in the prompt, since the provider methods that constrain decoding accept
only a string.
Every provider now overrides
cacheScope(). Two clients differing only inmaxTokens,safetySettings,baseURL, or declared capabilities hashed to thesame cache key, so one could be served a reply the other's configuration would
never have produced.
AnthropicRefusalErroris a subclass ofContentFilterErrorrather than analias of it. As an alias,
instanceof AnthropicRefusalErroralso matched OpenAIand Gemini content filters; as a subclass, a cross-provider
catchonContentFilterErrorstill works and narrowing to Anthropic means Anthropicagain.
3c950b4: Publish the testing utilities as
@ts-dspy/core/testing, and add record/replaycassettes.
MockLMalready existed but was never exported, so every consumer of a librarythat sells runtime validation had to hand-roll a fake model before it could test
anything. It now ships under a subpath export, with
import/requireconditions and types for both, and
scripts/verify-packaging.jsimports it fromboth module systems the way a real consumer would.
MockLMalso gainschatStream/generateStream, so it no longer advertisescapabilities it lacks; its existing API is unchanged.
CassetteLMis new: point it at a JSON file and it replays recorded providerreplies deterministically, or, given a live model and
mode: 'record', capturesthem. Cassettes are a plain array of
{ key, request, response }entries keyedby a hash of the request, so they diff and review like any other fixture. The
intended shape is to record once against a real provider and then run CI forever
with no API key and no flake.
57e3ad2: Make
configure({ cache })real. The flag has been exported and unit-testedsince 0.1, but nothing read it — enabling it bought you nothing.
BaseLMnow wrapsgenerate,chat, andgenerateStructured, so all threeproviders inherit caching without a line of provider code. The key is a SHA-256
hash of the provider, the model (including a per-call
modeloverride), theprompt or messages, the sampling parameters —
temperature,topP,maxTokens,stopSequences,frequencyPenalty,presencePenalty— and theJSON schema on structured calls, with object keys sorted so property order does
not split an entry. Transport options such as
timeoutandretriesareexcluded, because they cannot change the answer. Errors are never cached: a
transient 429 must not pin a failure to a prompt for the life of the process.
Cache hits are kept out of usage accounting.
UsageStatsgains acacheHitscounter, and a hit increments only that —
requestCountand the token totalskeep reflecting real provider traffic, so a figure multiplied by a published
price stays honest.
cachenow accepts an implementation as well as a boolean.Cacheallows asyncget/set, so a Redis-, SQLite-, or disk-backed store fits without a wrapper,and the new LRU
MemoryCache— the default forcache: true, with aconfigurable
maxSize— is exported for callers who want to size it themselves.getCache()andclearCache()are exported alongside the existingisCacheEnabled().BREAKING: caching now defaults to off rather than on. The old default was inert,
so no behaviour regresses, but a process-wide cache that replays answers for
repeated prompts changes what a program does — sampling stops varying, agent
loops stop exploring — so it is opt-in. Call
configure({ cache: true })toturn it on.
992718d: Emit strict-mode-correct JSON Schema, and add an
enumfield type.buildOutputJsonSchemaused to describe anobject/jsonfield as{ type: 'object', additionalProperties: true }and a barearray/listfieldas
{ type: 'array', items: {} }. OpenAI's strict structured output rejectsboth: it requires
additionalProperties: falseon every object in the document,nested ones included, and will not accept an empty
itemsschema. Any signaturewith such a field was therefore refused by the API on the provider path. Objects
now emit a closed, empty object and arrays declare
items: { type: 'string' }.Two consequences worth knowing, both documented on the site. One schema is built
per signature and handed to whichever provider is configured, so these shapes
land everywhere, not only on OpenAI. Strict mode cannot express a free-form
object at all, which means a bare
objectfield is now pinned to{}on everyprovider with structured output — declare the keys you want as their own
signature fields instead. And a bare
arraynow tells the provider its elementsare strings, so a list of figures arrives as
['1', '2']; declarenumber[]when the elements have a type worth naming. Only the text path, taken when a
model reports
supportsStructuredOutput: false, is unchanged.The new
enumfield type pins an output to a closed set, so the model cannotinvent a fourth value that still passes validation. Declare members with
@OutputField({ type: 'enum', values: ['positive', 'negative', 'neutral'] }),or inline in a string signature as
sentiment: enum(positive|negative|neutral)— pipe-separated, because commas already separate fields. Matching trims and
ignores case, in the same lenient spirit as the other coercions, and returns the
declared spelling; anything outside the set is a
ValidationErrorthat names themembers. The set is emitted into the provider schema as
enum, so it constrainsdecoding rather than only the check afterwards, and it is named in the prompt on
the text path, where nothing else could carry it. An optional enum admits
nullinto its member list so
type: [base, 'null']andenumdo not contradict eachother. An enum with no members, or a malformed inline declaration such as
enum(a|b, throws rather than degrading to an unconstrained string.fca1917: Make
configure({ tracing: true })do something. Tracing was a flag nothingread, and
Prediction.tracewas a field nothing populated — so when a signaturemisbehaved there was no way to see the prompt that had actually been sent.
Every module invocation now records a
TraceEntrywhile tracing is on: theprompt, the raw reply, the parsed output, the token usage attributable to that
invocation, its duration, and the module's id. Multi-step modules
(
ChainOfThought,RespAct) record each language-model call individually undercalls. Failed invocations are recorded too, with the error attached, because aValidationErroris exactly when the prompt matters.New
inspectHistory(n?)returns the lastnentries from a bounded in-memoryring buffer — 100 by default, configurable via
traceHistorySize. NewclearHistory()empties it.configure({ onTrace })forwards each entry toLangfuse, OpenTelemetry, or your own logger as it is recorded; a handler that
throws is ignored, so instrumentation cannot fail the run it instruments.
Tracing stays off by default and costs a single boolean check when off — nothing
is timed, copied, or stored.
9965514: Add a typed error taxonomy so callers can branch on a class instead of sniffing
HTTP status numbers.
RateLimitError,AuthError,ContextLengthError,ContentFilterErrorandTimeoutErrornow joinLMError, which they all extend — existingcatch (e) { if (e instanceof LMError) }handlers keep working unchanged. A sharedclassify(status, { type, code })helper in core picks the class, and eachprovider's
toLMError()delegates to it with whatever discriminators its SDKactually supplies: OpenAI's
code(the only dependable signal for acontext-length overflow), Anthropic's typed
error.typeunion, and, for Gemini,nothing but an HTTP status.
Content filtering is a 200-response condition on all three providers rather than
a thrown SDK error, so
ContentFilterErrorcomes from response inspection.AnthropicRefusalErroris now a deprecated alias ofContentFilterError. It isan alias of that class rather than a subclass of it, so two things change:
constructing one directly now takes
(provider, message, options)instead of(category, explanation), and aninstanceofcheck under the old name alsomatches an OpenAI or Gemini content filter. Test
error.providerto tell themapart.
Three bugs fixed along the way:
toLMError()coerced anystatuswithNumber(...), producingstatus: NaNfor errors carrying a non-numeric one, such as a Node systemerror.
finish_reason === 'content_filter', so a filteredcompletion was returned as an empty string with no error at all.
finishReason === 'MAX_TOKENS', so a truncatedstructured reply fell through to
JSON.parseand surfaced as a misleading"not valid JSON" error. It also never checked
candidates[].finishReason === 'SAFETY'.b3b0df9: Add opt-in validation self-repair to
PredictandChainOfThought.Runtime validation is the point of this library, but until now a response that
failed it was simply thrown away. Models frequently produce a nearly-correct
answer — a number written as prose, a required field left off the end — that one
more round-trip would fix.
Call options gain
repairAttempts, defaulting to0so existing behaviour isunchanged. When it is greater than zero, a
ValidationErrortriggers a follow-upprompt naming every failing field with its declared type and the value that
actually arrived, and the result is re-validated. Once the attempts are spent the
last
ValidationErroris rethrown, carrying the usualissuesandrawOutput.The value is capped at 10, and the loop stops early when an attempt reproduces
the previous failure exactly — the next prompt would be byte-identical, so
against a deterministic model the remaining calls cannot do better.
Both of
Predict's paths are covered: the provider's native structured-outputmode and the labelled-text fallback.
ChainOfThoughtrepairs the answering steponly, reusing the reasoning it already has rather than regenerating it.
RespActalready recovered from a malformedFinal Answerinside its reasoningloop. That prompt now comes from the same shared helper as the new
Predictpath, so there is one repair wording rather than two that can drift apart. The
helper is exported as
buildRepairPrompt,buildRepairObservation,describeValidationIssuesandlistFailingFields.628deed: Send images, not just text.
ChatMessage.contentis widened fromstringtostring | ContentPart[], where aContentPartis either text or an imagecarried as an
https://URL, adata:URI, or base64 plus a media type. Plainstrings remain valid content and behave exactly as before, so text-only code —
generate(),generateStructured(), and every module — is untouched.Signature inputs can now be declared as images with
@ImageField(or theimagetype in a string signature), and the newbuildPromptContent()renderssuch a signature as content parts, returning a plain string when every input is
text.
buildPrompt()still returns a string, rendering an image input as an[image: image/png]placeholder.Each provider maps parts onto its own SDK shape: OpenAI
image_urlparts (onlyon user turns, since system and assistant messages accept text alone),
Anthropic
imageblocks with a base64 or URL source, and GeminiinlineDataorfileData. Anthropic's merging of consecutive same-role turns now concatenatesblock arrays rather than strings; it previously merged only when both turns were
strings, which silently skipped the merge for image turns and produced two
adjacent user messages that the Messages API rejects.
Widening
ChatMessage.contentis a breaking change to a public type — codethat treats it as a
stringwithout narrowing will need a narrowing step. Perthe pre-1.0 convention this ships as a minor.
supportsVisionis reported per model rather than hardcoded totrue: falsefor
gpt-3.5,o1-miniando3-mini, forclaude-3-5-haikuand older Claudemodels, and for Gemini embedding models.
e75c7fb: Add zod-native signatures: a third signature form, alongside decorated classes
and the string shorthand, built with the new
signature()factory from a pair ofz.object()schemas.Decorators record fields at runtime, so TypeScript could never infer per-field
types from a signature class — callers had to hand-write a
TOutputtypeargument to get real types back, and the decorator field-type list had no
spelling for an enum, a union, a nested object, or a numeric bound. A zod
signature carries its shape in the type system instead, so
Predict,ChainOfThoughtandRespActinfer the result type with no type argument, andthe input keys are typed too. It also needs no
experimentalDecorators, whichwas a real adoption barrier for projects that cannot enable it.
The caller's zod schema is used verbatim as the validator, so every constraint
they express is enforced. Text responses are still coerced leniently —
"42"satisfies a number field,
"a, b"satisfies astring[]— with the coercionapplied at the object level so optionality, defaults and object-level
refinements survive. On the provider structured-output path the schema is
converted with
z.toJSONSchema()and then rewritten for OpenAI strict mode:every property listed in
required,additionalProperties: falseon everyobject including nested ones, and optional fields expressed as
type: [base, 'null'].Decorator and string signatures are unchanged.
Patch Changes
run —
zod,json-schema,structured-outputs,validation,type-safe,tool-calling, and per-provider terms likegpt,claude, andgemini-api.@ts-dspy/gemini@0.6.0
Minor Changes
c3f4ee8: Add
signaltoLLMCallOptionsso an in-flight provider call can be cancelled.timeoutbounds how long a call may take, but there was no way to drop one whoseanswer nobody is waiting for any more — a React component that unmounted, or a
server request whose client hung up.
LLMCallOptions.signaltakes anyAbortSignal; the call rejects as soon as it aborts. OpenAI and Anthropic pass itstraight to their SDK request options. Gemini exposes a single
abortSignalslot,so a caller-supplied signal and the timeout signal are combined with
AbortSignal.any(), built freshly per request.The Gemini provider also gains the reliability options the other two already had.
GeminiConfignow acceptstimeoutandmaxRetriesat construction, and per-callretriesis honoured instead of being silently ignored.@google/genaireads itsretry policy from client-level options only — a per-call
retriescannot beexpressed through it, and its wrapper replaces API errors with generic ones and
keeps retrying after an abort — so the provider runs the loop itself: exponential
backoff on 408/409/429/5xx and transport failures, never on an abort or a client
error, with the status code preserved on the resulting
LMError.maxRetriesdefaults to 2, as the other two SDKs do, so a Gemini instance built with no
options now rides out a transient failure the way the others already did.
Aborting mid-stream now rejects with an
LMErrorand incrementserrorCountonthe OpenAI and Gemini providers, matching Anthropic; previously the raw SDK error
escaped
generateStream/chatStreamuncounted.c7db627: Native tool calling, end to end.
All three providers reported
supportsFunctionCalling: truewhile implementingnothing, and
RespActran ReAct purely by text prompting — regex-extractingAction:/Action Input:from raw completions. That capped every tool at exactlyone string argument, ruled out parallel calls, and left the loop at the mercy of
the model formatting its output correctly. The flag is now honest.
LLMCallOptionsgainstoolsandtoolChoice, andILanguageModelgainschatWithTools, which returns text, tool calls, and a normalised finish reasonfrom one turn.
BaseLMsupplies a text-only default, so the capability flag —not feature detection — is what callers branch on. Each provider translates the
declarations into its own request shape (OpenAI
tools/tool_calls, Anthropicinput_schema/tool_use, GeminifunctionDeclarations/functionCall) andreads the calls back out.
RespActuses that path whenever the model supports it and tools are declared,and keeps the text-parsing loop as the fallback for local models and providers
without native tool calling — the same task completes either way. Tools can now
declare a JSON Schema or Zod schema for their arguments and receive a validated
object instead of a single string; bare functions and
{ description, function }keep working unchanged. Parallel tool calls in one turn are executed and reported
individually, and the whole
RespActEventsurface stays meaningful on bothpaths.
forceTextModepins a tool-capable model to the text loop.Breaking:
ToolCallis reshaped for cross-provider use. It was a copy ofOpenAI's encoding — a required
id, atype: 'function'literal, and a nestedfunction.argumentsJSON string — which no other provider can populatefaithfully. It is now
{ id?, name, arguments, rawArguments? }, whereargumentsis always a parsed object andidis optional because Gemini'sfunction calls have none. The dead
ChatMessage.functionCallfield is removed;ChatMessagegainstoolCallIdto correlate a tool result with its call.That correlation also fixes a silent role collapse in all three converters:
toolandfunctionturns were downgraded tousertext, and Anthropic couldthen merge a tool result into the preceding user turn. Anthropic additionally
dropped
tool_useblocks on the floor (textOfkeeps onlytextblocks) andignored
input_json_deltawhile streaming; both are now surfaced.ae35bd2: Close the gaps left where the 0.6 features met each other.
Images now reach the model through
PredictandChainOfThought. A signaturedeclaring an
imageinput previously had it flattened to an[image: …]placeholder before the request was built, so the model never saw the picture;
the prompt now travels as chat content whenever a field is declared
image,and as a plain string otherwise. Structured output over an image asks for the
schema in the prompt, since the provider methods that constrain decoding accept
only a string.
Every provider now overrides
cacheScope(). Two clients differing only inmaxTokens,safetySettings,baseURL, or declared capabilities hashed to thesame cache key, so one could be served a reply the other's configuration would
never have produced.
AnthropicRefusalErroris a subclass ofContentFilterErrorrather than analias of it. As an alias,
instanceof AnthropicRefusalErroralso matched OpenAIand Gemini content filters; as a subclass, a cross-provider
catchonContentFilterErrorstill works and narrowing to Anthropic means Anthropicagain.
9965514: Add a typed error taxonomy so callers can branch on a class instead of sniffing
HTTP status numbers.
RateLimitError,AuthError,ContextLengthError,ContentFilterErrorandTimeoutErrornow joinLMError, which they all extend — existingcatch (e) { if (e instanceof LMError) }handlers keep working unchanged. A sharedclassify(status, { type, code })helper in core picks the class, and eachprovider's
toLMError()delegates to it with whatever discriminators its SDKactually supplies: OpenAI's
code(the only dependable signal for acontext-length overflow), Anthropic's typed
error.typeunion, and, for Gemini,nothing but an HTTP status.
Content filtering is a 200-response condition on all three providers rather than
a thrown SDK error, so
ContentFilterErrorcomes from response inspection.AnthropicRefusalErroris now a deprecated alias ofContentFilterError. It isan alias of that class rather than a subclass of it, so two things change:
constructing one directly now takes
(provider, message, options)instead of(category, explanation), and aninstanceofcheck under the old name alsomatches an OpenAI or Gemini content filter. Test
error.providerto tell themapart.
Three bugs fixed along the way:
toLMError()coerced anystatuswithNumber(...), producingstatus: NaNfor errors carrying a non-numeric one, such as a Node systemerror.
finish_reason === 'content_filter', so a filteredcompletion was returned as an empty string with no error at all.
finishReason === 'MAX_TOKENS', so a truncatedstructured reply fell through to
JSON.parseand surfaced as a misleading"not valid JSON" error. It also never checked
candidates[].finishReason === 'SAFETY'.628deed: Send images, not just text.
ChatMessage.contentis widened fromstringtostring | ContentPart[], where aContentPartis either text or an imagecarried as an
https://URL, adata:URI, or base64 plus a media type. Plainstrings remain valid content and behave exactly as before, so text-only code —
generate(),generateStructured(), and every module — is untouched.Signature inputs can now be declared as images with
@ImageField(or theimagetype in a string signature), and the newbuildPromptContent()renderssuch a signature as content parts, returning a plain string when every input is
text.
buildPrompt()still returns a string, rendering an image input as an[image: image/png]placeholder.Each provider maps parts onto its own SDK shape: OpenAI
image_urlparts (onlyon user turns, since system and assistant messages accept text alone),
Anthropic
imageblocks with a base64 or URL source, and GeminiinlineDataorfileData. Anthropic's merging of consecutive same-role turns now concatenatesblock arrays rather than strings; it previously merged only when both turns were
strings, which silently skipped the merge for image turns and produced two
adjacent user messages that the Messages API rejects.
Widening
ChatMessage.contentis a breaking change to a public type — codethat treats it as a
stringwithout narrowing will need a narrowing step. Perthe pre-1.0 convention this ships as a minor.
supportsVisionis reported per model rather than hardcoded totrue: falsefor
gpt-3.5,o1-miniando3-mini, forclaude-3-5-haikuand older Claudemodels, and for Gemini embedding models.
Patch Changes
run —
zod,json-schema,structured-outputs,validation,type-safe,tool-calling, and per-provider terms likegpt,claude, andgemini-api.@ts-dspy/openai@0.6.0
Minor Changes
c3f4ee8: Add
signaltoLLMCallOptionsso an in-flight provider call can be cancelled.timeoutbounds how long a call may take, but there was no way to drop one whoseanswer nobody is waiting for any more — a React component that unmounted, or a
server request whose client hung up.
LLMCallOptions.signaltakes anyAbortSignal; the call rejects as soon as it aborts. OpenAI and Anthropic pass itstraight to their SDK request options. Gemini exposes a single
abortSignalslot,so a caller-supplied signal and the timeout signal are combined with
AbortSignal.any(), built freshly per request.The Gemini provider also gains the reliability options the other two already had.
GeminiConfignow acceptstimeoutandmaxRetriesat construction, and per-callretriesis honoured instead of being silently ignored.@google/genaireads itsretry policy from client-level options only — a per-call
retriescannot beexpressed through it, and its wrapper replaces API errors with generic ones and
keeps retrying after an abort — so the provider runs the loop itself: exponential
backoff on 408/409/429/5xx and transport failures, never on an abort or a client
error, with the status code preserved on the resulting
LMError.maxRetriesdefaults to 2, as the other two SDKs do, so a Gemini instance built with no
options now rides out a transient failure the way the others already did.
Aborting mid-stream now rejects with an
LMErrorand incrementserrorCountonthe OpenAI and Gemini providers, matching Anthropic; previously the raw SDK error
escaped
generateStream/chatStreamuncounted.c7db627: Native tool calling, end to end.
All three providers reported
supportsFunctionCalling: truewhile implementingnothing, and
RespActran ReAct purely by text prompting — regex-extractingAction:/Action Input:from raw completions. That capped every tool at exactlyone string argument, ruled out parallel calls, and left the loop at the mercy of
the model formatting its output correctly. The flag is now honest.
LLMCallOptionsgainstoolsandtoolChoice, andILanguageModelgainschatWithTools, which returns text, tool calls, and a normalised finish reasonfrom one turn.
BaseLMsupplies a text-only default, so the capability flag —not feature detection — is what callers branch on. Each provider translates the
declarations into its own request shape (OpenAI
tools/tool_calls, Anthropicinput_schema/tool_use, GeminifunctionDeclarations/functionCall) andreads the calls back out.
RespActuses that path whenever the model supports it and tools are declared,and keeps the text-parsing loop as the fallback for local models and providers
without native tool calling — the same task completes either way. Tools can now
declare a JSON Schema or Zod schema for their arguments and receive a validated
object instead of a single string; bare functions and
{ description, function }keep working unchanged. Parallel tool calls in one turn are executed and reported
individually, and the whole
RespActEventsurface stays meaningful on bothpaths.
forceTextModepins a tool-capable model to the text loop.Breaking:
ToolCallis reshaped for cross-provider use. It was a copy ofOpenAI's encoding — a required
id, atype: 'function'literal, and a nestedfunction.argumentsJSON string — which no other provider can populatefaithfully. It is now
{ id?, name, arguments, rawArguments? }, whereargumentsis always a parsed object andidis optional because Gemini'sfunction calls have none. The dead
ChatMessage.functionCallfield is removed;ChatMessagegainstoolCallIdto correlate a tool result with its call.That correlation also fixes a silent role collapse in all three converters:
toolandfunctionturns were downgraded tousertext, and Anthropic couldthen merge a tool result into the preceding user turn. Anthropic additionally
dropped
tool_useblocks on the floor (textOfkeeps onlytextblocks) andignored
input_json_deltawhile streaming; both are now surfaced.66d4b66: Add
OpenAICompatibleLM, a first-class provider for the many servers that speakthe OpenAI chat-completions API — Ollama, LM Studio, vLLM, Groq, Together, and
OpenRouter.
OpenAILMcould already be pointed at them throughbaseURL, butevery default it carries is wrong once you leave
api.openai.com.baseURLandmodelare now required, sincegpt-5.2means nothing to Ollama.The API key defaults to a placeholder for local servers that want an
Authorizationheader but ignore its contents, which removes the confusing authfailure a user with no
OPENAI_API_KEYhit before a request was ever sent. Modelcapabilities come from config with conservative defaults instead of
OpenAILM'shardcoded optimism:
supportsStructuredOutputmatters most, becausePredictbranches on it and a wrongly-
truevalue makes every call ship a strictJSON-schema
response_formatthat most compatible servers reject outright. Thecontext window is configurable too, rather than falling through a
gpt-*prefixtable that never matches
llama-3.3-70band silently reports 128k.Also exports
OPENAI_COMPATIBLE_BASE_URLSwith the known-good endpoint URLs, andadds
examples/ollama-local.ts(npm run example:ollama), which runs end to endwith no cloud key.
ae35bd2: Close the gaps left where the 0.6 features met each other.
Images now reach the model through
PredictandChainOfThought. A signaturedeclaring an
imageinput previously had it flattened to an[image: …]placeholder before the request was built, so the model never saw the picture;
the prompt now travels as chat content whenever a field is declared
image,and as a plain string otherwise. Structured output over an image asks for the
schema in the prompt, since the provider methods that constrain decoding accept
only a string.
Every provider now overrides
cacheScope(). Two clients differing only inmaxTokens,safetySettings,baseURL, or declared capabilities hashed to thesame cache key, so one could be served a reply the other's configuration would
never have produced.
AnthropicRefusalErroris a subclass ofContentFilterErrorrather than analias of it. As an alias,
instanceof AnthropicRefusalErroralso matched OpenAIand Gemini content filters; as a subclass, a cross-provider
catchonContentFilterErrorstill works and narrowing to Anthropic means Anthropicagain.
9965514: Add a typed error taxonomy so callers can branch on a class instead of sniffing
HTTP status numbers.
RateLimitError,AuthError,ContextLengthError,ContentFilterErrorandTimeoutErrornow joinLMError, which they all extend — existingcatch (e) { if (e instanceof LMError) }handlers keep working unchanged. A sharedclassify(status, { type, code })helper in core picks the class, and eachprovider's
toLMError()delegates to it with whatever discriminators its SDKactually supplies: OpenAI's
code(the only dependable signal for acontext-length overflow), Anthropic's typed
error.typeunion, and, for Gemini,nothing but an HTTP status.
Content filtering is a 200-response condition on all three providers rather than
a thrown SDK error, so
ContentFilterErrorcomes from response inspection.AnthropicRefusalErroris now a deprecated alias ofContentFilterError. It isan alias of that class rather than a subclass of it, so two things change:
constructing one directly now takes
(provider, message, options)instead of(category, explanation), and aninstanceofcheck under the old name alsomatches an OpenAI or Gemini content filter. Test
error.providerto tell themapart.
Three bugs fixed along the way:
toLMError()coerced anystatuswithNumber(...), producingstatus: NaNfor errors carrying a non-numeric one, such as a Node systemerror.
finish_reason === 'content_filter', so a filteredcompletion was returned as an empty string with no error at all.
finishReason === 'MAX_TOKENS', so a truncatedstructured reply fell through to
JSON.parseand surfaced as a misleading"not valid JSON" error. It also never checked
candidates[].finishReason === 'SAFETY'.628deed: Send images, not just text.
ChatMessage.contentis widened fromstringtostring | ContentPart[], where aContentPartis either text or an imagecarried as an
https://URL, adata:URI, or base64 plus a media type. Plainstrings remain valid content and behave exactly as before, so text-only code —
generate(),generateStructured(), and every module — is untouched.Signature inputs can now be declared as images with
@ImageField(or theimagetype in a string signature), and the newbuildPromptContent()renderssuch a signature as content parts, returning a plain string when every input is
text.
buildPrompt()still returns a string, rendering an image input as an[image: image/png]placeholder.Each provider maps parts onto its own SDK shape: OpenAI
image_urlparts (onlyon user turns, since system and assistant messages accept text alone),
Anthropic
imageblocks with a base64 or URL source, and GeminiinlineDataorfileData. Anthropic's merging of consecutive same-role turns now concatenatesblock arrays rather than strings; it previously merged only when both turns were
strings, which silently skipped the merge for image turns and produced two
adjacent user messages that the Messages API rejects.
Widening
ChatMessage.contentis a breaking change to a public type — codethat treats it as a
stringwithout narrowing will need a narrowing step. Perthe pre-1.0 convention this ships as a minor.
supportsVisionis reported per model rather than hardcoded totrue: falsefor
gpt-3.5,o1-miniando3-mini, forclaude-3-5-haikuand older Claudemodels, and for Gemini embedding models.
Patch Changes
run —
zod,json-schema,structured-outputs,validation,type-safe,tool-calling, and per-provider terms likegpt,claude, andgemini-api.