@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
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
__exportStar(require("./error.types"), exports);
|
|
18
|
+
__exportStar(require("./passthrough.type"), exports);
|
|
19
|
+
__exportStar(require("./chat.types"), exports);
|
|
20
|
+
__exportStar(require("./embeddings.types"), exports);
|
|
21
|
+
__exportStar(require("./provider-id.enum"), exports);
|
|
22
|
+
__exportStar(require("./config-map.type"), exports);
|
|
23
|
+
__exportStar(require("./provider.interface"), exports);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface Passthrough {
|
|
2
|
+
/**
|
|
3
|
+
* Merged into the outbound request JSON body; overrides connector-built fields
|
|
4
|
+
* on key collision. Never transformed or validated — if you populate this from
|
|
5
|
+
* untrusted input, sanitize it yourself (you own the keys that reach the wire).
|
|
6
|
+
*/
|
|
7
|
+
body?: Record<string, unknown>;
|
|
8
|
+
/** Merged into request headers. */
|
|
9
|
+
headers?: Record<string, string>;
|
|
10
|
+
/** Appended to the request URL query string. */
|
|
11
|
+
query?: Record<string, string>;
|
|
12
|
+
}
|
|
13
|
+
export type WithPassthrough<T> = T & {
|
|
14
|
+
_passthrough?: Passthrough;
|
|
15
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider ids implemented so far — the 15 first-class OpenAI-compatible
|
|
3
|
+
* providers (one shared connector) plus native-adapter providers as they land
|
|
4
|
+
* (`anthropic`). Remaining natives (bedrock, gemini, vertex, cohere) are added
|
|
5
|
+
* when their adapters ship.
|
|
6
|
+
*/
|
|
7
|
+
export declare const LLM_PROVIDER_IDS: readonly ["openai", "azure-openai", "openrouter", "groq", "together", "fireworks", "deepseek", "xai", "mistral", "perplexity", "deepinfra", "cloudflare", "vllm", "ollama", "lmstudio", "anthropic", "bedrock", "gemini"];
|
|
8
|
+
export type LlmProviderId = (typeof LLM_PROVIDER_IDS)[number];
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LLM_PROVIDER_IDS = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Provider ids implemented so far — the 15 first-class OpenAI-compatible
|
|
6
|
+
* providers (one shared connector) plus native-adapter providers as they land
|
|
7
|
+
* (`anthropic`). Remaining natives (bedrock, gemini, vertex, cohere) are added
|
|
8
|
+
* when their adapters ship.
|
|
9
|
+
*/
|
|
10
|
+
exports.LLM_PROVIDER_IDS = [
|
|
11
|
+
'openai',
|
|
12
|
+
'azure-openai',
|
|
13
|
+
'openrouter',
|
|
14
|
+
'groq',
|
|
15
|
+
'together',
|
|
16
|
+
'fireworks',
|
|
17
|
+
'deepseek',
|
|
18
|
+
'xai',
|
|
19
|
+
'mistral',
|
|
20
|
+
'perplexity',
|
|
21
|
+
'deepinfra',
|
|
22
|
+
'cloudflare',
|
|
23
|
+
'vllm',
|
|
24
|
+
'ollama',
|
|
25
|
+
'lmstudio',
|
|
26
|
+
'anthropic',
|
|
27
|
+
'bedrock',
|
|
28
|
+
'gemini',
|
|
29
|
+
];
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ChatInput, ChatResult, ChatStreamDelta } from './chat.types';
|
|
2
|
+
/** The contract every chat connector satisfies — shared-compat or native adapter. */
|
|
3
|
+
export interface IChatConnector {
|
|
4
|
+
readonly id: string;
|
|
5
|
+
complete(input: ChatInput): Promise<ChatResult>;
|
|
6
|
+
stream(input: ChatInput): AsyncIterable<ChatStreamDelta>;
|
|
7
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { ConnectorError } from '../types/index.js';
|
|
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,209 @@
|
|
|
1
|
+
import { ConnectorError } from '../types/index.js';
|
|
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 class BaseConnector {
|
|
8
|
+
constructor(fetchImpl) {
|
|
9
|
+
const baseFetch = fetchImpl ?? globalThis.fetch;
|
|
10
|
+
// Force `redirect: 'error'` on every request at the transport seam so a 3xx
|
|
11
|
+
// can never silently re-send credentials to the redirect target. `fetch`
|
|
12
|
+
// strips only `Authorization`/`Cookie` cross-origin, so provider key headers
|
|
13
|
+
// (x-api-key, x-goog-api-key, api-key, …) would otherwise leak. No LLM
|
|
14
|
+
// endpoint legitimately redirects a POST.
|
|
15
|
+
this.fetchImpl = ((input, init) => baseFetch(input, { ...init, redirect: 'error' }));
|
|
16
|
+
}
|
|
17
|
+
async sendPostJson(url, body, headers, signal) {
|
|
18
|
+
return this.invokeFetch(url, {
|
|
19
|
+
method: 'POST',
|
|
20
|
+
headers: { 'Content-Type': 'application/json', ...headers },
|
|
21
|
+
body: JSON.stringify(body),
|
|
22
|
+
signal,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
async invokeFetch(url, init) {
|
|
26
|
+
try {
|
|
27
|
+
return await this.fetchImpl(url, init);
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
throw transportErrorFrom(err);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Guard a decoded 2xx body: a `null` here means the body was empty or not
|
|
35
|
+
* JSON (a proxy/captive-portal HTML page, a truncated read). Parsing that into
|
|
36
|
+
* an empty "successful" result is silent data loss, so raise instead.
|
|
37
|
+
*/
|
|
38
|
+
requireDecodedBody(json, status) {
|
|
39
|
+
if (json === null) {
|
|
40
|
+
const message = 'Provider returned a successful status with an empty or non-JSON body';
|
|
41
|
+
throw new ConnectorError({
|
|
42
|
+
message,
|
|
43
|
+
statusCode: status,
|
|
44
|
+
providerCode: 'provider_unavailable',
|
|
45
|
+
providerMessage: message,
|
|
46
|
+
cause: null,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return json;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Map a caught transport-layer failure to a `ConnectorError`. Shared by the
|
|
54
|
+
* request path (`invokeFetch`) and the streaming read loop (`parseSSEStream`) so
|
|
55
|
+
* a mid-stream TCP drop / abort surfaces with the same taxonomy as a failure
|
|
56
|
+
* before the first byte — never a raw `TypeError`/`DOMException`.
|
|
57
|
+
*
|
|
58
|
+
* A manual `AbortController.abort()` raises an `AbortError` (consumer cancelled
|
|
59
|
+
* → `invalid_request`); `AbortSignal.timeout()` raises a distinct `TimeoutError`
|
|
60
|
+
* (the provider failed to respond in time → `provider_unavailable`). Any other
|
|
61
|
+
* rejection (network reset, DNS) is `provider_unavailable`.
|
|
62
|
+
*/
|
|
63
|
+
export function transportErrorFrom(err) {
|
|
64
|
+
if (err instanceof ConnectorError)
|
|
65
|
+
return err;
|
|
66
|
+
const name = err?.name;
|
|
67
|
+
if (name === 'AbortError') {
|
|
68
|
+
return new ConnectorError({
|
|
69
|
+
message: err.message || 'Request cancelled',
|
|
70
|
+
statusCode: null,
|
|
71
|
+
providerCode: 'invalid_request',
|
|
72
|
+
cause: { raw: err },
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return new ConnectorError({
|
|
76
|
+
message: err?.message || 'Network error',
|
|
77
|
+
statusCode: null,
|
|
78
|
+
providerCode: 'provider_unavailable',
|
|
79
|
+
cause: { raw: err },
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Append `_passthrough.query` params to a request URL. EVERY request path must
|
|
84
|
+
* honor this — not just OpenAI-compat chat — because some providers require a
|
|
85
|
+
* query param the facade doesn't model (e.g. Azure OpenAI's `api-version`).
|
|
86
|
+
* Preserves any query string the URL already carries.
|
|
87
|
+
*/
|
|
88
|
+
export function appendPassthroughQuery(url, query) {
|
|
89
|
+
if (!query || Object.keys(query).length === 0)
|
|
90
|
+
return url;
|
|
91
|
+
const sep = url.includes('?') ? '&' : '?';
|
|
92
|
+
return `${url}${sep}${new URLSearchParams(query).toString()}`;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Upper bound on the residual (not-yet-terminated) SSE buffer. A stream that
|
|
96
|
+
* never emits an event boundary would otherwise grow `buffer` without limit
|
|
97
|
+
* (memory-exhaustion DoS from a malicious/broken upstream). Generous enough for
|
|
98
|
+
* any single legitimate LLM event; complete events are drained before this is
|
|
99
|
+
* checked, so many-small-event streams never approach it.
|
|
100
|
+
*/
|
|
101
|
+
const MAX_SSE_EVENT_CHARS = 16 * 1024 * 1024;
|
|
102
|
+
/**
|
|
103
|
+
* Parse a Server-Sent-Events response body into a sequence of JSON-decoded
|
|
104
|
+
* `data:` payloads. Skips comments/blank lines, stops at the `[DONE]` sentinel,
|
|
105
|
+
* and ignores any `data:` line that isn't valid JSON. Transport-agnostic over
|
|
106
|
+
* `\n\n` and `\r\n\r\n` event boundaries.
|
|
107
|
+
*/
|
|
108
|
+
export async function* parseSSEStream(response) {
|
|
109
|
+
const body = response.body;
|
|
110
|
+
if (!body)
|
|
111
|
+
return;
|
|
112
|
+
const reader = body.getReader();
|
|
113
|
+
const decoder = new TextDecoder();
|
|
114
|
+
let buffer = '';
|
|
115
|
+
let drained = false;
|
|
116
|
+
try {
|
|
117
|
+
for (;;) {
|
|
118
|
+
let done;
|
|
119
|
+
let value;
|
|
120
|
+
try {
|
|
121
|
+
({ done, value } = await reader.read());
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
// A mid-stream read rejection (TCP reset, abort, undici error) must
|
|
125
|
+
// surface with the ConnectorError taxonomy, not as a raw DOMException —
|
|
126
|
+
// streams are the failures consumers are least able to catch generically.
|
|
127
|
+
throw transportErrorFrom(err);
|
|
128
|
+
}
|
|
129
|
+
if (done)
|
|
130
|
+
break;
|
|
131
|
+
buffer += decoder.decode(value, { stream: true });
|
|
132
|
+
let boundary;
|
|
133
|
+
while ((boundary = indexOfEventBoundary(buffer)) !== -1) {
|
|
134
|
+
const rawEvent = buffer.slice(0, boundary);
|
|
135
|
+
buffer = buffer.slice(boundary).replace(/^(?:\r\n|\r|\n)+/, '');
|
|
136
|
+
const payload = extractData(rawEvent);
|
|
137
|
+
if (payload === null)
|
|
138
|
+
continue;
|
|
139
|
+
if (payload === '[DONE]') {
|
|
140
|
+
// Leave `drained = false` so the `finally` cancels the reader. The
|
|
141
|
+
// sentinel does NOT mean the body is fully consumed — every
|
|
142
|
+
// OpenAI-compat stream ends here, and without a cancel undici can't
|
|
143
|
+
// release the socket (leak on every successful stream).
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const parsed = tryParseJson(payload);
|
|
147
|
+
if (parsed !== undefined)
|
|
148
|
+
yield parsed;
|
|
149
|
+
}
|
|
150
|
+
if (buffer.length > MAX_SSE_EVENT_CHARS) {
|
|
151
|
+
throw new ConnectorError({
|
|
152
|
+
message: `SSE event exceeded ${MAX_SSE_EVENT_CHARS} characters without a boundary`,
|
|
153
|
+
statusCode: null,
|
|
154
|
+
providerCode: 'provider_unavailable',
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const tail = buffer.trim();
|
|
159
|
+
if (tail) {
|
|
160
|
+
const payload = extractData(tail);
|
|
161
|
+
if (payload !== null && payload !== '[DONE]') {
|
|
162
|
+
const parsed = tryParseJson(payload);
|
|
163
|
+
if (parsed !== undefined)
|
|
164
|
+
yield parsed;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
drained = true;
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
// If the consumer broke out early (or we threw), the body is not fully read —
|
|
171
|
+
// cancel it so the underlying connection is released instead of leaking.
|
|
172
|
+
if (!drained) {
|
|
173
|
+
try {
|
|
174
|
+
await reader.cancel();
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
/* best-effort */
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
reader.releaseLock();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// An SSE event boundary is a blank line: two consecutive line terminators. The
|
|
184
|
+
// spec permits CRLF, CR, or LF as a terminator, so accept any pairing
|
|
185
|
+
// (`\n\n`, `\r\n\r\n`, `\r\r`, and mixed `\n\r\n` / `\r\n\n`) rather than only
|
|
186
|
+
// the two common forms — a CR-only or mixed upstream would otherwise never
|
|
187
|
+
// split and buffer until MAX_SSE_EVENT_CHARS.
|
|
188
|
+
const EVENT_BOUNDARY_RE = /(?:\r\n|\r|\n)(?:\r\n|\r|\n)/;
|
|
189
|
+
function indexOfEventBoundary(buffer) {
|
|
190
|
+
const m = EVENT_BOUNDARY_RE.exec(buffer);
|
|
191
|
+
return m ? m.index : -1;
|
|
192
|
+
}
|
|
193
|
+
function extractData(rawEvent) {
|
|
194
|
+
const dataParts = [];
|
|
195
|
+
for (const line of rawEvent.split(/\r\n|\r|\n/)) {
|
|
196
|
+
if (line.startsWith('data:')) {
|
|
197
|
+
dataParts.push(line.slice(5).replace(/^ /, ''));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return dataParts.length === 0 ? null : dataParts.join('\n');
|
|
201
|
+
}
|
|
202
|
+
function tryParseJson(text) {
|
|
203
|
+
try {
|
|
204
|
+
return JSON.parse(text);
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { LlmProviderId, ChatInput, ChatResult, ChatStreamDelta, IChatConnector, ProviderConfigMap } from '../types/index.js';
|
|
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,59 @@
|
|
|
1
|
+
import { OpenAICompatConnector } from '../providers/_shared/openai-compat.connector.js';
|
|
2
|
+
import { SPECS } from '../providers/_shared/spec.js';
|
|
3
|
+
import { AnthropicConnector } from '../providers/anthropic/index.js';
|
|
4
|
+
import { BedrockConnector } from '../providers/bedrock/index.js';
|
|
5
|
+
import { GeminiConnector } from '../providers/gemini/index.js';
|
|
6
|
+
import { ConnectorError } from '../types/index.js';
|
|
7
|
+
/**
|
|
8
|
+
* The chat-completion facade. Construct by provider id + config — the facade
|
|
9
|
+
* dispatches to the shared OpenAI-compatible connector (native adapters slot in
|
|
10
|
+
* here as they land) — or pass a connector instance directly for advanced use.
|
|
11
|
+
* Switching vendor is a change of provider id + `model` only; `ChatInput` /
|
|
12
|
+
* `ChatResult` are identical across providers.
|
|
13
|
+
*/
|
|
14
|
+
export class Chat {
|
|
15
|
+
constructor(arg, config) {
|
|
16
|
+
if (typeof arg === 'object' && arg !== null) {
|
|
17
|
+
this.connector = arg;
|
|
18
|
+
this.id = arg.id;
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const providerId = arg;
|
|
22
|
+
this.id = providerId;
|
|
23
|
+
if (!config) {
|
|
24
|
+
throw new ConnectorError({
|
|
25
|
+
message: 'Chat facade requires `config` when constructed with a provider id',
|
|
26
|
+
statusCode: null,
|
|
27
|
+
providerCode: 'invalid_request',
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
// Native adapters — structurally different wire, same normalized surface.
|
|
31
|
+
if (providerId === 'anthropic') {
|
|
32
|
+
this.connector = new AnthropicConnector(config);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (providerId === 'bedrock') {
|
|
36
|
+
this.connector = new BedrockConnector(config);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (providerId === 'gemini') {
|
|
40
|
+
this.connector = new GeminiConnector(config);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const spec = SPECS[providerId];
|
|
44
|
+
if (!spec) {
|
|
45
|
+
throw new ConnectorError({
|
|
46
|
+
message: `Unsupported LLM provider: ${String(providerId)}`,
|
|
47
|
+
statusCode: null,
|
|
48
|
+
providerCode: 'invalid_request',
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
this.connector = new OpenAICompatConnector(spec, config);
|
|
52
|
+
}
|
|
53
|
+
complete(input) {
|
|
54
|
+
return this.connector.complete(input);
|
|
55
|
+
}
|
|
56
|
+
stream(input) {
|
|
57
|
+
return this.connector.stream(input);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { EmbeddingsInput, EmbeddingsResult, IEmbeddingsConnector, OpenAICompatConfig } from '../types/index.js';
|
|
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,59 @@
|
|
|
1
|
+
import { OpenAICompatEmbeddingsConnector } from '../providers/_shared/openai-compat.embeddings.connector.js';
|
|
2
|
+
import { SPECS } from '../providers/_shared/spec.js';
|
|
3
|
+
import { ConnectorError } from '../types/index.js';
|
|
4
|
+
/**
|
|
5
|
+
* First-class providers that expose an OpenAI-float-shaped `/embeddings`
|
|
6
|
+
* surface (FR-AI-E1). Deliberately excludes: providers with no native
|
|
7
|
+
* embeddings (deepseek, xai), quantized/non-float shapes (perplexity int8),
|
|
8
|
+
* unstable coverage (groq), and the native-adapter providers (Anthropic →
|
|
9
|
+
* Voyage; Bedrock/Gemini native embeddings have a different shape — a future
|
|
10
|
+
* per-connector embeddings surface).
|
|
11
|
+
*/
|
|
12
|
+
export const EMBEDDINGS_PROVIDER_IDS = [
|
|
13
|
+
'openai',
|
|
14
|
+
'azure-openai',
|
|
15
|
+
'openrouter',
|
|
16
|
+
'together',
|
|
17
|
+
'fireworks',
|
|
18
|
+
'mistral',
|
|
19
|
+
'deepinfra',
|
|
20
|
+
'cloudflare',
|
|
21
|
+
'vllm',
|
|
22
|
+
'ollama',
|
|
23
|
+
'lmstudio',
|
|
24
|
+
];
|
|
25
|
+
const SUPPORTED = new Set(EMBEDDINGS_PROVIDER_IDS);
|
|
26
|
+
/**
|
|
27
|
+
* The embeddings facade — a separate operation from `Chat`. Construct by
|
|
28
|
+
* provider id + config, or with a connector instance. Only the OpenAI-float
|
|
29
|
+
* subset is accepted (type-restricted); other ids throw `ConnectorError`.
|
|
30
|
+
*/
|
|
31
|
+
export class Embeddings {
|
|
32
|
+
constructor(arg, config) {
|
|
33
|
+
if (typeof arg === 'object' && arg !== null) {
|
|
34
|
+
this.connector = arg;
|
|
35
|
+
this.id = arg.id;
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const providerId = arg;
|
|
39
|
+
this.id = providerId;
|
|
40
|
+
if (!SUPPORTED.has(providerId)) {
|
|
41
|
+
throw new ConnectorError({
|
|
42
|
+
message: `Provider '${String(providerId)}' has no OpenAI-compatible embeddings surface`,
|
|
43
|
+
statusCode: null,
|
|
44
|
+
providerCode: 'invalid_request',
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
if (!config) {
|
|
48
|
+
throw new ConnectorError({
|
|
49
|
+
message: 'Embeddings facade requires `config` when constructed with a provider id',
|
|
50
|
+
statusCode: null,
|
|
51
|
+
providerCode: 'invalid_request',
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
this.connector = new OpenAICompatEmbeddingsConnector(SPECS[providerId], config);
|
|
55
|
+
}
|
|
56
|
+
create(input) {
|
|
57
|
+
return this.connector.create(input);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { Chat } from './facades/chat.facade.js';
|
|
2
|
+
export type { ChatConfig } from './facades/chat.facade.js';
|
|
3
|
+
export { Embeddings, EMBEDDINGS_PROVIDER_IDS } from './facades/embeddings.facade.js';
|
|
4
|
+
export type { EmbeddingsProviderId } from './facades/embeddings.facade.js';
|
|
5
|
+
export { OpenAICompatConnector } from './providers/_shared/openai-compat.connector.js';
|
|
6
|
+
export { OpenAICompatEmbeddingsConnector } from './providers/_shared/openai-compat.embeddings.connector.js';
|
|
7
|
+
export { SPECS } from './providers/_shared/spec.js';
|
|
8
|
+
export type { OpenAICompatSpec, SpecId } from './providers/_shared/spec.js';
|
|
9
|
+
export { AnthropicConnector } from './providers/anthropic/index.js';
|
|
10
|
+
export type { AnthropicConfig } from './providers/anthropic/index.js';
|
|
11
|
+
export { BedrockConnector } from './providers/bedrock/index.js';
|
|
12
|
+
export type { BedrockConfig } from './providers/bedrock/index.js';
|
|
13
|
+
export { GeminiConnector } from './providers/gemini/index.js';
|
|
14
|
+
export type { GeminiConfig } from './providers/gemini/index.js';
|
|
15
|
+
export * from './types/index.js';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { Chat } from './facades/chat.facade.js';
|
|
2
|
+
export { Embeddings, EMBEDDINGS_PROVIDER_IDS } from './facades/embeddings.facade.js';
|
|
3
|
+
export { OpenAICompatConnector } from './providers/_shared/openai-compat.connector.js';
|
|
4
|
+
export { OpenAICompatEmbeddingsConnector } from './providers/_shared/openai-compat.embeddings.connector.js';
|
|
5
|
+
export { SPECS } from './providers/_shared/spec.js';
|
|
6
|
+
export { AnthropicConnector } from './providers/anthropic/index.js';
|
|
7
|
+
export { BedrockConnector } from './providers/bedrock/index.js';
|
|
8
|
+
export { GeminiConnector } from './providers/gemini/index.js';
|
|
9
|
+
export * from './types/index.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"module"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { BaseConnector } from '../../base/base.connector.js';
|
|
2
|
+
import type { ChatInput, ChatResult, ChatStreamDelta, IChatConnector, OpenAICompatConfig } from '../../types/index.js';
|
|
3
|
+
import type { OpenAICompatSpec } from './spec.js';
|
|
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
|
+
}
|