mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-18 16:13:19 +00:00
refactor: rename the application package to docsgpt
The backend import package is now docsgpt, the name it will carry on PyPI; application was far too generic to install into anyone's site-packages. git mv plus a mechanical rewrite of every import, dotted string and path reference: 734 Python files, the compose files, Dockerfile, workflows, docs, setup scripts, devcontainer, k8s manifests, vscode config, pytest and coverage config, .gitignore. Behaviour is unchanged. Kept for one release: - A top-level application package whose meta-path finder resolves application.x.y to the already-imported docsgpt.x.y object, so old imports and entry points (celery -A application.app.celery, uvicorn application.asgi:asgi_app) keep working with a FutureWarning. - Celery registers every application.* task name as an alias of its docsgpt.* task on start-up, so messages queued by the previous release still run. The redbeat key prefix moves to redbeat:docsgpt:v2: so schedule entries the previous release wrote are left unread instead of firing twice. The backend image builds from the repository root (docker build -f docsgpt/Dockerfile .) so it can ship the alias package; a root .dockerignore allow-lists docsgpt/ and application/ and keeps caches, local data, .env files, the sample index files and the Dockerfile out. Compose and the image workflows point at the new context.
This commit is contained in:
1 parent
3e7f1f91b4
commit
574f96341e
984 files changed
+9847
-9682
No files matched your search
@@ -18,8 +18,8 @@ from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from application.core.model_registry import ModelRegistry
|
||||
from application.core.model_utils import (
|
||||
from docsgpt.core.model_registry import ModelRegistry
|
||||
from docsgpt.core.model_utils import (
|
||||
get_all_available_models,
|
||||
get_api_key_for_model,
|
||||
get_base_url_for_model,
|
||||
@@ -28,7 +28,7 @@ from application.core.model_utils import (
|
||||
get_token_limit,
|
||||
validate_model_id,
|
||||
)
|
||||
from application.storage.db.repositories.user_custom_models import (
|
||||
from docsgpt.storage.db.repositories.user_custom_models import (
|
||||
UserCustomModelsRepository,
|
||||
)
|
||||
|
||||
@@ -61,7 +61,7 @@ def _make_settings(**overrides):
|
||||
s.MODELS_CONFIG_DIR = None
|
||||
s.DEFAULT_LLM_TOKEN_LIMIT = 128000
|
||||
# Concrete strings — module-level imports under patched settings
|
||||
# (e.g. application.api.user.base's storage init) fail with
|
||||
# (e.g. docsgpt.api.user.base's storage init) fail with
|
||||
# MagicMock attribute values.
|
||||
s.STORAGE_TYPE = "local"
|
||||
for k, v in overrides.items():
|
||||
@@ -110,8 +110,8 @@ class TestHelpersThreadUserId:
|
||||
):
|
||||
model_uuid, user_id = byom_model
|
||||
s = _make_settings()
|
||||
with patch("application.core.settings.settings", s), patch(
|
||||
"application.storage.db.session.db_readonly",
|
||||
with patch("docsgpt.core.settings.settings", s), patch(
|
||||
"docsgpt.storage.db.session.db_readonly",
|
||||
lambda: _yield(pg_conn),
|
||||
):
|
||||
ModelRegistry()
|
||||
@@ -122,8 +122,8 @@ class TestHelpersThreadUserId:
|
||||
):
|
||||
model_uuid, user_id = byom_model
|
||||
s = _make_settings()
|
||||
with patch("application.core.settings.settings", s), patch(
|
||||
"application.storage.db.session.db_readonly",
|
||||
with patch("docsgpt.core.settings.settings", s), patch(
|
||||
"docsgpt.storage.db.session.db_readonly",
|
||||
lambda: _yield(pg_conn),
|
||||
):
|
||||
ModelRegistry()
|
||||
@@ -134,8 +134,8 @@ class TestHelpersThreadUserId:
|
||||
):
|
||||
model_uuid, user_id = byom_model
|
||||
s = _make_settings()
|
||||
with patch("application.core.settings.settings", s), patch(
|
||||
"application.storage.db.session.db_readonly",
|
||||
with patch("docsgpt.core.settings.settings", s), patch(
|
||||
"docsgpt.storage.db.session.db_readonly",
|
||||
lambda: _yield(pg_conn),
|
||||
):
|
||||
ModelRegistry()
|
||||
@@ -150,8 +150,8 @@ class TestHelpersThreadUserId:
|
||||
):
|
||||
model_uuid, user_id = byom_model
|
||||
s = _make_settings()
|
||||
with patch("application.core.settings.settings", s), patch(
|
||||
"application.storage.db.session.db_readonly",
|
||||
with patch("docsgpt.core.settings.settings", s), patch(
|
||||
"docsgpt.storage.db.session.db_readonly",
|
||||
lambda: _yield(pg_conn),
|
||||
):
|
||||
ModelRegistry()
|
||||
@@ -165,8 +165,8 @@ class TestHelpersThreadUserId:
|
||||
):
|
||||
model_uuid, user_id = byom_model
|
||||
s = _make_settings()
|
||||
with patch("application.core.settings.settings", s), patch(
|
||||
"application.storage.db.session.db_readonly",
|
||||
with patch("docsgpt.core.settings.settings", s), patch(
|
||||
"docsgpt.storage.db.session.db_readonly",
|
||||
lambda: _yield(pg_conn),
|
||||
):
|
||||
ModelRegistry()
|
||||
@@ -181,8 +181,8 @@ class TestHelpersThreadUserId:
|
||||
):
|
||||
model_uuid, user_id = byom_model
|
||||
s = _make_settings()
|
||||
with patch("application.core.settings.settings", s), patch(
|
||||
"application.storage.db.session.db_readonly",
|
||||
with patch("docsgpt.core.settings.settings", s), patch(
|
||||
"docsgpt.storage.db.session.db_readonly",
|
||||
lambda: _yield(pg_conn),
|
||||
):
|
||||
ModelRegistry()
|
||||
@@ -197,8 +197,8 @@ class TestHelpersThreadUserId:
|
||||
):
|
||||
model_uuid, user_id = byom_model
|
||||
s = _make_settings()
|
||||
with patch("application.core.settings.settings", s), patch(
|
||||
"application.storage.db.session.db_readonly",
|
||||
with patch("docsgpt.core.settings.settings", s), patch(
|
||||
"docsgpt.storage.db.session.db_readonly",
|
||||
lambda: _yield(pg_conn),
|
||||
):
|
||||
ModelRegistry()
|
||||
@@ -213,8 +213,8 @@ class TestHelpersThreadUserId:
|
||||
):
|
||||
model_uuid, user_id = byom_model
|
||||
s = _make_settings()
|
||||
with patch("application.core.settings.settings", s), patch(
|
||||
"application.storage.db.session.db_readonly",
|
||||
with patch("docsgpt.core.settings.settings", s), patch(
|
||||
"docsgpt.storage.db.session.db_readonly",
|
||||
lambda: _yield(pg_conn),
|
||||
):
|
||||
ModelRegistry()
|
||||
@@ -238,7 +238,7 @@ class TestSecurityNoCredLeakOnUndecryptableBYOM:
|
||||
def test_undecryptable_record_skipped_in_registry(
|
||||
self, pg_conn, monkeypatch
|
||||
):
|
||||
from application.storage.db.repositories.user_custom_models import (
|
||||
from docsgpt.storage.db.repositories.user_custom_models import (
|
||||
UserCustomModelsRepository,
|
||||
)
|
||||
|
||||
@@ -260,8 +260,8 @@ class TestSecurityNoCredLeakOnUndecryptableBYOM:
|
||||
)
|
||||
|
||||
s = _make_settings()
|
||||
with patch("application.core.settings.settings", s), patch(
|
||||
"application.storage.db.session.db_readonly",
|
||||
with patch("docsgpt.core.settings.settings", s), patch(
|
||||
"docsgpt.storage.db.session.db_readonly",
|
||||
lambda: _yield(pg_conn),
|
||||
):
|
||||
ModelRegistry()
|
||||
@@ -276,13 +276,13 @@ class TestSecurityNoCredLeakOnUndecryptableBYOM:
|
||||
def test_llmcreator_refuses_user_model_with_no_api_key(self, pg_conn):
|
||||
"""Belt-and-braces: even if a source=user AvailableModel slipped
|
||||
into the registry without an api_key, LLMCreator must refuse."""
|
||||
from application.core.model_settings import (
|
||||
from docsgpt.core.model_settings import (
|
||||
AvailableModel,
|
||||
ModelProvider,
|
||||
)
|
||||
|
||||
s = _make_settings()
|
||||
with patch("application.core.settings.settings", s):
|
||||
with patch("docsgpt.core.settings.settings", s):
|
||||
reg = ModelRegistry()
|
||||
# Inject a malformed BYOM record directly to bypass the
|
||||
# registry-level guard tested above.
|
||||
@@ -303,7 +303,7 @@ class TestSecurityNoCredLeakOnUndecryptableBYOM:
|
||||
},
|
||||
)
|
||||
|
||||
from application.llm.llm_creator import LLMCreator
|
||||
from docsgpt.llm.llm_creator import LLMCreator
|
||||
|
||||
with pytest.raises(ValueError, match="no usable API key"):
|
||||
LLMCreator.create_llm(
|
||||
@@ -323,13 +323,13 @@ class TestSecurityDispatchSSRFGuard:
|
||||
guard or were inserted via direct DB edits."""
|
||||
|
||||
def test_dispatch_rejects_user_model_with_unsafe_base_url(self, pg_conn):
|
||||
from application.core.model_settings import (
|
||||
from docsgpt.core.model_settings import (
|
||||
AvailableModel,
|
||||
ModelProvider,
|
||||
)
|
||||
|
||||
s = _make_settings()
|
||||
with patch("application.core.settings.settings", s):
|
||||
with patch("docsgpt.core.settings.settings", s):
|
||||
reg = ModelRegistry()
|
||||
uuid = "0b7e0f4c-1234-5678-9abc-deadbeef0099"
|
||||
_seed_user_layer(
|
||||
@@ -348,7 +348,7 @@ class TestSecurityDispatchSSRFGuard:
|
||||
},
|
||||
)
|
||||
|
||||
from application.llm.llm_creator import LLMCreator
|
||||
from docsgpt.llm.llm_creator import LLMCreator
|
||||
|
||||
with pytest.raises(ValueError, match="Refusing to dispatch"):
|
||||
LLMCreator.create_llm(
|
||||
@@ -361,7 +361,7 @@ class TestSecurityDispatchSSRFGuard:
|
||||
|
||||
def test_dispatch_allows_user_model_with_safe_base_url(self, pg_conn):
|
||||
"""The SSRF guard must not break the happy path."""
|
||||
from application.core.model_settings import (
|
||||
from docsgpt.core.model_settings import (
|
||||
AvailableModel,
|
||||
ModelProvider,
|
||||
)
|
||||
@@ -374,8 +374,8 @@ class TestSecurityDispatchSSRFGuard:
|
||||
captured["base_url"] = kw.get("base_url")
|
||||
|
||||
s = _make_settings()
|
||||
with patch("application.core.settings.settings", s), patch(
|
||||
"application.security.safe_url.socket.getaddrinfo"
|
||||
with patch("docsgpt.core.settings.settings", s), patch(
|
||||
"docsgpt.security.safe_url.socket.getaddrinfo"
|
||||
) as gai:
|
||||
gai.return_value = [(None, None, None, None, ("104.18.0.1", 0))]
|
||||
reg = ModelRegistry()
|
||||
@@ -396,8 +396,8 @@ class TestSecurityDispatchSSRFGuard:
|
||||
},
|
||||
)
|
||||
|
||||
from application.llm.llm_creator import LLMCreator
|
||||
from application.llm.providers import PROVIDERS_BY_NAME
|
||||
from docsgpt.llm.llm_creator import LLMCreator
|
||||
from docsgpt.llm.providers import PROVIDERS_BY_NAME
|
||||
|
||||
with patch.object(
|
||||
PROVIDERS_BY_NAME["openai_compatible"], "llm_class", _FakeLLM
|
||||
@@ -423,11 +423,11 @@ class TestSecurityDispatchSSRFGuard:
|
||||
rebinding window — the pinned client is what does."""
|
||||
import httpx
|
||||
|
||||
from application.core.model_settings import (
|
||||
from docsgpt.core.model_settings import (
|
||||
AvailableModel,
|
||||
ModelProvider,
|
||||
)
|
||||
from application.security.safe_url import _PinnedHTTPSTransport
|
||||
from docsgpt.security.safe_url import _PinnedHTTPSTransport
|
||||
|
||||
captured: dict = {}
|
||||
|
||||
@@ -436,8 +436,8 @@ class TestSecurityDispatchSSRFGuard:
|
||||
captured["http_client"] = kw.get("http_client")
|
||||
|
||||
s = _make_settings()
|
||||
with patch("application.core.settings.settings", s), patch(
|
||||
"application.security.safe_url.socket.getaddrinfo"
|
||||
with patch("docsgpt.core.settings.settings", s), patch(
|
||||
"docsgpt.security.safe_url.socket.getaddrinfo"
|
||||
) as gai:
|
||||
gai.return_value = [(None, None, None, None, ("104.18.0.1", 0))]
|
||||
reg = ModelRegistry()
|
||||
@@ -458,8 +458,8 @@ class TestSecurityDispatchSSRFGuard:
|
||||
},
|
||||
)
|
||||
|
||||
from application.llm.llm_creator import LLMCreator
|
||||
from application.llm.providers import PROVIDERS_BY_NAME
|
||||
from docsgpt.llm.llm_creator import LLMCreator
|
||||
from docsgpt.llm.providers import PROVIDERS_BY_NAME
|
||||
|
||||
with patch.object(
|
||||
PROVIDERS_BY_NAME["openai_compatible"], "llm_class", _FakeLLM
|
||||
@@ -491,7 +491,7 @@ class TestSecurityDispatchSSRFGuard:
|
||||
their endpoints are operator-trusted. Skipping avoids
|
||||
unnecessary DNS lookups and keeps the SDK's default httpx
|
||||
client behavior unchanged for the common path."""
|
||||
from application.core.model_settings import (
|
||||
from docsgpt.core.model_settings import (
|
||||
AvailableModel,
|
||||
ModelProvider,
|
||||
)
|
||||
@@ -503,7 +503,7 @@ class TestSecurityDispatchSSRFGuard:
|
||||
captured["http_client"] = kw.get("http_client")
|
||||
|
||||
s = _make_settings()
|
||||
with patch("application.core.settings.settings", s):
|
||||
with patch("docsgpt.core.settings.settings", s):
|
||||
reg = ModelRegistry()
|
||||
uuid = "0b7e0f4c-1234-5678-9abc-deadbeef0102"
|
||||
reg._builtin_models = {
|
||||
@@ -517,8 +517,8 @@ class TestSecurityDispatchSSRFGuard:
|
||||
)
|
||||
}
|
||||
|
||||
from application.llm.llm_creator import LLMCreator
|
||||
from application.llm.providers import PROVIDERS_BY_NAME
|
||||
from docsgpt.llm.llm_creator import LLMCreator
|
||||
from docsgpt.llm.providers import PROVIDERS_BY_NAME
|
||||
|
||||
with patch.object(
|
||||
PROVIDERS_BY_NAME["openai_compatible"], "llm_class", _FakeLLM
|
||||
@@ -544,10 +544,10 @@ class TestSharedAgentResolvesOwnerBYOM:
|
||||
against the *owner's* per-user layer, not the caller's."""
|
||||
|
||||
def test_owner_byom_resolves_for_caller_request(self, pg_conn):
|
||||
from application.api.answer.services.stream_processor import (
|
||||
from docsgpt.api.answer.services.stream_processor import (
|
||||
StreamProcessor,
|
||||
)
|
||||
from application.storage.db.repositories.user_custom_models import (
|
||||
from docsgpt.storage.db.repositories.user_custom_models import (
|
||||
UserCustomModelsRepository,
|
||||
)
|
||||
|
||||
@@ -562,8 +562,8 @@ class TestSharedAgentResolvesOwnerBYOM:
|
||||
)
|
||||
|
||||
s = _make_settings()
|
||||
with patch("application.core.settings.settings", s), patch(
|
||||
"application.storage.db.session.db_readonly",
|
||||
with patch("docsgpt.core.settings.settings", s), patch(
|
||||
"docsgpt.storage.db.session.db_readonly",
|
||||
lambda: _yield(pg_conn),
|
||||
):
|
||||
ModelRegistry()
|
||||
@@ -588,9 +588,9 @@ class TestSharedAgentResolvesOwnerBYOM:
|
||||
the rephrase LLM gets the owner's api_key/base_url and dispatches
|
||||
the upstream model name (e.g. ``mistral-large-latest``) — not the
|
||||
registry UUID — when called with chat history + active docs."""
|
||||
from application.llm.providers import PROVIDERS_BY_NAME
|
||||
from application.retriever.classic_rag import ClassicRAG
|
||||
from application.storage.db.repositories.user_custom_models import (
|
||||
from docsgpt.llm.providers import PROVIDERS_BY_NAME
|
||||
from docsgpt.retriever.classic_rag import ClassicRAG
|
||||
from docsgpt.storage.db.repositories.user_custom_models import (
|
||||
UserCustomModelsRepository,
|
||||
)
|
||||
|
||||
@@ -623,8 +623,8 @@ class TestSharedAgentResolvesOwnerBYOM:
|
||||
)
|
||||
|
||||
s = _make_settings(API_KEY="instance-secret")
|
||||
with patch("application.core.settings.settings", s), patch(
|
||||
"application.storage.db.session.db_readonly",
|
||||
with patch("docsgpt.core.settings.settings", s), patch(
|
||||
"docsgpt.storage.db.session.db_readonly",
|
||||
lambda: _yield(pg_conn),
|
||||
):
|
||||
ModelRegistry()
|
||||
@@ -654,10 +654,10 @@ class TestSharedAgentResolvesOwnerBYOM:
|
||||
agent record so _validate_and_set_model can reach the owner's
|
||||
BYOM layer. Without this, shared-agent BYOM defaults silently
|
||||
fall back to the system default for any non-owner caller."""
|
||||
from application.api.answer.services.stream_processor import (
|
||||
from docsgpt.api.answer.services.stream_processor import (
|
||||
StreamProcessor,
|
||||
)
|
||||
from application.storage.db.repositories.user_custom_models import (
|
||||
from docsgpt.storage.db.repositories.user_custom_models import (
|
||||
UserCustomModelsRepository,
|
||||
)
|
||||
|
||||
@@ -671,8 +671,8 @@ class TestSharedAgentResolvesOwnerBYOM:
|
||||
)
|
||||
|
||||
s = _make_settings()
|
||||
with patch("application.core.settings.settings", s), patch(
|
||||
"application.storage.db.session.db_readonly",
|
||||
with patch("docsgpt.core.settings.settings", s), patch(
|
||||
"docsgpt.storage.db.session.db_readonly",
|
||||
lambda: _yield(pg_conn),
|
||||
):
|
||||
ModelRegistry()
|
||||
@@ -722,10 +722,10 @@ class TestCompressionThresholdHonorsByomContextWindow:
|
||||
BYOM context windows are honored (not the default 128k limit)."""
|
||||
|
||||
def test_orchestrator_passes_user_id_to_should_compress(self, monkeypatch):
|
||||
from application.api.answer.services.compression.orchestrator import (
|
||||
from docsgpt.api.answer.services.compression.orchestrator import (
|
||||
CompressionOrchestrator,
|
||||
)
|
||||
from application.api.answer.services.compression.threshold_checker import (
|
||||
from docsgpt.api.answer.services.compression.threshold_checker import (
|
||||
CompressionThresholdChecker,
|
||||
)
|
||||
|
||||
@@ -770,9 +770,9 @@ class TestContinuationPreservesByomScope:
|
||||
"""``resume_from_tool_actions`` reads ``model_user_id`` from the
|
||||
saved ``agent_config`` and forwards it to LLMCreator so the
|
||||
resumed dispatch resolves the owner's BYOM."""
|
||||
from application.api.answer.services import stream_processor as sp_mod
|
||||
from application.llm import llm_creator as llm_creator_mod
|
||||
from application.llm.handlers import handler_creator as handler_mod
|
||||
from docsgpt.api.answer.services import stream_processor as sp_mod
|
||||
from docsgpt.llm import llm_creator as llm_creator_mod
|
||||
from docsgpt.llm.handlers import handler_creator as handler_mod
|
||||
|
||||
captured: dict = {}
|
||||
|
||||
@@ -813,14 +813,14 @@ class TestContinuationPreservesByomScope:
|
||||
# ContinuationService is imported lazily inside the method —
|
||||
# patch the source module so the lookup at call time resolves
|
||||
# to our mock.
|
||||
from application.api.answer.services import (
|
||||
from docsgpt.api.answer.services import (
|
||||
continuation_service as cont_mod,
|
||||
)
|
||||
|
||||
monkeypatch.setattr(
|
||||
cont_mod, "ContinuationService", lambda: cont_service
|
||||
)
|
||||
from application.agents import tool_executor as te_mod
|
||||
from docsgpt.agents import tool_executor as te_mod
|
||||
|
||||
monkeypatch.setattr(
|
||||
te_mod, "ToolExecutor", lambda **kw: MagicMock(client_tools=None)
|
||||
@@ -828,7 +828,7 @@ class TestContinuationPreservesByomScope:
|
||||
|
||||
# Stub AgentCreator.create_agent so we don't need a full agent
|
||||
# tree — we're only exercising the LLMCreator forwarding.
|
||||
from application.agents import agent_creator as ac_mod
|
||||
from docsgpt.agents import agent_creator as ac_mod
|
||||
|
||||
monkeypatch.setattr(
|
||||
ac_mod.AgentCreator, "create_agent", lambda *a, **kw: MagicMock()
|
||||
@@ -867,7 +867,7 @@ class TestContinuationPreservesByomScope:
|
||||
to the continuation store."""
|
||||
import inspect
|
||||
|
||||
from application.api.answer.routes.base import BaseAnswerResource
|
||||
from docsgpt.api.answer.routes.base import BaseAnswerResource
|
||||
|
||||
# complete_stream is the only callsite that constructs the
|
||||
# save dict. Confirm the parameter is in scope and the key is
|
||||
@@ -891,7 +891,7 @@ class TestStreamProcessorForwardsByomScopeToBudgetCalls:
|
||||
sized against the default 128k, overfilling the upstream provider."""
|
||||
|
||||
def _make_processor(self, model_user_id="owner", initial_user_id="caller"):
|
||||
from application.api.answer.services.stream_processor import (
|
||||
from docsgpt.api.answer.services.stream_processor import (
|
||||
StreamProcessor,
|
||||
)
|
||||
|
||||
@@ -913,7 +913,7 @@ class TestStreamProcessorForwardsByomScopeToBudgetCalls:
|
||||
def test_configure_retriever_passes_model_user_id_to_doc_budget(
|
||||
self, monkeypatch
|
||||
):
|
||||
from application.api.answer.services import stream_processor as sp_mod
|
||||
from docsgpt.api.answer.services import stream_processor as sp_mod
|
||||
|
||||
captured: dict = {}
|
||||
|
||||
@@ -939,7 +939,7 @@ class TestStreamProcessorForwardsByomScopeToBudgetCalls:
|
||||
def test_load_conversation_history_passes_model_user_id_to_trim(
|
||||
self, monkeypatch
|
||||
):
|
||||
from application.api.answer.services import stream_processor as sp_mod
|
||||
from docsgpt.api.answer.services import stream_processor as sp_mod
|
||||
|
||||
captured: dict = {}
|
||||
|
||||
@@ -1001,7 +1001,7 @@ class TestBaseAgentTokenLimitUsesModelUserId:
|
||||
owner-BYOM token-limit checks size against the caller's layer."""
|
||||
|
||||
def _build_agent(self, model_user_id="owner", caller="caller"):
|
||||
from application.agents.classic_agent import ClassicAgent
|
||||
from docsgpt.agents.classic_agent import ClassicAgent
|
||||
|
||||
# Stub LLM/handler/executor — we only exercise the get_token_limit
|
||||
# callsites in BaseAgent.
|
||||
@@ -1025,7 +1025,7 @@ class TestBaseAgentTokenLimitUsesModelUserId:
|
||||
return 8000
|
||||
|
||||
monkeypatch.setattr(
|
||||
"application.core.model_utils.get_token_limit", _fake
|
||||
"docsgpt.core.model_utils.get_token_limit", _fake
|
||||
)
|
||||
|
||||
agent = self._build_agent(model_user_id="owner", caller="caller")
|
||||
@@ -1041,7 +1041,7 @@ class TestBaseAgentTokenLimitUsesModelUserId:
|
||||
return 8000
|
||||
|
||||
monkeypatch.setattr(
|
||||
"application.core.model_utils.get_token_limit", _fake
|
||||
"docsgpt.core.model_utils.get_token_limit", _fake
|
||||
)
|
||||
|
||||
agent = self._build_agent(model_user_id="owner", caller="caller")
|
||||
@@ -1057,7 +1057,7 @@ class TestBaseAgentTokenLimitUsesModelUserId:
|
||||
return 8000
|
||||
|
||||
monkeypatch.setattr(
|
||||
"application.core.model_utils.get_token_limit", _fake
|
||||
"docsgpt.core.model_utils.get_token_limit", _fake
|
||||
)
|
||||
|
||||
agent = self._build_agent(model_user_id="owner", caller="caller")
|
||||
@@ -1078,7 +1078,7 @@ class TestBaseAgentTokenLimitUsesModelUserId:
|
||||
return 8000
|
||||
|
||||
monkeypatch.setattr(
|
||||
"application.core.model_utils.get_token_limit", _fake
|
||||
"docsgpt.core.model_utils.get_token_limit", _fake
|
||||
)
|
||||
|
||||
agent = self._build_agent(model_user_id=None, caller="worker-owner")
|
||||
@@ -1099,7 +1099,7 @@ class TestNonAgentCallSitesUseUpstreamId:
|
||||
def test_handler_tool_loop_uses_upstream_id(self):
|
||||
"""``handlers.base.handle_response`` re-calls the LLM after a
|
||||
tool invocation. The model arg must be the upstream name."""
|
||||
from application.llm.handlers.base import LLMHandler
|
||||
from docsgpt.llm.handlers.base import LLMHandler
|
||||
|
||||
captured: dict = {}
|
||||
|
||||
@@ -1143,7 +1143,7 @@ class TestNonAgentCallSitesUseUpstreamId:
|
||||
"""``conversation_service.save_conversation`` calls
|
||||
``llm.gen(model=model_id, ...)`` to summarize. ``model_id`` is
|
||||
the registry UUID; must use ``llm.model_id`` instead."""
|
||||
from application.api.answer.services.conversation_service import (
|
||||
from docsgpt.api.answer.services.conversation_service import (
|
||||
ConversationService,
|
||||
)
|
||||
|
||||
@@ -1180,7 +1180,7 @@ class TestNonAgentCallSitesUseUpstreamId:
|
||||
from inspect import getsourcefile
|
||||
from pathlib import Path
|
||||
|
||||
from application.api.answer.services.compression.service import (
|
||||
from docsgpt.api.answer.services.compression.service import (
|
||||
CompressionService,
|
||||
)
|
||||
|
||||
@@ -1194,7 +1194,7 @@ class TestNonAgentCallSitesUseUpstreamId:
|
||||
from inspect import getsourcefile
|
||||
from pathlib import Path
|
||||
|
||||
from application.retriever.classic_rag import ClassicRAG
|
||||
from docsgpt.retriever.classic_rag import ClassicRAG
|
||||
|
||||
text = Path(getsourcefile(ClassicRAG)).read_text()
|
||||
assert "getattr(self.llm" in text
|
||||
@@ -1235,20 +1235,20 @@ class TestAgentSendsUpstreamModelId:
|
||||
return None
|
||||
|
||||
s = _make_settings()
|
||||
with patch("application.core.settings.settings", s), patch(
|
||||
"application.storage.db.session.db_readonly",
|
||||
with patch("docsgpt.core.settings.settings", s), patch(
|
||||
"docsgpt.storage.db.session.db_readonly",
|
||||
lambda: _yield(pg_conn),
|
||||
):
|
||||
ModelRegistry()
|
||||
|
||||
from application.llm.providers import PROVIDERS_BY_NAME
|
||||
from docsgpt.llm.providers import PROVIDERS_BY_NAME
|
||||
|
||||
with patch.object(
|
||||
PROVIDERS_BY_NAME["openai_compatible"],
|
||||
"llm_class",
|
||||
_FakeOpenAILLM,
|
||||
):
|
||||
from application.agents.classic_agent import ClassicAgent
|
||||
from docsgpt.agents.classic_agent import ClassicAgent
|
||||
|
||||
agent = ClassicAgent(
|
||||
endpoint="https://x",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Tests for ``application.core.db_uri``.
|
||||
"""Tests for ``docsgpt.core.db_uri``.
|
||||
|
||||
DocsGPT has two Postgres connection strings — ``POSTGRES_URI`` (consumed
|
||||
by SQLAlchemy) and ``PGVECTOR_CONNECTION_STRING`` (consumed by
|
||||
@@ -8,7 +8,7 @@ and libpq rejects it. Each field has its own normalizer so operators
|
||||
can write whichever form feels natural and cross-pollination between
|
||||
the two fields is forgiven.
|
||||
|
||||
The normalizers live in ``application.core.db_uri`` as plain functions
|
||||
The normalizers live in ``docsgpt.core.db_uri`` as plain functions
|
||||
so these tests can exercise them directly without having to instantiate
|
||||
``Settings`` (which would pull in ``.env`` file side effects).
|
||||
"""
|
||||
@@ -17,7 +17,7 @@ from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from application.core.db_uri import (
|
||||
from docsgpt.core.db_uri import (
|
||||
normalize_pgvector_connection_string,
|
||||
normalize_postgres_uri,
|
||||
)
|
||||
|
||||
@@ -4,9 +4,9 @@ from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
from application.core import shutdown
|
||||
from application.core.settings import settings
|
||||
from application.gunicorn_worker import (
|
||||
from docsgpt.core import shutdown
|
||||
from docsgpt.core.settings import settings
|
||||
from docsgpt.gunicorn_worker import (
|
||||
BoundedDrainUvicornWorker,
|
||||
_ShutdownAwareServer,
|
||||
)
|
||||
|
||||
@@ -6,8 +6,8 @@ import logging
|
||||
|
||||
import pytest
|
||||
|
||||
from application.core import log_context
|
||||
from application.core.logging_config import _ContextFilter
|
||||
from docsgpt.core import log_context
|
||||
from docsgpt.core.logging_config import _ContextFilter
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
@@ -129,7 +129,7 @@ class TestFilterWiringEndToEnd:
|
||||
"""
|
||||
|
||||
def test_propagated_record_gets_stamped(self):
|
||||
from application.core.logging_config import _install_context_filter
|
||||
from docsgpt.core.logging_config import _install_context_filter
|
||||
|
||||
captured: list[logging.LogRecord] = []
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Tests for setup_logging — in particular the OTEL log-handler hand-off.
|
||||
|
||||
`opentelemetry-instrument` attaches an OTEL `LoggingHandler` to the root
|
||||
logger before our module-level `setup_logging()` runs in `application/app.py`.
|
||||
logger before our module-level `setup_logging()` runs in `docsgpt/app.py`.
|
||||
The default `dictConfig` call replaces `root.handlers`, which would silently
|
||||
drop the OTEL handler. setup_logging snapshots and re-attaches OTEL handlers
|
||||
when OTLP log export is enabled.
|
||||
@@ -17,7 +17,7 @@ from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
||||
from application.core.logging_config import setup_logging
|
||||
from docsgpt.core.logging_config import setup_logging
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
@@ -125,7 +125,7 @@ class TestAlembicDoesNotSilenceApplicationLoggers:
|
||||
import ast
|
||||
import pathlib
|
||||
|
||||
source = pathlib.Path("application/alembic/env.py").read_text()
|
||||
source = pathlib.Path("docsgpt/alembic/env.py").read_text()
|
||||
calls = [
|
||||
node
|
||||
for node in ast.walk(ast.parse(source))
|
||||
@@ -152,12 +152,12 @@ class TestAlembicDoesNotSilenceApplicationLoggers:
|
||||
probe.write_text(
|
||||
"import logging\n"
|
||||
"from logging.config import fileConfig\n"
|
||||
"log = logging.getLogger('application.api.answer.routes.stream')\n"
|
||||
"fileConfig('application/alembic.ini', disable_existing_loggers=True)\n"
|
||||
"kept = logging.getLogger('application.api.answer.routes.stream')\n"
|
||||
"log = logging.getLogger('docsgpt.api.answer.routes.stream')\n"
|
||||
"fileConfig('docsgpt/alembic.ini', disable_existing_loggers=True)\n"
|
||||
"kept = logging.getLogger('docsgpt.api.answer.routes.stream')\n"
|
||||
"print('default_disables', kept.disabled)\n"
|
||||
"kept.disabled = False\n"
|
||||
"fileConfig('application/alembic.ini', disable_existing_loggers=False)\n"
|
||||
"fileConfig('docsgpt/alembic.ini', disable_existing_loggers=False)\n"
|
||||
"print('flag_preserves', not kept.disabled)\n"
|
||||
)
|
||||
out = subprocess.run(
|
||||
@@ -184,7 +184,7 @@ class TestMigrationsDoNotClobberAppLogging:
|
||||
def test_bootstrap_tells_env_py_to_leave_logging_alone(self, monkeypatch):
|
||||
from logging.config import fileConfig
|
||||
|
||||
from application.storage.db import bootstrap
|
||||
from docsgpt.storage.db import bootstrap
|
||||
|
||||
# Skip the best-effort revision precheck; it needs a live DB. It is
|
||||
# wrapped in try/except, so raising here lands on the "upgrade anyway"
|
||||
@@ -217,13 +217,13 @@ class TestMigrationsDoNotClobberAppLogging:
|
||||
assert seen["configure_logger"] is False
|
||||
assert root.level == before_level
|
||||
assert marker in root.handlers
|
||||
assert logging.getLogger("application.probe").isEnabledFor(logging.INFO)
|
||||
assert logging.getLogger("docsgpt.probe").isEnabledFor(logging.INFO)
|
||||
|
||||
def test_env_py_honours_the_opt_out(self):
|
||||
"""The other half of the contract lives in env.py's module-level guard."""
|
||||
env_py = (
|
||||
pathlib.Path(__file__).resolve().parents[2]
|
||||
/ "application"
|
||||
/ "docsgpt"
|
||||
/ "alembic"
|
||||
/ "env.py"
|
||||
)
|
||||
|
||||
@@ -16,8 +16,8 @@ from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from application.core.model_registry import ModelRegistry
|
||||
from application.core.model_yaml import (
|
||||
from docsgpt.core.model_registry import ModelRegistry
|
||||
from docsgpt.core.model_yaml import (
|
||||
BUILTIN_MODELS_DIR,
|
||||
load_model_yamls,
|
||||
)
|
||||
@@ -167,7 +167,7 @@ class TestYAMLLoader:
|
||||
class TestRegistryPermutations:
|
||||
def test_openai_only(self):
|
||||
s = _make_settings(OPENAI_API_KEY="sk-test", LLM_PROVIDER="openai")
|
||||
with patch("application.core.settings.settings", s):
|
||||
with patch("docsgpt.core.settings.settings", s):
|
||||
reg = ModelRegistry()
|
||||
ids = {m.id for m in reg.get_all_models()}
|
||||
assert ids == EXPECTED_IDS["openai"] | EXPECTED_IDS["docsgpt"]
|
||||
@@ -179,7 +179,7 @@ class TestRegistryPermutations:
|
||||
LLM_PROVIDER="openai",
|
||||
LLM_NAME="llama3,gemma",
|
||||
)
|
||||
with patch("application.core.settings.settings", s):
|
||||
with patch("docsgpt.core.settings.settings", s):
|
||||
reg = ModelRegistry()
|
||||
ids = {m.id for m in reg.get_all_models()}
|
||||
# Custom local endpoint suppresses both the openai catalog AND
|
||||
@@ -188,7 +188,7 @@ class TestRegistryPermutations:
|
||||
|
||||
def test_anthropic_only(self):
|
||||
s = _make_settings(ANTHROPIC_API_KEY="sk-ant")
|
||||
with patch("application.core.settings.settings", s):
|
||||
with patch("docsgpt.core.settings.settings", s):
|
||||
reg = ModelRegistry()
|
||||
ids = {m.id for m in reg.get_all_models()}
|
||||
assert ids == EXPECTED_IDS["anthropic"] | EXPECTED_IDS["docsgpt"]
|
||||
@@ -200,7 +200,7 @@ class TestRegistryPermutations:
|
||||
s = _make_settings(
|
||||
LLM_PROVIDER="anthropic", API_KEY="key", LLM_NAME="claude-haiku-4-5"
|
||||
)
|
||||
with patch("application.core.settings.settings", s):
|
||||
with patch("docsgpt.core.settings.settings", s):
|
||||
reg = ModelRegistry()
|
||||
anthropic_ids = {
|
||||
m.id for m in reg.get_all_models() if m.provider.value == "anthropic"
|
||||
@@ -209,42 +209,42 @@ class TestRegistryPermutations:
|
||||
|
||||
def test_google_only(self):
|
||||
s = _make_settings(GOOGLE_API_KEY="g-test")
|
||||
with patch("application.core.settings.settings", s):
|
||||
with patch("docsgpt.core.settings.settings", s):
|
||||
reg = ModelRegistry()
|
||||
ids = {m.id for m in reg.get_all_models()}
|
||||
assert ids == EXPECTED_IDS["google"] | EXPECTED_IDS["docsgpt"]
|
||||
|
||||
def test_groq_only(self):
|
||||
s = _make_settings(GROQ_API_KEY="g-test")
|
||||
with patch("application.core.settings.settings", s):
|
||||
with patch("docsgpt.core.settings.settings", s):
|
||||
reg = ModelRegistry()
|
||||
ids = {m.id for m in reg.get_all_models()}
|
||||
assert ids == EXPECTED_IDS["groq"] | EXPECTED_IDS["docsgpt"]
|
||||
|
||||
def test_openrouter_only(self):
|
||||
s = _make_settings(OPEN_ROUTER_API_KEY="or-test")
|
||||
with patch("application.core.settings.settings", s):
|
||||
with patch("docsgpt.core.settings.settings", s):
|
||||
reg = ModelRegistry()
|
||||
ids = {m.id for m in reg.get_all_models()}
|
||||
assert ids == EXPECTED_IDS["openrouter"] | EXPECTED_IDS["docsgpt"]
|
||||
|
||||
def test_novita_only(self):
|
||||
s = _make_settings(NOVITA_API_KEY="n-test")
|
||||
with patch("application.core.settings.settings", s):
|
||||
with patch("docsgpt.core.settings.settings", s):
|
||||
reg = ModelRegistry()
|
||||
ids = {m.id for m in reg.get_all_models()}
|
||||
assert ids == EXPECTED_IDS["novita"] | EXPECTED_IDS["docsgpt"]
|
||||
|
||||
def test_huggingface_only(self):
|
||||
s = _make_settings(HUGGINGFACE_API_KEY="hf-test")
|
||||
with patch("application.core.settings.settings", s):
|
||||
with patch("docsgpt.core.settings.settings", s):
|
||||
reg = ModelRegistry()
|
||||
ids = {m.id for m in reg.get_all_models()}
|
||||
assert ids == EXPECTED_IDS["huggingface"] | EXPECTED_IDS["docsgpt"]
|
||||
|
||||
def test_no_credentials_only_docsgpt(self):
|
||||
s = _make_settings()
|
||||
with patch("application.core.settings.settings", s):
|
||||
with patch("docsgpt.core.settings.settings", s):
|
||||
reg = ModelRegistry()
|
||||
ids = {m.id for m in reg.get_all_models()}
|
||||
assert ids == EXPECTED_IDS["docsgpt"]
|
||||
@@ -261,7 +261,7 @@ class TestRegistryPermutations:
|
||||
HUGGINGFACE_API_KEY="x",
|
||||
OPENAI_API_BASE="x",
|
||||
)
|
||||
with patch("application.core.settings.settings", s):
|
||||
with patch("docsgpt.core.settings.settings", s):
|
||||
reg = ModelRegistry()
|
||||
ids = {m.id for m in reg.get_all_models()}
|
||||
all_expected = set()
|
||||
@@ -279,13 +279,13 @@ class TestDefaultModelResolution:
|
||||
s = _make_settings(
|
||||
ANTHROPIC_API_KEY="sk-ant", LLM_NAME="claude-opus-4-7"
|
||||
)
|
||||
with patch("application.core.settings.settings", s):
|
||||
with patch("docsgpt.core.settings.settings", s):
|
||||
reg = ModelRegistry()
|
||||
assert reg.default_model_id == "claude-opus-4-7"
|
||||
|
||||
def test_falls_back_to_first_model_when_no_match(self):
|
||||
s = _make_settings()
|
||||
with patch("application.core.settings.settings", s):
|
||||
with patch("docsgpt.core.settings.settings", s):
|
||||
reg = ModelRegistry()
|
||||
assert reg.default_model_id is not None
|
||||
assert reg.default_model_id in reg.models
|
||||
@@ -298,7 +298,7 @@ class TestDefaultModelResolution:
|
||||
class TestUserIdForwardCompat:
|
||||
def test_lookup_methods_accept_user_id(self):
|
||||
s = _make_settings(OPENAI_API_KEY="sk-test")
|
||||
with patch("application.core.settings.settings", s):
|
||||
with patch("docsgpt.core.settings.settings", s):
|
||||
reg = ModelRegistry()
|
||||
# All lookup methods must accept user_id (currently ignored,
|
||||
# reserved for end-user BYOM).
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""Tests for application/core/model_settings.py.
|
||||
"""Tests for docsgpt/core/model_settings.py.
|
||||
|
||||
The provider-specific load logic that used to live in private
|
||||
``_add_<X>_models`` methods now lives in plugin classes under
|
||||
``application/llm/providers/`` and YAML catalogs under
|
||||
``application/core/models/``. End-to-end coverage of the registry +
|
||||
``docsgpt/llm/providers/`` and YAML catalogs under
|
||||
``docsgpt/core/models/``. End-to-end coverage of the registry +
|
||||
plugin pipeline is in ``tests/core/test_model_registry_yaml.py``.
|
||||
|
||||
This file covers the data classes (``AvailableModel``,
|
||||
@@ -15,7 +15,7 @@ from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from application.core.model_settings import (
|
||||
from docsgpt.core.model_settings import (
|
||||
AvailableModel,
|
||||
ModelCapabilities,
|
||||
ModelProvider,
|
||||
|
||||
@@ -2,7 +2,7 @@ from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from application.core.model_settings import (
|
||||
from docsgpt.core.model_settings import (
|
||||
AvailableModel,
|
||||
ModelCapabilities,
|
||||
ModelProvider,
|
||||
@@ -51,11 +51,11 @@ def _make_model(
|
||||
|
||||
class TestGetApiKeyForProvider:
|
||||
"""settings is lazily imported inside the function body, so we patch
|
||||
at application.core.settings.settings (the actual module attribute)."""
|
||||
at docsgpt.core.settings.settings (the actual module attribute)."""
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_openai_key(self):
|
||||
with patch("application.core.settings.settings") as mock_settings:
|
||||
with patch("docsgpt.core.settings.settings") as mock_settings:
|
||||
mock_settings.OPENAI_API_KEY = "sk-openai"
|
||||
mock_settings.API_KEY = "sk-fallback"
|
||||
mock_settings.OPEN_ROUTER_API_KEY = None
|
||||
@@ -65,94 +65,94 @@ class TestGetApiKeyForProvider:
|
||||
mock_settings.GROQ_API_KEY = None
|
||||
mock_settings.HUGGINGFACE_API_KEY = None
|
||||
|
||||
from application.core.model_utils import get_api_key_for_provider
|
||||
from docsgpt.core.model_utils import get_api_key_for_provider
|
||||
|
||||
assert get_api_key_for_provider("openai") == "sk-openai"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_anthropic_key(self):
|
||||
with patch("application.core.settings.settings") as mock_settings:
|
||||
with patch("docsgpt.core.settings.settings") as mock_settings:
|
||||
mock_settings.ANTHROPIC_API_KEY = "sk-anthropic"
|
||||
mock_settings.API_KEY = "sk-fallback"
|
||||
|
||||
from application.core.model_utils import get_api_key_for_provider
|
||||
from docsgpt.core.model_utils import get_api_key_for_provider
|
||||
|
||||
assert get_api_key_for_provider("anthropic") == "sk-anthropic"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_google_key(self):
|
||||
with patch("application.core.settings.settings") as mock_settings:
|
||||
with patch("docsgpt.core.settings.settings") as mock_settings:
|
||||
mock_settings.GOOGLE_API_KEY = "sk-google"
|
||||
mock_settings.API_KEY = "sk-fallback"
|
||||
|
||||
from application.core.model_utils import get_api_key_for_provider
|
||||
from docsgpt.core.model_utils import get_api_key_for_provider
|
||||
|
||||
assert get_api_key_for_provider("google") == "sk-google"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_groq_key(self):
|
||||
with patch("application.core.settings.settings") as mock_settings:
|
||||
with patch("docsgpt.core.settings.settings") as mock_settings:
|
||||
mock_settings.GROQ_API_KEY = "sk-groq"
|
||||
mock_settings.API_KEY = "sk-fallback"
|
||||
|
||||
from application.core.model_utils import get_api_key_for_provider
|
||||
from docsgpt.core.model_utils import get_api_key_for_provider
|
||||
|
||||
assert get_api_key_for_provider("groq") == "sk-groq"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_openrouter_key(self):
|
||||
with patch("application.core.settings.settings") as mock_settings:
|
||||
with patch("docsgpt.core.settings.settings") as mock_settings:
|
||||
mock_settings.OPEN_ROUTER_API_KEY = "sk-or"
|
||||
mock_settings.API_KEY = "sk-fallback"
|
||||
|
||||
from application.core.model_utils import get_api_key_for_provider
|
||||
from docsgpt.core.model_utils import get_api_key_for_provider
|
||||
|
||||
assert get_api_key_for_provider("openrouter") == "sk-or"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_novita_key(self):
|
||||
with patch("application.core.settings.settings") as mock_settings:
|
||||
with patch("docsgpt.core.settings.settings") as mock_settings:
|
||||
mock_settings.NOVITA_API_KEY = "sk-novita"
|
||||
mock_settings.API_KEY = "sk-fallback"
|
||||
|
||||
from application.core.model_utils import get_api_key_for_provider
|
||||
from docsgpt.core.model_utils import get_api_key_for_provider
|
||||
|
||||
assert get_api_key_for_provider("novita") == "sk-novita"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_huggingface_key(self):
|
||||
with patch("application.core.settings.settings") as mock_settings:
|
||||
with patch("docsgpt.core.settings.settings") as mock_settings:
|
||||
mock_settings.HUGGINGFACE_API_KEY = "hf-key"
|
||||
mock_settings.API_KEY = "sk-fallback"
|
||||
|
||||
from application.core.model_utils import get_api_key_for_provider
|
||||
from docsgpt.core.model_utils import get_api_key_for_provider
|
||||
|
||||
assert get_api_key_for_provider("huggingface") == "hf-key"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_docsgpt_returns_fallback(self):
|
||||
with patch("application.core.settings.settings") as mock_settings:
|
||||
with patch("docsgpt.core.settings.settings") as mock_settings:
|
||||
mock_settings.API_KEY = "sk-fallback"
|
||||
|
||||
from application.core.model_utils import get_api_key_for_provider
|
||||
from docsgpt.core.model_utils import get_api_key_for_provider
|
||||
|
||||
assert get_api_key_for_provider("docsgpt") == "sk-fallback"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_llama_cpp_returns_fallback(self):
|
||||
with patch("application.core.settings.settings") as mock_settings:
|
||||
with patch("docsgpt.core.settings.settings") as mock_settings:
|
||||
mock_settings.API_KEY = "sk-fallback"
|
||||
|
||||
from application.core.model_utils import get_api_key_for_provider
|
||||
from docsgpt.core.model_utils import get_api_key_for_provider
|
||||
|
||||
assert get_api_key_for_provider("llama.cpp") == "sk-fallback"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_unknown_provider_returns_fallback(self):
|
||||
with patch("application.core.settings.settings") as mock_settings:
|
||||
with patch("docsgpt.core.settings.settings") as mock_settings:
|
||||
mock_settings.API_KEY = "sk-fallback"
|
||||
|
||||
from application.core.model_utils import get_api_key_for_provider
|
||||
from docsgpt.core.model_utils import get_api_key_for_provider
|
||||
|
||||
assert get_api_key_for_provider("unknown_provider") == "sk-fallback"
|
||||
|
||||
@@ -161,7 +161,7 @@ class TestGetApiKeyForProvider:
|
||||
|
||||
class TestGetAllAvailableModels:
|
||||
@pytest.mark.unit
|
||||
@patch("application.core.model_utils.ModelRegistry.get_instance")
|
||||
@patch("docsgpt.core.model_utils.ModelRegistry.get_instance")
|
||||
def test_returns_enabled_models_as_dict(self, mock_get_instance):
|
||||
model_a = _make_model("model-a", display_name="Model A")
|
||||
model_b = _make_model("model-b", display_name="Model B")
|
||||
@@ -169,7 +169,7 @@ class TestGetAllAvailableModels:
|
||||
mock_registry.get_enabled_models.return_value = [model_a, model_b]
|
||||
mock_get_instance.return_value = mock_registry
|
||||
|
||||
from application.core.model_utils import get_all_available_models
|
||||
from docsgpt.core.model_utils import get_all_available_models
|
||||
|
||||
result = get_all_available_models()
|
||||
|
||||
@@ -179,13 +179,13 @@ class TestGetAllAvailableModels:
|
||||
assert result["model-b"]["display_name"] == "Model B"
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch("application.core.model_utils.ModelRegistry.get_instance")
|
||||
@patch("docsgpt.core.model_utils.ModelRegistry.get_instance")
|
||||
def test_empty_registry(self, mock_get_instance):
|
||||
mock_registry = MagicMock()
|
||||
mock_registry.get_enabled_models.return_value = []
|
||||
mock_get_instance.return_value = mock_registry
|
||||
|
||||
from application.core.model_utils import get_all_available_models
|
||||
from docsgpt.core.model_utils import get_all_available_models
|
||||
|
||||
assert get_all_available_models() == {}
|
||||
|
||||
@@ -195,24 +195,24 @@ class TestGetAllAvailableModels:
|
||||
|
||||
class TestValidateModelId:
|
||||
@pytest.mark.unit
|
||||
@patch("application.core.model_utils.ModelRegistry.get_instance")
|
||||
@patch("docsgpt.core.model_utils.ModelRegistry.get_instance")
|
||||
def test_exists(self, mock_get_instance):
|
||||
mock_registry = MagicMock()
|
||||
mock_registry.model_exists.return_value = True
|
||||
mock_get_instance.return_value = mock_registry
|
||||
|
||||
from application.core.model_utils import validate_model_id
|
||||
from docsgpt.core.model_utils import validate_model_id
|
||||
|
||||
assert validate_model_id("gpt-4") is True
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch("application.core.model_utils.ModelRegistry.get_instance")
|
||||
@patch("docsgpt.core.model_utils.ModelRegistry.get_instance")
|
||||
def test_not_exists(self, mock_get_instance):
|
||||
mock_registry = MagicMock()
|
||||
mock_registry.model_exists.return_value = False
|
||||
mock_get_instance.return_value = mock_registry
|
||||
|
||||
from application.core.model_utils import validate_model_id
|
||||
from docsgpt.core.model_utils import validate_model_id
|
||||
|
||||
assert validate_model_id("nonexistent") is False
|
||||
|
||||
@@ -222,7 +222,7 @@ class TestValidateModelId:
|
||||
|
||||
class TestGetModelCapabilities:
|
||||
@pytest.mark.unit
|
||||
@patch("application.core.model_utils.ModelRegistry.get_instance")
|
||||
@patch("docsgpt.core.model_utils.ModelRegistry.get_instance")
|
||||
def test_model_found(self, mock_get_instance):
|
||||
model = _make_model(
|
||||
"gpt-4",
|
||||
@@ -235,7 +235,7 @@ class TestGetModelCapabilities:
|
||||
mock_registry.get_model.return_value = model
|
||||
mock_get_instance.return_value = mock_registry
|
||||
|
||||
from application.core.model_utils import get_model_capabilities
|
||||
from docsgpt.core.model_utils import get_model_capabilities
|
||||
|
||||
caps = get_model_capabilities("gpt-4")
|
||||
|
||||
@@ -246,13 +246,13 @@ class TestGetModelCapabilities:
|
||||
assert caps["context_window"] == 8192
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch("application.core.model_utils.ModelRegistry.get_instance")
|
||||
@patch("docsgpt.core.model_utils.ModelRegistry.get_instance")
|
||||
def test_model_not_found(self, mock_get_instance):
|
||||
mock_registry = MagicMock()
|
||||
mock_registry.get_model.return_value = None
|
||||
mock_get_instance.return_value = mock_registry
|
||||
|
||||
from application.core.model_utils import get_model_capabilities
|
||||
from docsgpt.core.model_utils import get_model_capabilities
|
||||
|
||||
assert get_model_capabilities("nonexistent") is None
|
||||
|
||||
@@ -262,13 +262,13 @@ class TestGetModelCapabilities:
|
||||
|
||||
class TestGetDefaultModelId:
|
||||
@pytest.mark.unit
|
||||
@patch("application.core.model_utils.ModelRegistry.get_instance")
|
||||
@patch("docsgpt.core.model_utils.ModelRegistry.get_instance")
|
||||
def test_returns_default(self, mock_get_instance):
|
||||
mock_registry = MagicMock()
|
||||
mock_registry.default_model_id = "gpt-4"
|
||||
mock_get_instance.return_value = mock_registry
|
||||
|
||||
from application.core.model_utils import get_default_model_id
|
||||
from docsgpt.core.model_utils import get_default_model_id
|
||||
|
||||
assert get_default_model_id() == "gpt-4"
|
||||
|
||||
@@ -278,25 +278,25 @@ class TestGetDefaultModelId:
|
||||
|
||||
class TestGetProviderFromModelId:
|
||||
@pytest.mark.unit
|
||||
@patch("application.core.model_utils.ModelRegistry.get_instance")
|
||||
@patch("docsgpt.core.model_utils.ModelRegistry.get_instance")
|
||||
def test_model_found(self, mock_get_instance):
|
||||
model = _make_model("gpt-4", provider=ModelProvider.OPENAI)
|
||||
mock_registry = MagicMock()
|
||||
mock_registry.get_model.return_value = model
|
||||
mock_get_instance.return_value = mock_registry
|
||||
|
||||
from application.core.model_utils import get_provider_from_model_id
|
||||
from docsgpt.core.model_utils import get_provider_from_model_id
|
||||
|
||||
assert get_provider_from_model_id("gpt-4") == "openai"
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch("application.core.model_utils.ModelRegistry.get_instance")
|
||||
@patch("docsgpt.core.model_utils.ModelRegistry.get_instance")
|
||||
def test_model_not_found(self, mock_get_instance):
|
||||
mock_registry = MagicMock()
|
||||
mock_registry.get_model.return_value = None
|
||||
mock_get_instance.return_value = mock_registry
|
||||
|
||||
from application.core.model_utils import get_provider_from_model_id
|
||||
from docsgpt.core.model_utils import get_provider_from_model_id
|
||||
|
||||
assert get_provider_from_model_id("nonexistent") is None
|
||||
|
||||
@@ -306,28 +306,28 @@ class TestGetProviderFromModelId:
|
||||
|
||||
class TestGetTokenLimit:
|
||||
@pytest.mark.unit
|
||||
@patch("application.core.model_utils.ModelRegistry.get_instance")
|
||||
@patch("docsgpt.core.model_utils.ModelRegistry.get_instance")
|
||||
def test_model_found(self, mock_get_instance):
|
||||
model = _make_model("gpt-4", context_window=8192)
|
||||
mock_registry = MagicMock()
|
||||
mock_registry.get_model.return_value = model
|
||||
mock_get_instance.return_value = mock_registry
|
||||
|
||||
from application.core.model_utils import get_token_limit
|
||||
from docsgpt.core.model_utils import get_token_limit
|
||||
|
||||
assert get_token_limit("gpt-4") == 8192
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch("application.core.model_utils.ModelRegistry.get_instance")
|
||||
@patch("docsgpt.core.model_utils.ModelRegistry.get_instance")
|
||||
def test_model_not_found_returns_default(self, mock_get_instance):
|
||||
mock_registry = MagicMock()
|
||||
mock_registry.get_model.return_value = None
|
||||
mock_get_instance.return_value = mock_registry
|
||||
|
||||
with patch("application.core.settings.settings") as mock_settings:
|
||||
with patch("docsgpt.core.settings.settings") as mock_settings:
|
||||
mock_settings.DEFAULT_LLM_TOKEN_LIMIT = 128000
|
||||
|
||||
from application.core.model_utils import get_token_limit
|
||||
from docsgpt.core.model_utils import get_token_limit
|
||||
|
||||
assert get_token_limit("nonexistent") == 128000
|
||||
|
||||
@@ -337,36 +337,36 @@ class TestGetTokenLimit:
|
||||
|
||||
class TestGetBaseUrlForModel:
|
||||
@pytest.mark.unit
|
||||
@patch("application.core.model_utils.ModelRegistry.get_instance")
|
||||
@patch("docsgpt.core.model_utils.ModelRegistry.get_instance")
|
||||
def test_model_with_base_url(self, mock_get_instance):
|
||||
model = _make_model("custom-model", base_url="http://localhost:8080")
|
||||
mock_registry = MagicMock()
|
||||
mock_registry.get_model.return_value = model
|
||||
mock_get_instance.return_value = mock_registry
|
||||
|
||||
from application.core.model_utils import get_base_url_for_model
|
||||
from docsgpt.core.model_utils import get_base_url_for_model
|
||||
|
||||
assert get_base_url_for_model("custom-model") == "http://localhost:8080"
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch("application.core.model_utils.ModelRegistry.get_instance")
|
||||
@patch("docsgpt.core.model_utils.ModelRegistry.get_instance")
|
||||
def test_model_without_base_url(self, mock_get_instance):
|
||||
model = _make_model("gpt-4", base_url=None)
|
||||
mock_registry = MagicMock()
|
||||
mock_registry.get_model.return_value = model
|
||||
mock_get_instance.return_value = mock_registry
|
||||
|
||||
from application.core.model_utils import get_base_url_for_model
|
||||
from docsgpt.core.model_utils import get_base_url_for_model
|
||||
|
||||
assert get_base_url_for_model("gpt-4") is None
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch("application.core.model_utils.ModelRegistry.get_instance")
|
||||
@patch("docsgpt.core.model_utils.ModelRegistry.get_instance")
|
||||
def test_model_not_found(self, mock_get_instance):
|
||||
mock_registry = MagicMock()
|
||||
mock_registry.get_model.return_value = None
|
||||
mock_get_instance.return_value = mock_registry
|
||||
|
||||
from application.core.model_utils import get_base_url_for_model
|
||||
from docsgpt.core.model_utils import get_base_url_for_model
|
||||
|
||||
assert get_base_url_for_model("nonexistent") is None
|
||||
@@ -15,7 +15,7 @@ from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from application.core.model_registry import ModelRegistry
|
||||
from docsgpt.core.model_registry import ModelRegistry
|
||||
|
||||
|
||||
def _make_settings(**overrides):
|
||||
@@ -67,7 +67,7 @@ class TestOperatorAddsNewProvider:
|
||||
monkeypatch.setenv("FIREWORKS_API_KEY", "fw-key")
|
||||
|
||||
s = _make_settings(MODELS_CONFIG_DIR=str(tmp_path))
|
||||
with patch("application.core.settings.settings", s):
|
||||
with patch("docsgpt.core.settings.settings", s):
|
||||
reg = ModelRegistry()
|
||||
|
||||
m = reg.get_model("accounts/fireworks/models/llama-v3p3-70b-instruct")
|
||||
@@ -99,7 +99,7 @@ class TestOperatorExtendsExistingProvider:
|
||||
ANTHROPIC_API_KEY="sk-ant",
|
||||
MODELS_CONFIG_DIR=str(tmp_path),
|
||||
)
|
||||
with patch("application.core.settings.settings", s):
|
||||
with patch("docsgpt.core.settings.settings", s):
|
||||
reg = ModelRegistry()
|
||||
|
||||
# Built-in models still present
|
||||
@@ -137,7 +137,7 @@ class TestOperatorOverridesBuiltinCapabilities:
|
||||
MODELS_CONFIG_DIR=str(tmp_path),
|
||||
)
|
||||
with caplog.at_level(logging.WARNING):
|
||||
with patch("application.core.settings.settings", s):
|
||||
with patch("docsgpt.core.settings.settings", s):
|
||||
reg = ModelRegistry()
|
||||
|
||||
m = reg.get_model("claude-haiku-4-5")
|
||||
@@ -164,7 +164,7 @@ class TestMisconfiguredOperatorDir:
|
||||
s = _make_settings(MODELS_CONFIG_DIR=str(bogus))
|
||||
|
||||
with caplog.at_level(logging.WARNING):
|
||||
with patch("application.core.settings.settings", s):
|
||||
with patch("docsgpt.core.settings.settings", s):
|
||||
reg = ModelRegistry()
|
||||
|
||||
# Built-in catalog still loaded
|
||||
@@ -178,7 +178,7 @@ class TestMisconfiguredOperatorDir:
|
||||
|
||||
s = _make_settings(MODELS_CONFIG_DIR=str(afile))
|
||||
with caplog.at_level(logging.WARNING):
|
||||
with patch("application.core.settings.settings", s):
|
||||
with patch("docsgpt.core.settings.settings", s):
|
||||
reg = ModelRegistry()
|
||||
|
||||
assert reg.get_model("docsgpt-local") is not None
|
||||
@@ -199,7 +199,7 @@ class TestOperatorValidation:
|
||||
"""))
|
||||
|
||||
s = _make_settings(MODELS_CONFIG_DIR=str(tmp_path))
|
||||
with patch("application.core.settings.settings", s):
|
||||
with patch("docsgpt.core.settings.settings", s):
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
ModelRegistry()
|
||||
# Could be ModelYAMLError (enum check) or ValueError (registry check);
|
||||
|
||||
@@ -14,9 +14,9 @@ from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from application.core.model_registry import ModelRegistry
|
||||
from application.core.model_settings import ModelProvider
|
||||
from application.core.model_yaml import BUILTIN_MODELS_DIR, load_model_yamls
|
||||
from docsgpt.core.model_registry import ModelRegistry
|
||||
from docsgpt.core.model_settings import ModelProvider
|
||||
from docsgpt.core.model_yaml import BUILTIN_MODELS_DIR, load_model_yamls
|
||||
|
||||
|
||||
def _make_settings(**overrides):
|
||||
@@ -102,7 +102,7 @@ class TestYAMLCompatibleProvider:
|
||||
monkeypatch.setenv("MISTRAL_API_KEY", "sk-mistral-test")
|
||||
|
||||
s = _make_settings(MODELS_CONFIG_DIR=str(tmp_path))
|
||||
with patch("application.core.settings.settings", s):
|
||||
with patch("docsgpt.core.settings.settings", s):
|
||||
reg = ModelRegistry()
|
||||
|
||||
m = reg.get_model("mistral-large-latest")
|
||||
@@ -121,7 +121,7 @@ class TestYAMLCompatibleProvider:
|
||||
monkeypatch.delenv("MISTRAL_API_KEY", raising=False)
|
||||
|
||||
s = _make_settings(MODELS_CONFIG_DIR=str(tmp_path))
|
||||
with patch("application.core.settings.settings", s):
|
||||
with patch("docsgpt.core.settings.settings", s):
|
||||
reg = ModelRegistry()
|
||||
|
||||
# Catalog skipped when no key — no Mistral models in the registry
|
||||
@@ -136,7 +136,7 @@ class TestYAMLCompatibleProvider:
|
||||
monkeypatch.setenv("TOGETHER_API_KEY", "sk-together")
|
||||
|
||||
s = _make_settings(MODELS_CONFIG_DIR=str(tmp_path))
|
||||
with patch("application.core.settings.settings", s):
|
||||
with patch("docsgpt.core.settings.settings", s):
|
||||
reg = ModelRegistry()
|
||||
|
||||
mistral = reg.get_model("mistral-large-latest")
|
||||
@@ -159,7 +159,7 @@ class TestYAMLCompatibleProvider:
|
||||
monkeypatch.delenv("TOGETHER_API_KEY", raising=False)
|
||||
|
||||
s = _make_settings(MODELS_CONFIG_DIR=str(tmp_path))
|
||||
with patch("application.core.settings.settings", s):
|
||||
with patch("docsgpt.core.settings.settings", s):
|
||||
reg = ModelRegistry()
|
||||
|
||||
assert reg.get_model("mistral-large-latest") is not None
|
||||
@@ -177,7 +177,7 @@ class TestYAMLCompatibleProvider:
|
||||
monkeypatch.setenv("SOME_KEY", "k")
|
||||
|
||||
s = _make_settings(MODELS_CONFIG_DIR=str(tmp_path))
|
||||
with patch("application.core.settings.settings", s):
|
||||
with patch("docsgpt.core.settings.settings", s):
|
||||
with pytest.raises(ValueError, match="must set 'base_url'"):
|
||||
ModelRegistry()
|
||||
|
||||
@@ -192,7 +192,7 @@ class TestYAMLCompatibleProvider:
|
||||
"""))
|
||||
|
||||
s = _make_settings(MODELS_CONFIG_DIR=str(tmp_path))
|
||||
with patch("application.core.settings.settings", s):
|
||||
with patch("docsgpt.core.settings.settings", s):
|
||||
with pytest.raises(ValueError, match="must set 'api_key_env'"):
|
||||
ModelRegistry()
|
||||
|
||||
@@ -203,7 +203,7 @@ class TestYAMLCompatibleProvider:
|
||||
monkeypatch.setenv("MISTRAL_API_KEY", "sk")
|
||||
|
||||
s = _make_settings(MODELS_CONFIG_DIR=str(tmp_path))
|
||||
with patch("application.core.settings.settings", s):
|
||||
with patch("docsgpt.core.settings.settings", s):
|
||||
reg = ModelRegistry()
|
||||
|
||||
d = reg.get_model("mistral-large-latest").to_dict()
|
||||
@@ -233,7 +233,7 @@ class TestYAMLCompatibleProvider:
|
||||
monkeypatch.setenv("MISTRAL_API_KEY", "sk")
|
||||
|
||||
s = _make_settings(MODELS_CONFIG_DIR=str(tmp_path))
|
||||
with patch("application.core.settings.settings", s):
|
||||
with patch("docsgpt.core.settings.settings", s):
|
||||
reg = ModelRegistry()
|
||||
|
||||
assert (
|
||||
@@ -254,7 +254,7 @@ class TestLegacyOpenAIBaseURLPath:
|
||||
LLM_PROVIDER="openai",
|
||||
LLM_NAME="llama3,gemma",
|
||||
)
|
||||
with patch("application.core.settings.settings", s):
|
||||
with patch("docsgpt.core.settings.settings", s):
|
||||
reg = ModelRegistry()
|
||||
|
||||
ids = {m.id for m in reg.get_all_models()}
|
||||
@@ -276,7 +276,7 @@ class TestLegacyOpenAIBaseURLPath:
|
||||
LLM_PROVIDER="openai",
|
||||
LLM_NAME="llama3",
|
||||
)
|
||||
with patch("application.core.settings.settings", s):
|
||||
with patch("docsgpt.core.settings.settings", s):
|
||||
reg = ModelRegistry()
|
||||
assert reg.get_model("llama3").api_key == "sk-generic"
|
||||
|
||||
@@ -307,20 +307,20 @@ class TestLLMCreatorDispatch:
|
||||
captured["base_url"] = kwargs.get("base_url")
|
||||
captured["model_id"] = kwargs.get("model_id")
|
||||
|
||||
with patch("application.core.settings.settings", s):
|
||||
with patch("docsgpt.core.settings.settings", s):
|
||||
ModelRegistry.reset()
|
||||
ModelRegistry() # warm up the registry under patched settings
|
||||
|
||||
# Now patch the OpenAI plugin's class so we can capture the
|
||||
# constructor args without spinning up the real OpenAILLM.
|
||||
from application.llm.providers import PROVIDERS_BY_NAME
|
||||
from docsgpt.llm.providers import PROVIDERS_BY_NAME
|
||||
|
||||
with patch.object(
|
||||
PROVIDERS_BY_NAME["openai_compatible"],
|
||||
"llm_class",
|
||||
_FakeLLM,
|
||||
):
|
||||
from application.llm.llm_creator import LLMCreator
|
||||
from docsgpt.llm.llm_creator import LLMCreator
|
||||
|
||||
LLMCreator.create_llm(
|
||||
type="openai_compatible",
|
||||
|
||||
@@ -6,7 +6,7 @@ from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from application.core import optional_deps
|
||||
from docsgpt.core import optional_deps
|
||||
|
||||
|
||||
class TestExtras:
|
||||
|
||||
@@ -13,9 +13,9 @@ from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from application.core.model_registry import ModelRegistry
|
||||
from application.core.model_settings import ModelProvider
|
||||
from application.storage.db.repositories.user_custom_models import (
|
||||
from docsgpt.core.model_registry import ModelRegistry
|
||||
from docsgpt.core.model_settings import ModelProvider
|
||||
from docsgpt.storage.db.repositories.user_custom_models import (
|
||||
UserCustomModelsRepository,
|
||||
)
|
||||
|
||||
@@ -66,8 +66,8 @@ class TestPerUserLayer:
|
||||
)
|
||||
|
||||
s = _make_settings()
|
||||
with patch("application.core.settings.settings", s), patch(
|
||||
"application.storage.db.session.db_readonly",
|
||||
with patch("docsgpt.core.settings.settings", s), patch(
|
||||
"docsgpt.storage.db.session.db_readonly",
|
||||
lambda: _yield(pg_conn),
|
||||
):
|
||||
reg = ModelRegistry()
|
||||
@@ -87,8 +87,8 @@ class TestPerUserLayer:
|
||||
)
|
||||
|
||||
s = _make_settings()
|
||||
with patch("application.core.settings.settings", s), patch(
|
||||
"application.storage.db.session.db_readonly",
|
||||
with patch("docsgpt.core.settings.settings", s), patch(
|
||||
"docsgpt.storage.db.session.db_readonly",
|
||||
lambda: _yield(pg_conn),
|
||||
):
|
||||
reg = ModelRegistry()
|
||||
@@ -110,8 +110,8 @@ class TestPerUserLayer:
|
||||
)
|
||||
|
||||
s = _make_settings()
|
||||
with patch("application.core.settings.settings", s), patch(
|
||||
"application.storage.db.session.db_readonly",
|
||||
with patch("docsgpt.core.settings.settings", s), patch(
|
||||
"docsgpt.storage.db.session.db_readonly",
|
||||
lambda: _yield(pg_conn),
|
||||
):
|
||||
reg = ModelRegistry()
|
||||
@@ -144,11 +144,11 @@ class TestPerUserLayer:
|
||||
# without hitting a real broker. The P1 fix calls ``incr`` on
|
||||
# invalidate; here we just need it not to raise.
|
||||
fake_redis = MagicMock()
|
||||
with patch("application.core.settings.settings", s), patch(
|
||||
"application.storage.db.session.db_readonly",
|
||||
with patch("docsgpt.core.settings.settings", s), patch(
|
||||
"docsgpt.storage.db.session.db_readonly",
|
||||
lambda: _yield(pg_conn),
|
||||
), patch(
|
||||
"application.cache.get_redis_instance", return_value=fake_redis
|
||||
"docsgpt.cache.get_redis_instance", return_value=fake_redis
|
||||
):
|
||||
reg = ModelRegistry()
|
||||
assert reg.get_model(created["id"], user_id="user-1") is not None
|
||||
@@ -185,17 +185,17 @@ class TestLLMCreatorDispatchUsesUpstreamModelId:
|
||||
captured["model_id"] = kwargs.get("model_id")
|
||||
|
||||
s = _make_settings()
|
||||
with patch("application.core.settings.settings", s), patch(
|
||||
"application.storage.db.session.db_readonly",
|
||||
with patch("docsgpt.core.settings.settings", s), patch(
|
||||
"docsgpt.storage.db.session.db_readonly",
|
||||
lambda: _yield(pg_conn),
|
||||
):
|
||||
ModelRegistry()
|
||||
from application.llm.providers import PROVIDERS_BY_NAME
|
||||
from docsgpt.llm.providers import PROVIDERS_BY_NAME
|
||||
|
||||
with patch.object(
|
||||
PROVIDERS_BY_NAME["openai_compatible"], "llm_class", _FakeLLM
|
||||
):
|
||||
from application.llm.llm_creator import LLMCreator
|
||||
from docsgpt.llm.llm_creator import LLMCreator
|
||||
|
||||
LLMCreator.create_llm(
|
||||
type="openai_compatible",
|
||||
@@ -239,17 +239,17 @@ class TestLLMCreatorDispatchUsesUpstreamModelId:
|
||||
captured["capabilities"] = kwargs.get("capabilities")
|
||||
|
||||
s = _make_settings()
|
||||
with patch("application.core.settings.settings", s), patch(
|
||||
"application.storage.db.session.db_readonly",
|
||||
with patch("docsgpt.core.settings.settings", s), patch(
|
||||
"docsgpt.storage.db.session.db_readonly",
|
||||
lambda: _yield(pg_conn),
|
||||
):
|
||||
ModelRegistry()
|
||||
from application.llm.providers import PROVIDERS_BY_NAME
|
||||
from docsgpt.llm.providers import PROVIDERS_BY_NAME
|
||||
|
||||
with patch.object(
|
||||
PROVIDERS_BY_NAME["openai_compatible"], "llm_class", _FakeLLM
|
||||
):
|
||||
from application.llm.llm_creator import LLMCreator
|
||||
from docsgpt.llm.llm_creator import LLMCreator
|
||||
|
||||
LLMCreator.create_llm(
|
||||
type="openai_compatible",
|
||||
@@ -285,8 +285,8 @@ class TestLLMCreatorDispatchUsesUpstreamModelId:
|
||||
)
|
||||
|
||||
s = _make_settings()
|
||||
with patch("application.core.settings.settings", s), patch(
|
||||
"application.storage.db.session.db_readonly",
|
||||
with patch("docsgpt.core.settings.settings", s), patch(
|
||||
"docsgpt.storage.db.session.db_readonly",
|
||||
lambda: _yield(pg_conn),
|
||||
):
|
||||
reg = ModelRegistry()
|
||||
@@ -321,8 +321,8 @@ class TestLLMCreatorDispatchUsesUpstreamModelId:
|
||||
)
|
||||
|
||||
s = _make_settings()
|
||||
with patch("application.core.settings.settings", s), patch(
|
||||
"application.storage.db.session.db_readonly",
|
||||
with patch("docsgpt.core.settings.settings", s), patch(
|
||||
"docsgpt.storage.db.session.db_readonly",
|
||||
lambda: _yield(pg_conn),
|
||||
):
|
||||
reg = ModelRegistry()
|
||||
@@ -359,11 +359,11 @@ class TestCrossProcessInvalidation:
|
||||
|
||||
fake_redis = MagicMock()
|
||||
s = _make_settings()
|
||||
with patch("application.core.settings.settings", s), patch(
|
||||
"application.storage.db.session.db_readonly",
|
||||
with patch("docsgpt.core.settings.settings", s), patch(
|
||||
"docsgpt.storage.db.session.db_readonly",
|
||||
lambda: _yield(pg_conn),
|
||||
), patch(
|
||||
"application.cache.get_redis_instance", return_value=fake_redis
|
||||
"docsgpt.cache.get_redis_instance", return_value=fake_redis
|
||||
):
|
||||
ModelRegistry().get_model("anything", user_id="user-1")
|
||||
ModelRegistry.invalidate_user("user-1")
|
||||
@@ -375,7 +375,7 @@ class TestCrossProcessInvalidation:
|
||||
process's CRUD bumps the version and updates Postgres; peer's
|
||||
next post-TTL access sees the version mismatch and reloads,
|
||||
picking up the rotated key it never invalidated locally."""
|
||||
from application.core import model_registry as registry_mod
|
||||
from docsgpt.core import model_registry as registry_mod
|
||||
|
||||
repo = UserCustomModelsRepository(pg_conn)
|
||||
created = repo.create(
|
||||
@@ -401,11 +401,11 @@ class TestCrossProcessInvalidation:
|
||||
s = _make_settings()
|
||||
# Force TTL to 0 so any subsequent access takes the post-TTL
|
||||
# path without waiting 60s.
|
||||
with patch("application.core.settings.settings", s), patch(
|
||||
"application.storage.db.session.db_readonly",
|
||||
with patch("docsgpt.core.settings.settings", s), patch(
|
||||
"docsgpt.storage.db.session.db_readonly",
|
||||
lambda: _yield(pg_conn),
|
||||
), patch(
|
||||
"application.cache.get_redis_instance", return_value=_FakeRedis()
|
||||
"docsgpt.cache.get_redis_instance", return_value=_FakeRedis()
|
||||
), patch.object(
|
||||
registry_mod, "_USER_CACHE_TTL_SECONDS", 0.0
|
||||
):
|
||||
@@ -433,7 +433,7 @@ class TestCrossProcessInvalidation:
|
||||
def test_ttl_bounds_staleness_when_redis_unavailable(self, pg_conn):
|
||||
"""Redis down → fall back to TTL-only invalidation. After the
|
||||
TTL elapses, peers reload regardless."""
|
||||
from application.core import model_registry as registry_mod
|
||||
from docsgpt.core import model_registry as registry_mod
|
||||
|
||||
repo = UserCustomModelsRepository(pg_conn)
|
||||
created = repo.create(
|
||||
@@ -445,11 +445,11 @@ class TestCrossProcessInvalidation:
|
||||
)
|
||||
|
||||
s = _make_settings()
|
||||
with patch("application.core.settings.settings", s), patch(
|
||||
"application.storage.db.session.db_readonly",
|
||||
with patch("docsgpt.core.settings.settings", s), patch(
|
||||
"docsgpt.storage.db.session.db_readonly",
|
||||
lambda: _yield(pg_conn),
|
||||
), patch(
|
||||
"application.cache.get_redis_instance", return_value=None
|
||||
"docsgpt.cache.get_redis_instance", return_value=None
|
||||
), patch.object(
|
||||
registry_mod, "_USER_CACHE_TTL_SECONDS", 0.0
|
||||
):
|
||||
@@ -469,7 +469,7 @@ class TestCrossProcessInvalidation:
|
||||
def test_unchanged_version_extends_ttl_without_db_read(self, pg_conn):
|
||||
"""Hot path: TTL expires but Redis says no invalidation
|
||||
happened — extend the entry without re-reading Postgres."""
|
||||
from application.core import model_registry as registry_mod
|
||||
from docsgpt.core import model_registry as registry_mod
|
||||
|
||||
repo = UserCustomModelsRepository(pg_conn)
|
||||
created = repo.create(
|
||||
@@ -491,11 +491,11 @@ class TestCrossProcessInvalidation:
|
||||
yield pg_conn
|
||||
|
||||
s = _make_settings()
|
||||
with patch("application.core.settings.settings", s), patch(
|
||||
"application.storage.db.session.db_readonly",
|
||||
with patch("docsgpt.core.settings.settings", s), patch(
|
||||
"docsgpt.storage.db.session.db_readonly",
|
||||
_counting_db_readonly,
|
||||
), patch(
|
||||
"application.cache.get_redis_instance", return_value=fake_redis
|
||||
"docsgpt.cache.get_redis_instance", return_value=fake_redis
|
||||
), patch.object(
|
||||
registry_mod, "_USER_CACHE_TTL_SECONDS", 0.0
|
||||
):
|
||||
|
||||
@@ -4,7 +4,7 @@ import pytest
|
||||
|
||||
|
||||
def test_configured_secret_is_used_without_touching_the_filesystem(tmp_path):
|
||||
from application.core.secret_key import resolve_jwt_secret_key
|
||||
from docsgpt.core.secret_key import resolve_jwt_secret_key
|
||||
|
||||
key_file = tmp_path / "missing" / "jwt-secret"
|
||||
|
||||
@@ -16,14 +16,14 @@ def test_configured_secret_is_used_without_touching_the_filesystem(tmp_path):
|
||||
|
||||
|
||||
def test_cloud_deployment_requires_an_explicit_shared_secret(tmp_path):
|
||||
from application.core.secret_key import resolve_jwt_secret_key
|
||||
from docsgpt.core.secret_key import resolve_jwt_secret_key
|
||||
|
||||
with pytest.raises(RuntimeError, match="JWT_SECRET_KEY must be set"):
|
||||
resolve_jwt_secret_key("", "cloud", tmp_path / "jwt-secret")
|
||||
|
||||
|
||||
def test_local_secret_is_created_once_with_owner_only_permissions(tmp_path):
|
||||
from application.core.secret_key import resolve_jwt_secret_key
|
||||
from docsgpt.core.secret_key import resolve_jwt_secret_key
|
||||
|
||||
key_file = tmp_path / "jwt-secret"
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import pytest
|
||||
|
||||
from application.core import shutdown
|
||||
from docsgpt.core import shutdown
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import pytest
|
||||
from unittest.mock import patch
|
||||
|
||||
from application.core.url_validation import (
|
||||
from docsgpt.core.url_validation import (
|
||||
SSRFError,
|
||||
validate_url,
|
||||
validate_url_safe,
|
||||
@@ -62,13 +62,13 @@ class TestValidateUrl:
|
||||
"""Tests for validate_url function."""
|
||||
|
||||
def test_adds_scheme_if_missing(self):
|
||||
with patch("application.core.url_validation.resolve_hostname") as mock_resolve:
|
||||
with patch("docsgpt.core.url_validation.resolve_hostname") as mock_resolve:
|
||||
mock_resolve.return_value = "93.184.216.34" # Public IP
|
||||
result = validate_url("example.com")
|
||||
assert result == "http://example.com"
|
||||
|
||||
def test_preserves_https_scheme(self):
|
||||
with patch("application.core.url_validation.resolve_hostname") as mock_resolve:
|
||||
with patch("docsgpt.core.url_validation.resolve_hostname") as mock_resolve:
|
||||
mock_resolve.return_value = "93.184.216.34"
|
||||
result = validate_url("https://example.com")
|
||||
assert result == "https://example.com"
|
||||
@@ -129,14 +129,14 @@ class TestValidateUrl:
|
||||
assert "scheme" in str(exc_info.value).lower()
|
||||
|
||||
def test_blocks_hostname_resolving_to_private_ip(self):
|
||||
with patch("application.core.url_validation.resolve_hostname") as mock_resolve:
|
||||
with patch("docsgpt.core.url_validation.resolve_hostname") as mock_resolve:
|
||||
mock_resolve.return_value = "192.168.1.1"
|
||||
with pytest.raises(SSRFError) as exc_info:
|
||||
validate_url("http://internal.example.com")
|
||||
assert "private" in str(exc_info.value).lower() or "internal" in str(exc_info.value).lower()
|
||||
|
||||
def test_blocks_hostname_resolving_to_metadata_ip(self):
|
||||
with patch("application.core.url_validation.resolve_hostname") as mock_resolve:
|
||||
with patch("docsgpt.core.url_validation.resolve_hostname") as mock_resolve:
|
||||
mock_resolve.return_value = "169.254.169.254"
|
||||
with pytest.raises(SSRFError) as exc_info:
|
||||
validate_url("http://evil.example.com")
|
||||
@@ -147,13 +147,13 @@ class TestValidateUrl:
|
||||
assert result == "http://8.8.8.8"
|
||||
|
||||
def test_allows_public_hostname(self):
|
||||
with patch("application.core.url_validation.resolve_hostname") as mock_resolve:
|
||||
with patch("docsgpt.core.url_validation.resolve_hostname") as mock_resolve:
|
||||
mock_resolve.return_value = "93.184.216.34"
|
||||
result = validate_url("https://example.com")
|
||||
assert result == "https://example.com"
|
||||
|
||||
def test_raises_on_unresolvable_hostname(self):
|
||||
with patch("application.core.url_validation.resolve_hostname") as mock_resolve:
|
||||
with patch("docsgpt.core.url_validation.resolve_hostname") as mock_resolve:
|
||||
mock_resolve.return_value = None
|
||||
with pytest.raises(SSRFError) as exc_info:
|
||||
validate_url("http://nonexistent.invalid")
|
||||
@@ -177,7 +177,7 @@ class TestValidateUrlSafe:
|
||||
"""Tests for validate_url_safe non-throwing function."""
|
||||
|
||||
def test_returns_tuple_on_success(self):
|
||||
with patch("application.core.url_validation.resolve_hostname") as mock_resolve:
|
||||
with patch("docsgpt.core.url_validation.resolve_hostname") as mock_resolve:
|
||||
mock_resolve.return_value = "93.184.216.34"
|
||||
is_valid, url, error = validate_url_safe("https://example.com")
|
||||
assert is_valid is True
|
||||
@@ -197,7 +197,7 @@ class TestValidateUrlSafe:
|
||||
assert "private" in error.lower() or "internal" in error.lower()
|
||||
|
||||
def test_adds_scheme_when_missing(self):
|
||||
with patch("application.core.url_validation.resolve_hostname") as mock_resolve:
|
||||
with patch("docsgpt.core.url_validation.resolve_hostname") as mock_resolve:
|
||||
mock_resolve.return_value = "93.184.216.34"
|
||||
is_valid, url, error = validate_url_safe("example.com")
|
||||
assert is_valid is True
|
||||
@@ -235,7 +235,7 @@ class TestValidateUrlExtended:
|
||||
validate_url("http://metadata")
|
||||
|
||||
def test_allows_localhost_with_flag(self):
|
||||
with patch("application.core.url_validation.resolve_hostname") as mock_resolve:
|
||||
with patch("docsgpt.core.url_validation.resolve_hostname") as mock_resolve:
|
||||
mock_resolve.return_value = "192.168.1.1"
|
||||
result = validate_url(
|
||||
"http://internal.local", allow_localhost=True
|
||||
@@ -251,7 +251,7 @@ class TestValidateUrlExtended:
|
||||
validate_url("http://[fd00:ec2::254]")
|
||||
|
||||
def test_blocks_hostname_resolving_to_loopback(self):
|
||||
with patch("application.core.url_validation.resolve_hostname") as mock_resolve:
|
||||
with patch("docsgpt.core.url_validation.resolve_hostname") as mock_resolve:
|
||||
mock_resolve.return_value = "127.0.0.1"
|
||||
with pytest.raises(SSRFError):
|
||||
validate_url("http://sneaky.example.com")
|
||||
|
||||
Reference in new issue
Block a user