diff --git a/surfsense_backend/app/services/provider_registry.py b/surfsense_backend/app/services/provider_registry.py index 67d1c4db4..6392e38c3 100644 --- a/surfsense_backend/app/services/provider_registry.py +++ b/surfsense_backend/app/services/provider_registry.py @@ -87,6 +87,15 @@ REGISTRY: dict[str, ProviderSpec] = { "bearer", "OpenAI-compatible provider", ), + "openai_compatible_raw": ProviderSpec( + Transport.NATIVE, + "openai", + "none", + None, + True, + "bearer", + "OpenAI-compatible raw endpoint", + ), "lm_studio": ProviderSpec( Transport.OPENAI_COMPATIBLE, "openai", diff --git a/surfsense_backend/tests/unit/services/test_model_connections.py b/surfsense_backend/tests/unit/services/test_model_connections.py index b4e7c18d7..bb5ca318e 100644 --- a/surfsense_backend/tests/unit/services/test_model_connections.py +++ b/surfsense_backend/tests/unit/services/test_model_connections.py @@ -64,6 +64,22 @@ def test_openai_compatible_resolver_uses_explicit_api_base() -> None: assert ensure_v1("http://example.com/v1") == "http://example.com/v1" +def test_openai_compatible_raw_resolver_does_not_append_v1() -> None: + model, kwargs = to_litellm( + { + "provider": "openai_compatible_raw", + "base_url": "https://ark.cn-beijing.volces.com/api/v3", + "api_key": "ark-key", + "extra": {}, + }, + "ep-20260101000000-test", + ) + + assert model == "openai/ep-20260101000000-test" + assert kwargs["api_base"] == "https://ark.cn-beijing.volces.com/api/v3" + assert kwargs["api_key"] == "ark-key" + + def test_ollama_resolver_uses_native_api_base() -> None: model, kwargs = to_litellm( { diff --git a/surfsense_web/components/settings/model-connections/default-connect-form.tsx b/surfsense_web/components/settings/model-connections/default-connect-form.tsx index e3111202d..6ca96ced2 100644 --- a/surfsense_web/components/settings/model-connections/default-connect-form.tsx +++ b/surfsense_web/components/settings/model-connections/default-connect-form.tsx @@ -9,7 +9,10 @@ function baseUrlHint(provider: string) { return "For local servers, use host.docker.internal instead of localhost."; } if (provider === "openai_compatible") { - return "Enter the full endpoint URL."; + return "Enter the full endpoint URL. This provider expects a /v1-compatible endpoint."; + } + if (provider === "openai_compatible_raw") { + return "Enter the exact chat-completions API base URL. SurfSense will not append /v1."; } if (provider === "openai" || provider === "anthropic" || provider === "openrouter") { return "Override only if you route through a proxy or gateway."; diff --git a/surfsense_web/components/settings/model-connections/provider-metadata.tsx b/surfsense_web/components/settings/model-connections/provider-metadata.tsx index 8b8a877b9..0a5cc53c8 100644 --- a/surfsense_web/components/settings/model-connections/provider-metadata.tsx +++ b/surfsense_web/components/settings/model-connections/provider-metadata.tsx @@ -10,6 +10,7 @@ export const PROVIDER_ORDER = [ "ollama_chat", "lm_studio", "openai_compatible", + "openai_compatible_raw", ]; export const PROVIDER_DISPLAY: Record< @@ -37,6 +38,11 @@ export const PROVIDER_DISPLAY: Record< subtitle: "OpenAI-compatible endpoint", iconKey: "custom", }, + openai_compatible_raw: { + name: "OpenAI-Compatible Raw", + subtitle: "Use the exact base URL, no /v1 is appended", + iconKey: "custom", + }, openrouter: { name: "OpenRouter", subtitle: "OpenRouter",