mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-06-26 18:26:38 +00:00
Add Portkey provider with custom headers and models - Add Portkey provider for AI gateway functionality - Support for custom headers via UI (default: x-portkey-debug: false) - Support for custom models configuration via UI - Integrate HeaderManager and ModelManager components - Update CloudProvidersTab to support Portkey configuration - Add environment variables for PORTKEY_API_KEY and PORTKEY_API_BASE_URL - Fix linting issues and import paths
This commit is contained in:
parent
5d3fb1d7de
commit
99d1506011
236
app/components/@settings/tabs/providers/HeaderManager.tsx
Normal file
236
app/components/@settings/tabs/providers/HeaderManager.tsx
Normal file
@ -0,0 +1,236 @@
|
||||
import React, { useState } from 'react';
|
||||
import { IconButton } from '~/components/ui/IconButton';
|
||||
import type { IProviderSetting } from '~/types/model';
|
||||
import { classNames } from '~/utils/classNames';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
|
||||
interface HeaderManagerProps {
|
||||
provider: string;
|
||||
settings: IProviderSetting;
|
||||
onUpdateSettings: (settings: IProviderSetting) => void;
|
||||
}
|
||||
|
||||
interface HeaderFormData {
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export const HeaderManager: React.FC<HeaderManagerProps> = ({ provider, settings, onUpdateSettings }) => {
|
||||
const [isAddingHeader, setIsAddingHeader] = useState(false);
|
||||
const [editingHeaderKey, setEditingHeaderKey] = useState<string | null>(null);
|
||||
const [headerForm, setHeaderForm] = useState<HeaderFormData>({
|
||||
name: '',
|
||||
value: '',
|
||||
});
|
||||
|
||||
const customHeaders = settings.customHeaders || {};
|
||||
const headerEntries = Object.entries(customHeaders);
|
||||
|
||||
const handleAddHeader = () => {
|
||||
if (!headerForm.name.trim() || !headerForm.value.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedHeaders = {
|
||||
...customHeaders,
|
||||
[headerForm.name.trim()]: headerForm.value.trim(),
|
||||
};
|
||||
|
||||
const updatedSettings = {
|
||||
...settings,
|
||||
customHeaders: updatedHeaders,
|
||||
};
|
||||
|
||||
onUpdateSettings(updatedSettings);
|
||||
setHeaderForm({ name: '', value: '' });
|
||||
setIsAddingHeader(false);
|
||||
};
|
||||
|
||||
const handleEditHeader = (key: string) => {
|
||||
setHeaderForm({
|
||||
name: key,
|
||||
value: customHeaders[key],
|
||||
});
|
||||
setEditingHeaderKey(key);
|
||||
};
|
||||
|
||||
const handleUpdateHeader = () => {
|
||||
if (editingHeaderKey === null || !headerForm.name.trim() || !headerForm.value.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedHeaders = { ...customHeaders };
|
||||
|
||||
// Remove old key if name changed
|
||||
if (editingHeaderKey !== headerForm.name.trim()) {
|
||||
delete updatedHeaders[editingHeaderKey];
|
||||
}
|
||||
|
||||
// Add/update with new values
|
||||
updatedHeaders[headerForm.name.trim()] = headerForm.value.trim();
|
||||
|
||||
const updatedSettings = {
|
||||
...settings,
|
||||
customHeaders: updatedHeaders,
|
||||
};
|
||||
|
||||
onUpdateSettings(updatedSettings);
|
||||
setHeaderForm({ name: '', value: '' });
|
||||
setEditingHeaderKey(null);
|
||||
};
|
||||
|
||||
const handleDeleteHeader = (key: string) => {
|
||||
const updatedHeaders = { ...customHeaders };
|
||||
delete updatedHeaders[key];
|
||||
|
||||
const updatedSettings = {
|
||||
...settings,
|
||||
customHeaders: updatedHeaders,
|
||||
};
|
||||
onUpdateSettings(updatedSettings);
|
||||
};
|
||||
|
||||
const handleCancelEdit = () => {
|
||||
setHeaderForm({ name: '', value: '' });
|
||||
setIsAddingHeader(false);
|
||||
setEditingHeaderKey(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-sm font-medium text-bolt-elements-textPrimary">Custom Headers</h4>
|
||||
<IconButton
|
||||
onClick={() => setIsAddingHeader(true)}
|
||||
title="Add Header"
|
||||
className="bg-green-500/10 hover:bg-green-500/20 text-green-500"
|
||||
disabled={isAddingHeader || editingHeaderKey !== null}
|
||||
>
|
||||
<div className="i-ph:plus w-4 h-4" />
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
{/* Header Form */}
|
||||
<AnimatePresence>
|
||||
{(isAddingHeader || editingHeaderKey !== null) && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: 'auto' }}
|
||||
exit={{ opacity: 0, height: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="border border-bolt-elements-borderColor rounded-lg p-4 bg-bolt-elements-background-depth-3"
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-bolt-elements-textSecondary mb-1">Header Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={headerForm.name}
|
||||
onChange={(e) => setHeaderForm({ ...headerForm, name: e.target.value })}
|
||||
placeholder="e.g., x-api-version"
|
||||
className={classNames(
|
||||
'w-full px-3 py-2 rounded-lg text-sm',
|
||||
'bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor',
|
||||
'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
|
||||
'focus:outline-none focus:ring-2 focus:ring-purple-500/30',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-bolt-elements-textSecondary mb-1">
|
||||
Header Value
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={headerForm.value}
|
||||
onChange={(e) => setHeaderForm({ ...headerForm, value: e.target.value })}
|
||||
placeholder="e.g., v1.0"
|
||||
className={classNames(
|
||||
'w-full px-3 py-2 rounded-lg text-sm',
|
||||
'bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor',
|
||||
'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
|
||||
'focus:outline-none focus:ring-2 focus:ring-purple-500/30',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<IconButton
|
||||
onClick={handleCancelEdit}
|
||||
title="Cancel"
|
||||
className="bg-red-500/10 hover:bg-red-500/20 text-red-500"
|
||||
>
|
||||
<div className="i-ph:x w-4 h-4" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={editingHeaderKey !== null ? handleUpdateHeader : handleAddHeader}
|
||||
title={editingHeaderKey !== null ? 'Update Header' : 'Add Header'}
|
||||
className="bg-green-500/10 hover:bg-green-500/20 text-green-500"
|
||||
disabled={!headerForm.name.trim() || !headerForm.value.trim()}
|
||||
>
|
||||
<div className="i-ph:check w-4 h-4" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Headers List */}
|
||||
<div className="space-y-2">
|
||||
<AnimatePresence>
|
||||
{headerEntries.map(([key, value]) => (
|
||||
<motion.div
|
||||
key={key}
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className={classNames(
|
||||
'flex items-center justify-between p-3 rounded-lg',
|
||||
'bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor',
|
||||
'hover:bg-bolt-elements-background-depth-3 transition-colors',
|
||||
)}
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-bolt-elements-textPrimary truncate">{key}</span>
|
||||
<span className="text-xs px-2 py-0.5 rounded bg-blue-500/10 text-blue-500">{value}</span>
|
||||
</div>
|
||||
<p className="text-xs text-bolt-elements-textSecondary mt-0.5">Custom header for {provider}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<IconButton
|
||||
onClick={() => handleEditHeader(key)}
|
||||
title="Edit Header"
|
||||
className="bg-blue-500/10 hover:bg-blue-500/20 text-blue-500"
|
||||
disabled={isAddingHeader || editingHeaderKey !== null}
|
||||
>
|
||||
<div className="i-ph:pencil-simple w-4 h-4" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={() => handleDeleteHeader(key)}
|
||||
title="Delete Header"
|
||||
className="bg-red-500/10 hover:bg-red-500/20 text-red-500"
|
||||
disabled={isAddingHeader || editingHeaderKey !== null}
|
||||
>
|
||||
<div className="i-ph:trash w-4 h-4" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
|
||||
{headerEntries.length === 0 && !isAddingHeader && (
|
||||
<div className="text-center py-8 text-bolt-elements-textSecondary">
|
||||
<div className="i-ph:list-dashes w-8 h-8 mx-auto mb-2 opacity-50" />
|
||||
<p className="text-sm">No custom headers configured</p>
|
||||
<p className="text-xs mt-1">Click the + button to add a header</p>
|
||||
{provider === 'Portkey' && <p className="text-xs mt-2 text-purple-500">Default: x-portkey-debug: false</p>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
255
app/components/@settings/tabs/providers/ModelManager.tsx
Normal file
255
app/components/@settings/tabs/providers/ModelManager.tsx
Normal file
@ -0,0 +1,255 @@
|
||||
import React, { useState } from 'react';
|
||||
import { IconButton } from '~/components/ui/IconButton';
|
||||
import type { ModelInfo } from '~/lib/modules/llm/types';
|
||||
import type { IProviderSetting } from '~/types/model';
|
||||
import { classNames } from '~/utils/classNames';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
|
||||
interface ModelManagerProps {
|
||||
provider: string;
|
||||
settings: IProviderSetting;
|
||||
onUpdateSettings: (settings: IProviderSetting) => void;
|
||||
}
|
||||
|
||||
interface ModelFormData {
|
||||
name: string;
|
||||
label: string;
|
||||
maxTokenAllowed: number;
|
||||
}
|
||||
|
||||
export const ModelManager: React.FC<ModelManagerProps> = ({ provider, settings, onUpdateSettings }) => {
|
||||
const [isAddingModel, setIsAddingModel] = useState(false);
|
||||
const [editingModelIndex, setEditingModelIndex] = useState<number | null>(null);
|
||||
const [modelForm, setModelForm] = useState<ModelFormData>({
|
||||
name: '',
|
||||
label: '',
|
||||
maxTokenAllowed: 8000,
|
||||
});
|
||||
|
||||
const customModels = settings.customModels || [];
|
||||
|
||||
const handleAddModel = () => {
|
||||
if (!modelForm.name.trim() || !modelForm.label.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newModel: ModelInfo = {
|
||||
name: modelForm.name.trim(),
|
||||
label: modelForm.label.trim(),
|
||||
provider,
|
||||
maxTokenAllowed: modelForm.maxTokenAllowed,
|
||||
};
|
||||
|
||||
const updatedSettings = {
|
||||
...settings,
|
||||
customModels: [...customModels, newModel],
|
||||
};
|
||||
|
||||
onUpdateSettings(updatedSettings);
|
||||
setModelForm({ name: '', label: '', maxTokenAllowed: 8000 });
|
||||
setIsAddingModel(false);
|
||||
};
|
||||
|
||||
const handleEditModel = (index: number) => {
|
||||
const model = customModels[index];
|
||||
setModelForm({
|
||||
name: model.name,
|
||||
label: model.label,
|
||||
maxTokenAllowed: model.maxTokenAllowed,
|
||||
});
|
||||
setEditingModelIndex(index);
|
||||
};
|
||||
|
||||
const handleUpdateModel = () => {
|
||||
if (editingModelIndex === null || !modelForm.name.trim() || !modelForm.label.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedModels = [...customModels];
|
||||
updatedModels[editingModelIndex] = {
|
||||
name: modelForm.name.trim(),
|
||||
label: modelForm.label.trim(),
|
||||
provider,
|
||||
maxTokenAllowed: modelForm.maxTokenAllowed,
|
||||
};
|
||||
|
||||
const updatedSettings = {
|
||||
...settings,
|
||||
customModels: updatedModels,
|
||||
};
|
||||
|
||||
onUpdateSettings(updatedSettings);
|
||||
setModelForm({ name: '', label: '', maxTokenAllowed: 8000 });
|
||||
setEditingModelIndex(null);
|
||||
};
|
||||
|
||||
const handleDeleteModel = (index: number) => {
|
||||
const updatedModels = customModels.filter((_, i) => i !== index);
|
||||
const updatedSettings = {
|
||||
...settings,
|
||||
customModels: updatedModels,
|
||||
};
|
||||
onUpdateSettings(updatedSettings);
|
||||
};
|
||||
|
||||
const handleCancelEdit = () => {
|
||||
setModelForm({ name: '', label: '', maxTokenAllowed: 8000 });
|
||||
setIsAddingModel(false);
|
||||
setEditingModelIndex(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-sm font-medium text-bolt-elements-textPrimary">Custom Models</h4>
|
||||
<IconButton
|
||||
onClick={() => setIsAddingModel(true)}
|
||||
title="Add Model"
|
||||
className="bg-green-500/10 hover:bg-green-500/20 text-green-500"
|
||||
disabled={isAddingModel || editingModelIndex !== null}
|
||||
>
|
||||
<div className="i-ph:plus w-4 h-4" />
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
{/* Model Form */}
|
||||
<AnimatePresence>
|
||||
{(isAddingModel || editingModelIndex !== null) && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: 'auto' }}
|
||||
exit={{ opacity: 0, height: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="border border-bolt-elements-borderColor rounded-lg p-4 bg-bolt-elements-background-depth-3"
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-bolt-elements-textSecondary mb-1">Model Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={modelForm.name}
|
||||
onChange={(e) => setModelForm({ ...modelForm, name: e.target.value })}
|
||||
placeholder="e.g., gpt-4o"
|
||||
className={classNames(
|
||||
'w-full px-3 py-2 rounded-lg text-sm',
|
||||
'bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor',
|
||||
'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
|
||||
'focus:outline-none focus:ring-2 focus:ring-purple-500/30',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-bolt-elements-textSecondary mb-1">
|
||||
Display Label
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={modelForm.label}
|
||||
onChange={(e) => setModelForm({ ...modelForm, label: e.target.value })}
|
||||
placeholder="e.g., GPT-4o (via Portkey)"
|
||||
className={classNames(
|
||||
'w-full px-3 py-2 rounded-lg text-sm',
|
||||
'bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor',
|
||||
'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
|
||||
'focus:outline-none focus:ring-2 focus:ring-purple-500/30',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-bolt-elements-textSecondary mb-1">Max Tokens</label>
|
||||
<input
|
||||
type="number"
|
||||
value={modelForm.maxTokenAllowed}
|
||||
onChange={(e) => setModelForm({ ...modelForm, maxTokenAllowed: parseInt(e.target.value) || 8000 })}
|
||||
min="1000"
|
||||
max="2000000"
|
||||
className={classNames(
|
||||
'w-full px-3 py-2 rounded-lg text-sm',
|
||||
'bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor',
|
||||
'text-bolt-elements-textPrimary',
|
||||
'focus:outline-none focus:ring-2 focus:ring-purple-500/30',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<IconButton
|
||||
onClick={handleCancelEdit}
|
||||
title="Cancel"
|
||||
className="bg-red-500/10 hover:bg-red-500/20 text-red-500"
|
||||
>
|
||||
<div className="i-ph:x w-4 h-4" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={editingModelIndex !== null ? handleUpdateModel : handleAddModel}
|
||||
title={editingModelIndex !== null ? 'Update Model' : 'Add Model'}
|
||||
className="bg-green-500/10 hover:bg-green-500/20 text-green-500"
|
||||
disabled={!modelForm.name.trim() || !modelForm.label.trim()}
|
||||
>
|
||||
<div className="i-ph:check w-4 h-4" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Model List */}
|
||||
<div className="space-y-2">
|
||||
<AnimatePresence>
|
||||
{customModels.map((model, index) => (
|
||||
<motion.div
|
||||
key={`${model.name}-${index}`}
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className={classNames(
|
||||
'flex items-center justify-between p-3 rounded-lg',
|
||||
'bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor',
|
||||
'hover:bg-bolt-elements-background-depth-3 transition-colors',
|
||||
)}
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-bolt-elements-textPrimary truncate">{model.label}</span>
|
||||
<span className="text-xs px-2 py-0.5 rounded bg-purple-500/10 text-purple-500">{model.name}</span>
|
||||
</div>
|
||||
<p className="text-xs text-bolt-elements-textSecondary mt-0.5">
|
||||
Max tokens: {model.maxTokenAllowed.toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<IconButton
|
||||
onClick={() => handleEditModel(index)}
|
||||
title="Edit Model"
|
||||
className="bg-blue-500/10 hover:bg-blue-500/20 text-blue-500"
|
||||
disabled={isAddingModel || editingModelIndex !== null}
|
||||
>
|
||||
<div className="i-ph:pencil-simple w-4 h-4" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={() => handleDeleteModel(index)}
|
||||
title="Delete Model"
|
||||
className="bg-red-500/10 hover:bg-red-500/20 text-red-500"
|
||||
disabled={isAddingModel || editingModelIndex !== null}
|
||||
>
|
||||
<div className="i-ph:trash w-4 h-4" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
|
||||
{customModels.length === 0 && !isAddingModel && (
|
||||
<div className="text-center py-8 text-bolt-elements-textSecondary">
|
||||
<div className="i-ph:list-dashes w-8 h-8 mx-auto mb-2 opacity-50" />
|
||||
<p className="text-sm">No custom models configured</p>
|
||||
<p className="text-xs mt-1">Click the + button to add a model</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -2,7 +2,7 @@ import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { Switch } from '~/components/ui/Switch';
|
||||
import { useSettings } from '~/lib/hooks/useSettings';
|
||||
import { URL_CONFIGURABLE_PROVIDERS } from '~/lib/stores/settings';
|
||||
import type { IProviderConfig } from '~/types/model';
|
||||
import type { IProviderConfig, ProviderInfo } from '~/types/model';
|
||||
import { logStore } from '~/lib/stores/logs';
|
||||
import { motion } from 'framer-motion';
|
||||
import { classNames } from '~/utils/classNames';
|
||||
@ -14,6 +14,9 @@ import { TbBrain, TbCloudComputing } from 'react-icons/tb';
|
||||
import { BiCodeBlock, BiChip } from 'react-icons/bi';
|
||||
import { FaCloud, FaBrain } from 'react-icons/fa';
|
||||
import type { IconType } from 'react-icons';
|
||||
import { APIKeyManager, getApiKeysFromCookies } from '~/components/chat/APIKeyManager';
|
||||
import { ModelManager } from '~/components/@settings/tabs/providers/ModelManager';
|
||||
import { HeaderManager } from '~/components/@settings/tabs/providers/HeaderManager';
|
||||
|
||||
// Add type for provider names to ensure type safety
|
||||
type ProviderName =
|
||||
@ -30,7 +33,8 @@ type ProviderName =
|
||||
| 'OpenRouter'
|
||||
| 'Perplexity'
|
||||
| 'Together'
|
||||
| 'XAI';
|
||||
| 'XAI'
|
||||
| 'Portkey';
|
||||
|
||||
// Update the PROVIDER_ICONS type to use the ProviderName type
|
||||
const PROVIDER_ICONS: Record<ProviderName, IconType> = {
|
||||
@ -48,12 +52,14 @@ const PROVIDER_ICONS: Record<ProviderName, IconType> = {
|
||||
Perplexity: SiPerplexity,
|
||||
Together: BsCloud,
|
||||
XAI: BsRobot,
|
||||
Portkey: BsCloud,
|
||||
};
|
||||
|
||||
// Update PROVIDER_DESCRIPTIONS to use the same type
|
||||
const PROVIDER_DESCRIPTIONS: Partial<Record<ProviderName, string>> = {
|
||||
Anthropic: 'Access Claude and other Anthropic models',
|
||||
OpenAI: 'Use GPT-4, GPT-3.5, and other OpenAI models',
|
||||
Portkey: 'AI gateway with custom model configuration',
|
||||
};
|
||||
|
||||
const CloudProvidersTab = () => {
|
||||
@ -61,6 +67,13 @@ const CloudProvidersTab = () => {
|
||||
const [editingProvider, setEditingProvider] = useState<string | null>(null);
|
||||
const [filteredProviders, setFilteredProviders] = useState<IProviderConfig[]>([]);
|
||||
const [categoryEnabled, setCategoryEnabled] = useState<boolean>(false);
|
||||
const [apiKeys, setApiKeys] = useState<Record<string, string>>({});
|
||||
|
||||
// Load API keys from cookies on mount
|
||||
useEffect(() => {
|
||||
const savedApiKeys = getApiKeysFromCookies();
|
||||
setApiKeys(savedApiKeys);
|
||||
}, []);
|
||||
|
||||
// Load and filter providers
|
||||
useEffect(() => {
|
||||
@ -283,6 +296,38 @@ const CloudProvidersTab = () => {
|
||||
)}
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* Special Portkey configuration */}
|
||||
{provider.settings.enabled && provider.name === 'Portkey' && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: 'auto' }}
|
||||
exit={{ opacity: 0, height: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="mt-4 space-y-4 border-t border-bolt-elements-borderColor pt-4"
|
||||
>
|
||||
{/* API Key Manager */}
|
||||
<APIKeyManager
|
||||
provider={provider as ProviderInfo}
|
||||
apiKey={apiKeys[provider.name] || ''}
|
||||
setApiKey={(key) => setApiKeys((prev) => ({ ...prev, [provider.name]: key }))}
|
||||
/>
|
||||
|
||||
{/* Model Manager */}
|
||||
<ModelManager
|
||||
provider={provider.name}
|
||||
settings={provider.settings}
|
||||
onUpdateSettings={(newSettings) => settings.updateProviderSettings(provider.name, newSettings)}
|
||||
/>
|
||||
|
||||
{/* Header Manager */}
|
||||
<HeaderManager
|
||||
provider={provider.name}
|
||||
settings={provider.settings}
|
||||
onUpdateSettings={(newSettings) => settings.updateProviderSettings(provider.name, newSettings)}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -119,10 +119,16 @@ export abstract class BaseProvider implements ProviderInfo {
|
||||
|
||||
type OptionalApiKey = string | undefined;
|
||||
|
||||
export function getOpenAILikeModel(baseURL: string, apiKey: OptionalApiKey, model: string) {
|
||||
export function getOpenAILikeModel(
|
||||
baseURL: string,
|
||||
apiKey: OptionalApiKey,
|
||||
model: string,
|
||||
customHeaders?: Record<string, string>,
|
||||
) {
|
||||
const openai = createOpenAI({
|
||||
baseURL,
|
||||
apiKey,
|
||||
headers: customHeaders,
|
||||
});
|
||||
|
||||
return openai(model);
|
||||
|
56
app/lib/modules/llm/providers/portkey.ts
Normal file
56
app/lib/modules/llm/providers/portkey.ts
Normal file
@ -0,0 +1,56 @@
|
||||
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';
|
||||
|
||||
export default class PortkeyProvider extends BaseProvider {
|
||||
name = 'Portkey';
|
||||
getApiKeyLink = 'https://portkey.ai/docs/api-reference/authentication';
|
||||
|
||||
config = {
|
||||
baseUrlKey: 'PORTKEY_API_BASE_URL',
|
||||
apiTokenKey: 'PORTKEY_API_KEY',
|
||||
};
|
||||
|
||||
// No static models - all models are user-configurable
|
||||
staticModels: ModelInfo[] = [];
|
||||
|
||||
// Get custom models from user settings
|
||||
async getDynamicModels(
|
||||
apiKeys?: Record<string, string>,
|
||||
settings?: IProviderSetting,
|
||||
_serverEnv: Record<string, string> = {},
|
||||
): Promise<ModelInfo[]> {
|
||||
// Return user-configured custom models
|
||||
return settings?.customModels || [];
|
||||
}
|
||||
|
||||
getModelInstance(options: {
|
||||
model: string;
|
||||
serverEnv: Env;
|
||||
apiKeys?: Record<string, string>;
|
||||
providerSettings?: Record<string, IProviderSetting>;
|
||||
}): LanguageModelV1 {
|
||||
const { model, serverEnv, apiKeys, providerSettings } = options;
|
||||
|
||||
const { baseUrl, apiKey } = this.getProviderBaseUrlAndKey({
|
||||
apiKeys,
|
||||
providerSettings: providerSettings?.[this.name],
|
||||
serverEnv: serverEnv as any,
|
||||
defaultBaseUrlKey: 'PORTKEY_API_BASE_URL',
|
||||
defaultApiTokenKey: 'PORTKEY_API_KEY',
|
||||
});
|
||||
|
||||
if (!baseUrl || !apiKey) {
|
||||
throw new Error(`Missing configuration for ${this.name} provider`);
|
||||
}
|
||||
|
||||
// Get custom headers from settings, with Portkey defaults
|
||||
const customHeaders = {
|
||||
'x-portkey-debug': 'false', // Default Portkey header
|
||||
...providerSettings?.[this.name]?.customHeaders, // User-defined headers override defaults
|
||||
};
|
||||
|
||||
return getOpenAILikeModel(baseUrl, apiKey, model, customHeaders);
|
||||
}
|
||||
}
|
@ -16,6 +16,7 @@ import XAIProvider from './providers/xai';
|
||||
import HyperbolicProvider from './providers/hyperbolic';
|
||||
import AmazonBedrockProvider from './providers/amazon-bedrock';
|
||||
import GithubProvider from './providers/github';
|
||||
import PortkeyProvider from './providers/portkey';
|
||||
|
||||
export {
|
||||
AnthropicProvider,
|
||||
@ -36,4 +37,5 @@ export {
|
||||
LMStudioProvider,
|
||||
AmazonBedrockProvider,
|
||||
GithubProvider,
|
||||
PortkeyProvider,
|
||||
};
|
||||
|
@ -29,7 +29,7 @@ export interface Shortcuts {
|
||||
toggleTerminal: Shortcut;
|
||||
}
|
||||
|
||||
export const URL_CONFIGURABLE_PROVIDERS = ['Ollama', 'LMStudio', 'OpenAILike'];
|
||||
export const URL_CONFIGURABLE_PROVIDERS = ['Ollama', 'LMStudio', 'OpenAILike', 'Portkey'];
|
||||
export const LOCAL_PROVIDERS = ['OpenAILike', 'LMStudio', 'Ollama'];
|
||||
|
||||
export type ProviderSetting = Record<string, IProviderConfig>;
|
||||
|
@ -17,6 +17,8 @@ export type ProviderInfo = {
|
||||
export interface IProviderSetting {
|
||||
enabled?: boolean;
|
||||
baseUrl?: string;
|
||||
customModels?: ModelInfo[];
|
||||
customHeaders?: Record<string, string>;
|
||||
}
|
||||
|
||||
export type IProviderConfig = ProviderInfo & {
|
||||
|
2
worker-configuration.d.ts
vendored
2
worker-configuration.d.ts
vendored
@ -18,4 +18,6 @@ interface Env {
|
||||
XAI_API_KEY: string;
|
||||
PERPLEXITY_API_KEY: string;
|
||||
AWS_BEDROCK_CONFIG: string;
|
||||
PORTKEY_API_KEY: string;
|
||||
PORTKEY_API_BASE_URL: string;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user