Fix forward reference crash in Google and Anthropic LLM ThinkingConfig

ThinkingConfig was defined as an inner class on the service but referenced in the Settings dataclass declared before the service class, causing a crash at import time. Move ThinkingConfig to a standalone class defined before Settings, and keep a class attribute alias for backward compatibility.
This commit is contained in:
Paul Kompfner
2026-02-19 15:06:48 -05:00
parent cc54ff4708
commit ebb42a3c6d
2 changed files with 64 additions and 62 deletions

View File

@@ -70,6 +70,25 @@ except ModuleNotFoundError as e:
raise Exception(f"Missing module: {e}") raise Exception(f"Missing module: {e}")
class AnthropicThinkingConfig(BaseModel):
"""Configuration for extended thinking.
Parameters:
type: Type of thinking mode (currently only "enabled" or "disabled").
budget_tokens: Maximum number of tokens for thinking.
With today's models, the minimum is 1024.
Only allowed if type is "enabled".
"""
# Why `| str` here? To not break compatibility in case Anthropic adds
# more types in the future.
type: Literal["enabled", "disabled"] | str
# Why not enforce minimnum of 1024 here? To not break compatibility in
# case Anthropic changes this requirement in the future.
budget_tokens: int
@dataclass @dataclass
class AnthropicLLMSettings(LLMSettings): class AnthropicLLMSettings(LLMSettings):
"""Settings for Anthropic LLM services. """Settings for Anthropic LLM services.
@@ -80,20 +99,18 @@ class AnthropicLLMSettings(LLMSettings):
""" """
enable_prompt_caching: bool | _NotGiven = field(default_factory=lambda: _NOT_GIVEN) enable_prompt_caching: bool | _NotGiven = field(default_factory=lambda: _NOT_GIVEN)
thinking: "AnthropicLLMService.ThinkingConfig" | _NotGiven = field( thinking: AnthropicThinkingConfig | _NotGiven = field(default_factory=lambda: _NOT_GIVEN)
default_factory=lambda: _NOT_GIVEN
)
@classmethod @classmethod
def from_mapping(cls, settings): def from_mapping(cls, settings):
"""Convert a plain dict to settings, coercing thinking dicts. """Convert a plain dict to settings, coercing thinking dicts.
For backward compatibility, a ``thinking`` value that is a plain dict For backward compatibility, a ``thinking`` value that is a plain dict
is converted to a :class:`AnthropicLLMService.ThinkingConfig`. is converted to a :class:`AnthropicThinkingConfig`.
""" """
instance = super().from_mapping(settings) instance = super().from_mapping(settings)
if is_given(instance.thinking) and isinstance(instance.thinking, dict): if is_given(instance.thinking) and isinstance(instance.thinking, dict):
instance.thinking = AnthropicLLMService.ThinkingConfig(**instance.thinking) instance.thinking = AnthropicThinkingConfig(**instance.thinking)
return instance return instance
@@ -148,23 +165,8 @@ class AnthropicLLMService(LLMService):
# Overriding the default adapter to use the Anthropic one. # Overriding the default adapter to use the Anthropic one.
adapter_class = AnthropicLLMAdapter adapter_class = AnthropicLLMAdapter
class ThinkingConfig(BaseModel): # Backward compatibility: ThinkingConfig used to be defined inline here.
"""Configuration for extended thinking. ThinkingConfig = AnthropicThinkingConfig
Parameters:
type: Type of thinking mode (currently only "enabled" or "disabled").
budget_tokens: Maximum number of tokens for thinking.
With today's models, the minimum is 1024.
Only allowed if type is "enabled".
"""
# Why `| str` here? To not break compatibility in case Anthropic adds
# more types in the future.
type: Literal["enabled", "disabled"] | str
# Why not enforce minimnum of 1024 here? To not break compatibility in
# case Anthropic changes this requirement in the future.
budget_tokens: int
class InputParams(BaseModel): class InputParams(BaseModel):
"""Input parameters for Anthropic model inference. """Input parameters for Anthropic model inference.
@@ -193,9 +195,7 @@ class AnthropicLLMService(LLMService):
temperature: Optional[float] = Field(default_factory=lambda: NOT_GIVEN, ge=0.0, le=1.0) temperature: Optional[float] = Field(default_factory=lambda: NOT_GIVEN, ge=0.0, le=1.0)
top_k: Optional[int] = Field(default_factory=lambda: NOT_GIVEN, ge=0) top_k: Optional[int] = Field(default_factory=lambda: NOT_GIVEN, ge=0)
top_p: Optional[float] = Field(default_factory=lambda: NOT_GIVEN, ge=0.0, le=1.0) top_p: Optional[float] = Field(default_factory=lambda: NOT_GIVEN, ge=0.0, le=1.0)
thinking: Optional["AnthropicLLMService.ThinkingConfig"] = Field( thinking: Optional[AnthropicThinkingConfig] = Field(default_factory=lambda: NOT_GIVEN)
default_factory=lambda: NOT_GIVEN
)
extra: Optional[Dict[str, Any]] = Field(default_factory=dict) extra: Optional[Dict[str, Any]] = Field(default_factory=dict)
def model_post_init(self, __context): def model_post_init(self, __context):

View File

