Files
slate/app/lib/modules/llm/providers/openai-like.ts
Stijnus b7ef2247b8 feat: add Cerebras and Fireworks AI LLM providers (#2113)
* 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>
2026-02-07 15:26:46 +01:00

176 lines
4.9 KiB
TypeScript

import { BaseProvider, getOpenAILikeModel } from '~/lib/modules/llm/base-provider';
import type { ModelInfo } from '~/lib/modules/llm/types';
import type { IProviderSetting } from '~/types/model';
import type { LanguageModelV1 } from 'ai';
import { logger } from '~/utils/logger';
interface OpenAIModelsResponse {
data: Array<{ id: string }>;
}
export default class OpenAILikeProvider extends BaseProvider {
name = 'OpenAILike';
getApiKeyLink = undefined;
config = {
baseUrlKey: 'OPENAI_LIKE_API_BASE_URL',
apiTokenKey: 'OPENAI_LIKE_API_KEY',
modelsKey: 'OPENAI_LIKE_API_MODELS',
};
staticModels: ModelInfo[] = [];
async getDynamicModels(
apiKeys?: Record<string, string>,
settings?: IProviderSetting,
serverEnv: Record<string, string> = {},
): Promise<ModelInfo[]> {
const { baseUrl, apiKey } = this.getProviderBaseUrlAndKey({
apiKeys,
providerSettings: settings,
serverEnv,
defaultBaseUrlKey: 'OPENAI_LIKE_API_BASE_URL',
defaultApiTokenKey: 'OPENAI_LIKE_API_KEY',
});
if (!baseUrl || !apiKey) {
return [];
}
try {
const response = await fetch(`${baseUrl}/models`, {
headers: {
Authorization: `Bearer ${apiKey}`,
},
signal: this.createTimeoutSignal(),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const res = (await response.json()) as OpenAIModelsResponse;
return res.data.map((model) => ({
name: model.id,
label: model.id,
provider: this.name,
maxTokenAllowed: 8000,
}));
} catch (error) {
logger.info(`${this.name}: Could not fetch /models endpoint, checking fallback env`, error);
// Fallback to OPENAI_LIKE_API_MODELS if available
// eslint-disable-next-line dot-notation
const modelsEnv = serverEnv['OPENAI_LIKE_API_MODELS'] || settings?.OPENAI_LIKE_API_MODELS;
if (modelsEnv) {
logger.info(`${this.name}: Using OPENAI_LIKE_API_MODELS fallback`);
return this._parseModelsFromEnv(modelsEnv);
}
return [];
}
}
/**
* Parse OPENAI_LIKE_API_MODELS environment variable
* Format: path/to/model1:limit;path/to/model2:limit;path/to/model3:limit
*/
private _parseModelsFromEnv(modelsEnv: string): ModelInfo[] {
if (!modelsEnv) {
return [];
}
try {
const models: ModelInfo[] = [];
const modelEntries = modelsEnv.split(';');
for (const entry of modelEntries) {
const trimmedEntry = entry.trim();
if (!trimmedEntry) {
continue;
}
const [modelPath, limitStr] = trimmedEntry.split(':');
if (!modelPath) {
continue;
}
const limit = limitStr ? parseInt(limitStr.trim(), 10) : 8000;
const modelName = modelPath.trim();
// Generate a readable label from the model path
const label = this._generateModelLabel(modelName);
models.push({
name: modelName,
label,
provider: this.name,
maxTokenAllowed: limit,
});
}
logger.info(`${this.name}: Parsed ${models.length} models from env`);
return models;
} catch (error) {
logger.error(`${this.name}: Error parsing OPENAI_LIKE_API_MODELS:`, error);
return [];
}
}
/**
* Generate a readable label from model path
*/
private _generateModelLabel(modelPath: string): string {
// Extract the last part of the path and clean it up
const parts = modelPath.split('/');
const lastPart = parts[parts.length - 1];
// Remove common prefixes and clean up the name
let label = lastPart
.replace(/^accounts\//, '')
.replace(/^fireworks\/models\//, '')
.replace(/^models\//, '')
// Capitalize first letter of each word
.replace(/\b\w/g, (l) => l.toUpperCase())
// Replace spaces with hyphens for a cleaner look
.replace(/\s+/g, '-');
// Add provider suffix if not already present
if (!label.includes('Fireworks') && !label.includes('OpenAI')) {
label += ' (OpenAI Compatible)';
}
return label;
}
getModelInstance(options: {
model: string;
serverEnv: Env;
apiKeys?: Record<string, string>;
providerSettings?: Record<string, IProviderSetting>;
}): LanguageModelV1 {
const { model, serverEnv, apiKeys, providerSettings } = options;
const envRecord = this.convertEnvToRecord(serverEnv);
const { baseUrl, apiKey } = this.getProviderBaseUrlAndKey({
apiKeys,
providerSettings: providerSettings?.[this.name],
serverEnv: envRecord,
defaultBaseUrlKey: 'OPENAI_LIKE_API_BASE_URL',
defaultApiTokenKey: 'OPENAI_LIKE_API_KEY',
});
if (!baseUrl || !apiKey) {
throw new Error(`Missing configuration for ${this.name} provider`);
}
return getOpenAILikeModel(baseUrl, apiKey, model);
}
}