@push.rocks/smartai 0.13.3 → 2.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/dist_ts/00_commitinfo_data.js +3 -3
- package/dist_ts/index.d.ts +6 -11
- package/dist_ts/index.js +6 -12
- package/dist_ts/plugins.d.ts +10 -15
- package/dist_ts/plugins.js +13 -19
- package/dist_ts/smartai.classes.smartai.d.ts +7 -0
- package/dist_ts/smartai.classes.smartai.js +51 -0
- package/dist_ts/smartai.interfaces.d.ts +41 -0
- package/dist_ts/smartai.interfaces.js +2 -0
- package/dist_ts/smartai.middleware.anthropic.d.ts +7 -0
- package/dist_ts/smartai.middleware.anthropic.js +36 -0
- package/dist_ts/smartai.provider.ollama.d.ts +8 -0
- package/dist_ts/smartai.provider.ollama.js +378 -0
- package/dist_ts_audio/index.d.ts +9 -0
- package/dist_ts_audio/index.js +15 -0
- package/dist_ts_audio/plugins.d.ts +2 -0
- package/dist_ts_audio/plugins.js +3 -0
- package/dist_ts_document/index.d.ts +11 -0
- package/dist_ts_document/index.js +45 -0
- package/dist_ts_document/plugins.d.ts +3 -0
- package/dist_ts_document/plugins.js +4 -0
- package/dist_ts_image/index.d.ts +46 -0
- package/dist_ts_image/index.js +110 -0
- package/dist_ts_image/plugins.d.ts +3 -0
- package/dist_ts_image/plugins.js +4 -0
- package/dist_ts_research/index.d.ts +19 -0
- package/dist_ts_research/index.js +98 -0
- package/dist_ts_research/plugins.d.ts +2 -0
- package/dist_ts_research/plugins.js +3 -0
- package/dist_ts_vision/index.d.ts +8 -0
- package/dist_ts_vision/index.js +21 -0
- package/dist_ts_vision/plugins.d.ts +2 -0
- package/dist_ts_vision/plugins.js +3 -0
- package/package.json +50 -22
- package/readme.hints.md +34 -88
- package/readme.md +284 -547
- package/ts/00_commitinfo_data.ts +2 -2
- package/ts/index.ts +8 -11
- package/ts/plugins.ts +19 -35
- package/ts/smartai.classes.smartai.ts +51 -0
- package/ts/smartai.interfaces.ts +53 -0
- package/ts/smartai.middleware.anthropic.ts +38 -0
- package/ts/smartai.provider.ollama.ts +426 -0
- package/ts_audio/index.ts +24 -0
- package/ts_audio/plugins.ts +2 -0
- package/ts_document/index.ts +61 -0
- package/ts_document/plugins.ts +3 -0
- package/ts_image/index.ts +147 -0
- package/ts_image/plugins.ts +3 -0
- package/ts_research/index.ts +120 -0
- package/ts_research/plugins.ts +2 -0
- package/ts_vision/index.ts +29 -0
- package/ts_vision/plugins.ts +2 -0
- package/dist_ts/abstract.classes.multimodal.d.ts +0 -212
- package/dist_ts/abstract.classes.multimodal.js +0 -43
- package/dist_ts/classes.conversation.d.ts +0 -31
- package/dist_ts/classes.conversation.js +0 -150
- package/dist_ts/classes.smartai.d.ts +0 -59
- package/dist_ts/classes.smartai.js +0 -139
- package/dist_ts/classes.tts.d.ts +0 -6
- package/dist_ts/classes.tts.js +0 -10
- package/dist_ts/interfaces.d.ts +0 -1
- package/dist_ts/interfaces.js +0 -2
- package/dist_ts/paths.d.ts +0 -2
- package/dist_ts/paths.js +0 -4
- package/dist_ts/provider.anthropic.d.ts +0 -48
- package/dist_ts/provider.anthropic.js +0 -369
- package/dist_ts/provider.elevenlabs.d.ts +0 -43
- package/dist_ts/provider.elevenlabs.js +0 -64
- package/dist_ts/provider.exo.d.ts +0 -40
- package/dist_ts/provider.exo.js +0 -116
- package/dist_ts/provider.groq.d.ts +0 -39
- package/dist_ts/provider.groq.js +0 -178
- package/dist_ts/provider.mistral.d.ts +0 -61
- package/dist_ts/provider.mistral.js +0 -288
- package/dist_ts/provider.ollama.d.ts +0 -141
- package/dist_ts/provider.ollama.js +0 -529
- package/dist_ts/provider.openai.d.ts +0 -62
- package/dist_ts/provider.openai.js +0 -403
- package/dist_ts/provider.perplexity.d.ts +0 -37
- package/dist_ts/provider.perplexity.js +0 -215
- package/dist_ts/provider.xai.d.ts +0 -52
- package/dist_ts/provider.xai.js +0 -160
- package/ts/abstract.classes.multimodal.ts +0 -240
- package/ts/classes.conversation.ts +0 -176
- package/ts/classes.smartai.ts +0 -187
- package/ts/classes.tts.ts +0 -15
- package/ts/interfaces.ts +0 -0
- package/ts/paths.ts +0 -4
- package/ts/provider.anthropic.ts +0 -446
- package/ts/provider.elevenlabs.ts +0 -116
- package/ts/provider.exo.ts +0 -155
- package/ts/provider.groq.ts +0 -219
- package/ts/provider.mistral.ts +0 -352
- package/ts/provider.ollama.ts +0 -705
- package/ts/provider.openai.ts +0 -462
- package/ts/provider.perplexity.ts +0 -259
- package/ts/provider.xai.ts +0 -214
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@push.rocks/smartai',
|
|
6
|
-
version: '0.
|
|
7
|
-
description: '
|
|
6
|
+
version: '2.0.0',
|
|
7
|
+
description: 'Provider registry and capability utilities for ai-sdk (Vercel AI SDK). Core export returns LanguageModel; subpath exports provide vision, audio, image, document and research capabilities.'
|
|
8
8
|
}
|
package/ts/index.ts
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
|
-
export
|
|
2
|
-
export
|
|
3
|
-
export
|
|
4
|
-
export
|
|
5
|
-
|
|
6
|
-
export
|
|
7
|
-
export
|
|
8
|
-
export
|
|
9
|
-
export * from './provider.xai.js';
|
|
10
|
-
export * from './provider.exo.js';
|
|
11
|
-
export * from './provider.elevenlabs.js';
|
|
1
|
+
export { getModel } from './smartai.classes.smartai.js';
|
|
2
|
+
export type { ISmartAiOptions, TProvider, IOllamaModelOptions, LanguageModelV3 } from './smartai.interfaces.js';
|
|
3
|
+
export { createAnthropicCachingMiddleware } from './smartai.middleware.anthropic.js';
|
|
4
|
+
export { createOllamaModel } from './smartai.provider.ollama.js';
|
|
5
|
+
|
|
6
|
+
// Re-export commonly used ai-sdk functions for consumer convenience
|
|
7
|
+
export { generateText, streamText, tool, jsonSchema } from 'ai';
|
|
8
|
+
export type { ModelMessage, ToolSet, StreamTextResult } from 'ai';
|
package/ts/plugins.ts
CHANGED
|
@@ -1,38 +1,22 @@
|
|
|
1
|
-
//
|
|
2
|
-
import
|
|
1
|
+
// ai sdk core
|
|
2
|
+
import { generateText, streamText, wrapLanguageModel, tool, jsonSchema } from 'ai';
|
|
3
|
+
export { generateText, streamText, wrapLanguageModel, tool, jsonSchema };
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
import
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
-
import * as smartpath from '@push.rocks/smartpath';
|
|
13
|
-
import * as smartpdf from '@push.rocks/smartpdf';
|
|
14
|
-
import * as smartpromise from '@push.rocks/smartpromise';
|
|
15
|
-
import * as smartrequest from '@push.rocks/smartrequest';
|
|
16
|
-
import * as webstream from '@push.rocks/webstream';
|
|
17
|
-
|
|
18
|
-
export {
|
|
19
|
-
smartarray,
|
|
20
|
-
qenv,
|
|
21
|
-
smartfs,
|
|
22
|
-
smartpath,
|
|
23
|
-
smartpdf,
|
|
24
|
-
smartpromise,
|
|
25
|
-
smartrequest,
|
|
26
|
-
webstream,
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// third party
|
|
30
|
-
import * as anthropic from '@anthropic-ai/sdk';
|
|
31
|
-
import * as mistralai from '@mistralai/mistralai';
|
|
32
|
-
import * as openai from 'openai';
|
|
5
|
+
// ai sdk providers
|
|
6
|
+
import { createAnthropic } from '@ai-sdk/anthropic';
|
|
7
|
+
import { createOpenAI } from '@ai-sdk/openai';
|
|
8
|
+
import { createGoogleGenerativeAI } from '@ai-sdk/google';
|
|
9
|
+
import { createGroq } from '@ai-sdk/groq';
|
|
10
|
+
import { createMistral } from '@ai-sdk/mistral';
|
|
11
|
+
import { createXai } from '@ai-sdk/xai';
|
|
12
|
+
import { createPerplexity } from '@ai-sdk/perplexity';
|
|
33
13
|
|
|
34
14
|
export {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
15
|
+
createAnthropic,
|
|
16
|
+
createOpenAI,
|
|
17
|
+
createGoogleGenerativeAI,
|
|
18
|
+
createGroq,
|
|
19
|
+
createMistral,
|
|
20
|
+
createXai,
|
|
21
|
+
createPerplexity,
|
|
22
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import type { ISmartAiOptions, LanguageModelV3 } from './smartai.interfaces.js';
|
|
3
|
+
import { createOllamaModel } from './smartai.provider.ollama.js';
|
|
4
|
+
import { createAnthropicCachingMiddleware } from './smartai.middleware.anthropic.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Returns a LanguageModelV3 for the given provider and model.
|
|
8
|
+
* This is the primary API — consumers use the returned model with AI SDK's
|
|
9
|
+
* generateText(), streamText(), etc.
|
|
10
|
+
*/
|
|
11
|
+
export function getModel(options: ISmartAiOptions): LanguageModelV3 {
|
|
12
|
+
switch (options.provider) {
|
|
13
|
+
case 'anthropic': {
|
|
14
|
+
const p = plugins.createAnthropic({ apiKey: options.apiKey });
|
|
15
|
+
const base = p(options.model) as LanguageModelV3;
|
|
16
|
+
if (options.promptCaching === false) return base;
|
|
17
|
+
return plugins.wrapLanguageModel({
|
|
18
|
+
model: base,
|
|
19
|
+
middleware: createAnthropicCachingMiddleware(),
|
|
20
|
+
}) as unknown as LanguageModelV3;
|
|
21
|
+
}
|
|
22
|
+
case 'openai': {
|
|
23
|
+
const p = plugins.createOpenAI({ apiKey: options.apiKey });
|
|
24
|
+
return p(options.model) as LanguageModelV3;
|
|
25
|
+
}
|
|
26
|
+
case 'google': {
|
|
27
|
+
const p = plugins.createGoogleGenerativeAI({ apiKey: options.apiKey });
|
|
28
|
+
return p(options.model) as LanguageModelV3;
|
|
29
|
+
}
|
|
30
|
+
case 'groq': {
|
|
31
|
+
const p = plugins.createGroq({ apiKey: options.apiKey });
|
|
32
|
+
return p(options.model) as LanguageModelV3;
|
|
33
|
+
}
|
|
34
|
+
case 'mistral': {
|
|
35
|
+
const p = plugins.createMistral({ apiKey: options.apiKey });
|
|
36
|
+
return p(options.model) as LanguageModelV3;
|
|
37
|
+
}
|
|
38
|
+
case 'xai': {
|
|
39
|
+
const p = plugins.createXai({ apiKey: options.apiKey });
|
|
40
|
+
return p(options.model) as LanguageModelV3;
|
|
41
|
+
}
|
|
42
|
+
case 'perplexity': {
|
|
43
|
+
const p = plugins.createPerplexity({ apiKey: options.apiKey });
|
|
44
|
+
return p(options.model) as LanguageModelV3;
|
|
45
|
+
}
|
|
46
|
+
case 'ollama':
|
|
47
|
+
return createOllamaModel(options);
|
|
48
|
+
default:
|
|
49
|
+
throw new Error(`Unknown provider: ${(options as ISmartAiOptions).provider}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { LanguageModelV3 } from '@ai-sdk/provider';
|
|
2
|
+
|
|
3
|
+
export type TProvider =
|
|
4
|
+
| 'anthropic'
|
|
5
|
+
| 'openai'
|
|
6
|
+
| 'google'
|
|
7
|
+
| 'groq'
|
|
8
|
+
| 'mistral'
|
|
9
|
+
| 'xai'
|
|
10
|
+
| 'perplexity'
|
|
11
|
+
| 'ollama';
|
|
12
|
+
|
|
13
|
+
export interface ISmartAiOptions {
|
|
14
|
+
provider: TProvider;
|
|
15
|
+
model: string;
|
|
16
|
+
apiKey?: string;
|
|
17
|
+
/** For Ollama: base URL of the local server. Default: http://localhost:11434 */
|
|
18
|
+
baseUrl?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Ollama-specific model runtime options.
|
|
21
|
+
* Only used when provider === 'ollama'.
|
|
22
|
+
*/
|
|
23
|
+
ollamaOptions?: IOllamaModelOptions;
|
|
24
|
+
/**
|
|
25
|
+
* Enable Anthropic prompt caching on system + recent messages.
|
|
26
|
+
* Only used when provider === 'anthropic'. Default: true.
|
|
27
|
+
*/
|
|
28
|
+
promptCaching?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Ollama model runtime options passed in the request body `options` field.
|
|
33
|
+
* @see https://github.com/ollama/ollama/blob/main/docs/modelfile.md
|
|
34
|
+
*/
|
|
35
|
+
export interface IOllamaModelOptions {
|
|
36
|
+
/** Context window size. Default: 2048. */
|
|
37
|
+
num_ctx?: number;
|
|
38
|
+
/** 0 = deterministic. Default: 0.8. For Qwen models use 0.55. */
|
|
39
|
+
temperature?: number;
|
|
40
|
+
top_k?: number;
|
|
41
|
+
top_p?: number;
|
|
42
|
+
repeat_penalty?: number;
|
|
43
|
+
num_predict?: number;
|
|
44
|
+
stop?: string[];
|
|
45
|
+
seed?: number;
|
|
46
|
+
/**
|
|
47
|
+
* Enable thinking/reasoning mode (Qwen3, QwQ, DeepSeek-R1 etc.).
|
|
48
|
+
* The custom Ollama provider handles this directly.
|
|
49
|
+
*/
|
|
50
|
+
think?: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type { LanguageModelV3 };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { LanguageModelV3Middleware, LanguageModelV3Prompt } from '@ai-sdk/provider';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates middleware that adds Anthropic prompt caching directives.
|
|
5
|
+
* Marks the last system message and last user message with ephemeral cache control,
|
|
6
|
+
* reducing input token cost and latency on repeated calls.
|
|
7
|
+
*/
|
|
8
|
+
export function createAnthropicCachingMiddleware(): LanguageModelV3Middleware {
|
|
9
|
+
return {
|
|
10
|
+
specificationVersion: 'v3',
|
|
11
|
+
transformParams: async ({ params }) => {
|
|
12
|
+
const messages = [...params.prompt] as Array<Record<string, unknown>>;
|
|
13
|
+
|
|
14
|
+
// Find the last system message and last user message
|
|
15
|
+
let lastSystemIdx = -1;
|
|
16
|
+
let lastUserIdx = -1;
|
|
17
|
+
for (let i = 0; i < messages.length; i++) {
|
|
18
|
+
if (messages[i].role === 'system') lastSystemIdx = i;
|
|
19
|
+
if (messages[i].role === 'user') lastUserIdx = i;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const targets = [lastSystemIdx, lastUserIdx].filter(i => i >= 0);
|
|
23
|
+
for (const idx of targets) {
|
|
24
|
+
const msg = { ...messages[idx] };
|
|
25
|
+
msg.providerOptions = {
|
|
26
|
+
...(msg.providerOptions as Record<string, unknown> || {}),
|
|
27
|
+
anthropic: {
|
|
28
|
+
...((msg.providerOptions as Record<string, unknown>)?.anthropic as Record<string, unknown> || {}),
|
|
29
|
+
cacheControl: { type: 'ephemeral' },
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
messages[idx] = msg;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return { ...params, prompt: messages as unknown as LanguageModelV3Prompt };
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
LanguageModelV3,
|
|
3
|
+
LanguageModelV3CallOptions,
|
|
4
|
+
LanguageModelV3GenerateResult,
|
|
5
|
+
LanguageModelV3StreamResult,
|
|
6
|
+
LanguageModelV3StreamPart,
|
|
7
|
+
LanguageModelV3Prompt,
|
|
8
|
+
LanguageModelV3Content,
|
|
9
|
+
LanguageModelV3Usage,
|
|
10
|
+
LanguageModelV3FinishReason,
|
|
11
|
+
} from '@ai-sdk/provider';
|
|
12
|
+
import type { ISmartAiOptions, IOllamaModelOptions } from './smartai.interfaces.js';
|
|
13
|
+
|
|
14
|
+
interface IOllamaMessage {
|
|
15
|
+
role: string;
|
|
16
|
+
content: string;
|
|
17
|
+
images?: string[];
|
|
18
|
+
tool_calls?: Array<{
|
|
19
|
+
function: { name: string; arguments: Record<string, unknown> };
|
|
20
|
+
}>;
|
|
21
|
+
thinking?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface IOllamaTool {
|
|
25
|
+
type: 'function';
|
|
26
|
+
function: {
|
|
27
|
+
name: string;
|
|
28
|
+
description: string;
|
|
29
|
+
parameters: Record<string, unknown>;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Convert AI SDK V3 prompt messages to Ollama's message format.
|
|
35
|
+
*/
|
|
36
|
+
function convertPromptToOllamaMessages(prompt: LanguageModelV3Prompt): IOllamaMessage[] {
|
|
37
|
+
const messages: IOllamaMessage[] = [];
|
|
38
|
+
|
|
39
|
+
for (const msg of prompt) {
|
|
40
|
+
if (msg.role === 'system') {
|
|
41
|
+
// System message content is a plain string in V3
|
|
42
|
+
messages.push({ role: 'system', content: msg.content });
|
|
43
|
+
} else if (msg.role === 'user') {
|
|
44
|
+
let text = '';
|
|
45
|
+
const images: string[] = [];
|
|
46
|
+
for (const part of msg.content) {
|
|
47
|
+
if (part.type === 'text') {
|
|
48
|
+
text += part.text;
|
|
49
|
+
} else if (part.type === 'file' && part.mediaType?.startsWith('image/')) {
|
|
50
|
+
// Handle image files — Ollama expects base64 images
|
|
51
|
+
if (typeof part.data === 'string') {
|
|
52
|
+
images.push(part.data);
|
|
53
|
+
} else if (part.data instanceof Uint8Array) {
|
|
54
|
+
images.push(Buffer.from(part.data).toString('base64'));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const m: IOllamaMessage = { role: 'user', content: text };
|
|
59
|
+
if (images.length > 0) m.images = images;
|
|
60
|
+
messages.push(m);
|
|
61
|
+
} else if (msg.role === 'assistant') {
|
|
62
|
+
let text = '';
|
|
63
|
+
let thinking = '';
|
|
64
|
+
const toolCalls: IOllamaMessage['tool_calls'] = [];
|
|
65
|
+
for (const part of msg.content) {
|
|
66
|
+
if (part.type === 'text') {
|
|
67
|
+
text += part.text;
|
|
68
|
+
} else if (part.type === 'reasoning') {
|
|
69
|
+
thinking += part.text;
|
|
70
|
+
} else if (part.type === 'tool-call') {
|
|
71
|
+
const args = typeof part.input === 'string'
|
|
72
|
+
? JSON.parse(part.input as string)
|
|
73
|
+
: (part.input as Record<string, unknown>);
|
|
74
|
+
toolCalls.push({
|
|
75
|
+
function: {
|
|
76
|
+
name: part.toolName,
|
|
77
|
+
arguments: args,
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const m: IOllamaMessage = { role: 'assistant', content: text };
|
|
83
|
+
if (toolCalls.length > 0) m.tool_calls = toolCalls;
|
|
84
|
+
if (thinking) m.thinking = thinking;
|
|
85
|
+
messages.push(m);
|
|
86
|
+
} else if (msg.role === 'tool') {
|
|
87
|
+
for (const part of msg.content) {
|
|
88
|
+
if (part.type === 'tool-result') {
|
|
89
|
+
let resultContent = '';
|
|
90
|
+
if (part.output) {
|
|
91
|
+
if (part.output.type === 'text') {
|
|
92
|
+
resultContent = part.output.value;
|
|
93
|
+
} else if (part.output.type === 'json') {
|
|
94
|
+
resultContent = JSON.stringify(part.output.value);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
messages.push({ role: 'tool', content: resultContent });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return messages;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Convert AI SDK V3 tools to Ollama's tool format.
|
|
108
|
+
*/
|
|
109
|
+
function convertToolsToOllamaTools(tools: LanguageModelV3CallOptions['tools']): IOllamaTool[] | undefined {
|
|
110
|
+
if (!tools || tools.length === 0) return undefined;
|
|
111
|
+
|
|
112
|
+
return tools
|
|
113
|
+
.filter((t): t is Extract<typeof t, { type: 'function' }> => t.type === 'function')
|
|
114
|
+
.map(t => ({
|
|
115
|
+
type: 'function' as const,
|
|
116
|
+
function: {
|
|
117
|
+
name: t.name,
|
|
118
|
+
description: t.description ?? '',
|
|
119
|
+
parameters: t.inputSchema as Record<string, unknown>,
|
|
120
|
+
},
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function makeUsage(promptTokens?: number, completionTokens?: number): LanguageModelV3Usage {
|
|
125
|
+
return {
|
|
126
|
+
inputTokens: {
|
|
127
|
+
total: promptTokens,
|
|
128
|
+
noCache: undefined,
|
|
129
|
+
cacheRead: undefined,
|
|
130
|
+
cacheWrite: undefined,
|
|
131
|
+
},
|
|
132
|
+
outputTokens: {
|
|
133
|
+
total: completionTokens,
|
|
134
|
+
text: completionTokens,
|
|
135
|
+
reasoning: undefined,
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function makeFinishReason(reason?: string): LanguageModelV3FinishReason {
|
|
141
|
+
if (reason === 'tool_calls' || reason === 'tool-calls') {
|
|
142
|
+
return { unified: 'tool-calls', raw: reason };
|
|
143
|
+
}
|
|
144
|
+
return { unified: 'stop', raw: reason ?? 'stop' };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let idCounter = 0;
|
|
148
|
+
function generateId(): string {
|
|
149
|
+
return `ollama-${Date.now()}-${idCounter++}`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Custom LanguageModelV3 implementation for Ollama.
|
|
154
|
+
* Calls Ollama's native /api/chat endpoint directly to support
|
|
155
|
+
* think, num_ctx, temperature, and other model options.
|
|
156
|
+
*/
|
|
157
|
+
export function createOllamaModel(options: ISmartAiOptions): LanguageModelV3 {
|
|
158
|
+
const baseUrl = options.baseUrl ?? 'http://localhost:11434';
|
|
159
|
+
const modelId = options.model;
|
|
160
|
+
const ollamaOpts: IOllamaModelOptions = { ...options.ollamaOptions };
|
|
161
|
+
|
|
162
|
+
// Apply default temperature of 0.55 for Qwen models
|
|
163
|
+
if (modelId.toLowerCase().includes('qwen') && ollamaOpts.temperature === undefined) {
|
|
164
|
+
ollamaOpts.temperature = 0.55;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const model: LanguageModelV3 = {
|
|
168
|
+
specificationVersion: 'v3',
|
|
169
|
+
provider: 'ollama',
|
|
170
|
+
modelId,
|
|
171
|
+
supportedUrls: {},
|
|
172
|
+
|
|
173
|
+
async doGenerate(callOptions: LanguageModelV3CallOptions): Promise<LanguageModelV3GenerateResult> {
|
|
174
|
+
const messages = convertPromptToOllamaMessages(callOptions.prompt);
|
|
175
|
+
const tools = convertToolsToOllamaTools(callOptions.tools);
|
|
176
|
+
|
|
177
|
+
const ollamaModelOptions: Record<string, unknown> = { ...ollamaOpts };
|
|
178
|
+
// Override with call-level options if provided
|
|
179
|
+
if (callOptions.temperature !== undefined) ollamaModelOptions.temperature = callOptions.temperature;
|
|
180
|
+
if (callOptions.topP !== undefined) ollamaModelOptions.top_p = callOptions.topP;
|
|
181
|
+
if (callOptions.topK !== undefined) ollamaModelOptions.top_k = callOptions.topK;
|
|
182
|
+
if (callOptions.maxOutputTokens !== undefined) ollamaModelOptions.num_predict = callOptions.maxOutputTokens;
|
|
183
|
+
if (callOptions.seed !== undefined) ollamaModelOptions.seed = callOptions.seed;
|
|
184
|
+
if (callOptions.stopSequences) ollamaModelOptions.stop = callOptions.stopSequences;
|
|
185
|
+
// Remove think from options — it goes at the top level
|
|
186
|
+
const { think, ...modelOpts } = ollamaModelOptions;
|
|
187
|
+
|
|
188
|
+
const requestBody: Record<string, unknown> = {
|
|
189
|
+
model: modelId,
|
|
190
|
+
messages,
|
|
191
|
+
stream: false,
|
|
192
|
+
options: modelOpts,
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
// Add think parameter at the top level (Ollama API requirement)
|
|
196
|
+
if (ollamaOpts.think !== undefined) {
|
|
197
|
+
requestBody.think = ollamaOpts.think;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (tools) requestBody.tools = tools;
|
|
201
|
+
|
|
202
|
+
const response = await fetch(`${baseUrl}/api/chat`, {
|
|
203
|
+
method: 'POST',
|
|
204
|
+
headers: { 'Content-Type': 'application/json' },
|
|
205
|
+
body: JSON.stringify(requestBody),
|
|
206
|
+
signal: callOptions.abortSignal,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
if (!response.ok) {
|
|
210
|
+
const body = await response.text();
|
|
211
|
+
throw new Error(`Ollama API error ${response.status}: ${body}`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const result = await response.json() as Record<string, unknown>;
|
|
215
|
+
const message = result.message as Record<string, unknown>;
|
|
216
|
+
|
|
217
|
+
// Build content array
|
|
218
|
+
const content: LanguageModelV3Content[] = [];
|
|
219
|
+
|
|
220
|
+
// Add reasoning if present
|
|
221
|
+
if (message.thinking && typeof message.thinking === 'string') {
|
|
222
|
+
content.push({ type: 'reasoning', text: message.thinking });
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Add text content
|
|
226
|
+
if (message.content && typeof message.content === 'string') {
|
|
227
|
+
content.push({ type: 'text', text: message.content });
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Add tool calls if present
|
|
231
|
+
if (Array.isArray(message.tool_calls)) {
|
|
232
|
+
for (const tc of message.tool_calls as Array<Record<string, unknown>>) {
|
|
233
|
+
const fn = tc.function as Record<string, unknown>;
|
|
234
|
+
content.push({
|
|
235
|
+
type: 'tool-call',
|
|
236
|
+
toolCallId: generateId(),
|
|
237
|
+
toolName: fn.name as string,
|
|
238
|
+
input: JSON.stringify(fn.arguments),
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const finishReason = Array.isArray(message.tool_calls) && (message.tool_calls as unknown[]).length > 0
|
|
244
|
+
? makeFinishReason('tool_calls')
|
|
245
|
+
: makeFinishReason('stop');
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
content,
|
|
249
|
+
finishReason,
|
|
250
|
+
usage: makeUsage(
|
|
251
|
+
(result.prompt_eval_count as number) ?? undefined,
|
|
252
|
+
(result.eval_count as number) ?? undefined,
|
|
253
|
+
),
|
|
254
|
+
warnings: [],
|
|
255
|
+
request: { body: requestBody },
|
|
256
|
+
};
|
|
257
|
+
},
|
|
258
|
+
|
|
259
|
+
async doStream(callOptions: LanguageModelV3CallOptions): Promise<LanguageModelV3StreamResult> {
|
|
260
|
+
const messages = convertPromptToOllamaMessages(callOptions.prompt);
|
|
261
|
+
const tools = convertToolsToOllamaTools(callOptions.tools);
|
|
262
|
+
|
|
263
|
+
const ollamaModelOptions: Record<string, unknown> = { ...ollamaOpts };
|
|
264
|
+
if (callOptions.temperature !== undefined) ollamaModelOptions.temperature = callOptions.temperature;
|
|
265
|
+
if (callOptions.topP !== undefined) ollamaModelOptions.top_p = callOptions.topP;
|
|
266
|
+
if (callOptions.topK !== undefined) ollamaModelOptions.top_k = callOptions.topK;
|
|
267
|
+
if (callOptions.maxOutputTokens !== undefined) ollamaModelOptions.num_predict = callOptions.maxOutputTokens;
|
|
268
|
+
if (callOptions.seed !== undefined) ollamaModelOptions.seed = callOptions.seed;
|
|
269
|
+
if (callOptions.stopSequences) ollamaModelOptions.stop = callOptions.stopSequences;
|
|
270
|
+
const { think, ...modelOpts } = ollamaModelOptions;
|
|
271
|
+
|
|
272
|
+
const requestBody: Record<string, unknown> = {
|
|
273
|
+
model: modelId,
|
|
274
|
+
messages,
|
|
275
|
+
stream: true,
|
|
276
|
+
options: modelOpts,
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
if (ollamaOpts.think !== undefined) {
|
|
280
|
+
requestBody.think = ollamaOpts.think;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (tools) requestBody.tools = tools;
|
|
284
|
+
|
|
285
|
+
const response = await fetch(`${baseUrl}/api/chat`, {
|
|
286
|
+
method: 'POST',
|
|
287
|
+
headers: { 'Content-Type': 'application/json' },
|
|
288
|
+
body: JSON.stringify(requestBody),
|
|
289
|
+
signal: callOptions.abortSignal,
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
if (!response.ok) {
|
|
293
|
+
const body = await response.text();
|
|
294
|
+
throw new Error(`Ollama API error ${response.status}: ${body}`);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const reader = response.body!.getReader();
|
|
298
|
+
const decoder = new TextDecoder();
|
|
299
|
+
|
|
300
|
+
const textId = generateId();
|
|
301
|
+
const reasoningId = generateId();
|
|
302
|
+
let textStarted = false;
|
|
303
|
+
let reasoningStarted = false;
|
|
304
|
+
let hasToolCalls = false;
|
|
305
|
+
let closed = false;
|
|
306
|
+
|
|
307
|
+
const stream = new ReadableStream<LanguageModelV3StreamPart>({
|
|
308
|
+
async pull(controller) {
|
|
309
|
+
if (closed) return;
|
|
310
|
+
|
|
311
|
+
const processLine = (line: string) => {
|
|
312
|
+
if (!line.trim()) return;
|
|
313
|
+
let json: Record<string, unknown>;
|
|
314
|
+
try {
|
|
315
|
+
json = JSON.parse(line);
|
|
316
|
+
} catch {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const msg = json.message as Record<string, unknown> | undefined;
|
|
321
|
+
|
|
322
|
+
// Handle thinking/reasoning content
|
|
323
|
+
if (msg?.thinking && typeof msg.thinking === 'string') {
|
|
324
|
+
if (!reasoningStarted) {
|
|
325
|
+
reasoningStarted = true;
|
|
326
|
+
controller.enqueue({ type: 'reasoning-start', id: reasoningId });
|
|
327
|
+
}
|
|
328
|
+
controller.enqueue({ type: 'reasoning-delta', id: reasoningId, delta: msg.thinking });
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Handle text content
|
|
332
|
+
if (msg?.content && typeof msg.content === 'string') {
|
|
333
|
+
if (reasoningStarted && !textStarted) {
|
|
334
|
+
controller.enqueue({ type: 'reasoning-end', id: reasoningId });
|
|
335
|
+
}
|
|
336
|
+
if (!textStarted) {
|
|
337
|
+
textStarted = true;
|
|
338
|
+
controller.enqueue({ type: 'text-start', id: textId });
|
|
339
|
+
}
|
|
340
|
+
controller.enqueue({ type: 'text-delta', id: textId, delta: msg.content });
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Handle tool calls
|
|
344
|
+
if (Array.isArray(msg?.tool_calls)) {
|
|
345
|
+
hasToolCalls = true;
|
|
346
|
+
for (const tc of msg!.tool_calls as Array<Record<string, unknown>>) {
|
|
347
|
+
const fn = tc.function as Record<string, unknown>;
|
|
348
|
+
const callId = generateId();
|
|
349
|
+
controller.enqueue({
|
|
350
|
+
type: 'tool-call',
|
|
351
|
+
toolCallId: callId,
|
|
352
|
+
toolName: fn.name as string,
|
|
353
|
+
input: JSON.stringify(fn.arguments),
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Handle done
|
|
359
|
+
if (json.done) {
|
|
360
|
+
if (reasoningStarted && !textStarted) {
|
|
361
|
+
controller.enqueue({ type: 'reasoning-end', id: reasoningId });
|
|
362
|
+
}
|
|
363
|
+
if (textStarted) {
|
|
364
|
+
controller.enqueue({ type: 'text-end', id: textId });
|
|
365
|
+
}
|
|
366
|
+
controller.enqueue({
|
|
367
|
+
type: 'finish',
|
|
368
|
+
finishReason: hasToolCalls
|
|
369
|
+
? makeFinishReason('tool_calls')
|
|
370
|
+
: makeFinishReason('stop'),
|
|
371
|
+
usage: makeUsage(
|
|
372
|
+
(json.prompt_eval_count as number) ?? undefined,
|
|
373
|
+
(json.eval_count as number) ?? undefined,
|
|
374
|
+
),
|
|
375
|
+
});
|
|
376
|
+
closed = true;
|
|
377
|
+
controller.close();
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
try {
|
|
382
|
+
let buffer = '';
|
|
383
|
+
while (true) {
|
|
384
|
+
const { done, value } = await reader.read();
|
|
385
|
+
if (done) {
|
|
386
|
+
if (buffer.trim()) processLine(buffer);
|
|
387
|
+
if (!closed) {
|
|
388
|
+
controller.enqueue({
|
|
389
|
+
type: 'finish',
|
|
390
|
+
finishReason: makeFinishReason('stop'),
|
|
391
|
+
usage: makeUsage(undefined, undefined),
|
|
392
|
+
});
|
|
393
|
+
closed = true;
|
|
394
|
+
controller.close();
|
|
395
|
+
}
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
buffer += decoder.decode(value, { stream: true });
|
|
400
|
+
const lines = buffer.split('\n');
|
|
401
|
+
buffer = lines.pop() || '';
|
|
402
|
+
for (const line of lines) {
|
|
403
|
+
processLine(line);
|
|
404
|
+
if (closed) return;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
} catch (error) {
|
|
408
|
+
if (!closed) {
|
|
409
|
+
controller.error(error);
|
|
410
|
+
closed = true;
|
|
411
|
+
}
|
|
412
|
+
} finally {
|
|
413
|
+
reader.releaseLock();
|
|
414
|
+
}
|
|
415
|
+
},
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
return {
|
|
419
|
+
stream,
|
|
420
|
+
request: { body: requestBody },
|
|
421
|
+
};
|
|
422
|
+
},
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
return model;
|
|
426
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import { Readable } from 'stream';
|
|
3
|
+
|
|
4
|
+
export interface IOpenAiTtsOptions {
|
|
5
|
+
apiKey: string;
|
|
6
|
+
text: string;
|
|
7
|
+
voice?: 'alloy' | 'echo' | 'fable' | 'onyx' | 'nova' | 'shimmer';
|
|
8
|
+
model?: 'tts-1' | 'tts-1-hd';
|
|
9
|
+
responseFormat?: 'mp3' | 'opus' | 'aac' | 'flac';
|
|
10
|
+
speed?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function textToSpeech(options: IOpenAiTtsOptions): Promise<NodeJS.ReadableStream> {
|
|
14
|
+
const client = new plugins.OpenAI({ apiKey: options.apiKey });
|
|
15
|
+
const result = await client.audio.speech.create({
|
|
16
|
+
model: options.model ?? 'tts-1',
|
|
17
|
+
voice: options.voice ?? 'alloy',
|
|
18
|
+
input: options.text,
|
|
19
|
+
response_format: options.responseFormat ?? 'mp3',
|
|
20
|
+
speed: options.speed ?? 1,
|
|
21
|
+
});
|
|
22
|
+
const stream = result.body;
|
|
23
|
+
return Readable.fromWeb(stream as any);
|
|
24
|
+
}
|