Desktop: - No Custom Headers field for custom OpenAI-compatible providers (#6681)

Signed-off-by: Lymah123 <fimihanodunola625@gmail.com>
Co-authored-by: Zane Staggs <zane@squareup.com>
This commit is contained in:
Lymah
2026-02-12 01:20:56 +01:00
committed by GitHub
parent 153872a47c
commit f96d38e382
7 changed files with 246 additions and 25 deletions
@@ -207,7 +207,11 @@ pub fn update_custom_provider(params: UpdateCustomProviderParams) -> Result<()>
api_key_env,
base_url: params.api_url,
models: model_infos,
headers: params.headers.or(existing_config.headers),
headers: match params.headers {
Some(h) if h.is_empty() => None,
Some(h) => Some(h),
None => existing_config.headers,
},
timeout_seconds: existing_config.timeout_seconds,
supports_streaming: params.supports_streaming,
requires_auth: params.requires_auth,
+15 -19
View File
@@ -126,6 +126,7 @@ export default function ChatInput({
const [displayValue, setDisplayValue] = useState(initialValue); // For immediate visual feedback
const [isFocused, setIsFocused] = useState(false);
const [pastedImages, setPastedImages] = useState<PastedImage[]>([]);
const [isFilePickerOpen, setIsFilePickerOpen] = useState(false);
// Derived state - chatState != Idle means we're in some form of loading state
const isLoading = chatState !== ChatState.Idle;
@@ -148,7 +149,6 @@ export default function ChatInput({
const [diagnosticsOpen, setDiagnosticsOpen] = useState(false);
const [showCreateRecipeModal, setShowCreateRecipeModal] = useState(false);
const [showEditRecipeModal, setShowEditRecipeModal] = useState(false);
const [isFilePickerOpen, setIsFilePickerOpen] = useState(false);
const [sessionWorkingDir, setSessionWorkingDir] = useState<string | null>(null);
useEffect(() => {
@@ -1190,13 +1190,11 @@ export default function ChatInput({
return (
<div
className={`flex flex-col relative h-auto p-4 transition-colors ${
disableAnimation ? '' : 'page-transition'
} ${
isFocused
className={`flex flex-col relative h-auto p-4 transition-colors ${disableAnimation ? '' : 'page-transition'
} ${isFocused
? 'border-border-strong hover:border-border-strong'
: 'border-border-default hover:border-border-default'
} bg-background-default z-10 rounded-t-2xl`}
} bg-background-default z-10 rounded-t-2xl`}
data-drop-zone="true"
onDrop={handleLocalDrop}
onDragOver={handleLocalDragOver}
@@ -1265,7 +1263,7 @@ export default function ChatInput({
size="sm"
shape="round"
variant="outline"
onClick={() => {}}
onClick={() => { }}
disabled={true}
className="bg-slate-600 text-white cursor-not-allowed opacity-50 border-slate-600 rounded-full px-6 py-2"
>
@@ -1312,13 +1310,12 @@ export default function ChatInput({
}
}}
disabled={isTranscribing}
className={`rounded-full px-6 py-2 ${
isRecording
? 'bg-red-500 text-white hover:bg-red-600 border-red-500'
: isTranscribing
? 'bg-slate-600 text-white cursor-not-allowed animate-pulse border-slate-600'
: 'bg-slate-600 text-white hover:bg-slate-700 border-slate-600'
}`}
className={`rounded-full px-6 py-2 ${isRecording
? 'bg-red-500 text-white hover:bg-red-600 border-red-500'
: isTranscribing
? 'bg-slate-600 text-white cursor-not-allowed animate-pulse border-slate-600'
: 'bg-slate-600 text-white hover:bg-slate-700 border-slate-600'
}`}
>
<Microphone />
</Button>
@@ -1356,11 +1353,10 @@ export default function ChatInput({
shape="round"
variant="outline"
disabled={isSubmitButtonDisabled}
className={`rounded-full px-10 py-2 flex items-center gap-2 ${
isSubmitButtonDisabled
? 'bg-slate-600 text-white cursor-not-allowed opacity-50 border-slate-600'
: 'bg-slate-600 text-white hover:bg-slate-700 border-slate-600 hover:cursor-pointer'
}`}
className={`rounded-full px-10 py-2 flex items-center gap-2 ${isSubmitButtonDisabled
? 'bg-slate-600 text-white cursor-not-allowed opacity-50 border-slate-600'
: 'bg-slate-600 text-white hover:bg-slate-700 border-slate-600 hover:cursor-pointer'
}`}
>
<Send className="w-4 h-4" />
<span className="text-sm">Send</span>
@@ -156,6 +156,19 @@ export default function ExtensionModal({
};
const handleHeaderChange = (index: number, field: 'key' | 'value', value: string) => {
if (field === 'key') {
if (value.includes(' ')) {
return;
}
const trimmedNewKey = value.trim();
const normalizedNewKey = trimmedNewKey.toLowerCase();
const isDuplicate = formData.headers.some(
(h, i) => i !== index && h.key.trim().toLowerCase() === normalizedNewKey,
);
if (isDuplicate && trimmedNewKey !== '') {
return;
}
}
const newHeaders = [...formData.headers];
newHeaders[index][field] = value;
@@ -44,6 +44,10 @@ export default function HeadersSection({
const keyEmpty = !newKey.trim();
const valueEmpty = !newValue.trim();
const keyHasSpaces = newKey.includes(' ');
const normalizedNewKey = newKey.trim().toLowerCase();
const isDuplicate = headers.some(
h => h.key.trim().toLowerCase() === normalizedNewKey
);
if (keyEmpty || valueEmpty) {
setInvalidFields({
@@ -63,6 +67,15 @@ export default function HeadersSection({
return;
}
if (isDuplicate) {
setInvalidFields({
key: true,
value: false,
});
setValidationError('A header with this name already exists');
return;
}
setValidationError(null);
setInvalidFields({ key: false, value: false });
onAdd(newKey, newValue);
@@ -100,8 +100,8 @@ export function extensionToFormData(extension: FixedExtensionEntry): ExtensionFo
description: extension.description || '',
type:
extension.type === 'frontend' ||
extension.type === 'inline_python' ||
extension.type === 'platform'
extension.type === 'inline_python' ||
extension.type === 'platform'
? 'stdio'
: extension.type,
cmd: extension.type === 'stdio' ? quoteShell([extension.cmd, ...extension.args]) : undefined,
@@ -155,7 +155,7 @@ export function createExtensionConfig(formData: ExtensionFormData): ExtensionCon
timeout: formData.timeout,
uri: formData.endpoint || '',
...(env_keys.length > 0 ? { env_keys } : {}),
...(Object.keys(headers).length > 0 ? { headers } : {}),
headers,
};
} else {
// For other types
@@ -238,6 +238,7 @@ function ProviderCards({
models: editingProvider.config.models.map((m) => m.name),
supports_streaming: editingProvider.config.supports_streaming ?? true,
requires_auth: editingProvider.config.requires_auth ?? true,
headers: editingProvider.config.headers ?? undefined,
};
const editable = editingProvider ? editingProvider.isEditable : true;
@@ -246,7 +247,7 @@ function ProviderCards({
<>
{providerCards}
<Dialog open={showCustomProviderModal} onOpenChange={handleCloseModal}>
<DialogContent className="sm:max-w-[600px]">
<DialogContent className="sm:max-w-[600px] max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
</DialogHeader>
@@ -4,7 +4,8 @@ import { Select } from '../../../../../ui/Select';
import { Button } from '../../../../../ui/button';
import { SecureStorageNotice } from '../SecureStorageNotice';
import { UpdateCustomProviderRequest } from '../../../../../../api';
import { Trash2, AlertTriangle } from 'lucide-react';
import { Plus, X, Trash2, AlertTriangle } from 'lucide-react';
import { cn } from '../../../../../../utils';
interface CustomProviderFormProps {
onSubmit: (data: UpdateCustomProviderRequest) => void;
@@ -30,6 +31,14 @@ export default function CustomProviderForm({
const [models, setModels] = useState('');
const [requiresApiKey, setRequiresApiKey] = useState(false);
const [supportsStreaming, setSupportsStreaming] = useState(true);
const [headers, setHeaders] = useState<{ key: string; value: string }[]>([]);
const [newHeaderKey, setNewHeaderKey] = useState('');
const [newHeaderValue, setNewHeaderValue] = useState('');
const [headerValidationError, setHeaderValidationError] = useState<string | null>(null);
const [invalidHeaderFields, setInvalidHeaderFields] = useState<{ key: boolean; value: boolean }>({
key: false,
value: false,
});
const [validationErrors, setValidationErrors] = useState<Record<string, string>>({});
const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false);
@@ -46,6 +55,14 @@ export default function CustomProviderForm({
setModels(initialData.models.join(', '));
setSupportsStreaming(initialData.supports_streaming ?? true);
setRequiresApiKey(initialData.requires_auth ?? true);
if (initialData.headers) {
const headerList = Object.entries(initialData.headers).map(([key, value]) => ({
key,
value,
}));
setHeaders(headerList);
}
}
}, [initialData]);
@@ -56,6 +73,85 @@ export default function CustomProviderForm({
}
};
const handleAddHeader = () => {
const keyEmpty = !newHeaderKey.trim();
const valueEmpty = !newHeaderValue.trim();
const keyHasSpaces = newHeaderKey.includes(' ');
const normalizedNewKey = newHeaderKey.trim().toLowerCase();
const isDuplicate = headers.some(h => h.key.trim().toLowerCase() === normalizedNewKey);
if (keyEmpty || valueEmpty) {
setInvalidHeaderFields({
key: keyEmpty,
value: valueEmpty,
});
setHeaderValidationError('Both header name and value must be entered');
return;
}
if (keyHasSpaces) {
setInvalidHeaderFields({
key: true,
value: false,
});
setHeaderValidationError('Header name cannot contain spaces');
return;
}
if (isDuplicate) {
setInvalidHeaderFields({
key: true,
value: false,
});
setHeaderValidationError('A header with this name already exists');
return;
}
setHeaderValidationError(null);
setInvalidHeaderFields({ key: false, value: false });
setHeaders([...headers, { key: newHeaderKey, value: newHeaderValue }]);
setNewHeaderKey('');
setNewHeaderValue('');
};
const handleRemoveHeader = (index: number) => {
setHeaders(headers.filter((_, i) => i !== index));
};
const handleHeaderChange = (index: number, field: 'key' | 'value', value: string) => {
if (field === 'key') {
if (value.includes(' ')) {
return;
}
const normalizedValue = value.trim().toLowerCase();
const isDuplicate = headers.some(
(h, i) => i !== index && h.key.trim().toLowerCase() === normalizedValue,
);
if (isDuplicate && normalizedValue !== '') {
return;
}
const updatedHeaders = [...headers];
updatedHeaders[index].key = value;
setHeaders(updatedHeaders);
return;
}
const updatedHeaders = [...headers];
updatedHeaders[index][field] = value;
setHeaders(updatedHeaders);
};
const clearHeaderValidation = () => {
setHeaderValidationError(null);
setInvalidHeaderFields({ key: false, value: false });
};
const handleHeaderKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault();
handleAddHeader();
}
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
@@ -76,6 +172,30 @@ export default function CustomProviderForm({
.map((m) => m.trim())
.filter((m) => m);
let allHeaders = [...headers];
if (newHeaderKey.trim() && newHeaderValue.trim()) {
const keyHasSpaces = newHeaderKey.includes(' ');
const normalizedPendingKey = newHeaderKey.trim().toLowerCase();
const isDuplicate = headers.some(
(h) => h.key.trim().toLowerCase() === normalizedPendingKey,
);
if (!keyHasSpaces && !isDuplicate) {
allHeaders.push({ key: newHeaderKey, value: newHeaderValue });
}
}
const headersObject = allHeaders.reduce(
(acc, header) => {
if (header.key.trim() && header.value.trim()) {
acc[header.key.trim()] = header.value.trim();
}
return acc;
},
{} as Record<string, string>
);
onSubmit({
engine,
display_name: displayName,
@@ -84,6 +204,7 @@ export default function CustomProviderForm({
models: modelList,
supports_streaming: supportsStreaming,
requires_auth: requiresApiKey,
headers: headersObject,
});
};
@@ -260,6 +381,79 @@ export default function CustomProviderForm({
Provider supports streaming responses
</label>
</div>
<div>
<label className="text-sm font-medium text-textStandard mb-2 block">
Custom Headers
</label>
<p className="text-xs text-textSubtle mb-4">
Add custom HTTP headers to include in requests to the provider. Click the "+" button to add after filling both fields.
</p>
<div className="grid grid-cols-[1fr_1fr_auto] gap-2 items-center">
{headers.map((header, index) => (
<React.Fragment key={index}>
<Input
value={header.key}
onChange={(e) => handleHeaderChange(index, 'key', e.target.value)}
placeholder="Header name"
className="w-full text-textStandard border-borderSubtle hover:border-borderStandard"
/>
<Input
value={header.value}
onChange={(e) => handleHeaderChange(index, 'value', e.target.value)}
placeholder="Value"
className="w-full text-textStandard border-borderSubtle hover:border-borderStandard"
/>
<Button
onClick={() => handleRemoveHeader(index)}
variant="ghost"
type="button"
className="group p-2 h-auto text-iconSubtle hover:bg-transparent"
>
<X className="h-3 w-3 text-gray-400 group-hover:text-white group-hover:drop-shadow-sm transition-all" />
</Button>
</React.Fragment>
))}
<Input
value={newHeaderKey}
onChange={(e) => {
setNewHeaderKey(e.target.value);
clearHeaderValidation();
}}
onKeyDown={handleHeaderKeyDown}
placeholder="Header name"
className={cn(
'w-full text-textStandard border-borderSubtle hover:border-borderStandard',
invalidHeaderFields.key && 'border-red-500 focus:border-red-500'
)}
/>
<Input
value={newHeaderValue}
onChange={(e) => {
setNewHeaderValue(e.target.value);
clearHeaderValidation();
}}
onKeyDown={handleHeaderKeyDown}
placeholder="Value"
className={cn(
'w-full text-textStandard border-borderSubtle hover:border-borderStandard',
invalidHeaderFields.value && 'border-red-500 focus:border-red-500'
)}
/>
<Button
onClick={handleAddHeader}
variant="ghost"
type="button"
className="flex items-center justify-start gap-1 px-2 pr-4 text-sm rounded-full text-textStandard bg-background-default border border-borderSubtle hover:border-borderStandard transition-colors min-w-[60px] h-9 [&>svg]:!size-4"
>
<Plus /> Add
</Button>
</div>
{headerValidationError && (
<div className="mt-2 text-red-500 text-sm">{headerValidationError}</div>
)}
</div>
</>
)}
<SecureStorageNotice />