llm-api-adapter 0.3.0__tar.gz → 0.3.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. {llm_api_adapter-0.3.0 → llm_api_adapter-0.3.2}/PKG-INFO +12 -6
  2. {llm_api_adapter-0.3.0 → llm_api_adapter-0.3.2}/README.md +11 -5
  3. {llm_api_adapter-0.3.0 → llm_api_adapter-0.3.2}/pyproject.toml +1 -1
  4. {llm_api_adapter-0.3.0 → llm_api_adapter-0.3.2}/src/llm_api_adapter/adapters/anthropic_adapter.py +76 -39
  5. {llm_api_adapter-0.3.0 → llm_api_adapter-0.3.2}/src/llm_api_adapter/adapters/google_adapter.py +40 -21
  6. {llm_api_adapter-0.3.0 → llm_api_adapter-0.3.2}/src/llm_api_adapter/adapters/openai_adapter.py +21 -12
  7. {llm_api_adapter-0.3.0 → llm_api_adapter-0.3.2}/src/llm_api_adapter/llm_registry/llm_registry.json +28 -17
  8. {llm_api_adapter-0.3.0 → llm_api_adapter-0.3.2}/src/llm_api_adapter/llms/anthropic/sync_client.py +15 -4
  9. {llm_api_adapter-0.3.0 → llm_api_adapter-0.3.2}/src/llm_api_adapter/models/messages/chat_message.py +14 -7
  10. {llm_api_adapter-0.3.0 → llm_api_adapter-0.3.2}/src/llm_api_adapter/models/responses/chat_response.py +7 -0
  11. {llm_api_adapter-0.3.0 → llm_api_adapter-0.3.2}/src/llm_api_adapter/models/tools/tool_call.py +1 -0
  12. {llm_api_adapter-0.3.0 → llm_api_adapter-0.3.2}/LICENSE +0 -0
  13. {llm_api_adapter-0.3.0 → llm_api_adapter-0.3.2}/MANIFEST.in +0 -0
  14. {llm_api_adapter-0.3.0 → llm_api_adapter-0.3.2}/setup.cfg +0 -0
  15. {llm_api_adapter-0.3.0 → llm_api_adapter-0.3.2}/src/llm_api_adapter/__init__.py +0 -0
  16. {llm_api_adapter-0.3.0 → llm_api_adapter-0.3.2}/src/llm_api_adapter/adapters/__init__.py +0 -0
  17. {llm_api_adapter-0.3.0 → llm_api_adapter-0.3.2}/src/llm_api_adapter/adapters/base_adapter.py +0 -0
  18. {llm_api_adapter-0.3.0 → llm_api_adapter-0.3.2}/src/llm_api_adapter/errors/__init__.py +0 -0
  19. {llm_api_adapter-0.3.0 → llm_api_adapter-0.3.2}/src/llm_api_adapter/errors/config_errors.py +0 -0
  20. {llm_api_adapter-0.3.0 → llm_api_adapter-0.3.2}/src/llm_api_adapter/errors/llm_api_error.py +0 -0
  21. {llm_api_adapter-0.3.0 → llm_api_adapter-0.3.2}/src/llm_api_adapter/llm_registry/__init__.py +0 -0
  22. {llm_api_adapter-0.3.0 → llm_api_adapter-0.3.2}/src/llm_api_adapter/llm_registry/llm_registry.py +0 -0
  23. {llm_api_adapter-0.3.0 → llm_api_adapter-0.3.2}/src/llm_api_adapter/llms/__init__.py +0 -0
  24. {llm_api_adapter-0.3.0 → llm_api_adapter-0.3.2}/src/llm_api_adapter/llms/anthropic/__init__.py +0 -0
  25. {llm_api_adapter-0.3.0 → llm_api_adapter-0.3.2}/src/llm_api_adapter/llms/google/__init__.py +0 -0
  26. {llm_api_adapter-0.3.0 → llm_api_adapter-0.3.2}/src/llm_api_adapter/llms/google/sync_client.py +0 -0
  27. {llm_api_adapter-0.3.0 → llm_api_adapter-0.3.2}/src/llm_api_adapter/llms/openai/__init__.py +0 -0
  28. {llm_api_adapter-0.3.0 → llm_api_adapter-0.3.2}/src/llm_api_adapter/llms/openai/sync_client.py +0 -0
  29. {llm_api_adapter-0.3.0 → llm_api_adapter-0.3.2}/src/llm_api_adapter/models/__init__.py +0 -0
  30. {llm_api_adapter-0.3.0 → llm_api_adapter-0.3.2}/src/llm_api_adapter/models/messages/__init__.py +0 -0
  31. {llm_api_adapter-0.3.0 → llm_api_adapter-0.3.2}/src/llm_api_adapter/models/responses/__init__.py +0 -0
  32. {llm_api_adapter-0.3.0 → llm_api_adapter-0.3.2}/src/llm_api_adapter/models/tools/__init__.py +0 -0
  33. {llm_api_adapter-0.3.0 → llm_api_adapter-0.3.2}/src/llm_api_adapter/models/tools/tool_spec.py +0 -0
  34. {llm_api_adapter-0.3.0 → llm_api_adapter-0.3.2}/src/llm_api_adapter/universal_adapter.py +0 -0
  35. {llm_api_adapter-0.3.0 → llm_api_adapter-0.3.2}/src/llm_api_adapter.egg-info/SOURCES.txt +0 -0
  36. {llm_api_adapter-0.3.0 → llm_api_adapter-0.3.2}/tests/tests_runner.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: llm-api-adapter
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: Lightweight, pluggable adapter for multiple LLM APIs (OpenAI, Anthropic, Google)
5
5
  Author: Sergey Inozemtsev
