@@ -66,7 +66,8 @@ class ModelQuantizationSettings(BaseModel):
6666class ModelSettings (BaseModel ):
6767 base_model : str
6868 quantization : ModelQuantizationSettings
69- cot_adapter_path : str
69+ # None runs the base CoT model with no LoRA adapter (e.g. MedGemma, already instruction-tuned).
70+ cot_adapter_path : str | None = None
7071 reranker_model_path : str
7172 reranker_adapter_path : str
7273 trust_remote_code : bool = False
@@ -84,16 +85,25 @@ class GlobalSettings(BaseModel):
8485
8586
8687class SearchBackendSettings (BaseModel ):
87- backend : Literal ["lancedb" ] = "lancedb"
88+ # Resolved via the plugin registry at build_search_backend (raises on a typo), so a plain str
89+ # not a Literal that needs widening per backend. ``type`` preferred; ``backend`` is legacy.
90+ type : str | None = None
91+ backend : str = "lancedb"
8892 db_path : str = "data/search"
8993 trials_table : str = "trials"
9094 criteria_table : str = "criteria"
9195 candidate_limit : int = Field (1000 , ge = 1 )
92- # Vector similarity for the ANN index and re-ranker. "cosine" (default) suits normalized
93- # embedders (bge-m3 ); "dot" suits inner-product dual-encoders (e.g. MedCPT) .
94- vector_metric : Literal ["cosine" , "dot" ] = "cosine"
96+ # ANN index/ re-ranker similarity. None follows the embedder (cosine if normalized, dot if not,
97+ # e.g. MedCPT ); set explicitly to pin it regardless of the embedder .
98+ vector_metric : Literal ["cosine" , "dot" ] | None = None
9599 # Hybrid blend of the first-level score: (1 - vector_weight) * text + vector_weight * vector.
96100 vector_weight : float = Field (0.5 , ge = 0.0 , le = 1.0 )
101+ # Re-embed the corpus with the config embedder at index-build time (vs reusing prepare-time
102+ # vectors). Required to switch embedders: otherwise the new-model query dim-mismatches the old
103+ # index vectors and silently falls back to BM25.
104+ reembed_index : bool = False
105+ # Keep undeclared backend knobs; they reach the tolerant .get() consumer via the non-lossy loader.
106+ model_config = ConfigDict (extra = "allow" )
97107
98108
99109class RegistrySettings (BaseModel ):
@@ -111,7 +121,10 @@ class RegistrySettings(BaseModel):
111121
112122
113123class EmbedderSettings (BaseModel ):
114- backend : Literal ["hf" , "hashing" ] = "hf"
124+ # Embedder family resolved via the plugin registry at build_embedder; plain str (not a Literal)
125+ # so a new family needs no schema edit. ``type`` preferred; ``backend`` is legacy (hf | hashing).
126+ type : str | None = None
127+ backend : str = "hf"
115128 model_name : str = "BAAI/bge-m3"
116129 # A distinct query_model_name selects an asymmetric dual-encoder (separate document/query
117130 # encoders sharing one space, e.g. MedCPT's article/query encoders); None = symmetric.
@@ -128,6 +141,11 @@ class EmbedderSettings(BaseModel):
128141 hashing_dimensions : int = Field (64 , ge = 1 )
129142 # Instruction prepended to queries only, for instruction-tuned embedders (e.g. Qwen3-Embedding).
130143 query_instruction : str | None = None
144+ # Vector metric this embedder's space is trained for; None derives from ``normalize`` (cosine
145+ # if normalized, dot if not). The search backend reads it so the metric follows the embedder.
146+ native_metric : Literal ["cosine" , "dot" ] | None = None
147+ # Keep undeclared embedder knobs (they reach the tolerant .get() consumer via the non-lossy loader).
148+ model_config = ConfigDict (extra = "allow" )
131149
132150 @field_validator ("pooling" )
133151 @classmethod
@@ -163,11 +181,9 @@ class SearchSettings(BaseModel):
163181 max_trials_second_level : int = Field (100 , ge = 1 )
164182 # Keep the top 1/N of reranked second-level trials before CoT (N=1 keeps all).
165183 second_level_keep_divisor : int = Field (3 , ge = 1 )
166- # How the second-level shortlist combines the first-level (retrieval) and second-level
167- # (reranker) rankings. "rrf" fuses the two by rank, so a strong retrieval hit keeps a
168- # floor and is not evicted when the reranker fails to score its criteria; "score_sum"
169- # is the earlier behaviour of adding the two raw scores (whose scales differ, so one
170- # signal dominates and can drop retrieval-ranked trials).
184+ # How the shortlist fuses first-level (retrieval) and second-level (reranker) rankings. "rrf"
185+ # fuses by rank, so a strong retrieval hit isn't evicted when the reranker fails to score it;
186+ # "score_sum" adds raw scores (mismatched scales let one signal dominate and drop retrieval hits).
171187 shortlist_fusion : Literal ["rrf" , "score_sum" ] = "rrf"
172188 shortlist_rrf_k : int = Field (60 , ge = 1 )
173189 shortlist_first_level_weight : float = Field (1.0 , ge = 0.0 )
@@ -208,6 +224,10 @@ class RagSettings(BaseModel):
208224 backend : Literal ["vllm" , "transformers" ] = "vllm"
209225 batch_size : int = Field (4 , ge = 1 )
210226 max_trials_rag : int = Field (20 , ge = 1 )
227+ # Suppress chain-of-thought <think> in the eligibility stage for reasoning models (Qwen3):
228+ # sends enable_thinking=False / a /no_think prefix and strips residual think tags.
229+ no_think : bool = False
230+ model_config = ConfigDict (extra = "allow" )
211231
212232
213233class VllmSettings (BaseModel ):
@@ -220,10 +240,24 @@ class VllmSettings(BaseModel):
220240 gpu_memory_utilization : float = Field (0.5 , gt = 0.0 , le = 1.0 )
221241 max_model_len : int = Field (8192 , ge = 256 )
222242 tensor_parallel_size : int = Field (1 , ge = 1 )
243+ # In-flight vLLM weight quantization (e.g. "bitsandbytes" NF4 4-bit) so a large base model (32B
244+ # CoT) fits one card without a pre-quantized checkpoint. "" = none (bf16). Must be a field or
245+ # it is dropped before the loader.
246+ quantization : str = ""
247+ # Disable vLLM's custom all-reduce kernel. Required for tensor_parallel_size>1 on multi-GPU
248+ # nodes WITHOUT NVLink (A40/L40 over PCIe), where it dies with CUDA 'invalid argument'; falls
249+ # back to NCCL all-reduce (pair with NCCL_P2P_DISABLE=1).
250+ disable_custom_all_reduce : bool = False
251+ # Skip CUDA graph capture, freeing ~6-7GB per GPU for KV cache. Useful when a large model
252+ # (e.g. a 27B tensor-parallel across 48GB cards) would otherwise starve the KV cache.
253+ enforce_eager : bool = False
223254 # fp8 KV cache halves KV memory so a large window (e.g. 8192) fits on one 48GB card;
224255 # max_num_seqs caps concurrency so that tight KV budget does not thrash.
225256 kv_cache_dtype : Literal ["auto" , "fp8" , "fp8_e4m3" , "fp8_e5m2" ] | None = None
226257 max_num_seqs : int | None = Field (None , ge = 1 )
258+ # Keep undeclared vLLM runtime knobs (e.g. swap_space, enable_prefix_caching) — they reach
259+ # load_vllm_engine's tolerant .get() via the non-lossy loader, no new field required.
260+ model_config = ConfigDict (extra = "allow" )
227261
228262
229263class CotSettings (BaseModel ):
@@ -238,6 +272,7 @@ class LLMRerankerSettings(BaseModel):
238272 # to fit both engines on a smaller card (e.g. 48GB A40/L40).
239273 gpu_memory_utilization : float = Field (0.4 , gt = 0.0 , le = 1.0 )
240274 tensor_parallel_size : int = Field (1 , ge = 1 )
275+ model_config = ConfigDict (extra = "allow" )
241276
242277
243278class QueryExpansionSettings (BaseModel ):
@@ -251,6 +286,9 @@ class QueryExpansionSettings(BaseModel):
251286 max_main_conditions : int = Field (11 , ge = 1 )
252287 max_other_conditions : int = Field (50 , ge = 1 )
253288 trust_remote_code : bool = False
289+ # Suppress <think> during expansion for reasoning models (read at query_expansion.py:_resolve_settings).
290+ no_think : bool = False
291+ model_config = ConfigDict (extra = "allow" )
254292
255293
256294class ReportingSettings (BaseModel ):
0 commit comments