From 828e126f9b60aad81e4a3ebaa8e9bf594a92e74c Mon Sep 17 00:00:00 2001 From: John Yin <10972267+john-yin2333@user.noreply.gitee.com> Date: Fri, 8 May 2026 13:09:17 +0800 Subject: [PATCH 1/3] fix(provider): support custom Azure deployments Allow Azure OpenAI users to configure deployment names directly in the provider setup flow and cover the runtime/test-credentials path so custom deployments are usable. Co-authored-by: Cursor --- tests/provider/test_azure_provider.py | 24 ++++++ tests/provider/test_test_credentials.py | 40 +++++++++ .../routes/test_custom_provider_runtime.py | 33 +++++++ webui/src/locales/en-US/model.json | 9 +- webui/src/locales/zh-CN/model.json | 9 +- webui/src/pages/Model/index.tsx | 85 +++++++++++++++++-- 6 files changed, 193 insertions(+), 7 deletions(-) create mode 100644 tests/provider/test_azure_provider.py diff --git a/tests/provider/test_azure_provider.py b/tests/provider/test_azure_provider.py new file mode 100644 index 00000000..0210ad8a --- /dev/null +++ b/tests/provider/test_azure_provider.py @@ -0,0 +1,24 @@ +from flocks.provider.provider import ModelCapabilities, ModelInfo +from flocks.provider.sdk.azure import AzureProvider + + +def test_azure_provider_returns_configured_deployment_models(): + provider = AzureProvider() + provider._config_models = [ + ModelInfo( + id="customer-prod-deployment", + name="Customer Production Deployment", + provider_id="azure", + capabilities=ModelCapabilities( + supports_tools=True, + supports_streaming=True, + context_window=128000, + max_tokens=4096, + ), + ) + ] + + models = provider.get_models() + + assert [m.id for m in models] == ["customer-prod-deployment"] + assert models[0].name == "Customer Production Deployment" diff --git a/tests/provider/test_test_credentials.py b/tests/provider/test_test_credentials.py index e01d5db7..464bd68a 100644 --- a/tests/provider/test_test_credentials.py +++ b/tests/provider/test_test_credentials.py @@ -754,3 +754,43 @@ async def test_existing_custom_settings_are_preserved_during_provider_test(self) assert configured.api_key == "gateway-api-key" assert configured.base_url == "https://gateway.internal/v1" assert configured.custom_settings["verify_ssl"] is False + + @pytest.mark.asyncio + async def test_requested_azure_deployment_model_is_used_for_provider_test(self): + from flocks.server.routes.provider import TestCredentialRequest, test_provider_credentials + + provider = MagicMock() + provider._config = MagicMock( + custom_settings={}, + base_url="https://example-resource.openai.azure.com/", + ) + provider.chat = AsyncMock(return_value=MagicMock(content="Paris")) + + model = MagicMock() + model.id = "customer-prod-deployment" + + mock_secrets = MagicMock() + mock_secrets.get.return_value = "azure-api-key" + + mock_config = MagicMock() + + with ( + patch(_PATCH_SECRET_MGR, return_value=mock_secrets), + patch(_PATCH_CONFIG_GET, new_callable=AsyncMock, return_value=mock_config), + patch(_PATCH_PROVIDER) as mock_provider_cls, + ): + mock_provider_cls._ensure_initialized = MagicMock() + mock_provider_cls._load_dynamic_providers = MagicMock() + mock_provider_cls.apply_config = AsyncMock() + mock_provider_cls.get.return_value = provider + mock_provider_cls.list_models.return_value = [model] + + result = await test_provider_credentials( + "azure-openai", + TestCredentialRequest(model_id="customer-prod-deployment"), + ) + + assert result["success"] is True, result + assert result["model_id"] == "customer-prod-deployment" + provider.chat.assert_awaited_once() + assert provider.chat.await_args.args[0] == "customer-prod-deployment" diff --git a/tests/server/routes/test_custom_provider_runtime.py b/tests/server/routes/test_custom_provider_runtime.py index 0e8baaee..16b93d00 100644 --- a/tests/server/routes/test_custom_provider_runtime.py +++ b/tests/server/routes/test_custom_provider_runtime.py @@ -48,3 +48,36 @@ class DummyProvider: assert provider._config_models[0].capabilities.supports_reasoning is True finally: Provider._models = original_models + + +def test_add_azure_deployment_to_runtime_config_models(monkeypatch): + class DynamicAzureProvider: + _config_models = [] + + provider = DynamicAzureProvider() + body = CreateModelReq( + model_id="customer-prod-deployment", + name="Customer Production Deployment", + context_window=128000, + max_output_tokens=4096, + supports_vision=False, + supports_tools=True, + supports_streaming=True, + supports_reasoning=False, + input_price=0.0, + output_price=0.0, + currency="USD", + ) + + original_models = Provider._models + Provider._models = {} + monkeypatch.setattr(Provider, "get", classmethod(lambda cls, provider_id: provider)) + + try: + _add_model_to_runtime("azure-openai", body) + + assert Provider._models[body.model_id].provider_id == "azure-openai" + assert provider._config_models[0].id == "customer-prod-deployment" + assert provider._config_models[0].name == "Customer Production Deployment" + finally: + Provider._models = original_models diff --git a/webui/src/locales/en-US/model.json b/webui/src/locales/en-US/model.json index c2480436..a83c78f5 100644 --- a/webui/src/locales/en-US/model.json +++ b/webui/src/locales/en-US/model.json @@ -119,7 +119,14 @@ "loadFailed": "Failed to load provider catalog", "noModelsToTest": "No enabled models to test", "batchTestDone": "Batch test complete", - "batchTestSummary": "{{success}} succeeded, {{failed}} failed" + "batchTestSummary": "{{success}} succeeded, {{failed}} failed", + "azureDeploymentName": "Azure Deployment Name", + "azureDeploymentPlaceholder": "e.g. my-gpt-4o-prod", + "azureDeploymentHint": "Azure OpenAI requests use the deployment name, not a fixed model name. The preset models are examples; enter your own deployment name here.", + "azureDeploymentDisplayName": "Display Name (optional)", + "azureDeploymentDisplayPlaceholder": "e.g. GPT-4o Production", + "azureDeploymentRequired": "Select at least one preset model or enter an Azure deployment name", + "azureModelIdHint": "For Azure OpenAI, Model ID should be the deployment name from Azure Portal." }, "wizard": { "providerSaved": "Provider Saved", diff --git a/webui/src/locales/zh-CN/model.json b/webui/src/locales/zh-CN/model.json index 29cb71e4..2c610732 100644 --- a/webui/src/locales/zh-CN/model.json +++ b/webui/src/locales/zh-CN/model.json @@ -119,7 +119,14 @@ "loadFailed": "加载 Provider 目录失败", "noModelsToTest": "没有已启用的模型可测试", "batchTestDone": "批量测试完成", - "batchTestSummary": "{{success}} 成功, {{failed}} 失败" + "batchTestSummary": "{{success}} 成功, {{failed}} 失败", + "azureDeploymentName": "Azure Deployment Name", + "azureDeploymentPlaceholder": "例如 my-gpt-4o-prod", + "azureDeploymentHint": "Azure OpenAI 请求使用 deployment name,而不是固定模型名。预设模型只是常用示例,你可以在这里填写自己的部署名称。", + "azureDeploymentDisplayName": "显示名称(可选)", + "azureDeploymentDisplayPlaceholder": "例如 GPT-4o Production", + "azureDeploymentRequired": "请至少选择一个预设模型,或填写 Azure deployment name", + "azureModelIdHint": "对于 Azure OpenAI,模型 ID 请填写 Azure Portal 中的 deployment name。" }, "wizard": { "providerSaved": "Provider 已保存", diff --git a/webui/src/pages/Model/index.tsx b/webui/src/pages/Model/index.tsx index cb15baf3..3df71a10 100644 --- a/webui/src/pages/Model/index.tsx +++ b/webui/src/pages/Model/index.tsx @@ -1088,6 +1088,8 @@ function AddProviderDialog({ connectedIds, onClose, onAdded }: { const [baseUrl, setBaseUrl] = useState(''); const [description, setDescription] = useState(''); const [providerName, setProviderName] = useState(''); + const [azureDeploymentName, setAzureDeploymentName] = useState(''); + const [azureDeploymentDisplayName, setAzureDeploymentDisplayName] = useState(''); // Model selection (for catalog providers) const [selectedModelIds, setSelectedModelIds] = useState>(new Set()); @@ -1172,6 +1174,8 @@ function AddProviderDialog({ connectedIds, onClose, onAdded }: { setDescription(provider.description || ''); setSelectedModelIds(new Set(provider.models.map(m => m.id))); setProviderName(''); + setAzureDeploymentName(''); + setAzureDeploymentDisplayName(''); } }; @@ -1212,7 +1216,14 @@ function AddProviderDialog({ connectedIds, onClose, onAdded }: { base_url: baseUrl.trim() || undefined, provider_name: selectedCatalogId === 'openai-compatible' && providerName.trim() ? providerName.trim() : undefined, }); - const res = await providerAPI.testCredentials(selectedCatalogId); + const azureModelId = selectedCatalogId === 'azure-openai' ? azureDeploymentName.trim() : ''; + if (azureModelId) { + await modelV2API.createDefinition(selectedCatalogId, { + model_id: azureModelId, + name: azureDeploymentDisplayName.trim() || azureModelId, + }); + } + const res = await providerAPI.testCredentials(selectedCatalogId, azureModelId || undefined); setTestResult({ success: res.data.success, message: res.data.message || (res.data.success ? t('status.connected') : t('form.testFailed')), @@ -1235,6 +1246,11 @@ function AddProviderDialog({ connectedIds, onClose, onAdded }: { toast.warning('Please enter API Key'); return; } + const azureModelId = selectedCatalogId === 'azure-openai' ? azureDeploymentName.trim() : ''; + if (selectedCatalogId === 'azure-openai' && selectedModelIds.size === 0 && !azureModelId) { + toast.warning(t('form.azureDeploymentRequired')); + return; + } try { setSaving(true); if (selectedCatalogId === 'openai-compatible') { @@ -1259,6 +1275,20 @@ function AddProviderDialog({ connectedIds, onClose, onAdded }: { const unselected = selectedCatalog.models.filter(m => !selectedModelIds.has(m.id)).map(m => m.id); await Promise.all(unselected.map(id => modelV2API.deleteDefinition(selectedCatalogId, id).catch(() => {}))); } + if (azureModelId) { + await modelV2API.createDefinition(selectedCatalogId, { + model_id: azureModelId, + name: azureDeploymentDisplayName.trim() || azureModelId, + }); + try { + const res = await providerAPI.testCredentials(selectedCatalogId, azureModelId); + if (!res.data.success) { + toast.error(t('form.testFailed'), res.data.error || res.data.message); + } + } catch (testErr: any) { + toast.error(t('form.testFailed'), testErr.response?.data?.detail || testErr.message); + } + } toast.success(t('providerAdded'), displayName); setSavedProviderId(selectedCatalogId); setSavedProviderName(displayName); @@ -1600,6 +1630,36 @@ function AddProviderDialog({ connectedIds, onClose, onAdded }: { )} + {selectedCatalogId === 'azure-openai' && ( +
+
+ + setAzureDeploymentName(e.target.value)} + className="w-full px-3 py-2 border border-blue-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-300 text-sm bg-white" + placeholder={t('form.azureDeploymentPlaceholder')} + /> +

{t('form.azureDeploymentHint')}

+
+
+ + setAzureDeploymentDisplayName(e.target.value)} + className="w-full px-3 py-2 border border-blue-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-300 text-sm bg-white" + placeholder={azureDeploymentName.trim() || t('form.azureDeploymentDisplayPlaceholder')} + /> +
+
+ )} + {selectedCatalog.models.length > 0 && (
@@ -1712,7 +1772,13 @@ function AddProviderDialog({ connectedIds, onClose, onAdded }: {

{t('wizard.modelsAdded', { count: addedModelCount })}

)} - +
)} @@ -1791,10 +1857,12 @@ function useModelForm() { }; } -function ModelFormFields({ form, testResult, testing }: { +function ModelFormFields({ form, testResult, testing, modelIdPlaceholder, modelIdHint }: { form: ReturnType; testResult: { success: boolean; message: string; latency?: number } | null; testing: boolean; + modelIdPlaceholder?: string; + modelIdHint?: string; }) { const { t } = useTranslation('model'); return ( @@ -1809,8 +1877,9 @@ function ModelFormFields({ form, testResult, testing }: { value={form.modelId} onChange={e => form.setModelId(e.target.value)} className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-slate-400 text-sm" - placeholder="gpt-4o-custom" + placeholder={modelIdPlaceholder || 'gpt-4o-custom'} /> + {modelIdHint &&

{modelIdHint}

}
- + ); From d0fede34021bc26914f0382136078f118d94ca3a Mon Sep 17 00:00:00 2001 From: John Yin <10972267+john-yin2333@user.noreply.gitee.com> Date: Fri, 8 May 2026 13:16:11 +0800 Subject: [PATCH 2/3] fix(provider): show Azure custom deployments in settings Keep saved Azure deployment names visible when editing provider settings and separate catalog model counts from custom deployments. Co-authored-by: Cursor --- webui/src/locales/en-US/model.json | 4 +- webui/src/locales/zh-CN/model.json | 4 +- webui/src/pages/Model/index.tsx | 70 +++++++++++++++++++++++++++++- 3 files changed, 75 insertions(+), 3 deletions(-) diff --git a/webui/src/locales/en-US/model.json b/webui/src/locales/en-US/model.json index a83c78f5..abdf84f6 100644 --- a/webui/src/locales/en-US/model.json +++ b/webui/src/locales/en-US/model.json @@ -126,7 +126,9 @@ "azureDeploymentDisplayName": "Display Name (optional)", "azureDeploymentDisplayPlaceholder": "e.g. GPT-4o Production", "azureDeploymentRequired": "Select at least one preset model or enter an Azure deployment name", - "azureModelIdHint": "For Azure OpenAI, Model ID should be the deployment name from Azure Portal." + "azureModelIdHint": "For Azure OpenAI, Model ID should be the deployment name from Azure Portal.", + "azureCustomDeployments": "Custom Azure Deployments", + "azureNoCustomDeployments": "No custom Azure deployment has been added yet." }, "wizard": { "providerSaved": "Provider Saved", diff --git a/webui/src/locales/zh-CN/model.json b/webui/src/locales/zh-CN/model.json index 2c610732..cc85cb09 100644 --- a/webui/src/locales/zh-CN/model.json +++ b/webui/src/locales/zh-CN/model.json @@ -126,7 +126,9 @@ "azureDeploymentDisplayName": "显示名称(可选)", "azureDeploymentDisplayPlaceholder": "例如 GPT-4o Production", "azureDeploymentRequired": "请至少选择一个预设模型,或填写 Azure deployment name", - "azureModelIdHint": "对于 Azure OpenAI,模型 ID 请填写 Azure Portal 中的 deployment name。" + "azureModelIdHint": "对于 Azure OpenAI,模型 ID 请填写 Azure Portal 中的 deployment name。", + "azureCustomDeployments": "自定义 Azure Deployments", + "azureNoCustomDeployments": "尚未添加自定义 Azure deployment。" }, "wizard": { "providerSaved": "Provider 已保存", diff --git a/webui/src/pages/Model/index.tsx b/webui/src/pages/Model/index.tsx index 3df71a10..6b84b6c5 100644 --- a/webui/src/pages/Model/index.tsx +++ b/webui/src/pages/Model/index.tsx @@ -2161,6 +2161,18 @@ function ConfigureProviderDialog({ provider, existingCredentials, models, onClos // Catalog model management const [catalogModels, setCatalogModels] = useState([]); const [selectedModelIds, setSelectedModelIds] = useState>(new Set(models.map(m => m.id))); + const [newAzureDeploymentName, setNewAzureDeploymentName] = useState(''); + const [newAzureDeploymentDisplayName, setNewAzureDeploymentDisplayName] = useState(''); + const isAzureProvider = provider.id === 'azure-openai' || provider.id === 'azure'; + const catalogModelIds = useMemo(() => new Set(catalogModels.map(m => m.id)), [catalogModels]); + const selectedCatalogModelCount = useMemo( + () => catalogModels.filter(m => selectedModelIds.has(m.id)).length, + [catalogModels, selectedModelIds] + ); + const azureCustomModels = useMemo( + () => isAzureProvider ? models.filter(m => !catalogModelIds.has(m.id)) : [], + [catalogModelIds, isAzureProvider, models] + ); useEffect(() => { setApiKey(existingKey); @@ -2223,6 +2235,13 @@ function ConfigureProviderDialog({ provider, existingCredentials, models, onClos ...toAdd.map(m => modelV2API.createDefinition(provider.id, { model_id: m.id, name: m.name }).catch(() => {})), ]); } + const azureModelId = newAzureDeploymentName.trim(); + if (isAzureProvider && azureModelId) { + await modelV2API.createDefinition(provider.id, { + model_id: azureModelId, + name: newAzureDeploymentDisplayName.trim() || azureModelId, + }); + } toast.success(t('credentialsSaved')); onConfigured(); @@ -2418,7 +2437,7 @@ ${hasExisting ? '你已有凭证配置,可以更新或测试连接。' : '请