chat-agent-toolkit 1.2.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 +234 -0
- package/package.json +101 -0
- package/src/config/config-manager.ts +319 -0
- package/src/config/config-types.ts +65 -0
- package/src/config/environment-variables.ts +8 -0
- package/src/config/index.ts +26 -0
- package/src/config/language-models-database.ts +1426 -0
- package/src/config/mcp-server-registry.ts +24 -0
- package/src/config/model-registry.ts +256 -0
- package/src/config/model-tester.ts +211 -0
- package/src/config/model-utils.ts +193 -0
- package/src/config/provider-ui-config.ts +196 -0
- package/src/connectors/composio.json +154 -0
- package/src/index.ts +22 -0
- package/src/memory/ARCHITECTURE.md +302 -0
- package/src/memory/MASTRA_INTEGRATION.md +106 -0
- package/src/memory/README.md +224 -0
- package/src/memory/agent-memory-manager.ts +416 -0
- package/src/memory/example.ts +343 -0
- package/src/memory/index.ts +47 -0
- package/src/memory/mastra-integration.ts +609 -0
- package/src/memory/storage/drizzle-storage.ts +205 -0
- package/src/memory/storage/in-memory-storage.ts +551 -0
- package/src/memory/storage/storage-interface.ts +68 -0
- package/src/memory/types.ts +125 -0
- package/src/models/providers.ts +5 -0
- package/src/models/registry.ts +4 -0
- package/src/models/types.ts +4 -0
- package/src/search.ts +5 -0
- package/src/tools/composio-mastra.ts +305 -0
- package/src/tools/composio-mcp.ts +205 -0
- package/src/tools/index.ts +13 -0
- package/src/tools/qwksearch-api-tools.ts +327 -0
- package/src/tools/search/doc-utils.ts +75 -0
- package/src/tools/search/document.ts +47 -0
- package/src/tools/search/index.ts +13 -0
- package/src/tools/search/link-summarizer.ts +112 -0
- package/src/tools/search/meta-search-types.ts +62 -0
- package/src/tools/search/metaSearchAgent.ts +454 -0
- package/src/tools/search/search-handlers.ts +85 -0
- package/src/tools/search/suggestionGeneratorAgent.ts +54 -0
- package/src/types.d.ts +137 -0
- package/src/utils/README.md +98 -0
- package/src/utils/chat-helpers.ts +18 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/markdown-to-html.ts +53 -0
- package/src/utils/outputParser.ts +73 -0
- package/src/utils/provider-image-cropper.ts +130 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { configManager } from "./index";
|
|
2
|
+
import type { ConfigModelProvider } from "./config-types";
|
|
3
|
+
|
|
4
|
+
export const getConfiguredModelProviders = (): ConfigModelProvider[] => {
|
|
5
|
+
return configManager.getConfig("modelProviders", []);
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const getConfiguredModelProviderById = (
|
|
9
|
+
id: string,
|
|
10
|
+
): ConfigModelProvider | undefined => {
|
|
11
|
+
return getConfiguredModelProviders().find((p) => p.id === id) ?? undefined;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const getSearxngURL = () =>
|
|
15
|
+
configManager.getConfig("search.searxngURL", "");
|
|
16
|
+
|
|
17
|
+
export const getTavilyApiKey = () =>
|
|
18
|
+
configManager.getConfig("search.tavilyApiKey", "");
|
|
19
|
+
|
|
20
|
+
export const getSourceScrapeCount = (): number =>
|
|
21
|
+
parseInt(configManager.getConfig("search.sourceScrapeCount", "3"), 10) || 3;
|
|
22
|
+
|
|
23
|
+
export const getSourceScrapeTimeout = (): number =>
|
|
24
|
+
parseInt(configManager.getConfig("search.sourceScrapeTimeout", "5"), 10) || 5;
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Registry for managing and loading Vercel AI SDK model providers.
|
|
3
|
+
*
|
|
4
|
+
* Exposes the full API expected by the app's API routes and chat handler:
|
|
5
|
+
* - `activeProviders` (sync getter) / `getActiveProviders()`
|
|
6
|
+
* - `isProviderEnvBased(providerId)`
|
|
7
|
+
* - `loadChatModel(providerId, modelKey)` — returns an AI SDK `LanguageModel`
|
|
8
|
+
* - provider/model CRUD passthroughs to {@link configManager}
|
|
9
|
+
*/
|
|
10
|
+
import configManager from "./config-manager";
|
|
11
|
+
import { LANGUAGE_MODELS } from "./language-models-database";
|
|
12
|
+
import { getModelProvidersUIConfigSection } from "./provider-ui-config";
|
|
13
|
+
import type { ConfigModelProvider, Model } from "./config-types";
|
|
14
|
+
|
|
15
|
+
// Maps a provider UI key to the matching `provider` name in the
|
|
16
|
+
// LANGUAGE_MODELS database (e.g. the "gemini" UI key ↔ "Google" model list).
|
|
17
|
+
const PROVIDER_KEY_TO_DB_NAME: Record<string, string> = {
|
|
18
|
+
openai: "openai",
|
|
19
|
+
anthropic: "anthropic",
|
|
20
|
+
gemini: "google",
|
|
21
|
+
groq: "groq",
|
|
22
|
+
deepseek: "deepseek",
|
|
23
|
+
nvidia: "nvidia",
|
|
24
|
+
openrouter: "openrouter",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** Default chat models for a provider type from the LANGUAGE_MODELS database. */
|
|
28
|
+
const getDefaultChatModels = (providerType: string): Model[] => {
|
|
29
|
+
const dbName = PROVIDER_KEY_TO_DB_NAME[providerType] ?? providerType;
|
|
30
|
+
const entry = LANGUAGE_MODELS.find(
|
|
31
|
+
(p) => p.provider.toLowerCase() === dbName.toLowerCase(),
|
|
32
|
+
);
|
|
33
|
+
if (!entry?.models) return [];
|
|
34
|
+
return entry.models.map((m: any) => ({ name: m.name, key: m.id }));
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** Merges default and user-added models, deduplicated by model key. */
|
|
38
|
+
const mergeChatModels = (provider: ConfigModelProvider): Model[] => {
|
|
39
|
+
const merged = new Map<string, Model>();
|
|
40
|
+
for (const m of getDefaultChatModels(provider.type)) {
|
|
41
|
+
merged.set(m.key, m);
|
|
42
|
+
}
|
|
43
|
+
for (const m of provider.chatModels || []) {
|
|
44
|
+
merged.set(m.key, { key: m.key, name: m.name });
|
|
45
|
+
}
|
|
46
|
+
return [...merged.values()];
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export default class ModelRegistry {
|
|
50
|
+
/**
|
|
51
|
+
* Currently configured providers (env-based + user-added).
|
|
52
|
+
* Sync getter used by the chat handler for logging and lookups.
|
|
53
|
+
*/
|
|
54
|
+
get activeProviders(): ConfigModelProvider[] {
|
|
55
|
+
return configManager.getCurrentConfig().modelProviders;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Providers with their full chat model lists (defaults merged with
|
|
60
|
+
* user-added models), shaped for the settings/providers UI.
|
|
61
|
+
*/
|
|
62
|
+
async getActiveProviders() {
|
|
63
|
+
return this.activeProviders.map((p) => ({
|
|
64
|
+
id: p.id,
|
|
65
|
+
name: p.name,
|
|
66
|
+
type: p.type,
|
|
67
|
+
chatModels: mergeChatModels(p),
|
|
68
|
+
}));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Finds a provider by id, falling back to free providers in order:
|
|
73
|
+
* OpenRouter (no daily limits, best for guests), Groq (fastest, daily limits),
|
|
74
|
+
* then NVIDIA, then the first configured provider.
|
|
75
|
+
* Client-side provider ids are config hashes that go stale whenever
|
|
76
|
+
* server env config changes, so a graceful fallback keeps existing
|
|
77
|
+
* chat sessions working after a redeploy.
|
|
78
|
+
*/
|
|
79
|
+
private findProvider(providerId?: string): ConfigModelProvider | undefined {
|
|
80
|
+
const providers = this.activeProviders;
|
|
81
|
+
let provider = providers.find((p) => p.id === providerId);
|
|
82
|
+
|
|
83
|
+
if (!provider && providers.length > 0) {
|
|
84
|
+
// Prioritize OpenRouter (no daily limits) over Groq (has daily limits)
|
|
85
|
+
provider =
|
|
86
|
+
providers.find((p) => p.name.toLowerCase().includes("openrouter")) ??
|
|
87
|
+
providers.find((p) => p.name.toLowerCase().includes("groq")) ??
|
|
88
|
+
providers.find((p) => p.name.toLowerCase().includes("nvidia")) ??
|
|
89
|
+
providers[0];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return provider;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Whether the resolved provider was configured from environment variables. */
|
|
96
|
+
isProviderEnvBased(providerId?: string): boolean {
|
|
97
|
+
return this.findProvider(providerId)?.isEnvBased === true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Instantiates a Vercel AI SDK language model for the given provider and
|
|
102
|
+
* model key. Falls back to the provider's first/default model when no key
|
|
103
|
+
* is given. Temperature is a per-call setting in the AI SDK, so callers
|
|
104
|
+
* pass it to generateText/streamText rather than the model instance.
|
|
105
|
+
*/
|
|
106
|
+
async loadChatModel(providerId?: string, modelKey?: string): Promise<any> {
|
|
107
|
+
const provider = this.findProvider(providerId);
|
|
108
|
+
|
|
109
|
+
if (!provider) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
"No model providers configured. Please add a provider in settings.",
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const type = provider.type.toLowerCase();
|
|
116
|
+
const config = provider.config || {};
|
|
117
|
+
const apiKey = config.apiKey || "";
|
|
118
|
+
const availableModels = mergeChatModels(provider);
|
|
119
|
+
const modelName =
|
|
120
|
+
modelKey || availableModels[0]?.key || "";
|
|
121
|
+
|
|
122
|
+
console.log(
|
|
123
|
+
`[ModelRegistry] loadChatModel: providerId=${providerId} provider=${provider.name} type=${type} requestedModel=${modelKey ?? "(default)"} resolvedModel=${modelName}`,
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
if (!modelName) {
|
|
127
|
+
throw new Error(`No chat models available for provider "${provider.name}"`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Validate that the requested model exists in the provider's model list
|
|
131
|
+
if (modelKey && !availableModels.some((m) => m.key === modelKey)) {
|
|
132
|
+
console.warn(
|
|
133
|
+
`[ModelRegistry] Requested model "${modelKey}" not found in provider "${provider.name}". Available models: ${availableModels.map((m) => m.key).join(", ")}`,
|
|
134
|
+
);
|
|
135
|
+
throw new Error(
|
|
136
|
+
`Model "${modelKey}" is not available for provider "${provider.name}". Please select a different model in Settings.`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Validate API key is present
|
|
141
|
+
if (!apiKey) {
|
|
142
|
+
throw new Error(
|
|
143
|
+
`No API key configured for provider "${provider.name}". Please add your API key in Settings → Model Providers.`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// OpenAI-compatible providers differ only by base URL.
|
|
148
|
+
const openAICompatibleBaseURLs: Record<string, string> = {
|
|
149
|
+
openai: "https://api.openai.com/v1",
|
|
150
|
+
togetherai: "https://api.together.xyz/v1",
|
|
151
|
+
perplexity: "https://api.perplexity.ai",
|
|
152
|
+
nvidia: "https://integrate.api.nvidia.com/v1",
|
|
153
|
+
openrouter: "https://openrouter.ai/api/v1",
|
|
154
|
+
deepseek: "https://api.deepseek.com",
|
|
155
|
+
xai: "https://api.x.ai/v1",
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
if (type in openAICompatibleBaseURLs) {
|
|
159
|
+
const { createOpenAI } = await import("@ai-sdk/openai");
|
|
160
|
+
return createOpenAI({
|
|
161
|
+
apiKey,
|
|
162
|
+
baseURL: config.baseURL || openAICompatibleBaseURLs[type],
|
|
163
|
+
}).chat(modelName);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
switch (type) {
|
|
167
|
+
case "cloudflare": {
|
|
168
|
+
const { createOpenAI } = await import("@ai-sdk/openai");
|
|
169
|
+
const [cfApiToken, accountId] = apiKey.split(":");
|
|
170
|
+
return createOpenAI({
|
|
171
|
+
apiKey: cfApiToken,
|
|
172
|
+
baseURL: `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/v1`,
|
|
173
|
+
}).chat(modelName);
|
|
174
|
+
}
|
|
175
|
+
case "groq": {
|
|
176
|
+
const { createGroq } = await import("@ai-sdk/groq");
|
|
177
|
+
return createGroq({ apiKey })(modelName);
|
|
178
|
+
}
|
|
179
|
+
case "anthropic": {
|
|
180
|
+
const { createAnthropic } = await import("@ai-sdk/anthropic");
|
|
181
|
+
return createAnthropic({ apiKey })(modelName);
|
|
182
|
+
}
|
|
183
|
+
case "gemini":
|
|
184
|
+
case "google": {
|
|
185
|
+
const { createGoogleGenerativeAI } = await import("@ai-sdk/google");
|
|
186
|
+
return createGoogleGenerativeAI({ apiKey })(modelName);
|
|
187
|
+
}
|
|
188
|
+
default:
|
|
189
|
+
throw new Error(`Unsupported provider type: ${type}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Registers a new provider. Accepts both `(type, config)` — used by the
|
|
195
|
+
* providers API route — and `(type, name, config)`.
|
|
196
|
+
*/
|
|
197
|
+
async addProvider(type: string, nameOrConfig: any, config?: Record<string, any>) {
|
|
198
|
+
let name: string;
|
|
199
|
+
let finalConfig: Record<string, any>;
|
|
200
|
+
if (typeof nameOrConfig === "object" && nameOrConfig !== null && !config) {
|
|
201
|
+
finalConfig = nameOrConfig;
|
|
202
|
+
const section = getModelProvidersUIConfigSection().find(
|
|
203
|
+
(s) => s.key === type,
|
|
204
|
+
);
|
|
205
|
+
name = section?.name || type.toUpperCase();
|
|
206
|
+
} else {
|
|
207
|
+
name = String(nameOrConfig);
|
|
208
|
+
finalConfig = config || {};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const newProvider = configManager.addModelProvider(type, name, finalConfig);
|
|
212
|
+
return {
|
|
213
|
+
...newProvider,
|
|
214
|
+
chatModels: getDefaultChatModels(type),
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async removeProvider(providerId: string) {
|
|
219
|
+
configManager.removeModelProvider(providerId);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Updates a provider's config. Accepts both `(id, config)` — used by the
|
|
224
|
+
* providers API route — and `(id, name, config)`.
|
|
225
|
+
*/
|
|
226
|
+
async updateProvider(providerId: string, nameOrConfig: any, config?: Record<string, any>) {
|
|
227
|
+
let name: string;
|
|
228
|
+
let finalConfig: Record<string, any>;
|
|
229
|
+
if (typeof nameOrConfig === "object" && nameOrConfig !== null && !config) {
|
|
230
|
+
finalConfig = nameOrConfig;
|
|
231
|
+
const existing = this.activeProviders.find((p) => p.id === providerId);
|
|
232
|
+
name = existing?.name || providerId;
|
|
233
|
+
} else {
|
|
234
|
+
name = String(nameOrConfig);
|
|
235
|
+
finalConfig = config || {};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const updated = await configManager.updateModelProvider(
|
|
239
|
+
providerId,
|
|
240
|
+
name,
|
|
241
|
+
finalConfig,
|
|
242
|
+
);
|
|
243
|
+
return {
|
|
244
|
+
...updated,
|
|
245
|
+
chatModels: mergeChatModels(updated),
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async addProviderModel(providerId: string, type: "chat", model: any) {
|
|
250
|
+
return configManager.addProviderModel(providerId, type, model);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async removeProviderModel(providerId: string, type: "chat", modelKey: string) {
|
|
254
|
+
configManager.removeProviderModel(providerId, type, modelKey);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Model availability testing and validation
|
|
3
|
+
* Tests which models are actually working for a given provider
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { generateText } from "ai";
|
|
7
|
+
import { createLLMProvider } from "write-language/provider-factory";
|
|
8
|
+
|
|
9
|
+
export interface ModelTestResult {
|
|
10
|
+
modelId: string;
|
|
11
|
+
modelName: string;
|
|
12
|
+
available: boolean;
|
|
13
|
+
error?: string;
|
|
14
|
+
latency?: number;
|
|
15
|
+
type?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ProviderTestResult {
|
|
19
|
+
provider: string;
|
|
20
|
+
totalModels: number;
|
|
21
|
+
availableModels: ModelTestResult[];
|
|
22
|
+
unavailableModels: ModelTestResult[];
|
|
23
|
+
testDuration: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Test a single model to see if it's working
|
|
28
|
+
*/
|
|
29
|
+
export async function testModel(
|
|
30
|
+
provider: string,
|
|
31
|
+
apiKey: string,
|
|
32
|
+
modelId: string,
|
|
33
|
+
modelName: string,
|
|
34
|
+
modelType: string = "text-generation",
|
|
35
|
+
timeout: number = 10000
|
|
36
|
+
): Promise<ModelTestResult> {
|
|
37
|
+
const startTime = Date.now();
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
// Only test text-generation models
|
|
41
|
+
if (modelType !== "text-generation") {
|
|
42
|
+
return {
|
|
43
|
+
modelId,
|
|
44
|
+
modelName,
|
|
45
|
+
available: false,
|
|
46
|
+
error: `Skipped: ${modelType} models not testable via text generation`,
|
|
47
|
+
type: modelType,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const model = createLLMProvider(
|
|
52
|
+
provider.toLowerCase(),
|
|
53
|
+
apiKey,
|
|
54
|
+
modelId,
|
|
55
|
+
0.1
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
if (!model) {
|
|
59
|
+
return {
|
|
60
|
+
modelId,
|
|
61
|
+
modelName,
|
|
62
|
+
available: false,
|
|
63
|
+
error: "Provider not supported",
|
|
64
|
+
type: modelType,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Create a promise that will timeout
|
|
69
|
+
const timeoutPromise = new Promise((_, reject) =>
|
|
70
|
+
setTimeout(() => reject(new Error("Test timeout")), timeout)
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// Test with a simple prompt
|
|
74
|
+
const testPromise = generateText({
|
|
75
|
+
model,
|
|
76
|
+
prompt: "Reply with just 'OK'",
|
|
77
|
+
maxTokens: 10,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const result = await Promise.race([testPromise, timeoutPromise]) as any;
|
|
81
|
+
|
|
82
|
+
const latency = Date.now() - startTime;
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
modelId,
|
|
86
|
+
modelName,
|
|
87
|
+
available: true,
|
|
88
|
+
latency,
|
|
89
|
+
type: modelType,
|
|
90
|
+
};
|
|
91
|
+
} catch (error: any) {
|
|
92
|
+
return {
|
|
93
|
+
modelId,
|
|
94
|
+
modelName,
|
|
95
|
+
available: false,
|
|
96
|
+
error: error.message || String(error),
|
|
97
|
+
latency: Date.now() - startTime,
|
|
98
|
+
type: modelType,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Test all models for a provider
|
|
105
|
+
*/
|
|
106
|
+
export async function testProviderModels(
|
|
107
|
+
provider: string,
|
|
108
|
+
apiKey: string,
|
|
109
|
+
models: Array<{ id: string; name: string; type?: string; free?: boolean }>,
|
|
110
|
+
options: {
|
|
111
|
+
onlyFree?: boolean;
|
|
112
|
+
concurrency?: number;
|
|
113
|
+
timeout?: number;
|
|
114
|
+
onProgress?: (current: number, total: number, modelName: string) => void;
|
|
115
|
+
} = {}
|
|
116
|
+
): Promise<ProviderTestResult> {
|
|
117
|
+
const {
|
|
118
|
+
onlyFree = false,
|
|
119
|
+
concurrency = 3,
|
|
120
|
+
timeout = 10000,
|
|
121
|
+
onProgress,
|
|
122
|
+
} = options;
|
|
123
|
+
|
|
124
|
+
const startTime = Date.now();
|
|
125
|
+
|
|
126
|
+
// Filter models
|
|
127
|
+
let modelsToTest = models;
|
|
128
|
+
if (onlyFree) {
|
|
129
|
+
modelsToTest = models.filter((m) => m.free !== false);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const results: ModelTestResult[] = [];
|
|
133
|
+
const queue = [...modelsToTest];
|
|
134
|
+
let completed = 0;
|
|
135
|
+
|
|
136
|
+
// Test models with concurrency control
|
|
137
|
+
const workers = Array(Math.min(concurrency, modelsToTest.length))
|
|
138
|
+
.fill(null)
|
|
139
|
+
.map(async () => {
|
|
140
|
+
while (queue.length > 0) {
|
|
141
|
+
const model = queue.shift();
|
|
142
|
+
if (!model) break;
|
|
143
|
+
|
|
144
|
+
const result = await testModel(
|
|
145
|
+
provider,
|
|
146
|
+
apiKey,
|
|
147
|
+
model.id,
|
|
148
|
+
model.name,
|
|
149
|
+
model.type || "text-generation",
|
|
150
|
+
timeout
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
results.push(result);
|
|
154
|
+
completed++;
|
|
155
|
+
|
|
156
|
+
if (onProgress) {
|
|
157
|
+
onProgress(completed, modelsToTest.length, model.name);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
await Promise.all(workers);
|
|
163
|
+
|
|
164
|
+
const availableModels = results.filter((r) => r.available);
|
|
165
|
+
const unavailableModels = results.filter((r) => !r.available);
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
provider,
|
|
169
|
+
totalModels: modelsToTest.length,
|
|
170
|
+
availableModels,
|
|
171
|
+
unavailableModels,
|
|
172
|
+
testDuration: Date.now() - startTime,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Get only free models from a model list
|
|
178
|
+
*/
|
|
179
|
+
export function getOnlyFreeModels<T extends { free?: boolean }>(
|
|
180
|
+
models: T[]
|
|
181
|
+
): T[] {
|
|
182
|
+
return models.filter((m) => m.free === true);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Categorize models by type
|
|
187
|
+
*/
|
|
188
|
+
export function categorizeModelsByType<
|
|
189
|
+
T extends { type?: string; id: string; name: string }
|
|
190
|
+
>(models: T[]): Record<string, T[]> {
|
|
191
|
+
const categories: Record<string, T[]> = {
|
|
192
|
+
"text-generation": [],
|
|
193
|
+
vision: [],
|
|
194
|
+
embedding: [],
|
|
195
|
+
reranker: [],
|
|
196
|
+
image: [],
|
|
197
|
+
video: [],
|
|
198
|
+
audio: [],
|
|
199
|
+
other: [],
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
for (const model of models) {
|
|
203
|
+
const type = model.type || "text-generation";
|
|
204
|
+
if (!categories[type]) {
|
|
205
|
+
categories[type] = [];
|
|
206
|
+
}
|
|
207
|
+
categories[type].push(model);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return categories;
|
|
211
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for working with AI models
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* List of model IDs that are free to use (no per-token cost)
|
|
7
|
+
* Updated regularly as providers change their pricing
|
|
8
|
+
*/
|
|
9
|
+
export const FREE_MODELS = new Set([
|
|
10
|
+
// OpenRouter free models
|
|
11
|
+
"meta-llama/llama-3.3-70b-instruct",
|
|
12
|
+
"nvidia/llama-3.1-nemotron-70b-instruct",
|
|
13
|
+
"qwen/qwen-2.5-72b-instruct",
|
|
14
|
+
"deepseek/deepseek-v3",
|
|
15
|
+
"deepseek/deepseek-r1",
|
|
16
|
+
"meta-llama/llama-4-scout-17b-16e-instruct",
|
|
17
|
+
|
|
18
|
+
// NVIDIA NIM free tier models
|
|
19
|
+
"moonshotai/kimi-k2.5",
|
|
20
|
+
"nvidia/nemotron-nano-12b-v2-vl",
|
|
21
|
+
"nvidia/llama-nemotron",
|
|
22
|
+
"qwen/qwen2.5",
|
|
23
|
+
"meta/llama-4",
|
|
24
|
+
|
|
25
|
+
// Groq free tier models
|
|
26
|
+
"deepseek-r1-distill-llama-70b",
|
|
27
|
+
"meta-llama/llama-4-maverick-17b-128e-instruct",
|
|
28
|
+
"meta-llama/llama-4-scout-17b-16e-instruct",
|
|
29
|
+
"llama-3.3-70b-versatile",
|
|
30
|
+
"llama-3.3-70b-specdec",
|
|
31
|
+
"llama-3.2-3b-preview",
|
|
32
|
+
"llama-3.2-11b-vision-preview",
|
|
33
|
+
"llama-3.2-90b-vision-preview",
|
|
34
|
+
"llama-3.1-70b-versatile",
|
|
35
|
+
"llama-3.1-8b-instant",
|
|
36
|
+
"mixtral-8x7b-32768",
|
|
37
|
+
|
|
38
|
+
// Cloudflare Workers AI (free tier)
|
|
39
|
+
"llama-4-scout-17b-16e-instruct",
|
|
40
|
+
"llama-3.3-70b-instruct-fp8-fast",
|
|
41
|
+
"llama-3.1-8b-instruct-fast",
|
|
42
|
+
"gemma-3-12b-it",
|
|
43
|
+
"mistral-small-3.1-24b-instruct",
|
|
44
|
+
"qwq-32b",
|
|
45
|
+
"qwen2.5-coder-32b-instruct",
|
|
46
|
+
"deepseek-r1-distill-qwen-32b",
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Providers that offer free tiers with generous limits
|
|
51
|
+
*/
|
|
52
|
+
export const FREE_TIER_PROVIDERS = new Set([
|
|
53
|
+
"openrouter", // Free models available
|
|
54
|
+
"nvidia", // Free NIM tier
|
|
55
|
+
"groq", // Free tier with rate limits
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Check if a model is free to use (no per-token cost)
|
|
60
|
+
* Note: Free models may have rate limits
|
|
61
|
+
*/
|
|
62
|
+
export function isModelFree(modelId: string): boolean {
|
|
63
|
+
return FREE_MODELS.has(modelId);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Check if a provider offers a free tier
|
|
68
|
+
*/
|
|
69
|
+
export function hasFreeTier(providerType: string): boolean {
|
|
70
|
+
return FREE_TIER_PROVIDERS.has(providerType.toLowerCase());
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Get a display name for a model with cost indicator
|
|
75
|
+
* Example: "Llama 3.3 70B (Free)" or "GPT-4o"
|
|
76
|
+
*/
|
|
77
|
+
export function getModelDisplayName(modelName: string, modelId: string): string {
|
|
78
|
+
const isFree = isModelFree(modelId);
|
|
79
|
+
|
|
80
|
+
// If already has "(Free)" in the name, return as-is
|
|
81
|
+
if (modelName.includes("(Free)")) {
|
|
82
|
+
return modelName;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return isFree ? `${modelName} (Free)` : modelName;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Get provider-specific information
|
|
90
|
+
*/
|
|
91
|
+
export function getProviderInfo(providerType: string): {
|
|
92
|
+
name: string;
|
|
93
|
+
hasFreeTier: boolean;
|
|
94
|
+
signupUrl: string;
|
|
95
|
+
docsUrl: string;
|
|
96
|
+
} {
|
|
97
|
+
const type = providerType.toLowerCase();
|
|
98
|
+
|
|
99
|
+
const providers: Record<string, any> = {
|
|
100
|
+
openrouter: {
|
|
101
|
+
name: "OpenRouter",
|
|
102
|
+
hasFreeTier: true,
|
|
103
|
+
signupUrl: "https://openrouter.ai",
|
|
104
|
+
docsUrl: "https://openrouter.ai/docs",
|
|
105
|
+
},
|
|
106
|
+
nvidia: {
|
|
107
|
+
name: "NVIDIA",
|
|
108
|
+
hasFreeTier: true,
|
|
109
|
+
signupUrl: "https://build.nvidia.com",
|
|
110
|
+
docsUrl: "https://docs.api.nvidia.com/nim/",
|
|
111
|
+
},
|
|
112
|
+
groq: {
|
|
113
|
+
name: "Groq",
|
|
114
|
+
hasFreeTier: true,
|
|
115
|
+
signupUrl: "https://console.groq.com",
|
|
116
|
+
docsUrl: "https://console.groq.com/docs",
|
|
117
|
+
},
|
|
118
|
+
anthropic: {
|
|
119
|
+
name: "Anthropic",
|
|
120
|
+
hasFreeTier: false,
|
|
121
|
+
signupUrl: "https://console.anthropic.com",
|
|
122
|
+
docsUrl: "https://docs.anthropic.com",
|
|
123
|
+
},
|
|
124
|
+
openai: {
|
|
125
|
+
name: "OpenAI",
|
|
126
|
+
hasFreeTier: false,
|
|
127
|
+
signupUrl: "https://platform.openai.com",
|
|
128
|
+
docsUrl: "https://platform.openai.com/docs",
|
|
129
|
+
},
|
|
130
|
+
google: {
|
|
131
|
+
name: "Google Gemini",
|
|
132
|
+
hasFreeTier: true,
|
|
133
|
+
signupUrl: "https://aistudio.google.com",
|
|
134
|
+
docsUrl: "https://ai.google.dev/docs",
|
|
135
|
+
},
|
|
136
|
+
gemini: {
|
|
137
|
+
name: "Google Gemini",
|
|
138
|
+
hasFreeTier: true,
|
|
139
|
+
signupUrl: "https://aistudio.google.com",
|
|
140
|
+
docsUrl: "https://ai.google.dev/docs",
|
|
141
|
+
},
|
|
142
|
+
deepseek: {
|
|
143
|
+
name: "DeepSeek",
|
|
144
|
+
hasFreeTier: false,
|
|
145
|
+
signupUrl: "https://platform.deepseek.com",
|
|
146
|
+
docsUrl: "https://platform.deepseek.com/docs",
|
|
147
|
+
},
|
|
148
|
+
cloudflare: {
|
|
149
|
+
name: "Cloudflare Workers AI",
|
|
150
|
+
hasFreeTier: true,
|
|
151
|
+
signupUrl: "https://dash.cloudflare.com",
|
|
152
|
+
docsUrl: "https://developers.cloudflare.com/workers-ai/",
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
return providers[type] || {
|
|
157
|
+
name: providerType,
|
|
158
|
+
hasFreeTier: false,
|
|
159
|
+
signupUrl: "",
|
|
160
|
+
docsUrl: "",
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Get recommended free providers for getting started
|
|
166
|
+
*/
|
|
167
|
+
export function getRecommendedFreeProviders(): Array<{
|
|
168
|
+
type: string;
|
|
169
|
+
name: string;
|
|
170
|
+
reason: string;
|
|
171
|
+
signupUrl: string;
|
|
172
|
+
}> {
|
|
173
|
+
return [
|
|
174
|
+
{
|
|
175
|
+
type: "openrouter",
|
|
176
|
+
name: "OpenRouter",
|
|
177
|
+
reason: "Access to Llama 3.3 70B, Nemotron, and other free models",
|
|
178
|
+
signupUrl: "https://openrouter.ai",
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
type: "groq",
|
|
182
|
+
name: "Groq",
|
|
183
|
+
reason: "Ultra-fast inference on Llama models, generous free tier",
|
|
184
|
+
signupUrl: "https://console.groq.com",
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
type: "nvidia",
|
|
188
|
+
name: "NVIDIA",
|
|
189
|
+
reason: "Free access to Nemotron and other powerful models",
|
|
190
|
+
signupUrl: "https://build.nvidia.com",
|
|
191
|
+
},
|
|
192
|
+
];
|
|
193
|
+
}
|