mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
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:
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
+195
-1
@@ -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 />
|
||||
|
||||
Reference in New Issue
Block a user