diff --git a/console/src/api/types/provider.ts b/console/src/api/types/provider.ts index 1b31a5840..3ca7221de 100644 --- a/console/src/api/types/provider.ts +++ b/console/src/api/types/provider.ts @@ -23,6 +23,8 @@ export interface ProviderInfo { api_key: string; base_url: string; generate_kwargs: Record; + /** Custom headers for API requests. */ + default_headers: Record; } export interface ProviderConfigRequest { @@ -30,6 +32,7 @@ export interface ProviderConfigRequest { base_url?: string; chat_model?: string; generate_kwargs?: Record; + default_headers?: Record; } export interface ModelSlotConfig { @@ -55,6 +58,7 @@ export interface CreateCustomProviderRequest { api_key_prefix?: string; chat_model?: string; models?: ModelInfo[]; + default_headers?: Record; } export interface AddModelRequest { @@ -126,10 +130,12 @@ export interface TestProviderRequest { base_url?: string; chat_model?: string; generate_kwargs?: Record; + default_headers?: Record; } export interface TestModelRequest { model_id: string; + default_headers?: Record; } export interface DiscoverModelsResponse { diff --git a/console/src/pages/Settings/Models/components/modals/CustomProviderModal.tsx b/console/src/pages/Settings/Models/components/modals/CustomProviderModal.tsx index c739752de..ccf2036db 100644 --- a/console/src/pages/Settings/Models/components/modals/CustomProviderModal.tsx +++ b/console/src/pages/Settings/Models/components/modals/CustomProviderModal.tsx @@ -1,8 +1,28 @@ import { useState, useEffect } from "react"; -import { Form, Input, Modal, Select, message } from "@agentscope-ai/design"; +import { + Form, + Input, + Modal, + Select, + message, + Button, +} from "@agentscope-ai/design"; +import { Space } from "antd"; +import { + PlusOutlined, + DeleteOutlined, + DownOutlined, + RightOutlined, +} from "@ant-design/icons"; import api from "../../../../../api"; import { useTranslation } from "react-i18next"; +interface HeaderItem { + id: string; + key: string; + value: string; +} + interface CustomProviderModalProps { open: boolean; onClose: () => void; @@ -17,23 +37,61 @@ export function CustomProviderModal({ const { t } = useTranslation(); const [saving, setSaving] = useState(false); const [form] = Form.useForm(); + const [headers, setHeaders] = useState([]); + const [advancedOpen, setAdvancedOpen] = useState(false); useEffect(() => { if (open) { form.resetFields(); + setHeaders([]); + setAdvancedOpen(false); } }, [open, form]); + const addHeader = () => { + // Pre-fill User-Agent for the first header (common for Kimi Coding Plan) + const newHeader: HeaderItem = { + id: crypto.randomUUID(), + key: headers.length === 0 ? "User-Agent" : "", + value: "", + }; + setHeaders([...headers, newHeader]); + }; + + const removeHeader = (index: number) => { + setHeaders(headers.filter((_, i) => i !== index)); + }; + + const updateHeader = ( + index: number, + field: keyof Omit, + value: string, + ) => { + const newHeaders = [...headers]; + newHeaders[index][field] = value; + setHeaders(newHeaders); + }; + const handleSubmit = async () => { try { const values = await form.validateFields(); setSaving(true); + + // Convert headers array to object + const defaultHeaders: Record = {}; + headers.forEach(({ key, value }) => { + if (key.trim()) { + defaultHeaders[key.trim()] = value; + } + }); + await api.createCustomProvider({ id: values.id.trim(), name: values.name.trim(), default_base_url: values.default_base_url?.trim() || "", api_key_prefix: values.api_key_prefix?.trim() || "", chat_model: values.chat_model || "OpenAIChatModel", + default_headers: defaultHeaders, }); message.success( t("models.providerCreated", { name: values.name.trim() }), @@ -123,6 +181,78 @@ export function CustomProviderModal({ ]} /> + + {/* Advanced Config - Headers */} +
+ + + {advancedOpen && ( +
+
+ {t("models.customHeaders", "Custom Headers")} +
+ {headers.map((header, index) => ( + + updateHeader(index, "key", e.target.value)} + style={{ width: 200 }} + /> + + updateHeader(index, "value", e.target.value) + } + style={{ width: 200 }} + /> + +
+ {t( + "models.headersHint", + "Optional: Add custom HTTP headers for API requests. Example: User-Agent: KimiCLI/0.77", + )} +
+
+ )} +
); diff --git a/console/src/pages/Settings/Models/components/modals/ProviderConfigModal.tsx b/console/src/pages/Settings/Models/components/modals/ProviderConfigModal.tsx index fe77f8a14..273df6831 100644 --- a/console/src/pages/Settings/Models/components/modals/ProviderConfigModal.tsx +++ b/console/src/pages/Settings/Models/components/modals/ProviderConfigModal.tsx @@ -8,7 +8,14 @@ import { Button, Select, } from "@agentscope-ai/design"; -import { ApiOutlined, DownOutlined, RightOutlined } from "@ant-design/icons"; +import { Space } from "antd"; +import { + ApiOutlined, + DownOutlined, + RightOutlined, + DeleteOutlined, + PlusOutlined, +} from "@ant-design/icons"; import type { ProviderConfigRequest } from "../../../../../api/types"; import api from "../../../../../api"; import { useTranslation } from "react-i18next"; @@ -241,6 +248,12 @@ function JsonCodeEditor({ ); } +interface HeaderItem { + id: string; + key: string; + value: string; +} + interface ProviderConfigModalProps { provider: { id: string; @@ -252,6 +265,7 @@ interface ProviderConfigModalProps { freeze_url: boolean; chat_model: string; generate_kwargs: Record; + default_headers?: Record; }; activeModels: any; open: boolean; @@ -272,9 +286,69 @@ export function ProviderConfigModal({ const [formDirty, setFormDirty] = useState(false); const [advancedOpen, setAdvancedOpen] = useState(false); const [form] = Form.useForm(); + const [headers, setHeaders] = useState([]); const selectedChatModel = Form.useWatch("chat_model", form); const canEditBaseUrl = !provider.freeze_url; + // Convert default_headers object to array for UI + useEffect(() => { + if ( + provider.default_headers && + Object.keys(provider.default_headers).length > 0 + ) { + const headerArray = Object.entries(provider.default_headers).map( + ([key, value]) => ({ + id: crypto.randomUUID(), + key, + value: String(value), + }), + ); + setHeaders(headerArray); + } else { + setHeaders([]); + } + }, [provider.default_headers, open]); + + const addHeader = () => { + // Pre-fill User-Agent for the first header (common for Kimi Coding Plan) + const newHeader: HeaderItem = { + id: crypto.randomUUID(), + key: headers.length === 0 ? "User-Agent" : "", + value: "", + }; + setHeaders([...headers, newHeader]); + setFormDirty(true); + }; + + const removeHeader = (index: number) => { + setHeaders(headers.filter((_, i) => i !== index)); + setFormDirty(true); + }; + + const updateHeader = ( + index: number, + field: keyof Omit, + value: string, + ) => { + const newHeaders = [...headers]; + newHeaders[index][field] = value; + setHeaders(newHeaders); + setFormDirty(true); + }; + + // Helper function to convert headers array to object + const headersToObject = ( + headersArray: HeaderItem[], + ): Record => { + const result: Record = {}; + headersArray.forEach(({ key, value }) => { + if (key.trim()) { + result[key.trim()] = value; + } + }); + return result; + }; + const parseGenerateConfig = (value?: string) => { const trimmed = value?.trim(); if (!trimmed) { @@ -391,6 +465,8 @@ export function ProviderConfigModal({ values.generate_kwargs_text?.trim(), ); + const defaultHeaders = headersToObject(headers); + // Validate connection before saving // For local providers, we might skip this or just check if models exist (which the backend does) if (!provider.is_custom) { @@ -398,6 +474,7 @@ export function ProviderConfigModal({ api_key: values.api_key, base_url: values.base_url, chat_model: values.chat_model, + default_headers: defaultHeaders, }); if (!result.success) { @@ -412,6 +489,7 @@ export function ProviderConfigModal({ base_url: values.base_url, chat_model: values.chat_model, generate_kwargs: hasGenerateConfigInput ? generateConfig : {}, + default_headers: defaultHeaders, }); await onSaved(); @@ -436,10 +514,14 @@ export function ProviderConfigModal({ "base_url", "chat_model", ]); + + const defaultHeaders = headersToObject(headers); + const result = await api.testProviderConnection(provider.id, { api_key: values.api_key, base_url: values.base_url, chat_model: values.chat_model, + default_headers: defaultHeaders, }); if (result.success) { message.success(result.message || t("models.testConnectionSuccess")); @@ -657,6 +739,65 @@ export function ProviderConfigModal({ + {/* Custom Headers Section */} + {advancedOpen && ( +
+
+ {t("models.customHeaders", "Custom Headers")} +
+ {headers.map((header, index) => ( + + updateHeader(index, "key", e.target.value)} + style={{ width: 220 }} + /> + + updateHeader(index, "value", e.target.value) + } + style={{ width: 220 }} + /> + +
+ {t( + "models.headersHint", + "Optional: Add custom HTTP headers for API requests. Example: User-Agent: KimiCLI/0.77", + )} +
+
+ )} +