Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions flocks/server/routes/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -2320,9 +2320,15 @@ async def test_provider_credentials(provider_id: str, body: Optional[TestCredent
requested_model_id = body.model_id if body else None
test_model_id = requested_model_id or models[0].id

# Validate model belongs to this provider
# Validate model belongs to this provider. Azure OpenAI is the
# exception: users may test a deployment name before saving it.
valid_ids = {m.id for m in models}
if test_model_id not in valid_ids:
is_unsaved_azure_deployment = (
requested_model_id
and provider_id in {"azure-openai", "azure"}
and test_model_id not in valid_ids
)
if test_model_id not in valid_ids and not is_unsaved_azure_deployment:
response = {
"success": False,
"message": f"模型 '{test_model_id}' 不属于该 Provider",
Expand Down
33 changes: 33 additions & 0 deletions tests/provider/test_azure_provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
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"


def test_azure_provider_returns_fallback_models_without_config():
provider = AzureProvider()

models = provider.get_models()

assert {m.id for m in models} == {"gpt-5.4", "gpt-5-mini"}
assert all(m.provider_id == "azure" for m in models)
80 changes: 80 additions & 0 deletions tests/provider/test_test_credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -754,3 +754,83 @@ 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"

@pytest.mark.asyncio
async def test_unsaved_azure_deployment_can_be_tested_without_model_definition(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"))

catalog_model = MagicMock()
catalog_model.id = "gpt-5.4"

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 = [catalog_model]

result = await test_provider_credentials(
"azure-openai",
TestCredentialRequest(model_id="unsaved-prod-deployment"),
)

assert result["success"] is True, result
assert result["model_id"] == "unsaved-prod-deployment"
provider.chat.assert_awaited_once()
assert provider.chat.await_args.args[0] == "unsaved-prod-deployment"
33 changes: 33 additions & 0 deletions tests/server/routes/test_custom_provider_runtime.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from flocks.provider.provider import ModelCapabilities, ModelInfo, Provider
from flocks.provider.sdk.azure import AzureProvider
from flocks.server.routes.custom_provider import CreateModelReq, _add_model_to_runtime


Expand Down Expand Up @@ -48,3 +49,35 @@ 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):
provider = AzureProvider()
provider.id = "azure-openai"
provider._config_models = []
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
11 changes: 10 additions & 1 deletion webui/src/locales/en-US/model.json
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,16 @@
"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.",
"azureCustomDeployments": "Custom Azure Deployments",
"azureNoCustomDeployments": "No custom Azure deployment has been added yet."
},
"wizard": {
"providerSaved": "Provider Saved",
Expand Down
11 changes: 10 additions & 1 deletion webui/src/locales/zh-CN/model.json
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,16 @@
"loadFailed": "加载 Provider 目录失败",
"noModelsToTest": "没有已启用的模型可测试",
"batchTestDone": "批量测试完成",
"batchTestSummary": "{{success}} 成功, {{failed}} 失败"
"batchTestSummary": "{{success}} 成功, {{failed}} 失败",
"azureDeploymentName": "Azure 部署名称",
"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。",
"azureCustomDeployments": "自定义 Azure Deployments",
"azureNoCustomDeployments": "尚未添加自定义 Azure deployment。"
},
"wizard": {
"providerSaved": "Provider 已保存",
Expand Down
Loading
Loading