From ae0ff482c90bf1335856efeec4238b503dd2a22c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 22 Aug 2026 22:34:09 +0000 Subject: [PATCH] chore(release): version packages --- .changeset/abort-signal-support.md | 31 -- .changeset/batch-concurrency.md | 28 -- .changeset/evaluate-harness.md | 34 -- .changeset/expand-npm-keywords.md | 10 - .changeset/few-shot-optimizers.md | 26 -- .changeset/module-level-streaming.md | 30 -- .changeset/native-tool-calling.md | 45 --- .changeset/openai-compatible-provider.md | 23 -- .changeset/post-merge-followups.md | 27 -- .changeset/public-testing-utilities.md | 22 -- .changeset/response-caching.md | 34 -- .changeset/strict-json-schema-and-enum.md | 37 -- .changeset/tracing-inspect-history.md | 23 -- .changeset/typed-error-taxonomy.md | 39 -- .changeset/validation-self-repair.md | 29 -- .changeset/vision-multimodal-input.md | 34 -- .changeset/zod-native-signatures.md | 28 -- packages/anthropic/CHANGELOG.md | 173 +++++++++ packages/anthropic/package.json | 4 +- packages/core/CHANGELOG.md | 417 ++++++++++++++++++++++ packages/core/package.json | 2 +- packages/gemini/CHANGELOG.md | 173 +++++++++ packages/gemini/package.json | 4 +- packages/openai/CHANGELOG.md | 193 ++++++++++ packages/openai/package.json | 4 +- 25 files changed, 963 insertions(+), 507 deletions(-) delete mode 100644 .changeset/abort-signal-support.md delete mode 100644 .changeset/batch-concurrency.md delete mode 100644 .changeset/evaluate-harness.md delete mode 100644 .changeset/expand-npm-keywords.md delete mode 100644 .changeset/few-shot-optimizers.md delete mode 100644 .changeset/module-level-streaming.md delete mode 100644 .changeset/native-tool-calling.md delete mode 100644 .changeset/openai-compatible-provider.md delete mode 100644 .changeset/post-merge-followups.md delete mode 100644 .changeset/public-testing-utilities.md delete mode 100644 .changeset/response-caching.md delete mode 100644 .changeset/strict-json-schema-and-enum.md delete mode 100644 .changeset/tracing-inspect-history.md delete mode 100644 .changeset/typed-error-taxonomy.md delete mode 100644 .changeset/validation-self-repair.md delete mode 100644 .changeset/vision-multimodal-input.md delete mode 100644 .changeset/zod-native-signatures.md diff --git a/.changeset/abort-signal-support.md b/.changeset/abort-signal-support.md deleted file mode 100644 index eba667d..0000000 --- a/.changeset/abort-signal-support.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -'@ts-dspy/anthropic': minor -'@ts-dspy/gemini': minor -'@ts-dspy/openai': minor -'@ts-dspy/core': minor ---- - -Add `signal` to `LLMCallOptions` so an in-flight provider call can be cancelled. - -`timeout` bounds how long a call may take, but there was no way to drop one whose -answer nobody is waiting for any more — a React component that unmounted, or a -server request whose client hung up. `LLMCallOptions.signal` takes any -`AbortSignal`; the call rejects as soon as it aborts. OpenAI and Anthropic pass it -straight to their SDK request options. Gemini exposes a single `abortSignal` slot, -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. -`GeminiConfig` now accepts `timeout` and `maxRetries` at construction, and per-call -`retries` is honoured instead of being silently ignored. `@google/genai` reads its -retry policy from client-level options only — a per-call `retries` cannot be -expressed 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`. `maxRetries` -defaults 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 `LMError` and increments `errorCount` on -the OpenAI and Gemini providers, matching Anthropic; previously the raw SDK error -escaped `generateStream`/`chatStream` uncounted. diff --git a/.changeset/batch-concurrency.md b/.changeset/batch-concurrency.md deleted file mode 100644 index adc844d..0000000 --- a/.changeset/batch-concurrency.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -'@ts-dspy/core': minor ---- - -Add `Module.batch()` and a bounded worker pool. - -Every module — `Predict`, `ChainOfThought`, `RespAct` — now inherits -`batch(inputs, options)`, which runs a list of inputs with at most -`concurrency` calls in flight (eight by default). Results come back in -input order regardless of the order the calls finished in, which is the -detail hand-rolled loops get wrong: `Promise.all` over fixed-size slices -stalls 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 a -ten-thousand-row job. `stopOnError: true` rejects the whole batch on the -first failure instead — with the lowest-indexed failure, not whichever one -landed first — `onProgress` fires as inputs settle, and an `AbortSignal` -stops new inputs from starting. Both of those reject rather than returning -the inputs that already finished. A `concurrency` that is not a -positive integer throws `RangeError` rather than hanging forever. Every -other 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 nothing -to do with a module. diff --git a/.changeset/evaluate-harness.md b/.changeset/evaluate-harness.md deleted file mode 100644 index 83c3ffd..0000000 --- a/.changeset/evaluate-harness.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -'@ts-dspy/core': minor ---- - -Add `evaluate`, a harness for measuring a program against a dataset, plus the -built-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 over `Example` records — the class has always -split inputs from outputs via `withInputs()`, which is exactly the split an -evaluation 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`, -`normalizedMatch` for case- and whitespace-insensitive text, `numericMatch` for -a tolerance, `fieldAccuracy` for per-field partial credit on a multi-output -signature, and `tokenF1` for free-text answers — with `matchMetric`, -`fieldAccuracyMetric`, and `tokenF1Metric` as the configurable factories behind -them. A `Metric` is just `(example, prediction) => number | boolean`, so a -metric 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. `formatReport` renders a report as -plain text for the caller to print, since the library itself never writes to a -console. diff --git a/.changeset/expand-npm-keywords.md b/.changeset/expand-npm-keywords.md deleted file mode 100644 index ba6c365..0000000 --- a/.changeset/expand-npm-keywords.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@ts-dspy/anthropic': patch -'@ts-dspy/gemini': patch -'@ts-dspy/openai': patch -'@ts-dspy/core': patch ---- - -Expand npm keywords so the packages surface for the searches people actually -run — `zod`, `json-schema`, `structured-outputs`, `validation`, `type-safe`, -`tool-calling`, and per-provider terms like `gpt`, `claude`, and `gemini-api`. diff --git a/.changeset/few-shot-optimizers.md b/.changeset/few-shot-optimizers.md deleted file mode 100644 index 026358c..0000000 --- a/.changeset/few-shot-optimizers.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -'@ts-dspy/core': minor ---- - -Add few-shot demos and optimizers, so a program can improve itself from data -rather than from prompt edits. - -`Predict` now accepts demos — `new Predict(Sig, { demos })`, or `withDemos()` for -a configured copy — and renders them into the prompt as worked examples before -the real input, in the same labelled `field: value` shape 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. `LabeledFewShot` -selects _k_ of your own labels and makes no model calls at all. `BootstrapFewShot` -runs the module over the trainset, scores each attempt with a metric, and -promotes the runs that passed into demos; a `teacher` option generates them with -a 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. - -`Predict` also gains `withLM()`, and `renderDemos()` is exported for inspecting -the few-shot text a set of demos produces. diff --git a/.changeset/module-level-streaming.md b/.changeset/module-level-streaming.md deleted file mode 100644 index fc4d09a..0000000 --- a/.changeset/module-level-streaming.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -'@ts-dspy/core': minor ---- - -Add module-level streaming: `Predict.stream()` (inherited and extended by -`ChainOfThought`) returns an async generator of progressively-filled output -fields, so a field can be rendered as its tokens arrive. Every provider already -implemented `chatStream`, but nothing above the provider layer exposed it, which -left 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 `Prediction` wrapper. A -stream that ends in something the signature rejects still throws a -`ValidationError`, so streaming does not opt out of the runtime checks. Only the -final snapshot is guaranteed to match the declared types, since coercion belongs -to validation, and snapshots are typed as `PartialOutput` to say so rather -than claiming a field is a `number` while the model is still writing `'0.'`. - -Both of `complete()`'s paths are covered. Providers with native structured -output stream JSON, read by a new dependency-free incremental parser exported as -`parsePartialJson`, which recovers the fields present in a document truncated -mid-string, mid-key, or after a comma without throwing. Everything else streams -labelled text through the existing `parseOutput` heuristics over an accumulating -buffer. - -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 an `AbortSignal`, and abandoning the generator early -closes the underlying provider stream. diff --git a/.changeset/native-tool-calling.md b/.changeset/native-tool-calling.md deleted file mode 100644 index 646f17a..0000000 --- a/.changeset/native-tool-calling.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -'@ts-dspy/anthropic': minor -'@ts-dspy/gemini': minor -'@ts-dspy/openai': minor -'@ts-dspy/core': minor ---- - -Native tool calling, end to end. - -All three providers reported `supportsFunctionCalling: true` while implementing -nothing, and `RespAct` ran ReAct purely by text prompting — regex-extracting -`Action:`/`Action Input:` from raw completions. That capped every tool at exactly -one 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. - -`LLMCallOptions` gains `tools` and `toolChoice`, and `ILanguageModel` gains -`chatWithTools`, which returns text, tool calls, and a normalised finish reason -from one turn. `BaseLM` supplies 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`, Anthropic -`input_schema`/`tool_use`, Gemini `functionDeclarations`/`functionCall`) and -reads the calls back out. - -`RespAct` uses 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 `RespActEvent` surface stays meaningful on both -paths. `forceTextMode` pins a tool-capable model to the text loop. - -**Breaking:** `ToolCall` is reshaped for cross-provider use. It was a copy of -OpenAI's encoding — a required `id`, a `type: 'function'` literal, and a nested -`function.arguments` JSON *string* — which no other provider can populate -faithfully. It is now `{ id?, name, arguments, rawArguments? }`, where -`arguments` is always a parsed object and `id` is optional because Gemini's -function calls have none. The dead `ChatMessage.functionCall` field is removed; -`ChatMessage` gains `toolCallId` to correlate a tool result with its call. - -That correlation also fixes a silent role collapse in all three converters: -`tool` and `function` turns were downgraded to `user` text, and Anthropic could -then merge a tool result into the preceding user turn. Anthropic additionally -dropped `tool_use` blocks on the floor (`textOf` keeps only `text` blocks) and -ignored `input_json_delta` while streaming; both are now surfaced. diff --git a/.changeset/openai-compatible-provider.md b/.changeset/openai-compatible-provider.md deleted file mode 100644 index 7570b93..0000000 --- a/.changeset/openai-compatible-provider.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -'@ts-dspy/openai': minor ---- - -Add `OpenAICompatibleLM`, a first-class provider for the many servers that speak -the OpenAI chat-completions API — Ollama, LM Studio, vLLM, Groq, Together, and -OpenRouter. `OpenAILM` could already be pointed at them through `baseURL`, but -every default it carries is wrong once you leave `api.openai.com`. - -`baseURL` and `model` are now required, since `gpt-5.2` means nothing to Ollama. -The API key defaults to a placeholder for local servers that want an -`Authorization` header but ignore its contents, which removes the confusing auth -failure a user with no `OPENAI_API_KEY` hit before a request was ever sent. Model -capabilities come from config with conservative defaults instead of `OpenAILM`'s -hardcoded optimism: `supportsStructuredOutput` matters most, because `Predict` -branches on it and a wrongly-`true` value makes every call ship a strict -JSON-schema `response_format` that most compatible servers reject outright. The -context window is configurable too, rather than falling through a `gpt-*` prefix -table that never matches `llama-3.3-70b` and silently reports 128k. - -Also exports `OPENAI_COMPATIBLE_BASE_URLS` with the known-good endpoint URLs, and -adds `examples/ollama-local.ts` (`npm run example:ollama`), which runs end to end -with no cloud key. diff --git a/.changeset/post-merge-followups.md b/.changeset/post-merge-followups.md deleted file mode 100644 index a80ca1a..0000000 --- a/.changeset/post-merge-followups.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -'@ts-dspy/anthropic': minor -'@ts-dspy/gemini': minor -'@ts-dspy/openai': minor -'@ts-dspy/core': minor ---- - -Close the gaps left where the 0.6 features met each other. - -Images now reach the model through `Predict` and `ChainOfThought`. A signature -declaring an `image` input 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 in -`maxTokens`, `safetySettings`, `baseURL`, or declared capabilities hashed to the -same cache key, so one could be served a reply the other's configuration would -never have produced. - -`AnthropicRefusalError` is a subclass of `ContentFilterError` rather than an -alias of it. As an alias, `instanceof AnthropicRefusalError` also matched OpenAI -and Gemini content filters; as a subclass, a cross-provider `catch` on -`ContentFilterError` still works and narrowing to Anthropic means Anthropic -again. diff --git a/.changeset/public-testing-utilities.md b/.changeset/public-testing-utilities.md deleted file mode 100644 index 9bc3df9..0000000 --- a/.changeset/public-testing-utilities.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -'@ts-dspy/core': minor ---- - -Publish the testing utilities as `@ts-dspy/core/testing`, and add record/replay -cassettes. - -`MockLM` already existed but was never exported, so every consumer of a library -that sells runtime validation had to hand-roll a fake model before it could test -anything. It now ships under a subpath export, with `import`/`require` -conditions and types for both, and `scripts/verify-packaging.js` imports it from -both module systems the way a real consumer would. - -`MockLM` also gains `chatStream`/`generateStream`, so it no longer advertises -capabilities it lacks; its existing API is unchanged. - -`CassetteLM` is new: point it at a JSON file and it replays recorded provider -replies deterministically, or, given a live model and `mode: 'record'`, captures -them. Cassettes are a plain array of `{ key, request, response }` entries keyed -by 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. diff --git a/.changeset/response-caching.md b/.changeset/response-caching.md deleted file mode 100644 index bd69c45..0000000 --- a/.changeset/response-caching.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -'@ts-dspy/core': minor ---- - -Make `configure({ cache })` real. The flag has been exported and unit-tested -since 0.1, but nothing read it — enabling it bought you nothing. - -`BaseLM` now wraps `generate`, `chat`, and `generateStructured`, so all three -providers inherit caching without a line of provider code. The key is a SHA-256 -hash of the provider, the model (including a per-call `model` override), the -prompt or messages, the sampling parameters — `temperature`, `topP`, -`maxTokens`, `stopSequences`, `frequencyPenalty`, `presencePenalty` — and the -JSON schema on structured calls, with object keys sorted so property order does -not split an entry. Transport options such as `timeout` and `retries` are -excluded, 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. `UsageStats` gains a `cacheHits` -counter, and a hit increments only that — `requestCount` and the token totals -keep reflecting real provider traffic, so a figure multiplied by a published -price stays honest. - -`cache` now accepts an implementation as well as a boolean. `Cache` allows async -`get`/`set`, so a Redis-, SQLite-, or disk-backed store fits without a wrapper, -and the new LRU `MemoryCache` — the default for `cache: true`, with a -configurable `maxSize` — is exported for callers who want to size it themselves. -`getCache()` and `clearCache()` are exported alongside the existing -`isCacheEnabled()`. - -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 })` to -turn it on. diff --git a/.changeset/strict-json-schema-and-enum.md b/.changeset/strict-json-schema-and-enum.md deleted file mode 100644 index 1b6e626..0000000 --- a/.changeset/strict-json-schema-and-enum.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -'@ts-dspy/core': minor ---- - -Emit strict-mode-correct JSON Schema, and add an `enum` field type. - -`buildOutputJsonSchema` used to describe an `object`/`json` field as -`{ type: 'object', additionalProperties: true }` and a bare `array`/`list` field -as `{ type: 'array', items: {} }`. OpenAI's strict structured output rejects -both: it requires `additionalProperties: false` on every object in the document, -nested ones included, and will not accept an empty `items` schema. Any signature -with 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 `object` field is now pinned to `{}` on every -provider with structured output — declare the keys you want as their own -signature fields instead. And a bare `array` now tells the provider its elements -are strings, so a list of figures arrives as `['1', '2']`; declare `number[]` -when the elements have a type worth naming. Only the text path, taken when a -model reports `supportsStructuredOutput: false`, is unchanged. - -The new `enum` field type pins an output to a closed set, so the model cannot -invent 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 `ValidationError` that names the -members. The set is emitted into the provider schema as `enum`, so it constrains -decoding 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 `null` -into its member list so `type: [base, 'null']` and `enum` do not contradict each -other. An enum with no members, or a malformed inline declaration such as -`enum(a|b`, throws rather than degrading to an unconstrained string. diff --git a/.changeset/tracing-inspect-history.md b/.changeset/tracing-inspect-history.md deleted file mode 100644 index 3c01182..0000000 --- a/.changeset/tracing-inspect-history.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -'@ts-dspy/core': minor ---- - -Make `configure({ tracing: true })` do something. Tracing was a flag nothing -read, and `Prediction.trace` was a field nothing populated — so when a signature -misbehaved there was no way to see the prompt that had actually been sent. - -Every module invocation now records a `TraceEntry` while tracing is on: the -prompt, 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 under -`calls`. Failed invocations are recorded too, with the error attached, because a -`ValidationError` is exactly when the prompt matters. - -New `inspectHistory(n?)` returns the last `n` entries from a bounded in-memory -ring buffer — 100 by default, configurable via `traceHistorySize`. New -`clearHistory()` empties it. `configure({ onTrace })` forwards each entry to -Langfuse, 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. diff --git a/.changeset/typed-error-taxonomy.md b/.changeset/typed-error-taxonomy.md deleted file mode 100644 index 02a6a71..0000000 --- a/.changeset/typed-error-taxonomy.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -'@ts-dspy/anthropic': minor -'@ts-dspy/gemini': minor -'@ts-dspy/openai': minor -'@ts-dspy/core': minor ---- - -Add a typed error taxonomy so callers can branch on a class instead of sniffing -HTTP status numbers. - -`RateLimitError`, `AuthError`, `ContextLengthError`, `ContentFilterError` and -`TimeoutError` now join `LMError`, which they all extend — existing `catch (e) { -if (e instanceof LMError) }` handlers keep working unchanged. A shared -`classify(status, { type, code })` helper in core picks the class, and each -provider's `toLMError()` delegates to it with whatever discriminators its SDK -actually supplies: OpenAI's `code` (the only dependable signal for a -context-length overflow), Anthropic's typed `error.type` union, 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 `ContentFilterError` comes from response inspection. -`AnthropicRefusalError` is now a deprecated alias of `ContentFilterError`. It is -an 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 an `instanceof` check under the old name also -matches an OpenAI or Gemini content filter. Test `error.provider` to tell them -apart. - -Three bugs fixed along the way: - -- Gemini's `toLMError()` coerced any `status` with `Number(...)`, producing - `status: NaN` for errors carrying a non-numeric one, such as a Node system - error. -- OpenAI never checked `finish_reason === 'content_filter'`, so a filtered - completion was returned as an empty string with no error at all. -- Gemini never checked `finishReason === 'MAX_TOKENS'`, so a truncated - structured reply fell through to `JSON.parse` and surfaced as a misleading - "not valid JSON" error. It also never checked - `candidates[].finishReason === 'SAFETY'`. diff --git a/.changeset/validation-self-repair.md b/.changeset/validation-self-repair.md deleted file mode 100644 index 0868da9..0000000 --- a/.changeset/validation-self-repair.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -'@ts-dspy/core': minor ---- - -Add opt-in validation self-repair to `Predict` and `ChainOfThought`. - -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 to `0` so existing behaviour is -unchanged. When it is greater than zero, a `ValidationError` triggers a follow-up -prompt 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 `ValidationError` is rethrown, carrying the usual `issues` and `rawOutput`. -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-output -mode and the labelled-text fallback. `ChainOfThought` repairs the answering step -only, reusing the reasoning it already has rather than regenerating it. - -`RespAct` already recovered from a malformed `Final Answer` inside its reasoning -loop. That prompt now comes from the same shared helper as the new `Predict` -path, so there is one repair wording rather than two that can drift apart. The -helper is exported as `buildRepairPrompt`, `buildRepairObservation`, -`describeValidationIssues` and `listFailingFields`. diff --git a/.changeset/vision-multimodal-input.md b/.changeset/vision-multimodal-input.md deleted file mode 100644 index 34181ba..0000000 --- a/.changeset/vision-multimodal-input.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -'@ts-dspy/anthropic': minor -'@ts-dspy/gemini': minor -'@ts-dspy/openai': minor -'@ts-dspy/core': minor ---- - -Send images, not just text. `ChatMessage.content` is widened from `string` to -`string | ContentPart[]`, where a `ContentPart` is either text or an image -carried as an `https://` URL, a `data:` URI, or base64 plus a media type. Plain -strings 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 the -`image` type in a string signature), and the new `buildPromptContent()` renders -such 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_url` parts (only -on user turns, since system and assistant messages accept text alone), -Anthropic `image` blocks with a base64 or URL source, and Gemini `inlineData` or -`fileData`. Anthropic's merging of consecutive same-role turns now concatenates -block 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.content` is a breaking change to a public type — code -that treats it as a `string` without narrowing will need a narrowing step. Per -the pre-1.0 convention this ships as a minor. - -`supportsVision` is reported per model rather than hardcoded to `true`: false -for `gpt-3.5`, `o1-mini` and `o3-mini`, for `claude-3-5-haiku` and older Claude -models, and for Gemini embedding models. diff --git a/.changeset/zod-native-signatures.md b/.changeset/zod-native-signatures.md deleted file mode 100644 index 23c919b..0000000 --- a/.changeset/zod-native-signatures.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -'@ts-dspy/core': minor ---- - -Add zod-native signatures: a third signature form, alongside decorated classes -and the string shorthand, built with the new `signature()` factory from a pair of -`z.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 `TOutput` type -argument 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`, -`ChainOfThought` and `RespAct` infer the result type with no type argument, and -the input keys are typed too. It also needs no `experimentalDecorators`, which -was 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 a `string[]` — with the coercion -applied 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: false` on every -object including nested ones, and optional fields expressed as -`type: [base, 'null']`. - -Decorator and string signatures are unchanged. diff --git a/packages/anthropic/CHANGELOG.md b/packages/anthropic/CHANGELOG.md index aadd285..13e3775 100644 --- a/packages/anthropic/CHANGELOG.md +++ b/packages/anthropic/CHANGELOG.md @@ -1,5 +1,178 @@ # @ts-dspy/anthropic +## 0.6.0 + +### Minor Changes + +- c3f4ee8: Add `signal` to `LLMCallOptions` so an in-flight provider call can be cancelled. + + `timeout` bounds how long a call may take, but there was no way to drop one whose + answer nobody is waiting for any more — a React component that unmounted, or a + server request whose client hung up. `LLMCallOptions.signal` takes any + `AbortSignal`; the call rejects as soon as it aborts. OpenAI and Anthropic pass it + straight to their SDK request options. Gemini exposes a single `abortSignal` slot, + 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. + `GeminiConfig` now accepts `timeout` and `maxRetries` at construction, and per-call + `retries` is honoured instead of being silently ignored. `@google/genai` reads its + retry policy from client-level options only — a per-call `retries` cannot be + expressed 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`. `maxRetries` + defaults 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 `LMError` and increments `errorCount` on + the OpenAI and Gemini providers, matching Anthropic; previously the raw SDK error + escaped `generateStream`/`chatStream` uncounted. + +- c7db627: Native tool calling, end to end. + + All three providers reported `supportsFunctionCalling: true` while implementing + nothing, and `RespAct` ran ReAct purely by text prompting — regex-extracting + `Action:`/`Action Input:` from raw completions. That capped every tool at exactly + one 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. + + `LLMCallOptions` gains `tools` and `toolChoice`, and `ILanguageModel` gains + `chatWithTools`, which returns text, tool calls, and a normalised finish reason + from one turn. `BaseLM` supplies 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`, Anthropic + `input_schema`/`tool_use`, Gemini `functionDeclarations`/`functionCall`) and + reads the calls back out. + + `RespAct` uses 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 `RespActEvent` surface stays meaningful on both + paths. `forceTextMode` pins a tool-capable model to the text loop. + + **Breaking:** `ToolCall` is reshaped for cross-provider use. It was a copy of + OpenAI's encoding — a required `id`, a `type: 'function'` literal, and a nested + `function.arguments` JSON _string_ — which no other provider can populate + faithfully. It is now `{ id?, name, arguments, rawArguments? }`, where + `arguments` is always a parsed object and `id` is optional because Gemini's + function calls have none. The dead `ChatMessage.functionCall` field is removed; + `ChatMessage` gains `toolCallId` to correlate a tool result with its call. + + That correlation also fixes a silent role collapse in all three converters: + `tool` and `function` turns were downgraded to `user` text, and Anthropic could + then merge a tool result into the preceding user turn. Anthropic additionally + dropped `tool_use` blocks on the floor (`textOf` keeps only `text` blocks) and + ignored `input_json_delta` while streaming; both are now surfaced. + +- ae35bd2: Close the gaps left where the 0.6 features met each other. + + Images now reach the model through `Predict` and `ChainOfThought`. A signature + declaring an `image` input 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 in + `maxTokens`, `safetySettings`, `baseURL`, or declared capabilities hashed to the + same cache key, so one could be served a reply the other's configuration would + never have produced. + + `AnthropicRefusalError` is a subclass of `ContentFilterError` rather than an + alias of it. As an alias, `instanceof AnthropicRefusalError` also matched OpenAI + and Gemini content filters; as a subclass, a cross-provider `catch` on + `ContentFilterError` still works and narrowing to Anthropic means Anthropic + again. + +- 9965514: Add a typed error taxonomy so callers can branch on a class instead of sniffing + HTTP status numbers. + + `RateLimitError`, `AuthError`, `ContextLengthError`, `ContentFilterError` and + `TimeoutError` now join `LMError`, which they all extend — existing `catch (e) { +if (e instanceof LMError) }` handlers keep working unchanged. A shared + `classify(status, { type, code })` helper in core picks the class, and each + provider's `toLMError()` delegates to it with whatever discriminators its SDK + actually supplies: OpenAI's `code` (the only dependable signal for a + context-length overflow), Anthropic's typed `error.type` union, 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 `ContentFilterError` comes from response inspection. + `AnthropicRefusalError` is now a deprecated alias of `ContentFilterError`. It is + an 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 an `instanceof` check under the old name also + matches an OpenAI or Gemini content filter. Test `error.provider` to tell them + apart. + + Three bugs fixed along the way: + + - Gemini's `toLMError()` coerced any `status` with `Number(...)`, producing + `status: NaN` for errors carrying a non-numeric one, such as a Node system + error. + - OpenAI never checked `finish_reason === 'content_filter'`, so a filtered + completion was returned as an empty string with no error at all. + - Gemini never checked `finishReason === 'MAX_TOKENS'`, so a truncated + structured reply fell through to `JSON.parse` and surfaced as a misleading + "not valid JSON" error. It also never checked + `candidates[].finishReason === 'SAFETY'`. + +- 628deed: Send images, not just text. `ChatMessage.content` is widened from `string` to + `string | ContentPart[]`, where a `ContentPart` is either text or an image + carried as an `https://` URL, a `data:` URI, or base64 plus a media type. Plain + strings 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 the + `image` type in a string signature), and the new `buildPromptContent()` renders + such 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_url` parts (only + on user turns, since system and assistant messages accept text alone), + Anthropic `image` blocks with a base64 or URL source, and Gemini `inlineData` or + `fileData`. Anthropic's merging of consecutive same-role turns now concatenates + block 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.content` is a breaking change to a public type — code + that treats it as a `string` without narrowing will need a narrowing step. Per + the pre-1.0 convention this ships as a minor. + + `supportsVision` is reported per model rather than hardcoded to `true`: false + for `gpt-3.5`, `o1-mini` and `o3-mini`, for `claude-3-5-haiku` and older Claude + models, and for Gemini embedding models. + +### Patch Changes + +- bb1a1c1: Expand npm keywords so the packages surface for the searches people actually + run — `zod`, `json-schema`, `structured-outputs`, `validation`, `type-safe`, + `tool-calling`, and per-provider terms like `gpt`, `claude`, and `gemini-api`. +- Updated dependencies [c3f4ee8] +- Updated dependencies [95eedf2] +- Updated dependencies [8b13de5] +- Updated dependencies [bb1a1c1] +- Updated dependencies [b13fe66] +- Updated dependencies [0232800] +- Updated dependencies [c7db627] +- Updated dependencies [ae35bd2] +- Updated dependencies [3c950b4] +- Updated dependencies [57e3ad2] +- Updated dependencies [992718d] +- Updated dependencies [fca1917] +- Updated dependencies [9965514] +- Updated dependencies [b3b0df9] +- Updated dependencies [628deed] +- Updated dependencies [e75c7fb] + - @ts-dspy/core@0.6.0 + ## 0.5.0 ### Minor Changes diff --git a/packages/anthropic/package.json b/packages/anthropic/package.json index d4dd8a1..4c68332 100644 --- a/packages/anthropic/package.json +++ b/packages/anthropic/package.json @@ -1,6 +1,6 @@ { "name": "@ts-dspy/anthropic", - "version": "0.5.0", + "version": "0.6.0", "description": "Anthropic Claude provider for TS-DSPy - type-safe LLM interactions with structured outputs, streaming, and tool calling", "type": "module", "sideEffects": false, @@ -35,7 +35,7 @@ }, "dependencies": { "@anthropic-ai/sdk": "^0.116.0", - "@ts-dspy/core": "^0.5.0" + "@ts-dspy/core": "^0.6.0" }, "keywords": [ "ai", diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index 2a3c942..a3bbae6 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,5 +1,422 @@ # @ts-dspy/core +## 0.6.0 + +### Minor Changes + +- c3f4ee8: Add `signal` to `LLMCallOptions` so an in-flight provider call can be cancelled. + + `timeout` bounds how long a call may take, but there was no way to drop one whose + answer nobody is waiting for any more — a React component that unmounted, or a + server request whose client hung up. `LLMCallOptions.signal` takes any + `AbortSignal`; the call rejects as soon as it aborts. OpenAI and Anthropic pass it + straight to their SDK request options. Gemini exposes a single `abortSignal` slot, + 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. + `GeminiConfig` now accepts `timeout` and `maxRetries` at construction, and per-call + `retries` is honoured instead of being silently ignored. `@google/genai` reads its + retry policy from client-level options only — a per-call `retries` cannot be + expressed 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`. `maxRetries` + defaults 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 `LMError` and increments `errorCount` on + the OpenAI and Gemini providers, matching Anthropic; previously the raw SDK error + escaped `generateStream`/`chatStream` uncounted. + +- 95eedf2: Add `Module.batch()` and a bounded worker pool. + + Every module — `Predict`, `ChainOfThought`, `RespAct` — now inherits + `batch(inputs, options)`, which runs a list of inputs with at most + `concurrency` calls in flight (eight by default). Results come back in + input order regardless of the order the calls finished in, which is the + detail hand-rolled loops get wrong: `Promise.all` over fixed-size slices + stalls 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 a + ten-thousand-row job. `stopOnError: true` rejects the whole batch on the + first failure instead — with the lowest-indexed failure, not whichever one + landed first — `onProgress` fires as inputs settle, and an `AbortSignal` + stops new inputs from starting. Both of those reject rather than returning + the inputs that already finished. A `concurrency` that is not a + positive integer throws `RangeError` rather than hanging forever. Every + other 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 nothing + to do with a module. + +- 8b13de5: Add `evaluate`, a harness for measuring a program against a dataset, plus the + built-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 over `Example` records — the class has always + split inputs from outputs via `withInputs()`, which is exactly the split an + evaluation 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`, + `normalizedMatch` for case- and whitespace-insensitive text, `numericMatch` for + a tolerance, `fieldAccuracy` for per-field partial credit on a multi-output + signature, and `tokenF1` for free-text answers — with `matchMetric`, + `fieldAccuracyMetric`, and `tokenF1Metric` as the configurable factories behind + them. A `Metric` is just `(example, prediction) => number | boolean`, so a + metric 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. `formatReport` renders a report as + plain 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. + + `Predict` now accepts demos — `new Predict(Sig, { demos })`, or `withDemos()` for + a configured copy — and renders them into the prompt as worked examples before + the real input, in the same labelled `field: value` shape 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. `LabeledFewShot` + selects _k_ of your own labels and makes no model calls at all. `BootstrapFewShot` + runs the module over the trainset, scores each attempt with a metric, and + promotes the runs that passed into demos; a `teacher` option generates them with + a 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. + + `Predict` also gains `withLM()`, and `renderDemos()` is exported for inspecting + the few-shot text a set of demos produces. + +- 0232800: Add module-level streaming: `Predict.stream()` (inherited and extended by + `ChainOfThought`) returns an async generator of progressively-filled output + fields, so a field can be rendered as its tokens arrive. Every provider already + implemented `chatStream`, but nothing above the provider layer exposed it, which + left 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 `Prediction` wrapper. A + stream that ends in something the signature rejects still throws a + `ValidationError`, so streaming does not opt out of the runtime checks. Only the + final snapshot is guaranteed to match the declared types, since coercion belongs + to validation, and snapshots are typed as `PartialOutput` to say so rather + than claiming a field is a `number` while the model is still writing `'0.'`. + + Both of `complete()`'s paths are covered. Providers with native structured + output stream JSON, read by a new dependency-free incremental parser exported as + `parsePartialJson`, which recovers the fields present in a document truncated + mid-string, mid-key, or after a comma without throwing. Everything else streams + labelled text through the existing `parseOutput` heuristics over an accumulating + buffer. + + 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 an `AbortSignal`, and abandoning the generator early + closes the underlying provider stream. + +- c7db627: Native tool calling, end to end. + + All three providers reported `supportsFunctionCalling: true` while implementing + nothing, and `RespAct` ran ReAct purely by text prompting — regex-extracting + `Action:`/`Action Input:` from raw completions. That capped every tool at exactly + one 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. + + `LLMCallOptions` gains `tools` and `toolChoice`, and `ILanguageModel` gains + `chatWithTools`, which returns text, tool calls, and a normalised finish reason + from one turn. `BaseLM` supplies 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`, Anthropic + `input_schema`/`tool_use`, Gemini `functionDeclarations`/`functionCall`) and + reads the calls back out. + + `RespAct` uses 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 `RespActEvent` surface stays meaningful on both + paths. `forceTextMode` pins a tool-capable model to the text loop. + + **Breaking:** `ToolCall` is reshaped for cross-provider use. It was a copy of + OpenAI's encoding — a required `id`, a `type: 'function'` literal, and a nested + `function.arguments` JSON _string_ — which no other provider can populate + faithfully. It is now `{ id?, name, arguments, rawArguments? }`, where + `arguments` is always a parsed object and `id` is optional because Gemini's + function calls have none. The dead `ChatMessage.functionCall` field is removed; + `ChatMessage` gains `toolCallId` to correlate a tool result with its call. + + That correlation also fixes a silent role collapse in all three converters: + `tool` and `function` turns were downgraded to `user` text, and Anthropic could + then merge a tool result into the preceding user turn. Anthropic additionally + dropped `tool_use` blocks on the floor (`textOf` keeps only `text` blocks) and + ignored `input_json_delta` while streaming; both are now surfaced. + +- ae35bd2: Close the gaps left where the 0.6 features met each other. + + Images now reach the model through `Predict` and `ChainOfThought`. A signature + declaring an `image` input 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 in + `maxTokens`, `safetySettings`, `baseURL`, or declared capabilities hashed to the + same cache key, so one could be served a reply the other's configuration would + never have produced. + + `AnthropicRefusalError` is a subclass of `ContentFilterError` rather than an + alias of it. As an alias, `instanceof AnthropicRefusalError` also matched OpenAI + and Gemini content filters; as a subclass, a cross-provider `catch` on + `ContentFilterError` still works and narrowing to Anthropic means Anthropic + again. + +- 3c950b4: Publish the testing utilities as `@ts-dspy/core/testing`, and add record/replay + cassettes. + + `MockLM` already existed but was never exported, so every consumer of a library + that sells runtime validation had to hand-roll a fake model before it could test + anything. It now ships under a subpath export, with `import`/`require` + conditions and types for both, and `scripts/verify-packaging.js` imports it from + both module systems the way a real consumer would. + + `MockLM` also gains `chatStream`/`generateStream`, so it no longer advertises + capabilities it lacks; its existing API is unchanged. + + `CassetteLM` is new: point it at a JSON file and it replays recorded provider + replies deterministically, or, given a live model and `mode: 'record'`, captures + them. Cassettes are a plain array of `{ key, request, response }` entries keyed + by 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-tested + since 0.1, but nothing read it — enabling it bought you nothing. + + `BaseLM` now wraps `generate`, `chat`, and `generateStructured`, so all three + providers inherit caching without a line of provider code. The key is a SHA-256 + hash of the provider, the model (including a per-call `model` override), the + prompt or messages, the sampling parameters — `temperature`, `topP`, + `maxTokens`, `stopSequences`, `frequencyPenalty`, `presencePenalty` — and the + JSON schema on structured calls, with object keys sorted so property order does + not split an entry. Transport options such as `timeout` and `retries` are + excluded, 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. `UsageStats` gains a `cacheHits` + counter, and a hit increments only that — `requestCount` and the token totals + keep reflecting real provider traffic, so a figure multiplied by a published + price stays honest. + + `cache` now accepts an implementation as well as a boolean. `Cache` allows async + `get`/`set`, so a Redis-, SQLite-, or disk-backed store fits without a wrapper, + and the new LRU `MemoryCache` — the default for `cache: true`, with a + configurable `maxSize` — is exported for callers who want to size it themselves. + `getCache()` and `clearCache()` are exported alongside the existing + `isCacheEnabled()`. + + 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 })` to + turn it on. + +- 992718d: Emit strict-mode-correct JSON Schema, and add an `enum` field type. + + `buildOutputJsonSchema` used to describe an `object`/`json` field as + `{ type: 'object', additionalProperties: true }` and a bare `array`/`list` field + as `{ type: 'array', items: {} }`. OpenAI's strict structured output rejects + both: it requires `additionalProperties: false` on every object in the document, + nested ones included, and will not accept an empty `items` schema. Any signature + with 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 `object` field is now pinned to `{}` on every + provider with structured output — declare the keys you want as their own + signature fields instead. And a bare `array` now tells the provider its elements + are strings, so a list of figures arrives as `['1', '2']`; declare `number[]` + when the elements have a type worth naming. Only the text path, taken when a + model reports `supportsStructuredOutput: false`, is unchanged. + + The new `enum` field type pins an output to a closed set, so the model cannot + invent 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 `ValidationError` that names the + members. The set is emitted into the provider schema as `enum`, so it constrains + decoding 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 `null` + into its member list so `type: [base, 'null']` and `enum` do not contradict each + other. 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 nothing + read, and `Prediction.trace` was a field nothing populated — so when a signature + misbehaved there was no way to see the prompt that had actually been sent. + + Every module invocation now records a `TraceEntry` while tracing is on: the + prompt, 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 under + `calls`. Failed invocations are recorded too, with the error attached, because a + `ValidationError` is exactly when the prompt matters. + + New `inspectHistory(n?)` returns the last `n` entries from a bounded in-memory + ring buffer — 100 by default, configurable via `traceHistorySize`. New + `clearHistory()` empties it. `configure({ onTrace })` forwards each entry to + Langfuse, 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`, `ContentFilterError` and + `TimeoutError` now join `LMError`, which they all extend — existing `catch (e) { +if (e instanceof LMError) }` handlers keep working unchanged. A shared + `classify(status, { type, code })` helper in core picks the class, and each + provider's `toLMError()` delegates to it with whatever discriminators its SDK + actually supplies: OpenAI's `code` (the only dependable signal for a + context-length overflow), Anthropic's typed `error.type` union, 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 `ContentFilterError` comes from response inspection. + `AnthropicRefusalError` is now a deprecated alias of `ContentFilterError`. It is + an 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 an `instanceof` check under the old name also + matches an OpenAI or Gemini content filter. Test `error.provider` to tell them + apart. + + Three bugs fixed along the way: + + - Gemini's `toLMError()` coerced any `status` with `Number(...)`, producing + `status: NaN` for errors carrying a non-numeric one, such as a Node system + error. + - OpenAI never checked `finish_reason === 'content_filter'`, so a filtered + completion was returned as an empty string with no error at all. + - Gemini never checked `finishReason === 'MAX_TOKENS'`, so a truncated + structured reply fell through to `JSON.parse` and surfaced as a misleading + "not valid JSON" error. It also never checked + `candidates[].finishReason === 'SAFETY'`. + +- b3b0df9: Add opt-in validation self-repair to `Predict` and `ChainOfThought`. + + 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 to `0` so existing behaviour is + unchanged. When it is greater than zero, a `ValidationError` triggers a follow-up + prompt 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 `ValidationError` is rethrown, carrying the usual `issues` and `rawOutput`. + 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-output + mode and the labelled-text fallback. `ChainOfThought` repairs the answering step + only, reusing the reasoning it already has rather than regenerating it. + + `RespAct` already recovered from a malformed `Final Answer` inside its reasoning + loop. That prompt now comes from the same shared helper as the new `Predict` + path, so there is one repair wording rather than two that can drift apart. The + helper is exported as `buildRepairPrompt`, `buildRepairObservation`, + `describeValidationIssues` and `listFailingFields`. + +- 628deed: Send images, not just text. `ChatMessage.content` is widened from `string` to + `string | ContentPart[]`, where a `ContentPart` is either text or an image + carried as an `https://` URL, a `data:` URI, or base64 plus a media type. Plain + strings 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 the + `image` type in a string signature), and the new `buildPromptContent()` renders + such 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_url` parts (only + on user turns, since system and assistant messages accept text alone), + Anthropic `image` blocks with a base64 or URL source, and Gemini `inlineData` or + `fileData`. Anthropic's merging of consecutive same-role turns now concatenates + block 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.content` is a breaking change to a public type — code + that treats it as a `string` without narrowing will need a narrowing step. Per + the pre-1.0 convention this ships as a minor. + + `supportsVision` is reported per model rather than hardcoded to `true`: false + for `gpt-3.5`, `o1-mini` and `o3-mini`, for `claude-3-5-haiku` and older Claude + models, 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 of + `z.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 `TOutput` type + argument 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`, + `ChainOfThought` and `RespAct` infer the result type with no type argument, and + the input keys are typed too. It also needs no `experimentalDecorators`, which + was 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 a `string[]` — with the coercion + applied 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: false` on every + object including nested ones, and optional fields expressed as + `type: [base, 'null']`. + + Decorator and string signatures are unchanged. + +### Patch Changes + +- bb1a1c1: Expand npm keywords so the packages surface for the searches people actually + run — `zod`, `json-schema`, `structured-outputs`, `validation`, `type-safe`, + `tool-calling`, and per-provider terms like `gpt`, `claude`, and `gemini-api`. + ## 0.5.0 ### Minor Changes diff --git a/packages/core/package.json b/packages/core/package.json index c269b3e..c199d76 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@ts-dspy/core", - "version": "0.5.0", + "version": "0.6.0", "description": "Core library for building type-safe LLM applications with structured input/output signatures, runtime validation, and reasoning patterns in TypeScript", "type": "module", "sideEffects": false, diff --git a/packages/gemini/CHANGELOG.md b/packages/gemini/CHANGELOG.md index a2dbacc..5cca3f8 100644 --- a/packages/gemini/CHANGELOG.md +++ b/packages/gemini/CHANGELOG.md @@ -1,5 +1,178 @@ # @ts-dspy/gemini +## 0.6.0 + +### Minor Changes + +- c3f4ee8: Add `signal` to `LLMCallOptions` so an in-flight provider call can be cancelled. + + `timeout` bounds how long a call may take, but there was no way to drop one whose + answer nobody is waiting for any more — a React component that unmounted, or a + server request whose client hung up. `LLMCallOptions.signal` takes any + `AbortSignal`; the call rejects as soon as it aborts. OpenAI and Anthropic pass it + straight to their SDK request options. Gemini exposes a single `abortSignal` slot, + 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. + `GeminiConfig` now accepts `timeout` and `maxRetries` at construction, and per-call + `retries` is honoured instead of being silently ignored. `@google/genai` reads its + retry policy from client-level options only — a per-call `retries` cannot be + expressed 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`. `maxRetries` + defaults 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 `LMError` and increments `errorCount` on + the OpenAI and Gemini providers, matching Anthropic; previously the raw SDK error + escaped `generateStream`/`chatStream` uncounted. + +- c7db627: Native tool calling, end to end. + + All three providers reported `supportsFunctionCalling: true` while implementing + nothing, and `RespAct` ran ReAct purely by text prompting — regex-extracting + `Action:`/`Action Input:` from raw completions. That capped every tool at exactly + one 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. + + `LLMCallOptions` gains `tools` and `toolChoice`, and `ILanguageModel` gains + `chatWithTools`, which returns text, tool calls, and a normalised finish reason + from one turn. `BaseLM` supplies 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`, Anthropic + `input_schema`/`tool_use`, Gemini `functionDeclarations`/`functionCall`) and + reads the calls back out. + + `RespAct` uses 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 `RespActEvent` surface stays meaningful on both + paths. `forceTextMode` pins a tool-capable model to the text loop. + + **Breaking:** `ToolCall` is reshaped for cross-provider use. It was a copy of + OpenAI's encoding — a required `id`, a `type: 'function'` literal, and a nested + `function.arguments` JSON _string_ — which no other provider can populate + faithfully. It is now `{ id?, name, arguments, rawArguments? }`, where + `arguments` is always a parsed object and `id` is optional because Gemini's + function calls have none. The dead `ChatMessage.functionCall` field is removed; + `ChatMessage` gains `toolCallId` to correlate a tool result with its call. + + That correlation also fixes a silent role collapse in all three converters: + `tool` and `function` turns were downgraded to `user` text, and Anthropic could + then merge a tool result into the preceding user turn. Anthropic additionally + dropped `tool_use` blocks on the floor (`textOf` keeps only `text` blocks) and + ignored `input_json_delta` while streaming; both are now surfaced. + +- ae35bd2: Close the gaps left where the 0.6 features met each other. + + Images now reach the model through `Predict` and `ChainOfThought`. A signature + declaring an `image` input 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 in + `maxTokens`, `safetySettings`, `baseURL`, or declared capabilities hashed to the + same cache key, so one could be served a reply the other's configuration would + never have produced. + + `AnthropicRefusalError` is a subclass of `ContentFilterError` rather than an + alias of it. As an alias, `instanceof AnthropicRefusalError` also matched OpenAI + and Gemini content filters; as a subclass, a cross-provider `catch` on + `ContentFilterError` still works and narrowing to Anthropic means Anthropic + again. + +- 9965514: Add a typed error taxonomy so callers can branch on a class instead of sniffing + HTTP status numbers. + + `RateLimitError`, `AuthError`, `ContextLengthError`, `ContentFilterError` and + `TimeoutError` now join `LMError`, which they all extend — existing `catch (e) { +if (e instanceof LMError) }` handlers keep working unchanged. A shared + `classify(status, { type, code })` helper in core picks the class, and each + provider's `toLMError()` delegates to it with whatever discriminators its SDK + actually supplies: OpenAI's `code` (the only dependable signal for a + context-length overflow), Anthropic's typed `error.type` union, 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 `ContentFilterError` comes from response inspection. + `AnthropicRefusalError` is now a deprecated alias of `ContentFilterError`. It is + an 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 an `instanceof` check under the old name also + matches an OpenAI or Gemini content filter. Test `error.provider` to tell them + apart. + + Three bugs fixed along the way: + + - Gemini's `toLMError()` coerced any `status` with `Number(...)`, producing + `status: NaN` for errors carrying a non-numeric one, such as a Node system + error. + - OpenAI never checked `finish_reason === 'content_filter'`, so a filtered + completion was returned as an empty string with no error at all. + - Gemini never checked `finishReason === 'MAX_TOKENS'`, so a truncated + structured reply fell through to `JSON.parse` and surfaced as a misleading + "not valid JSON" error. It also never checked + `candidates[].finishReason === 'SAFETY'`. + +- 628deed: Send images, not just text. `ChatMessage.content` is widened from `string` to + `string | ContentPart[]`, where a `ContentPart` is either text or an image + carried as an `https://` URL, a `data:` URI, or base64 plus a media type. Plain + strings 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 the + `image` type in a string signature), and the new `buildPromptContent()` renders + such 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_url` parts (only + on user turns, since system and assistant messages accept text alone), + Anthropic `image` blocks with a base64 or URL source, and Gemini `inlineData` or + `fileData`. Anthropic's merging of consecutive same-role turns now concatenates + block 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.content` is a breaking change to a public type — code + that treats it as a `string` without narrowing will need a narrowing step. Per + the pre-1.0 convention this ships as a minor. + + `supportsVision` is reported per model rather than hardcoded to `true`: false + for `gpt-3.5`, `o1-mini` and `o3-mini`, for `claude-3-5-haiku` and older Claude + models, and for Gemini embedding models. + +### Patch Changes + +- bb1a1c1: Expand npm keywords so the packages surface for the searches people actually + run — `zod`, `json-schema`, `structured-outputs`, `validation`, `type-safe`, + `tool-calling`, and per-provider terms like `gpt`, `claude`, and `gemini-api`. +- Updated dependencies [c3f4ee8] +- Updated dependencies [95eedf2] +- Updated dependencies [8b13de5] +- Updated dependencies [bb1a1c1] +- Updated dependencies [b13fe66] +- Updated dependencies [0232800] +- Updated dependencies [c7db627] +- Updated dependencies [ae35bd2] +- Updated dependencies [3c950b4] +- Updated dependencies [57e3ad2] +- Updated dependencies [992718d] +- Updated dependencies [fca1917] +- Updated dependencies [9965514] +- Updated dependencies [b3b0df9] +- Updated dependencies [628deed] +- Updated dependencies [e75c7fb] + - @ts-dspy/core@0.6.0 + ## 0.5.0 ### Minor Changes diff --git a/packages/gemini/package.json b/packages/gemini/package.json index d1edb87..5c98c85 100644 --- a/packages/gemini/package.json +++ b/packages/gemini/package.json @@ -1,6 +1,6 @@ { "name": "@ts-dspy/gemini", - "version": "0.5.0", + "version": "0.6.0", "description": "Google Gemini provider for TS-DSPy - type-safe LLM interactions with structured outputs, streaming, and tool calling", "type": "module", "sideEffects": false, @@ -35,7 +35,7 @@ }, "dependencies": { "@google/genai": "^1.30.0", - "@ts-dspy/core": "^0.5.0" + "@ts-dspy/core": "^0.6.0" }, "keywords": [ "ai", diff --git a/packages/openai/CHANGELOG.md b/packages/openai/CHANGELOG.md index f977b85..e56c30a 100644 --- a/packages/openai/CHANGELOG.md +++ b/packages/openai/CHANGELOG.md @@ -1,5 +1,198 @@ # @ts-dspy/openai +## 0.6.0 + +### Minor Changes + +- c3f4ee8: Add `signal` to `LLMCallOptions` so an in-flight provider call can be cancelled. + + `timeout` bounds how long a call may take, but there was no way to drop one whose + answer nobody is waiting for any more — a React component that unmounted, or a + server request whose client hung up. `LLMCallOptions.signal` takes any + `AbortSignal`; the call rejects as soon as it aborts. OpenAI and Anthropic pass it + straight to their SDK request options. Gemini exposes a single `abortSignal` slot, + 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. + `GeminiConfig` now accepts `timeout` and `maxRetries` at construction, and per-call + `retries` is honoured instead of being silently ignored. `@google/genai` reads its + retry policy from client-level options only — a per-call `retries` cannot be + expressed 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`. `maxRetries` + defaults 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 `LMError` and increments `errorCount` on + the OpenAI and Gemini providers, matching Anthropic; previously the raw SDK error + escaped `generateStream`/`chatStream` uncounted. + +- c7db627: Native tool calling, end to end. + + All three providers reported `supportsFunctionCalling: true` while implementing + nothing, and `RespAct` ran ReAct purely by text prompting — regex-extracting + `Action:`/`Action Input:` from raw completions. That capped every tool at exactly + one 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. + + `LLMCallOptions` gains `tools` and `toolChoice`, and `ILanguageModel` gains + `chatWithTools`, which returns text, tool calls, and a normalised finish reason + from one turn. `BaseLM` supplies 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`, Anthropic + `input_schema`/`tool_use`, Gemini `functionDeclarations`/`functionCall`) and + reads the calls back out. + + `RespAct` uses 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 `RespActEvent` surface stays meaningful on both + paths. `forceTextMode` pins a tool-capable model to the text loop. + + **Breaking:** `ToolCall` is reshaped for cross-provider use. It was a copy of + OpenAI's encoding — a required `id`, a `type: 'function'` literal, and a nested + `function.arguments` JSON _string_ — which no other provider can populate + faithfully. It is now `{ id?, name, arguments, rawArguments? }`, where + `arguments` is always a parsed object and `id` is optional because Gemini's + function calls have none. The dead `ChatMessage.functionCall` field is removed; + `ChatMessage` gains `toolCallId` to correlate a tool result with its call. + + That correlation also fixes a silent role collapse in all three converters: + `tool` and `function` turns were downgraded to `user` text, and Anthropic could + then merge a tool result into the preceding user turn. Anthropic additionally + dropped `tool_use` blocks on the floor (`textOf` keeps only `text` blocks) and + ignored `input_json_delta` while streaming; both are now surfaced. + +- 66d4b66: Add `OpenAICompatibleLM`, a first-class provider for the many servers that speak + the OpenAI chat-completions API — Ollama, LM Studio, vLLM, Groq, Together, and + OpenRouter. `OpenAILM` could already be pointed at them through `baseURL`, but + every default it carries is wrong once you leave `api.openai.com`. + + `baseURL` and `model` are now required, since `gpt-5.2` means nothing to Ollama. + The API key defaults to a placeholder for local servers that want an + `Authorization` header but ignore its contents, which removes the confusing auth + failure a user with no `OPENAI_API_KEY` hit before a request was ever sent. Model + capabilities come from config with conservative defaults instead of `OpenAILM`'s + hardcoded optimism: `supportsStructuredOutput` matters most, because `Predict` + branches on it and a wrongly-`true` value makes every call ship a strict + JSON-schema `response_format` that most compatible servers reject outright. The + context window is configurable too, rather than falling through a `gpt-*` prefix + table that never matches `llama-3.3-70b` and silently reports 128k. + + Also exports `OPENAI_COMPATIBLE_BASE_URLS` with the known-good endpoint URLs, and + adds `examples/ollama-local.ts` (`npm run example:ollama`), which runs end to end + with no cloud key. + +- ae35bd2: Close the gaps left where the 0.6 features met each other. + + Images now reach the model through `Predict` and `ChainOfThought`. A signature + declaring an `image` input 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 in + `maxTokens`, `safetySettings`, `baseURL`, or declared capabilities hashed to the + same cache key, so one could be served a reply the other's configuration would + never have produced. + + `AnthropicRefusalError` is a subclass of `ContentFilterError` rather than an + alias of it. As an alias, `instanceof AnthropicRefusalError` also matched OpenAI + and Gemini content filters; as a subclass, a cross-provider `catch` on + `ContentFilterError` still works and narrowing to Anthropic means Anthropic + again. + +- 9965514: Add a typed error taxonomy so callers can branch on a class instead of sniffing + HTTP status numbers. + + `RateLimitError`, `AuthError`, `ContextLengthError`, `ContentFilterError` and + `TimeoutError` now join `LMError`, which they all extend — existing `catch (e) { +if (e instanceof LMError) }` handlers keep working unchanged. A shared + `classify(status, { type, code })` helper in core picks the class, and each + provider's `toLMError()` delegates to it with whatever discriminators its SDK + actually supplies: OpenAI's `code` (the only dependable signal for a + context-length overflow), Anthropic's typed `error.type` union, 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 `ContentFilterError` comes from response inspection. + `AnthropicRefusalError` is now a deprecated alias of `ContentFilterError`. It is + an 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 an `instanceof` check under the old name also + matches an OpenAI or Gemini content filter. Test `error.provider` to tell them + apart. + + Three bugs fixed along the way: + + - Gemini's `toLMError()` coerced any `status` with `Number(...)`, producing + `status: NaN` for errors carrying a non-numeric one, such as a Node system + error. + - OpenAI never checked `finish_reason === 'content_filter'`, so a filtered + completion was returned as an empty string with no error at all. + - Gemini never checked `finishReason === 'MAX_TOKENS'`, so a truncated + structured reply fell through to `JSON.parse` and surfaced as a misleading + "not valid JSON" error. It also never checked + `candidates[].finishReason === 'SAFETY'`. + +- 628deed: Send images, not just text. `ChatMessage.content` is widened from `string` to + `string | ContentPart[]`, where a `ContentPart` is either text or an image + carried as an `https://` URL, a `data:` URI, or base64 plus a media type. Plain + strings 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 the + `image` type in a string signature), and the new `buildPromptContent()` renders + such 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_url` parts (only + on user turns, since system and assistant messages accept text alone), + Anthropic `image` blocks with a base64 or URL source, and Gemini `inlineData` or + `fileData`. Anthropic's merging of consecutive same-role turns now concatenates + block 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.content` is a breaking change to a public type — code + that treats it as a `string` without narrowing will need a narrowing step. Per + the pre-1.0 convention this ships as a minor. + + `supportsVision` is reported per model rather than hardcoded to `true`: false + for `gpt-3.5`, `o1-mini` and `o3-mini`, for `claude-3-5-haiku` and older Claude + models, and for Gemini embedding models. + +### Patch Changes + +- bb1a1c1: Expand npm keywords so the packages surface for the searches people actually + run — `zod`, `json-schema`, `structured-outputs`, `validation`, `type-safe`, + `tool-calling`, and per-provider terms like `gpt`, `claude`, and `gemini-api`. +- Updated dependencies [c3f4ee8] +- Updated dependencies [95eedf2] +- Updated dependencies [8b13de5] +- Updated dependencies [bb1a1c1] +- Updated dependencies [b13fe66] +- Updated dependencies [0232800] +- Updated dependencies [c7db627] +- Updated dependencies [ae35bd2] +- Updated dependencies [3c950b4] +- Updated dependencies [57e3ad2] +- Updated dependencies [992718d] +- Updated dependencies [fca1917] +- Updated dependencies [9965514] +- Updated dependencies [b3b0df9] +- Updated dependencies [628deed] +- Updated dependencies [e75c7fb] + - @ts-dspy/core@0.6.0 + ## 0.5.0 ### Minor Changes diff --git a/packages/openai/package.json b/packages/openai/package.json index 3082c40..eeddafd 100644 --- a/packages/openai/package.json +++ b/packages/openai/package.json @@ -1,6 +1,6 @@ { "name": "@ts-dspy/openai", - "version": "0.5.0", + "version": "0.6.0", "description": "OpenAI and OpenAI-compatible (Ollama, Groq, vLLM, OpenRouter) provider for TS-DSPy - type-safe LLM interactions with structured outputs, streaming, and tool calling", "type": "module", "sideEffects": false, @@ -34,7 +34,7 @@ "clean": "rm -rf dist" }, "dependencies": { - "@ts-dspy/core": "^0.5.0", + "@ts-dspy/core": "^0.6.0", "openai": "^6.9.0" }, "keywords": [