feat(OllamaProvider): add model options, streaming support, and thinking tokens

- Add IOllamaModelOptions interface for runtime options (num_ctx, temperature, etc.)
- Extend IOllamaProviderOptions with defaultOptions and defaultTimeout
- Add IOllamaChatOptions for per-request overrides
- Add IOllamaStreamChunk and IOllamaChatResponse interfaces
- Add chatStreamResponse() for async iteration with options
- Add collectStreamResponse() for streaming with progress callback
- Add chatWithOptions() for non-streaming with full options
- Update chat() to use defaultOptions and defaultTimeout
This commit is contained in:
2026-01-20 00:02:45 +00:00
parent a556053510
commit 126e9b239b
12 changed files with 320 additions and 74 deletions

View File

@@ -18,22 +18,22 @@
"@git.zone/tsbuild": "^4.1.2", "@git.zone/tsbuild": "^4.1.2",
"@git.zone/tsbundle": "^2.8.1", "@git.zone/tsbundle": "^2.8.1",
"@git.zone/tsrun": "^2.0.1", "@git.zone/tsrun": "^2.0.1",
"@git.zone/tstest": "^3.1.4", "@git.zone/tstest": "^3.1.6",
"@push.rocks/qenv": "^6.1.3", "@push.rocks/qenv": "^6.1.3",
"@types/node": "^22.15.17", "@types/node": "^25.0.9",
"typescript": "^5.9.3" "typescript": "^5.9.3"
}, },
"dependencies": { "dependencies": {
"@anthropic-ai/sdk": "^0.71.2", "@anthropic-ai/sdk": "^0.71.2",
"@mistralai/mistralai": "^1.11.0", "@mistralai/mistralai": "^1.12.0",
"@push.rocks/smartarray": "^1.1.0", "@push.rocks/smartarray": "^1.1.0",
"@push.rocks/smartfile": "^11.2.7", "@push.rocks/smartfs": "^1.3.1",
"@push.rocks/smartpath": "^6.0.0", "@push.rocks/smartpath": "^6.0.0",
"@push.rocks/smartpdf": "^4.1.1", "@push.rocks/smartpdf": "^4.1.1",
"@push.rocks/smartpromise": "^4.2.3", "@push.rocks/smartpromise": "^4.2.3",
"@push.rocks/smartrequest": "^5.0.1", "@push.rocks/smartrequest": "^5.0.1",
"@push.rocks/webstream": "^1.0.10", "@push.rocks/webstream": "^1.0.10",
"openai": "^5.12.2" "openai": "^6.16.0"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

106
pnpm-lock.yaml generated
View File

@@ -12,14 +12,14 @@ importers:
specifier: ^0.71.2 specifier: ^0.71.2
version: 0.71.2(zod@3.25.76) version: 0.71.2(zod@3.25.76)
'@mistralai/mistralai': '@mistralai/mistralai':
specifier: ^1.11.0 specifier: ^1.12.0
version: 1.11.0 version: 1.12.0
'@push.rocks/smartarray': '@push.rocks/smartarray':
specifier: ^1.1.0 specifier: ^1.1.0
version: 1.1.0 version: 1.1.0
'@push.rocks/smartfile': '@push.rocks/smartfs':
specifier: ^11.2.7 specifier: ^1.3.1
version: 11.2.7 version: 1.3.1
'@push.rocks/smartpath': '@push.rocks/smartpath':
specifier: ^6.0.0 specifier: ^6.0.0
version: 6.0.0 version: 6.0.0
@@ -36,8 +36,8 @@ importers:
specifier: ^1.0.10 specifier: ^1.0.10
version: 1.0.10 version: 1.0.10
openai: openai:
specifier: ^5.12.2 specifier: ^6.16.0
version: 5.12.2(ws@8.18.3)(zod@3.25.76) version: 6.16.0(ws@8.18.3)(zod@3.25.76)
devDependencies: devDependencies:
'@git.zone/tsbuild': '@git.zone/tsbuild':
specifier: ^4.1.2 specifier: ^4.1.2
@@ -49,14 +49,14 @@ importers:
specifier: ^2.0.1 specifier: ^2.0.1
version: 2.0.1 version: 2.0.1
'@git.zone/tstest': '@git.zone/tstest':
specifier: ^3.1.4 specifier: ^3.1.6
version: 3.1.4(@aws-sdk/credential-providers@3.808.0)(socks@2.8.4)(typescript@5.9.3) version: 3.1.6(@aws-sdk/credential-providers@3.808.0)(socks@2.8.4)(typescript@5.9.3)
'@push.rocks/qenv': '@push.rocks/qenv':
specifier: ^6.1.3 specifier: ^6.1.3
version: 6.1.3 version: 6.1.3
'@types/node': '@types/node':
specifier: ^22.15.17 specifier: ^25.0.9
version: 22.15.17 version: 25.0.9
typescript: typescript:
specifier: ^5.9.3 specifier: ^5.9.3
version: 5.9.3 version: 5.9.3
@@ -816,8 +816,8 @@ packages:
resolution: {integrity: sha512-NEcnsjvlC1o3Z6SS3VhKCf6Ev+Sh4EAinmggslrIR/ppMrvjDbXNFXoyr3PB+GLeSAR0JRZ1fGvVYjpEzjBdIg==} resolution: {integrity: sha512-NEcnsjvlC1o3Z6SS3VhKCf6Ev+Sh4EAinmggslrIR/ppMrvjDbXNFXoyr3PB+GLeSAR0JRZ1fGvVYjpEzjBdIg==}
hasBin: true hasBin: true
'@git.zone/tstest@3.1.4': '@git.zone/tstest@3.1.6':
resolution: {integrity: sha512-S7kubbb0yLYOh/QAzFsjG6a20lZiyNKo4pt0yK1yvd9I7X8Rw6/mCT/BicLkan7G7Nk7scUfxaK9+aFsHmdQdw==} resolution: {integrity: sha512-xRGc6wO4rJ6mohPCMIBDRH+oNjiIvX6Jeo8v/Y5o5VyKSHFmqol7FCKSBrojMcqgBpESnLHFPJAAOmT9W3JV8Q==}
hasBin: true hasBin: true
'@happy-dom/global-registrator@15.11.7': '@happy-dom/global-registrator@15.11.7':
@@ -1135,8 +1135,8 @@ packages:
'@lit/reactive-element@2.1.1': '@lit/reactive-element@2.1.1':
resolution: {integrity: sha512-N+dm5PAYdQ8e6UlywyyrgI2t++wFGXfHx+dSJ1oBrg6FAxUj40jId++EaRm80MKX5JnlH1sBsyZ5h0bcZKemCg==} resolution: {integrity: sha512-N+dm5PAYdQ8e6UlywyyrgI2t++wFGXfHx+dSJ1oBrg6FAxUj40jId++EaRm80MKX5JnlH1sBsyZ5h0bcZKemCg==}
'@mistralai/mistralai@1.11.0': '@mistralai/mistralai@1.12.0':
resolution: {integrity: sha512-6/BVj2mcaggYbpMzNSxtqtM2Tv/Jb5845XFd2CMYFO+O5VBkX70iLjtkBBTI4JFhh1l9vTCIMYXBVOjLoBVHGQ==} resolution: {integrity: sha512-oDr1hcS3wsIT/QupBG93TNiA5kilwBYoAIyl5BNYqMM2Ix/xsNq+wT8b++uhp/GTUMx44n+8Bn1mkATbwxe6bQ==}
'@mixmark-io/domino@2.2.0': '@mixmark-io/domino@2.2.0':
resolution: {integrity: sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==} resolution: {integrity: sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==}
@@ -2497,8 +2497,11 @@ packages:
'@types/node@16.9.1': '@types/node@16.9.1':
resolution: {integrity: sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==} resolution: {integrity: sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==}
'@types/node@22.15.17': '@types/node@22.19.7':
resolution: {integrity: sha512-wIX2aSZL5FE+MR0JlvF87BNVrtFWf6AE6rxSE9X7OwnVvoyCQjpzSRJ+M87se/4QCkCiebQAqrJ0y6fwIyi7nw==} resolution: {integrity: sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==}
'@types/node@25.0.9':
resolution: {integrity: sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw==}
'@types/ping@0.4.4': '@types/ping@0.4.4':
resolution: {integrity: sha512-ifvo6w2f5eJYlXm+HiVx67iJe8WZp87sfa683nlqED5Vnt9Z93onkokNoWqOG21EaE8fMxyKPobE+mkPEyxsdw==} resolution: {integrity: sha512-ifvo6w2f5eJYlXm+HiVx67iJe8WZp87sfa683nlqED5Vnt9Z93onkokNoWqOG21EaE8fMxyKPobE+mkPEyxsdw==}
@@ -4025,12 +4028,12 @@ packages:
resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==}
engines: {node: '>=12'} engines: {node: '>=12'}
openai@5.12.2: openai@6.16.0:
resolution: {integrity: sha512-xqzHHQch5Tws5PcKR2xsZGX9xtch+JQFz5zb14dGqlshmmDAFBFEWmeIpf7wVqWV+w7Emj7jRgkNJakyKE0tYQ==} resolution: {integrity: sha512-fZ1uBqjFUjXzbGc35fFtYKEOxd20kd9fDpFeqWtsOZWiubY8CZ1NAlXHW3iathaFvqmNtCWMIsosCuyeI7Joxg==}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
ws: ^8.18.0 ws: ^8.18.0
zod: ^3.23.8 zod: ^3.25 || ^4.0
peerDependenciesMeta: peerDependenciesMeta:
ws: ws:
optional: true optional: true
@@ -4721,6 +4724,9 @@ packages:
undici-types@6.21.0: undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
undici-types@7.16.0:
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
unified@11.0.5: unified@11.0.5:
resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==}
@@ -6917,7 +6923,7 @@ snapshots:
'@push.rocks/smartshell': 3.3.0 '@push.rocks/smartshell': 3.3.0
tsx: 4.21.0 tsx: 4.21.0
'@git.zone/tstest@3.1.4(@aws-sdk/credential-providers@3.808.0)(socks@2.8.4)(typescript@5.9.3)': '@git.zone/tstest@3.1.6(@aws-sdk/credential-providers@3.808.0)(socks@2.8.4)(typescript@5.9.3)':
dependencies: dependencies:
'@api.global/typedserver': 3.0.80 '@api.global/typedserver': 3.0.80
'@git.zone/tsbundle': 2.8.1 '@git.zone/tsbundle': 2.8.1
@@ -7071,7 +7077,7 @@ snapshots:
'@inquirer/figures': 1.0.15 '@inquirer/figures': 1.0.15
'@inquirer/type': 2.0.0 '@inquirer/type': 2.0.0
'@types/mute-stream': 0.0.4 '@types/mute-stream': 0.0.4
'@types/node': 22.15.17 '@types/node': 22.19.7
'@types/wrap-ansi': 3.0.0 '@types/wrap-ansi': 3.0.0
ansi-escapes: 4.3.2 ansi-escapes: 4.3.2
cli-width: 4.1.0 cli-width: 4.1.0
@@ -7361,7 +7367,7 @@ snapshots:
dependencies: dependencies:
'@lit-labs/ssr-dom-shim': 1.4.0 '@lit-labs/ssr-dom-shim': 1.4.0
'@mistralai/mistralai@1.11.0': '@mistralai/mistralai@1.12.0':
dependencies: dependencies:
zod: 3.25.76 zod: 3.25.76
zod-to-json-schema: 3.25.1(zod@3.25.76) zod-to-json-schema: 3.25.1(zod@3.25.76)
@@ -9651,27 +9657,27 @@ snapshots:
'@types/bn.js@5.2.0': '@types/bn.js@5.2.0':
dependencies: dependencies:
'@types/node': 22.15.17 '@types/node': 22.19.7
'@types/body-parser@1.19.6': '@types/body-parser@1.19.6':
dependencies: dependencies:
'@types/connect': 3.4.38 '@types/connect': 3.4.38
'@types/node': 22.15.17 '@types/node': 22.19.7
'@types/buffer-json@2.0.3': {} '@types/buffer-json@2.0.3': {}
'@types/clean-css@4.2.11': '@types/clean-css@4.2.11':
dependencies: dependencies:
'@types/node': 22.15.17 '@types/node': 22.19.7
source-map: 0.6.1 source-map: 0.6.1
'@types/connect@3.4.38': '@types/connect@3.4.38':
dependencies: dependencies:
'@types/node': 22.15.17 '@types/node': 22.19.7
'@types/cors@2.8.19': '@types/cors@2.8.19':
dependencies: dependencies:
'@types/node': 22.15.17 '@types/node': 22.19.7
'@types/debug@4.1.12': '@types/debug@4.1.12':
dependencies: dependencies:
@@ -9681,7 +9687,7 @@ snapshots:
'@types/dns-packet@5.6.5': '@types/dns-packet@5.6.5':
dependencies: dependencies:
'@types/node': 22.15.17 '@types/node': 22.19.7
'@types/elliptic@6.4.18': '@types/elliptic@6.4.18':
dependencies: dependencies:
@@ -9689,7 +9695,7 @@ snapshots:
'@types/express-serve-static-core@5.0.7': '@types/express-serve-static-core@5.0.7':
dependencies: dependencies:
'@types/node': 22.15.17 '@types/node': 22.19.7
'@types/qs': 6.14.0 '@types/qs': 6.14.0
'@types/range-parser': 1.2.7 '@types/range-parser': 1.2.7
'@types/send': 0.17.5 '@types/send': 0.17.5
@@ -9713,16 +9719,16 @@ snapshots:
'@types/fs-extra@11.0.4': '@types/fs-extra@11.0.4':
dependencies: dependencies:
'@types/jsonfile': 6.1.4 '@types/jsonfile': 6.1.4
'@types/node': 22.15.17 '@types/node': 22.19.7
'@types/fs-extra@9.0.13': '@types/fs-extra@9.0.13':
dependencies: dependencies:
'@types/node': 22.15.17 '@types/node': 22.19.7
'@types/glob@7.2.0': '@types/glob@7.2.0':
dependencies: dependencies:
'@types/minimatch': 5.1.2 '@types/minimatch': 5.1.2
'@types/node': 22.15.17 '@types/node': 22.19.7
'@types/hast@3.0.4': '@types/hast@3.0.4':
dependencies: dependencies:
@@ -9744,7 +9750,7 @@ snapshots:
'@types/jsonfile@6.1.4': '@types/jsonfile@6.1.4':
dependencies: dependencies:
'@types/node': 22.15.17 '@types/node': 22.19.7
'@types/mdast@4.0.4': '@types/mdast@4.0.4':
dependencies: dependencies:
@@ -9762,18 +9768,22 @@ snapshots:
'@types/mute-stream@0.0.4': '@types/mute-stream@0.0.4':
dependencies: dependencies:
'@types/node': 22.15.17 '@types/node': 22.19.7
'@types/node-forge@1.3.11': '@types/node-forge@1.3.11':
dependencies: dependencies:
'@types/node': 22.15.17 '@types/node': 22.19.7
'@types/node@16.9.1': {} '@types/node@16.9.1': {}
'@types/node@22.15.17': '@types/node@22.19.7':
dependencies: dependencies:
undici-types: 6.21.0 undici-types: 6.21.0
'@types/node@25.0.9':
dependencies:
undici-types: 7.16.0
'@types/ping@0.4.4': {} '@types/ping@0.4.4': {}
'@types/qs@6.14.0': {} '@types/qs@6.14.0': {}
@@ -9789,28 +9799,28 @@ snapshots:
'@types/send@0.17.5': '@types/send@0.17.5':
dependencies: dependencies:
'@types/mime': 1.3.5 '@types/mime': 1.3.5
'@types/node': 22.15.17 '@types/node': 22.19.7
'@types/serve-static@1.15.8': '@types/serve-static@1.15.8':
dependencies: dependencies:
'@types/http-errors': 2.0.5 '@types/http-errors': 2.0.5
'@types/node': 22.15.17 '@types/node': 22.19.7
'@types/send': 0.17.5 '@types/send': 0.17.5
'@types/serve-static@2.2.0': '@types/serve-static@2.2.0':
dependencies: dependencies:
'@types/http-errors': 2.0.5 '@types/http-errors': 2.0.5
'@types/node': 22.15.17 '@types/node': 22.19.7
'@types/symbol-tree@3.2.5': {} '@types/symbol-tree@3.2.5': {}
'@types/tar-stream@3.1.4': '@types/tar-stream@3.1.4':
dependencies: dependencies:
'@types/node': 22.15.17 '@types/node': 22.19.7
'@types/through2@2.0.41': '@types/through2@2.0.41':
dependencies: dependencies:
'@types/node': 22.15.17 '@types/node': 22.19.7
'@types/trusted-types@2.0.7': {} '@types/trusted-types@2.0.7': {}
@@ -9832,7 +9842,7 @@ snapshots:
'@types/whatwg-url@8.2.2': '@types/whatwg-url@8.2.2':
dependencies: dependencies:
'@types/node': 22.15.17 '@types/node': 22.19.7
'@types/webidl-conversions': 7.0.3 '@types/webidl-conversions': 7.0.3
'@types/which@3.0.4': {} '@types/which@3.0.4': {}
@@ -9841,11 +9851,11 @@ snapshots:
'@types/ws@8.18.1': '@types/ws@8.18.1':
dependencies: dependencies:
'@types/node': 22.15.17 '@types/node': 22.19.7
'@types/yauzl@2.10.3': '@types/yauzl@2.10.3':
dependencies: dependencies:
'@types/node': 22.15.17 '@types/node': 22.19.7
optional: true optional: true
'@ungap/structured-clone@1.3.0': {} '@ungap/structured-clone@1.3.0': {}
@@ -10305,7 +10315,7 @@ snapshots:
engine.io@6.6.4: engine.io@6.6.4:
dependencies: dependencies:
'@types/cors': 2.8.19 '@types/cors': 2.8.19
'@types/node': 22.15.17 '@types/node': 22.19.7
accepts: 1.3.8 accepts: 1.3.8
base64id: 2.0.0 base64id: 2.0.0
cookie: 0.7.2 cookie: 0.7.2
@@ -11572,7 +11582,7 @@ snapshots:
is-docker: 2.2.1 is-docker: 2.2.1
is-wsl: 2.2.0 is-wsl: 2.2.0
openai@5.12.2(ws@8.18.3)(zod@3.25.76): openai@6.16.0(ws@8.18.3)(zod@3.25.76):
optionalDependencies: optionalDependencies:
ws: 8.18.3 ws: 8.18.3
zod: 3.25.76 zod: 3.25.76
@@ -12367,6 +12377,8 @@ snapshots:
undici-types@6.21.0: {} undici-types@6.21.0: {}
undici-types@7.16.0: {}
unified@11.0.5: unified@11.0.5:
dependencies: dependencies:
'@types/unist': 3.0.3 '@types/unist': 3.0.3

