* fix: improve local model provider robustness and UX - Extract shared Docker URL rewriting and env conversion into BaseProvider to eliminate 4x duplicated code across Ollama and LMStudio - Add error handling and 5s timeouts to all model-listing fetches so one unreachable provider doesn't block the entire model list - Fix Ollama using createOllama() instead of mutating provider internals - Fix LLMManager singleton ignoring env updates on subsequent requests - Narrow cache key to only include provider-relevant env vars instead of the entire server environment - Fix 'as any' casts in LMStudio and OpenAILike by using shared convertEnvToRecord helper - Replace console.log/error with structured logger in OpenAILike - Fix typo: filteredStaticModesl -> filteredStaticModels in manager - Add connection status indicator (green/red dot) for local providers in the ModelSelector dropdown - Show helpful "is X running?" message when local provider has no models Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add Cerebras LLM provider - Add Cerebras provider with 8 models (Llama, GPT OSS, Qwen, ZAI GLM) - Integrate @ai-sdk/cerebras@0.2.16 for compatibility - Add CEREBRAS_API_KEY to environment configuration - Register provider in LLMManager registry Models included: - llama3.1-8b, llama-3.3-70b - gpt-oss-120b (reasoning) - qwen-3-32b, qwen-3-235b variants - zai-glm-4.6, zai-glm-4.7 (reasoning) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * feat: add Fireworks AI LLM provider - Add Fireworks provider with 6 popular models - Integrate @ai-sdk/fireworks@0.2.16 for compatibility - Add FIREWORKS_API_KEY to environment configuration - Register provider in LLMManager registry Models included: - Llama 3.1 variants (405B, 70B, 8B Instruct) - DeepSeek R1 (reasoning model) - Qwen 2.5 72B Instruct - FireFunction V2 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * feat: add coding-specific models to existing providers Enhanced providers with state-of-the-art coding models: **DeepSeek Provider:** + DeepSeek V3.2 (integrates thinking + tool-use) + DeepSeek V3.2-Speciale (high-compute variant, beats GPT-5) **Fireworks Provider:** + Qwen3-Coder 480B (262K context, best for coding) + Qwen3-Coder 30B (fast coding specialist) **Cerebras Provider:** + Qwen3-Coder 480B (2000 tokens/sec!) - Removed deprecated models (qwen-3-32b, llama-3.3-70b) Total new models: 4 Total coding models across all providers: 12+ Performance highlights: - Qwen3-Coder: State-of-the-art coding performance - DeepSeek V3.2: Integrates thinking directly into tool-use - ZAI GLM 4.6: 73.8% SWE-bench score - Ultra-fast inference: 2000 tok/s on Cerebras Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * feat: add dynamic model discovery to providers Implemented getDynamicModels() for automatic model discovery: **DeepSeek Provider:** - Fetches models from https://api.deepseek.com/models - Automatically discovers new models as DeepSeek adds them - Filters out static models to avoid duplicates **Cerebras Provider:** - Fetches models from https://api.cerebras.ai/v1/models - Auto-discovers new Cerebras models - Keeps UI up-to-date with latest offerings **Fireworks Provider:** - Fetches from https://api.fireworks.ai/v1/accounts/fireworks/models - Includes context_length from API response - Discovers new Qwen-Coder and other models automatically **Moonshot Provider:** - Fetches from https://api.moonshot.ai/v1/models - OpenAI-compatible endpoint - Auto-discovers new Kimi models Benefits: - ✅ No manual updates needed when providers add new models - ✅ Users always have access to latest models - ✅ Graceful fallback to static models if API fails - ✅ 5-second timeout prevents hanging - ✅ Caching system built into BaseProvider Technical details: - Uses BaseProvider's built-in caching system - Cache invalidates when API keys change - Failed API calls fallback to static models - All endpoints have 5-second timeout protection Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * feat: add Z.AI provider with GLM models and JWT authentication Merged changes from PR #2069 to add Z.AI provider: - Added GLM-4.6 (200K), GLM-4.5 (128K), and GLM-4.5 Flash models - Implemented secure JWT token generation with HMAC-SHA256 signing - Added dynamic model discovery from Z.AI API - Included proper error handling and token validation - GLM-4.6 achieves 73.8% on SWE-bench coding benchmarks Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
213 lines
6.9 KiB
TypeScript
213 lines
6.9 KiB
TypeScript
import type { IProviderSetting } from '~/types/model';
|
|
import { BaseProvider } from './base-provider';
|
|
import type { ModelInfo, ProviderInfo } from './types';
|
|
import * as providers from './registry';
|
|
import { createScopedLogger } from '~/utils/logger';
|
|
|
|
const logger = createScopedLogger('LLMManager');
|
|
export class LLMManager {
|
|
private static _instance: LLMManager;
|
|
private _providers: Map<string, BaseProvider> = new Map();
|
|
private _modelList: ModelInfo[] = [];
|
|
private _env: Record<string, string> = {};
|
|
|
|
private constructor(_env: Record<string, string>) {
|
|
this._registerProvidersFromDirectory();
|
|
this._env = _env;
|
|
}
|
|
|
|
static getInstance(env: Record<string, string> = {}): LLMManager {
|
|
if (!LLMManager._instance) {
|
|
LLMManager._instance = new LLMManager(env);
|
|
} else if (Object.keys(env).length > 0) {
|
|
// Update env on subsequent calls so Cloudflare Workers get fresh bindings
|
|
LLMManager._instance._env = env;
|
|
}
|
|
|
|
return LLMManager._instance;
|
|
}
|
|
get env() {
|
|
return this._env;
|
|
}
|
|
|
|
private async _registerProvidersFromDirectory() {
|
|
try {
|
|
/*
|
|
* Dynamically import all files from the providers directory
|
|
* const providerModules = import.meta.glob('./providers/*.ts', { eager: true });
|
|
*/
|
|
|
|
// Look for exported classes that extend BaseProvider
|
|
for (const exportedItem of Object.values(providers)) {
|
|
if (typeof exportedItem === 'function' && exportedItem.prototype instanceof BaseProvider) {
|
|
const provider = new exportedItem();
|
|
|
|
try {
|
|
this.registerProvider(provider);
|
|
} catch (error: any) {
|
|
logger.warn('Failed To Register Provider: ', provider.name, 'error:', error.message);
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
logger.error('Error registering providers:', error);
|
|
}
|
|
}
|
|
|
|
registerProvider(provider: BaseProvider) {
|
|
if (this._providers.has(provider.name)) {
|
|
logger.warn(`Provider ${provider.name} is already registered. Skipping.`);
|
|
return;
|
|
}
|
|
|
|
logger.info('Registering Provider: ', provider.name);
|
|
this._providers.set(provider.name, provider);
|
|
this._modelList = [...this._modelList, ...provider.staticModels];
|
|
}
|
|
|
|
getProvider(name: string): BaseProvider | undefined {
|
|
return this._providers.get(name);
|
|
}
|
|
|
|
getAllProviders(): BaseProvider[] {
|
|
return Array.from(this._providers.values());
|
|
}
|
|
|
|
getModelList(): ModelInfo[] {
|
|
return this._modelList;
|
|
}
|
|
|
|
async updateModelList(options: {
|
|
apiKeys?: Record<string, string>;
|
|
providerSettings?: Record<string, IProviderSetting>;
|
|
serverEnv?: Record<string, string>;
|
|
}): Promise<ModelInfo[]> {
|
|
const { apiKeys, providerSettings, serverEnv } = options;
|
|
|
|
let enabledProviders = Array.from(this._providers.values()).map((p) => p.name);
|
|
|
|
if (providerSettings && Object.keys(providerSettings).length > 0) {
|
|
enabledProviders = enabledProviders.filter((p) => providerSettings[p].enabled);
|
|
}
|
|
|
|
// Get dynamic models from all providers that support them
|
|
const dynamicModels = await Promise.all(
|
|
Array.from(this._providers.values())
|
|
.filter((provider) => enabledProviders.includes(provider.name))
|
|
.filter(
|
|
(provider): provider is BaseProvider & Required<Pick<ProviderInfo, 'getDynamicModels'>> =>
|
|
!!provider.getDynamicModels,
|
|
)
|
|
.map(async (provider) => {
|
|
const cachedModels = provider.getModelsFromCache(options);
|
|
|
|
if (cachedModels) {
|
|
return cachedModels;
|
|
}
|
|
|
|
const dynamicModels = await provider
|
|
.getDynamicModels(apiKeys, providerSettings?.[provider.name], serverEnv)
|
|
.then((models) => {
|
|
logger.info(`Caching ${models.length} dynamic models for ${provider.name}`);
|
|
provider.storeDynamicModels(options, models);
|
|
|
|
return models;
|
|
})
|
|
.catch((err) => {
|
|
logger.error(`Error getting dynamic models ${provider.name} :`, err);
|
|
return [];
|
|
});
|
|
|
|
return dynamicModels;
|
|
}),
|
|
);
|
|
const staticModels = Array.from(this._providers.values()).flatMap((p) => p.staticModels || []);
|
|
const dynamicModelsFlat = dynamicModels.flat();
|
|
const dynamicModelKeys = dynamicModelsFlat.map((d) => `${d.name}-${d.provider}`);
|
|
const filteredStaticModels = staticModels.filter((m) => !dynamicModelKeys.includes(`${m.name}-${m.provider}`));
|
|
|
|
// Combine static and dynamic models
|
|
const modelList = [...dynamicModelsFlat, ...filteredStaticModels];
|
|
modelList.sort((a, b) => a.name.localeCompare(b.name));
|
|
this._modelList = modelList;
|
|
|
|
return modelList;
|
|
}
|
|
getStaticModelList() {
|
|
return [...this._providers.values()].flatMap((p) => p.staticModels || []);
|
|
}
|
|
async getModelListFromProvider(
|
|
providerArg: BaseProvider,
|
|
options: {
|
|
apiKeys?: Record<string, string>;
|
|
providerSettings?: Record<string, IProviderSetting>;
|
|
serverEnv?: Record<string, string>;
|
|
},
|
|
): Promise<ModelInfo[]> {
|
|
const provider = this._providers.get(providerArg.name);
|
|
|
|
if (!provider) {
|
|
throw new Error(`Provider ${providerArg.name} not found`);
|
|
}
|
|
|
|
const staticModels = provider.staticModels || [];
|
|
|
|
if (!provider.getDynamicModels) {
|
|
return staticModels;
|
|
}
|
|
|
|
const { apiKeys, providerSettings, serverEnv } = options;
|
|
|
|
const cachedModels = provider.getModelsFromCache({
|
|
apiKeys,
|
|
providerSettings,
|
|
serverEnv,
|
|
});
|
|
|
|
if (cachedModels) {
|
|
logger.info(`Found ${cachedModels.length} cached models for ${provider.name}`);
|
|
return [...cachedModels, ...staticModels];
|
|
}
|
|
|
|
logger.info(`Getting dynamic models for ${provider.name}`);
|
|
|
|
const dynamicModels = await provider
|
|
.getDynamicModels?.(apiKeys, providerSettings?.[provider.name], serverEnv)
|
|
.then((models) => {
|
|
logger.info(`Got ${models.length} dynamic models for ${provider.name}`);
|
|
provider.storeDynamicModels(options, models);
|
|
|
|
return models;
|
|
})
|
|
.catch((err) => {
|
|
logger.error(`Error getting dynamic models ${provider.name} :`, err);
|
|
return [];
|
|
});
|
|
const dynamicModelsName = dynamicModels.map((d) => d.name);
|
|
const filteredStaticList = staticModels.filter((m) => !dynamicModelsName.includes(m.name));
|
|
const modelList = [...dynamicModels, ...filteredStaticList];
|
|
modelList.sort((a, b) => a.name.localeCompare(b.name));
|
|
|
|
return modelList;
|
|
}
|
|
getStaticModelListFromProvider(providerArg: BaseProvider) {
|
|
const provider = this._providers.get(providerArg.name);
|
|
|
|
if (!provider) {
|
|
throw new Error(`Provider ${providerArg.name} not found`);
|
|
}
|
|
|
|
return [...(provider.staticModels || [])];
|
|
}
|
|
|
|
getDefaultProvider(): BaseProvider {
|
|
const firstProvider = this._providers.values().next().value;
|
|
|
|
if (!firstProvider) {
|
|
throw new Error('No providers registered');
|
|
}
|
|
|
|
return firstProvider;
|
|
}
|
|
}
|