mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-16 19:04:37 +00:00
Custom provider WIP
This commit is contained in:
parent
389c80d7a8
commit
1f9a3a00cc
17 changed files with 1185 additions and 107 deletions
268
packages/web-ui/src/dialogs/CustomProviderDialog.ts
Normal file
268
packages/web-ui/src/dialogs/CustomProviderDialog.ts
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
import { Button, DialogBase, html, Input, i18n, Label, Select, type TemplateResult } from "@mariozechner/mini-lit";
|
||||
import type { Model } from "@mariozechner/pi-ai";
|
||||
import { state } from "lit/decorators.js";
|
||||
import { getAppStorage } from "../storage/app-storage.js";
|
||||
import type { CustomProvider, CustomProviderType } from "../storage/stores/custom-providers-store.js";
|
||||
import { discoverModels } from "../utils/model-discovery.js";
|
||||
|
||||
export class CustomProviderDialog extends DialogBase {
|
||||
private provider?: CustomProvider;
|
||||
private initialType?: CustomProviderType;
|
||||
private onSaveCallback?: () => void;
|
||||
|
||||
@state() private name = "";
|
||||
@state() private type: CustomProviderType = "openai-completions";
|
||||
@state() private baseUrl = "";
|
||||
@state() private apiKey = "";
|
||||
@state() private testing = false;
|
||||
@state() private testError = "";
|
||||
@state() private discoveredModels: Model<any>[] = [];
|
||||
|
||||
protected modalWidth = "min(800px, 90vw)";
|
||||
protected modalHeight = "min(700px, 90vh)";
|
||||
|
||||
static async open(
|
||||
provider: CustomProvider | undefined,
|
||||
initialType: CustomProviderType | undefined,
|
||||
onSave?: () => void,
|
||||
) {
|
||||
const dialog = new CustomProviderDialog();
|
||||
dialog.provider = provider;
|
||||
dialog.initialType = initialType;
|
||||
dialog.onSaveCallback = onSave;
|
||||
document.body.appendChild(dialog);
|
||||
dialog.initializeFromProvider();
|
||||
dialog.open();
|
||||
dialog.requestUpdate();
|
||||
}
|
||||
|
||||
private initializeFromProvider() {
|
||||
if (this.provider) {
|
||||
this.name = this.provider.name;
|
||||
this.type = this.provider.type;
|
||||
this.baseUrl = this.provider.baseUrl;
|
||||
this.apiKey = this.provider.apiKey || "";
|
||||
this.discoveredModels = this.provider.models || [];
|
||||
} else {
|
||||
this.name = "";
|
||||
this.type = this.initialType || "openai-completions";
|
||||
this.baseUrl = "";
|
||||
this.updateDefaultBaseUrl();
|
||||
this.apiKey = "";
|
||||
this.discoveredModels = [];
|
||||
}
|
||||
this.testError = "";
|
||||
this.testing = false;
|
||||
}
|
||||
|
||||
private updateDefaultBaseUrl() {
|
||||
if (this.baseUrl) return;
|
||||
|
||||
const defaults: Record<string, string> = {
|
||||
ollama: "http://localhost:11434",
|
||||
"llama.cpp": "http://localhost:8080",
|
||||
vllm: "http://localhost:8000",
|
||||
lmstudio: "http://localhost:1234",
|
||||
"openai-completions": "",
|
||||
"openai-responses": "",
|
||||
"anthropic-messages": "",
|
||||
};
|
||||
|
||||
this.baseUrl = defaults[this.type] || "";
|
||||
}
|
||||
|
||||
private isAutoDiscoveryType(): boolean {
|
||||
return this.type === "ollama" || this.type === "llama.cpp" || this.type === "vllm" || this.type === "lmstudio";
|
||||
}
|
||||
|
||||
private async testConnection() {
|
||||
if (!this.isAutoDiscoveryType()) return;
|
||||
|
||||
this.testing = true;
|
||||
this.testError = "";
|
||||
this.discoveredModels = [];
|
||||
|
||||
try {
|
||||
const models = await discoverModels(
|
||||
this.type as "ollama" | "llama.cpp" | "vllm" | "lmstudio",
|
||||
this.baseUrl,
|
||||
this.apiKey || undefined,
|
||||
);
|
||||
|
||||
this.discoveredModels = models.map((model) => ({
|
||||
...model,
|
||||
provider: this.name || this.type,
|
||||
}));
|
||||
|
||||
this.testError = "";
|
||||
} catch (error) {
|
||||
this.testError = error instanceof Error ? error.message : String(error);
|
||||
this.discoveredModels = [];
|
||||
} finally {
|
||||
this.testing = false;
|
||||
this.requestUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
private async save() {
|
||||
if (!this.name || !this.baseUrl) {
|
||||
alert(i18n("Please fill in all required fields"));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const storage = getAppStorage();
|
||||
|
||||
const provider: CustomProvider = {
|
||||
id: this.provider?.id || crypto.randomUUID(),
|
||||
name: this.name,
|
||||
type: this.type,
|
||||
baseUrl: this.baseUrl,
|
||||
apiKey: this.apiKey || undefined,
|
||||
models: this.isAutoDiscoveryType() ? undefined : this.provider?.models || [],
|
||||
};
|
||||
|
||||
await storage.customProviders.set(provider);
|
||||
|
||||
if (this.onSaveCallback) {
|
||||
this.onSaveCallback();
|
||||
}
|
||||
this.close();
|
||||
} catch (error) {
|
||||
console.error("Failed to save provider:", error);
|
||||
alert(i18n("Failed to save provider"));
|
||||
}
|
||||
}
|
||||
|
||||
protected override renderContent(): TemplateResult {
|
||||
const providerTypes = [
|
||||
{ value: "ollama", label: "Ollama (auto-discovery)" },
|
||||
{ value: "llama.cpp", label: "llama.cpp (auto-discovery)" },
|
||||
{ value: "vllm", label: "vLLM (auto-discovery)" },
|
||||
{ value: "lmstudio", label: "LM Studio (auto-discovery)" },
|
||||
{ value: "openai-completions", label: "OpenAI Completions Compatible" },
|
||||
{ value: "openai-responses", label: "OpenAI Responses Compatible" },
|
||||
{ value: "anthropic-messages", label: "Anthropic Messages Compatible" },
|
||||
];
|
||||
|
||||
return html`
|
||||
<div class="flex flex-col h-full overflow-hidden">
|
||||
<div class="p-6 flex-shrink-0 border-b border-border">
|
||||
<h2 class="text-lg font-semibold text-foreground">
|
||||
${this.provider ? i18n("Edit Provider") : i18n("Add Provider")}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto p-6">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
${Label({ htmlFor: "provider-name", children: i18n("Provider Name") })}
|
||||
${Input({
|
||||
value: this.name,
|
||||
placeholder: i18n("e.g., My Ollama Server"),
|
||||
onInput: (e: Event) => {
|
||||
this.name = (e.target as HTMLInputElement).value;
|
||||
this.requestUpdate();
|
||||
},
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
${Label({ htmlFor: "provider-type", children: i18n("Provider Type") })}
|
||||
${Select({
|
||||
value: this.type,
|
||||
options: providerTypes.map((pt) => ({
|
||||
value: pt.value,
|
||||
label: pt.label,
|
||||
})),
|
||||
onChange: (value: string) => {
|
||||
this.type = value as CustomProviderType;
|
||||
this.baseUrl = "";
|
||||
this.updateDefaultBaseUrl();
|
||||
this.requestUpdate();
|
||||
},
|
||||
width: "100%",
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
${Label({ htmlFor: "base-url", children: i18n("Base URL") })}
|
||||
${Input({
|
||||
value: this.baseUrl,
|
||||
placeholder: i18n("e.g., http://localhost:11434"),
|
||||
onInput: (e: Event) => {
|
||||
this.baseUrl = (e.target as HTMLInputElement).value;
|
||||
this.requestUpdate();
|
||||
},
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
${Label({ htmlFor: "api-key", children: i18n("API Key (Optional)") })}
|
||||
${Input({
|
||||
type: "password",
|
||||
value: this.apiKey,
|
||||
placeholder: i18n("Leave empty if not required"),
|
||||
onInput: (e: Event) => {
|
||||
this.apiKey = (e.target as HTMLInputElement).value;
|
||||
this.requestUpdate();
|
||||
},
|
||||
})}
|
||||
</div>
|
||||
|
||||
${
|
||||
this.isAutoDiscoveryType()
|
||||
? html`
|
||||
<div class="flex flex-col gap-2">
|
||||
${Button({
|
||||
onClick: () => this.testConnection(),
|
||||
variant: "outline",
|
||||
disabled: this.testing || !this.baseUrl,
|
||||
children: this.testing ? i18n("Testing...") : i18n("Test Connection"),
|
||||
})}
|
||||
${this.testError ? html` <div class="text-sm text-destructive">${this.testError}</div> ` : ""}
|
||||
${
|
||||
this.discoveredModels.length > 0
|
||||
? html`
|
||||
<div class="text-sm text-muted-foreground">
|
||||
${i18n("Discovered")} ${this.discoveredModels.length} ${i18n("models")}:
|
||||
<ul class="list-disc list-inside mt-2">
|
||||
${this.discoveredModels.slice(0, 5).map((model) => html`<li>${model.name}</li>`)}
|
||||
${
|
||||
this.discoveredModels.length > 5
|
||||
? html`<li>...${i18n("and")} ${this.discoveredModels.length - 5} ${i18n("more")}</li>`
|
||||
: ""
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
`
|
||||
: html` <div class="text-sm text-muted-foreground">
|
||||
${i18n("For manual provider types, add models after saving the provider.")}
|
||||
</div>`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-6 flex-shrink-0 border-t border-border flex justify-end gap-2">
|
||||
${Button({
|
||||
onClick: () => this.close(),
|
||||
variant: "ghost",
|
||||
children: i18n("Cancel"),
|
||||
})}
|
||||
${Button({
|
||||
onClick: () => this.save(),
|
||||
variant: "default",
|
||||
disabled: !this.name || !this.baseUrl,
|
||||
children: i18n("Save"),
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("custom-provider-dialog", CustomProviderDialog);
|
||||
|
|
@ -1,14 +1,15 @@
|
|||
import { Badge, Button, DialogBase, DialogHeader, html, icon, type TemplateResult } from "@mariozechner/mini-lit";
|
||||
import type { Model } from "@mariozechner/pi-ai";
|
||||
import { MODELS } from "@mariozechner/pi-ai/dist/models.generated.js";
|
||||
import { getModels, getProviders, type Model } from "@mariozechner/pi-ai";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { createRef, ref } from "lit/directives/ref.js";
|
||||
import { Brain, Image as ImageIcon } from "lucide";
|
||||
import { Ollama } from "ollama/dist/browser.mjs";
|
||||
import { Input } from "../components/Input.js";
|
||||
import { getAppStorage } from "../storage/app-storage.js";
|
||||
import type { AutoDiscoveryProviderType } from "../storage/stores/custom-providers-store.js";
|
||||
import { formatModelCost } from "../utils/format.js";
|
||||
import { i18n } from "../utils/i18n.js";
|
||||
import { discoverModels } from "../utils/model-discovery.js";
|
||||
|
||||
@customElement("agent-model-selector")
|
||||
export class ModelSelector extends DialogBase {
|
||||
|
|
@ -16,10 +17,10 @@ export class ModelSelector extends DialogBase {
|
|||
@state() searchQuery = "";
|
||||
@state() filterThinking = false;
|
||||
@state() filterVision = false;
|
||||
@state() ollamaModels: Model<any>[] = [];
|
||||
@state() ollamaError: string | null = null;
|
||||
@state() customProvidersLoading = false;
|
||||
@state() selectedIndex = 0;
|
||||
@state() private navigationMode: "mouse" | "keyboard" = "mouse";
|
||||
@state() private customProviderModels: Model<any>[] = [];
|
||||
|
||||
private onSelectCallback?: (model: Model<any>) => void;
|
||||
private scrollContainerRef = createRef<HTMLDivElement>();
|
||||
|
|
@ -33,7 +34,7 @@ export class ModelSelector extends DialogBase {
|
|||
selector.currentModel = currentModel;
|
||||
selector.onSelectCallback = onSelect;
|
||||
selector.open();
|
||||
selector.fetchOllamaModels();
|
||||
selector.loadCustomProviders();
|
||||
}
|
||||
|
||||
override async firstUpdated(changedProperties: PropertyValues): Promise<void> {
|
||||
|
|
@ -91,67 +92,50 @@ export class ModelSelector extends DialogBase {
|
|||
});
|
||||
}
|
||||
|
||||
private async fetchOllamaModels() {
|
||||
private async loadCustomProviders() {
|
||||
this.customProvidersLoading = true;
|
||||
const allCustomModels: Model<any>[] = [];
|
||||
|
||||
try {
|
||||
// Create Ollama client
|
||||
const ollama = new Ollama({ host: "http://localhost:11434" });
|
||||
const storage = getAppStorage();
|
||||
const customProviders = await storage.customProviders.getAll();
|
||||
|
||||
// Get list of available models
|
||||
const { models } = await ollama.list();
|
||||
// Load models from custom providers
|
||||
for (const provider of customProviders) {
|
||||
const isAutoDiscovery: boolean =
|
||||
provider.type === "ollama" ||
|
||||
provider.type === "llama.cpp" ||
|
||||
provider.type === "vllm" ||
|
||||
provider.type === "lmstudio";
|
||||
|
||||
// Fetch details for each model and convert to Model format
|
||||
const ollamaModelPromises: Promise<Model<any> | null>[] = models
|
||||
.map(async (model: any) => {
|
||||
if (isAutoDiscovery) {
|
||||
try {
|
||||
// Get model details
|
||||
const details = await ollama.show({
|
||||
model: model.name,
|
||||
});
|
||||
const models = await discoverModels(
|
||||
provider.type as AutoDiscoveryProviderType,
|
||||
provider.baseUrl,
|
||||
provider.apiKey,
|
||||
);
|
||||
|
||||
// Some Ollama servers don't report capabilities; don't filter on them
|
||||
const modelsWithProvider = models.map((model) => ({
|
||||
...model,
|
||||
provider: provider.name,
|
||||
}));
|
||||
|
||||
// Extract model info
|
||||
const modelInfo: any = details.model_info || {};
|
||||
|
||||
// Get context window size - look for architecture-specific keys
|
||||
const architecture = modelInfo["general.architecture"] || "";
|
||||
const contextKey = `${architecture}.context_length`;
|
||||
const contextWindow = parseInt(modelInfo[contextKey] || "8192", 10);
|
||||
const maxTokens = 4096; // Default max output tokens
|
||||
|
||||
// Create Model object manually since ollama models aren't in MODELS constant
|
||||
const ollamaModel: Model<any> = {
|
||||
id: model.name,
|
||||
name: model.name,
|
||||
api: "openai-completions" as any,
|
||||
provider: "ollama",
|
||||
baseUrl: "http://localhost:11434/v1",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: contextWindow,
|
||||
maxTokens: maxTokens,
|
||||
};
|
||||
|
||||
return ollamaModel;
|
||||
} catch (err) {
|
||||
console.error(`Failed to fetch details for model ${model.name}:`, err);
|
||||
return null;
|
||||
allCustomModels.push(...modelsWithProvider);
|
||||
} catch (error) {
|
||||
console.debug(`Failed to load models from ${provider.name}:`, error);
|
||||
}
|
||||
})
|
||||
.filter((m: any) => m !== null);
|
||||
|
||||
const results = await Promise.all(ollamaModelPromises);
|
||||
this.ollamaModels = results.filter((m): m is Model<any> => m !== null);
|
||||
} catch (err) {
|
||||
// Ollama not available or other error - silently ignore
|
||||
console.debug("Ollama not available:", err);
|
||||
this.ollamaError = err instanceof Error ? err.message : String(err);
|
||||
} else if (provider.models) {
|
||||
// Manual provider - models already defined
|
||||
allCustomModels.push(...provider.models);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load custom providers:", error);
|
||||
} finally {
|
||||
this.customProviderModels = allCustomModels;
|
||||
this.customProvidersLoading = false;
|
||||
this.requestUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -169,21 +153,20 @@ export class ModelSelector extends DialogBase {
|
|||
}
|
||||
|
||||
private getFilteredModels(): Array<{ provider: string; id: string; model: any }> {
|
||||
// Collect all models from all providers
|
||||
// Collect all models from known providers
|
||||
const allModels: Array<{ provider: string; id: string; model: any }> = [];
|
||||
for (const [provider, providerData] of Object.entries(MODELS)) {
|
||||
for (const [modelId, model] of Object.entries(providerData)) {
|
||||
allModels.push({ provider, id: modelId, model });
|
||||
const knownProviders = getProviders();
|
||||
|
||||
for (const provider of knownProviders) {
|
||||
const models = getModels(provider as any);
|
||||
for (const model of models) {
|
||||
allModels.push({ provider, id: model.id, model });
|
||||
}
|
||||
}
|
||||
|
||||
// Add Ollama models
|
||||
for (const ollamaModel of this.ollamaModels) {
|
||||
allModels.push({
|
||||
id: ollamaModel.id,
|
||||
provider: "ollama",
|
||||
model: ollamaModel,
|
||||
});
|
||||
// Add custom provider models
|
||||
for (const model of this.customProviderModels) {
|
||||
allModels.push({ provider: model.provider, id: model.id, model });
|
||||
}
|
||||
|
||||
// Filter models based on search and capability filters
|
||||
|
|
@ -283,8 +266,7 @@ export class ModelSelector extends DialogBase {
|
|||
<!-- Scrollable model list -->
|
||||
<div class="flex-1 overflow-y-auto" ${ref(this.scrollContainerRef)}>
|
||||
${filteredModels.map(({ provider, id, model }, index) => {
|
||||
// Check if this is the current model by comparing IDs
|
||||
const isCurrent = this.currentModel?.id === model.id;
|
||||
const isCurrent = this.currentModel?.id === model.id && this.currentModel?.provider === model.provider;
|
||||
const isSelected = index === this.selectedIndex;
|
||||
return html`
|
||||
<div
|
||||
|
|
|
|||
211
packages/web-ui/src/dialogs/ProvidersModelsTab.ts
Normal file
211
packages/web-ui/src/dialogs/ProvidersModelsTab.ts
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
import { Button, html, i18n, Select, type TemplateResult } from "@mariozechner/mini-lit";
|
||||
import { getProviders } from "@mariozechner/pi-ai";
|
||||
import { LitElement } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import "../components/CustomProviderCard.js";
|
||||
import "../components/ProviderKeyInput.js";
|
||||
import { getAppStorage } from "../storage/app-storage.js";
|
||||
import type {
|
||||
AutoDiscoveryProviderType,
|
||||
CustomProvider,
|
||||
CustomProviderType,
|
||||
} from "../storage/stores/custom-providers-store.js";
|
||||
import { discoverModels } from "../utils/model-discovery.js";
|
||||
import { CustomProviderDialog } from "./CustomProviderDialog.js";
|
||||
import { SettingsTab } from "./SettingsDialog.js";
|
||||
|
||||
@customElement("providers-models-tab")
|
||||
export class ProvidersModelsTab extends SettingsTab {
|
||||
@state() private customProviders: CustomProvider[] = [];
|
||||
@state() private providerStatus: Map<
|
||||
string,
|
||||
{ modelCount: number; status: "connected" | "disconnected" | "checking" }
|
||||
> = new Map();
|
||||
|
||||
override async connectedCallback() {
|
||||
super.connectedCallback();
|
||||
await this.loadCustomProviders();
|
||||
}
|
||||
|
||||
private async loadCustomProviders() {
|
||||
try {
|
||||
const storage = getAppStorage();
|
||||
this.customProviders = await storage.customProviders.getAll();
|
||||
|
||||
// Check status for auto-discovery providers
|
||||
for (const provider of this.customProviders) {
|
||||
const isAutoDiscovery =
|
||||
provider.type === "ollama" ||
|
||||
provider.type === "llama.cpp" ||
|
||||
provider.type === "vllm" ||
|
||||
provider.type === "lmstudio";
|
||||
if (isAutoDiscovery) {
|
||||
this.checkProviderStatus(provider);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load custom providers:", error);
|
||||
}
|
||||
}
|
||||
|
||||
getTabName(): string {
|
||||
return "Providers & Models";
|
||||
}
|
||||
|
||||
private async checkProviderStatus(provider: CustomProvider) {
|
||||
this.providerStatus.set(provider.id, { modelCount: 0, status: "checking" });
|
||||
this.requestUpdate();
|
||||
|
||||
try {
|
||||
const models = await discoverModels(
|
||||
provider.type as AutoDiscoveryProviderType,
|
||||
provider.baseUrl,
|
||||
provider.apiKey,
|
||||
);
|
||||
|
||||
this.providerStatus.set(provider.id, { modelCount: models.length, status: "connected" });
|
||||
} catch (error) {
|
||||
this.providerStatus.set(provider.id, { modelCount: 0, status: "disconnected" });
|
||||
}
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private renderKnownProviders(): TemplateResult {
|
||||
const providers = getProviders();
|
||||
|
||||
return html`
|
||||
<div class="flex flex-col gap-6">
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-foreground mb-2">Cloud Providers</h3>
|
||||
<p class="text-sm text-muted-foreground mb-4">
|
||||
Cloud LLM providers with predefined models. API keys are stored locally in your browser.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-col gap-6">
|
||||
${providers.map((provider) => html` <provider-key-input .provider=${provider}></provider-key-input> `)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderCustomProviders(): TemplateResult {
|
||||
const isAutoDiscovery = (type: string) =>
|
||||
type === "ollama" || type === "llama.cpp" || type === "vllm" || type === "lmstudio";
|
||||
|
||||
return html`
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-foreground mb-2">Custom Providers</h3>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
User-configured servers with auto-discovered or manually defined models.
|
||||
</p>
|
||||
</div>
|
||||
${Select({
|
||||
placeholder: i18n("Add Provider"),
|
||||
options: [
|
||||
{ value: "ollama", label: "Ollama" },
|
||||
{ value: "llama.cpp", label: "llama.cpp" },
|
||||
{ value: "vllm", label: "vLLM" },
|
||||
{ value: "lmstudio", label: "LM Studio" },
|
||||
{ value: "openai-completions", label: i18n("OpenAI Completions Compatible") },
|
||||
{ value: "openai-responses", label: i18n("OpenAI Responses Compatible") },
|
||||
{ value: "anthropic-messages", label: i18n("Anthropic Messages Compatible") },
|
||||
],
|
||||
onChange: (value: string) => this.addCustomProvider(value as CustomProviderType),
|
||||
variant: "outline",
|
||||
size: "sm",
|
||||
})}
|
||||
</div>
|
||||
|
||||
${
|
||||
this.customProviders.length === 0
|
||||
? html`
|
||||
<div class="text-sm text-muted-foreground text-center py-8">
|
||||
No custom providers configured. Click 'Add Provider' to get started.
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div class="flex flex-col gap-4">
|
||||
${this.customProviders.map(
|
||||
(provider) => html`
|
||||
<custom-provider-card
|
||||
.provider=${provider}
|
||||
.isAutoDiscovery=${isAutoDiscovery(provider.type)}
|
||||
.status=${this.providerStatus.get(provider.id)}
|
||||
.onRefresh=${(p: CustomProvider) => this.refreshProvider(p)}
|
||||
.onEdit=${(p: CustomProvider) => this.editProvider(p)}
|
||||
.onDelete=${(p: CustomProvider) => this.deleteProvider(p)}
|
||||
></custom-provider-card>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private async addCustomProvider(type: CustomProviderType) {
|
||||
await CustomProviderDialog.open(undefined, type, async () => {
|
||||
await this.loadCustomProviders();
|
||||
this.requestUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
private async editProvider(provider: CustomProvider) {
|
||||
await CustomProviderDialog.open(provider, undefined, async () => {
|
||||
await this.loadCustomProviders();
|
||||
this.requestUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
private async refreshProvider(provider: CustomProvider) {
|
||||
this.providerStatus.set(provider.id, { modelCount: 0, status: "checking" });
|
||||
this.requestUpdate();
|
||||
|
||||
try {
|
||||
const models = await discoverModels(
|
||||
provider.type as AutoDiscoveryProviderType,
|
||||
provider.baseUrl,
|
||||
provider.apiKey,
|
||||
);
|
||||
|
||||
this.providerStatus.set(provider.id, { modelCount: models.length, status: "connected" });
|
||||
this.requestUpdate();
|
||||
|
||||
console.log(`Refreshed ${models.length} models from ${provider.name}`);
|
||||
} catch (error) {
|
||||
this.providerStatus.set(provider.id, { modelCount: 0, status: "disconnected" });
|
||||
this.requestUpdate();
|
||||
|
||||
console.error(`Failed to refresh provider ${provider.name}:`, error);
|
||||
alert(`Failed to refresh provider: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async deleteProvider(provider: CustomProvider) {
|
||||
if (!confirm("Are you sure you want to delete this provider?")) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const storage = getAppStorage();
|
||||
await storage.customProviders.delete(provider.id);
|
||||
await this.loadCustomProviders();
|
||||
this.requestUpdate();
|
||||
} catch (error) {
|
||||
console.error("Failed to delete provider:", error);
|
||||
}
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
return html`
|
||||
<div class="flex flex-col gap-8">
|
||||
${this.renderKnownProviders()}
|
||||
<div class="border-t border-border"></div>
|
||||
${this.renderCustomProviders()}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue