@thinwrap/llm 1.0.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 +21 -0
- package/README.md +62 -0
- package/dist/cjs/base/base.connector.d.ts +44 -0
- package/dist/cjs/base/base.connector.js +216 -0
- package/dist/cjs/facades/chat.facade.d.ts +17 -0
- package/dist/cjs/facades/chat.facade.js +63 -0
- package/dist/cjs/facades/embeddings.facade.d.ts +23 -0
- package/dist/cjs/facades/embeddings.facade.js +63 -0
- package/dist/cjs/index.d.ts +15 -0
- package/dist/cjs/index.js +35 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/providers/_shared/openai-compat.connector.d.ts +23 -0
- package/dist/cjs/providers/_shared/openai-compat.connector.js +320 -0
- package/dist/cjs/providers/_shared/openai-compat.embeddings.connector.d.ts +18 -0
- package/dist/cjs/providers/_shared/openai-compat.embeddings.connector.js +105 -0
- package/dist/cjs/providers/_shared/spec.d.ts +99 -0
- package/dist/cjs/providers/_shared/spec.js +84 -0
- package/dist/cjs/providers/anthropic/anthropic.config.d.ts +17 -0
- package/dist/cjs/providers/anthropic/anthropic.config.js +2 -0
- package/dist/cjs/providers/anthropic/anthropic.connector.d.ts +30 -0
- package/dist/cjs/providers/anthropic/anthropic.connector.js +427 -0
- package/dist/cjs/providers/anthropic/index.d.ts +2 -0
- package/dist/cjs/providers/anthropic/index.js +5 -0
- package/dist/cjs/providers/bedrock/bedrock.config.d.ts +18 -0
- package/dist/cjs/providers/bedrock/bedrock.config.js +2 -0
- package/dist/cjs/providers/bedrock/bedrock.connector.d.ts +61 -0
- package/dist/cjs/providers/bedrock/bedrock.connector.js +479 -0
- package/dist/cjs/providers/bedrock/index.d.ts +2 -0
- package/dist/cjs/providers/bedrock/index.js +5 -0
- package/dist/cjs/providers/gemini/gemini.config.d.ts +12 -0
- package/dist/cjs/providers/gemini/gemini.config.js +2 -0
- package/dist/cjs/providers/gemini/gemini.connector.d.ts +28 -0
- package/dist/cjs/providers/gemini/gemini.connector.js +434 -0
- package/dist/cjs/providers/gemini/index.d.ts +2 -0
- package/dist/cjs/providers/gemini/index.js +5 -0
- package/dist/cjs/types/chat.types.d.ts +115 -0
- package/dist/cjs/types/chat.types.js +2 -0
- package/dist/cjs/types/config-map.type.d.ts +29 -0
- package/dist/cjs/types/config-map.type.js +2 -0
- package/dist/cjs/types/embeddings.types.d.ts +29 -0
- package/dist/cjs/types/embeddings.types.js +2 -0
- package/dist/cjs/types/error.types.d.ts +20 -0
- package/dist/cjs/types/error.types.js +22 -0
- package/dist/cjs/types/index.d.ts +7 -0
- package/dist/cjs/types/index.js +23 -0
- package/dist/cjs/types/passthrough.type.d.ts +15 -0
- package/dist/cjs/types/passthrough.type.js +2 -0
- package/dist/cjs/types/provider-id.enum.d.ts +8 -0
- package/dist/cjs/types/provider-id.enum.js +29 -0
- package/dist/cjs/types/provider.interface.d.ts +7 -0
- package/dist/cjs/types/provider.interface.js +2 -0
- package/dist/esm/base/base.connector.d.ts +44 -0
- package/dist/esm/base/base.connector.js +209 -0
- package/dist/esm/facades/chat.facade.d.ts +17 -0
- package/dist/esm/facades/chat.facade.js +59 -0
- package/dist/esm/facades/embeddings.facade.d.ts +23 -0
- package/dist/esm/facades/embeddings.facade.js +59 -0
- package/dist/esm/index.d.ts +15 -0
- package/dist/esm/index.js +9 -0
- package/dist/esm/package.json +1 -0
- package/dist/esm/providers/_shared/openai-compat.connector.d.ts +23 -0
- package/dist/esm/providers/_shared/openai-compat.connector.js +316 -0
- package/dist/esm/providers/_shared/openai-compat.embeddings.connector.d.ts +18 -0
- package/dist/esm/providers/_shared/openai-compat.embeddings.connector.js +101 -0
- package/dist/esm/providers/_shared/spec.d.ts +99 -0
- package/dist/esm/providers/_shared/spec.js +81 -0
- package/dist/esm/providers/anthropic/anthropic.config.d.ts +17 -0
- package/dist/esm/providers/anthropic/anthropic.config.js +1 -0
- package/dist/esm/providers/anthropic/anthropic.connector.d.ts +30 -0
- package/dist/esm/providers/anthropic/anthropic.connector.js +423 -0
- package/dist/esm/providers/anthropic/index.d.ts +2 -0
- package/dist/esm/providers/anthropic/index.js +1 -0
- package/dist/esm/providers/bedrock/bedrock.config.d.ts +18 -0
- package/dist/esm/providers/bedrock/bedrock.config.js +1 -0
- package/dist/esm/providers/bedrock/bedrock.connector.d.ts +61 -0
- package/dist/esm/providers/bedrock/bedrock.connector.js +474 -0
- package/dist/esm/providers/bedrock/index.d.ts +2 -0
- package/dist/esm/providers/bedrock/index.js +1 -0
- package/dist/esm/providers/gemini/gemini.config.d.ts +12 -0
- package/dist/esm/providers/gemini/gemini.config.js +1 -0
- package/dist/esm/providers/gemini/gemini.connector.d.ts +28 -0
- package/dist/esm/providers/gemini/gemini.connector.js +430 -0
- package/dist/esm/providers/gemini/index.d.ts +2 -0
- package/dist/esm/providers/gemini/index.js +1 -0
- package/dist/esm/types/chat.types.d.ts +115 -0
- package/dist/esm/types/chat.types.js +1 -0
- package/dist/esm/types/config-map.type.d.ts +29 -0
- package/dist/esm/types/config-map.type.js +1 -0
- package/dist/esm/types/embeddings.types.d.ts +29 -0
- package/dist/esm/types/embeddings.types.js +1 -0
- package/dist/esm/types/error.types.d.ts +20 -0
- package/dist/esm/types/error.types.js +18 -0
- package/dist/esm/types/index.d.ts +7 -0
- package/dist/esm/types/index.js +7 -0
- package/dist/esm/types/passthrough.type.d.ts +15 -0
- package/dist/esm/types/passthrough.type.js +1 -0
- package/dist/esm/types/provider-id.enum.d.ts +8 -0
- package/dist/esm/types/provider-id.enum.js +26 -0
- package/dist/esm/types/provider.interface.d.ts +7 -0
- package/dist/esm/types/provider.interface.js +1 -0
- package/package.json +73 -0
- package/src/providers/anthropic/README.md +64 -0
- package/src/providers/azure-openai/README.md +62 -0
- package/src/providers/bedrock/README.md +71 -0
- package/src/providers/cloudflare/README.md +62 -0
- package/src/providers/deepinfra/README.md +61 -0
- package/src/providers/deepseek/README.md +50 -0
- package/src/providers/fireworks/README.md +62 -0
- package/src/providers/gemini/README.md +67 -0
- package/src/providers/groq/README.md +50 -0
- package/src/providers/lmstudio/README.md +62 -0
- package/src/providers/mistral/README.md +61 -0
- package/src/providers/ollama/README.md +62 -0
- package/src/providers/openai/README.md +62 -0
- package/src/providers/openrouter/README.md +62 -0
- package/src/providers/perplexity/README.md +50 -0
- package/src/providers/together/README.md +61 -0
- package/src/providers/vllm/README.md +62 -0
- package/src/providers/xai/README.md +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dmitry Polyanovsky & contributors
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# @thinwrap/llm
|
|
2
|
+
|
|
3
|
+
Unified, **zero-dependency** TypeScript facade over LLM chat-completion providers. One typed `Chat` facade; switch vendor by changing the provider id + model. Stateless, bring-your-own `fetch`, no vendor SDKs — the in-process, zero-egress complement to an LLM gateway.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @thinwrap/llm
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Node ≥ 18 (native `fetch`).
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { Chat } from '@thinwrap/llm';
|
|
17
|
+
|
|
18
|
+
const chat = new Chat('openai', { apiKey: process.env.OPENAI_API_KEY! });
|
|
19
|
+
|
|
20
|
+
const res = await chat.complete({
|
|
21
|
+
model: 'gpt-5.6',
|
|
22
|
+
messages: [{ role: 'user', content: 'Say hi in one word.' }],
|
|
23
|
+
});
|
|
24
|
+
console.log(res.message.content, res.usage);
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Switching vendor is the provider id + model:
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
const chat = new Chat('anthropic', { apiKey: process.env.ANTHROPIC_API_KEY! });
|
|
31
|
+
// same .complete(input) shape, same ChatResult
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Streaming:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
for await (const delta of new Chat('groq', { apiKey }).stream(input)) {
|
|
38
|
+
if (delta.contentDelta) process.stdout.write(delta.contentDelta);
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Design
|
|
43
|
+
|
|
44
|
+
Facade → connector dispatch → shared `OpenAICompatConnector` (or a native adapter). The normalized facade holds only the ≥90%-supported core; provider-specific features ride through `_passthrough` (request) / `raw` (result). See the contributor docs in `.ai/`.
|
|
45
|
+
|
|
46
|
+
Per-provider docs (auth, config, quirks, rate-limit links, error mapping) live in
|
|
47
|
+
[`src/providers/`](src/providers) — one README per provider.
|
|
48
|
+
|
|
49
|
+
## Timeouts & cancellation
|
|
50
|
+
|
|
51
|
+
The wrapper is stateless and holds no timeout of its own — it awaits whatever your `fetch` does. To bound a hung provider, bring your own timeout via the injected `fetch`:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
const chat = new Chat('openai', {
|
|
55
|
+
apiKey: process.env.OPENAI_API_KEY,
|
|
56
|
+
fetch: (url, init) => fetch(url, { ...init, signal: AbortSignal.timeout(30_000) }),
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
An aborted request surfaces as a `ConnectorError` (`providerCode: 'invalid_request'`), same as every other failure.
|
|
61
|
+
|
|
62
|
+
MIT © Dmitry Polyanovsky
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { ConnectorError } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Shared HTTP plumbing for every connector. Holds the BYO `fetch` and maps
|
|
4
|
+
* transport-level failures (abort, network) to `ConnectorError`. Applies no
|
|
5
|
+
* automatic key transformation and holds no state (umbrella invariants).
|
|
6
|
+
*/
|
|
7
|
+
export declare abstract class BaseConnector {
|
|
8
|
+
protected readonly fetchImpl: typeof fetch;
|
|
9
|
+
protected constructor(fetchImpl?: typeof fetch);
|
|
10
|
+
protected sendPostJson(url: string, body: unknown, headers: Record<string, string>, signal?: AbortSignal): Promise<Response>;
|
|
11
|
+
protected invokeFetch(url: string, init: RequestInit): Promise<Response>;
|
|
12
|
+
/**
|
|
13
|
+
* Guard a decoded 2xx body: a `null` here means the body was empty or not
|
|
14
|
+
* JSON (a proxy/captive-portal HTML page, a truncated read). Parsing that into
|
|
15
|
+
* an empty "successful" result is silent data loss, so raise instead.
|
|
16
|
+
*/
|
|
17
|
+
protected requireDecodedBody<T>(json: T | null, status: number): T;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Map a caught transport-layer failure to a `ConnectorError`. Shared by the
|
|
21
|
+
* request path (`invokeFetch`) and the streaming read loop (`parseSSEStream`) so
|
|
22
|
+
* a mid-stream TCP drop / abort surfaces with the same taxonomy as a failure
|
|
23
|
+
* before the first byte — never a raw `TypeError`/`DOMException`.
|
|
24
|
+
*
|
|
25
|
+
* A manual `AbortController.abort()` raises an `AbortError` (consumer cancelled
|
|
26
|
+
* → `invalid_request`); `AbortSignal.timeout()` raises a distinct `TimeoutError`
|
|
27
|
+
* (the provider failed to respond in time → `provider_unavailable`). Any other
|
|
28
|
+
* rejection (network reset, DNS) is `provider_unavailable`.
|
|
29
|
+
*/
|
|
30
|
+
export declare function transportErrorFrom(err: unknown): ConnectorError;
|
|
31
|
+
/**
|
|
32
|
+
* Append `_passthrough.query` params to a request URL. EVERY request path must
|
|
33
|
+
* honor this — not just OpenAI-compat chat — because some providers require a
|
|
34
|
+
* query param the facade doesn't model (e.g. Azure OpenAI's `api-version`).
|
|
35
|
+
* Preserves any query string the URL already carries.
|
|
36
|
+
*/
|
|
37
|
+
export declare function appendPassthroughQuery(url: string, query?: Record<string, string>): string;
|
|
38
|
+
/**
|
|
39
|
+
* Parse a Server-Sent-Events response body into a sequence of JSON-decoded
|
|
40
|
+
* `data:` payloads. Skips comments/blank lines, stops at the `[DONE]` sentinel,
|
|
41
|
+
* and ignores any `data:` line that isn't valid JSON. Transport-agnostic over
|
|
42
|
+
* `\n\n` and `\r\n\r\n` event boundaries.
|
|
43
|
+
*/
|
|
44
|
+
export declare function parseSSEStream(response: Response): AsyncGenerator<unknown>;
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BaseConnector = void 0;
|
|
4
|
+
exports.transportErrorFrom = transportErrorFrom;
|
|
5
|
+
exports.appendPassthroughQuery = appendPassthroughQuery;
|
|
6
|
+
exports.parseSSEStream = parseSSEStream;
|
|
7
|
+
const types_1 = require("../types");
|
|
8
|
+
/**
|
|
9
|
+
* Shared HTTP plumbing for every connector. Holds the BYO `fetch` and maps
|
|
10
|
+
* transport-level failures (abort, network) to `ConnectorError`. Applies no
|
|
11
|
+
* automatic key transformation and holds no state (umbrella invariants).
|
|
12
|
+
*/
|
|
13
|
+
class BaseConnector {
|
|
14
|
+
constructor(fetchImpl) {
|
|
15
|
+
const baseFetch = fetchImpl ?? globalThis.fetch;
|
|
16
|
+
// Force `redirect: 'error'` on every request at the transport seam so a 3xx
|
|
17
|
+
// can never silently re-send credentials to the redirect target. `fetch`
|
|
18
|
+
// strips only `Authorization`/`Cookie` cross-origin, so provider key headers
|
|
19
|
+
// (x-api-key, x-goog-api-key, api-key, …) would otherwise leak. No LLM
|
|
20
|
+
// endpoint legitimately redirects a POST.
|
|
21
|
+
this.fetchImpl = ((input, init) => baseFetch(input, { ...init, redirect: 'error' }));
|
|
22
|
+
}
|
|
23
|
+
async sendPostJson(url, body, headers, signal) {
|
|
24
|
+
return this.invokeFetch(url, {
|
|
25
|
+
method: 'POST',
|
|
26
|
+
headers: { 'Content-Type': 'application/json', ...headers },
|
|
27
|
+
body: JSON.stringify(body),
|
|
28
|
+
signal,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
async invokeFetch(url, init) {
|
|
32
|
+
try {
|
|
33
|
+
return await this.fetchImpl(url, init);
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
throw transportErrorFrom(err);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Guard a decoded 2xx body: a `null` here means the body was empty or not
|
|
41
|
+
* JSON (a proxy/captive-portal HTML page, a truncated read). Parsing that into
|
|
42
|
+
* an empty "successful" result is silent data loss, so raise instead.
|
|
43
|
+
*/
|
|
44
|
+
requireDecodedBody(json, status) {
|
|
45
|
+
if (json === null) {
|
|
46
|
+
const message = 'Provider returned a successful status with an empty or non-JSON body';
|
|
47
|
+
throw new types_1.ConnectorError({
|
|
48
|
+
message,
|
|
49
|
+
statusCode: status,
|
|
50
|
+
providerCode: 'provider_unavailable',
|
|
51
|
+
providerMessage: message,
|
|
52
|
+
cause: null,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return json;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
exports.BaseConnector = BaseConnector;
|
|
59
|
+
/**
|
|
60
|
+
* Map a caught transport-layer failure to a `ConnectorError`. Shared by the
|
|
61
|
+
* request path (`invokeFetch`) and the streaming read loop (`parseSSEStream`) so
|
|
62
|
+
* a mid-stream TCP drop / abort surfaces with the same taxonomy as a failure
|
|
63
|
+
* before the first byte — never a raw `TypeError`/`DOMException`.
|
|
64
|
+
*
|
|
65
|
+
* A manual `AbortController.abort()` raises an `AbortError` (consumer cancelled
|
|
66
|
+
* → `invalid_request`); `AbortSignal.timeout()` raises a distinct `TimeoutError`
|
|
67
|
+
* (the provider failed to respond in time → `provider_unavailable`). Any other
|
|
68
|
+
* rejection (network reset, DNS) is `provider_unavailable`.
|
|
69
|
+
*/
|
|
70
|
+
function transportErrorFrom(err) {
|
|
71
|
+
if (err instanceof types_1.ConnectorError)
|
|
72
|
+
return err;
|
|
73
|
+
const name = err?.name;
|
|
74
|
+
if (name === 'AbortError') {
|
|
75
|
+
return new types_1.ConnectorError({
|
|
76
|
+
message: err.message || 'Request cancelled',
|
|
77
|
+
statusCode: null,
|
|
78
|
+
providerCode: 'invalid_request',
|
|
79
|
+
cause: { raw: err },
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return new types_1.ConnectorError({
|
|
83
|
+
message: err?.message || 'Network error',
|
|
84
|
+
statusCode: null,
|
|
85
|
+
providerCode: 'provider_unavailable',
|
|
86
|
+
cause: { raw: err },
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Append `_passthrough.query` params to a request URL. EVERY request path must
|
|
91
|
+
* honor this — not just OpenAI-compat chat — because some providers require a
|
|
92
|
+
* query param the facade doesn't model (e.g. Azure OpenAI's `api-version`).
|
|
93
|
+
* Preserves any query string the URL already carries.
|
|
94
|
+
*/
|
|
95
|
+
function appendPassthroughQuery(url, query) {
|
|
96
|
+
if (!query || Object.keys(query).length === 0)
|
|
97
|
+
return url;
|
|
98
|
+
const sep = url.includes('?') ? '&' : '?';
|
|
99
|
+
return `${url}${sep}${new URLSearchParams(query).toString()}`;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Upper bound on the residual (not-yet-terminated) SSE buffer. A stream that
|
|
103
|
+
* never emits an event boundary would otherwise grow `buffer` without limit
|
|
104
|
+
* (memory-exhaustion DoS from a malicious/broken upstream). Generous enough for
|
|
105
|
+
* any single legitimate LLM event; complete events are drained before this is
|
|
106
|
+
* checked, so many-small-event streams never approach it.
|
|
107
|
+
*/
|
|
108
|
+
const MAX_SSE_EVENT_CHARS = 16 * 1024 * 1024;
|
|
109
|
+
/**
|
|
110
|
+
* Parse a Server-Sent-Events response body into a sequence of JSON-decoded
|
|
111
|
+
* `data:` payloads. Skips comments/blank lines, stops at the `[DONE]` sentinel,
|
|
112
|
+
* and ignores any `data:` line that isn't valid JSON. Transport-agnostic over
|
|
113
|
+
* `\n\n` and `\r\n\r\n` event boundaries.
|
|
114
|
+
*/
|
|
115
|
+
async function* parseSSEStream(response) {
|
|
116
|
+
const body = response.body;
|
|
117
|
+
if (!body)
|
|
118
|
+
return;
|
|
119
|
+
const reader = body.getReader();
|
|
120
|
+
const decoder = new TextDecoder();
|
|
121
|
+
let buffer = '';
|
|
122
|
+
let drained = false;
|
|
123
|
+
try {
|
|
124
|
+
for (;;) {
|
|
125
|
+
let done;
|
|
126
|
+
let value;
|
|
127
|
+
try {
|
|
128
|
+
({ done, value } = await reader.read());
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
// A mid-stream read rejection (TCP reset, abort, undici error) must
|
|
132
|
+
// surface with the ConnectorError taxonomy, not as a raw DOMException —
|
|
133
|
+
// streams are the failures consumers are least able to catch generically.
|
|
134
|
+
throw transportErrorFrom(err);
|
|
135
|
+
}
|
|
136
|
+
if (done)
|
|
137
|
+
break;
|
|
138
|
+
buffer += decoder.decode(value, { stream: true });
|
|
139
|
+
let boundary;
|
|
140
|
+
while ((boundary = indexOfEventBoundary(buffer)) !== -1) {
|
|
141
|
+
const rawEvent = buffer.slice(0, boundary);
|
|
142
|
+
buffer = buffer.slice(boundary).replace(/^(?:\r\n|\r|\n)+/, '');
|
|
143
|
+
const payload = extractData(rawEvent);
|
|
144
|
+
if (payload === null)
|
|
145
|
+
continue;
|
|
146
|
+
if (payload === '[DONE]') {
|
|
147
|
+
// Leave `drained = false` so the `finally` cancels the reader. The
|
|
148
|
+
// sentinel does NOT mean the body is fully consumed — every
|
|
149
|
+
// OpenAI-compat stream ends here, and without a cancel undici can't
|
|
150
|
+
// release the socket (leak on every successful stream).
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const parsed = tryParseJson(payload);
|
|
154
|
+
if (parsed !== undefined)
|
|
155
|
+
yield parsed;
|
|
156
|
+
}
|
|
157
|
+
if (buffer.length > MAX_SSE_EVENT_CHARS) {
|
|
158
|
+
throw new types_1.ConnectorError({
|
|
159
|
+
message: `SSE event exceeded ${MAX_SSE_EVENT_CHARS} characters without a boundary`,
|
|
160
|
+
statusCode: null,
|
|
161
|
+
providerCode: 'provider_unavailable',
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const tail = buffer.trim();
|
|
166
|
+
if (tail) {
|
|
167
|
+
const payload = extractData(tail);
|
|
168
|
+
if (payload !== null && payload !== '[DONE]') {
|
|
169
|
+
const parsed = tryParseJson(payload);
|
|
170
|
+
if (parsed !== undefined)
|
|
171
|
+
yield parsed;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
drained = true;
|
|
175
|
+
}
|
|
176
|
+
finally {
|
|
177
|
+
// If the consumer broke out early (or we threw), the body is not fully read —
|
|
178
|
+
// cancel it so the underlying connection is released instead of leaking.
|
|
179
|
+
if (!drained) {
|
|
180
|
+
try {
|
|
181
|
+
await reader.cancel();
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
/* best-effort */
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
reader.releaseLock();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// An SSE event boundary is a blank line: two consecutive line terminators. The
|
|
191
|
+
// spec permits CRLF, CR, or LF as a terminator, so accept any pairing
|
|
192
|
+
// (`\n\n`, `\r\n\r\n`, `\r\r`, and mixed `\n\r\n` / `\r\n\n`) rather than only
|
|
193
|
+
// the two common forms — a CR-only or mixed upstream would otherwise never
|
|
194
|
+
// split and buffer until MAX_SSE_EVENT_CHARS.
|
|
195
|
+
const EVENT_BOUNDARY_RE = /(?:\r\n|\r|\n)(?:\r\n|\r|\n)/;
|
|
196
|
+
function indexOfEventBoundary(buffer) {
|
|
197
|
+
const m = EVENT_BOUNDARY_RE.exec(buffer);
|
|
198
|
+
return m ? m.index : -1;
|
|
199
|
+
}
|
|
200
|
+
function extractData(rawEvent) {
|
|
201
|
+
const dataParts = [];
|
|
202
|
+
for (const line of rawEvent.split(/\r\n|\r|\n/)) {
|
|
203
|
+
if (line.startsWith('data:')) {
|
|
204
|
+
dataParts.push(line.slice(5).replace(/^ /, ''));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return dataParts.length === 0 ? null : dataParts.join('\n');
|
|
208
|
+
}
|
|
209
|
+
function tryParseJson(text) {
|
|
210
|
+
try {
|
|
211
|
+
return JSON.parse(text);
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { LlmProviderId, ChatInput, ChatResult, ChatStreamDelta, IChatConnector, ProviderConfigMap } from '../types';
|
|
2
|
+
export type ChatConfig<P extends LlmProviderId> = ProviderConfigMap[P];
|
|
3
|
+
/**
|
|
4
|
+
* The chat-completion facade. Construct by provider id + config — the facade
|
|
5
|
+
* dispatches to the shared OpenAI-compatible connector (native adapters slot in
|
|
6
|
+
* here as they land) — or pass a connector instance directly for advanced use.
|
|
7
|
+
* Switching vendor is a change of provider id + `model` only; `ChatInput` /
|
|
8
|
+
* `ChatResult` are identical across providers.
|
|
9
|
+
*/
|
|
10
|
+
export declare class Chat<P extends LlmProviderId = LlmProviderId> {
|
|
11
|
+
readonly id: string;
|
|
12
|
+
private readonly connector;
|
|
13
|
+
constructor(providerId: P, config: ChatConfig<P>);
|
|
14
|
+
constructor(connector: IChatConnector);
|
|
15
|
+
complete(input: ChatInput): Promise<ChatResult>;
|
|
16
|
+
stream(input: ChatInput): AsyncIterable<ChatStreamDelta>;
|
|
17
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Chat = void 0;
|
|
4
|
+
const openai_compat_connector_1 = require("../providers/_shared/openai-compat.connector");
|
|
5
|
+
const spec_1 = require("../providers/_shared/spec");
|
|
6
|
+
const anthropic_1 = require("../providers/anthropic");
|
|
7
|
+
const bedrock_1 = require("../providers/bedrock");
|
|
8
|
+
const gemini_1 = require("../providers/gemini");
|
|
9
|
+
const types_1 = require("../types");
|
|
10
|
+
/**
|
|
11
|
+
* The chat-completion facade. Construct by provider id + config — the facade
|
|
12
|
+
* dispatches to the shared OpenAI-compatible connector (native adapters slot in
|
|
13
|
+
* here as they land) — or pass a connector instance directly for advanced use.
|
|
14
|
+
* Switching vendor is a change of provider id + `model` only; `ChatInput` /
|
|
15
|
+
* `ChatResult` are identical across providers.
|
|
16
|
+
*/
|
|
17
|
+
class Chat {
|
|
18
|
+
constructor(arg, config) {
|
|
19
|
+
if (typeof arg === 'object' && arg !== null) {
|
|
20
|
+
this.connector = arg;
|
|
21
|
+
this.id = arg.id;
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const providerId = arg;
|
|
25
|
+
this.id = providerId;
|
|
26
|
+
if (!config) {
|
|
27
|
+
throw new types_1.ConnectorError({
|
|
28
|
+
message: 'Chat facade requires `config` when constructed with a provider id',
|
|
29
|
+
statusCode: null,
|
|
30
|
+
providerCode: 'invalid_request',
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
// Native adapters — structurally different wire, same normalized surface.
|
|
34
|
+
if (providerId === 'anthropic') {
|
|
35
|
+
this.connector = new anthropic_1.AnthropicConnector(config);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (providerId === 'bedrock') {
|
|
39
|
+
this.connector = new bedrock_1.BedrockConnector(config);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (providerId === 'gemini') {
|
|
43
|
+
this.connector = new gemini_1.GeminiConnector(config);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const spec = spec_1.SPECS[providerId];
|
|
47
|
+
if (!spec) {
|
|
48
|
+
throw new types_1.ConnectorError({
|
|
49
|
+
message: `Unsupported LLM provider: ${String(providerId)}`,
|
|
50
|
+
statusCode: null,
|
|
51
|
+
providerCode: 'invalid_request',
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
this.connector = new openai_compat_connector_1.OpenAICompatConnector(spec, config);
|
|
55
|
+
}
|
|
56
|
+
complete(input) {
|
|
57
|
+
return this.connector.complete(input);
|
|
58
|
+
}
|
|
59
|
+
stream(input) {
|
|
60
|
+
return this.connector.stream(input);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
exports.Chat = Chat;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { EmbeddingsInput, EmbeddingsResult, IEmbeddingsConnector, OpenAICompatConfig } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* First-class providers that expose an OpenAI-float-shaped `/embeddings`
|
|
4
|
+
* surface (FR-AI-E1). Deliberately excludes: providers with no native
|
|
5
|
+
* embeddings (deepseek, xai), quantized/non-float shapes (perplexity int8),
|
|
6
|
+
* unstable coverage (groq), and the native-adapter providers (Anthropic →
|
|
7
|
+
* Voyage; Bedrock/Gemini native embeddings have a different shape — a future
|
|
8
|
+
* per-connector embeddings surface).
|
|
9
|
+
*/
|
|
10
|
+
export declare const EMBEDDINGS_PROVIDER_IDS: readonly ["openai", "azure-openai", "openrouter", "together", "fireworks", "mistral", "deepinfra", "cloudflare", "vllm", "ollama", "lmstudio"];
|
|
11
|
+
export type EmbeddingsProviderId = (typeof EMBEDDINGS_PROVIDER_IDS)[number];
|
|
12
|
+
/**
|
|
13
|
+
* The embeddings facade — a separate operation from `Chat`. Construct by
|
|
14
|
+
* provider id + config, or with a connector instance. Only the OpenAI-float
|
|
15
|
+
* subset is accepted (type-restricted); other ids throw `ConnectorError`.
|
|
16
|
+
*/
|
|
17
|
+
export declare class Embeddings<P extends EmbeddingsProviderId = EmbeddingsProviderId> {
|
|
18
|
+
readonly id: string;
|
|
19
|
+
private readonly connector;
|
|
20
|
+
constructor(providerId: P, config: OpenAICompatConfig);
|
|
21
|
+
constructor(connector: IEmbeddingsConnector);
|
|
22
|
+
create(input: EmbeddingsInput): Promise<EmbeddingsResult>;
|
|
23
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Embeddings = exports.EMBEDDINGS_PROVIDER_IDS = void 0;
|
|
4
|
+
const openai_compat_embeddings_connector_1 = require("../providers/_shared/openai-compat.embeddings.connector");
|
|
5
|
+
const spec_1 = require("../providers/_shared/spec");
|
|
6
|
+
const types_1 = require("../types");
|
|
7
|
+
/**
|
|
8
|
+
* First-class providers that expose an OpenAI-float-shaped `/embeddings`
|
|
9
|
+
* surface (FR-AI-E1). Deliberately excludes: providers with no native
|
|
10
|
+
* embeddings (deepseek, xai), quantized/non-float shapes (perplexity int8),
|
|
11
|
+
* unstable coverage (groq), and the native-adapter providers (Anthropic →
|
|
12
|
+
* Voyage; Bedrock/Gemini native embeddings have a different shape — a future
|
|
13
|
+
* per-connector embeddings surface).
|
|
14
|
+
*/
|
|
15
|
+
exports.EMBEDDINGS_PROVIDER_IDS = [
|
|
16
|
+
'openai',
|
|
17
|
+
'azure-openai',
|
|
18
|
+
'openrouter',
|
|
19
|
+
'together',
|
|
20
|
+
'fireworks',
|
|
21
|
+
'mistral',
|
|
22
|
+
'deepinfra',
|
|
23
|
+
'cloudflare',
|
|
24
|
+
'vllm',
|
|
25
|
+
'ollama',
|
|
26
|
+
'lmstudio',
|
|
27
|
+
];
|
|
28
|
+
const SUPPORTED = new Set(exports.EMBEDDINGS_PROVIDER_IDS);
|
|
29
|
+
/**
|
|
30
|
+
* The embeddings facade — a separate operation from `Chat`. Construct by
|
|
31
|
+
* provider id + config, or with a connector instance. Only the OpenAI-float
|
|
32
|
+
* subset is accepted (type-restricted); other ids throw `ConnectorError`.
|
|
33
|
+
*/
|
|
34
|
+
class Embeddings {
|
|
35
|
+
constructor(arg, config) {
|
|
36
|
+
if (typeof arg === 'object' && arg !== null) {
|
|
37
|
+
this.connector = arg;
|
|
38
|
+
this.id = arg.id;
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const providerId = arg;
|
|
42
|
+
this.id = providerId;
|
|
43
|
+
if (!SUPPORTED.has(providerId)) {
|
|
44
|
+
throw new types_1.ConnectorError({
|
|
45
|
+
message: `Provider '${String(providerId)}' has no OpenAI-compatible embeddings surface`,
|
|
46
|
+
statusCode: null,
|
|
47
|
+
providerCode: 'invalid_request',
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
if (!config) {
|
|
51
|
+
throw new types_1.ConnectorError({
|
|
52
|
+
message: 'Embeddings facade requires `config` when constructed with a provider id',
|
|
53
|
+
statusCode: null,
|
|
54
|
+
providerCode: 'invalid_request',
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
this.connector = new openai_compat_embeddings_connector_1.OpenAICompatEmbeddingsConnector(spec_1.SPECS[providerId], config);
|
|
58
|
+
}
|
|
59
|
+
create(input) {
|
|
60
|
+
return this.connector.create(input);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
exports.Embeddings = Embeddings;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { Chat } from './facades/chat.facade';
|
|
2
|
+
export type { ChatConfig } from './facades/chat.facade';
|
|
3
|
+
export { Embeddings, EMBEDDINGS_PROVIDER_IDS } from './facades/embeddings.facade';
|
|
4
|
+
export type { EmbeddingsProviderId } from './facades/embeddings.facade';
|
|
5
|
+
export { OpenAICompatConnector } from './providers/_shared/openai-compat.connector';
|
|
6
|
+
export { OpenAICompatEmbeddingsConnector } from './providers/_shared/openai-compat.embeddings.connector';
|
|
7
|
+
export { SPECS } from './providers/_shared/spec';
|
|
8
|
+
export type { OpenAICompatSpec, SpecId } from './providers/_shared/spec';
|
|
9
|
+
export { AnthropicConnector } from './providers/anthropic';
|
|
10
|
+
export type { AnthropicConfig } from './providers/anthropic';
|
|
11
|
+
export { BedrockConnector } from './providers/bedrock';
|
|
12
|
+
export type { BedrockConfig } from './providers/bedrock';
|
|
13
|
+
export { GeminiConnector } from './providers/gemini';
|
|
14
|
+
export type { GeminiConfig } from './providers/gemini';
|
|
15
|
+
export * from './types';
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.GeminiConnector = exports.BedrockConnector = exports.AnthropicConnector = exports.SPECS = exports.OpenAICompatEmbeddingsConnector = exports.OpenAICompatConnector = exports.EMBEDDINGS_PROVIDER_IDS = exports.Embeddings = exports.Chat = void 0;
|
|
18
|
+
var chat_facade_1 = require("./facades/chat.facade");
|
|
19
|
+
Object.defineProperty(exports, "Chat", { enumerable: true, get: function () { return chat_facade_1.Chat; } });
|
|
20
|
+
var embeddings_facade_1 = require("./facades/embeddings.facade");
|
|
21
|
+
Object.defineProperty(exports, "Embeddings", { enumerable: true, get: function () { return embeddings_facade_1.Embeddings; } });
|
|
22
|
+
Object.defineProperty(exports, "EMBEDDINGS_PROVIDER_IDS", { enumerable: true, get: function () { return embeddings_facade_1.EMBEDDINGS_PROVIDER_IDS; } });
|
|
23
|
+
var openai_compat_connector_1 = require("./providers/_shared/openai-compat.connector");
|
|
24
|
+
Object.defineProperty(exports, "OpenAICompatConnector", { enumerable: true, get: function () { return openai_compat_connector_1.OpenAICompatConnector; } });
|
|
25
|
+
var openai_compat_embeddings_connector_1 = require("./providers/_shared/openai-compat.embeddings.connector");
|
|
26
|
+
Object.defineProperty(exports, "OpenAICompatEmbeddingsConnector", { enumerable: true, get: function () { return openai_compat_embeddings_connector_1.OpenAICompatEmbeddingsConnector; } });
|
|
27
|
+
var spec_1 = require("./providers/_shared/spec");
|
|
28
|
+
Object.defineProperty(exports, "SPECS", { enumerable: true, get: function () { return spec_1.SPECS; } });
|
|
29
|
+
var anthropic_1 = require("./providers/anthropic");
|
|
30
|
+
Object.defineProperty(exports, "AnthropicConnector", { enumerable: true, get: function () { return anthropic_1.AnthropicConnector; } });
|
|
31
|
+
var bedrock_1 = require("./providers/bedrock");
|
|
32
|
+
Object.defineProperty(exports, "BedrockConnector", { enumerable: true, get: function () { return bedrock_1.BedrockConnector; } });
|
|
33
|
+
var gemini_1 = require("./providers/gemini");
|
|
34
|
+
Object.defineProperty(exports, "GeminiConnector", { enumerable: true, get: function () { return gemini_1.GeminiConnector; } });
|
|
35
|
+
__exportStar(require("./types"), exports);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"commonjs"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { BaseConnector } from '../../base/base.connector';
|
|
2
|
+
import type { ChatInput, ChatResult, ChatStreamDelta, IChatConnector, OpenAICompatConfig } from '../../types';
|
|
3
|
+
import type { OpenAICompatSpec } from './spec';
|
|
4
|
+
/**
|
|
5
|
+
* The single connector shared by every first-class OpenAI-compatible provider.
|
|
6
|
+
* It builds the `/chat/completions` request from the normalized `ChatInput`,
|
|
7
|
+
* parses the response (and SSE stream) into the normalized shapes, and maps
|
|
8
|
+
* vendor errors to `ConnectorError`. Provider differences are expressed by the
|
|
9
|
+
* `OpenAICompatSpec`, not by subclassing.
|
|
10
|
+
*/
|
|
11
|
+
export declare class OpenAICompatConnector extends BaseConnector implements IChatConnector {
|
|
12
|
+
readonly id: string;
|
|
13
|
+
private readonly baseUrl;
|
|
14
|
+
private readonly spec;
|
|
15
|
+
private readonly config;
|
|
16
|
+
constructor(spec: OpenAICompatSpec, config: OpenAICompatConfig);
|
|
17
|
+
complete(input: ChatInput): Promise<ChatResult>;
|
|
18
|
+
stream(input: ChatInput): AsyncGenerator<ChatStreamDelta>;
|
|
19
|
+
private buildRequest;
|
|
20
|
+
private parseResult;
|
|
21
|
+
private mapStreamChunk;
|
|
22
|
+
private mapVendorError;
|
|
23
|
+
}
|