@seanhogg/builderforce-memory 2026.9.30 → 2026.9.31
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/dist/bridges/FetchBridge.d.ts +12 -5
- package/dist/bridges/FetchBridge.d.ts.map +1 -1
- package/dist/bridges/FetchBridge.js +6 -4
- package/dist/bridges/FetchBridge.js.map +1 -1
- package/dist/bridges/OpenAIBridge.d.ts +17 -4
- package/dist/bridges/OpenAIBridge.d.ts.map +1 -1
- package/dist/bridges/OpenAIBridge.js +68 -57
- package/dist/bridges/OpenAIBridge.js.map +1 -1
- package/dist/bridges/TransformerBridge.d.ts +6 -0
- package/dist/bridges/TransformerBridge.d.ts.map +1 -1
- package/dist/wire/chatClient.d.ts +100 -0
- package/dist/wire/chatClient.d.ts.map +1 -0
- package/dist/wire/chatClient.js +123 -0
- package/dist/wire/chatClient.js.map +1 -0
- package/dist/wire/index.d.ts +3 -1
- package/dist/wire/index.d.ts.map +1 -1
- package/dist/wire/index.js +2 -1
- package/dist/wire/index.js.map +1 -1
- package/dist/wire/sse.d.ts +17 -9
- package/dist/wire/sse.d.ts.map +1 -1
- package/dist/wire/sse.js +45 -22
- package/dist/wire/sse.js.map +1 -1
- package/package.json +3 -3
- package/src/bridges/FetchBridge.ts +15 -6
- package/src/bridges/OpenAIBridge.ts +82 -72
- package/src/bridges/TransformerBridge.ts +6 -0
- package/src/wire/chatClient.ts +196 -0
- package/src/wire/index.ts +3 -1
- package/src/wire/sse.ts +43 -21
package/dist/wire/sse.d.ts
CHANGED
|
@@ -2,28 +2,36 @@
|
|
|
2
2
|
* Server-Sent-Events `data:` framing.
|
|
3
3
|
*
|
|
4
4
|
* Every consumer of an SSE LLM stream does the same line-level dance: trim the line,
|
|
5
|
-
* keep only `data:` frames,
|
|
6
|
-
* anything
|
|
7
|
-
*
|
|
5
|
+
* keep only `data:` frames, and stop at the `[DONE]` sentinel, which ends an
|
|
6
|
+
* OpenAI-style stream (anything after it is not part of the response). JSON streams
|
|
7
|
+
* then parse each payload and skip empty or malformed ones.
|
|
8
8
|
*
|
|
9
9
|
* The line rule is `slice(5).trim()`, which accepts both `data: {…}` and the
|
|
10
10
|
* spaceless `data:{…}` some providers emit — `slice(6)` silently drops the latter.
|
|
11
11
|
*
|
|
12
|
+
* Two levels, one rule: {@link readSseDataPayloads} yields payload TEXT (for callers
|
|
13
|
+
* whose frames are not all JSON, or that parse per wire shape), and
|
|
14
|
+
* {@link readSseDataFrames} yields parsed JSON on top of it.
|
|
15
|
+
*
|
|
12
16
|
* Pure and defensive: a malformed frame is skipped, never thrown.
|
|
13
17
|
*/
|
|
18
|
+
/** The payload text of a `data:` line, or `undefined` for any other line. */
|
|
19
|
+
export declare function sseDataPayload(line: string): string | undefined;
|
|
20
|
+
/** True for the `data: [DONE]` line that ends an OpenAI-style stream. */
|
|
21
|
+
export declare function isSseDoneLine(line: string): boolean;
|
|
14
22
|
/**
|
|
15
23
|
* One SSE line's decoded JSON payload, or `undefined` when the line is not a usable
|
|
16
24
|
* data frame (not a `data:` line, the `[DONE]` sentinel, empty, or malformed JSON).
|
|
17
25
|
*/
|
|
18
26
|
export declare function parseSseDataLine(line: string): unknown | undefined;
|
|
19
|
-
/** True for the `data: [DONE]` line that ends an OpenAI-style stream. */
|
|
20
|
-
export declare function isSseDoneLine(line: string): boolean;
|
|
21
27
|
/** Every parsed `data:` frame of a whole, already-buffered SSE body, up to `[DONE]`. */
|
|
22
28
|
export declare function parseSseDataFrames(raw: string): Generator<unknown>;
|
|
23
29
|
/**
|
|
24
|
-
* Every
|
|
25
|
-
* A
|
|
26
|
-
* released however iteration ends.
|
|
30
|
+
* Every `data:` payload of a live SSE byte stream, as text, as it arrives, up to
|
|
31
|
+
* `[DONE]`. A line split across chunks is held until its newline arrives; the reader
|
|
32
|
+
* lock is released however iteration ends. A missing body yields nothing.
|
|
27
33
|
*/
|
|
28
|
-
export declare function
|
|
34
|
+
export declare function readSseDataPayloads(body: ReadableStream<Uint8Array> | null | undefined): AsyncGenerator<string>;
|
|
35
|
+
/** Every parsed JSON `data:` frame of a live SSE byte stream, up to `[DONE]`. */
|
|
36
|
+
export declare function readSseDataFrames(body: ReadableStream<Uint8Array> | null | undefined): AsyncGenerator<unknown>;
|
|
29
37
|
//# sourceMappingURL=sse.d.ts.map
|
package/dist/wire/sse.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sse.d.ts","sourceRoot":"","sources":["../../src/wire/sse.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"sse.d.ts","sourceRoot":"","sources":["../../src/wire/sse.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,6EAA6E;AAC7E,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAI/D;AAED,yEAAyE;AACzE,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEnD;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,CAQlE;AAED,wFAAwF;AACxF,wBAAiB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAC,OAAO,CAAC,CAMnE;AAED;;;;GAIG;AACH,wBAAuB,mBAAmB,CAAC,IAAI,EAAE,cAAc,CAAC,UAAU,CAAC,GAAG,IAAI,GAAG,SAAS,GAAG,cAAc,CAAC,MAAM,CAAC,CAyBtH;AAED,iFAAiF;AACjF,wBAAuB,iBAAiB,CAAC,IAAI,EAAE,cAAc,CAAC,UAAU,CAAC,GAAG,IAAI,GAAG,SAAS,GAAG,cAAc,CAAC,OAAO,CAAC,CASrH"}
|
package/dist/wire/sse.js
CHANGED
|
@@ -2,24 +2,36 @@
|
|
|
2
2
|
* Server-Sent-Events `data:` framing.
|
|
3
3
|
*
|
|
4
4
|
* Every consumer of an SSE LLM stream does the same line-level dance: trim the line,
|
|
5
|
-
* keep only `data:` frames,
|
|
6
|
-
* anything
|
|
7
|
-
*
|
|
5
|
+
* keep only `data:` frames, and stop at the `[DONE]` sentinel, which ends an
|
|
6
|
+
* OpenAI-style stream (anything after it is not part of the response). JSON streams
|
|
7
|
+
* then parse each payload and skip empty or malformed ones.
|
|
8
8
|
*
|
|
9
9
|
* The line rule is `slice(5).trim()`, which accepts both `data: {…}` and the
|
|
10
10
|
* spaceless `data:{…}` some providers emit — `slice(6)` silently drops the latter.
|
|
11
11
|
*
|
|
12
|
+
* Two levels, one rule: {@link readSseDataPayloads} yields payload TEXT (for callers
|
|
13
|
+
* whose frames are not all JSON, or that parse per wire shape), and
|
|
14
|
+
* {@link readSseDataFrames} yields parsed JSON on top of it.
|
|
15
|
+
*
|
|
12
16
|
* Pure and defensive: a malformed frame is skipped, never thrown.
|
|
13
17
|
*/
|
|
18
|
+
/** The payload text of a `data:` line, or `undefined` for any other line. */
|
|
19
|
+
export function sseDataPayload(line) {
|
|
20
|
+
const trimmed = line.trim();
|
|
21
|
+
if (!trimmed.startsWith('data:'))
|
|
22
|
+
return undefined;
|
|
23
|
+
return trimmed.slice(5).trim();
|
|
24
|
+
}
|
|
25
|
+
/** True for the `data: [DONE]` line that ends an OpenAI-style stream. */
|
|
26
|
+
export function isSseDoneLine(line) {
|
|
27
|
+
return sseDataPayload(line) === '[DONE]';
|
|
28
|
+
}
|
|
14
29
|
/**
|
|
15
30
|
* One SSE line's decoded JSON payload, or `undefined` when the line is not a usable
|
|
16
31
|
* data frame (not a `data:` line, the `[DONE]` sentinel, empty, or malformed JSON).
|
|
17
32
|
*/
|
|
18
33
|
export function parseSseDataLine(line) {
|
|
19
|
-
const
|
|
20
|
-
if (!trimmed.startsWith('data:'))
|
|
21
|
-
return undefined;
|
|
22
|
-
const data = trimmed.slice(5).trim();
|
|
34
|
+
const data = sseDataPayload(line);
|
|
23
35
|
if (!data || data === '[DONE]')
|
|
24
36
|
return undefined;
|
|
25
37
|
try {
|
|
@@ -29,11 +41,6 @@ export function parseSseDataLine(line) {
|
|
|
29
41
|
return undefined;
|
|
30
42
|
}
|
|
31
43
|
}
|
|
32
|
-
/** True for the `data: [DONE]` line that ends an OpenAI-style stream. */
|
|
33
|
-
export function isSseDoneLine(line) {
|
|
34
|
-
const trimmed = line.trim();
|
|
35
|
-
return trimmed.startsWith('data:') && trimmed.slice(5).trim() === '[DONE]';
|
|
36
|
-
}
|
|
37
44
|
/** Every parsed `data:` frame of a whole, already-buffered SSE body, up to `[DONE]`. */
|
|
38
45
|
export function* parseSseDataFrames(raw) {
|
|
39
46
|
for (const line of raw.split('\n')) {
|
|
@@ -45,11 +52,13 @@ export function* parseSseDataFrames(raw) {
|
|
|
45
52
|
}
|
|
46
53
|
}
|
|
47
54
|
/**
|
|
48
|
-
* Every
|
|
49
|
-
* A
|
|
50
|
-
* released however iteration ends.
|
|
55
|
+
* Every `data:` payload of a live SSE byte stream, as text, as it arrives, up to
|
|
56
|
+
* `[DONE]`. A line split across chunks is held until its newline arrives; the reader
|
|
57
|
+
* lock is released however iteration ends. A missing body yields nothing.
|
|
51
58
|
*/
|
|
52
|
-
export async function*
|
|
59
|
+
export async function* readSseDataPayloads(body) {
|
|
60
|
+
if (!body)
|
|
61
|
+
return;
|
|
53
62
|
const reader = body.getReader();
|
|
54
63
|
const decoder = new TextDecoder();
|
|
55
64
|
let buffer = '';
|
|
@@ -62,20 +71,34 @@ export async function* readSseDataFrames(body) {
|
|
|
62
71
|
const lines = buffer.split('\n');
|
|
63
72
|
buffer = lines.pop(); // split() always yields ≥1 element
|
|
64
73
|
for (const line of lines) {
|
|
65
|
-
|
|
74
|
+
const data = sseDataPayload(line);
|
|
75
|
+
if (data === undefined)
|
|
76
|
+
continue;
|
|
77
|
+
if (data === '[DONE]')
|
|
66
78
|
return;
|
|
67
|
-
|
|
68
|
-
if (parsed !== undefined)
|
|
69
|
-
yield parsed;
|
|
79
|
+
yield data;
|
|
70
80
|
}
|
|
71
81
|
}
|
|
72
82
|
buffer += decoder.decode();
|
|
73
|
-
const tail =
|
|
74
|
-
if (tail !== undefined)
|
|
83
|
+
const tail = sseDataPayload(buffer);
|
|
84
|
+
if (tail !== undefined && tail !== '[DONE]')
|
|
75
85
|
yield tail;
|
|
76
86
|
}
|
|
77
87
|
finally {
|
|
78
88
|
reader.releaseLock();
|
|
79
89
|
}
|
|
80
90
|
}
|
|
91
|
+
/** Every parsed JSON `data:` frame of a live SSE byte stream, up to `[DONE]`. */
|
|
92
|
+
export async function* readSseDataFrames(body) {
|
|
93
|
+
for await (const data of readSseDataPayloads(body)) {
|
|
94
|
+
if (!data)
|
|
95
|
+
continue;
|
|
96
|
+
try {
|
|
97
|
+
yield JSON.parse(data);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// Skip malformed SSE frames rather than failing the stream.
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
81
104
|
//# sourceMappingURL=sse.js.map
|
package/dist/wire/sse.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sse.js","sourceRoot":"","sources":["../../src/wire/sse.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"sse.js","sourceRoot":"","sources":["../../src/wire/sse.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seanhogg/builderforce-memory",
|
|
3
|
-
"version": "2026.9.
|
|
3
|
+
"version": "2026.9.31",
|
|
4
4
|
"description": "BuilderForce Agent Memory — runtime layer. SSM execution, Transformer orchestration, online distillation, enterprise RAG (ingestion pipelines, vector-store adapters, tenant/ACL-scoped hybrid retrieval), multi-agent graph orchestration, LLM telemetry and cost accounting, and persistent agent memory for BuilderForce.ai agents.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.js",
|
|
@@ -95,7 +95,7 @@
|
|
|
95
95
|
},
|
|
96
96
|
"homepage": "https://github.com/SeanHogg/builderforce-memory/tree/main/packages/memory#readme",
|
|
97
97
|
"peerDependencies": {
|
|
98
|
-
"@seanhogg/builderforce-memory-engine": "^2026.9.
|
|
98
|
+
"@seanhogg/builderforce-memory-engine": "^2026.9.31"
|
|
99
99
|
},
|
|
100
100
|
"devDependencies": {
|
|
101
101
|
"@jest/globals": "^29.7.0",
|
|
@@ -109,7 +109,7 @@
|
|
|
109
109
|
"ts-jest": "^29.2.0",
|
|
110
110
|
"typescript": "^6.0.3",
|
|
111
111
|
"@typescript/native-preview": "7.0.0-dev.20260627.2",
|
|
112
|
-
"@seanhogg/builderforce-memory-engine": "2026.9.
|
|
112
|
+
"@seanhogg/builderforce-memory-engine": "2026.9.31"
|
|
113
113
|
},
|
|
114
114
|
"jest": {
|
|
115
115
|
"preset": "ts-jest/presets/default-esm",
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* FetchBridge – generic OpenAI-compatible bridge for local or hosted endpoints.
|
|
3
3
|
*
|
|
4
|
-
* Works with Ollama, LM Studio, vLLM, llama.cpp server, or any
|
|
5
|
-
* exposes a /chat/completions endpoint compatible with the OpenAI
|
|
6
|
-
* and response schema.
|
|
4
|
+
* Works with Ollama, LM Studio, vLLM, llama.cpp server, a routing gateway, or any
|
|
5
|
+
* service that exposes a /chat/completions endpoint compatible with the OpenAI
|
|
6
|
+
* request and response schema.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { OpenAIBridge } from './OpenAIBridge.js';
|
|
@@ -14,12 +14,19 @@ export interface FetchBridgeOptions {
|
|
|
14
14
|
baseUrl : string;
|
|
15
15
|
/** API key — many local servers require any non-empty string. Default: 'local'. */
|
|
16
16
|
apiKey? : string;
|
|
17
|
-
/** Model name understood by the server. Default: 'default'. */
|
|
17
|
+
/** Model name understood by the server. Default: 'default' (none with `serverDefaults`). */
|
|
18
18
|
model? : string;
|
|
19
19
|
/** Default system prompt. */
|
|
20
20
|
systemPrompt? : string;
|
|
21
|
-
/** Default max tokens. Default: 512. */
|
|
21
|
+
/** Default max tokens. Default: 512 (none with `serverDefaults`). */
|
|
22
22
|
maxTokens? : number;
|
|
23
|
+
/**
|
|
24
|
+
* Send only what a call states, so a routing gateway picks the model and the
|
|
25
|
+
* sampling settings. Default false.
|
|
26
|
+
*/
|
|
27
|
+
serverDefaults?: boolean;
|
|
28
|
+
/** Abort a request after this long. */
|
|
29
|
+
timeoutMs? : number;
|
|
23
30
|
}
|
|
24
31
|
|
|
25
32
|
/**
|
|
@@ -30,10 +37,12 @@ export class FetchBridge extends OpenAIBridge {
|
|
|
30
37
|
constructor(opts: FetchBridgeOptions) {
|
|
31
38
|
super({
|
|
32
39
|
apiKey : opts.apiKey ?? 'local',
|
|
33
|
-
model : opts.model ?? 'default',
|
|
40
|
+
model : opts.model ?? (opts.serverDefaults ? undefined : 'default'),
|
|
34
41
|
baseUrl : opts.baseUrl,
|
|
35
42
|
systemPrompt : opts.systemPrompt,
|
|
36
43
|
maxTokens : opts.maxTokens,
|
|
44
|
+
serverDefaults: opts.serverDefaults,
|
|
45
|
+
timeoutMs : opts.timeoutMs,
|
|
37
46
|
});
|
|
38
47
|
}
|
|
39
48
|
}
|
|
@@ -3,12 +3,25 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Supports both non-streaming and streaming (SSE) completions.
|
|
5
5
|
* Compatible with any OpenAI-compatible endpoint via the `baseUrl` option.
|
|
6
|
+
*
|
|
7
|
+
* The request, error mapping and stream reading are the `/wire` chat client's — the
|
|
8
|
+
* same one a host uses for tool-calling turns — so this class only supplies the
|
|
9
|
+
* bridge's defaults and maps usage onto {@link LlmUsage}.
|
|
6
10
|
*/
|
|
7
11
|
|
|
8
12
|
import { SSMError } from '../errors/SSMError.js';
|
|
9
13
|
import type { LlmUsage } from '../telemetry/types.js';
|
|
10
14
|
import type { TransformerBridge, BridgeGenerateOptions, BridgeCallInfo } from './TransformerBridge.js';
|
|
11
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
chatComplete,
|
|
17
|
+
chatStream,
|
|
18
|
+
ChatCompletionError,
|
|
19
|
+
readUsageFields,
|
|
20
|
+
type ChatClient,
|
|
21
|
+
type ChatMessage,
|
|
22
|
+
type ChatRequest,
|
|
23
|
+
type ChatStreamEvent,
|
|
24
|
+
} from '../wire/index.js';
|
|
12
25
|
|
|
13
26
|
export interface OpenAIBridgeOptions {
|
|
14
27
|
/** OpenAI API key (or compatible service key). */
|
|
@@ -21,6 +34,13 @@ export interface OpenAIBridgeOptions {
|
|
|
21
34
|
systemPrompt? : string;
|
|
22
35
|
/** Default max tokens. Default: 512. */
|
|
23
36
|
maxTokens? : number;
|
|
37
|
+
/**
|
|
38
|
+
* Send only what a call states — no model, max-tokens, temperature or top-p
|
|
39
|
+
* defaults — so a routing gateway picks them. Default false.
|
|
40
|
+
*/
|
|
41
|
+
serverDefaults?: boolean;
|
|
42
|
+
/** Abort a request after this long. */
|
|
43
|
+
timeoutMs? : number;
|
|
24
44
|
}
|
|
25
45
|
|
|
26
46
|
export class OpenAIBridge implements TransformerBridge {
|
|
@@ -28,18 +48,22 @@ export class OpenAIBridge implements TransformerBridge {
|
|
|
28
48
|
|
|
29
49
|
readonly supportsStreaming = true as const;
|
|
30
50
|
|
|
31
|
-
private readonly
|
|
32
|
-
private readonly _model : string;
|
|
33
|
-
private readonly _baseUrl : string;
|
|
51
|
+
private readonly _client : ChatClient;
|
|
52
|
+
private readonly _model : string | undefined;
|
|
34
53
|
private readonly _systemPrompt: string;
|
|
35
|
-
private readonly _maxTokens : number;
|
|
54
|
+
private readonly _maxTokens : number | undefined;
|
|
55
|
+
private readonly _serverDefaults: boolean;
|
|
36
56
|
|
|
37
57
|
constructor(opts: OpenAIBridgeOptions) {
|
|
38
|
-
this.
|
|
39
|
-
this.
|
|
40
|
-
|
|
58
|
+
this._serverDefaults = opts.serverDefaults ?? false;
|
|
59
|
+
this._client = {
|
|
60
|
+
apiKey: opts.apiKey,
|
|
61
|
+
baseUrl: opts.baseUrl ?? 'https://api.openai.com/v1',
|
|
62
|
+
...(opts.timeoutMs ? { timeoutMs: opts.timeoutMs } : {}),
|
|
63
|
+
};
|
|
64
|
+
this._model = opts.model ?? (this._serverDefaults ? undefined : 'gpt-4o-mini');
|
|
41
65
|
this._systemPrompt = opts.systemPrompt ?? '';
|
|
42
|
-
this._maxTokens = opts.maxTokens
|
|
66
|
+
this._maxTokens = opts.maxTokens ?? (this._serverDefaults ? undefined : 512);
|
|
43
67
|
}
|
|
44
68
|
|
|
45
69
|
/** Provider-reported usage for the last call — see {@link BridgeCallInfo}. */
|
|
@@ -48,88 +72,74 @@ export class OpenAIBridge implements TransformerBridge {
|
|
|
48
72
|
}
|
|
49
73
|
|
|
50
74
|
async generate(prompt: string, opts: BridgeGenerateOptions = {}): Promise<string> {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
throw new SSMError(
|
|
57
|
-
'BRIDGE_REQUEST_FAILED',
|
|
58
|
-
`OpenAI API returned ${res.status}: ${text}`,
|
|
59
|
-
);
|
|
75
|
+
let result;
|
|
76
|
+
try {
|
|
77
|
+
result = await chatComplete(this._client, this._request(prompt, opts));
|
|
78
|
+
} catch (err) {
|
|
79
|
+
throw this._requestError('OpenAI API', err);
|
|
60
80
|
}
|
|
61
|
-
|
|
62
|
-
const json = await res.json() as Record<string, unknown>;
|
|
63
|
-
const content = (json as any).choices?.[0]?.message?.content;
|
|
64
|
-
if (typeof content !== 'string') {
|
|
81
|
+
if (!result.hasTextContent) {
|
|
65
82
|
throw new SSMError('BRIDGE_RESPONSE_INVALID', 'Unexpected OpenAI response shape.');
|
|
66
83
|
}
|
|
67
84
|
this._lastCall = {
|
|
68
|
-
usage: readOpenAIUsage(
|
|
85
|
+
usage: readOpenAIUsage(result.usage, result.model ?? opts.model ?? this._model ?? ''),
|
|
69
86
|
};
|
|
70
|
-
return content;
|
|
87
|
+
return result.content;
|
|
71
88
|
}
|
|
72
89
|
|
|
73
90
|
async *stream(prompt: string, opts: BridgeGenerateOptions = {}): AsyncIterable<string> {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (!res.body) {
|
|
86
|
-
throw new SSMError('BRIDGE_RESPONSE_INVALID', 'OpenAI streaming response has no body.');
|
|
91
|
+
let events;
|
|
92
|
+
try {
|
|
93
|
+
events = chatStream(this._client, { ...this._request(prompt, opts), includeUsage: true });
|
|
94
|
+
// The request is sent on the first pull; surface a failed request as a bridge error.
|
|
95
|
+
const first = await events.next();
|
|
96
|
+
if (first.done) return;
|
|
97
|
+
yield* this._text(first.value);
|
|
98
|
+
} catch (err) {
|
|
99
|
+
throw this._requestError('OpenAI streaming API', err);
|
|
87
100
|
}
|
|
101
|
+
for await (const event of events) yield* this._text(event);
|
|
102
|
+
}
|
|
88
103
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const reported = readOpenAIUsage((chunk as any).usage, (chunk as any).model ?? model);
|
|
96
|
-
if (reported) usage = reported;
|
|
97
|
-
const delta = (chunk as any).choices?.[0]?.delta?.content;
|
|
98
|
-
if (typeof delta === 'string' && delta.length > 0) yield delta;
|
|
104
|
+
/** Text deltas out; the closing event records usage (sent because of `includeUsage`). */
|
|
105
|
+
private *_text(event: ChatStreamEvent): Generator<string> {
|
|
106
|
+
if (event.type === 'text-delta') yield event.delta;
|
|
107
|
+
else if (event.type === 'done') {
|
|
108
|
+
const usage = readOpenAIUsage(event.result.usage, event.result.model ?? this._model ?? '');
|
|
109
|
+
this._lastCall = usage ? { usage } : {};
|
|
99
110
|
}
|
|
100
|
-
|
|
101
|
-
this._lastCall = usage ? { usage } : {};
|
|
102
111
|
}
|
|
103
112
|
|
|
104
|
-
private
|
|
113
|
+
private _request(prompt: string, opts: BridgeGenerateOptions): ChatRequest {
|
|
105
114
|
const sys = opts.systemPrompt ?? this._systemPrompt;
|
|
106
|
-
const messages:
|
|
115
|
+
const messages: ChatMessage[] = [];
|
|
107
116
|
if (sys) messages.push({ role: 'system', content: sys });
|
|
108
117
|
messages.push({ role: 'user', content: prompt });
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
118
|
+
const d = !this._serverDefaults;
|
|
119
|
+
const model = opts.model ?? this._model;
|
|
120
|
+
const maxTokens = opts.maxTokens ?? this._maxTokens;
|
|
121
|
+
const temperature = opts.temperature ?? (d ? 0.7 : undefined);
|
|
122
|
+
const topP = opts.topP ?? (d ? 0.9 : undefined);
|
|
123
|
+
return {
|
|
112
124
|
messages,
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
// estimate. Non-streaming requests reject the field, hence the guard.
|
|
120
|
-
...(stream ? { stream_options: { include_usage: true } } : {}),
|
|
121
|
-
});
|
|
125
|
+
...(model ? { model } : {}),
|
|
126
|
+
...(maxTokens !== undefined ? { maxTokens } : {}),
|
|
127
|
+
...(temperature !== undefined ? { temperature } : {}),
|
|
128
|
+
...(topP !== undefined ? { topP } : {}),
|
|
129
|
+
...(opts.extra ? { extra: opts.extra } : {}),
|
|
130
|
+
};
|
|
122
131
|
}
|
|
123
132
|
|
|
124
|
-
private
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
'
|
|
130
|
-
}
|
|
131
|
-
body
|
|
132
|
-
}
|
|
133
|
+
private _requestError(what: string, err: unknown): SSMError {
|
|
134
|
+
if (err instanceof SSMError) return err;
|
|
135
|
+
if (err instanceof ChatCompletionError) {
|
|
136
|
+
// A 2xx that still failed is a stream with no body: the server answered, badly.
|
|
137
|
+
if (err.status >= 200 && err.status < 300) {
|
|
138
|
+
return new SSMError('BRIDGE_RESPONSE_INVALID', `${what} returned no response body.`);
|
|
139
|
+
}
|
|
140
|
+
return new SSMError('BRIDGE_REQUEST_FAILED', `${what} returned ${err.status}: ${err.body}`);
|
|
141
|
+
}
|
|
142
|
+
return new SSMError('BRIDGE_REQUEST_FAILED', `${what} request failed: ${err instanceof Error ? err.message : String(err)}`, err);
|
|
133
143
|
}
|
|
134
144
|
}
|
|
135
145
|
|
|
@@ -22,6 +22,12 @@ export interface BridgeGenerateOptions {
|
|
|
22
22
|
systemPrompt? : string;
|
|
23
23
|
/** Model string, overriding the adapter's default. */
|
|
24
24
|
model? : string;
|
|
25
|
+
/**
|
|
26
|
+
* Vendor extras merged into an OpenAI-compatible request body (a gateway's
|
|
27
|
+
* routing `provider`, `response_format`, …). Bridges that do not speak that
|
|
28
|
+
* wire format ignore it.
|
|
29
|
+
*/
|
|
30
|
+
extra? : Record<string, unknown>;
|
|
25
31
|
}
|
|
26
32
|
|
|
27
33
|
export interface TransformerBridge {
|