@wrongstack/providers 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/LICENSE ADDED
@@ -0,0 +1,17 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ Copyright 2026 ECOSTACK TECHNOLOGY OÜ
6
+
7
+ Licensed under the Apache License, Version 2.0 (the "License");
8
+ you may not use this file except in compliance with the License.
9
+ You may obtain a copy of the License at
10
+
11
+ http://www.apache.org/licenses/LICENSE-2.0
12
+
13
+ Unless required by applicable law or agreed to in writing, software
14
+ distributed under the License is distributed on an "AS IS" BASIS,
15
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ See the License for the specific language governing permissions and
17
+ limitations under the License.
@@ -0,0 +1,311 @@
1
+ import { Provider, Capabilities, Request, Response, StreamEvent, ProviderError, TextBlock, Message, Tool, ModelsRegistry, StopReason, ContentBlock, Logger, ProviderFactory, ProviderConfig } from '@wrongstack/core';
2
+
3
+ /**
4
+ * Minimal Server-Sent Events parser for HTTP streaming responses.
5
+ *
6
+ * Yields parsed events as `{ event, data }` pairs. Per spec:
7
+ * - Each event is separated by a blank line
8
+ * - `event: foo` sets the event name (defaults to "message")
9
+ * - `data: ...` lines accumulate into the data buffer
10
+ * - `:` lines are comments and ignored
11
+ * - `id` / `retry` fields are accepted and ignored
12
+ *
13
+ * For Anthropic the wire format is canonical SSE with explicit `event:` lines.
14
+ * For OpenAI / OpenAI-compatible the format omits `event:` and just emits
15
+ * `data: <json>` chunks, with a final `data: [DONE]`. Both work with this
16
+ * parser; consumers branch on event name or just on `data`.
17
+ */
18
+ interface SSEMessage {
19
+ event: string;
20
+ data: string;
21
+ }
22
+ declare function parseSSE(body: ReadableStream<Uint8Array> | NodeJS.ReadableStream | null): AsyncIterable<SSEMessage>;
23
+
24
+ /**
25
+ * Shared HTTP mechanics for streaming providers.
26
+ * Providers extend this to get:
27
+ * - canonical error handling (ProviderError with retryable flag)
28
+ * - SSE body parsing via parseSSE()
29
+ * - abort signal wiring
30
+ *
31
+ * Subclasses implement the abstract members to provide their specific wire format.
32
+ */
33
+ declare abstract class WireAdapter implements Provider {
34
+ protected readonly apiKey: string;
35
+ protected readonly baseUrl: string;
36
+ readonly fetchImpl: typeof fetch;
37
+ abstract readonly id: string;
38
+ abstract readonly capabilities: Capabilities;
39
+ constructor(apiKey: string, baseUrl: string, fetchImpl?: typeof fetch);
40
+ complete(req: Request, opts: {
41
+ signal: AbortSignal;
42
+ }): Promise<Response>;
43
+ stream(req: Request, opts: {
44
+ signal: AbortSignal;
45
+ }): AsyncIterable<StreamEvent>;
46
+ /** HTTP endpoint for this provider's chat completions / messages API. */
47
+ protected abstract buildUrl(req: Request): string;
48
+ /** Per-request headers. `apiKey` is already in scope — call `super.buildHeaders` first. */
49
+ protected buildHeaders(_req: Request): Record<string, string>;
50
+ /** Map Request fields to the wire request body. */
51
+ protected abstract buildBody(req: Request): Record<string, unknown>;
52
+ /** Translate wire SSE events into canonical StreamEvent[]. */
53
+ protected abstract parseStream(body: ReadableStream<Uint8Array> | NodeJS.ReadableStream | null, fallbackModel: string): AsyncIterable<StreamEvent>;
54
+ /** Build a ProviderError from an HTTP failure response. */
55
+ protected translateError(status: number, body: string): ProviderError;
56
+ }
57
+
58
+ interface AnthropicProviderOptions {
59
+ apiKey: string;
60
+ baseUrl?: string;
61
+ apiVersion?: string;
62
+ beta?: string[];
63
+ fetchImpl?: typeof fetch;
64
+ }
65
+ declare class AnthropicProvider extends WireAdapter {
66
+ readonly id = "anthropic";
67
+ readonly capabilities: Capabilities;
68
+ private readonly opts;
69
+ constructor(opts: AnthropicProviderOptions);
70
+ protected buildUrl(_req: Request): string;
71
+ protected buildHeaders(req: Request): Record<string, string>;
72
+ protected buildBody(req: Request): Record<string, unknown>;
73
+ protected parseStream(body: Parameters<typeof parseSSE>[0], fallbackModel: string): AsyncIterable<StreamEvent>;
74
+ protected translateError(status: number, text: string): ProviderError;
75
+ private normalizeMessage;
76
+ }
77
+
78
+ interface OpenAIToolSchema {
79
+ type: 'function';
80
+ function: {
81
+ name: string;
82
+ description: string;
83
+ parameters: Record<string, unknown>;
84
+ };
85
+ }
86
+ declare function toolsToOpenAI(tools: Tool[]): OpenAIToolSchema[];
87
+ interface OpenAIMessage {
88
+ role: 'system' | 'user' | 'assistant' | 'tool';
89
+ content?: string | OpenAIContent[];
90
+ tool_calls?: OpenAIToolCall[];
91
+ tool_call_id?: string;
92
+ name?: string;
93
+ }
94
+ interface OpenAIContent {
95
+ type: 'text' | 'image_url';
96
+ text?: string;
97
+ image_url?: {
98
+ url: string;
99
+ };
100
+ }
101
+ interface OpenAIToolCall {
102
+ id: string;
103
+ type: 'function';
104
+ function: {
105
+ name: string;
106
+ arguments: string;
107
+ };
108
+ }
109
+ interface ConvertOptions {
110
+ flattenContentToString?: boolean;
111
+ stripCacheControl?: boolean;
112
+ systemAsMessage?: boolean;
113
+ emptyToolCallContent?: 'null' | 'empty_string';
114
+ }
115
+ declare function messagesToOpenAI(system: TextBlock[] | undefined, messages: Message[], opts?: ConvertOptions): OpenAIMessage[];
116
+
117
+ interface OpenAIProviderOptions {
118
+ apiKey: string;
119
+ baseUrl?: string;
120
+ organization?: string;
121
+ fetchImpl?: typeof fetch;
122
+ quirks?: ConvertOptions & {
123
+ parallelToolsDisabled?: boolean;
124
+ jsonArgumentsBuggy?: boolean;
125
+ };
126
+ id?: string;
127
+ capabilities?: Partial<Capabilities>;
128
+ }
129
+ declare class OpenAIProvider extends WireAdapter {
130
+ readonly id: string;
131
+ readonly capabilities: Capabilities;
132
+ protected readonly opts: OpenAIProviderOptions;
133
+ constructor(opts: OpenAIProviderOptions);
134
+ protected buildUrl(_req: Request): string;
135
+ protected buildHeaders(req: Request): Record<string, string>;
136
+ protected buildBody(req: Request): Record<string, unknown>;
137
+ protected parseStream(body: Parameters<typeof parseSSE>[0], fallbackModel: string): AsyncIterable<StreamEvent>;
138
+ protected translateError(status: number, text: string): ProviderError;
139
+ private stripCacheControl;
140
+ }
141
+
142
+ interface CompatibilityQuirks {
143
+ stripCacheControl?: boolean;
144
+ systemAsMessage?: boolean;
145
+ flattenContentToString?: boolean;
146
+ preserveToolCallIds?: boolean;
147
+ parallelToolsDisabled?: boolean;
148
+ jsonArgumentsBuggy?: boolean;
149
+ emptyToolCallContent?: 'null' | 'empty_string';
150
+ }
151
+ interface OpenAICompatibleOptions {
152
+ id: string;
153
+ apiKey: string;
154
+ baseUrl: string;
155
+ headers?: Record<string, string>;
156
+ quirks?: CompatibilityQuirks;
157
+ capabilities?: Partial<Capabilities>;
158
+ fetchImpl?: typeof fetch;
159
+ /**
160
+ * Optional override for URL construction. Receives the base URL and request,
161
+ * returns the full URL to use. Allows custom providers with non-standard
162
+ * URL structures (e.g. Google with model-in-path, Anthropic with /v1/messages).
163
+ */
164
+ urlOverride?: (baseUrl: string, req: Request) => string;
165
+ }
166
+ declare class OpenAICompatibleProvider extends OpenAIProvider {
167
+ private readonly extraHeaders?;
168
+ private readonly urlOverride?;
169
+ constructor(opts: OpenAICompatibleOptions);
170
+ protected buildUrl(req: Request): string;
171
+ protected buildHeaders(req: Request): Record<string, string>;
172
+ }
173
+
174
+ /**
175
+ * Google Gemini wire format (generativelanguage.googleapis.com).
176
+ *
177
+ * Differences vs OpenAI:
178
+ * - Endpoint includes the model in the path: /v1beta/models/{model}:generateContent
179
+ * - Messages → `contents: [{ role: 'user'|'model', parts: [...] }]`
180
+ * - System prompt → `systemInstruction: { parts: [{ text }] }`
181
+ * - Tools → `tools: [{ functionDeclarations: [...] }]`
182
+ * - Tool call → `parts: [{ functionCall: { name, args } }]`
183
+ * - Tool result → `parts: [{ functionResponse: { name, response } }]`
184
+ * - Auth via `?key=` query param or `x-goog-api-key` header
185
+ */
186
+ interface GoogleProviderOptions {
187
+ apiKey: string;
188
+ baseUrl?: string;
189
+ fetchImpl?: typeof fetch;
190
+ id?: string;
191
+ capabilities?: Partial<Capabilities>;
192
+ }
193
+ declare class GoogleProvider extends WireAdapter {
194
+ readonly id: string;
195
+ readonly capabilities: Capabilities;
196
+ private readonly opts;
197
+ constructor(opts: GoogleProviderOptions);
198
+ protected buildUrl(req: Request): string;
199
+ protected buildHeaders(req: Request): Record<string, string>;
200
+ protected buildBody(req: Request): Record<string, unknown>;
201
+ protected parseStream(body: Parameters<typeof parseSSE>[0], fallbackModel: string): AsyncIterable<StreamEvent>;
202
+ protected translateError(status: number, text: string): ProviderError;
203
+ private buildGenConfig;
204
+ }
205
+
206
+ /**
207
+ * Resolve capabilities for a (provider, model) pair using the family default
208
+ * as a baseline and overlaying per-model facts from the ModelsRegistry.
209
+ */
210
+ declare function capabilitiesFor(registry: ModelsRegistry, providerId: string, modelId: string): Promise<Capabilities>;
211
+
212
+ /**
213
+ * Provider HTTP error bodies come in three or four shapes depending on
214
+ * vendor. Rather than dump the raw JSON into the error message (which is
215
+ * what was shipped to the user log before this module existed), we parse
216
+ * out the fields we care about — `type`, `message`, `requestId` — and put
217
+ * them on `ProviderError.body` for `describe()` and downstream rendering.
218
+ *
219
+ * The function is intentionally tolerant: anything we can't parse falls
220
+ * back to a truncated raw string, never throws.
221
+ */
222
+ declare function parseProviderHttpError(providerId: string, status: number, rawText: string): ProviderError;
223
+
224
+ declare function normalizeAnthropic(stop: string | null | undefined): StopReason;
225
+ declare function normalizeOpenAI(stop: string | null | undefined): StopReason;
226
+
227
+ interface AnthropicToolSchema {
228
+ name: string;
229
+ description: string;
230
+ input_schema: Record<string, unknown>;
231
+ }
232
+ declare function toolsToAnthropic(tools: Tool[]): AnthropicToolSchema[];
233
+
234
+ interface AnthropicBlock {
235
+ type: string;
236
+ text?: string;
237
+ id?: string;
238
+ name?: string;
239
+ input?: unknown;
240
+ content?: unknown;
241
+ tool_use_id?: string;
242
+ is_error?: boolean;
243
+ source?: {
244
+ type?: 'base64' | 'url';
245
+ media_type?: string;
246
+ data?: string;
247
+ url?: string;
248
+ };
249
+ }
250
+ interface FromAnthropicOptions {
251
+ /**
252
+ * Called once for each block whose `type` the converter doesn't recognize.
253
+ * The block is still dropped — this hook only exists so callers can wire
254
+ * it into observability (event bus, logger) instead of silently losing
255
+ * data. Anthropic ships new block types over time (`thinking`,
256
+ * `server_tool_use`, etc.) and we want a way to find out without
257
+ * inflating the conversion logic itself.
258
+ */
259
+ onUnsupported?: (type: string, block: AnthropicBlock) => void;
260
+ }
261
+ declare function contentFromAnthropic(blocks: AnthropicBlock[], opts?: FromAnthropicOptions): ContentBlock[];
262
+
263
+ interface OpenAIChoice {
264
+ message: {
265
+ role: string;
266
+ content: string | null;
267
+ tool_calls?: OpenAIToolCall[];
268
+ };
269
+ finish_reason: string | null;
270
+ }
271
+ interface FromOpenAIOptions {
272
+ /**
273
+ * Deprecated: the sanitizer fallback is now always attempted. Kept for
274
+ * backward compatibility; the value is ignored.
275
+ */
276
+ jsonArgumentsBuggy?: boolean;
277
+ /**
278
+ * Called when a tool call's `arguments` field can't be parsed even after
279
+ * the sanitizer pass. Callers can use this to emit a structured event,
280
+ * log it, or surface it in a UI. The block is still appended with
281
+ * `{ __raw_arguments }` so the tool gets *something* to fail on, but
282
+ * silently producing garbage input is the kind of bug that wastes
283
+ * debugging hours — this is the hook to find out.
284
+ */
285
+ onParseFailure?: (info: {
286
+ toolName: string;
287
+ toolCallId: string;
288
+ raw: string;
289
+ }) => void;
290
+ }
291
+ declare function contentFromOpenAI(choice: OpenAIChoice, opts?: FromOpenAIOptions): ContentBlock[];
292
+
293
+ interface BuildFactoriesOptions {
294
+ registry: ModelsRegistry;
295
+ /** Used to log unsupported families during boot. */
296
+ log?: Logger;
297
+ }
298
+ /**
299
+ * Build one ProviderFactory per provider known to models.dev. The factory's
300
+ * `create(cfg)` resolves the wire-family at construction time and returns the
301
+ * matching transport. Unsupported families return a stub that throws when
302
+ * complete() is called, so the system can still boot.
303
+ */
304
+ declare function buildProviderFactoriesFromRegistry(opts: BuildFactoriesOptions): Promise<ProviderFactory[]>;
305
+ /**
306
+ * Build a Provider purely from config — no models.dev lookup at all.
307
+ * Used for user-defined providers and offline operation.
308
+ */
309
+ declare function makeProviderFromConfig(id: string, cfg: ProviderConfig): Provider;
310
+
311
+ export { AnthropicProvider, type AnthropicProviderOptions, type BuildFactoriesOptions, type CompatibilityQuirks, type ConvertOptions, GoogleProvider, type GoogleProviderOptions, type OpenAIChoice, type OpenAICompatibleOptions, OpenAICompatibleProvider, type OpenAIMessage, OpenAIProvider, type OpenAIProviderOptions, type OpenAIToolCall, WireAdapter, buildProviderFactoriesFromRegistry, capabilitiesFor, contentFromAnthropic, contentFromOpenAI, makeProviderFromConfig, messagesToOpenAI, normalizeAnthropic, normalizeOpenAI, parseProviderHttpError, toolsToAnthropic, toolsToOpenAI };