2222from collections .abc import AsyncIterator
2323
2424from backend .app .api .interfaces import DependencyUnavailable , RoutedSection
25+ from backend .app .settings import get_settings
2526
2627DEFAULT_GENERATION_MODEL = os .environ .get ("GENERATION_MODEL" , "claude-opus-4-8" )
2728"""Answer-synthesis model id (overridable via the GENERATION_MODEL env var)."""
2829
29- DEFAULT_MAX_TOKENS = int (os .environ .get ("GENERATION_MAX_TOKENS" , "16000" ))
30- """Output token ceiling for a synthesized answer (must exceed DEFAULT_THINKING_BUDGET_TOKENS)."""
31-
32- DEFAULT_THINKING_BUDGET_TOKENS = 5000
33- """Default extended-thinking budget in tokens."""
34-
3530DEFAULT_STREAM_TIMEOUT = 300.0
3631"""Total timeout in seconds for a streaming answer generation call."""
3732
33+ _THINKING_CAPABLE_MODELS = (
34+ "claude-3-7" ,
35+ "claude-opus-4" ,
36+ "claude-sonnet-4" ,
37+ "claude-haiku-4" ,
38+ "claude-fable" ,
39+ )
40+ """Model id prefixes that support extended thinking (budget_tokens parameter)."""
41+
42+
43+ def _default_max_tokens () -> int :
44+ """Return the configured max-tokens ceiling, falling back to 16 000.
45+
46+ Returns:
47+ The generation_max_tokens value from settings, or 16 000 on error.
48+ """
49+ try :
50+ return get_settings ().generation_max_tokens
51+ except Exception :
52+ return 16000
53+
54+
55+ def _default_thinking_budget () -> int :
56+ """Return the configured thinking-budget token count, falling back to 5 000.
57+
58+ Returns:
59+ The generation_thinking_budget_tokens value from settings, or 5 000 on error.
60+ """
61+ try :
62+ return get_settings ().generation_thinking_budget_tokens
63+ except Exception :
64+ return 5000
65+
66+
3867_SYSTEM_PROMPT = (
3968 "You are a retrieval-augmented answer assistant. Answer the user's question "
4069 "using only the routed document sections provided as context. Cite section "
@@ -72,24 +101,32 @@ class ClaudeGenerationLLM:
72101 def __init__ (
73102 self ,
74103 model : str = DEFAULT_GENERATION_MODEL ,
75- max_tokens : int = DEFAULT_MAX_TOKENS ,
76- thinking_budget_tokens : int = DEFAULT_THINKING_BUDGET_TOKENS ,
104+ max_tokens : int | None = None ,
105+ thinking_budget_tokens : int | None = None ,
77106 client : object | None = None ,
78107 ) -> None :
79108 """Initialize the adapter.
80109
81110 Args:
82111 model: Generation model id (defaults to Claude Opus 4.8).
83- max_tokens: Output token ceiling for the synthesized answer.
112+ max_tokens: Output token ceiling for the synthesized answer. When
113+ omitted, the value is read from settings (GENERATION_MAX_TOKENS).
84114 thinking_budget_tokens: Extended-thinking budget in tokens passed to
85- the Anthropic ``thinking`` parameter.
115+ the Anthropic ``thinking`` parameter. When omitted, read from
116+ settings (GENERATION_THINKING_BUDGET_TOKENS).
86117 client: Optional pre-built ``anthropic.AsyncAnthropic`` instance. When
87118 omitted, one is constructed lazily on first use so importing this
88119 module never requires credentials.
89120 """
90121 self ._model = model
91- self ._max_tokens = max_tokens
92- self ._thinking_budget_tokens = thinking_budget_tokens
122+ self ._max_tokens = max_tokens if max_tokens is not None else _default_max_tokens ()
123+ self ._thinking_budget_tokens = (
124+ thinking_budget_tokens
125+ if thinking_budget_tokens is not None
126+ else _default_thinking_budget ()
127+ )
128+ if self ._thinking_budget_tokens >= self ._max_tokens :
129+ self ._thinking_budget_tokens = max (1000 , self ._max_tokens - 1000 )
93130 self ._client = client
94131 self ._client_lock = asyncio .Lock ()
95132
@@ -155,17 +192,26 @@ async def stream_answer(
155192 "</routed_sections>\n \n "
156193 "Answer the question using only the information in the routed sections above."
157194 )
195+ stream_kwargs : dict [str , object ] = {
196+ "model" : self ._model ,
197+ "max_tokens" : self ._max_tokens ,
198+ "system" : _SYSTEM_PROMPT ,
199+ "messages" : [{"role" : "user" , "content" : user_message }],
200+ "timeout" : DEFAULT_STREAM_TIMEOUT ,
201+ }
202+ if any (self ._model .startswith (prefix ) for prefix in _THINKING_CAPABLE_MODELS ):
203+ stream_kwargs ["thinking" ] = {
204+ "type" : "enabled" ,
205+ "budget_tokens" : self ._thinking_budget_tokens ,
206+ }
158207 try :
159208 async with client .messages .stream ( # type: ignore[attr-defined]
160- model = self ._model ,
161- max_tokens = self ._max_tokens ,
162- thinking = {"type" : "enabled" , "budget_tokens" : self ._thinking_budget_tokens },
163- system = _SYSTEM_PROMPT ,
164- messages = [{"role" : "user" , "content" : user_message }],
165- timeout = DEFAULT_STREAM_TIMEOUT ,
209+ ** stream_kwargs
166210 ) as stream :
167211 async for text in stream .text_stream :
168212 yield text
213+ except asyncio .CancelledError :
214+ raise
169215 except DependencyUnavailable :
170216 raise
171217 except Exception as exc :
@@ -177,6 +223,7 @@ async def stream_answer(
177223 _anthropic .RateLimitError ,
178224 _anthropic .AuthenticationError ,
179225 _anthropic .APIConnectionError ,
226+ _anthropic .APIStatusError ,
180227 ),
181228 ):
182229 raise DependencyUnavailable (
0 commit comments