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.
- {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/PKG-INFO +85 -3
- {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/README.md +84 -2
- {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/pyproject.toml +1 -1
- {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/adapters/anthropic_adapter.py +10 -0
- {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/adapters/base_adapter.py +40 -1
- {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/adapters/google_adapter.py +23 -0
- {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/adapters/openai_adapter.py +22 -0
- {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/errors/llm_api_error.py +6 -0
- {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/llm_registry/llm_registry.json +9 -1
- {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/llms/anthropic/sync_client.py +3 -2
- {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/models/responses/chat_response.py +1 -0
- {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/LICENSE +0 -0
- {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/MANIFEST.in +0 -0
- {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/setup.cfg +0 -0
- {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/__init__.py +0 -0
- {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/adapters/__init__.py +0 -0
- {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/errors/__init__.py +0 -0
- {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/errors/config_errors.py +0 -0
- {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/llm_registry/__init__.py +0 -0
- {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/llm_registry/llm_registry.py +0 -0
- {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/llms/__init__.py +0 -0
- {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/llms/anthropic/__init__.py +0 -0
- {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/llms/google/__init__.py +0 -0
- {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/llms/google/sync_client.py +0 -0
- {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/llms/openai/__init__.py +0 -0
- {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/llms/openai/sync_client.py +0 -0
- {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/models/__init__.py +0 -0
- {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/models/messages/__init__.py +0 -0
- {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/models/messages/chat_message.py +0 -0
- {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/models/responses/__init__.py +0 -0
- {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/models/tools/__init__.py +0 -0
- {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/models/tools/tool_call.py +0 -0
- {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/models/tools/tool_spec.py +0 -0
- {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/universal_adapter.py +0 -0
- {llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter.egg-info/SOURCES.txt +0 -0
- {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
|
+
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.
|
|
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.
|
|
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.
|
|
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"
|
{llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/adapters/anthropic_adapter.py
RENAMED
|
@@ -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,
|
{llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/adapters/base_adapter.py
RENAMED
|
@@ -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}' "
|
{llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/adapters/google_adapter.py
RENAMED
|
@@ -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,
|
{llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/adapters/openai_adapter.py
RENAMED
|
@@ -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."
|
{llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/llm_registry/llm_registry.json
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": 6,
|
|
3
|
-
"effective_date": "2026-
|
|
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
|
{llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/llms/anthropic/sync_client.py
RENAMED
|
@@ -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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/llm_registry/__init__.py
RENAMED
|
File without changes
|
{llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/llm_registry/llm_registry.py
RENAMED
|
File without changes
|
|
File without changes
|
{llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/llms/anthropic/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/llms/google/sync_client.py
RENAMED
|
File without changes
|
|
File without changes
|
{llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/llms/openai/sync_client.py
RENAMED
|
File without changes
|
|
File without changes
|
{llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/models/messages/__init__.py
RENAMED
|
File without changes
|
{llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/models/messages/chat_message.py
RENAMED
|
File without changes
|
{llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/models/responses/__init__.py
RENAMED
|
File without changes
|
{llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/models/tools/__init__.py
RENAMED
|
File without changes
|
{llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/models/tools/tool_call.py
RENAMED
|
File without changes
|
{llm_api_adapter-0.3.2 → llm_api_adapter-0.4.0}/src/llm_api_adapter/models/tools/tool_spec.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|