Skip to content

Commit 24df900

Browse files
authored
v1.0.2
v1.0.2
2 parents 445e762 + 9676d88 commit 24df900

12 files changed

Lines changed: 233 additions & 135 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
3636
### 📦 Releases
3737

38+
> **[2026.4.11]** [v1.0.2](https://github.com/HKUDS/DeepTutor/releases/tag/v1.0.2) — Search consolidation simplification with SearXNG fallback, provider switch fix, explicit runtime config in test runner, and frontend resource leak fixes.
39+
3840
> **[2026.4.10]** [v1.0.1](https://github.com/HKUDS/DeepTutor/releases/tag/v1.0.1) — New Visualize capability with Chart.js/SVG rendering pipeline, quiz duplicate prevention with generation history, o4-mini model support, and server logging improvements.
3941
4042
> **[2026.4.10]** [v1.0.0-beta.4](https://github.com/HKUDS/DeepTutor/releases/tag/v1.0.0-beta.4) — Embedding progress tracking with HTTP 429 rate limit retry, cross-platform start tour dependency management, and case-insensitive MIME validation fix.

assets/releases/ver1-0-2.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# DeepTutor v1.0.2 Release Notes
2+
3+
**Release Date:** 2026.04.11
4+
5+
## Highlights
6+
7+
### Search Consolidation Simplification & SearXNG Fallback
8+
Removed the explicit `consolidation_type` parameter — consolidation now runs automatically for any provider that doesn't return its own answer. A new generic fallback formatter handles providers (e.g. SearXNG) that lack a dedicated Jinja2 template, fixing the "no template consolidation available" error. The `CONSOLIDATION_TYPES` constant and related config fields have been removed.
9+
10+
### Provider Switch Fix
11+
Settings page now always overwrites `base_url` when the user selects a different provider, instead of only filling it when the field was previously empty. This prevents stale base URLs from persisting across provider changes.
12+
13+
### Explicit Runtime Config in Test Runner
14+
`ConfigTestRunner` now builds LLM, Embedding, and Search configs directly from the resolved runtime catalog instead of relying on the global config cache, ensuring test runs always reflect the current active selection.
15+
16+
### Frontend Resource Leak Fixes
17+
- Added `AbortController` cleanup across all Playground testers (ToolExecutor, DeepQuestionTester, DeepResearchTester, CapabilityTester) and the SaveToNotebookModal, preventing orphaned fetch requests on unmount or re-execution.
18+
- Introduced a `MAX_CACHED_SESSIONS = 20` eviction policy in UnifiedChatContext to prevent unbounded session memory growth.
19+
- WebSocket runners and retry timers are now properly cleaned up on provider unmount.
20+
- Fixed auto-scroll throttle timer leak by returning a cleanup function from the throttle effect.
21+
22+
## Community Contributions
23+
24+
- **@OlegSob-glitch** — SearXNG auto-fallback for search providers without templates (#286)
25+
26+
**Full Changelog**: https://github.com/HKUDS/DeepTutor/compare/v1.0.1...v1.0.2

deeptutor/services/config/test_runner.py

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@
1313

1414
from .env_store import get_env_store
1515
from .model_catalog import get_model_catalog_service
16-
from .provider_runtime import resolve_search_runtime_config
16+
from .provider_runtime import (
17+
resolve_embedding_runtime_config,
18+
resolve_llm_runtime_config,
19+
resolve_search_runtime_config,
20+
)
1721

1822

1923
def _redact(value: str) -> str:
@@ -115,11 +119,11 @@ def _run_sync(self, run: TestRun, catalog: dict[str, Any]) -> None:
115119

116120
with temporary_env(env_values):
117121
if service == "llm":
118-
asyncio.run(self._test_llm(run))
122+
asyncio.run(self._test_llm(run, catalog))
119123
elif service == "embedding":
120-
asyncio.run(self._test_embedding(run, model or {}))
124+
asyncio.run(self._test_embedding(run, model or {}, catalog))
121125
elif service == "search":
122-
self._test_search(run)
126+
self._test_search(run, catalog)
123127
else:
124128
raise ValueError(f"Unsupported service: {service}")
125129
if not run.cancelled and run.status == "running":
@@ -129,13 +133,26 @@ def _run_sync(self, run: TestRun, catalog: dict[str, Any]) -> None:
129133
run.status = "failed"
130134
run.emit("failed", str(exc))
131135

132-
async def _test_llm(self, run: TestRun) -> None:
136+
async def _test_llm(self, run: TestRun, catalog: dict[str, Any]) -> None:
133137
from deeptutor.services.llm import clear_llm_config_cache, complete as llm_complete
134-
from deeptutor.services.llm import get_llm_config, get_token_limit_kwargs
138+
from deeptutor.services.llm import get_token_limit_kwargs
139+
from deeptutor.services.llm.config import LLMConfig
135140

136141
clear_llm_config_cache()
137142
run.emit("info", "Loading LLM config from the active catalog selection.")
138-
llm_config = get_llm_config()
143+
resolved = resolve_llm_runtime_config(catalog=catalog)
144+
llm_config = LLMConfig(
145+
model=resolved.model,
146+
api_key=resolved.api_key,
147+
base_url=resolved.base_url,
148+
effective_url=resolved.effective_url,
149+
binding=resolved.binding,
150+
provider_name=resolved.provider_name,
151+
provider_mode=resolved.provider_mode,
152+
api_version=resolved.api_version,
153+
extra_headers=resolved.extra_headers,
154+
reasoning_effort=resolved.reasoning_effort,
155+
)
139156
run.emit("info", f"Resolved model `{llm_config.model}` with binding `{llm_config.binding}`.")
140157
run.emit("info", f"Request target: {llm_config.base_url}")
141158
token_kwargs = get_token_limit_kwargs(llm_config.model, max_tokens=200)
@@ -155,14 +172,30 @@ async def _test_llm(self, run: TestRun) -> None:
155172
if not snippet:
156173
raise ValueError("LLM returned an empty response.")
157174

158-
async def _test_embedding(self, run: TestRun, model: dict[str, Any]) -> None:
159-
from deeptutor.services.embedding import get_embedding_client, get_embedding_config
175+
async def _test_embedding(self, run: TestRun, model: dict[str, Any], catalog: dict[str, Any]) -> None:
176+
from deeptutor.services.embedding.client import EmbeddingClient
177+
from deeptutor.services.embedding.config import EmbeddingConfig
160178

161179
run.emit("info", "Loading embedding config from the active catalog selection.")
162-
config = get_embedding_config()
180+
resolved = resolve_embedding_runtime_config(catalog=catalog)
181+
config = EmbeddingConfig(
182+
model=resolved.model,
183+
api_key=resolved.api_key,
184+
base_url=resolved.base_url,
185+
effective_url=resolved.effective_url,
186+
binding=resolved.binding,
187+
provider_name=resolved.provider_name,
188+
provider_mode=resolved.provider_mode,
189+
api_version=resolved.api_version,
190+
extra_headers=resolved.extra_headers,
191+
dim=resolved.dimension,
192+
request_timeout=max(1, resolved.request_timeout),
193+
batch_size=max(1, resolved.batch_size),
194+
batch_delay=max(0.0, resolved.batch_delay),
195+
)
163196
run.emit("info", f"Resolved embedding model `{config.model}` with binding `{config.binding}`.")
164197
run.emit("info", f"Request target: {config.base_url}")
165-
client = get_embedding_client()
198+
client = EmbeddingClient(config)
166199
vectors = await client.embed(["DeepTutor embedding smoke test"])
167200
if not vectors or not vectors[0]:
168201
raise ValueError("Embedding service returned an empty vector.")
@@ -179,10 +212,10 @@ async def _test_embedding(self, run: TestRun, model: dict[str, Any]) -> None:
179212
f"Embedding dimension mismatch. expected={expected_dimension}, actual={actual_dimension}"
180213
)
181214

182-
def _test_search(self, run: TestRun) -> None:
215+
def _test_search(self, run: TestRun, catalog: dict[str, Any]) -> None:
183216
from deeptutor.services.search import web_search
184217

185-
resolved = resolve_search_runtime_config()
218+
resolved = resolve_search_runtime_config(catalog=catalog)
186219
if not resolved.requested_provider:
187220
run.status = "completed"
188221
run.emit("completed", "Search skipped because no active provider is configured.")

deeptutor/services/search/__init__.py

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
)
1919

2020
from .base import SEARCH_API_KEY_ENV, BaseSearchProvider
21-
from .consolidation import CONSOLIDATION_TYPES, PROVIDER_TEMPLATES, AnswerConsolidator
21+
from .consolidation import PROVIDER_TEMPLATES, AnswerConsolidator
2222
from .providers import (
2323
_DEPRECATED_UNSUPPORTED,
2424
get_available_providers,
@@ -84,12 +84,16 @@ def web_search(
8484
output_dir: str | None = None,
8585
verbose: bool = False,
8686
provider: str | None = None,
87-
consolidation: str | None = None,
8887
consolidation_custom_template: str | None = None,
8988
consolidation_llm_model: str | None = None,
9089
**provider_kwargs: Any,
9190
) -> dict[str, Any]:
92-
"""Execute web search and return DeepTutor structured response shape."""
91+
"""Execute web search and return DeepTutor structured response shape.
92+
93+
Consolidation is automatic for providers that return raw SERP results
94+
(``supports_answer=False``). Pass ``consolidation_llm_model`` to
95+
upgrade from template formatting to LLM synthesis.
96+
"""
9397
config = _get_web_search_config()
9498
if not config.get("enabled", True):
9599
_logger.warning("Web search is disabled in config")
@@ -138,19 +142,16 @@ def web_search(
138142
_logger.error(f"[{search_provider.name}] Search failed: {exc}")
139143
raise Exception(f"{search_provider.name} search failed: {exc}") from exc
140144

141-
# Compatibility layer: only apply optional consolidation when requested.
142-
if consolidation is None:
143-
consolidation = config.get("consolidation")
144-
if consolidation_custom_template is None:
145-
consolidation_custom_template = config.get("consolidation_template") or None
146-
if consolidation and not search_provider.supports_answer:
147-
llm_config = {}
148-
if consolidation_llm_model:
149-
llm_config["model"] = consolidation_llm_model
145+
# Auto-consolidate for providers that don't generate their own answers.
146+
if not search_provider.supports_answer:
147+
if consolidation_custom_template is None:
148+
consolidation_custom_template = config.get("consolidation_template") or None
149+
use_llm = bool(consolidation_llm_model)
150+
llm_config = {"model": consolidation_llm_model} if consolidation_llm_model else None
150151
consolidator = AnswerConsolidator(
151-
consolidation_type=consolidation,
152+
use_llm=use_llm,
152153
custom_template=consolidation_custom_template,
153-
llm_config=llm_config if llm_config else None,
154+
llm_config=llm_config,
154155
)
155156
response = consolidator.consolidate(response)
156157

@@ -184,9 +185,7 @@ def get_current_config() -> dict[str, Any]:
184185
"providers": get_providers_info(),
185186
"supported_providers": sorted(SUPPORTED_SEARCH_PROVIDERS),
186187
"deprecated_providers": sorted(DEPRECATED_SEARCH_PROVIDERS),
187-
"consolidation": config.get("consolidation"),
188188
"consolidation_template": config.get("consolidation_template") or None,
189-
"consolidation_types": CONSOLIDATION_TYPES,
190189
"template_providers": list(PROVIDER_TEMPLATES.keys()),
191190
}
192191

@@ -205,7 +204,6 @@ def get_current_config() -> dict[str, Any]:
205204
"Citation",
206205
"SearchResult",
207206
"AnswerConsolidator",
208-
"CONSOLIDATION_TYPES",
209207
"PROVIDER_TEMPLATES",
210208
"BaseSearchProvider",
211209
"SearchProvider",

0 commit comments

Comments
 (0)