View File

@@ -3,9 +3,10 @@
## Dependencies ## Dependencies
- Uses `@git.zone/tstest` v3.x for testing (import from `@git.zone/tstest/tapbundle`) - Uses `@git.zone/tstest` v3.x for testing (import from `@git.zone/tstest/tapbundle`)
- `@push.rocks/smartfile` is kept at v11 to avoid migration to factory pattern - `@push.rocks/smartfs` v1.x for file system operations (replaced smartfile)
- `@anthropic-ai/sdk` v0.71.x with extended thinking support - `@anthropic-ai/sdk` v0.71.x with extended thinking support
- `@mistralai/mistralai` v1.x for Mistral OCR and chat capabilities - `@mistralai/mistralai` v1.x for Mistral OCR and chat capabilities
- `openai` v6.x for OpenAI API integration
- `@push.rocks/smartrequest` v5.x - uses `response.stream()` + `Readable.fromWeb()` for streaming - `@push.rocks/smartrequest` v5.x - uses `response.stream()` + `Readable.fromWeb()` for streaming
## Important Notes ## Important Notes

View File

@@ -6,7 +6,7 @@
[![TypeScript](https://img.shields.io/badge/TypeScript-5.x-blue.svg)](https://www.typescriptlang.org/) [![TypeScript](https://img.shields.io/badge/TypeScript-5.x-blue.svg)](https://www.typescriptlang.org/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
SmartAI unifies the world's leading AI providers - OpenAI, Anthropic, Perplexity, Ollama, Groq, XAI, Exo, and ElevenLabs - under a single, elegant TypeScript interface. Build AI applications at lightning speed without vendor lock-in. SmartAI unifies the world's leading AI providers - OpenAI, Anthropic, Mistral, Perplexity, Ollama, Groq, XAI, Exo, and ElevenLabs - under a single, elegant TypeScript interface. Build AI applications at lightning speed without vendor lock-in.
## Issue Reporting and Security ## Issue Reporting and Security
@@ -58,6 +58,7 @@ Choose the right provider for your use case:
| -------------- | :--: | :-------: | :-: | :----: | :-------: | :------: | :----: | --------------------------------------------------------------- | | -------------- | :--: | :-------: | :-: | :----: | :-------: | :------: | :----: | --------------------------------------------------------------- |
| **OpenAI** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | • gpt-image-1<br>• DALL-E 3<br>• Deep research API | | **OpenAI** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | • gpt-image-1<br>• DALL-E 3<br>• Deep research API |
| **Anthropic** | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ | • Claude Sonnet 4.5<br>• Superior reasoning<br>• Web search API | | **Anthropic** | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ | • Claude Sonnet 4.5<br>• Superior reasoning<br>• Web search API |
| **Mistral** | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ | • Native PDF OCR<br>• mistral-large<br>• Fast inference |
| **ElevenLabs** | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | • Premium TTS<br>• 70+ languages<br>• Natural voices | | **ElevenLabs** | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | • Premium TTS<br>• 70+ languages<br>• Natural voices |
| **Ollama** | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ | • 100% local<br>• Privacy-first<br>• No API costs | | **Ollama** | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ | • 100% local<br>• Privacy-first<br>• No API costs |
| **XAI** | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | • Grok models<br>• Real-time data<br>• Uncensored | | **XAI** | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | • Grok models<br>• Real-time data<br>• Uncensored |
@@ -282,6 +283,38 @@ const response = await anthropic.chat({
- Use `'quick'` for simple factual queries where deep reasoning isn't needed - Use `'quick'` for simple factual queries where deep reasoning isn't needed
- Thinking budget counts against total token usage - Thinking budget counts against total token usage
### 📑 Native PDF OCR (Mistral)
Mistral provides native PDF document processing via their OCR API - no image conversion required:
```typescript
import { MistralProvider } from '@push.rocks/smartai';
const mistral = new MistralProvider({
mistralToken: 'your-api-key',
chatModel: 'mistral-large-latest', // Default
ocrModel: 'mistral-ocr-latest', // Default
tableFormat: 'markdown', // 'markdown' | 'html'
});
await mistral.start();
// Direct PDF processing - no image conversion overhead
const result = await mistral.document({
systemMessage: 'You are a document analyst.',
userMessage: 'Extract all invoice details and calculate the total.',
pdfDocuments: [invoicePdfBuffer],
messageHistory: [],
});
```
**Key Advantage**: Unlike other providers that convert PDFs to images first, Mistral's OCR API processes PDFs natively, potentially offering faster and more accurate text extraction for document-heavy workloads.
**Supported Formats:**
- Native PDF processing via Files API
- Image OCR (JPEG, PNG, GIF, WebP) for vision tasks
- Table extraction with markdown or HTML output
### 🎨 Image Generation & Editing ### 🎨 Image Generation & Editing
Generate and edit images with OpenAI's cutting-edge models: Generate and edit images with OpenAI's cutting-edge models:
@@ -645,6 +678,7 @@ export ELEVENLABS_API_KEY=sk-...
| --------------------- | -------------------- | --------------------------------------------------------- | | --------------------- | -------------------- | --------------------------------------------------------- |
| **General Purpose** | OpenAI | Most features, stable, well-documented | | **General Purpose** | OpenAI | Most features, stable, well-documented |
| **Complex Reasoning** | Anthropic | Superior logical thinking, safer outputs | | **Complex Reasoning** | Anthropic | Superior logical thinking, safer outputs |
| **Document OCR** | Mistral | Native PDF processing, no image conversion overhead |
| **Research & Facts** | Perplexity | Web-aware, provides citations | | **Research & Facts** | Perplexity | Web-aware, provides citations |
| **Deep Research** | OpenAI | Deep Research API with comprehensive analysis | | **Deep Research** | OpenAI | Deep Research API with comprehensive analysis |
| **Premium TTS** | ElevenLabs | Most natural voices, 70+ languages, superior quality (v3) | | **Premium TTS** | ElevenLabs | Most natural voices, 70+ languages, superior quality (v3) |

View File

@@ -1,8 +1,9 @@
import { expect, tap } from '@git.zone/tstest/tapbundle'; import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as qenv from '@push.rocks/qenv'; import * as qenv from '@push.rocks/qenv';
import * as smartfile from '@push.rocks/smartfile'; import { SmartFs, SmartFsProviderNode } from '@push.rocks/smartfs';
const testQenv = new qenv.Qenv('./', './.nogit/'); const testQenv = new qenv.Qenv('./', './.nogit/');
const smartfs = new SmartFs(new SmartFsProviderNode());
import * as smartai from '../ts/index.js'; import * as smartai from '../ts/index.js';
@@ -27,7 +28,7 @@ tap.test('ElevenLabs Audio: should create audio response', async () => {
chunks.push(chunk as Uint8Array); chunks.push(chunk as Uint8Array);
} }
const audioBuffer = Buffer.concat(chunks); const audioBuffer = Buffer.concat(chunks);
await smartfile.fs.toFs(audioBuffer, './.nogit/testoutput_elevenlabs.mp3'); await smartfs.file('./.nogit/testoutput_elevenlabs.mp3').write(audioBuffer);
console.log(`Audio Buffer length: ${audioBuffer.length}`); console.log(`Audio Buffer length: ${audioBuffer.length}`);
expect(audioBuffer.length).toBeGreaterThan(0); expect(audioBuffer.length).toBeGreaterThan(0);
}); });
@@ -42,7 +43,7 @@ tap.test('ElevenLabs Audio: should create audio with custom voice', async () =>
chunks.push(chunk as Uint8Array); chunks.push(chunk as Uint8Array);
} }
const audioBuffer = Buffer.concat(chunks); const audioBuffer = Buffer.concat(chunks);
await smartfile.fs.toFs(audioBuffer, './.nogit/testoutput_elevenlabs_custom.mp3'); await smartfs.file('./.nogit/testoutput_elevenlabs_custom.mp3').write(audioBuffer);
console.log(`Audio Buffer length (custom voice): ${audioBuffer.length}`); console.log(`Audio Buffer length (custom voice): ${audioBuffer.length}`);
expect(audioBuffer.length).toBeGreaterThan(0); expect(audioBuffer.length).toBeGreaterThan(0);
}); });

