Added complete source code and pre-compiled application: Source Code: - app.asar (compiled Electron app) - app-extracted/ (all extracted source files) - dist/services/aiProviderService.js - dist/services/settingsService.js - dist/ipcHandlers.js - dist/aiProviderAPI.ts - dist/ai-provider-settings.html - And all other application files Build Tools: - scripts/extract-app.sh - scripts/repack-app.sh - scripts/build-deb.sh - scripts/install-deb.sh Documentation: - SOURCE_CODE.md (repository structure) - BUILD.md (build instructions) - README.md (overview) - docs/ (complete API docs) - AI_PROVIDER_SPECIFICATION.md - AI_PROVIDER_README.md - IMPLEMENTATION_SUMMARY.md Features included: - 17+ AI provider presets - One-click provider setup - Model auto-fetching - Connection testing - Modern GUI interface - Complete IPC handlers (14 new) - TypeScript API wrapper - Persistent settings Ready to build and customize!
520 lines
19 KiB
JavaScript
520 lines
19 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.AIProviderService = exports.AIProviderType = exports.AIProviderCapability = void 0;
|
|
var AIProviderCapability;
|
|
(function (AIProviderCapability) {
|
|
AIProviderCapability["CHAT"] = "chat";
|
|
AIProviderCapability["COMPLETION"] = "completion";
|
|
AIProviderCapability["EMBEDDING"] = "embedding";
|
|
AIProviderCapability["VISION"] = "vision";
|
|
AIProviderCapability["TOOL_USE"] = "tool_use";
|
|
AIProviderCapability["STREAMING"] = "streaming";
|
|
})(AIProviderCapability = exports.AIProviderCapability || (exports.AIProviderCapability = {}));
|
|
var AIProviderType;
|
|
(function (AIProviderType) {
|
|
AIProviderType["OPENAI"] = "openai";
|
|
AIProviderType["ANTHROPIC"] = "anthropic";
|
|
AIProviderType["OLLAMA"] = "ollama";
|
|
AIProviderType["GROQ"] = "groq";
|
|
AIProviderType["OPENROUTER"] = "openrouter";
|
|
AIProviderType["CUSTOM"] = "custom";
|
|
AIProviderType["GOOGLE_GEMINI"] = "google_gemini";
|
|
AIProviderType["COMMAND_CODE"] = "command_code";
|
|
AIProviderType["NVIDIA_NIM"] = "nvidia_nim";
|
|
AIProviderType["CROF_AI"] = "crof_ai";
|
|
AIProviderType["KILO_AI"] = "kilo_ai";
|
|
AIProviderType["OPEN_ADAPTER"] = "open_adapter";
|
|
AIProviderType["Z_AI"] = "z_ai";
|
|
AIProviderType["GOOGLE_ANTIGRAVITY"] = "google_antigravity";
|
|
})(AIProviderType = exports.AIProviderType || (exports.AIProviderType = {}));
|
|
|
|
const PROVIDER_PRESETS = {
|
|
"Custom": {
|
|
type: AIProviderType.CUSTOM,
|
|
endpoint: "",
|
|
capabilities: [AIProviderCapability.CHAT, AIProviderCapability.STREAMING],
|
|
},
|
|
"OpenAI": {
|
|
type: AIProviderType.OPENAI,
|
|
endpoint: "https://api.openai.com/v1",
|
|
models: ["gpt-4o", "gpt-4o-mini", "gpt-4-turbo", "gpt-3.5-turbo"],
|
|
capabilities: [AIProviderCapability.CHAT, AIProviderCapability.COMPLETION, AIProviderCapability.EMBEDDING, AIProviderCapability.VISION, AIProviderCapability.TOOL_USE, AIProviderCapability.STREAMING],
|
|
},
|
|
"Anthropic": {
|
|
type: AIProviderType.ANTHROPIC,
|
|
endpoint: "https://api.anthropic.com/v1",
|
|
models: ["claude-sonnet-4-20250514", "claude-3-5-sonnet-latest", "claude-3-opus-latest", "claude-3-haiku-latest"],
|
|
capabilities: [AIProviderCapability.CHAT, AIProviderCapability.VISION, AIProviderCapability.TOOL_USE, AIProviderCapability.STREAMING],
|
|
},
|
|
"OpenCode Zen (OpenAI-compatible)": {
|
|
type: AIProviderType.CUSTOM,
|
|
endpoint: "https://opencode.ai/zen/v1",
|
|
models: [
|
|
"glm-5.1", "glm-5", "kimi-k2.5", "kimi-k2.6",
|
|
"minimax-m2.7", "minimax-m2.5", "minimax-m2.5-free",
|
|
"deepseek-v4-flash-free", "nemotron-3-super-free",
|
|
"qwen3.6-plus", "qwen3.5-plus", "big-pickle",
|
|
],
|
|
capabilities: [AIProviderCapability.CHAT, AIProviderCapability.COMPLETION, AIProviderCapability.STREAMING],
|
|
},
|
|
"OpenCode Zen (Anthropic)": {
|
|
type: AIProviderType.ANTHROPIC,
|
|
endpoint: "https://opencode.ai/zen/v1",
|
|
models: [
|
|
"claude-opus-4-7", "claude-opus-4-6", "claude-opus-4-5",
|
|
"claude-opus-4-1", "claude-sonnet-4-6", "claude-sonnet-4-5",
|
|
"claude-sonnet-4", "claude-haiku-4-5", "claude-3-5-haiku",
|
|
],
|
|
capabilities: [AIProviderCapability.CHAT, AIProviderCapability.VISION, AIProviderCapability.TOOL_USE, AIProviderCapability.STREAMING],
|
|
},
|
|
"OpenCode Go (OpenAI-compatible)": {
|
|
type: AIProviderType.CUSTOM,
|
|
endpoint: "https://opencode.ai/zen/go/v1",
|
|
models: [
|
|
"glm-5.1", "glm-5", "kimi-k2.5", "kimi-k2.6",
|
|
"mimo-v2.5", "mimo-v2.5-pro", "minimax-m2.7", "minimax-m2.5",
|
|
"qwen3.6-plus", "qwen3.5-plus", "deepseek-v4-pro", "deepseek-v4-flash",
|
|
],
|
|
capabilities: [AIProviderCapability.CHAT, AIProviderCapability.COMPLETION, AIProviderCapability.STREAMING],
|
|
},
|
|
"OpenCode Go (Anthropic)": {
|
|
type: AIProviderType.ANTHROPIC,
|
|
endpoint: "https://opencode.ai/zen/go/v1",
|
|
models: ["minimax-m2.7", "minimax-m2.5"],
|
|
capabilities: [AIProviderCapability.CHAT, AIProviderCapability.VISION, AIProviderCapability.TOOL_USE, AIProviderCapability.STREAMING],
|
|
},
|
|
"Crof.ai": {
|
|
type: AIProviderType.CUSTOM,
|
|
endpoint: "https://crof.ai/v1",
|
|
models: [],
|
|
capabilities: [AIProviderCapability.CHAT, AIProviderCapability.COMPLETION, AIProviderCapability.STREAMING],
|
|
},
|
|
"NVIDIA NIM": {
|
|
type: AIProviderType.NVIDIA_NIM,
|
|
endpoint: "https://integrate.api.nvidia.com/v1",
|
|
models: [],
|
|
capabilities: [AIProviderCapability.CHAT, AIProviderCapability.COMPLETION, AIProviderCapability.STREAMING],
|
|
},
|
|
"Kilo.ai Gateway": {
|
|
type: AIProviderType.KILO_AI,
|
|
endpoint: "https://api.kilo.ai/api/gateway",
|
|
models: [],
|
|
capabilities: [AIProviderCapability.CHAT, AIProviderCapability.COMPLETION, AIProviderCapability.STREAMING],
|
|
},
|
|
"Command Code": {
|
|
type: AIProviderType.COMMAND_CODE,
|
|
endpoint: "https://api.commandcode.ai",
|
|
models: [
|
|
"deepseek/deepseek-v4-flash", "deepseek/deepseek-v4-pro",
|
|
"anthropic:claude-sonnet-4-6", "anthropic:claude-haiku-4-5-20251001",
|
|
"anthropic:claude-opus-4-7", "anthropic:claude-opus-4-6",
|
|
"openai:gpt-5.5", "openai:gpt-5.4", "openai:gpt-5.4-mini", "openai:gpt-5.3-codex",
|
|
"moonshotai/Kimi-K2.6", "moonshotai/Kimi-K2.5",
|
|
"zai-org/GLM-5.1", "zai-org/GLM-5",
|
|
"MiniMaxAI/MiniMax-M2.7", "MiniMaxAI/MiniMax-M2.5",
|
|
"Qwen/Qwen3.6-Max-Preview", "Qwen/Qwen3.6-Plus",
|
|
"stepfun/Step-3.5-Flash", "google/gemini-3.1-flash-lite",
|
|
],
|
|
capabilities: [AIProviderCapability.CHAT, AIProviderCapability.TOOL_USE, AIProviderCapability.STREAMING],
|
|
},
|
|
"OpenRouter": {
|
|
type: AIProviderType.OPENROUTER,
|
|
endpoint: "https://openrouter.ai/api/v1",
|
|
models: [],
|
|
capabilities: [AIProviderCapability.CHAT, AIProviderCapability.COMPLETION, AIProviderCapability.STREAMING],
|
|
},
|
|
"Google Gemini (API Key)": {
|
|
type: AIProviderType.GOOGLE_GEMINI,
|
|
endpoint: "https://generativelanguage.googleapis.com/v1beta/openai",
|
|
models: [
|
|
"gemini-2.5-flash", "gemini-2.5-pro",
|
|
"gemini-2.0-flash", "gemini-2.0-flash-lite",
|
|
"gemini-2.5-flash-preview-native-audio-dialog",
|
|
],
|
|
capabilities: [AIProviderCapability.CHAT, AIProviderCapability.VISION, AIProviderCapability.STREAMING],
|
|
},
|
|
"Google Gemini (OAuth)": {
|
|
type: AIProviderType.GOOGLE_GEMINI,
|
|
endpoint: "https://cloudcode-pa.googleapis.com",
|
|
models: ["gemini-2.5-flash", "gemini-2.5-pro"],
|
|
capabilities: [AIProviderCapability.CHAT, AIProviderCapability.VISION, AIProviderCapability.TOOL_USE, AIProviderCapability.STREAMING],
|
|
},
|
|
"Google Antigravity (OAuth)": {
|
|
type: AIProviderType.GOOGLE_ANTIGRAVITY,
|
|
endpoint: "https://daily-cloudcode-pa.sandbox.googleapis.com",
|
|
models: [
|
|
"antigravity-gemini-3-flash",
|
|
"antigravity-gemini-3-pro",
|
|
"antigravity-gemini-3.1-pro",
|
|
"antigravity-claude-sonnet-4-6",
|
|
"antigravity-claude-opus-4-6-thinking",
|
|
"gemini-2.5-flash", "gemini-2.5-pro",
|
|
"gemini-3-flash-preview", "gemini-3-pro-preview", "gemini-3.1-pro-preview",
|
|
],
|
|
capabilities: [AIProviderCapability.CHAT, AIProviderCapability.VISION, AIProviderCapability.TOOL_USE, AIProviderCapability.STREAMING],
|
|
},
|
|
"OpenAdapter": {
|
|
type: AIProviderType.OPEN_ADAPTER,
|
|
endpoint: "https://api.openadapter.in/v1",
|
|
models: [
|
|
"0G-DeepSeek-V3",
|
|
"0G-DeepSeek-v4-Pro",
|
|
"0G-GLM-5",
|
|
"0G-GLM-5.1",
|
|
"0G-Qwen3.6",
|
|
"0G-Qwen-VL",
|
|
],
|
|
capabilities: [AIProviderCapability.CHAT, AIProviderCapability.COMPLETION, AIProviderCapability.STREAMING],
|
|
},
|
|
"Z.ai Coding": {
|
|
type: AIProviderType.Z_AI,
|
|
endpoint: "https://api.z.ai/api/coding/paas/v4",
|
|
models: [
|
|
"glm-5.1", "glm-4.7", "GLM-4-Plus", "GLM-4-Long",
|
|
"GLM-4-Flash", "GLM-4-FlashX", "GLM-Z1-Flash",
|
|
],
|
|
capabilities: [AIProviderCapability.CHAT, AIProviderCapability.COMPLETION, AIProviderCapability.STREAMING],
|
|
},
|
|
"Ollama (Local)": {
|
|
type: AIProviderType.OLLAMA,
|
|
endpoint: "http://localhost:11434/v1",
|
|
apiKey: "ollama",
|
|
models: ["llama3", "codellama", "mistral", "neural-chat"],
|
|
capabilities: [AIProviderCapability.CHAT, AIProviderCapability.COMPLETION, AIProviderCapability.STREAMING],
|
|
},
|
|
"Groq": {
|
|
type: AIProviderType.GROQ,
|
|
endpoint: "https://api.groq.com/openai/v1",
|
|
models: ["llama-3.1-8b-instant", "llama-3.1-70b-versatile", "mixtral-8x7b-32768"],
|
|
capabilities: [AIProviderCapability.CHAT, AIProviderCapability.COMPLETION, AIProviderCapability.STREAMING],
|
|
},
|
|
};
|
|
|
|
const DEFAULT_PROVIDERS = [
|
|
{
|
|
id: 'openai-default',
|
|
name: 'OpenAI',
|
|
type: AIProviderType.OPENAI,
|
|
endpoint: 'https://api.openai.com/v1',
|
|
apiKey: '',
|
|
models: ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'gpt-3.5-turbo'],
|
|
defaultModel: 'gpt-4o',
|
|
capabilities: [
|
|
AIProviderCapability.CHAT,
|
|
AIProviderCapability.COMPLETION,
|
|
AIProviderCapability.EMBEDDING,
|
|
AIProviderCapability.VISION,
|
|
AIProviderCapability.TOOL_USE,
|
|
AIProviderCapability.STREAMING,
|
|
],
|
|
isEnabled: true,
|
|
isDefault: true,
|
|
},
|
|
{
|
|
id: 'anthropic-default',
|
|
name: 'Anthropic',
|
|
type: AIProviderType.ANTHROPIC,
|
|
endpoint: 'https://api.anthropic.com/v1',
|
|
apiKey: '',
|
|
models: ['claude-sonnet-4-20250514', 'claude-3-5-sonnet-latest', 'claude-3-opus-latest', 'claude-3-haiku-latest'],
|
|
defaultModel: 'claude-sonnet-4-20250514',
|
|
capabilities: [
|
|
AIProviderCapability.CHAT,
|
|
AIProviderCapability.VISION,
|
|
AIProviderCapability.TOOL_USE,
|
|
AIProviderCapability.STREAMING,
|
|
],
|
|
isEnabled: true,
|
|
isDefault: false,
|
|
},
|
|
{
|
|
id: 'ollama-default',
|
|
name: 'Ollama (Local)',
|
|
type: AIProviderType.OLLAMA,
|
|
endpoint: 'http://localhost:11434/v1',
|
|
apiKey: 'ollama',
|
|
models: ['llama3', 'codellama', 'mistral', 'neural-chat'],
|
|
defaultModel: 'llama3',
|
|
capabilities: [
|
|
AIProviderCapability.CHAT,
|
|
AIProviderCapability.COMPLETION,
|
|
AIProviderCapability.STREAMING,
|
|
],
|
|
isEnabled: true,
|
|
isDefault: false,
|
|
},
|
|
];
|
|
|
|
class AIProviderService {
|
|
constructor(storageManager) {
|
|
this.storageManager = storageManager;
|
|
this.providers = new Map();
|
|
this.eventEmitter = new (require('events').EventEmitter)();
|
|
}
|
|
|
|
async initialize() {
|
|
const items = await this.storageManager.getItems();
|
|
const storedProviders = items['aiProviders'];
|
|
|
|
if (storedProviders) {
|
|
try {
|
|
const providersArray = JSON.parse(storedProviders);
|
|
providersArray.forEach((provider) => {
|
|
this.providers.set(provider.id, provider);
|
|
});
|
|
} catch (e) {
|
|
console.error('Error loading AI providers:', e);
|
|
this.loadDefaultProviders();
|
|
}
|
|
} else {
|
|
this.loadDefaultProviders();
|
|
}
|
|
}
|
|
|
|
loadDefaultProviders() {
|
|
DEFAULT_PROVIDERS.forEach((provider) => {
|
|
this.providers.set(provider.id, { ...provider });
|
|
});
|
|
this.persistProviders();
|
|
}
|
|
|
|
async persistProviders() {
|
|
const providersArray = Array.from(this.providers.values());
|
|
await this.storageManager.updateItems({
|
|
'aiProviders': JSON.stringify(providersArray),
|
|
});
|
|
}
|
|
|
|
getAllProviders() {
|
|
return Array.from(this.providers.values());
|
|
}
|
|
|
|
getProvider(id) {
|
|
return this.providers.get(id);
|
|
}
|
|
|
|
getEnabledProviders() {
|
|
return this.getAllProviders().filter(p => p.isEnabled);
|
|
}
|
|
|
|
getDefaultProvider() {
|
|
return this.getAllProviders().find(p => p.isDefault) || this.getAllProviders()[0];
|
|
}
|
|
|
|
getAvailablePresets() {
|
|
return Object.keys(PROVIDER_PRESETS);
|
|
}
|
|
|
|
getPreset(presetName) {
|
|
return PROVIDER_PRESETS[presetName];
|
|
}
|
|
|
|
async addProvider(provider) {
|
|
const newProvider = {
|
|
id: `custom-${Date.now()}`,
|
|
name: provider.name || 'Custom Provider',
|
|
type: provider.type || AIProviderType.CUSTOM,
|
|
endpoint: provider.endpoint || '',
|
|
apiKey: provider.apiKey || '',
|
|
models: provider.models || [],
|
|
defaultModel: provider.defaultModel || (provider.models && provider.models[0]) || '',
|
|
capabilities: provider.capabilities || [AIProviderCapability.CHAT],
|
|
isEnabled: true,
|
|
isDefault: false,
|
|
...provider,
|
|
};
|
|
|
|
this.providers.set(newProvider.id, newProvider);
|
|
await this.persistProviders();
|
|
this.emit('provider-added', newProvider);
|
|
return newProvider;
|
|
}
|
|
|
|
async addProviderFromPreset(presetName, apiKey = '') {
|
|
const preset = PROVIDER_PRESETS[presetName];
|
|
if (!preset) {
|
|
throw new Error(`Preset "${presetName}" not found`);
|
|
}
|
|
|
|
const normalizedName = presetName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
const newProvider = {
|
|
id: `${normalizedName}-${Date.now()}`,
|
|
name: presetName,
|
|
type: preset.type || AIProviderType.CUSTOM,
|
|
endpoint: preset.endpoint || '',
|
|
apiKey: apiKey || preset.apiKey || '',
|
|
models: preset.models || [],
|
|
defaultModel: preset.models && preset.models[0] || '',
|
|
capabilities: preset.capabilities || [AIProviderCapability.CHAT, AIProviderCapability.STREAMING],
|
|
isEnabled: true,
|
|
isDefault: false,
|
|
presetName: presetName,
|
|
};
|
|
|
|
this.providers.set(newProvider.id, newProvider);
|
|
await this.persistProviders();
|
|
this.emit('provider-added', newProvider);
|
|
return newProvider;
|
|
}
|
|
|
|
async updateProvider(id, updates) {
|
|
const provider = this.providers.get(id);
|
|
if (!provider) {
|
|
throw new Error(`Provider with id ${id} not found`);
|
|
}
|
|
|
|
const updatedProvider = {
|
|
...provider,
|
|
...updates,
|
|
id: provider.id,
|
|
};
|
|
|
|
this.providers.set(id, updatedProvider);
|
|
await this.persistProviders();
|
|
this.emit('provider-updated', updatedProvider);
|
|
return updatedProvider;
|
|
}
|
|
|
|
async deleteProvider(id) {
|
|
const provider = this.providers.get(id);
|
|
if (!provider) {
|
|
throw new Error(`Provider with id ${id} not found`);
|
|
}
|
|
|
|
this.providers.delete(id);
|
|
await this.persistProviders();
|
|
this.emit('provider-deleted', id);
|
|
}
|
|
|
|
async setDefaultProvider(id) {
|
|
const provider = this.providers.get(id);
|
|
if (!provider) {
|
|
throw new Error(`Provider with id ${id} not found`);
|
|
}
|
|
|
|
this.getAllProviders().forEach((p) => {
|
|
p.isDefault = p.id === id;
|
|
});
|
|
|
|
await this.persistProviders();
|
|
this.emit('default-provider-changed', id);
|
|
}
|
|
|
|
async toggleProvider(id, enabled) {
|
|
const provider = this.providers.get(id);
|
|
if (!provider) {
|
|
throw new Error(`Provider with id ${id} not found`);
|
|
}
|
|
|
|
provider.isEnabled = enabled;
|
|
await this.persistProviders();
|
|
this.emit('provider-toggled', { id, enabled });
|
|
}
|
|
|
|
async testConnection(id) {
|
|
const provider = this.providers.get(id);
|
|
if (!provider) {
|
|
throw new Error(`Provider with id ${id} not found`);
|
|
}
|
|
|
|
if (!provider.endpoint) {
|
|
return {
|
|
success: false,
|
|
status: -1,
|
|
message: 'No endpoint configured',
|
|
};
|
|
}
|
|
|
|
try {
|
|
const headers = {
|
|
'Content-Type': 'application/json',
|
|
};
|
|
|
|
if (provider.apiKey && provider.apiKey !== 'ollama') {
|
|
headers['Authorization'] = `Bearer ${provider.apiKey}`;
|
|
}
|
|
|
|
const response = await fetch(`${provider.endpoint}/models`, {
|
|
method: 'GET',
|
|
headers: headers,
|
|
});
|
|
|
|
return {
|
|
success: response.ok,
|
|
status: response.status,
|
|
message: response.ok ? 'Connection successful' : `Error: ${response.statusText}`,
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
status: -1,
|
|
message: `Connection failed: ${error.message}`,
|
|
};
|
|
}
|
|
}
|
|
|
|
async fetchModels(id) {
|
|
const provider = this.providers.get(id);
|
|
if (!provider) {
|
|
throw new Error(`Provider with id ${id} not found`);
|
|
}
|
|
|
|
if (!provider.endpoint) {
|
|
throw new Error('No endpoint configured');
|
|
}
|
|
|
|
try {
|
|
const headers = {
|
|
'Content-Type': 'application/json',
|
|
};
|
|
|
|
if (provider.apiKey && provider.apiKey !== 'ollama') {
|
|
headers['Authorization'] = `Bearer ${provider.apiKey}`;
|
|
}
|
|
|
|
const response = await fetch(`${provider.endpoint}/models`, {
|
|
method: 'GET',
|
|
headers: headers,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`API error: ${response.statusText}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
let models = [];
|
|
if (data.data && Array.isArray(data.data)) {
|
|
models = data.data.map(model => model.id || model.model || model.name).filter(Boolean);
|
|
} else if (data.models && Array.isArray(data.models)) {
|
|
models = data.models.map(model => model.id || model.model || model.name).filter(Boolean);
|
|
}
|
|
|
|
if (models.length > 0) {
|
|
provider.models = models;
|
|
provider.defaultModel = models[0];
|
|
await this.persistProviders();
|
|
this.emit('provider-updated', provider);
|
|
}
|
|
|
|
return models;
|
|
} catch (error) {
|
|
throw new Error(`Failed to fetch models: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
on(event, listener) {
|
|
this.eventEmitter.on(event, listener);
|
|
}
|
|
|
|
off(event, listener) {
|
|
this.eventEmitter.off(event, listener);
|
|
}
|
|
|
|
emit(event, ...args) {
|
|
this.eventEmitter.emit(event, ...args);
|
|
}
|
|
}
|
|
|
|
exports.AIProviderService = AIProviderService;
|