@@ -673,6 +673,39 @@ class GoogleLLMContext(OpenAILLMContext):
self._messages = [m for m in self._messages if m.parts] self._messages = [m for m in self._messages if m.parts]
class GoogleThinkingConfig(BaseModel):
"""Configuration for controlling the model's internal "thinking" process used before generating a response.
Gemini 2.5 and 3 series models have this thinking process.
Parameters:
thinking_level: Thinking level for Gemini 3 models.
For Gemini 3 Pro, this can be "low" or "high".
For Gemini 3 Flash, this can be "minimal", "low", "medium", or "high".
If not provided, Gemini 3 models default to "high".
Note: Gemini 2.5 series must use thinking_budget instead.
thinking_budget: Token budget for thinking, for Gemini 2.5 series.
-1 for dynamic thinking (model decides), 0 to disable thinking,
or a specific token count (e.g., 128-32768 for 2.5 Pro).
If not provided, most models today default to dynamic thinking.
See https://ai.google.dev/gemini-api/docs/thinking#set-budget
for default values and allowed ranges.
Note: Gemini 3 models must use thinking_level instead.
include_thoughts: Whether to include thought summaries in the response.
Today's models default to not including thoughts (False).
"""
thinking_budget: Optional[int] = Field(default=None)
# Why `| str` here? To not break compatibility in case Google adds more
# levels in the future.
thinking_level: Optional[Literal["low", "high", "medium", "minimal"] | str] = Field(
default=None
)
include_thoughts: Optional[bool] = Field(default=None)
@dataclass @dataclass
class GoogleLLMSettings(LLMSettings): class GoogleLLMSettings(LLMSettings):
"""Settings for Google LLM services. """Settings for Google LLM services.
@@ -681,20 +714,18 @@ class GoogleLLMSettings(LLMSettings):
thinking: Thinking configuration. thinking: Thinking configuration.
""" """
thinking: "GoogleLLMService.ThinkingConfig" | _NotGiven = field( thinking: GoogleThinkingConfig | _NotGiven = field(default_factory=lambda: NOT_GIVEN)
default_factory=lambda: NOT_GIVEN
)
@classmethod @classmethod
def from_mapping(cls, settings): def from_mapping(cls, settings):
"""Convert a plain dict to settings, coercing thinking dicts. """Convert a plain dict to settings, coercing thinking dicts.
For backward compatibility, a ``thinking`` value that is a plain dict For backward compatibility, a ``thinking`` value that is a plain dict
is converted to a :class:`GoogleLLMService.ThinkingConfig`. is converted to a :class:`GoogleThinkingConfig`.
""" """
instance = super().from_mapping(settings) instance = super().from_mapping(settings)
if is_given(instance.thinking) and isinstance(instance.thinking, dict): if is_given(instance.thinking) and isinstance(instance.thinking, dict):
instance.thinking = GoogleLLMService.ThinkingConfig(**instance.thinking) instance.thinking = GoogleThinkingConfig(**instance.thinking)
return instance return instance
@@ -711,37 +742,8 @@ class GoogleLLMService(LLMService):
# Overriding the default adapter to use the Gemini one. # Overriding the default adapter to use the Gemini one.
adapter_class = GeminiLLMAdapter adapter_class = GeminiLLMAdapter
class ThinkingConfig(BaseModel): # Backward compatibility: ThinkingConfig used to be defined inline here.
"""Configuration for controlling the model's internal "thinking" process used before generating a response. ThinkingConfig = GoogleThinkingConfig
Gemini 2.5 and 3 series models have this thinking process.
Parameters:
thinking_level: Thinking level for Gemini 3 models.
For Gemini 3 Pro, this can be "low" or "high".
For Gemini 3 Flash, this can be "minimal", "low", "medium", or "high".
If not provided, Gemini 3 models default to "high".
Note: Gemini 2.5 series must use thinking_budget instead.
thinking_budget: Token budget for thinking, for Gemini 2.5 series.
-1 for dynamic thinking (model decides), 0 to disable thinking,
or a specific token count (e.g., 128-32768 for 2.5 Pro).
If not provided, most models today default to dynamic thinking.
See https://ai.google.dev/gemini-api/docs/thinking#set-budget
for default values and allowed ranges.
Note: Gemini 3 models must use thinking_level instead.
include_thoughts: Whether to include thought summaries in the response.
Today's models default to not including thoughts (False).
"""
thinking_budget: Optional[int] = Field(default=None)
# Why `| str` here? To not break compatibility in case Google adds more
# levels in the future.
thinking_level: Optional[Literal["low", "high", "medium", "minimal"] | str] = Field(
default=None
)
include_thoughts: Optional[bool] = Field(default=None)
class InputParams(BaseModel): class InputParams(BaseModel):
"""Input parameters for Google AI models. """Input parameters for Google AI models.
@@ -764,7 +766,7 @@ class GoogleLLMService(LLMService):
temperature: Optional[float] = Field(default=None, ge=0.0, le=2.0) temperature: Optional[float] = Field(default=None, ge=0.0, le=2.0)
top_k: Optional[int] = Field(default=None, ge=0) top_k: Optional[int] = Field(default=None, ge=0)
top_p: Optional[float] = Field(default=None, ge=0.0, le=1.0) top_p: Optional[float] = Field(default=None, ge=0.0, le=1.0)
thinking: Optional["GoogleLLMService.ThinkingConfig"] = Field(default=None) thinking: Optional[GoogleThinkingConfig] = Field(default=None)
extra: Optional[Dict[str, Any]] = Field(default_factory=dict) extra: Optional[Dict[str, Any]] = Field(default_factory=dict)
def __init__( def __init__(