View File

@@ -1,8 +1,9 @@
import { expect, tap } from '@git.zone/tstest/tapbundle'; import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as qenv from '@push.rocks/qenv'; import * as qenv from '@push.rocks/qenv';
import * as smartfile from '@push.rocks/smartfile'; import { SmartFs, SmartFsProviderNode } from '@push.rocks/smartfs';
const testQenv = new qenv.Qenv('./', './.nogit/'); const testQenv = new qenv.Qenv('./', './.nogit/');
const smartfs = new SmartFs(new SmartFsProviderNode());
import * as smartai from '../ts/index.js'; import * as smartai from '../ts/index.js';
@@ -26,7 +27,7 @@ tap.test('OpenAI Audio: should create audio response', async () => {
chunks.push(chunk as Uint8Array); chunks.push(chunk as Uint8Array);
} }
const audioBuffer = Buffer.concat(chunks); const audioBuffer = Buffer.concat(chunks);
await smartfile.fs.toFs(audioBuffer, './.nogit/testoutput.mp3'); await smartfs.file('./.nogit/testoutput.mp3').write(audioBuffer);
console.log(`Audio Buffer length: ${audioBuffer.length}`); console.log(`Audio Buffer length: ${audioBuffer.length}`);
// Assert that the resulting buffer is not empty. // Assert that the resulting buffer is not empty.
expect(audioBuffer.length).toBeGreaterThan(0); expect(audioBuffer.length).toBeGreaterThan(0);

