@qorejs/qore 0.7.2 → 0.9.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/README.md +294 -5
- package/dist/src/core/owner.d.ts +14 -0
- package/dist/src/core/owner.js +66 -0
- package/dist/src/core/response-runtime.js +5 -5
- package/dist/src/core/response-state.d.ts +4 -0
- package/dist/src/core/response-state.js +67 -2
- package/dist/src/core/signal-context.js +19 -9
- package/dist/src/core/signal-nodes.d.ts +5 -0
- package/dist/src/core/signal-nodes.js +51 -21
- package/dist/src/core/signal-types.d.ts +2 -0
- package/dist/src/core/signal.d.ts +1 -0
- package/dist/src/core/signal.js +2 -0
- package/dist/src/core/stream-types.d.ts +12 -0
- package/dist/src/core/stream.d.ts +1 -1
- package/dist/src/core/stream.js +144 -0
- package/dist/src/dom/app.js +2 -3
- package/dist/src/dom/dom.d.ts +1 -0
- package/dist/src/dom/dom.js +96 -9
- package/dist/src/dom/scope.d.ts +3 -1
- package/dist/src/dom/scope.js +10 -3
- package/dist/src/index.d.ts +10 -4
- package/dist/src/index.js +7 -2
- package/dist/src/providers/anthropic.js +6 -4
- package/dist/src/providers/deepseek.d.ts +2 -0
- package/dist/src/providers/deepseek.js +90 -0
- package/dist/src/providers/line-adapter.d.ts +2 -0
- package/dist/src/providers/line-adapter.js +83 -0
- package/dist/src/providers/line-parser.d.ts +5 -0
- package/dist/src/providers/line-parser.js +103 -0
- package/dist/src/providers/metadata.d.ts +8 -0
- package/dist/src/providers/metadata.js +193 -0
- package/dist/src/providers/ollama.d.ts +2 -0
- package/dist/src/providers/ollama.js +82 -0
- package/dist/src/providers/openai.js +6 -4
- package/dist/src/providers/openrouter.d.ts +2 -0
- package/dist/src/providers/openrouter.js +90 -0
- package/dist/src/providers/sse-adapter.js +129 -44
- package/dist/src/providers/sse-parser.d.ts +1 -1
- package/dist/src/providers/sse-parser.js +74 -38
- package/dist/src/providers/sse.d.ts +7 -1
- package/dist/src/providers/sse.js +6 -0
- package/dist/src/providers/types.d.ts +145 -0
- package/dist/src/server/sse-response.d.ts +16 -0
- package/dist/src/server/sse-response.js +102 -0
- package/dist/src/shared/utils.d.ts +1 -0
- package/dist/src/shared/utils.js +8 -1
- package/package.json +9 -2
package/dist/src/index.js
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
// Re-export the public runtime surface from a single module entrypoint.
|
|
2
|
-
export { signal, computed, effect, batch, untrack, isSignal } from './core/signal.js';
|
|
2
|
+
export { signal, computed, createRoot, effect, onCleanup, batch, untrack, isSignal } from './core/signal.js';
|
|
3
3
|
export { stream, createStream, from, mapStream, scanStream, toAsyncIterable } from './core/stream.js';
|
|
4
4
|
export { createResponse, response } from './core/response.js';
|
|
5
5
|
export { createApp } from './dom/app.js';
|
|
6
6
|
export { dynamic, fragment, h, list, mount, renderResponse, show, text } from './dom/dom.js';
|
|
7
|
+
export { assertCanUseDOM, canUseDOM } from './dom/scope.js';
|
|
8
|
+
export { createSSEResponse } from './server/sse-response.js';
|
|
7
9
|
export { createAnthropic } from './providers/anthropic.js';
|
|
10
|
+
export { createDeepSeek } from './providers/deepseek.js';
|
|
11
|
+
export { createOllama } from './providers/ollama.js';
|
|
8
12
|
export { createOpenAI } from './providers/openai.js';
|
|
9
|
-
export {
|
|
13
|
+
export { createOpenRouter } from './providers/openrouter.js';
|
|
14
|
+
export { collectProviderMetadata, createLineAdapter, createSSEAdapter, extractAnthropicMetadata, extractDeepSeekMetadata, extractOllamaMetadata, extractOpenAIMetadata, extractOpenRouterMetadata, mergeProviderMetadata } from './providers/sse.js';
|
|
10
15
|
export { normalizeError, sleep } from './shared/utils.js';
|
|
@@ -18,7 +18,7 @@ function normalizeMessages(input) {
|
|
|
18
18
|
}
|
|
19
19
|
// Keep provider setup explicit because real API keys should stay off the client.
|
|
20
20
|
export function createAnthropic(options = {}) {
|
|
21
|
-
const { apiKey, baseURL = DEFAULT_BASE_URL, model = DEFAULT_MODEL, version = DEFAULT_VERSION, maxTokens = DEFAULT_MAX_TOKENS, headers: defaultHeaders = {}, fetch: fetchImpl = globalThis.fetch } = options;
|
|
21
|
+
const { apiKey, baseURL = DEFAULT_BASE_URL, model = DEFAULT_MODEL, version = DEFAULT_VERSION, maxTokens = DEFAULT_MAX_TOKENS, headers: defaultHeaders = {}, fetch: fetchImpl = globalThis.fetch, retry } = options;
|
|
22
22
|
const resolvedApiKey = apiKey ?? readEnv('ANTHROPIC_API_KEY');
|
|
23
23
|
if (!resolvedApiKey) {
|
|
24
24
|
throw new Error('Qore Anthropic adapter requires an API key. Pass apiKey or set ANTHROPIC_API_KEY.');
|
|
@@ -33,8 +33,9 @@ export function createAnthropic(options = {}) {
|
|
|
33
33
|
...defaultHeaders
|
|
34
34
|
},
|
|
35
35
|
fetch: fetchImpl,
|
|
36
|
+
...(retry ? { retry } : {}),
|
|
36
37
|
buildRequest(request, requestOptions = {}) {
|
|
37
|
-
const { signal, headers = {}, ...overrides } = requestOptions;
|
|
38
|
+
const { signal, headers = {}, retry: _retry, ...overrides } = requestOptions;
|
|
38
39
|
const config = {
|
|
39
40
|
method: 'POST',
|
|
40
41
|
headers,
|
|
@@ -88,14 +89,15 @@ export function createAnthropic(options = {}) {
|
|
|
88
89
|
},
|
|
89
90
|
// Match the Qore narrative directly: stream(anthropic.chat(prompt)).
|
|
90
91
|
chat(input, requestOptions = {}) {
|
|
91
|
-
const { signal, headers, ...rest } = requestOptions;
|
|
92
|
+
const { signal, headers, retry, ...rest } = requestOptions;
|
|
92
93
|
const request = { ...rest };
|
|
93
94
|
if (!('messages' in request)) {
|
|
94
95
|
request.messages = normalizeMessages(input);
|
|
95
96
|
}
|
|
96
97
|
return streamText(request, {
|
|
97
98
|
...(signal ? { signal } : {}),
|
|
98
|
-
...(headers ? { headers } : {})
|
|
99
|
+
...(headers ? { headers } : {}),
|
|
100
|
+
...(retry ? { retry } : {})
|
|
99
101
|
});
|
|
100
102
|
}
|
|
101
103
|
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { createSSEAdapter, readEnv } from './sse.js';
|
|
2
|
+
const DEFAULT_BASE_URL = 'https://api.deepseek.com';
|
|
3
|
+
const DEFAULT_MODEL = 'deepseek-chat';
|
|
4
|
+
function normalizeChatInput(input) {
|
|
5
|
+
if (typeof input === 'string') {
|
|
6
|
+
return [{ role: 'user', content: input }];
|
|
7
|
+
}
|
|
8
|
+
if (Array.isArray(input)) {
|
|
9
|
+
return input;
|
|
10
|
+
}
|
|
11
|
+
if (input && typeof input === 'object' && 'role' in input) {
|
|
12
|
+
return [input];
|
|
13
|
+
}
|
|
14
|
+
return input;
|
|
15
|
+
}
|
|
16
|
+
export function createDeepSeek(options = {}) {
|
|
17
|
+
const { apiKey, baseURL = DEFAULT_BASE_URL, model = DEFAULT_MODEL, headers: defaultHeaders = {}, fetch: fetchImpl = globalThis.fetch, retry } = options;
|
|
18
|
+
const resolvedApiKey = apiKey ?? readEnv('DEEPSEEK_API_KEY');
|
|
19
|
+
if (!resolvedApiKey) {
|
|
20
|
+
throw new Error('Qore DeepSeek adapter requires an API key. Pass apiKey or set DEEPSEEK_API_KEY.');
|
|
21
|
+
}
|
|
22
|
+
const transport = createSSEAdapter({
|
|
23
|
+
name: 'DeepSeek',
|
|
24
|
+
url: `${baseURL}/chat/completions`,
|
|
25
|
+
headers: {
|
|
26
|
+
Authorization: `Bearer ${resolvedApiKey}`,
|
|
27
|
+
'Content-Type': 'application/json',
|
|
28
|
+
...defaultHeaders
|
|
29
|
+
},
|
|
30
|
+
fetch: fetchImpl,
|
|
31
|
+
...(retry ? { retry } : {}),
|
|
32
|
+
buildRequest(request, requestOptions = {}) {
|
|
33
|
+
const { signal, headers = {}, retry: _retry, ...overrides } = requestOptions;
|
|
34
|
+
const config = {
|
|
35
|
+
method: 'POST',
|
|
36
|
+
headers,
|
|
37
|
+
body: JSON.stringify({
|
|
38
|
+
model,
|
|
39
|
+
stream: true,
|
|
40
|
+
...request,
|
|
41
|
+
...overrides
|
|
42
|
+
})
|
|
43
|
+
};
|
|
44
|
+
if (signal) {
|
|
45
|
+
config.signal = signal;
|
|
46
|
+
}
|
|
47
|
+
return config;
|
|
48
|
+
},
|
|
49
|
+
parse: (data) => JSON.parse(data),
|
|
50
|
+
eventToText: (event) => {
|
|
51
|
+
const nextChoice = event.data?.choices?.[0];
|
|
52
|
+
return typeof nextChoice?.delta?.content === 'string'
|
|
53
|
+
? nextChoice.delta.content
|
|
54
|
+
: undefined;
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
async function* streamEvents(request, requestOptions = {}) {
|
|
58
|
+
for await (const event of transport.stream(request, requestOptions)) {
|
|
59
|
+
yield event.data;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async function* streamText(input, requestOptions = {}) {
|
|
63
|
+
const request = input && typeof input === 'object' && 'messages' in input
|
|
64
|
+
? input
|
|
65
|
+
: { messages: input };
|
|
66
|
+
for await (const chunk of transport.streamText(request, requestOptions)) {
|
|
67
|
+
yield chunk;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
chatCompletions: {
|
|
72
|
+
stream: streamEvents
|
|
73
|
+
},
|
|
74
|
+
streamText(input, requestOptions = {}) {
|
|
75
|
+
return streamText(input, requestOptions);
|
|
76
|
+
},
|
|
77
|
+
chat(input, requestOptions = {}) {
|
|
78
|
+
const { signal, headers, retry, ...rest } = requestOptions;
|
|
79
|
+
const request = { ...rest };
|
|
80
|
+
if (!('messages' in request)) {
|
|
81
|
+
request.messages = normalizeChatInput(input);
|
|
82
|
+
}
|
|
83
|
+
return streamText(request, {
|
|
84
|
+
...(signal ? { signal } : {}),
|
|
85
|
+
...(headers ? { headers } : {}),
|
|
86
|
+
...(retry ? { retry } : {})
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import type { LineAdapter, LineAdapterOptions } from './types.js';
|
|
2
|
+
export declare function createLineAdapter<TRequest = Record<string, unknown>, TChatInput = unknown, TData = unknown>(options?: LineAdapterOptions<TRequest, TChatInput, TData>): LineAdapter<TRequest, TChatInput, TData>;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { normalizeAbortReason, normalizeError } from '../shared/utils.js';
|
|
2
|
+
import { mergeHeaders, readErrorBody } from './sse-env.js';
|
|
3
|
+
import { getLineErrorMessage, isLineError, parseLineData, readLines } from './line-parser.js';
|
|
4
|
+
function isRequestConfig(value) {
|
|
5
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
6
|
+
}
|
|
7
|
+
// Expose a generic line-stream adapter so Qore can integrate with NDJSON-style endpoints.
|
|
8
|
+
export function createLineAdapter(options = {}) {
|
|
9
|
+
const { name = 'LineStream', url: defaultURL, method: defaultMethod = 'POST', headers: defaultHeaders = {}, fetch: fetchImpl = globalThis.fetch, buildRequest = null, buildChatRequest = null, parse = parseLineData, isError = isLineError, getError = getLineErrorMessage, lineToText = (event) => typeof event.data === 'string' ? event.data : undefined } = options;
|
|
10
|
+
if (typeof fetchImpl !== 'function') {
|
|
11
|
+
throw new Error(`Qore ${name} adapter requires fetch in the current runtime`);
|
|
12
|
+
}
|
|
13
|
+
async function* stream(request = {}, requestOptions = {}) {
|
|
14
|
+
const builtRequest = buildRequest
|
|
15
|
+
? await buildRequest(request, requestOptions)
|
|
16
|
+
: request;
|
|
17
|
+
const requestConfig = isRequestConfig(builtRequest)
|
|
18
|
+
? builtRequest
|
|
19
|
+
: { body: builtRequest };
|
|
20
|
+
const { url = defaultURL, method = defaultMethod, headers: requestHeaders = {}, signal: requestSignal, ...init } = requestConfig;
|
|
21
|
+
const { signal: overrideSignal, headers: overrideHeaders = {} } = requestOptions;
|
|
22
|
+
const signal = requestSignal ?? overrideSignal;
|
|
23
|
+
if (!url) {
|
|
24
|
+
throw new Error(`Qore ${name} adapter requires a request URL`);
|
|
25
|
+
}
|
|
26
|
+
if (signal?.aborted) {
|
|
27
|
+
throw normalizeAbortReason(signal.reason, `${name} request aborted`);
|
|
28
|
+
}
|
|
29
|
+
const initConfig = {
|
|
30
|
+
method,
|
|
31
|
+
headers: mergeHeaders(defaultHeaders, mergeHeaders(requestHeaders, overrideHeaders)),
|
|
32
|
+
...init
|
|
33
|
+
};
|
|
34
|
+
if (signal) {
|
|
35
|
+
initConfig.signal = signal;
|
|
36
|
+
}
|
|
37
|
+
const response = await fetchImpl(url, initConfig);
|
|
38
|
+
if (!response.ok) {
|
|
39
|
+
throw new Error(await readErrorBody(response));
|
|
40
|
+
}
|
|
41
|
+
if (!response.body) {
|
|
42
|
+
throw new Error(`${name} streaming response did not include a readable body`);
|
|
43
|
+
}
|
|
44
|
+
for await (const rawEvent of readLines(response.body, signal)) {
|
|
45
|
+
let parsedEvent;
|
|
46
|
+
try {
|
|
47
|
+
parsedEvent = await parse(rawEvent.data, rawEvent);
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
throw normalizeError(error);
|
|
51
|
+
}
|
|
52
|
+
const nextEvent = {
|
|
53
|
+
...rawEvent,
|
|
54
|
+
data: parsedEvent
|
|
55
|
+
};
|
|
56
|
+
if (await isError(nextEvent)) {
|
|
57
|
+
throw new Error(await getError(nextEvent, name));
|
|
58
|
+
}
|
|
59
|
+
yield nextEvent;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async function* streamText(request = {}, requestOptions = {}) {
|
|
63
|
+
for await (const event of stream(request, requestOptions)) {
|
|
64
|
+
const nextText = await lineToText(event, request, requestOptions);
|
|
65
|
+
if (typeof nextText === 'string') {
|
|
66
|
+
yield nextText;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
// Stream parsed line-delimited events from an arbitrary HTTP endpoint.
|
|
72
|
+
stream,
|
|
73
|
+
// Stream only the text payload selected by the adapter's lineToText mapping.
|
|
74
|
+
streamText,
|
|
75
|
+
// Offer the same narrative shape as provider SDKs when buildChatRequest is supplied.
|
|
76
|
+
chat(input, requestOptions = {}) {
|
|
77
|
+
if (typeof buildChatRequest !== 'function') {
|
|
78
|
+
throw new Error(`Qore ${name} adapter does not define chat(). Use stream() or streamText() instead.`);
|
|
79
|
+
}
|
|
80
|
+
return streamText(buildChatRequest(input, requestOptions), requestOptions);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { LineEvent } from './types.js';
|
|
2
|
+
export declare function readLines(body: ReadableStream<Uint8Array>, signal?: AbortSignal | null): AsyncIterable<LineEvent<string>>;
|
|
3
|
+
export declare function parseLineData(line: string): unknown;
|
|
4
|
+
export declare function isLineError(event: LineEvent<unknown>): boolean;
|
|
5
|
+
export declare function getLineErrorMessage(event: LineEvent<unknown>, name: string): string;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { normalizeAbortReason } from '../shared/utils.js';
|
|
2
|
+
// Read newline-delimited streaming bodies without depending on a provider SDK.
|
|
3
|
+
export async function* readLines(body, signal) {
|
|
4
|
+
const reader = body.getReader();
|
|
5
|
+
const decoder = new TextDecoder();
|
|
6
|
+
let buffer = '';
|
|
7
|
+
let lineNumber = 0;
|
|
8
|
+
const abortMessage = 'Line stream aborted';
|
|
9
|
+
const cancelReader = () => {
|
|
10
|
+
void reader.cancel(signal?.reason).catch(() => {
|
|
11
|
+
// Ignore reader cancellation races during abort handling.
|
|
12
|
+
});
|
|
13
|
+
};
|
|
14
|
+
const flushLines = function* (lines) {
|
|
15
|
+
for (const line of lines) {
|
|
16
|
+
if (!line) {
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
lineNumber += 1;
|
|
20
|
+
yield {
|
|
21
|
+
line: lineNumber,
|
|
22
|
+
raw: line,
|
|
23
|
+
data: line
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
if (signal?.aborted) {
|
|
28
|
+
cancelReader();
|
|
29
|
+
throw normalizeAbortReason(signal.reason, abortMessage);
|
|
30
|
+
}
|
|
31
|
+
signal?.addEventListener('abort', cancelReader, { once: true });
|
|
32
|
+
try {
|
|
33
|
+
while (true) {
|
|
34
|
+
if (signal?.aborted) {
|
|
35
|
+
throw normalizeAbortReason(signal.reason, abortMessage);
|
|
36
|
+
}
|
|
37
|
+
let chunk;
|
|
38
|
+
try {
|
|
39
|
+
chunk = await reader.read();
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (signal?.aborted) {
|
|
43
|
+
throw normalizeAbortReason(signal.reason, abortMessage);
|
|
44
|
+
}
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
const { value, done } = chunk;
|
|
48
|
+
buffer += decoder.decode(value ?? new Uint8Array(), { stream: !done });
|
|
49
|
+
const lines = buffer.split(/\r?\n/);
|
|
50
|
+
buffer = lines.pop() ?? '';
|
|
51
|
+
for (const event of flushLines(lines)) {
|
|
52
|
+
yield event;
|
|
53
|
+
}
|
|
54
|
+
if (done) {
|
|
55
|
+
if (signal?.aborted) {
|
|
56
|
+
throw normalizeAbortReason(signal.reason, abortMessage);
|
|
57
|
+
}
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (signal?.aborted) {
|
|
62
|
+
throw normalizeAbortReason(signal.reason, abortMessage);
|
|
63
|
+
}
|
|
64
|
+
if (buffer) {
|
|
65
|
+
lineNumber += 1;
|
|
66
|
+
yield {
|
|
67
|
+
line: lineNumber,
|
|
68
|
+
raw: buffer,
|
|
69
|
+
data: buffer
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
signal?.removeEventListener('abort', cancelReader);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Default to JSON payloads when possible, otherwise preserve the raw line text.
|
|
78
|
+
export function parseLineData(line) {
|
|
79
|
+
try {
|
|
80
|
+
return JSON.parse(line);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return line;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Surface provider-side error payloads from NDJSON-style streams.
|
|
87
|
+
export function isLineError(event) {
|
|
88
|
+
return Boolean(event.data
|
|
89
|
+
&& typeof event.data === 'object'
|
|
90
|
+
&& 'error' in event.data
|
|
91
|
+
&& (typeof event.data.error === 'string' || Boolean(event.data.error)));
|
|
92
|
+
}
|
|
93
|
+
// Pull a readable message from the common line-stream error shapes.
|
|
94
|
+
export function getLineErrorMessage(event, name) {
|
|
95
|
+
if (event.data && typeof event.data === 'object') {
|
|
96
|
+
const errorRecord = event.data;
|
|
97
|
+
if (typeof errorRecord.error === 'string') {
|
|
98
|
+
return errorRecord.error;
|
|
99
|
+
}
|
|
100
|
+
return errorRecord.error?.message ?? errorRecord.message ?? `${name} streaming error`;
|
|
101
|
+
}
|
|
102
|
+
return `${name} streaming error`;
|
|
103
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AnthropicEvent, DeepSeekEvent, OpenAIEvent, OllamaEvent, OpenRouterEvent, ProviderMetadataUpdate, ProviderStreamMetadata } from './types.js';
|
|
2
|
+
export declare function mergeProviderMetadata(metadata: ProviderStreamMetadata, update: ProviderMetadataUpdate | null | undefined): ProviderStreamMetadata;
|
|
3
|
+
export declare function collectProviderMetadata<TEvent>(provider: string, events: Iterable<TEvent> | AsyncIterable<TEvent>, extract: (event: TEvent) => ProviderMetadataUpdate | null | undefined): Promise<ProviderStreamMetadata>;
|
|
4
|
+
export declare function extractOpenAIMetadata(event: OpenAIEvent): ProviderMetadataUpdate | undefined;
|
|
5
|
+
export declare function extractOpenRouterMetadata(event: OpenRouterEvent): ProviderMetadataUpdate | undefined;
|
|
6
|
+
export declare function extractDeepSeekMetadata(event: DeepSeekEvent): ProviderMetadataUpdate | undefined;
|
|
7
|
+
export declare function extractAnthropicMetadata(event: AnthropicEvent): ProviderMetadataUpdate | undefined;
|
|
8
|
+
export declare function extractOllamaMetadata(event: OllamaEvent): ProviderMetadataUpdate | undefined;
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
function isRecord(value) {
|
|
2
|
+
return typeof value === 'object' && value !== null;
|
|
3
|
+
}
|
|
4
|
+
function readRecord(value) {
|
|
5
|
+
return isRecord(value) ? value : null;
|
|
6
|
+
}
|
|
7
|
+
function readString(value) {
|
|
8
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
9
|
+
}
|
|
10
|
+
function readNullableString(value) {
|
|
11
|
+
if (value === null) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
return readString(value);
|
|
15
|
+
}
|
|
16
|
+
function readNumber(value) {
|
|
17
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
18
|
+
}
|
|
19
|
+
function toUsage(update) {
|
|
20
|
+
if (!update) {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
const inputTokens = readNumber(update.inputTokens);
|
|
24
|
+
const outputTokens = readNumber(update.outputTokens);
|
|
25
|
+
const explicitTotal = readNumber(update.totalTokens);
|
|
26
|
+
const totalTokens = explicitTotal ?? (inputTokens !== undefined && outputTokens !== undefined
|
|
27
|
+
? (inputTokens ?? 0) + (outputTokens ?? 0)
|
|
28
|
+
: undefined);
|
|
29
|
+
if (inputTokens === undefined && outputTokens === undefined && totalTokens === undefined) {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
...(inputTokens !== undefined ? { inputTokens } : {}),
|
|
34
|
+
...(outputTokens !== undefined ? { outputTokens } : {}),
|
|
35
|
+
...(totalTokens !== undefined ? { totalTokens } : {})
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function usageFromOpenAIStyle(raw) {
|
|
39
|
+
const usage = readRecord(raw);
|
|
40
|
+
if (!usage) {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
const inputTokens = readNumber(usage['input_tokens'] ?? usage['prompt_tokens']);
|
|
44
|
+
const outputTokens = readNumber(usage['output_tokens'] ?? usage['completion_tokens']);
|
|
45
|
+
const totalTokens = readNumber(usage['total_tokens']);
|
|
46
|
+
return toUsage({
|
|
47
|
+
...(inputTokens !== undefined ? { inputTokens } : {}),
|
|
48
|
+
...(outputTokens !== undefined ? { outputTokens } : {}),
|
|
49
|
+
...(totalTokens !== undefined ? { totalTokens } : {})
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function usageFromAnthropic(raw) {
|
|
53
|
+
const usage = readRecord(raw);
|
|
54
|
+
if (!usage) {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
const inputTokens = readNumber(usage['input_tokens']);
|
|
58
|
+
const outputTokens = readNumber(usage['output_tokens']);
|
|
59
|
+
return toUsage({
|
|
60
|
+
...(inputTokens !== undefined ? { inputTokens } : {}),
|
|
61
|
+
...(outputTokens !== undefined ? { outputTokens } : {})
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
function usageFromOllama(event) {
|
|
65
|
+
const inputTokens = readNumber(event['prompt_eval_count']);
|
|
66
|
+
const outputTokens = readNumber(event['eval_count']);
|
|
67
|
+
return toUsage({
|
|
68
|
+
...(inputTokens !== undefined ? { inputTokens } : {}),
|
|
69
|
+
...(outputTokens !== undefined ? { outputTokens } : {})
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
function hasMetadata(update) {
|
|
73
|
+
return update.responseId !== undefined
|
|
74
|
+
|| update.model !== undefined
|
|
75
|
+
|| update.finishReason !== undefined
|
|
76
|
+
|| update.stopReason !== undefined
|
|
77
|
+
|| update.usage !== undefined;
|
|
78
|
+
}
|
|
79
|
+
function buildMetadataUpdate(fields) {
|
|
80
|
+
return {
|
|
81
|
+
...(fields.responseId !== undefined ? { responseId: fields.responseId } : {}),
|
|
82
|
+
...(fields.model !== undefined ? { model: fields.model } : {}),
|
|
83
|
+
...(fields.finishReason !== undefined ? { finishReason: fields.finishReason } : {}),
|
|
84
|
+
...(fields.stopReason !== undefined ? { stopReason: fields.stopReason } : {}),
|
|
85
|
+
...(fields.usage !== undefined ? { usage: fields.usage } : {})
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
export function mergeProviderMetadata(metadata, update) {
|
|
89
|
+
if (!update) {
|
|
90
|
+
return metadata;
|
|
91
|
+
}
|
|
92
|
+
const hasUsageUpdate = update.usage !== undefined;
|
|
93
|
+
const mergedInputTokens = update.usage?.inputTokens ?? metadata.usage?.inputTokens;
|
|
94
|
+
const mergedOutputTokens = update.usage?.outputTokens ?? metadata.usage?.outputTokens;
|
|
95
|
+
const mergedExplicitTotal = update.usage?.totalTokens ?? (hasUsageUpdate ? undefined : metadata.usage?.totalTokens);
|
|
96
|
+
const mergedUsage = toUsage({
|
|
97
|
+
...(mergedInputTokens !== undefined ? { inputTokens: mergedInputTokens } : {}),
|
|
98
|
+
...(mergedOutputTokens !== undefined ? { outputTokens: mergedOutputTokens } : {}),
|
|
99
|
+
...(mergedExplicitTotal !== undefined ? { totalTokens: mergedExplicitTotal } : {})
|
|
100
|
+
});
|
|
101
|
+
return {
|
|
102
|
+
provider: metadata.provider,
|
|
103
|
+
...(update.responseId !== undefined ? { responseId: update.responseId } : metadata.responseId !== undefined ? { responseId: metadata.responseId } : {}),
|
|
104
|
+
...(update.model !== undefined ? { model: update.model } : metadata.model !== undefined ? { model: metadata.model } : {}),
|
|
105
|
+
...(update.finishReason !== undefined
|
|
106
|
+
? { finishReason: update.finishReason }
|
|
107
|
+
: metadata.finishReason !== undefined
|
|
108
|
+
? { finishReason: metadata.finishReason }
|
|
109
|
+
: {}),
|
|
110
|
+
...(update.stopReason !== undefined
|
|
111
|
+
? { stopReason: update.stopReason }
|
|
112
|
+
: metadata.stopReason !== undefined
|
|
113
|
+
? { stopReason: metadata.stopReason }
|
|
114
|
+
: {}),
|
|
115
|
+
...(mergedUsage ? { usage: mergedUsage } : {})
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
export async function collectProviderMetadata(provider, events, extract) {
|
|
119
|
+
let metadata = { provider };
|
|
120
|
+
for await (const event of events) {
|
|
121
|
+
metadata = mergeProviderMetadata(metadata, extract(event));
|
|
122
|
+
}
|
|
123
|
+
return metadata;
|
|
124
|
+
}
|
|
125
|
+
export function extractOpenAIMetadata(event) {
|
|
126
|
+
const response = readRecord(event['response']);
|
|
127
|
+
if (!response) {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
const update = buildMetadataUpdate({
|
|
131
|
+
responseId: readString(response['id']),
|
|
132
|
+
model: readString(response['model']),
|
|
133
|
+
finishReason: readNullableString(response['finish_reason']) ?? readNullableString(response['status']),
|
|
134
|
+
stopReason: undefined,
|
|
135
|
+
usage: usageFromOpenAIStyle(response['usage'])
|
|
136
|
+
});
|
|
137
|
+
return hasMetadata(update) ? update : undefined;
|
|
138
|
+
}
|
|
139
|
+
export function extractOpenRouterMetadata(event) {
|
|
140
|
+
const firstChoice = Array.isArray(event.choices) ? event.choices[0] : undefined;
|
|
141
|
+
const finishReason = readNullableString(firstChoice?.finish_reason);
|
|
142
|
+
const update = buildMetadataUpdate({
|
|
143
|
+
responseId: readString(event.id),
|
|
144
|
+
model: readString(event.model),
|
|
145
|
+
finishReason,
|
|
146
|
+
stopReason: undefined,
|
|
147
|
+
usage: usageFromOpenAIStyle(readRecord(event)?.['usage'])
|
|
148
|
+
});
|
|
149
|
+
return hasMetadata(update) ? update : undefined;
|
|
150
|
+
}
|
|
151
|
+
export function extractDeepSeekMetadata(event) {
|
|
152
|
+
return extractOpenRouterMetadata(event);
|
|
153
|
+
}
|
|
154
|
+
export function extractAnthropicMetadata(event) {
|
|
155
|
+
if (event.type === 'message_start') {
|
|
156
|
+
const message = readRecord(event['message']);
|
|
157
|
+
if (!message) {
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
const update = buildMetadataUpdate({
|
|
161
|
+
responseId: readString(message['id']),
|
|
162
|
+
model: readString(message['model']),
|
|
163
|
+
finishReason: undefined,
|
|
164
|
+
stopReason: undefined,
|
|
165
|
+
usage: usageFromAnthropic(message['usage'])
|
|
166
|
+
});
|
|
167
|
+
return hasMetadata(update) ? update : undefined;
|
|
168
|
+
}
|
|
169
|
+
if (event.type === 'message_delta') {
|
|
170
|
+
const delta = readRecord(event['delta']);
|
|
171
|
+
const stopReason = readNullableString(delta?.['stop_reason']) ?? readNullableString(delta?.['stop_sequence']);
|
|
172
|
+
const update = buildMetadataUpdate({
|
|
173
|
+
responseId: undefined,
|
|
174
|
+
model: undefined,
|
|
175
|
+
finishReason: stopReason,
|
|
176
|
+
stopReason,
|
|
177
|
+
usage: usageFromAnthropic(event['usage'])
|
|
178
|
+
});
|
|
179
|
+
return hasMetadata(update) ? update : undefined;
|
|
180
|
+
}
|
|
181
|
+
return undefined;
|
|
182
|
+
}
|
|
183
|
+
export function extractOllamaMetadata(event) {
|
|
184
|
+
const finishReason = readNullableString(event.done_reason);
|
|
185
|
+
const update = buildMetadataUpdate({
|
|
186
|
+
responseId: undefined,
|
|
187
|
+
model: readString(event.model),
|
|
188
|
+
finishReason,
|
|
189
|
+
stopReason: finishReason,
|
|
190
|
+
usage: usageFromOllama(event)
|
|
191
|
+
});
|
|
192
|
+
return hasMetadata(update) ? update : undefined;
|
|
193
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { createLineAdapter } from './line-adapter.js';
|
|
2
|
+
const DEFAULT_BASE_URL = 'http://127.0.0.1:11434';
|
|
3
|
+
const DEFAULT_MODEL = 'llama3.2';
|
|
4
|
+
function normalizeChatInput(input) {
|
|
5
|
+
if (typeof input === 'string') {
|
|
6
|
+
return [{ role: 'user', content: input }];
|
|
7
|
+
}
|
|
8
|
+
if (Array.isArray(input)) {
|
|
9
|
+
return input;
|
|
10
|
+
}
|
|
11
|
+
if (input && typeof input === 'object' && 'role' in input) {
|
|
12
|
+
return [input];
|
|
13
|
+
}
|
|
14
|
+
return input;
|
|
15
|
+
}
|
|
16
|
+
export function createOllama(options = {}) {
|
|
17
|
+
const { baseURL = DEFAULT_BASE_URL, model = DEFAULT_MODEL, headers: defaultHeaders = {}, fetch: fetchImpl = globalThis.fetch } = options;
|
|
18
|
+
const transport = createLineAdapter({
|
|
19
|
+
name: 'Ollama',
|
|
20
|
+
url: `${baseURL}/api/chat`,
|
|
21
|
+
headers: {
|
|
22
|
+
'Content-Type': 'application/json',
|
|
23
|
+
...defaultHeaders
|
|
24
|
+
},
|
|
25
|
+
fetch: fetchImpl,
|
|
26
|
+
buildRequest(request, requestOptions = {}) {
|
|
27
|
+
const { signal, headers = {}, ...overrides } = requestOptions;
|
|
28
|
+
const config = {
|
|
29
|
+
method: 'POST',
|
|
30
|
+
headers,
|
|
31
|
+
body: JSON.stringify({
|
|
32
|
+
model,
|
|
33
|
+
stream: true,
|
|
34
|
+
...request,
|
|
35
|
+
...overrides
|
|
36
|
+
})
|
|
37
|
+
};
|
|
38
|
+
if (signal) {
|
|
39
|
+
config.signal = signal;
|
|
40
|
+
}
|
|
41
|
+
return config;
|
|
42
|
+
},
|
|
43
|
+
parse: (line) => JSON.parse(line),
|
|
44
|
+
lineToText: (event) => {
|
|
45
|
+
return typeof event.data?.message?.content === 'string'
|
|
46
|
+
? event.data.message.content
|
|
47
|
+
: undefined;
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
async function* streamEvents(request, requestOptions = {}) {
|
|
51
|
+
for await (const event of transport.stream(request, requestOptions)) {
|
|
52
|
+
yield event.data;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async function* streamText(input, requestOptions = {}) {
|
|
56
|
+
const request = input && typeof input === 'object' && 'messages' in input
|
|
57
|
+
? input
|
|
58
|
+
: { messages: input };
|
|
59
|
+
for await (const chunk of transport.streamText(request, requestOptions)) {
|
|
60
|
+
yield chunk;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
stream(request, requestOptions = {}) {
|
|
65
|
+
return streamEvents(request, requestOptions);
|
|
66
|
+
},
|
|
67
|
+
streamText(input, requestOptions = {}) {
|
|
68
|
+
return streamText(input, requestOptions);
|
|
69
|
+
},
|
|
70
|
+
chat(input, requestOptions = {}) {
|
|
71
|
+
const { signal, headers, ...rest } = requestOptions;
|
|
72
|
+
const request = { ...rest };
|
|
73
|
+
if (!('messages' in request)) {
|
|
74
|
+
request.messages = normalizeChatInput(input);
|
|
75
|
+
}
|
|
76
|
+
return streamText(request, {
|
|
77
|
+
...(signal ? { signal } : {}),
|
|
78
|
+
...(headers ? { headers } : {})
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -16,7 +16,7 @@ function normalizeChatInput(input) {
|
|
|
16
16
|
}
|
|
17
17
|
// Keep provider setup explicit because real API keys should stay off the client.
|
|
18
18
|
export function createOpenAI(options = {}) {
|
|
19
|
-
const { apiKey, baseURL = DEFAULT_BASE_URL, model = DEFAULT_MODEL, headers: defaultHeaders = {}, fetch: fetchImpl = globalThis.fetch } = options;
|
|
19
|
+
const { apiKey, baseURL = DEFAULT_BASE_URL, model = DEFAULT_MODEL, headers: defaultHeaders = {}, fetch: fetchImpl = globalThis.fetch, retry } = options;
|
|
20
20
|
const resolvedApiKey = apiKey ?? readEnv('OPENAI_API_KEY');
|
|
21
21
|
if (!resolvedApiKey) {
|
|
22
22
|
throw new Error('Qore OpenAI adapter requires an API key. Pass apiKey or set OPENAI_API_KEY.');
|
|
@@ -30,8 +30,9 @@ export function createOpenAI(options = {}) {
|
|
|
30
30
|
...defaultHeaders
|
|
31
31
|
},
|
|
32
32
|
fetch: fetchImpl,
|
|
33
|
+
...(retry ? { retry } : {}),
|
|
33
34
|
buildRequest(request, requestOptions = {}) {
|
|
34
|
-
const { signal, headers = {}, ...overrides } = requestOptions;
|
|
35
|
+
const { signal, headers = {}, retry: _retry, ...overrides } = requestOptions;
|
|
35
36
|
const config = {
|
|
36
37
|
method: 'POST',
|
|
37
38
|
headers,
|
|
@@ -82,14 +83,15 @@ export function createOpenAI(options = {}) {
|
|
|
82
83
|
},
|
|
83
84
|
// Match the Qore narrative directly: stream(openai.chat(prompt)).
|
|
84
85
|
chat(input, requestOptions = {}) {
|
|
85
|
-
const { signal, headers, ...rest } = requestOptions;
|
|
86
|
+
const { signal, headers, retry, ...rest } = requestOptions;
|
|
86
87
|
const request = { ...rest };
|
|
87
88
|
if (!('input' in request)) {
|
|
88
89
|
request.input = normalizeChatInput(input);
|
|
89
90
|
}
|
|
90
91
|
return streamText(request, {
|
|
91
92
|
...(signal ? { signal } : {}),
|
|
92
|
-
...(headers ? { headers } : {})
|
|
93
|
+
...(headers ? { headers } : {}),
|
|
94
|
+
...(retry ? { retry } : {})
|
|
93
95
|
});
|
|
94
96
|
}
|
|
95
97
|
};
|