llm-api-adapter 0.3.2__tar.gz → 0.4.0__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.2 → llm_api_adapter-0.4.0}/PKG-INFO +85 -3
  2. {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/README.md +84 -2
  3. {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/pyproject.toml +1 -1
  4. {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/adapters/anthropic_adapter.py +10 -0
  5. {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/adapters/base_adapter.py +40 -1
  6. {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/adapters/google_adapter.py +23 -0
  7. {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/adapters/openai_adapter.py +22 -0
  8. {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/errors/llm_api_error.py +6 -0
  9. {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/llm_registry/llm_registry.json +9 -1
  10. {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/llms/anthropic/sync_client.py +3 -2
  11. {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/models/responses/chat_response.py +1 -0
  12. {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/LICENSE +0 -0
  13. {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/MANIFEST.in +0 -0
  14. {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/setup.cfg +0 -0
  15. {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/__init__.py +0 -0
  16. {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/adapters/__init__.py +0 -0
  17. {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/errors/__init__.py +0 -0
  18. {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/errors/config_errors.py +0 -0
  19. {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/llm_registry/__init__.py +0 -0
  20. {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/llm_registry/llm_registry.py +0 -0
  21. {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/llms/__init__.py +0 -0
  22. {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/llms/anthropic/__init__.py +0 -0
  23. {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/llms/google/__init__.py +0 -0
  24. {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/llms/google/sync_client.py +0 -0
  25. {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/llms/openai/__init__.py +0 -0
  26. {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/llms/openai/sync_client.py +0 -0
  27. {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/models/__init__.py +0 -0
  28. {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/models/messages/__init__.py +0 -0
  29. {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/models/messages/chat_message.py +0 -0
  30. {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/models/responses/__init__.py +0 -0
  31. {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/models/tools/__init__.py +0 -0
  32. {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/models/tools/tool_call.py +0 -0
  33. {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/models/tools/tool_spec.py +0 -0
  34. {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/universal_adapter.py +0 -0
  35. {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter.egg-info/SOURCES.txt +0 -0
  36. {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/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.2
3
+ Version: 0.4.0
4
4
  Summary: Lightweight, pluggable adapter for multiple LLM APIs (OpenAI, Anthropic, Google)
5
5
  Author: Sergey Inozemtsev
6
6
  License: MIT License
@@ -51,7 +51,7 @@ Currently, the project supports OpenAI, Anthropic, and Google with a consistent
51
51
 
52
52
  ### Version
53
53
 
54
- Current version: 0.3.2
54
+ Current version: 0.4.0
55
55
 
56
56
 
57
57
  ## Features
@@ -67,6 +67,7 @@ Current version: 0.3.2
67
67
  - **Pricing Registry**: Model prices are stored in a unified JSON registry with per-model input/output pricing and currency support.
68
68
  - **Unified Reasoning Support**: A single `reasoning_level` parameter that works identically across all providers.
69
69
  - **Request Timeouts:** Per-request timeout control with a unified `timeout_s` parameter.
70
+ - **Strict JSON Mode**: Pass a JSON Schema to `chat()` and get a parsed object in `ChatResponse.parsed_json` — provider normalization is handled automatically.
70
71
 
71
72
  ## Installation
72
73
 
@@ -172,6 +173,14 @@ The SDK provides a set of standardized errors for easier debugging and integrati
172
173
 
173
174
  - **LLMAPIUsageLimitError**: Raised when usage limits are exceeded.
174
175
 
176
+ - **InvalidToolSchemaError**: Raised when a provided tool schema is invalid.
177
+
178
+ - **InvalidToolArgumentsError**: Raised when tool arguments cannot be parsed or validated.
179
+
180
+ - **ToolChoiceError**: Raised when `tool_choice` is invalid or references an unknown tool.
181
+
182
+ - **JSONSchemaError**: Raised when `json_schema` is not a `dict`, is passed together with `tools`, or the model response is not valid JSON.
183
+
175
184
  ### Config Errors
176
185
 
177
186
  - **LLMConfigError**: Raised when the request configuration is invalid or incompatible.
@@ -185,7 +194,7 @@ The SDK provides a set of standardized errors for easier debugging and integrati
185
194
  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.
186
195
 
187
196
  - **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`.
197
+ - **Anthropic**: Available models include `claude-fable-5`, `claude-sonnet-5`, `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
198
  - **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.
190
199
 
191
200
  Example:
@@ -284,6 +293,7 @@ The `ChatResponse` object returned by `chat` includes:
284
293
  8. **cost\_total**: Total combined cost.
285
294
  9. **content**: The generated text response.
286
295
  10. **finish\_reason**: Reason why generation stopped (e.g., `"stop"`, `"length"`).
296
+ 11. **parsed\_json**: Parsed JSON object when `json_schema` was provided, otherwise `None`.
287
297
 
288
298
  ## Timeout Support
289
299
 
@@ -511,6 +521,78 @@ if first.tool_calls:
511
521
  print(final.content)
512
522
  ```
513
523
 
524
+ ## Strict JSON Mode
525
+
526
+ The SDK supports structured JSON output via a `json_schema` parameter in `chat()`. Pass any JSON Schema object — the adapter normalizes it for each provider automatically and returns the parsed result in `ChatResponse.parsed_json`.
527
+
528
+ ```python
529
+ from llm_api_adapter.models.messages.chat_message import Prompt, UserMessage
530
+ from llm_api_adapter.universal_adapter import UniversalLLMAPIAdapter
531
+
532
+ schema = {
533
+ "type": "object",
534
+ "properties": {
535
+ "name": {"type": "string"},
536
+ "age": {"type": "integer"},
537
+ },
538
+ "required": ["name", "age"],
539
+ }
540
+
541
+ adapter = UniversalLLMAPIAdapter(
542
+ organization="openai",
543
+ model="gpt-5",
544
+ api_key=openai_api_key,
545
+ )
546
+
547
+ response = adapter.chat(
548
+ messages=[
549
+ Prompt("Extract structured data from the user's message."),
550
+ UserMessage("My name is Alice and I'm 30 years old."),
551
+ ],
552
+ json_schema=schema,
553
+ max_tokens=200,
554
+ )
555
+
556
+ print(response.content) # '{"name": "Alice", "age": 30}'
557
+ print(response.parsed_json) # {"name": "Alice", "age": 30}
558
+ ```
559
+
560
+ ### `json_schema` parameter
561
+
562
+ - Accepts a `dict` containing a JSON Schema object.
563
+ - Cannot be combined with `tools` — passing both raises `JSONSchemaError`.
564
+ - If the model returns a response that is not valid JSON, `JSONSchemaError` is raised.
565
+
566
+ ### `parsed_json` field
567
+
568
+ `ChatResponse.parsed_json` contains the parsed `dict` when `json_schema` was provided, or `None` otherwise.
569
+
570
+ ### Provider behavior
571
+
572
+ | Provider | Implementation |
573
+ |----------|----------------|
574
+ | **OpenAI** (standard) | Native `response_format.type=json_schema` with `strict=true` |
575
+ | **OpenAI** (Responses API / o-series) | Native `text.format.type=json_schema` with `strict=true` |
576
+ | **Anthropic** | Native `output_config.format.type=json_schema` |
577
+ | **Google** | `generationConfig.responseMimeType="application/json"` + `responseSchema` |
578
+
579
+ The adapter automatically handles provider-specific schema constraints, so the same schema works across all providers without changes.
580
+
581
+ ### Error handling
582
+
583
+ ```python
584
+ from llm_api_adapter.errors.llm_api_error import JSONSchemaError
585
+
586
+ try:
587
+ response = adapter.chat(
588
+ messages=[UserMessage("Give me the data.")],
589
+ json_schema=schema,
590
+ max_tokens=200,
591
+ )
592
+ except JSONSchemaError as e:
593
+ print(f"JSON schema error: {e}")
594
+ ```
595
+
514
596
  ## Token Usage and Pricing
515
597
 
516
598
  ### Token Usage and Pricing Example
@@ -11,7 +11,7 @@ Currently, the project supports OpenAI, Anthropic, and Google with a consistent
11
11
 
12
12
  ### Version
13
13
 
14
- Current version: 0.3.2
14
+ Current version: 0.4.0
15
15
 
16
16
 
17
17
  ## Features
@@ -27,6 +27,7 @@ Current version: 0.3.2
27
27
  - **Pricing Registry**: Model prices are stored in a unified JSON registry with per-model input/output pricing and currency support.
28
28
  - **Unified Reasoning Support**: A single `reasoning_level` parameter that works identically across all providers.
29
29
  - **Request Timeouts:** Per-request timeout control with a unified `timeout_s` parameter.
30
+ - **Strict JSON Mode**: Pass a JSON Schema to `chat()` and get a parsed object in `ChatResponse.parsed_json` — provider normalization is handled automatically.
30
31
 
31
32
  ## Installation
32
33
 
@@ -132,6 +133,14 @@ The SDK provides a set of standardized errors for easier debugging and integrati
132
133
 
133
134
  - **LLMAPIUsageLimitError**: Raised when usage limits are exceeded.
134
135
 
136
+ - **InvalidToolSchemaError**: Raised when a provided tool schema is invalid.
137
+
138
+ - **InvalidToolArgumentsError**: Raised when tool arguments cannot be parsed or validated.
139
+
140
+ - **ToolChoiceError**: Raised when `tool_choice` is invalid or references an unknown tool.
141
+
142
+ - **JSONSchemaError**: Raised when `json_schema` is not a `dict`, is passed together with `tools`, or the model response is not valid JSON.
143
+
135
144
  ### Config Errors
136
145
 
137
146
  - **LLMConfigError**: Raised when the request configuration is invalid or incompatible.
@@ -145,7 +154,7 @@ The SDK provides a set of standardized errors for easier debugging and integrati
145
154
  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.
146
155
 
147
156
  - **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`.
157
+ - **Anthropic**: Available models include `claude-fable-5`, `claude-sonnet-5`, `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
158
  - **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.
150
159
 
151
160
  Example:
@@ -244,6 +253,7 @@ The `ChatResponse` object returned by `chat` includes:
244
253
  8. **cost\_total**: Total combined cost.
245
254
  9. **content**: The generated text response.
246
255
  10. **finish\_reason**: Reason why generation stopped (e.g., `"stop"`, `"length"`).
256
+ 11. **parsed\_json**: Parsed JSON object when `json_schema` was provided, otherwise `None`.
247
257
 
248
258
  ## Timeout Support
249
259
 
@@ -471,6 +481,78 @@ if first.tool_calls:
471
481
  print(final.content)
472
482
  ```
473
483
 
484
+ ## Strict JSON Mode
485
+
486
+ The SDK supports structured JSON output via a `json_schema` parameter in `chat()`. Pass any JSON Schema object — the adapter normalizes it for each provider automatically and returns the parsed result in `ChatResponse.parsed_json`.
487
+
488
+ ```python
489
+ from llm_api_adapter.models.messages.chat_message import Prompt, UserMessage
490
+ from llm_api_adapter.universal_adapter import UniversalLLMAPIAdapter
491
+
492
+ schema = {
493
+ "type": "object",
494
+ "properties": {
495
+ "name": {"type": "string"},
496
+ "age": {"type": "integer"},
497
+ },
498
+ "required": ["name", "age"],
499
+ }
500
+
501
+ adapter = UniversalLLMAPIAdapter(
502
+ organization="openai",
503
+ model="gpt-5",
504
+ api_key=openai_api_key,
505
+ )
506
+
507
+ response = adapter.chat(
508
+ messages=[
509
+ Prompt("Extract structured data from the user's message."),
510
+ UserMessage("My name is Alice and I'm 30 years old."),
511
+ ],
512
+ json_schema=schema,
513
+ max_tokens=200,
514
+ )
515
+
516
+ print(response.content) # '{"name": "Alice", "age": 30}'
517
+ print(response.parsed_json) # {"name": "Alice", "age": 30}
518
+ ```
519
+
520
+ ### `json_schema` parameter
521
+
522
+ - Accepts a `dict` containing a JSON Schema object.
523
+ - Cannot be combined with `tools` — passing both raises `JSONSchemaError`.
524
+ - If the model returns a response that is not valid JSON, `JSONSchemaError` is raised.
525
+
526
+ ### `parsed_json` field
527
+
528
+ `ChatResponse.parsed_json` contains the parsed `dict` when `json_schema` was provided, or `None` otherwise.
529
+
530
+ ### Provider behavior
531
+
532
+ | Provider | Implementation |
533
+ |----------|----------------|
534
+ | **OpenAI** (standard) | Native `response_format.type=json_schema` with `strict=true` |
535
+ | **OpenAI** (Responses API / o-series) | Native `text.format.type=json_schema` with `strict=true` |
536
+ | **Anthropic** | Native `output_config.format.type=json_schema` |
537
+ | **Google** | `generationConfig.responseMimeType="application/json"` + `responseSchema` |
538
+
539
+ The adapter automatically handles provider-specific schema constraints, so the same schema works across all providers without changes.
540
+
541
+ ### Error handling
542
+
543
+ ```python
544
+ from llm_api_adapter.errors.llm_api_error import JSONSchemaError
545
+
546
+ try:
547
+ response = adapter.chat(
548
+ messages=[UserMessage("Give me the data.")],
549
+ json_schema=schema,
550
+ max_tokens=200,
551
+ )
552
+ except JSONSchemaError as e:
553
+ print(f"JSON schema error: {e}")
554
+ ```
555
+
474
556
  ## Token Usage and Pricing
475
557
 
476
558
  ### Token Usage and Pricing Example
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "llm-api-adapter"
7
- version = "0.3.2"
7
+ version = "0.4.0"
8
8
  description = "Lightweight, pluggable adapter for multiple LLM APIs (OpenAI, Anthropic, Google)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -33,11 +33,13 @@ class AnthropicAdapter(LLMAdapterBase):
33
33
  tool_choice: Optional[str | dict] = None,
34
34
  parallel_tool_calls: Optional[bool] = None,
35
35
  previous_response: Optional[ChatResponse] = None,
36
+ json_schema: Optional[dict] = None,
36
37
  ) -> ChatResponse:
37
38
  temperature = self._validate_parameter("temperature", temperature, 0, 2)
38
39
  top_p = self._validate_parameter("top_p", top_p, 0, 1)
39
40
  try:
40
41
  self._validate_tools(tools)
42
+ self._validate_json_schema(json_schema, tools)
41
43
  validated_tools = tools
42
44
  normalized_tool_choice = self._normalize_tool_choice(
43
45
  tool_choice,
@@ -67,6 +69,13 @@ class AnthropicAdapter(LLMAdapterBase):
67
69
  params["disable_parallel_tool_use"] = True
68
70
  elif parallel_tool_calls is True:
69
71
  params["disable_parallel_tool_use"] = False
72
+ if json_schema is not None:
73
+ params["output_config"] = {
74
+ "format": {
75
+ "type": "json_schema",
76
+ "schema": self._enforce_strict_schema(json_schema),
77
+ }
78
+ }
70
79
  if reasoning_level:
71
80
  normalized_reasoning_level = self._normalize_reasoning_level(
72
81
  reasoning_level
@@ -87,6 +96,7 @@ class AnthropicAdapter(LLMAdapterBase):
87
96
  client = ClaudeSyncClient(api_key=self.api_key)
88
97
  response = client.chat_completion(**params)
89
98
  chat_response = ChatResponse.from_anthropic_response(response)
99
+ chat_response.parsed_json = self._parse_json_response(chat_response.content, json_schema)
90
100
  if self.pricing:
91
101
  chat_response.apply_pricing(
92
102
  price_input_per_token=self.pricing.in_per_token,
@@ -3,12 +3,13 @@ from __future__ import annotations
3
3
  from abc import ABC, abstractmethod
4
4
  from copy import deepcopy
5
5
  from dataclasses import dataclass, field
6
+ import json
6
7
  import logging
7
8
  import re
8
9
  from typing import Any, Dict, List, Optional
9
10
  import warnings
10
11
 
11
- from ..errors.llm_api_error import InvalidToolSchemaError, ToolChoiceError
12
+ from ..errors.llm_api_error import InvalidToolSchemaError, JSONSchemaError, ToolChoiceError
12
13
  from ..llm_registry.llm_registry import Pricing, LLM_REGISTRY
13
14
  from ..models.messages.chat_message import Messages
14
15
  from ..models.responses.chat_response import ChatResponse
@@ -225,6 +226,44 @@ class LLMAdapterBase(ABC):
225
226
  detail="tool_choice='required' is not supported; use 'any'"
226
227
  )
227
228
 
229
+ def _enforce_strict_schema(self, schema: dict) -> dict:
230
+ """Recursively add additionalProperties: false to all object types — required by OpenAI and Anthropic strict mode."""
231
+ schema = dict(schema)
232
+ if schema.get("type") == "object":
233
+ schema["additionalProperties"] = False
234
+ if "properties" in schema:
235
+ schema["properties"] = {
236
+ k: self._enforce_strict_schema(v) if isinstance(v, dict) else v
237
+ for k, v in schema["properties"].items()
238
+ }
239
+ if "items" in schema and isinstance(schema["items"], dict):
240
+ schema["items"] = self._enforce_strict_schema(schema["items"])
241
+ return schema
242
+
243
+ def _validate_json_schema(
244
+ self,
245
+ json_schema: Optional[dict],
246
+ tools: Optional[List[ToolSpec]] = None,
247
+ ) -> None:
248
+ if json_schema is None:
249
+ return
250
+ if not isinstance(json_schema, dict):
251
+ raise JSONSchemaError(detail="json_schema must be a dict")
252
+ if tools is not None:
253
+ raise JSONSchemaError(detail="json_schema and tools cannot be used together")
254
+
255
+ def _parse_json_response(
256
+ self,
257
+ content: Optional[str],
258
+ json_schema: Optional[dict],
259
+ ) -> Optional[dict]:
260
+ if json_schema is None or content is None:
261
+ return None
262
+ try:
263
+ return json.loads(content)
264
+ except json.JSONDecodeError as e:
265
+ raise JSONSchemaError(detail=f"Model response is not valid JSON: {e}")
266
+
228
267
  def handle_error(self, error: Exception, error_message: Optional[str] = None):
229
268
  err_msg = (
230
269
  f"Error with the provider '{self.company}' "
@@ -32,6 +32,7 @@ class GoogleAdapter(LLMAdapterBase):
32
32
  tool_choice: Optional[str | dict] = None,
33
33
  parallel_tool_calls: Optional[bool] = None,
34
34
  previous_response: Optional[ChatResponse] = None,
35
+ json_schema: Optional[dict] = None,
35
36
  ) -> ChatResponse:
36
37
  temperature = self._validate_parameter(
37
38
  name="temperature",
@@ -47,6 +48,7 @@ class GoogleAdapter(LLMAdapterBase):
47
48
  )
48
49
  try:
49
50
  self._validate_tools(tools)
51
+ self._validate_json_schema(json_schema, tools)
50
52
  validated_tools = tools
51
53
  normalized_tool_choice = self._normalize_tool_choice(
52
54
  tool_choice,
@@ -60,6 +62,9 @@ class GoogleAdapter(LLMAdapterBase):
60
62
  "temperature": temperature,
61
63
  "topP": top_p,
62
64
  }
65
+ if json_schema is not None:
66
+ generation_config["responseMimeType"] = "application/json"
67
+ generation_config["responseSchema"] = self._to_google_schema(json_schema)
63
68
  if reasoning_level:
64
69
  normalized_reasoning_level = self._normalize_reasoning_level(
65
70
  reasoning_level
@@ -97,6 +102,7 @@ class GoogleAdapter(LLMAdapterBase):
97
102
  **payload,
98
103
  )
99
104
  chat_response = ChatResponse.from_google_response(response_json)
105
+ chat_response.parsed_json = self._parse_json_response(chat_response.content, json_schema)
100
106
  if self.pricing:
101
107
  chat_response.apply_pricing(
102
108
  price_input_per_token=self.pricing.in_per_token,
@@ -110,6 +116,23 @@ class GoogleAdapter(LLMAdapterBase):
110
116
  error_message = getattr(e, "text", None) or str(e)
111
117
  self.handle_error(error=e, error_message=error_message)
112
118
 
119
+ # Fields not supported by Google's responseSchema subset of JSON Schema.
120
+ _GOOGLE_SCHEMA_UNSUPPORTED = frozenset({"additionalProperties", "$schema", "$id", "$ref"})
121
+
122
+ def _to_google_schema(self, schema: dict) -> dict:
123
+ """Convert standard JSON Schema to Google's format (type uppercase, unsupported fields stripped)."""
124
+ schema = {k: v for k, v in schema.items() if k not in self._GOOGLE_SCHEMA_UNSUPPORTED}
125
+ if "type" in schema and isinstance(schema["type"], str):
126
+ schema["type"] = schema["type"].upper()
127
+ if "properties" in schema:
128
+ schema["properties"] = {
129
+ k: self._to_google_schema(v) if isinstance(v, dict) else v
130
+ for k, v in schema["properties"].items()
131
+ }
132
+ if "items" in schema and isinstance(schema["items"], dict):
133
+ schema["items"] = self._to_google_schema(schema["items"])
134
+ return schema
135
+
113
136
  def _to_google_function_declaration(self, tool: ToolSpec) -> Dict[str, Any]:
114
137
  declaration: Dict[str, Any] = {
115
138
  "name": tool.name,
@@ -31,6 +31,7 @@ class OpenAIAdapter(LLMAdapterBase):
31
31
  tool_choice: Any = None,
32
32
  parallel_tool_calls: Optional[bool] = None,
33
33
  previous_response: Optional[ChatResponse] = None,
34
+ json_schema: Optional[dict] = None,
34
35
  ) -> ChatResponse:
35
36
  temperature = self._validate_parameter(
36
37
  name="temperature",
@@ -45,6 +46,7 @@ class OpenAIAdapter(LLMAdapterBase):
45
46
  max_value=1,
46
47
  )
47
48
  self._validate_tools(tools)
49
+ self._validate_json_schema(json_schema, tools)
48
50
  normalized_tool_choice = self._normalize_tool_choice(tool_choice, tools)
49
51
 
50
52
  try:
@@ -90,9 +92,27 @@ class OpenAIAdapter(LLMAdapterBase):
90
92
  params["instructions"] = instructions
91
93
  if previous_response_id is not None:
92
94
  params["previous_response_id"] = previous_response_id
95
+ if json_schema is not None:
96
+ params["text"] = {
97
+ "format": {
98
+ "type": "json_schema",
99
+ "name": "response",
100
+ "strict": True,
101
+ "schema": self._enforce_strict_schema(json_schema),
102
+ }
103
+ }
93
104
  else:
94
105
  params["messages"] = transformed_messages
95
106
  params["parallel_tool_calls"] = parallel_tool_calls
107
+ if json_schema is not None:
108
+ params["response_format"] = {
109
+ "type": "json_schema",
110
+ "json_schema": {
111
+ "name": "response",
112
+ "strict": True,
113
+ "schema": self._enforce_strict_schema(json_schema),
114
+ },
115
+ }
96
116
 
97
117
  params = {k: v for k, v in params.items() if v is not None}
98
118
  response = client.complete(timeout=timeout_s, **params)
@@ -102,6 +122,8 @@ class OpenAIAdapter(LLMAdapterBase):
102
122
  else:
103
123
  chat_response = ChatResponse.from_openai_response(response)
104
124
 
125
+ chat_response.parsed_json = self._parse_json_response(chat_response.content, json_schema)
126
+
105
127
  if self.pricing:
106
128
  chat_response.apply_pricing(
107
129
  price_input_per_token=self.pricing.in_per_token,
@@ -98,3 +98,9 @@ class InvalidToolArgumentsError(LLMAPIClientError):
98
98
  class ToolChoiceError(LLMAPIClientError):
99
99
  """Raised when tool_choice is invalid or references unknown tool."""
100
100
  message: str = "Invalid tool_choice configuration."
101
+
102
+
103
+ @dataclass
104
+ class JSONSchemaError(LLMAPIClientError):
105
+ """Raised when json_schema usage is invalid or the model response fails schema validation."""
106
+ message: str = "JSON schema error."
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schema_version": 6,
3
- "effective_date": "2026-06-22",
3
+ "effective_date": "2026-07-07",
4
4
  "providers": {
5
5
  "openai": {
6
6
  "currency": "USD",
@@ -51,6 +51,14 @@
51
51
  "anthropic": {
52
52
  "currency": "USD",
53
53
  "models": {
54
+ "claude-fable-5": {
55
+ "pricing": {"in_per_1m": 10.0, "out_per_1m": 50.0},
56
+ "is_reasoning": true
57
+ },
58
+ "claude-sonnet-5": {
59
+ "pricing": {"in_per_1m": 3.0, "out_per_1m": 15.0},
60
+ "is_reasoning": true
61
+ },
54
62
  "claude-opus-4-8": {
55
63
  "pricing": {"in_per_1m": 5.0, "out_per_1m": 25.0},
56
64
  "is_reasoning": true
@@ -14,7 +14,7 @@ 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")
17
+ _ADAPTIVE_THINKING_MODELS = ("claude-opus-4-6", "claude-sonnet-4-6", "claude-opus-4-7", "claude-opus-4-8", "claude-fable-5", "claude-sonnet-5")
18
18
 
19
19
  @dataclass
20
20
  class ClaudeSyncClient:
@@ -42,7 +42,8 @@ class ClaudeSyncClient:
42
42
  kwargs.pop("top_p", None)
43
43
  if effort:
44
44
  kwargs["thinking"] = {"type": "adaptive"}
45
- kwargs["output_config"] = {"effort": effort}
45
+ existing = kwargs.get("output_config", {})
46
+ kwargs["output_config"] = {**existing, "effort": effort}
46
47
  else:
47
48
  if model.startswith(
48
49
  ("claude-sonnet-4-5", "claude-opus-4-1", "claude-haiku-4-5", "claude-opus-4-5")
@@ -27,6 +27,7 @@ class ChatResponse:
27
27
  content: Optional[str] = None
28
28
  tool_calls: Optional[List[ToolCall]] = None
29
29
  finish_reason: Optional[str] = None
30
+ parsed_json: Optional[dict] = None
30
31
 
31
32
  @classmethod
32
33
  def from_openai_response(cls, api_response: dict) -> "ChatResponse":
File without changes