View File

@@ -1,9 +1,10 @@
import { expect, tap } from '@git.zone/tstest/tapbundle'; import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as qenv from '@push.rocks/qenv'; import * as qenv from '@push.rocks/qenv';
import * as smartrequest from '@push.rocks/smartrequest'; import * as smartrequest from '@push.rocks/smartrequest';
import * as smartfile from '@push.rocks/smartfile'; import { SmartFs, SmartFsProviderNode } from '@push.rocks/smartfs';
const testQenv = new qenv.Qenv('./', './.nogit/'); const testQenv = new qenv.Qenv('./', './.nogit/');
const smartfs = new SmartFs(new SmartFsProviderNode());
import * as smartai from '../ts/index.js'; import * as smartai from '../ts/index.js';
@@ -41,7 +42,7 @@ tap.test('Anthropic Document: should handle complex document analysis', async ()
let pdfBuffer: Uint8Array; let pdfBuffer: Uint8Array;
try { try {
pdfBuffer = await smartfile.fs.toBuffer(pdfPath); pdfBuffer = await smartfs.file(pdfPath).read();
} catch (error) { } catch (error) {
// If the file doesn't exist, use the dummy PDF // If the file doesn't exist, use the dummy PDF
console.log('Demo PDF not found, using dummy PDF instead'); console.log('Demo PDF not found, using dummy PDF instead');

View File

@@ -1,9 +1,10 @@
import { expect, tap } from '@git.zone/tstest/tapbundle'; import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as qenv from '@push.rocks/qenv'; import * as qenv from '@push.rocks/qenv';
import * as smartrequest from '@push.rocks/smartrequest'; import * as smartrequest from '@push.rocks/smartrequest';
import * as smartfile from '@push.rocks/smartfile'; import { SmartFs, SmartFsProviderNode } from '@push.rocks/smartfs';
const testQenv = new qenv.Qenv('./', './.nogit/'); const testQenv = new qenv.Qenv('./', './.nogit/');
const smartfs = new SmartFs(new SmartFsProviderNode());
import * as smartai from '../ts/index.js'; import * as smartai from '../ts/index.js';
@@ -42,7 +43,7 @@ tap.test('Mistral Document: should handle complex document analysis', async () =
let pdfBuffer: Uint8Array; let pdfBuffer: Uint8Array;
try { try {
pdfBuffer = await smartfile.fs.toBuffer(pdfPath); pdfBuffer = await smartfs.file(pdfPath).read();
} catch (error) { } catch (error) {
// If the file doesn't exist, use the dummy PDF // If the file doesn't exist, use the dummy PDF
console.log('Demo PDF not found, using dummy PDF instead'); console.log('Demo PDF not found, using dummy PDF instead');

View File

@@ -1,9 +1,10 @@
import { expect, tap } from '@git.zone/tstest/tapbundle'; import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as qenv from '@push.rocks/qenv'; import * as qenv from '@push.rocks/qenv';
import * as smartrequest from '@push.rocks/smartrequest'; import * as smartrequest from '@push.rocks/smartrequest';
import * as smartfile from '@push.rocks/smartfile'; import { SmartFs, SmartFsProviderNode } from '@push.rocks/smartfs';
const testQenv = new qenv.Qenv('./', './.nogit/'); const testQenv = new qenv.Qenv('./', './.nogit/');
const smartfs = new SmartFs(new SmartFsProviderNode());
import * as smartai from '../ts/index.js'; import * as smartai from '../ts/index.js';
@@ -32,7 +33,7 @@ tap.test('OpenAI Document: should document a pdf', async () => {
}); });
tap.test('OpenAI Document: should recognize companies in a pdf', async () => { tap.test('OpenAI Document: should recognize companies in a pdf', async () => {
const pdfBuffer = await smartfile.fs.toBuffer('./.nogit/demo_without_textlayer.pdf'); const pdfBuffer = await smartfs.file('./.nogit/demo_without_textlayer.pdf').read();
const result = await testSmartai.openaiProvider.document({ const result = await testSmartai.openaiProvider.document({
systemMessage: ` systemMessage: `
summarize the document. summarize the document.

View File

@@ -1,8 +1,9 @@
import { expect, tap } from '@git.zone/tstest/tapbundle'; import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as qenv from '@push.rocks/qenv'; import * as qenv from '@push.rocks/qenv';
import * as smartfile from '@push.rocks/smartfile'; import { SmartFs, SmartFsProviderNode } from '@push.rocks/smartfs';
const testQenv = new qenv.Qenv('./', './.nogit/'); const testQenv = new qenv.Qenv('./', './.nogit/');
const smartfs = new SmartFs(new SmartFsProviderNode());
import * as smartai from '../ts/index.js'; import * as smartai from '../ts/index.js';
@@ -21,7 +22,7 @@ tap.test('Anthropic Vision: should analyze coffee image with latte art', async (
const imagePath = './test/testimages/coffee-dani/coffee.jpg'; const imagePath = './test/testimages/coffee-dani/coffee.jpg';
console.log(`Loading coffee image from: ${imagePath}`); console.log(`Loading coffee image from: ${imagePath}`);
const imageBuffer = await smartfile.fs.toBuffer(imagePath); const imageBuffer = await smartfs.file(imagePath).read();
console.log(`Image loaded, size: ${imageBuffer.length} bytes`); console.log(`Image loaded, size: ${imageBuffer.length} bytes`);
const result = await anthropicProvider.vision({ const result = await anthropicProvider.vision({
@@ -45,7 +46,7 @@ tap.test('Anthropic Vision: should analyze laptop/workspace image', async () =>
const imagePath = './test/testimages/laptop-nicolas/laptop.jpg'; const imagePath = './test/testimages/laptop-nicolas/laptop.jpg';
console.log(`Loading laptop image from: ${imagePath}`); console.log(`Loading laptop image from: ${imagePath}`);
const imageBuffer = await smartfile.fs.toBuffer(imagePath); const imageBuffer = await smartfs.file(imagePath).read();
console.log(`Image loaded, size: ${imageBuffer.length} bytes`); console.log(`Image loaded, size: ${imageBuffer.length} bytes`);
const result = await anthropicProvider.vision({ const result = await anthropicProvider.vision({
@@ -69,7 +70,7 @@ tap.test('Anthropic Vision: should analyze receipt/document image', async () =>
const imagePath = './test/testimages/receipt-annie/receipt.jpg'; const imagePath = './test/testimages/receipt-annie/receipt.jpg';
console.log(`Loading receipt image from: ${imagePath}`); console.log(`Loading receipt image from: ${imagePath}`);
const imageBuffer = await smartfile.fs.toBuffer(imagePath); const imageBuffer = await smartfs.file(imagePath).read();
console.log(`Image loaded, size: ${imageBuffer.length} bytes`); console.log(`Image loaded, size: ${imageBuffer.length} bytes`);
const result = await anthropicProvider.vision({ const result = await anthropicProvider.vision({

View File

@@ -8,7 +8,7 @@ export {
// @push.rocks scope // @push.rocks scope
import * as qenv from '@push.rocks/qenv'; import * as qenv from '@push.rocks/qenv';
import * as smartarray from '@push.rocks/smartarray'; import * as smartarray from '@push.rocks/smartarray';
import * as smartfile from '@push.rocks/smartfile'; import * as smartfs from '@push.rocks/smartfs';
import * as smartpath from '@push.rocks/smartpath'; import * as smartpath from '@push.rocks/smartpath';
import * as smartpdf from '@push.rocks/smartpdf'; import * as smartpdf from '@push.rocks/smartpdf';
import * as smartpromise from '@push.rocks/smartpromise'; import * as smartpromise from '@push.rocks/smartpromise';
@@ -18,7 +18,7 @@ import * as webstream from '@push.rocks/webstream';
export { export {
smartarray, smartarray,
qenv, qenv,
smartfile, smartfs,
smartpath, smartpath,
smartpdf, smartpdf,
smartpromise, smartpromise,

View File

@@ -12,10 +12,60 @@ import type {
ImageResponse ImageResponse
} from './abstract.classes.multimodal.js'; } from './abstract.classes.multimodal.js';
/**
* Ollama model runtime options
* @see https://github.com/ollama/ollama/blob/main/docs/modelfile.md
*/
export interface IOllamaModelOptions {
num_ctx?: number; // Context window (default: 2048)
temperature?: number; // 0 = deterministic (default: 0.8)
top_k?: number; // Top-k sampling (default: 40)
top_p?: number; // Nucleus sampling (default: 0.9)
repeat_penalty?: number;// Repeat penalty (default: 1.1)
num_predict?: number; // Max tokens to predict
stop?: string[]; // Stop sequences
seed?: number; // Random seed for reproducibility
}
export interface IOllamaProviderOptions { export interface IOllamaProviderOptions {
baseUrl?: string; baseUrl?: string;
model?: string; model?: string;
visionModel?: string; // Model to use for vision tasks (e.g. 'llava') visionModel?: string; // Model to use for vision tasks (e.g. 'llava')
defaultOptions?: IOllamaModelOptions; // Default model options
defaultTimeout?: number; // Default timeout in ms (default: 120000)
}
/**
* Extended chat options with Ollama-specific settings
*/
export interface IOllamaChatOptions extends ChatOptions {
options?: IOllamaModelOptions; // Per-request model options
timeout?: number; // Per-request timeout in ms
model?: string; // Per-request model override
}
/**
* Chunk emitted during streaming
*/
export interface IOllamaStreamChunk {
content: string;
thinking?: string; // For models with extended thinking
done: boolean;
stats?: {
totalDuration?: number;
evalCount?: number;
};
}
/**
* Extended chat response with Ollama-specific fields
*/
export interface IOllamaChatResponse extends ChatResponse {
thinking?: string;
stats?: {
totalDuration?: number;
evalCount?: number;
};
} }
export class OllamaProvider extends MultiModalModel { export class OllamaProvider extends MultiModalModel {
@@ -23,6 +73,8 @@ export class OllamaProvider extends MultiModalModel {
private baseUrl: string; private baseUrl: string;
private model: string; private model: string;
private visionModel: string; private visionModel: string;
private defaultOptions: IOllamaModelOptions;
private defaultTimeout: number;
constructor(optionsArg: IOllamaProviderOptions = {}) { constructor(optionsArg: IOllamaProviderOptions = {}) {
super(); super();
@@ -30,6 +82,8 @@ export class OllamaProvider extends MultiModalModel {
this.baseUrl = optionsArg.baseUrl || 'http://localhost:11434'; this.baseUrl = optionsArg.baseUrl || 'http://localhost:11434';
this.model = optionsArg.model || 'llama2'; this.model = optionsArg.model || 'llama2';
this.visionModel = optionsArg.visionModel || 'llava'; this.visionModel = optionsArg.visionModel || 'llava';
this.defaultOptions = optionsArg.defaultOptions || {};
this.defaultTimeout = optionsArg.defaultTimeout || 120000;
} }
async start() { async start() {
@@ -154,7 +208,7 @@ export class OllamaProvider extends MultiModalModel {
{ role: 'user', content: optionsArg.userMessage } { role: 'user', content: optionsArg.userMessage }
]; ];
// Make API call to Ollama // Make API call to Ollama with defaultOptions and timeout
const response = await fetch(`${this.baseUrl}/api/chat`, { const response = await fetch(`${this.baseUrl}/api/chat`, {
method: 'POST', method: 'POST',
headers: { headers: {
@@ -163,8 +217,10 @@ export class OllamaProvider extends MultiModalModel {
body: JSON.stringify({ body: JSON.stringify({
model: this.model, model: this.model,
messages: messages, messages: messages,
stream: false stream: false,
options: this.defaultOptions,
}), }),
signal: AbortSignal.timeout(this.defaultTimeout),
}); });
if (!response.ok) { if (!response.ok) {
@@ -172,13 +228,150 @@ export class OllamaProvider extends MultiModalModel {
} }
const result = await response.json(); const result = await response.json();
return { return {
role: 'assistant' as const, role: 'assistant' as const,
message: result.message.content, message: result.message.content,
}; };
} }
/**
* Streaming chat with async iteration and options support
*/
public async chatStreamResponse(
optionsArg: IOllamaChatOptions
): Promise<AsyncIterable<IOllamaStreamChunk>> {
const model = optionsArg.model || this.model;
const timeout = optionsArg.timeout || this.defaultTimeout;
const modelOptions = { ...this.defaultOptions, ...optionsArg.options };
const messages = [
{ role: 'system', content: optionsArg.systemMessage },
...optionsArg.messageHistory,
{ role: 'user', content: optionsArg.userMessage }
];
const response = await fetch(`${this.baseUrl}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model,
messages,
stream: true,
options: modelOptions,
}),
signal: AbortSignal.timeout(timeout),
});
if (!response.ok) {
throw new Error(`Ollama API error: ${response.status}`);
}
const reader = response.body!.getReader();
const decoder = new TextDecoder();
return {
[Symbol.asyncIterator]: async function* () {
let buffer = '';
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (!line.trim()) continue;
try {
const json = JSON.parse(line);
yield {
content: json.message?.content || '',
thinking: json.message?.thinking,
done: json.done || false,
stats: json.done ? {
totalDuration: json.total_duration,
evalCount: json.eval_count,
} : undefined,
} as IOllamaStreamChunk;
} catch { /* skip malformed */ }
}
}
} finally {
reader.releaseLock();
}
}
};
}
/**
* Stream and collect full response with optional progress callback
*/
public async collectStreamResponse(
optionsArg: IOllamaChatOptions,
onChunk?: (chunk: IOllamaStreamChunk) => void
): Promise<IOllamaChatResponse> {
const stream = await this.chatStreamResponse(optionsArg);
let content = '';
let thinking = '';
let stats: IOllamaChatResponse['stats'];
for await (const chunk of stream) {
if (chunk.content) content += chunk.content;
if (chunk.thinking) thinking += chunk.thinking;
if (chunk.stats) stats = chunk.stats;
if (onChunk) onChunk(chunk);
}
return {
role: 'assistant' as const,
message: content,
thinking: thinking || undefined,
stats,
};
}
/**
* Non-streaming chat with full options support
*/
public async chatWithOptions(optionsArg: IOllamaChatOptions): Promise<IOllamaChatResponse> {
const model = optionsArg.model || this.model;
const timeout = optionsArg.timeout || this.defaultTimeout;
const modelOptions = { ...this.defaultOptions, ...optionsArg.options };
const messages = [
{ role: 'system', content: optionsArg.systemMessage },
...optionsArg.messageHistory,
{ role: 'user', content: optionsArg.userMessage }
];
const response = await fetch(`${this.baseUrl}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model,
messages,
stream: false,
options: modelOptions,
}),
signal: AbortSignal.timeout(timeout),
});
if (!response.ok) {
throw new Error(`Ollama API error: ${response.statusText}`);
}
const result = await response.json();
return {
role: 'assistant' as const,
message: result.message.content,
thinking: result.message.thinking,
stats: {
totalDuration: result.total_duration,
evalCount: result.eval_count,
},
};
}
public async audio(optionsArg: { message: string }): Promise<NodeJS.ReadableStream> { public async audio(optionsArg: { message: string }): Promise<NodeJS.ReadableStream> {
throw new Error('Audio generation is not supported by Ollama.'); throw new Error('Audio generation is not supported by Ollama.');
} }