@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.
Files changed (47) hide show
  1. package/README.md +294 -5
  2. package/dist/src/core/owner.d.ts +14 -0
  3. package/dist/src/core/owner.js +66 -0
  4. package/dist/src/core/response-runtime.js +5 -5
  5. package/dist/src/core/response-state.d.ts +4 -0
  6. package/dist/src/core/response-state.js +67 -2
  7. package/dist/src/core/signal-context.js +19 -9
  8. package/dist/src/core/signal-nodes.d.ts +5 -0
  9. package/dist/src/core/signal-nodes.js +51 -21
  10. package/dist/src/core/signal-types.d.ts +2 -0
  11. package/dist/src/core/signal.d.ts +1 -0
  12. package/dist/src/core/signal.js +2 -0
  13. package/dist/src/core/stream-types.d.ts +12 -0
  14. package/dist/src/core/stream.d.ts +1 -1
  15. package/dist/src/core/stream.js +144 -0
  16. package/dist/src/dom/app.js +2 -3
  17. package/dist/src/dom/dom.d.ts +1 -0
  18. package/dist/src/dom/dom.js +96 -9
  19. package/dist/src/dom/scope.d.ts +3 -1
  20. package/dist/src/dom/scope.js +10 -3
  21. package/dist/src/index.d.ts +10 -4
  22. package/dist/src/index.js +7 -2
  23. package/dist/src/providers/anthropic.js +6 -4
  24. package/dist/src/providers/deepseek.d.ts +2 -0
  25. package/dist/src/providers/deepseek.js +90 -0
  26. package/dist/src/providers/line-adapter.d.ts +2 -0
  27. package/dist/src/providers/line-adapter.js +83 -0
  28. package/dist/src/providers/line-parser.d.ts +5 -0
  29. package/dist/src/providers/line-parser.js +103 -0
  30. package/dist/src/providers/metadata.d.ts +8 -0
  31. package/dist/src/providers/metadata.js +193 -0
  32. package/dist/src/providers/ollama.d.ts +2 -0
  33. package/dist/src/providers/ollama.js +82 -0
  34. package/dist/src/providers/openai.js +6 -4
  35. package/dist/src/providers/openrouter.d.ts +2 -0
  36. package/dist/src/providers/openrouter.js +90 -0
  37. package/dist/src/providers/sse-adapter.js +129 -44
  38. package/dist/src/providers/sse-parser.d.ts +1 -1
  39. package/dist/src/providers/sse-parser.js +74 -38
  40. package/dist/src/providers/sse.d.ts +7 -1
  41. package/dist/src/providers/sse.js +6 -0
  42. package/dist/src/providers/types.d.ts +145 -0
  43. package/dist/src/server/sse-response.d.ts +16 -0
  44. package/dist/src/server/sse-response.js +102 -0
  45. package/dist/src/shared/utils.d.ts +1 -0
  46. package/dist/src/shared/utils.js +8 -1
  47. 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 { createSSEAdapter } from './providers/sse.js';
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,2 @@
1
+ import type { DeepSeekAdapter, DeepSeekOptions } from './types.js';
2
+ export declare function createDeepSeek(options?: DeepSeekOptions): DeepSeekAdapter;
@@ -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,2 @@
1
+ import type { OllamaAdapter, OllamaOptions } from './types.js';
2
+ export declare function createOllama(options?: OllamaOptions): OllamaAdapter;
@@ -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
  };