6
6
  License: MIT License
@@ -41,6 +41,7 @@ Dynamic: license-file
41
41
  # LLM API Adapter SDK for Python
42
42
  [![PyPI Downloads](https://static.pepy.tech/personalized-badge/llm-api-adapter?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads)](https://pepy.tech/projects/llm-api-adapter)
43
43
  [![Tests](https://github.com/Inozem/llm_api_adapter/actions/workflows/ci-main.yml/badge.svg)](https://github.com/Inozem/llm_api_adapter/actions/workflows/ci-main.yml)
44
+ [![codecov](https://codecov.io/github/Inozem/llm_api_adapter/graph/badge.svg?token=T83ZPH1F7Z)](https://codecov.io/github/Inozem/llm_api_adapter)
44
45
 
45
46
  ## Overview
46
47
 
@@ -50,7 +51,7 @@ Currently, the project supports OpenAI, Anthropic, and Google with a consistent
50
51
 
51
52
  ### Version
52
53
 
53
- Current version: 0.3.0
54
+ Current version: 0.3.2
54
55
 
55
56
 
56
57
  ## Features
@@ -183,9 +184,9 @@ The SDK provides a set of standardized errors for easier debugging and integrati
183
184
 
184
185
  The SDK allows you to easily switch between LLM providers and specify the model you want to use. Currently supported providers are OpenAI, Anthropic, and Google.
185
186
 
186
- - **OpenAI**: You can use models like `gpt-5.4`, `gpt-5.2`, `gpt-5.1`, `gpt-5`, `gpt-5-mini`, `gpt-5-nano`, `gpt-4.1`, `gpt-4.1-mini`, `gpt-4.1-nano`, `gpt-4o`, `gpt-4o-mini`.
187
- - **Anthropic**: Available models include `claude-opus-4-6`, `claude-sonnet-4-6`, `claude-opus-4-5`, `claude-sonnet-4-5`, `claude-haiku-4-5`, `claude-opus-4-1`, `claude-opus-4-0`, `claude-sonnet-4-0`, `claude-3-haiku-20240307`.
188
- - **Google**: Models such as `gemini-3.1-pro-preview`, `gemini-3.1-flash-lite-preview`, `gemini-3-flash-preview`, `gemini-2.5-pro`, `gemini-2.5-flash`, `gemini-2.5-flash-lite` can be used.
187
+ - **OpenAI**: You can use models like `gpt-5.5`, `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.4-nano`, `gpt-5.2`, `gpt-5.1`, `gpt-5`, `gpt-5-mini`, `gpt-5-nano`, `gpt-4.1`, `gpt-4.1-mini`, `gpt-4.1-nano`, `gpt-4o`, `gpt-4o-mini`.
188
+ - **Anthropic**: Available models include `claude-opus-4-8`, `claude-opus-4-7`, `claude-opus-4-6`, `claude-sonnet-4-6`, `claude-opus-4-5`, `claude-sonnet-4-5`, `claude-haiku-4-5`, `claude-opus-4-1`.
189
+ - **Google**: Models such as `gemini-3.5-flash`, `gemini-3.1-pro-preview`, `gemini-3.1-flash-lite`, `gemini-3-flash-preview`, `gemini-2.5-pro`, `gemini-2.5-flash` can be used.
189
190
 
190
191
  Example:
191
192
 
@@ -487,6 +488,7 @@ first = adapter.chat(
487
488
  messages=messages,
488
489
  tools=tools,
489
490
  tool_choice="auto",
491
+ max_tokens=1000,
490
492
  )
491
493
 
492
494
  if first.tool_calls:
@@ -501,7 +503,11 @@ if first.tool_calls:
501
503
  )
502
504
  )
503
505
 
504
- final = adapter.chat(messages=messages)
506
+ final = adapter.chat(
507
+ messages=messages,
508
+ previous_response=first,
509
+ max_tokens=1000
510
+ )
505
511
  print(final.content)
506
512
  ```
507
513
 
@@ -1,6 +1,7 @@
1
1
  # LLM API Adapter SDK for Python
2
2
  [![PyPI Downloads](https://static.pepy.tech/personalized-badge/llm-api-adapter?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads)](https://pepy.tech/projects/llm-api-adapter)
3
3
  [![Tests](https://github.com/Inozem/llm_api_adapter/actions/workflows/ci-main.yml/badge.svg)](https://github.com/Inozem/llm_api_adapter/actions/workflows/ci-main.yml)
4
+ [![codecov](https://codecov.io/github/Inozem/llm_api_adapter/graph/badge.svg?token=T83ZPH1F7Z)](https://codecov.io/github/Inozem/llm_api_adapter)
4
5
 
5
6
  ## Overview
6
7
 
@@ -10,7 +11,7 @@ Currently, the project supports OpenAI, Anthropic, and Google with a consistent
10
11
 
11
12
  ### Version
12
13
 
13
- Current version: 0.3.0
14
+ Current version: 0.3.2
14
15
 
15
16
 
16
17
  ## Features
@@ -143,9 +144,9 @@ The SDK provides a set of standardized errors for easier debugging and integrati
143
144
 
144
145
  The SDK allows you to easily switch between LLM providers and specify the model you want to use. Currently supported providers are OpenAI, Anthropic, and Google.
145
146
 
146
- - **OpenAI**: You can use models like `gpt-5.4`, `gpt-5.2`, `gpt-5.1`, `gpt-5`, `gpt-5-mini`, `gpt-5-nano`, `gpt-4.1`, `gpt-4.1-mini`, `gpt-4.1-nano`, `gpt-4o`, `gpt-4o-mini`.
147
- - **Anthropic**: Available models include `claude-opus-4-6`, `claude-sonnet-4-6`, `claude-opus-4-5`, `claude-sonnet-4-5`, `claude-haiku-4-5`, `claude-opus-4-1`, `claude-opus-4-0`, `claude-sonnet-4-0`, `claude-3-haiku-20240307`.
148
- - **Google**: Models such as `gemini-3.1-pro-preview`, `gemini-3.1-flash-lite-preview`, `gemini-3-flash-preview`, `gemini-2.5-pro`, `gemini-2.5-flash`, `gemini-2.5-flash-lite` can be used.
147
+ - **OpenAI**: You can use models like `gpt-5.5`, `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.4-nano`, `gpt-5.2`, `gpt-5.1`, `gpt-5`, `gpt-5-mini`, `gpt-5-nano`, `gpt-4.1`, `gpt-4.1-mini`, `gpt-4.1-nano`, `gpt-4o`, `gpt-4o-mini`.
148
+ - **Anthropic**: Available models include `claude-opus-4-8`, `claude-opus-4-7`, `claude-opus-4-6`, `claude-sonnet-4-6`, `claude-opus-4-5`, `claude-sonnet-4-5`, `claude-haiku-4-5`, `claude-opus-4-1`.
149
+ - **Google**: Models such as `gemini-3.5-flash`, `gemini-3.1-pro-preview`, `gemini-3.1-flash-lite`, `gemini-3-flash-preview`, `gemini-2.5-pro`, `gemini-2.5-flash` can be used.
149
150
 
150
151
  Example:
151
152
 
@@ -447,6 +448,7 @@ first = adapter.chat(
447
448
  messages=messages,
448
449
  tools=tools,
449
450
  tool_choice="auto",
451
+ max_tokens=1000,
450
452
  )
451
453
 
452
454
  if first.tool_calls:
@@ -461,7 +463,11 @@ if first.tool_calls:
461
463
  )
462
464
  )
463
465
 
464
- final = adapter.chat(messages=messages)
466
+ final = adapter.chat(
467
+ messages=messages,
468
+ previous_response=first,
469
+ max_tokens=1000
470
+ )
465
471
  print(final.content)
466
472
  ```
467
473
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "llm-api-adapter"
7
- version = "0.3.0"
7
+ version = "0.3.2"
8
8
  description = "Lightweight, pluggable adapter for multiple LLM APIs (OpenAI, Anthropic, Google)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -32,19 +32,19 @@ class AnthropicAdapter(LLMAdapterBase):
32
32
  tools: Optional[List[ToolSpec]] = None,
33
33
  tool_choice: Optional[str | dict] = None,
34
34
  parallel_tool_calls: Optional[bool] = None,
35
- previous_response_id: Optional[str] = None,
35
+ previous_response: Optional[ChatResponse] = None,
36
36
  ) -> ChatResponse:
37
- temperature = self._validate_parameter(
38
- name="temperature", value=temperature, min_value=0, max_value=2
39
- )
40
- top_p = self._validate_parameter(name="top_p", value=top_p, min_value=0, max_value=1)
37
+ temperature = self._validate_parameter("temperature", temperature, 0, 2)
38
+ top_p = self._validate_parameter("top_p", top_p, 0, 1)
41
39
  try:
42
40
  self._validate_tools(tools)
43
41
  validated_tools = tools
44
- normalized_tool_choice = self._normalize_tool_choice(tool_choice, validated_tools)
42
+ normalized_tool_choice = self._normalize_tool_choice(
43
+ tool_choice,
44
+ validated_tools,
45
+ )
45
46
  normalized_messages = self._normalize_messages(messages)
46
47
  system_prompt, transformed_messages = normalized_messages.to_anthropic()
47
- client = ClaudeSyncClient(api_key=self.api_key)
48
48
  params: Dict[str, Any] = {
49
49
  "model": self.model,
50
50
  "messages": transformed_messages,
@@ -55,23 +55,36 @@ class AnthropicAdapter(LLMAdapterBase):
55
55
  "timeout_s": timeout_s,
56
56
  }
57
57
  if validated_tools:
58
- params["tools"] = [self._to_anthropic_tool(t) for t in validated_tools]
58
+ params["tools"] = [
59
+ self._to_anthropic_tool(tool)
60
+ for tool in validated_tools
61
+ ]
59
62
  if normalized_tool_choice is not None:
60
- params["tool_choice"] = self._to_anthropic_tool_choice(normalized_tool_choice)
63
+ params["tool_choice"] = self._to_anthropic_tool_choice(
64
+ normalized_tool_choice
65
+ )
61
66
  if parallel_tool_calls is False:
62
67
  params["disable_parallel_tool_use"] = True
63
68
  elif parallel_tool_calls is True:
64
69
  params["disable_parallel_tool_use"] = False
65
70
  if reasoning_level:
66
- normalized_reasoning_level = self._normalize_reasoning_level(reasoning_level)
71
+ normalized_reasoning_level = self._normalize_reasoning_level(
72
+ reasoning_level
73
+ )
67
74
  if normalized_reasoning_level:
68
75
  self.validate_reasoning_and_tokens(
69
76
  max_tokens=max_tokens,
70
77
  reasoning_level=reasoning_level,
71
78
  normalized_reasoning_level=normalized_reasoning_level,
72
79
  )
73
- params["thinking"] = {"type": "enabled", "budget_tokens": normalized_reasoning_level}
80
+ params["budget_tokens"] = normalized_reasoning_level
81
+ if self.is_reasoning:
82
+ effort = self._reasoning_level_to_effort(reasoning_level)
83
+ if effort:
84
+ params["effort"] = effort
74
85
  params = {k: v for k, v in params.items() if v is not None}
86
+ _ = previous_response
87
+ client = ClaudeSyncClient(api_key=self.api_key)
75
88
  response = client.chat_completion(**params)
76
89
  chat_response = ChatResponse.from_anthropic_response(response)
77
90
  if self.pricing:
@@ -88,12 +101,18 @@ class AnthropicAdapter(LLMAdapterBase):
88
101
  self.handle_error(error=e, error_message=error_message)
89
102
 
90
103
  def _to_anthropic_tool(self, tool: ToolSpec) -> Dict[str, Any]:
91
- payload: Dict[str, Any] = {"name": tool.name, "input_schema": tool.json_schema}
104
+ payload: Dict[str, Any] = {
105
+ "name": tool.name,
106
+ "input_schema": tool.json_schema,
107
+ }
92
108
  if tool.description:
93
109
  payload["description"] = tool.description
94
110
  return payload
95
111
 
96
- def _to_anthropic_tool_choice(self, tool_choice: Optional[str]) -> Optional[Dict[str, Any]]:
112
+ def _to_anthropic_tool_choice(
113
+ self,
114
+ tool_choice: Optional[str],
115
+ ) -> Optional[Dict[str, Any]]:
97
116
  if tool_choice is None:
98
117
  return None
99
118
  if tool_choice == "none":
@@ -102,42 +121,60 @@ class AnthropicAdapter(LLMAdapterBase):
102
121
  return {"type": tool_choice}
103
122
  return {"type": "tool", "name": tool_choice}
104
123
 
124
+ def _validate_not_bool(self, level) -> None:
125
+ if isinstance(level, bool):
126
+ raise ValueError("Invalid type for level: bool is not accepted")
127
+
128
+ def _validate_reasoning_str(self, level: str) -> None:
129
+ if level not in self.reasoning_levels:
130
+ raise ValueError(
131
+ f"Unknown reasoning level key: {level!r}. "
132
+ f"Valid keys: {list(self.reasoning_levels.keys())}"
133
+ )
134
+
135
+ def _resolve_reasoning_int(self, level: int) -> int:
136
+ if isinstance(level, int):
137
+ return level
138
+ raise ValueError(
139
+ "Invalid type for level: expected int or str, "
140
+ f"got {type(level).__name__!r}"
141
+ )
142
+
143
+ def _reasoning_level_to_effort(self, level: str | int) -> str | None:
144
+ self._validate_not_bool(level)
145
+ if isinstance(level, str):
146
+ self._validate_reasoning_str(level)
147
+ return None if level == "none" else level
148
+ numeric = self._resolve_reasoning_int(level)
149
+ for key, threshold in self.reasoning_levels.items():
150
+ if threshold > 0 and numeric <= threshold:
151
+ return key
152
+ return list(self.reasoning_levels)[-1]
153
+
105
154
  def _normalize_reasoning_level(self, level: str | int) -> int | None:
106
155
  minimum_level = 1024
107
- normalized_level: int | None = None
108
- if level is not None and not self.is_reasoning:
156
+ if not self.is_reasoning:
109
157
  warning_message = (
110
158
  f"Model '{self.model}' does not support reasoning — reasoning disabled."
111
159
  )
112
160
  warnings.warn(warning_message, UserWarning)
113
161
  logger.info(warning_message)
114
162
  return None
115
- if isinstance(level, bool):
116
- raise ValueError("Invalid type for level: bool is not accepted")
163
+ self._validate_not_bool(level)
117
164
  if isinstance(level, str):
118
- if level in self.reasoning_levels:
119
- normalized_level = self.reasoning_levels[level]
120
- else:
121
- raise ValueError(
122
- f"Unknown reasoning level key: {level!r}. "
123
- f"Valid keys: {list(self.reasoning_levels.keys())}"
124
- )
125
- if isinstance(level, int):
126
- normalized_level = level
127
- if normalized_level is not None:
128
- if normalized_level >= minimum_level:
129
- return normalized_level
130
- warning_message = (
131
- f"Reasoning level '{level}' is below the minimum supported value {minimum_level}; "
132
- f"using {minimum_level} instead."
133
- )
134
- warnings.warn(warning_message, UserWarning)
135
- logger.info(warning_message)
136
- return minimum_level
137
- raise ValueError(
138
- "Invalid type for level: expected int or str, "
139
- f"got {type(level).__name__!r}"
165
+ self._validate_reasoning_str(level)
166
+ numeric = self.reasoning_levels[level]
167
+ else:
168
+ numeric = self._resolve_reasoning_int(level)
169
+ if numeric >= minimum_level:
170
+ return numeric
171
+ warning_message = (
172
+ f"Reasoning level '{level}' is below the minimum supported value "
173
+ f"{minimum_level}; using {minimum_level} instead."
140
174
  )
175
+ warnings.warn(warning_message, UserWarning)
176
+ logger.info(warning_message)
177
+ return minimum_level
141
178
 
142
179
  def validate_reasoning_and_tokens(
143
180
  self,
@@ -31,29 +31,39 @@ class GoogleAdapter(LLMAdapterBase):
31
31
  tools: Optional[List[ToolSpec]] = None,
32
32
  tool_choice: Optional[str | dict] = None,
33
33
  parallel_tool_calls: Optional[bool] = None,
34
- previous_response_id: Optional[str] = None,
34
+ previous_response: Optional[ChatResponse] = None,
35
35
  ) -> ChatResponse:
36
36
  temperature = self._validate_parameter(
37
- name="temperature", value=temperature, min_value=0, max_value=2
37
+ name="temperature",
38
+ value=temperature,
39
+ min_value=0,
40
+ max_value=2,
38
41
  )
39
42
  top_p = self._validate_parameter(
40
- name="top_p", value=top_p, min_value=0, max_value=1
43
+ name="top_p",
44
+ value=top_p,
45
+ min_value=0,
46
+ max_value=1,
41
47
  )
42
48
  try:
43
49
  self._validate_tools(tools)
44
50
  validated_tools = tools
45
51
  normalized_tool_choice = self._normalize_tool_choice(
46
- tool_choice, validated_tools
52
+ tool_choice,
53
+ validated_tools,
47
54
  )
48
55
  normalized_messages = self._normalize_messages(messages)
56
+ _ = previous_response
49
57
  system_prompt, transformed_messages = normalized_messages.to_google()
50
- generation_config = {
58
+ generation_config: Dict[str, Any] = {
51
59
  "maxOutputTokens": max_tokens,
52
60
  "temperature": temperature,
53
61
  "topP": top_p,
54
62
  }
55
63
  if reasoning_level:
56
- normalized_reasoning_level = self._normalize_reasoning_level(reasoning_level)
64
+ normalized_reasoning_level = self._normalize_reasoning_level(
65
+ reasoning_level
66
+ )
57
67
  if normalized_reasoning_level is not None:
58
68
  generation_config["thinkingConfig"] = {
59
69
  "thinkingBudget": normalized_reasoning_level,
@@ -64,10 +74,17 @@ class GoogleAdapter(LLMAdapterBase):
64
74
  "generationConfig": generation_config,
65
75
  }
66
76
  if system_prompt:
67
- payload["system_instruction"] = {"parts": [{"text": system_prompt}]}
77
+ payload["system_instruction"] = {
78
+ "parts": [{"text": system_prompt}]
79
+ }
68
80
  if validated_tools:
69
81
  payload["tools"] = [
70
- {"functionDeclarations": [self._to_google_function_declaration(t) for t in validated_tools]}
82
+ {
83
+ "functionDeclarations": [
84
+ self._to_google_function_declaration(tool)
85
+ for tool in validated_tools
86
+ ]
87
+ }
71
88
  ]
72
89
  tool_config = self._to_google_tool_config(normalized_tool_choice)
73
90
  if tool_config is not None:
@@ -94,34 +111,36 @@ class GoogleAdapter(LLMAdapterBase):
94
111
  self.handle_error(error=e, error_message=error_message)
95
112
 
96
113
  def _to_google_function_declaration(self, tool: ToolSpec) -> Dict[str, Any]:
97
- decl: Dict[str, Any] = {
114
+ declaration: Dict[str, Any] = {
98
115
  "name": tool.name,
99
116
  "parametersJsonSchema": tool.json_schema,
100
117
  }
101
118
  if tool.description:
102
- decl["description"] = tool.description
103
- return decl
119
+ declaration["description"] = tool.description
120
+ return declaration
104
121
 
105
- def _to_google_tool_config(self, tool_choice: Optional[str], ) -> Optional[Dict[str, Any]]:
122
+ def _to_google_tool_config(
123
+ self,
124
+ tool_choice: Optional[str],
125
+ ) -> Optional[Dict[str, Any]]:
106
126
  if tool_choice is None:
107
127
  return None
108
128
  if tool_choice == "none":
109
129
  mode = "NONE"
110
- allowed = None
130
+ allowed_function_names = None
111
131
  elif tool_choice == "auto":
112
132
  mode = "AUTO"
113
- allowed = None
133
+ allowed_function_names = None
114
134
  elif tool_choice == "any":
115
135
  mode = "ANY"
116
- allowed = None
136
+ allowed_function_names = None
117
137
  else:
118
138
  mode = "ANY"
119
- allowed = [tool_choice]
120
-
121
- cfg: Dict[str, Any] = {"mode": mode}
122
- if allowed:
123
- cfg["allowedFunctionNames"] = allowed
124
- return {"functionCallingConfig": cfg}
139
+ allowed_function_names = [tool_choice]
140
+ function_calling_config: Dict[str, Any] = {"mode": mode}
141
+ if allowed_function_names:
142
+ function_calling_config["allowedFunctionNames"] = allowed_function_names
143
+ return {"functionCallingConfig": function_calling_config}
125
144
 
126
145
  def _normalize_reasoning_level(self, level: str | int | None) -> int | None:
127
146
  minimum_level = 0
@@ -30,7 +30,7 @@ class OpenAIAdapter(LLMAdapterBase):
30
30
  tools: Optional[List[ToolSpec]] = None,
31
31
  tool_choice: Any = None,
32
32
  parallel_tool_calls: Optional[bool] = None,
33
- previous_response_id: Optional[str] = None,
33
+ previous_response: Optional[ChatResponse] = None,
34
34
  ) -> ChatResponse:
35
35
  temperature = self._validate_parameter(
36
36
  name="temperature",
@@ -51,7 +51,13 @@ class OpenAIAdapter(LLMAdapterBase):
51
51
  client = OpenAISyncClient(api_key=self.api_key)
52
52
  normalized_messages = self._normalize_messages(messages)
53
53
  use_responses_api = client._should_use_responses_api(self.model)
54
- normalized_reasoning_level = self._normalize_reasoning_level(reasoning_level)
54
+ normalized_reasoning_level = self._normalize_reasoning_level(
55
+ reasoning_level
56
+ )
57
+
58
+ previous_response_id: Optional[str] = None
59
+ if previous_response is not None:
60
+ previous_response_id = previous_response.response_id
55
61
 
56
62
  if use_responses_api:
57
63
  transformed_messages = normalized_messages.to_openai_responses_input()
@@ -111,21 +117,24 @@ class OpenAIAdapter(LLMAdapterBase):
111
117
  error_message = getattr(e, "text", None) or str(e)
112
118
  self.handle_error(error=e, error_message=error_message)
113
119
 
114
- def _map_tools_to_openai(self, tools: Optional[List[ToolSpec]]) -> Optional[List[Dict[str, Any]]]:
120
+ def _map_tools_to_openai(
121
+ self,
122
+ tools: Optional[List[ToolSpec]],
123
+ ) -> Optional[List[Dict[str, Any]]]:
115
124
  if not tools:
116
125
  return None
117
126
  mapped: List[Dict[str, Any]] = []
118
- for t in tools:
119
- fn: Dict[str, Any] = {
120
- "name": t.name,
121
- "parameters": t.json_schema,
127
+ for tool in tools:
128
+ function_payload: Dict[str, Any] = {
129
+ "name": tool.name,
130
+ "parameters": tool.json_schema,
122
131
  }
123
- if t.description:
124
- fn["description"] = t.description
132
+ if tool.description:
133
+ function_payload["description"] = tool.description
125
134
  mapped.append(
126
135
  {
127
136
  "type": "function",
128
- "function": fn,
137
+ "function": function_payload,
129
138
  }
130
139
  )
131
140
  return mapped
@@ -167,7 +176,7 @@ class OpenAIAdapter(LLMAdapterBase):
167
176
  "Invalid type for level: expected int or str, "
168
177
  f"got {type(level).__name__!r}"
169
178
  )
170
-
179
+
171
180
  def _map_tools_to_openai_responses(
172
181
  self,
173
182
  tools: Optional[List[ToolSpec]],
@@ -186,7 +195,7 @@ class OpenAIAdapter(LLMAdapterBase):
186
195
  }
187
196
  )
188
197
  return result
189
-
198
+
190
199
  def _map_tool_choice_to_openai_responses(self, tool_choice: Any) -> Any:
191
200
  if tool_choice is None:
192
201
  return None
@@ -1,14 +1,26 @@
1
1
  {
2
- "schema_version": 5,
3
- "effective_date": "2026-03-08",
2
+ "schema_version": 6,
3
+ "effective_date": "2026-06-22",
4
4
  "providers": {
5
5
  "openai": {
6
6
  "currency": "USD",
7
7
  "models": {
8
+ "gpt-5.5": {
9
+ "pricing": {"in_per_1m": 5.0, "out_per_1m": 30.0},
10
+ "is_reasoning": true
11
+ },
8
12
  "gpt-5.4": {
9
13
  "pricing": {"in_per_1m": 2.5, "out_per_1m": 15.0},
10
14
  "is_reasoning": true
11
15
  },
16
+ "gpt-5.4-mini": {
17
+ "pricing": {"in_per_1m": 0.75, "out_per_1m": 4.5},
18
+ "is_reasoning": true
19
+ },
20
+ "gpt-5.4-nano": {
21
+ "pricing": {"in_per_1m": 0.2, "out_per_1m": 1.25},
22
+ "is_reasoning": true
23
+ },
12
24
  "gpt-5.2": {
13
25
  "pricing": {"in_per_1m": 1.75, "out_per_1m": 14.0},
14
26
  "is_reasoning": true
@@ -39,6 +51,14 @@
39
51
  "anthropic": {
40
52
  "currency": "USD",
41
53
  "models": {
54
+ "claude-opus-4-8": {
55
+ "pricing": {"in_per_1m": 5.0, "out_per_1m": 25.0},
56
+ "is_reasoning": true
57
+ },
58
+ "claude-opus-4-7": {
59
+ "pricing": {"in_per_1m": 5.0, "out_per_1m": 25.0},
60
+ "is_reasoning": true
61
+ },
42
62
  "claude-opus-4-6": {
43
63
  "pricing": {"in_per_1m": 5.0, "out_per_1m": 25.0},
44
64
  "is_reasoning": true
@@ -62,26 +82,21 @@
62
82
  "claude-opus-4-1": {
63
83
  "pricing": {"in_per_1m": 15.0, "out_per_1m": 75.0},
64
84
  "is_reasoning": true
65
- },
66
- "claude-opus-4-0": {
67
- "pricing": {"in_per_1m": 15.0, "out_per_1m": 75.0},
68
- "is_reasoning": true
69
- },
70
- "claude-sonnet-4-0": {
71
- "pricing": {"in_per_1m": 3.0, "out_per_1m": 15.0},
72
- "is_reasoning": true
73
- },
74
- "claude-3-haiku-20240307": {"pricing": {"in_per_1m": 0.25, "out_per_1m": 1.25}}
85
+ }
75
86
  }
76
87
  },
77
88
  "google": {
78
89
  "currency": "USD",
79
90
  "models": {
91
+ "gemini-3.5-flash": {
92
+ "pricing": {"in_per_1m": 1.5, "out_per_1m": 9.0},
93
+ "is_reasoning": true
94
+ },
80
95
  "gemini-3.1-pro-preview": {
81
96
  "pricing": {"in_per_1m": 2.0, "out_per_1m": 12.0},
82
97
  "is_reasoning": true
83
98
  },
84
- "gemini-3.1-flash-lite-preview": {
99
+ "gemini-3.1-flash-lite": {
85
100
  "pricing": {"in_per_1m": 0.25, "out_per_1m": 1.5},
86
101
  "is_reasoning": true
87
102
  },
@@ -96,10 +111,6 @@
96
111
  "gemini-2.5-flash": {
97
112
  "pricing": {"in_per_1m": 0.3, "out_per_1m": 2.5},
98
113
  "is_reasoning": true
99
- },
100
- "gemini-2.5-flash-lite": {
101
- "pricing": {"in_per_1m": 0.1, "out_per_1m": 0.4},
102
- "is_reasoning": true
103
114
  }
104
115
  }
105
116
  }
@@ -14,6 +14,8 @@ from ...errors.llm_api_error import (
14
14
 
15
15
  logger = logging.getLogger(__name__)
16
16
 
17
+ _ADAPTIVE_THINKING_MODELS = ("claude-opus-4-6", "claude-sonnet-4-6", "claude-opus-4-7", "claude-opus-4-8")
18
+
17
19
  @dataclass
18
20
  class ClaudeSyncClient:
19
21
  api_key: str
@@ -34,11 +36,20 @@ class ClaudeSyncClient:
34
36
  return response.json()
35
37
 
36
38
  def _prepare_chat_payload_for_model(self, model: str, kwargs: dict) -> dict:
37
- if model.startswith(
38
- ("claude-sonnet-4-5", "claude-opus-4-1", "claude-haiku-4-5", "claude-opus-4-5",
39
- "claude-opus-4-6", "claude-sonnet-4-6")
40
- ):
39
+ budget_tokens = kwargs.pop("budget_tokens", None)
40
+ effort = kwargs.pop("effort", None)
41
+ if model.startswith(_ADAPTIVE_THINKING_MODELS):
41
42
  kwargs.pop("top_p", None)
43
+ if effort:
44
+ kwargs["thinking"] = {"type": "adaptive"}
45
+ kwargs["output_config"] = {"effort": effort}
46
+ else:
47
+ if model.startswith(
48
+ ("claude-sonnet-4-5", "claude-opus-4-1", "claude-haiku-4-5", "claude-opus-4-5")
49
+ ):
50
+ kwargs.pop("top_p", None)
51
+ if budget_tokens:
52
+ kwargs["thinking"] = {"type": "enabled", "budget_tokens": budget_tokens}
42
53
  return {"model": model, **kwargs}
43
54
 
44
55
  def _send_request(self, url: str, payload: dict, timeout_s: float | None = None):
@@ -105,14 +105,17 @@ class AIMessage(Message):
105
105
  parts.append({"text": self.content})
106
106
  if self.tool_calls:
107
107
  for tc in self.tool_calls:
108
- parts.append(
109
- {
110
- "functionCall": {
111
- "name": tc.name,
112
- "args": tc.arguments or {},
113
- }
108
+ part: Dict[str, Any] = {
109
+ "functionCall": {
110
+ "name": tc.name,
111
+ "args": tc.arguments or {},
114
112
  }
115
- )
113
+ }
114
+ if tc.provider_data:
115
+ thought_signature = tc.provider_data.get("thoughtSignature")
116
+ if thought_signature is not None:
117
+ part["thoughtSignature"] = thought_signature
118
+ parts.append(part)
116
119
  if not parts:
117
120
  parts = [{"text": ""}]
118
121
  return {"role": "model", "parts": parts}
@@ -262,10 +265,14 @@ class Messages:
262
265
  if not isinstance(arguments, dict):
263
266
  raise ValueError("assistant.tool_calls.arguments must be dict")
264
267
  call_id = tool_call_raw.get("call_id")
268
+ provider_data = tool_call_raw.get("provider_data")
269
+ if provider_data is not None and not isinstance(provider_data, dict):
270
+ raise ValueError("assistant.tool_calls.provider_data must be dict")
265
271
  return ToolCall(
266
272
  name=name,
267
273
  arguments=arguments,
268
274
  call_id=call_id,
275
+ provider_data=provider_data,
269
276
  )
270
277
 
271
278
  def _parse_legacy_openai_tool_call(self, tool_call_raw: Dict[str, Any]) -> ToolCall:
@@ -253,11 +253,18 @@ class ChatResponse:
253
253
  raise InvalidToolArgumentsError(
254
254
  detail=f"Google functionCall args must be dict for tool={name!r}"
255
255
  )
256
+ thought_signature = part.get("thoughtSignature")
257
+ provider_data = None
258
+ if thought_signature is not None:
259
+ provider_data = {
260
+ "thoughtSignature": thought_signature,
261
+ }
256
262
  parsed_tool_calls.append(
257
263
  ToolCall(
258
264
  name=name,
259
265
  arguments=args,
260
266
  call_id=name,
267
+ provider_data=provider_data,
261
268
  )
262
269
  )
263
270
  return cls(
@@ -14,3 +14,4 @@ class ToolCall:
14
14
  name: str
15
15
  arguments: Dict[str, Any]
16
16
  call_id: Optional[str] = None
17
+ provider_data: Optional[dict] = None
File without changes