novixo-ai 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +172 -0
- package/dist/index.cjs +501 -0
- package/dist/index.d.cts +69 -0
- package/dist/index.d.ts +69 -0
- package/dist/index.js +474 -0
- package/package.json +39 -0
package/README.md
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# novixo-ai
|
|
2
|
+
|
|
3
|
+
Unified AI client for Node.js and the browser. One API for **15 AI providers** — with automatic fallback, rate-limit detection, and response caching built in.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install novixo-ai
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { NovixoAI } from "novixo-ai";
|
|
15
|
+
|
|
16
|
+
const ai = new NovixoAI({
|
|
17
|
+
keys: {
|
|
18
|
+
groq: process.env.GROQ_API_KEY,
|
|
19
|
+
gemini: process.env.GEMINI_API_KEY,
|
|
20
|
+
openai: process.env.OPENAI_API_KEY,
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Single prompt
|
|
25
|
+
const text = await ai.ask("Explain recursion in simple terms");
|
|
26
|
+
|
|
27
|
+
// Multi-turn chat
|
|
28
|
+
const response = await ai.chat([
|
|
29
|
+
{ role: "user", content: "What is a binary tree?" },
|
|
30
|
+
{ role: "assistant", content: "A binary tree is..." },
|
|
31
|
+
{ role: "user", content: "Give me an example in JavaScript" },
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
console.log(response.text);
|
|
35
|
+
console.log(response.provider); // which provider answered
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Supported providers
|
|
41
|
+
|
|
42
|
+
| Provider | Key name | Default model |
|
|
43
|
+
|----------------|-----------------|---------------------------------------------|
|
|
44
|
+
| Groq | `groq` | llama3-8b-8192 |
|
|
45
|
+
| Google Gemini | `gemini` | gemini-1.5-flash |
|
|
46
|
+
| Anthropic | `anthropic` | claude-haiku-4-5-20251001 |
|
|
47
|
+
| OpenAI | `openai` | gpt-4o-mini |
|
|
48
|
+
| Cohere | `cohere` | command-r-plus |
|
|
49
|
+
| Mistral | `mistral` | mistral-small-latest |
|
|
50
|
+
| Together AI | `together` | meta-llama/Llama-3-8b-chat-hf |
|
|
51
|
+
| Perplexity | `perplexity` | llama-3-sonar-small-32k-chat |
|
|
52
|
+
| Hugging Face | `huggingface` | mistralai/Mistral-7B-Instruct-v0.2 |
|
|
53
|
+
| OpenRouter | `openrouter` | openai/gpt-4o-mini (access to 100+ models) |
|
|
54
|
+
| Fireworks AI | `fireworks` | llama-v3-8b-instruct |
|
|
55
|
+
| DeepSeek | `deepseek` | deepseek-chat |
|
|
56
|
+
| xAI (Grok) | `xai` | grok-beta |
|
|
57
|
+
| AI21 | `ai21` | jamba-instruct |
|
|
58
|
+
| NLP Cloud | `nlpcloud` | finetuned-llama-3-70b |
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## How fallback works
|
|
63
|
+
|
|
64
|
+
novixo-ai tries providers **left to right** in your priority order:
|
|
65
|
+
|
|
66
|
+
1. If a provider is **rate limited**, it's skipped and retried after cooldown.
|
|
67
|
+
2. If a provider **fails**, the next one is tried automatically.
|
|
68
|
+
3. If **all providers fail**, an error is thrown with details from each attempt.
|
|
69
|
+
|
|
70
|
+
Default order: `groq → gemini → openai → mistral → anthropic → ...`
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Configuration
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
const ai = new NovixoAI({
|
|
78
|
+
// Only add keys for providers you have access to
|
|
79
|
+
keys: {
|
|
80
|
+
groq: "gsk_...",
|
|
81
|
+
gemini: "AIza...",
|
|
82
|
+
openai: "sk-...",
|
|
83
|
+
cohere: "...",
|
|
84
|
+
mistral: "...",
|
|
85
|
+
together: "...",
|
|
86
|
+
perplexity: "pplx-...",
|
|
87
|
+
huggingface: "hf_...",
|
|
88
|
+
openrouter: "sk-or-...",
|
|
89
|
+
fireworks: "fw-...",
|
|
90
|
+
deepseek: "...",
|
|
91
|
+
xai: "xai-...",
|
|
92
|
+
ai21: "...",
|
|
93
|
+
nlpcloud: "...",
|
|
94
|
+
anthropic: "sk-ant-...",
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
// Custom priority order
|
|
98
|
+
providers: ["groq", "gemini", "mistral", "openai"],
|
|
99
|
+
|
|
100
|
+
// Override models per provider
|
|
101
|
+
models: {
|
|
102
|
+
openai: "gpt-4o",
|
|
103
|
+
mistral: "mistral-large-latest",
|
|
104
|
+
groq: "llama3-70b-8192",
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
maxTokens: 1024, // default: 1024
|
|
108
|
+
temperature: 0.7, // default: 0.7
|
|
109
|
+
cache: true, // default: true
|
|
110
|
+
cacheTTL: 300_000, // default: 5 minutes
|
|
111
|
+
});
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## API
|
|
117
|
+
|
|
118
|
+
### `ai.ask(prompt, systemPrompt?)`
|
|
119
|
+
Single-turn shorthand. Returns response text as a string.
|
|
120
|
+
|
|
121
|
+
### `ai.chat(messages, options?)`
|
|
122
|
+
Multi-turn chat. Returns an `AIResponse`:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
{
|
|
126
|
+
text: string // The model's response
|
|
127
|
+
provider: string // Which provider answered
|
|
128
|
+
model: string // Which model was used
|
|
129
|
+
cached: boolean // Whether this came from cache
|
|
130
|
+
durationMs: number // Time taken in milliseconds
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### `ai.clearCache()`
|
|
135
|
+
Clears the in-memory response cache.
|
|
136
|
+
|
|
137
|
+
### `ai.cacheSize`
|
|
138
|
+
Number of cached entries.
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## With system prompts
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
const res = await ai.chat(
|
|
146
|
+
[{ role: "user", content: "Summarise this for me: ..." }],
|
|
147
|
+
{ systemPrompt: "You are a concise academic writing assistant." }
|
|
148
|
+
);
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Force a specific provider for one call
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
const res = await ai.chat(messages, {
|
|
155
|
+
providers: ["openai"], // only use OpenAI for this call
|
|
156
|
+
});
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Part of the NovixoTech ecosystem
|
|
162
|
+
|
|
163
|
+
- [novixo-engine](https://npmjs.com/package/novixo-engine) — Offline-first network SDK
|
|
164
|
+
- [novixo-agent-logger](https://npmjs.com/package/novixo-agent-logger) — AI agent audit trail
|
|
165
|
+
- **novixo-ai** — Multi-provider AI client ← you are here
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## License
|
|
170
|
+
|
|
171
|
+
MIT © [NovixoTech](https://github.com/NovixoTech)
|
|
172
|
+
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
NovixoAI: () => NovixoAI
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(index_exports);
|
|
26
|
+
|
|
27
|
+
// src/cache.ts
|
|
28
|
+
var ResponseCache = class {
|
|
29
|
+
constructor(ttlMs = 5 * 60 * 1e3) {
|
|
30
|
+
this.store = /* @__PURE__ */ new Map();
|
|
31
|
+
this.ttl = ttlMs;
|
|
32
|
+
}
|
|
33
|
+
key(messages, systemPrompt) {
|
|
34
|
+
return JSON.stringify({ messages, systemPrompt });
|
|
35
|
+
}
|
|
36
|
+
get(messages, systemPrompt) {
|
|
37
|
+
const k = this.key(messages, systemPrompt);
|
|
38
|
+
const entry = this.store.get(k);
|
|
39
|
+
if (!entry) return null;
|
|
40
|
+
if (Date.now() > entry.expiresAt) {
|
|
41
|
+
this.store.delete(k);
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
return entry.value;
|
|
45
|
+
}
|
|
46
|
+
set(messages, value, systemPrompt) {
|
|
47
|
+
const k = this.key(messages, systemPrompt);
|
|
48
|
+
this.store.set(k, { value, expiresAt: Date.now() + this.ttl });
|
|
49
|
+
}
|
|
50
|
+
clear() {
|
|
51
|
+
this.store.clear();
|
|
52
|
+
}
|
|
53
|
+
get size() {
|
|
54
|
+
return this.store.size;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// src/providers.ts
|
|
59
|
+
var DEFAULT_MODELS = {
|
|
60
|
+
groq: "llama3-8b-8192",
|
|
61
|
+
gemini: "gemini-1.5-flash",
|
|
62
|
+
anthropic: "claude-haiku-4-5-20251001",
|
|
63
|
+
openai: "gpt-4o-mini",
|
|
64
|
+
cohere: "command-r-plus",
|
|
65
|
+
mistral: "mistral-small-latest",
|
|
66
|
+
together: "meta-llama/Llama-3-8b-chat-hf",
|
|
67
|
+
perplexity: "llama-3-sonar-small-32k-chat",
|
|
68
|
+
huggingface: "mistralai/Mistral-7B-Instruct-v0.2",
|
|
69
|
+
openrouter: "openai/gpt-4o-mini",
|
|
70
|
+
fireworks: "accounts/fireworks/models/llama-v3-8b-instruct",
|
|
71
|
+
deepseek: "deepseek-chat",
|
|
72
|
+
xai: "grok-beta",
|
|
73
|
+
ai21: "jamba-instruct",
|
|
74
|
+
nlpcloud: "finetuned-llama-3-70b"
|
|
75
|
+
};
|
|
76
|
+
var rateLimitedUntil = {};
|
|
77
|
+
function isRateLimited(provider) {
|
|
78
|
+
const until = rateLimitedUntil[provider];
|
|
79
|
+
if (!until) return false;
|
|
80
|
+
if (Date.now() > until) {
|
|
81
|
+
delete rateLimitedUntil[provider];
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
function markRateLimited(provider, retryAfterMs = 6e4) {
|
|
87
|
+
rateLimitedUntil[provider] = Date.now() + retryAfterMs;
|
|
88
|
+
}
|
|
89
|
+
function getRetryAfter(headers, fallback = 6e4) {
|
|
90
|
+
const val = headers.get("retry-after");
|
|
91
|
+
if (!val) return fallback;
|
|
92
|
+
const secs = parseFloat(val);
|
|
93
|
+
return isNaN(secs) ? fallback : secs * 1e3;
|
|
94
|
+
}
|
|
95
|
+
async function callOpenAICompat(url, authHeader, model, messages, systemPrompt, maxTokens, temperature, provider, extraBody = {}) {
|
|
96
|
+
const res = await fetch(url, {
|
|
97
|
+
method: "POST",
|
|
98
|
+
headers: { "Content-Type": "application/json", ...authHeader },
|
|
99
|
+
body: JSON.stringify({
|
|
100
|
+
model,
|
|
101
|
+
messages: [
|
|
102
|
+
...systemPrompt ? [{ role: "system", content: systemPrompt }] : [],
|
|
103
|
+
...messages
|
|
104
|
+
],
|
|
105
|
+
max_tokens: maxTokens,
|
|
106
|
+
temperature,
|
|
107
|
+
...extraBody
|
|
108
|
+
})
|
|
109
|
+
});
|
|
110
|
+
if (res.status === 429) {
|
|
111
|
+
markRateLimited(provider, getRetryAfter(res.headers));
|
|
112
|
+
throw Object.assign(new Error(`${provider} rate limited`), { rateLimited: true });
|
|
113
|
+
}
|
|
114
|
+
if (!res.ok) {
|
|
115
|
+
const err = await res.json().catch(() => ({}));
|
|
116
|
+
throw new Error(err?.error?.message || `${provider} ${res.status}`);
|
|
117
|
+
}
|
|
118
|
+
const data = await res.json();
|
|
119
|
+
return data.choices[0].message.content;
|
|
120
|
+
}
|
|
121
|
+
async function callGroq(messages, systemPrompt, apiKey, model, maxTokens, temperature) {
|
|
122
|
+
return callOpenAICompat(
|
|
123
|
+
"https://api.groq.com/openai/v1/chat/completions",
|
|
124
|
+
{ Authorization: `Bearer ${apiKey}` },
|
|
125
|
+
model,
|
|
126
|
+
messages,
|
|
127
|
+
systemPrompt,
|
|
128
|
+
maxTokens,
|
|
129
|
+
temperature,
|
|
130
|
+
"groq"
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
async function callOpenAI(messages, systemPrompt, apiKey, model, maxTokens, temperature) {
|
|
134
|
+
return callOpenAICompat(
|
|
135
|
+
"https://api.openai.com/v1/chat/completions",
|
|
136
|
+
{ Authorization: `Bearer ${apiKey}` },
|
|
137
|
+
model,
|
|
138
|
+
messages,
|
|
139
|
+
systemPrompt,
|
|
140
|
+
maxTokens,
|
|
141
|
+
temperature,
|
|
142
|
+
"openai"
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
async function callMistral(messages, systemPrompt, apiKey, model, maxTokens, temperature) {
|
|
146
|
+
return callOpenAICompat(
|
|
147
|
+
"https://api.mistral.ai/v1/chat/completions",
|
|
148
|
+
{ Authorization: `Bearer ${apiKey}` },
|
|
149
|
+
model,
|
|
150
|
+
messages,
|
|
151
|
+
systemPrompt,
|
|
152
|
+
maxTokens,
|
|
153
|
+
temperature,
|
|
154
|
+
"mistral"
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
async function callTogether(messages, systemPrompt, apiKey, model, maxTokens, temperature) {
|
|
158
|
+
return callOpenAICompat(
|
|
159
|
+
"https://api.together.xyz/v1/chat/completions",
|
|
160
|
+
{ Authorization: `Bearer ${apiKey}` },
|
|
161
|
+
model,
|
|
162
|
+
messages,
|
|
163
|
+
systemPrompt,
|
|
164
|
+
maxTokens,
|
|
165
|
+
temperature,
|
|
166
|
+
"together"
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
async function callPerplexity(messages, systemPrompt, apiKey, model, maxTokens, temperature) {
|
|
170
|
+
return callOpenAICompat(
|
|
171
|
+
"https://api.perplexity.ai/chat/completions",
|
|
172
|
+
{ Authorization: `Bearer ${apiKey}` },
|
|
173
|
+
model,
|
|
174
|
+
messages,
|
|
175
|
+
systemPrompt,
|
|
176
|
+
maxTokens,
|
|
177
|
+
temperature,
|
|
178
|
+
"perplexity"
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
async function callOpenRouter(messages, systemPrompt, apiKey, model, maxTokens, temperature) {
|
|
182
|
+
return callOpenAICompat(
|
|
183
|
+
"https://openrouter.ai/api/v1/chat/completions",
|
|
184
|
+
{ Authorization: `Bearer ${apiKey}`, "HTTP-Referer": "https://github.com/NovixoTech/novixo-ai" },
|
|
185
|
+
model,
|
|
186
|
+
messages,
|
|
187
|
+
systemPrompt,
|
|
188
|
+
maxTokens,
|
|
189
|
+
temperature,
|
|
190
|
+
"openrouter"
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
async function callFireworks(messages, systemPrompt, apiKey, model, maxTokens, temperature) {
|
|
194
|
+
return callOpenAICompat(
|
|
195
|
+
"https://api.fireworks.ai/inference/v1/chat/completions",
|
|
196
|
+
{ Authorization: `Bearer ${apiKey}` },
|
|
197
|
+
model,
|
|
198
|
+
messages,
|
|
199
|
+
systemPrompt,
|
|
200
|
+
maxTokens,
|
|
201
|
+
temperature,
|
|
202
|
+
"fireworks"
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
async function callDeepSeek(messages, systemPrompt, apiKey, model, maxTokens, temperature) {
|
|
206
|
+
return callOpenAICompat(
|
|
207
|
+
"https://api.deepseek.com/v1/chat/completions",
|
|
208
|
+
{ Authorization: `Bearer ${apiKey}` },
|
|
209
|
+
model,
|
|
210
|
+
messages,
|
|
211
|
+
systemPrompt,
|
|
212
|
+
maxTokens,
|
|
213
|
+
temperature,
|
|
214
|
+
"deepseek"
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
async function callXAI(messages, systemPrompt, apiKey, model, maxTokens, temperature) {
|
|
218
|
+
return callOpenAICompat(
|
|
219
|
+
"https://api.x.ai/v1/chat/completions",
|
|
220
|
+
{ Authorization: `Bearer ${apiKey}` },
|
|
221
|
+
model,
|
|
222
|
+
messages,
|
|
223
|
+
systemPrompt,
|
|
224
|
+
maxTokens,
|
|
225
|
+
temperature,
|
|
226
|
+
"xai"
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
async function callGemini(messages, systemPrompt, apiKey, model, maxTokens, temperature) {
|
|
230
|
+
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`;
|
|
231
|
+
const contents = messages.map((m) => ({
|
|
232
|
+
role: m.role === "assistant" ? "model" : "user",
|
|
233
|
+
parts: [{ text: m.content }]
|
|
234
|
+
}));
|
|
235
|
+
const body = {
|
|
236
|
+
contents,
|
|
237
|
+
generationConfig: { maxOutputTokens: maxTokens, temperature }
|
|
238
|
+
};
|
|
239
|
+
if (systemPrompt) body.system_instruction = { parts: [{ text: systemPrompt }] };
|
|
240
|
+
const res = await fetch(url, {
|
|
241
|
+
method: "POST",
|
|
242
|
+
headers: { "Content-Type": "application/json" },
|
|
243
|
+
body: JSON.stringify(body)
|
|
244
|
+
});
|
|
245
|
+
if (res.status === 429) {
|
|
246
|
+
markRateLimited("gemini", getRetryAfter(res.headers));
|
|
247
|
+
throw Object.assign(new Error("Gemini rate limited"), { rateLimited: true });
|
|
248
|
+
}
|
|
249
|
+
if (!res.ok) {
|
|
250
|
+
const err = await res.json().catch(() => ({}));
|
|
251
|
+
throw new Error(err?.error?.message || `Gemini ${res.status}`);
|
|
252
|
+
}
|
|
253
|
+
const data = await res.json();
|
|
254
|
+
return data.candidates[0].content.parts[0].text;
|
|
255
|
+
}
|
|
256
|
+
async function callAnthropic(messages, systemPrompt, apiKey, model, maxTokens, temperature) {
|
|
257
|
+
const body = {
|
|
258
|
+
model,
|
|
259
|
+
max_tokens: maxTokens,
|
|
260
|
+
temperature,
|
|
261
|
+
messages: messages.filter((m) => m.role !== "system")
|
|
262
|
+
};
|
|
263
|
+
if (systemPrompt) body.system = systemPrompt;
|
|
264
|
+
const res = await fetch("https://api.anthropic.com/v1/messages", {
|
|
265
|
+
method: "POST",
|
|
266
|
+
headers: {
|
|
267
|
+
"Content-Type": "application/json",
|
|
268
|
+
"x-api-key": apiKey,
|
|
269
|
+
"anthropic-version": "2023-06-01"
|
|
270
|
+
},
|
|
271
|
+
body: JSON.stringify(body)
|
|
272
|
+
});
|
|
273
|
+
if (res.status === 429) {
|
|
274
|
+
markRateLimited("anthropic", getRetryAfter(res.headers));
|
|
275
|
+
throw Object.assign(new Error("Anthropic rate limited"), { rateLimited: true });
|
|
276
|
+
}
|
|
277
|
+
if (!res.ok) {
|
|
278
|
+
const err = await res.json().catch(() => ({}));
|
|
279
|
+
throw new Error(err?.error?.message || `Anthropic ${res.status}`);
|
|
280
|
+
}
|
|
281
|
+
const data = await res.json();
|
|
282
|
+
return data.content[0].text;
|
|
283
|
+
}
|
|
284
|
+
async function callCohere(messages, systemPrompt, apiKey, model, maxTokens, temperature) {
|
|
285
|
+
const history = messages.slice(0, -1).map((m) => ({
|
|
286
|
+
role: m.role === "assistant" ? "CHATBOT" : "USER",
|
|
287
|
+
message: m.content
|
|
288
|
+
}));
|
|
289
|
+
const lastMessage = messages[messages.length - 1]?.content ?? "";
|
|
290
|
+
const res = await fetch("https://api.cohere.com/v1/chat", {
|
|
291
|
+
method: "POST",
|
|
292
|
+
headers: {
|
|
293
|
+
"Content-Type": "application/json",
|
|
294
|
+
Authorization: `Bearer ${apiKey}`
|
|
295
|
+
},
|
|
296
|
+
body: JSON.stringify({
|
|
297
|
+
model,
|
|
298
|
+
message: lastMessage,
|
|
299
|
+
chat_history: history,
|
|
300
|
+
preamble: systemPrompt,
|
|
301
|
+
max_tokens: maxTokens,
|
|
302
|
+
temperature
|
|
303
|
+
})
|
|
304
|
+
});
|
|
305
|
+
if (res.status === 429) {
|
|
306
|
+
markRateLimited("cohere", getRetryAfter(res.headers));
|
|
307
|
+
throw Object.assign(new Error("Cohere rate limited"), { rateLimited: true });
|
|
308
|
+
}
|
|
309
|
+
if (!res.ok) {
|
|
310
|
+
const err = await res.json().catch(() => ({}));
|
|
311
|
+
throw new Error(err?.message || `Cohere ${res.status}`);
|
|
312
|
+
}
|
|
313
|
+
const data = await res.json();
|
|
314
|
+
return data.text;
|
|
315
|
+
}
|
|
316
|
+
async function callHuggingFace(messages, systemPrompt, apiKey, model, maxTokens, temperature) {
|
|
317
|
+
return callOpenAICompat(
|
|
318
|
+
`https://api-inference.huggingface.co/models/${model}/v1/chat/completions`,
|
|
319
|
+
{ Authorization: `Bearer ${apiKey}` },
|
|
320
|
+
model,
|
|
321
|
+
messages,
|
|
322
|
+
systemPrompt,
|
|
323
|
+
maxTokens,
|
|
324
|
+
temperature,
|
|
325
|
+
"huggingface"
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
async function callAI21(messages, systemPrompt, apiKey, model, maxTokens, temperature) {
|
|
329
|
+
return callOpenAICompat(
|
|
330
|
+
"https://api.ai21.com/studio/v1/chat/completions",
|
|
331
|
+
{ Authorization: `Bearer ${apiKey}` },
|
|
332
|
+
model,
|
|
333
|
+
messages,
|
|
334
|
+
systemPrompt,
|
|
335
|
+
maxTokens,
|
|
336
|
+
temperature,
|
|
337
|
+
"ai21"
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
async function callNLPCloud(messages, systemPrompt, apiKey, model, maxTokens, temperature) {
|
|
341
|
+
const allMessages = [
|
|
342
|
+
...systemPrompt ? [{ role: "system", content: systemPrompt }] : [],
|
|
343
|
+
...messages
|
|
344
|
+
];
|
|
345
|
+
const res = await fetch(`https://api.nlpcloud.io/v1/gpu/${model}/chatbot`, {
|
|
346
|
+
method: "POST",
|
|
347
|
+
headers: {
|
|
348
|
+
"Content-Type": "application/json",
|
|
349
|
+
Authorization: `Token ${apiKey}`
|
|
350
|
+
},
|
|
351
|
+
body: JSON.stringify({
|
|
352
|
+
input: allMessages[allMessages.length - 1]?.content ?? "",
|
|
353
|
+
history: allMessages.slice(0, -1).map((m) => ({
|
|
354
|
+
input: m.role === "user" ? m.content : void 0,
|
|
355
|
+
response: m.role === "assistant" ? m.content : void 0
|
|
356
|
+
})),
|
|
357
|
+
max_length: maxTokens,
|
|
358
|
+
temperature
|
|
359
|
+
})
|
|
360
|
+
});
|
|
361
|
+
if (res.status === 429) {
|
|
362
|
+
markRateLimited("nlpcloud", getRetryAfter(res.headers));
|
|
363
|
+
throw Object.assign(new Error("NLPCloud rate limited"), { rateLimited: true });
|
|
364
|
+
}
|
|
365
|
+
if (!res.ok) {
|
|
366
|
+
const err = await res.json().catch(() => ({}));
|
|
367
|
+
throw new Error(err?.detail || `NLPCloud ${res.status}`);
|
|
368
|
+
}
|
|
369
|
+
const data = await res.json();
|
|
370
|
+
return data.response;
|
|
371
|
+
}
|
|
372
|
+
async function callProvider(provider, messages, systemPrompt, apiKey, modelOverride, maxTokens, temperature) {
|
|
373
|
+
const model = modelOverride ?? DEFAULT_MODELS[provider];
|
|
374
|
+
const map = {
|
|
375
|
+
groq: callGroq,
|
|
376
|
+
openai: callOpenAI,
|
|
377
|
+
mistral: callMistral,
|
|
378
|
+
together: callTogether,
|
|
379
|
+
perplexity: callPerplexity,
|
|
380
|
+
openrouter: callOpenRouter,
|
|
381
|
+
fireworks: callFireworks,
|
|
382
|
+
deepseek: callDeepSeek,
|
|
383
|
+
xai: callXAI,
|
|
384
|
+
gemini: callGemini,
|
|
385
|
+
anthropic: callAnthropic,
|
|
386
|
+
cohere: callCohere,
|
|
387
|
+
huggingface: callHuggingFace,
|
|
388
|
+
ai21: callAI21,
|
|
389
|
+
nlpcloud: callNLPCloud
|
|
390
|
+
};
|
|
391
|
+
const fn = map[provider];
|
|
392
|
+
if (!fn) throw new Error(`Unknown provider: ${provider}`);
|
|
393
|
+
return fn(messages, systemPrompt, apiKey, model, maxTokens, temperature);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// src/client.ts
|
|
397
|
+
var DEFAULT_PROVIDERS = ["groq", "gemini", "anthropic"];
|
|
398
|
+
var NovixoAI = class {
|
|
399
|
+
constructor(config) {
|
|
400
|
+
this.config = {
|
|
401
|
+
keys: config.keys,
|
|
402
|
+
providers: config.providers ?? DEFAULT_PROVIDERS,
|
|
403
|
+
models: config.models ?? {},
|
|
404
|
+
maxTokens: config.maxTokens ?? 1024,
|
|
405
|
+
temperature: config.temperature ?? 0.7,
|
|
406
|
+
cache: config.cache ?? true,
|
|
407
|
+
cacheTTL: config.cacheTTL ?? 5 * 60 * 1e3
|
|
408
|
+
};
|
|
409
|
+
this.cache = this.config.cache ? new ResponseCache(this.config.cacheTTL) : null;
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Send a message and get a response.
|
|
413
|
+
* Tries providers in order, skipping rate-limited ones.
|
|
414
|
+
* Falls back automatically on failure.
|
|
415
|
+
*
|
|
416
|
+
* @example
|
|
417
|
+
* const res = await ai.chat([{ role: "user", content: "Explain recursion" }])
|
|
418
|
+
* console.log(res.text)
|
|
419
|
+
*/
|
|
420
|
+
async chat(messages, options = {}) {
|
|
421
|
+
const systemPrompt = options.systemPrompt;
|
|
422
|
+
const providers = options.providers ?? this.config.providers;
|
|
423
|
+
const errors = [];
|
|
424
|
+
if (this.cache) {
|
|
425
|
+
const cached = this.cache.get(messages, systemPrompt);
|
|
426
|
+
if (cached) {
|
|
427
|
+
return {
|
|
428
|
+
text: cached,
|
|
429
|
+
provider: "groq",
|
|
430
|
+
// placeholder; cached means we don't know original
|
|
431
|
+
model: "cached",
|
|
432
|
+
cached: true,
|
|
433
|
+
durationMs: 0
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
for (const provider of providers) {
|
|
438
|
+
const apiKey = this.config.keys[provider];
|
|
439
|
+
if (!apiKey) continue;
|
|
440
|
+
if (isRateLimited(provider)) {
|
|
441
|
+
errors.push({ provider, message: "Rate limited, skipping", rateLimited: true });
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
const start = Date.now();
|
|
445
|
+
try {
|
|
446
|
+
const text = await callProvider(
|
|
447
|
+
provider,
|
|
448
|
+
messages,
|
|
449
|
+
systemPrompt,
|
|
450
|
+
apiKey,
|
|
451
|
+
this.config.models[provider],
|
|
452
|
+
this.config.maxTokens,
|
|
453
|
+
this.config.temperature
|
|
454
|
+
);
|
|
455
|
+
const response = {
|
|
456
|
+
text,
|
|
457
|
+
provider,
|
|
458
|
+
model: this.config.models[provider] ?? DEFAULT_MODELS[provider],
|
|
459
|
+
cached: false,
|
|
460
|
+
durationMs: Date.now() - start
|
|
461
|
+
};
|
|
462
|
+
if (this.cache) {
|
|
463
|
+
this.cache.set(messages, text, systemPrompt);
|
|
464
|
+
}
|
|
465
|
+
return response;
|
|
466
|
+
} catch (err) {
|
|
467
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
468
|
+
const rateLimited = err?.rateLimited === true;
|
|
469
|
+
errors.push({ provider, message, rateLimited });
|
|
470
|
+
console.warn(`[novixo-ai] ${provider} failed: ${message}`);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
const summary = errors.map((e) => `${e.provider}: ${e.message}`).join(" | ");
|
|
474
|
+
throw new Error(`All AI providers failed. ${summary}`);
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Convenience: single-turn prompt → response string.
|
|
478
|
+
*
|
|
479
|
+
* @example
|
|
480
|
+
* const text = await ai.ask("What is photosynthesis?")
|
|
481
|
+
*/
|
|
482
|
+
async ask(prompt, systemPrompt) {
|
|
483
|
+
const res = await this.chat(
|
|
484
|
+
[{ role: "user", content: prompt }],
|
|
485
|
+
{ systemPrompt }
|
|
486
|
+
);
|
|
487
|
+
return res.text;
|
|
488
|
+
}
|
|
489
|
+
/** Clear the response cache */
|
|
490
|
+
clearCache() {
|
|
491
|
+
this.cache?.clear();
|
|
492
|
+
}
|
|
493
|
+
/** How many entries are in the cache */
|
|
494
|
+
get cacheSize() {
|
|
495
|
+
return this.cache?.size ?? 0;
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
499
|
+
0 && (module.exports = {
|
|
500
|
+
NovixoAI
|
|
501
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
type Provider = "groq" | "gemini" | "anthropic" | "openai" | "cohere" | "mistral" | "together" | "perplexity" | "huggingface" | "openrouter" | "fireworks" | "deepseek" | "xai" | "ai21" | "nlpcloud";
|
|
2
|
+
interface Message {
|
|
3
|
+
role: "user" | "assistant" | "system";
|
|
4
|
+
content: string;
|
|
5
|
+
}
|
|
6
|
+
interface NovixoAIConfig {
|
|
7
|
+
/** API keys for each provider */
|
|
8
|
+
keys: Partial<Record<Provider, string>>;
|
|
9
|
+
/**
|
|
10
|
+
* Provider priority order. novixo-ai tries them left to right.
|
|
11
|
+
* Defaults to ["groq", "gemini", "openai", "mistral", "anthropic", ...]
|
|
12
|
+
*/
|
|
13
|
+
providers?: Provider[];
|
|
14
|
+
/** Default model per provider. Override if needed. */
|
|
15
|
+
models?: Partial<Record<Provider, string>>;
|
|
16
|
+
/** Max tokens to generate (default: 1024) */
|
|
17
|
+
maxTokens?: number;
|
|
18
|
+
/** Temperature 0–1 (default: 0.7) */
|
|
19
|
+
temperature?: number;
|
|
20
|
+
/** Enable response caching (default: true) */
|
|
21
|
+
cache?: boolean;
|
|
22
|
+
/** Cache TTL in milliseconds (default: 5 minutes) */
|
|
23
|
+
cacheTTL?: number;
|
|
24
|
+
}
|
|
25
|
+
interface AIResponse {
|
|
26
|
+
text: string;
|
|
27
|
+
provider: Provider;
|
|
28
|
+
model: string;
|
|
29
|
+
cached: boolean;
|
|
30
|
+
durationMs: number;
|
|
31
|
+
}
|
|
32
|
+
interface AIError {
|
|
33
|
+
provider: Provider;
|
|
34
|
+
message: string;
|
|
35
|
+
rateLimited: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
declare class NovixoAI {
|
|
39
|
+
private config;
|
|
40
|
+
private cache;
|
|
41
|
+
constructor(config: NovixoAIConfig);
|
|
42
|
+
/**
|
|
43
|
+
* Send a message and get a response.
|
|
44
|
+
* Tries providers in order, skipping rate-limited ones.
|
|
45
|
+
* Falls back automatically on failure.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* const res = await ai.chat([{ role: "user", content: "Explain recursion" }])
|
|
49
|
+
* console.log(res.text)
|
|
50
|
+
*/
|
|
51
|
+
chat(messages: Message[], options?: {
|
|
52
|
+
systemPrompt?: string;
|
|
53
|
+
/** Override provider order for this call only */
|
|
54
|
+
providers?: Provider[];
|
|
55
|
+
}): Promise<AIResponse>;
|
|
56
|
+
/**
|
|
57
|
+
* Convenience: single-turn prompt → response string.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* const text = await ai.ask("What is photosynthesis?")
|
|
61
|
+
*/
|
|
62
|
+
ask(prompt: string, systemPrompt?: string): Promise<string>;
|
|
63
|
+
/** Clear the response cache */
|
|
64
|
+
clearCache(): void;
|
|
65
|
+
/** How many entries are in the cache */
|
|
66
|
+
get cacheSize(): number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export { type AIError, type AIResponse, type Message, NovixoAI, type NovixoAIConfig, type Provider };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
type Provider = "groq" | "gemini" | "anthropic" | "openai" | "cohere" | "mistral" | "together" | "perplexity" | "huggingface" | "openrouter" | "fireworks" | "deepseek" | "xai" | "ai21" | "nlpcloud";
|
|
2
|
+
interface Message {
|
|
3
|
+
role: "user" | "assistant" | "system";
|
|
4
|
+
content: string;
|
|
5
|
+
}
|
|
6
|
+
interface NovixoAIConfig {
|
|
7
|
+
/** API keys for each provider */
|
|
8
|
+
keys: Partial<Record<Provider, string>>;
|
|
9
|
+
/**
|
|
10
|
+
* Provider priority order. novixo-ai tries them left to right.
|
|
11
|
+
* Defaults to ["groq", "gemini", "openai", "mistral", "anthropic", ...]
|
|
12
|
+
*/
|
|
13
|
+
providers?: Provider[];
|
|
14
|
+
/** Default model per provider. Override if needed. */
|
|
15
|
+
models?: Partial<Record<Provider, string>>;
|
|
16
|
+
/** Max tokens to generate (default: 1024) */
|
|
17
|
+
maxTokens?: number;
|
|
18
|
+
/** Temperature 0–1 (default: 0.7) */
|
|
19
|
+
temperature?: number;
|
|
20
|
+
/** Enable response caching (default: true) */
|
|
21
|
+
cache?: boolean;
|
|
22
|
+
/** Cache TTL in milliseconds (default: 5 minutes) */
|
|
23
|
+
cacheTTL?: number;
|
|
24
|
+
}
|
|
25
|
+
interface AIResponse {
|
|
26
|
+
text: string;
|
|
27
|
+
provider: Provider;
|
|
28
|
+
model: string;
|
|
29
|
+
cached: boolean;
|
|
30
|
+
durationMs: number;
|
|
31
|
+
}
|
|
32
|
+
interface AIError {
|
|
33
|
+
provider: Provider;
|
|
34
|
+
message: string;
|
|
35
|
+
rateLimited: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
declare class NovixoAI {
|
|
39
|
+
private config;
|
|
40
|
+
private cache;
|
|
41
|
+
constructor(config: NovixoAIConfig);
|
|
42
|
+
/**
|
|
43
|
+
* Send a message and get a response.
|
|
44
|
+
* Tries providers in order, skipping rate-limited ones.
|
|
45
|
+
* Falls back automatically on failure.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* const res = await ai.chat([{ role: "user", content: "Explain recursion" }])
|
|
49
|
+
* console.log(res.text)
|
|
50
|
+
*/
|
|
51
|
+
chat(messages: Message[], options?: {
|
|
52
|
+
systemPrompt?: string;
|
|
53
|
+
/** Override provider order for this call only */
|
|
54
|
+
providers?: Provider[];
|
|
55
|
+
}): Promise<AIResponse>;
|
|
56
|
+
/**
|
|
57
|
+
* Convenience: single-turn prompt → response string.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* const text = await ai.ask("What is photosynthesis?")
|
|
61
|
+
*/
|
|
62
|
+
ask(prompt: string, systemPrompt?: string): Promise<string>;
|
|
63
|
+
/** Clear the response cache */
|
|
64
|
+
clearCache(): void;
|
|
65
|
+
/** How many entries are in the cache */
|
|
66
|
+
get cacheSize(): number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export { type AIError, type AIResponse, type Message, NovixoAI, type NovixoAIConfig, type Provider };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
// src/cache.ts
|
|
2
|
+
var ResponseCache = class {
|
|
3
|
+
constructor(ttlMs = 5 * 60 * 1e3) {
|
|
4
|
+
this.store = /* @__PURE__ */ new Map();
|
|
5
|
+
this.ttl = ttlMs;
|
|
6
|
+
}
|
|
7
|
+
key(messages, systemPrompt) {
|
|
8
|
+
return JSON.stringify({ messages, systemPrompt });
|
|
9
|
+
}
|
|
10
|
+
get(messages, systemPrompt) {
|
|
11
|
+
const k = this.key(messages, systemPrompt);
|
|
12
|
+
const entry = this.store.get(k);
|
|
13
|
+
if (!entry) return null;
|
|
14
|
+
if (Date.now() > entry.expiresAt) {
|
|
15
|
+
this.store.delete(k);
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
return entry.value;
|
|
19
|
+
}
|
|
20
|
+
set(messages, value, systemPrompt) {
|
|
21
|
+
const k = this.key(messages, systemPrompt);
|
|
22
|
+
this.store.set(k, { value, expiresAt: Date.now() + this.ttl });
|
|
23
|
+
}
|
|
24
|
+
clear() {
|
|
25
|
+
this.store.clear();
|
|
26
|
+
}
|
|
27
|
+
get size() {
|
|
28
|
+
return this.store.size;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// src/providers.ts
|
|
33
|
+
var DEFAULT_MODELS = {
|
|
34
|
+
groq: "llama3-8b-8192",
|
|
35
|
+
gemini: "gemini-1.5-flash",
|
|
36
|
+
anthropic: "claude-haiku-4-5-20251001",
|
|
37
|
+
openai: "gpt-4o-mini",
|
|
38
|
+
cohere: "command-r-plus",
|
|
39
|
+
mistral: "mistral-small-latest",
|
|
40
|
+
together: "meta-llama/Llama-3-8b-chat-hf",
|
|
41
|
+
perplexity: "llama-3-sonar-small-32k-chat",
|
|
42
|
+
huggingface: "mistralai/Mistral-7B-Instruct-v0.2",
|
|
43
|
+
openrouter: "openai/gpt-4o-mini",
|
|
44
|
+
fireworks: "accounts/fireworks/models/llama-v3-8b-instruct",
|
|
45
|
+
deepseek: "deepseek-chat",
|
|
46
|
+
xai: "grok-beta",
|
|
47
|
+
ai21: "jamba-instruct",
|
|
48
|
+
nlpcloud: "finetuned-llama-3-70b"
|
|
49
|
+
};
|
|
50
|
+
var rateLimitedUntil = {};
|
|
51
|
+
function isRateLimited(provider) {
|
|
52
|
+
const until = rateLimitedUntil[provider];
|
|
53
|
+
if (!until) return false;
|
|
54
|
+
if (Date.now() > until) {
|
|
55
|
+
delete rateLimitedUntil[provider];
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
function markRateLimited(provider, retryAfterMs = 6e4) {
|
|
61
|
+
rateLimitedUntil[provider] = Date.now() + retryAfterMs;
|
|
62
|
+
}
|
|
63
|
+
function getRetryAfter(headers, fallback = 6e4) {
|
|
64
|
+
const val = headers.get("retry-after");
|
|
65
|
+
if (!val) return fallback;
|
|
66
|
+
const secs = parseFloat(val);
|
|
67
|
+
return isNaN(secs) ? fallback : secs * 1e3;
|
|
68
|
+
}
|
|
69
|
+
async function callOpenAICompat(url, authHeader, model, messages, systemPrompt, maxTokens, temperature, provider, extraBody = {}) {
|
|
70
|
+
const res = await fetch(url, {
|
|
71
|
+
method: "POST",
|
|
72
|
+
headers: { "Content-Type": "application/json", ...authHeader },
|
|
73
|
+
body: JSON.stringify({
|
|
74
|
+
model,
|
|
75
|
+
messages: [
|
|
76
|
+
...systemPrompt ? [{ role: "system", content: systemPrompt }] : [],
|
|
77
|
+
...messages
|
|
78
|
+
],
|
|
79
|
+
max_tokens: maxTokens,
|
|
80
|
+
temperature,
|
|
81
|
+
...extraBody
|
|
82
|
+
})
|
|
83
|
+
});
|
|
84
|
+
if (res.status === 429) {
|
|
85
|
+
markRateLimited(provider, getRetryAfter(res.headers));
|
|
86
|
+
throw Object.assign(new Error(`${provider} rate limited`), { rateLimited: true });
|
|
87
|
+
}
|
|
88
|
+
if (!res.ok) {
|
|
89
|
+
const err = await res.json().catch(() => ({}));
|
|
90
|
+
throw new Error(err?.error?.message || `${provider} ${res.status}`);
|
|
91
|
+
}
|
|
92
|
+
const data = await res.json();
|
|
93
|
+
return data.choices[0].message.content;
|
|
94
|
+
}
|
|
95
|
+
async function callGroq(messages, systemPrompt, apiKey, model, maxTokens, temperature) {
|
|
96
|
+
return callOpenAICompat(
|
|
97
|
+
"https://api.groq.com/openai/v1/chat/completions",
|
|
98
|
+
{ Authorization: `Bearer ${apiKey}` },
|
|
99
|
+
model,
|
|
100
|
+
messages,
|
|
101
|
+
systemPrompt,
|
|
102
|
+
maxTokens,
|
|
103
|
+
temperature,
|
|
104
|
+
"groq"
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
async function callOpenAI(messages, systemPrompt, apiKey, model, maxTokens, temperature) {
|
|
108
|
+
return callOpenAICompat(
|
|
109
|
+
"https://api.openai.com/v1/chat/completions",
|
|
110
|
+
{ Authorization: `Bearer ${apiKey}` },
|
|
111
|
+
model,
|
|
112
|
+
messages,
|
|
113
|
+
systemPrompt,
|
|
114
|
+
maxTokens,
|
|
115
|
+
temperature,
|
|
116
|
+
"openai"
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
async function callMistral(messages, systemPrompt, apiKey, model, maxTokens, temperature) {
|
|
120
|
+
return callOpenAICompat(
|
|
121
|
+
"https://api.mistral.ai/v1/chat/completions",
|
|
122
|
+
{ Authorization: `Bearer ${apiKey}` },
|
|
123
|
+
model,
|
|
124
|
+
messages,
|
|
125
|
+
systemPrompt,
|
|
126
|
+
maxTokens,
|
|
127
|
+
temperature,
|
|
128
|
+
"mistral"
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
async function callTogether(messages, systemPrompt, apiKey, model, maxTokens, temperature) {
|
|
132
|
+
return callOpenAICompat(
|
|
133
|
+
"https://api.together.xyz/v1/chat/completions",
|
|
134
|
+
{ Authorization: `Bearer ${apiKey}` },
|
|
135
|
+
model,
|
|
136
|
+
messages,
|
|
137
|
+
systemPrompt,
|
|
138
|
+
maxTokens,
|
|
139
|
+
temperature,
|
|
140
|
+
"together"
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
async function callPerplexity(messages, systemPrompt, apiKey, model, maxTokens, temperature) {
|
|
144
|
+
return callOpenAICompat(
|
|
145
|
+
"https://api.perplexity.ai/chat/completions",
|
|
146
|
+
{ Authorization: `Bearer ${apiKey}` },
|
|
147
|
+
model,
|
|
148
|
+
messages,
|
|
149
|
+
systemPrompt,
|
|
150
|
+
maxTokens,
|
|
151
|
+
temperature,
|
|
152
|
+
"perplexity"
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
async function callOpenRouter(messages, systemPrompt, apiKey, model, maxTokens, temperature) {
|
|
156
|
+
return callOpenAICompat(
|
|
157
|
+
"https://openrouter.ai/api/v1/chat/completions",
|
|
158
|
+
{ Authorization: `Bearer ${apiKey}`, "HTTP-Referer": "https://github.com/NovixoTech/novixo-ai" },
|
|
159
|
+
model,
|
|
160
|
+
messages,
|
|
161
|
+
systemPrompt,
|
|
162
|
+
maxTokens,
|
|
163
|
+
temperature,
|
|
164
|
+
"openrouter"
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
async function callFireworks(messages, systemPrompt, apiKey, model, maxTokens, temperature) {
|
|
168
|
+
return callOpenAICompat(
|
|
169
|
+
"https://api.fireworks.ai/inference/v1/chat/completions",
|
|
170
|
+
{ Authorization: `Bearer ${apiKey}` },
|
|
171
|
+
model,
|
|
172
|
+
messages,
|
|
173
|
+
systemPrompt,
|
|
174
|
+
maxTokens,
|
|
175
|
+
temperature,
|
|
176
|
+
"fireworks"
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
async function callDeepSeek(messages, systemPrompt, apiKey, model, maxTokens, temperature) {
|
|
180
|
+
return callOpenAICompat(
|
|
181
|
+
"https://api.deepseek.com/v1/chat/completions",
|
|
182
|
+
{ Authorization: `Bearer ${apiKey}` },
|
|
183
|
+
model,
|
|
184
|
+
messages,
|
|
185
|
+
systemPrompt,
|
|
186
|
+
maxTokens,
|
|
187
|
+
temperature,
|
|
188
|
+
"deepseek"
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
async function callXAI(messages, systemPrompt, apiKey, model, maxTokens, temperature) {
|
|
192
|
+
return callOpenAICompat(
|
|
193
|
+
"https://api.x.ai/v1/chat/completions",
|
|
194
|
+
{ Authorization: `Bearer ${apiKey}` },
|
|
195
|
+
model,
|
|
196
|
+
messages,
|
|
197
|
+
systemPrompt,
|
|
198
|
+
maxTokens,
|
|
199
|
+
temperature,
|
|
200
|
+
"xai"
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
async function callGemini(messages, systemPrompt, apiKey, model, maxTokens, temperature) {
|
|
204
|
+
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`;
|
|
205
|
+
const contents = messages.map((m) => ({
|
|
206
|
+
role: m.role === "assistant" ? "model" : "user",
|
|
207
|
+
parts: [{ text: m.content }]
|
|
208
|
+
}));
|
|
209
|
+
const body = {
|
|
210
|
+
contents,
|
|
211
|
+
generationConfig: { maxOutputTokens: maxTokens, temperature }
|
|
212
|
+
};
|
|
213
|
+
if (systemPrompt) body.system_instruction = { parts: [{ text: systemPrompt }] };
|
|
214
|
+
const res = await fetch(url, {
|
|
215
|
+
method: "POST",
|
|
216
|
+
headers: { "Content-Type": "application/json" },
|
|
217
|
+
body: JSON.stringify(body)
|
|
218
|
+
});
|
|
219
|
+
if (res.status === 429) {
|
|
220
|
+
markRateLimited("gemini", getRetryAfter(res.headers));
|
|
221
|
+
throw Object.assign(new Error("Gemini rate limited"), { rateLimited: true });
|
|
222
|
+
}
|
|
223
|
+
if (!res.ok) {
|
|
224
|
+
const err = await res.json().catch(() => ({}));
|
|
225
|
+
throw new Error(err?.error?.message || `Gemini ${res.status}`);
|
|
226
|
+
}
|
|
227
|
+
const data = await res.json();
|
|
228
|
+
return data.candidates[0].content.parts[0].text;
|
|
229
|
+
}
|
|
230
|
+
async function callAnthropic(messages, systemPrompt, apiKey, model, maxTokens, temperature) {
|
|
231
|
+
const body = {
|
|
232
|
+
model,
|
|
233
|
+
max_tokens: maxTokens,
|
|
234
|
+
temperature,
|
|
235
|
+
messages: messages.filter((m) => m.role !== "system")
|
|
236
|
+
};
|
|
237
|
+
if (systemPrompt) body.system = systemPrompt;
|
|
238
|
+
const res = await fetch("https://api.anthropic.com/v1/messages", {
|
|
239
|
+
method: "POST",
|
|
240
|
+
headers: {
|
|
241
|
+
"Content-Type": "application/json",
|
|
242
|
+
"x-api-key": apiKey,
|
|
243
|
+
"anthropic-version": "2023-06-01"
|
|
244
|
+
},
|
|
245
|
+
body: JSON.stringify(body)
|
|
246
|
+
});
|
|
247
|
+
if (res.status === 429) {
|
|
248
|
+
markRateLimited("anthropic", getRetryAfter(res.headers));
|
|
249
|
+
throw Object.assign(new Error("Anthropic rate limited"), { rateLimited: true });
|
|
250
|
+
}
|
|
251
|
+
if (!res.ok) {
|
|
252
|
+
const err = await res.json().catch(() => ({}));
|
|
253
|
+
throw new Error(err?.error?.message || `Anthropic ${res.status}`);
|
|
254
|
+
}
|
|
255
|
+
const data = await res.json();
|
|
256
|
+
return data.content[0].text;
|
|
257
|
+
}
|
|
258
|
+
async function callCohere(messages, systemPrompt, apiKey, model, maxTokens, temperature) {
|
|
259
|
+
const history = messages.slice(0, -1).map((m) => ({
|
|
260
|
+
role: m.role === "assistant" ? "CHATBOT" : "USER",
|
|
261
|
+
message: m.content
|
|
262
|
+
}));
|
|
263
|
+
const lastMessage = messages[messages.length - 1]?.content ?? "";
|
|
264
|
+
const res = await fetch("https://api.cohere.com/v1/chat", {
|
|
265
|
+
method: "POST",
|
|
266
|
+
headers: {
|
|
267
|
+
"Content-Type": "application/json",
|
|
268
|
+
Authorization: `Bearer ${apiKey}`
|
|
269
|
+
},
|
|
270
|
+
body: JSON.stringify({
|
|
271
|
+
model,
|
|
272
|
+
message: lastMessage,
|
|
273
|
+
chat_history: history,
|
|
274
|
+
preamble: systemPrompt,
|
|
275
|
+
max_tokens: maxTokens,
|
|
276
|
+
temperature
|
|
277
|
+
})
|
|
278
|
+
});
|
|
279
|
+
if (res.status === 429) {
|
|
280
|
+
markRateLimited("cohere", getRetryAfter(res.headers));
|
|
281
|
+
throw Object.assign(new Error("Cohere rate limited"), { rateLimited: true });
|
|
282
|
+
}
|
|
283
|
+
if (!res.ok) {
|
|
284
|
+
const err = await res.json().catch(() => ({}));
|
|
285
|
+
throw new Error(err?.message || `Cohere ${res.status}`);
|
|
286
|
+
}
|
|
287
|
+
const data = await res.json();
|
|
288
|
+
return data.text;
|
|
289
|
+
}
|
|
290
|
+
async function callHuggingFace(messages, systemPrompt, apiKey, model, maxTokens, temperature) {
|
|
291
|
+
return callOpenAICompat(
|
|
292
|
+
`https://api-inference.huggingface.co/models/${model}/v1/chat/completions`,
|
|
293
|
+
{ Authorization: `Bearer ${apiKey}` },
|
|
294
|
+
model,
|
|
295
|
+
messages,
|
|
296
|
+
systemPrompt,
|
|
297
|
+
maxTokens,
|
|
298
|
+
temperature,
|
|
299
|
+
"huggingface"
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
async function callAI21(messages, systemPrompt, apiKey, model, maxTokens, temperature) {
|
|
303
|
+
return callOpenAICompat(
|
|
304
|
+
"https://api.ai21.com/studio/v1/chat/completions",
|
|
305
|
+
{ Authorization: `Bearer ${apiKey}` },
|
|
306
|
+
model,
|
|
307
|
+
messages,
|
|
308
|
+
systemPrompt,
|
|
309
|
+
maxTokens,
|
|
310
|
+
temperature,
|
|
311
|
+
"ai21"
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
async function callNLPCloud(messages, systemPrompt, apiKey, model, maxTokens, temperature) {
|
|
315
|
+
const allMessages = [
|
|
316
|
+
...systemPrompt ? [{ role: "system", content: systemPrompt }] : [],
|
|
317
|
+
...messages
|
|
318
|
+
];
|
|
319
|
+
const res = await fetch(`https://api.nlpcloud.io/v1/gpu/${model}/chatbot`, {
|
|
320
|
+
method: "POST",
|
|
321
|
+
headers: {
|
|
322
|
+
"Content-Type": "application/json",
|
|
323
|
+
Authorization: `Token ${apiKey}`
|
|
324
|
+
},
|
|
325
|
+
body: JSON.stringify({
|
|
326
|
+
input: allMessages[allMessages.length - 1]?.content ?? "",
|
|
327
|
+
history: allMessages.slice(0, -1).map((m) => ({
|
|
328
|
+
input: m.role === "user" ? m.content : void 0,
|
|
329
|
+
response: m.role === "assistant" ? m.content : void 0
|
|
330
|
+
})),
|
|
331
|
+
max_length: maxTokens,
|
|
332
|
+
temperature
|
|
333
|
+
})
|
|
334
|
+
});
|
|
335
|
+
if (res.status === 429) {
|
|
336
|
+
markRateLimited("nlpcloud", getRetryAfter(res.headers));
|
|
337
|
+
throw Object.assign(new Error("NLPCloud rate limited"), { rateLimited: true });
|
|
338
|
+
}
|
|
339
|
+
if (!res.ok) {
|
|
340
|
+
const err = await res.json().catch(() => ({}));
|
|
341
|
+
throw new Error(err?.detail || `NLPCloud ${res.status}`);
|
|
342
|
+
}
|
|
343
|
+
const data = await res.json();
|
|
344
|
+
return data.response;
|
|
345
|
+
}
|
|
346
|
+
async function callProvider(provider, messages, systemPrompt, apiKey, modelOverride, maxTokens, temperature) {
|
|
347
|
+
const model = modelOverride ?? DEFAULT_MODELS[provider];
|
|
348
|
+
const map = {
|
|
349
|
+
groq: callGroq,
|
|
350
|
+
openai: callOpenAI,
|
|
351
|
+
mistral: callMistral,
|
|
352
|
+
together: callTogether,
|
|
353
|
+
perplexity: callPerplexity,
|
|
354
|
+
openrouter: callOpenRouter,
|
|
355
|
+
fireworks: callFireworks,
|
|
356
|
+
deepseek: callDeepSeek,
|
|
357
|
+
xai: callXAI,
|
|
358
|
+
gemini: callGemini,
|
|
359
|
+
anthropic: callAnthropic,
|
|
360
|
+
cohere: callCohere,
|
|
361
|
+
huggingface: callHuggingFace,
|
|
362
|
+
ai21: callAI21,
|
|
363
|
+
nlpcloud: callNLPCloud
|
|
364
|
+
};
|
|
365
|
+
const fn = map[provider];
|
|
366
|
+
if (!fn) throw new Error(`Unknown provider: ${provider}`);
|
|
367
|
+
return fn(messages, systemPrompt, apiKey, model, maxTokens, temperature);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// src/client.ts
|
|
371
|
+
var DEFAULT_PROVIDERS = ["groq", "gemini", "anthropic"];
|
|
372
|
+
var NovixoAI = class {
|
|
373
|
+
constructor(config) {
|
|
374
|
+
this.config = {
|
|
375
|
+
keys: config.keys,
|
|
376
|
+
providers: config.providers ?? DEFAULT_PROVIDERS,
|
|
377
|
+
models: config.models ?? {},
|
|
378
|
+
maxTokens: config.maxTokens ?? 1024,
|
|
379
|
+
temperature: config.temperature ?? 0.7,
|
|
380
|
+
cache: config.cache ?? true,
|
|
381
|
+
cacheTTL: config.cacheTTL ?? 5 * 60 * 1e3
|
|
382
|
+
};
|
|
383
|
+
this.cache = this.config.cache ? new ResponseCache(this.config.cacheTTL) : null;
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Send a message and get a response.
|
|
387
|
+
* Tries providers in order, skipping rate-limited ones.
|
|
388
|
+
* Falls back automatically on failure.
|
|
389
|
+
*
|
|
390
|
+
* @example
|
|
391
|
+
* const res = await ai.chat([{ role: "user", content: "Explain recursion" }])
|
|
392
|
+
* console.log(res.text)
|
|
393
|
+
*/
|
|
394
|
+
async chat(messages, options = {}) {
|
|
395
|
+
const systemPrompt = options.systemPrompt;
|
|
396
|
+
const providers = options.providers ?? this.config.providers;
|
|
397
|
+
const errors = [];
|
|
398
|
+
if (this.cache) {
|
|
399
|
+
const cached = this.cache.get(messages, systemPrompt);
|
|
400
|
+
if (cached) {
|
|
401
|
+
return {
|
|
402
|
+
text: cached,
|
|
403
|
+
provider: "groq",
|
|
404
|
+
// placeholder; cached means we don't know original
|
|
405
|
+
model: "cached",
|
|
406
|
+
cached: true,
|
|
407
|
+
durationMs: 0
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
for (const provider of providers) {
|
|
412
|
+
const apiKey = this.config.keys[provider];
|
|
413
|
+
if (!apiKey) continue;
|
|
414
|
+
if (isRateLimited(provider)) {
|
|
415
|
+
errors.push({ provider, message: "Rate limited, skipping", rateLimited: true });
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
const start = Date.now();
|
|
419
|
+
try {
|
|
420
|
+
const text = await callProvider(
|
|
421
|
+
provider,
|
|
422
|
+
messages,
|
|
423
|
+
systemPrompt,
|
|
424
|
+
apiKey,
|
|
425
|
+
this.config.models[provider],
|
|
426
|
+
this.config.maxTokens,
|
|
427
|
+
this.config.temperature
|
|
428
|
+
);
|
|
429
|
+
const response = {
|
|
430
|
+
text,
|
|
431
|
+
provider,
|
|
432
|
+
model: this.config.models[provider] ?? DEFAULT_MODELS[provider],
|
|
433
|
+
cached: false,
|
|
434
|
+
durationMs: Date.now() - start
|
|
435
|
+
};
|
|
436
|
+
if (this.cache) {
|
|
437
|
+
this.cache.set(messages, text, systemPrompt);
|
|
438
|
+
}
|
|
439
|
+
return response;
|
|
440
|
+
} catch (err) {
|
|
441
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
442
|
+
const rateLimited = err?.rateLimited === true;
|
|
443
|
+
errors.push({ provider, message, rateLimited });
|
|
444
|
+
console.warn(`[novixo-ai] ${provider} failed: ${message}`);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
const summary = errors.map((e) => `${e.provider}: ${e.message}`).join(" | ");
|
|
448
|
+
throw new Error(`All AI providers failed. ${summary}`);
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Convenience: single-turn prompt → response string.
|
|
452
|
+
*
|
|
453
|
+
* @example
|
|
454
|
+
* const text = await ai.ask("What is photosynthesis?")
|
|
455
|
+
*/
|
|
456
|
+
async ask(prompt, systemPrompt) {
|
|
457
|
+
const res = await this.chat(
|
|
458
|
+
[{ role: "user", content: prompt }],
|
|
459
|
+
{ systemPrompt }
|
|
460
|
+
);
|
|
461
|
+
return res.text;
|
|
462
|
+
}
|
|
463
|
+
/** Clear the response cache */
|
|
464
|
+
clearCache() {
|
|
465
|
+
this.cache?.clear();
|
|
466
|
+
}
|
|
467
|
+
/** How many entries are in the cache */
|
|
468
|
+
get cacheSize() {
|
|
469
|
+
return this.cache?.size ?? 0;
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
export {
|
|
473
|
+
NovixoAI
|
|
474
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "novixo-ai",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Unified AI provider client with auto-fallback, rate-limit detection, and response caching. Works with Groq, Gemini, and Anthropic.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"require": "./dist/index.cjs"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsup src/index.ts --format esm,cjs --dts",
|
|
20
|
+
"dev": "tsup src/index.ts --format esm,cjs --dts --watch",
|
|
21
|
+
"test": "node --experimental-vm-modules node_modules/.bin/jest"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"ai",
|
|
25
|
+
"groq",
|
|
26
|
+
"gemini",
|
|
27
|
+
"anthropic",
|
|
28
|
+
"llm",
|
|
29
|
+
"fallback",
|
|
30
|
+
"multi-provider",
|
|
31
|
+
"novixo"
|
|
32
|
+
],
|
|
33
|
+
"author": "NovixoTech",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"tsup": "^8.0.0",
|
|
37
|
+
"typescript": "^5.0.0"
|
|
38
|
+
}
|
|
39
|
+
}
|