combined-ai 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,123 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project aims to follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2026-06-17
11
+
12
+ Initial release: a plain TypeScript library that unifies the Anthropic, OpenAI,
13
+ and Google (Gemini) APIs behind one provider-agnostic contract, talking to each
14
+ HTTP API directly over `fetch` (no SDK dependencies), and adds strategies for
15
+ combining several providers on one prompt.
16
+
17
+ ### Added
18
+
19
+ - **Provider-agnostic contract** (`src/types.ts`): `Provider`, `Message`, `Role`,
20
+ `CompletionRequest`, `CompletionResult`. Every provider implements `complete()`
21
+ (full text) and `stream()` (text deltas via SSE).
22
+ - **`ProviderRegistry`** — the package's single point of access, configured with
23
+ `{ anthropic?, openai?, google?, custom? }`. `select(name)` returns a provider
24
+ or throws (listing configured names); plus `has(name)` and `names()`. Concrete
25
+ provider classes are intentionally not exported. The library never reads
26
+ environment variables — keys come from config.
27
+ - **Anthropic, OpenAI, and Google (Gemini) providers.** Anthropic Messages API
28
+ (default `claude-opus-4-8`); OpenAI Chat Completions (default `gpt-4.1`, folds
29
+ `system` into a leading message, cap sent as `max_completion_tokens`); Gemini
30
+ Generative Language API (default `gemini-2.5-pro`, `assistant`→`model`,
31
+ `system`→`systemInstruction`, cap→`maxOutputTokens`). The `google` key is the
32
+ company name (consistent with `anthropic`/`openai`); the API it speaks is Gemini.
33
+ Note: Gemini 2.5 thinking tokens count against `maxTokens`, so a very small cap
34
+ can leave the visible answer empty/truncated.
35
+ - **Custom & gateway providers** via a `custom` map on the registry config. Two
36
+ forms — `{ kind: "openai-compatible", apiKey, baseUrl, model, headers?, retry? }`
37
+ points the OpenAI provider at any Chat Completions–compatible endpoint
38
+ (OpenRouter, Together, Groq, Ollama, a local server, …), and
39
+ `{ kind: "provider", provider }` brings your own `Provider`. Custom providers
40
+ work everywhere a built-in does; a name colliding with a built-in throws. A
41
+ gateway's errors and `provider.name` carry its alias. `ProviderName` accepts any
42
+ custom string while keeping autocomplete for the built-ins. Exported:
43
+ `CustomProviderConfig`, `CustomProviderInstance`, `OpenAICompatibleConfig`,
44
+ `BuiltInProviderName`. `OpenAIProviderOptions.headers?` adds extra headers
45
+ merged into (and able to override) every request.
46
+ - **Combine: cooperate across providers on one prompt** via
47
+ `ProviderRegistry.combine(request)` or the per-strategy methods `consensus()`,
48
+ `pipeline()`, `ensemble()`, and `broadcast()`. Per-strategy methods take that
49
+ strategy's request type (`ConsensusRequest`, `PipelineRequest`, `EnsembleRequest`,
50
+ `BroadcastRequest`) and return its concrete result; `combine()` is generic over
51
+ `strategy`, returning the concrete result for a literal strategy and the
52
+ `CombineResult` union (discriminated on `strategy`) for a dynamic one. Four
53
+ strategies:
54
+ - **consensus** — every participant drafts in parallel, critiques all drafts
55
+ (anonymized by default; `attribution: "attributed"` opts out), then a
56
+ synthesizer adjudicates on correctness over popularity; a final sanitizing
57
+ pass strips process narration. Tolerates partial failure down to
58
+ `minParticipants` (default 2); a single participant degrades to a plain
59
+ completion.
60
+ - **pipeline** — participants refine one answer in sequence (participant order =
61
+ conveyor order); a failed/empty stage carries the previous answer forward.
62
+ - **ensemble** — every participant answers under the same `responseFormat`, then
63
+ the objects are merged **mechanically** by field-wise majority vote (no LLM
64
+ synthesis) with per-field and overall `agreement` scores. `responseFormat` is
65
+ required (object root); rejected for consensus/pipeline/broadcast.
66
+ - **broadcast** — fan the prompt out to every participant and return all raw
67
+ answers, with no critique/synthesis/vote; `BroadcastResult` has no `text`.
68
+ - A participant is a provider name or `{ provider, model?, maxTokens?, label? }`,
69
+ so one combine can mix models or run the same provider twice; each gets a
70
+ unique `id` (results/events/usage are keyed by it). An optional `CombineOptions`
71
+ `onEvent` callback reports phase/per-participant progress (status only).
72
+ When every participant fails, the thrown error is an `AggregateError` carrying
73
+ the participants' own errors. Tools/`toolChoice` are not supported in combine.
74
+ Exported: `CombineRequest`, `CombineResult`, `ConsensusResult`,
75
+ `PipelineResult`, `EnsembleResult`, `EnsembleAgreement`, `BroadcastResult`,
76
+ `ParticipantSpec`, `ParticipantOutcome`, `StrategyName`, `CombineEvent`,
77
+ `StrategyRequest<S>`, `ResultFor<S>`, `CombineRequestBase`.
78
+ - **Structured message content**: `Message.content` is `string | ContentPart[]`
79
+ (a bare string is shorthand for one text part).
80
+ - **Multimodal input**: `ContentPart` is `TextPart | ImagePart | FilePart` (plus
81
+ `MediaSource`). Pass images and documents/PDFs alongside text, with `source` as
82
+ base64 bytes or a URL, mapped to each provider's wire shape. Provider support
83
+ varies — OpenAI's Chat Completions has no URL file source and throws a clear
84
+ error.
85
+ - **Structured output**: `CompletionRequest.responseFormat?` (`{ type: "json_schema",
86
+ schema, name? }`) constrains output to a raw JSON Schema (no Zod), mapped to each
87
+ provider's native mechanism. `complete()` also surfaces the parsed value on
88
+ `CompletionResult.parsed`. For one schema to work across all three, set
89
+ `additionalProperties: false` and list every property in `required`.
90
+ - **Tool / function calling** (single-provider, `complete()` only). Pass `tools`
91
+ and an optional `toolChoice` (`"auto" | "any" | "none" | { name }`); a tool call
92
+ is returned on `toolCalls` with `finishReason: "tool_use"`. Replay results with
93
+ `ToolUsePart`/`ToolResultPart` content parts, mapped to each provider's wire
94
+ shape. Exported: `ToolDefinition`, `ToolChoice`, `ToolCall`, `ToolUsePart`,
95
+ `ToolResultPart`.
96
+ - **Normalized finish reasons and refusals**: `finishReason` (a `FinishReason`
97
+ union) and `rawFinishReason` on `CompletionResult`, so a truncated/refused
98
+ answer is distinguishable from a genuinely empty one. A provider `refusal` is
99
+ surfaced and forces `finishReason: "content_filter"`.
100
+ - **Token usage accounting**: `CompletionResult.usage` (`Usage` of
101
+ `inputTokens`/`outputTokens`/`totalTokens`), or `undefined` when none is
102
+ reported. `CombineResult.usage` (`CombineUsage` with `total` plus a
103
+ per-participant `byParticipant` breakdown) aggregates every model call a combine
104
+ makes, so its true cost is visible.
105
+ - **Cancellation**: `CompletionRequest.signal?` (`AbortSignal`) is forwarded to
106
+ every provider `fetch` and threaded through `combine()`, so one signal cancels
107
+ every participant call. An aborted call rejects with a transport `ProviderError`.
108
+ - **Typed errors**: `ProviderError` (exported) carries `provider`, a `kind`
109
+ discriminant (`"api"` vs `"transport"`), `status` (for `"api"`), and a
110
+ `code`/`type` from the error body. `fetch` rejections are wrapped as
111
+ `"transport"`; an HTTP 200 with an `{ error }` body throws instead of returning a
112
+ silent empty result.
113
+ - **Automatic retry** with bounded exponential backoff on 429/503/529, for both
114
+ `complete()` and `stream()`. Honors `Retry-After`; the backoff respects the
115
+ request's `AbortSignal`. Configurable per provider via `retry`
116
+ (`{ maxRetries?, baseDelayMs? }`, exported as `RetryOptions`; default 2 retries
117
+ from 500ms, `0` disables). Transport failures are not retried.
118
+ - **Robust streaming**: SSE parsing tolerates blank/malformed `data:` frames, and
119
+ `stream()` releases the response body reader on every exit path (normal end,
120
+ error, or consumer `break`) so a long-running server can't leak a socket.
121
+ - Opt-in live integration tests (`*.integration.test.ts`), double-gated on
122
+ `RUN_LIVE_TESTS=1` plus the provider key; combine suites are triple-gated. Run
123
+ with `yarn test:integration` (optional filename pattern narrows to one suite).
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Anders Jansson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.