ai_api_unified 3.2.0__tar.gz → 3.5.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.
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/PKG-INFO +286 -143
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/README.md +285 -142
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/pyproject.toml +1 -1
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/__init__.py +21 -1
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/__version__.py +1 -1
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/ai_base.py +513 -73
- ai_api_unified-3.5.0/src/ai_api_unified/ai_completions_exceptions.py +107 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/ai_factory.py +29 -10
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/ai_models_config.py +36 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/ai_provider_exceptions.py +8 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/completions/ai_anthropic_completions.py +92 -25
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/completions/ai_bedrock_completions.py +152 -24
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/completions/ai_fallback_completions.py +83 -2
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/completions/ai_google_gemini_capabilities.py +2 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/completions/ai_google_gemini_completions.py +26 -23
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/completions/ai_openai_completions.py +34 -23
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/completions/ai_openai_responses_completions.py +22 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/completions/ai_router_completions.py +252 -172
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/middleware/__init__.py +12 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/middleware/observability.py +112 -29
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/middleware/observability_runtime.py +172 -51
- ai_api_unified-3.5.0/src/ai_api_unified/middleware/request_cost.py +736 -0
- ai_api_unified-3.5.0/src/ai_api_unified/thinking.py +545 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/util/env_settings.py +53 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/voice/ai_voice_factory.py +1 -1
- ai_api_unified-3.2.0/src/ai_api_unified/ai_completions_exceptions.py +0 -51
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/LICENSE +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/ai_anthropic_base.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/ai_bedrock_base.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/ai_google_base.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/ai_openai_base.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/ai_provider_loader.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/ai_provider_registry.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/classification/__init__.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/classification/ai_openai_decisions_classifier.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/classification/ai_structured_output_classifier.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/completions/__init__.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/completions/ai_openai_compatible_completions.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/embeddings/__init__.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/embeddings/ai_google_gemini_embeddings.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/embeddings/ai_openai_embeddings.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/embeddings/ai_titan_embeddings.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/embeddings/ai_voyage_embeddings.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/images/__init__.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/images/ai_bedrock_images.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/images/ai_google_gemini_images.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/images/ai_openai_images.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/middleware/impl/__init__.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/middleware/impl/_presidio_redactor.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/middleware/impl/base_redactor.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/middleware/impl/custom_recognizer_factory.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/middleware/impl/noop_redactor.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/middleware/impl/presidio_log_control.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/middleware/middleware.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/middleware/middleware_config.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/middleware/pii_redactor.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/middleware/redaction_exceptions.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/pricing/__init__.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/pricing/model_pricing.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/pricing/pricing_registry.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/routing/__init__.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/routing/router_config.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/util/__init__.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/util/_lazy_pydub.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/util/ai_model_checker.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/util/ai_model_probes.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/util/package_metadata_validation.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/util/utils.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/videos/__init__.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/videos/ai_bedrock_videos.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/videos/ai_google_gemini_videos.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/videos/ai_openai_videos.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/videos/frame_helpers.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/voice/__init__.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/voice/ai_voice_azure.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/voice/ai_voice_base.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/voice/ai_voice_elevenlabs.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/voice/ai_voice_google.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/voice/ai_voice_google_gemini_voices.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/voice/ai_voice_openai.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.5.0}/src/ai_api_unified/voice/audio_models.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: ai_api_unified
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.5.0
|
|
4
4
|
Summary: Unified access layer for AI completions (LLM), embedding (semantic), image, video, and voice services
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: llm,openai,anthropic,claude,bedrock,gemini,vertex,embeddings,completion,video,voice,api
|
|
@@ -77,7 +77,7 @@ Project-URL: Issues, https://github.com/davecthomas/ai-api-unified/issues
|
|
|
77
77
|
Project-URL: Repository, https://github.com/davecthomas/ai-api-unified
|
|
78
78
|
Description-Content-Type: text/markdown
|
|
79
79
|
|
|
80
|
-
# ai-api-unified 3.
|
|
80
|
+
# ai-api-unified 3.5.0
|
|
81
81
|
|
|
82
82
|
[](https://github.com/davecthomas/ai-api-unified/actions/workflows/ci.yml)
|
|
83
83
|
[](https://pypi.org/project/ai-api-unified/)
|
|
@@ -117,11 +117,11 @@ The current package architecture is registry-backed and lazy-loaded:
|
|
|
117
117
|
More than 100 models from 10 model makers (OpenAI, Anthropic, Google,
|
|
118
118
|
Amazon, DeepSeek, Alibaba Qwen, Z.ai, Voyage AI, ElevenLabs, and Microsoft)
|
|
119
119
|
behind one interface, plus any server that speaks the OpenAI API. Switching
|
|
120
|
-
models is a configuration change: set the capability's engine
|
|
121
|
-
`
|
|
120
|
+
models is a configuration change: set the capability's engine and model in
|
|
121
|
+
`models.json` (for example `completions.engine` and `completions.model`).
|
|
122
122
|
|
|
123
123
|
<!-- supported-models:start -->
|
|
124
|
-
| Capability | Provider | Engine (
|
|
124
|
+
| Capability | Provider | Engine (`<section>.engine`) | Models |
|
|
125
125
|
| --- | --- | --- | --- |
|
|
126
126
|
| Completions | OpenAI | `openai`, `openai-responses` | `gpt-6-astra`, `gpt-6-sol`, `gpt-6-luna`, `gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-5.6-luna` (default), `gpt-5.5`, `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.4-nano`, `gpt-5.2`, `gpt-5.1-codex-max`, `gpt-5`, `gpt-5-mini`, `gpt-5-nano`, `gpt-4.1`, `gpt-4.1-mini`, `gpt-4.1-nano` ⚠ until 2026-10-23, `o4-mini` ⚠ until 2026-10-23, `gpt-4o`, `gpt-4o-mini` |
|
|
127
127
|
| Completions | Anthropic | `claude` | `claude-fable-5-1`, `claude-opus-5-5`, `claude-fable-5`, `claude-opus-5` (default), `claude-sonnet-5`, `claude-opus-4-8`, `claude-opus-4-7`, `claude-opus-4-6`, `claude-sonnet-4-6`, `claude-haiku-4-5` |
|
|
@@ -131,7 +131,7 @@ models is a configuration change: set the capability's engine (for example
|
|
|
131
131
|
| Completions | Amazon Bedrock: DeepSeek | `bedrock` | `deepseek.v3.2`, `us.deepseek.r1-v1:0` |
|
|
132
132
|
| Completions | Amazon Bedrock: Qwen (Alibaba) | `bedrock` | `qwen.qwen3-next-80b-a3b`, `qwen.qwen3-235b-a22b-2507-v1:0`, `qwen.qwen3-coder-next`, `qwen.qwen3-32b-v1:0` |
|
|
133
133
|
| Completions | Amazon Bedrock: GLM (Z.ai) | `bedrock` | `zai.glm-5`, `zai.glm-4.7`, `zai.glm-4.7-flash` |
|
|
134
|
-
| Completions | Any OpenAI-compatible server (vLLM, Ollama, LiteLLM, ...) | `openai-compatible` | any model the server hosts (`
|
|
134
|
+
| Completions | Any OpenAI-compatible server (vLLM, Ollama, LiteLLM, ...) | `openai-compatible` | any model the server hosts (`completions.model`) |
|
|
135
135
|
| Embeddings | OpenAI | `openai` | `text-embedding-3-small` (default), `text-embedding-3-large`, `text-embedding-ada-002` |
|
|
136
136
|
| Embeddings | Google | `google-gemini` | `gemini-embedding-001` (default), `gemini-embedding-2` (multimodal) |
|
|
137
137
|
| Embeddings | Voyage AI | `voyage` | `voyage-4-large`, `voyage-4`, `voyage-4-lite`, `voyage-code-4`, `voyage-3.5`, `voyage-3.5-lite`, `voyage-3-lite`, `voyage-3` (default), `voyage-3-large`, `voyage-code-3`, `voyage-finance-2`, `voyage-law-2` |
|
|
@@ -287,30 +287,34 @@ poetry install --all-extras --with dev
|
|
|
287
287
|
| `similarity_score` | NumPy-based similarity helpers |
|
|
288
288
|
| `dev` | Optional dev dependencies from `[project.optional-dependencies]` |
|
|
289
289
|
|
|
290
|
-
### Environment File
|
|
290
|
+
### Environment File and models.json
|
|
291
291
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
292
|
+
Two files: `.env` holds credentials and endpoint overrides; `models.json`
|
|
293
|
+
holds every model selection. Copy [`env_template`](env_template) to `.env`
|
|
294
|
+
and fill in only the providers you use, and copy
|
|
295
|
+
[`models_sample.json`](models_sample.json) to `models.json` and keep the
|
|
296
|
+
sections you need. The sample defaults to Google with API-key auth:
|
|
295
297
|
|
|
296
298
|
```dotenv
|
|
297
|
-
COMPLETIONS_ENGINE=google-gemini
|
|
298
|
-
EMBEDDING_ENGINE=google-gemini
|
|
299
|
-
IMAGE_ENGINE=google-gemini
|
|
300
|
-
VIDEO_ENGINE=google-gemini
|
|
301
|
-
AI_VOICE_ENGINE=google
|
|
302
|
-
|
|
303
299
|
GOOGLE_GEMINI_API_KEY=...
|
|
304
300
|
GOOGLE_AUTH_METHOD=api_key
|
|
301
|
+
```
|
|
305
302
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
303
|
+
```json
|
|
304
|
+
{
|
|
305
|
+
"completions": {"engine": "google-gemini", "model": "gemini-3.7-flash", "thinking": "medium"},
|
|
306
|
+
"embeddings": {"engine": "google-gemini", "model": "gemini-embedding-001"},
|
|
307
|
+
"images": {"engine": "google-gemini", "model": "gemini-3.1-flash-image"},
|
|
308
|
+
"videos": {"engine": "google-gemini", "model": "veo-3.1-lite-generate-preview"},
|
|
309
|
+
"voice": {"engine": "google", "tts_model": "gemini-2.5-pro-tts"}
|
|
310
|
+
}
|
|
311
311
|
```
|
|
312
312
|
|
|
313
|
-
Leave `
|
|
313
|
+
Leave `embeddings.dimensions` at `0` unless you deliberately want a
|
|
314
|
+
provider-specific size. The library preserves provider defaults. A `.env`
|
|
315
|
+
from before `models.json` still works: each selection key it carries
|
|
316
|
+
overrides the file and logs a deprecation warning; `scripts/migrate2json.py`
|
|
317
|
+
moves them (see Configuration).
|
|
314
318
|
|
|
315
319
|
### Smoke Test
|
|
316
320
|
|
|
@@ -334,13 +338,15 @@ print(response)
|
|
|
334
338
|
|
|
335
339
|
### Completions with automatic fallback
|
|
336
340
|
|
|
337
|
-
Add one
|
|
341
|
+
Add one list and the same client keeps serving when the primary model
|
|
338
342
|
cannot. The chain below runs Claude first, then OpenAI, then Gemini:
|
|
339
343
|
|
|
340
|
-
```
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
+
```json
|
|
345
|
+
"completions": {
|
|
346
|
+
"engine": "claude",
|
|
347
|
+
"model": "claude-opus-5",
|
|
348
|
+
"fallbacks": ["openai:gpt-5.6-luna", "google-gemini:gemini-3.7-flash"]
|
|
349
|
+
}
|
|
344
350
|
```
|
|
345
351
|
|
|
346
352
|
```python
|
|
@@ -396,7 +402,7 @@ if client.capabilities.supports_token_counting:
|
|
|
396
402
|
### OpenAI Responses engine
|
|
397
403
|
|
|
398
404
|
OpenAI exposes two completions engines. The default `openai` engine uses Chat
|
|
399
|
-
Completions; the `openai-responses` engine (`
|
|
405
|
+
Completions; the `openai-responses` engine (`completions.engine: "openai-responses"`)
|
|
400
406
|
uses the Responses API, OpenAI's successor to Chat Completions. Both implement
|
|
401
407
|
`send_prompt`, `strict_schema_prompt`, and
|
|
402
408
|
`send_prompt_streaming`. The Responses engine is text-only for now; use the
|
|
@@ -411,10 +417,12 @@ surface as the `openai` engine: `send_prompt`, streaming,
|
|
|
411
417
|
`send_conversation` with tools, `send_structured_output`,
|
|
412
418
|
`strict_schema_prompt`, and the async variants.
|
|
413
419
|
|
|
420
|
+
```json
|
|
421
|
+
"completions": {"engine": "openai-compatible", "model": "Qwen/Qwen3-32B"}
|
|
422
|
+
```
|
|
423
|
+
|
|
414
424
|
```dotenv
|
|
415
|
-
COMPLETIONS_ENGINE=openai-compatible
|
|
416
425
|
OPENAI_COMPATIBLE_BASE_URL=http://localhost:8000/v1 # required
|
|
417
|
-
COMPLETIONS_MODEL_NAME=Qwen/Qwen3-32B # required
|
|
418
426
|
OPENAI_COMPATIBLE_API_KEY=... # optional for local servers
|
|
419
427
|
OPENAI_COMPATIBLE_STRUCTURED_OUTPUT=json_schema # or json_object
|
|
420
428
|
OPENAI_COMPATIBLE_CONTEXT_WINDOW=131072 # optional context guard
|
|
@@ -451,9 +459,11 @@ engine additionally implements the async variants and batch completions. Use
|
|
|
451
459
|
`claude` for the current model lineup on Anthropic's own endpoint; use
|
|
452
460
|
`anthropic` when your Claude access is provisioned through AWS.
|
|
453
461
|
|
|
462
|
+
```json
|
|
463
|
+
"completions": {"engine": "claude", "model": "claude-opus-5"}
|
|
464
|
+
```
|
|
465
|
+
|
|
454
466
|
```dotenv
|
|
455
|
-
COMPLETIONS_ENGINE=claude
|
|
456
|
-
COMPLETIONS_MODEL_NAME=claude-opus-5
|
|
457
467
|
ANTHROPIC_API_KEY=...
|
|
458
468
|
```
|
|
459
469
|
|
|
@@ -510,6 +520,85 @@ has no per-call timeout). See the support matrix below.
|
|
|
510
520
|
Unsupported combinations raise the typed `AiProviderCapabilityUnsupportedError`
|
|
511
521
|
and each engine's `client.capabilities` flags report support at runtime.
|
|
512
522
|
|
|
523
|
+
### Thinking levels
|
|
524
|
+
|
|
525
|
+
How much a model thinks before answering is one provider-neutral setting,
|
|
526
|
+
`AIThinkingLevel`, with six values: `off`, `minimal`, `low`, `medium`,
|
|
527
|
+
`high`, `max`. Configuration and application code use the level; each
|
|
528
|
+
engine applies it through its provider's own control, and no code or config
|
|
529
|
+
names a provider field. The default is `medium`.
|
|
530
|
+
|
|
531
|
+
```python
|
|
532
|
+
from ai_api_unified import AIFactory, AIThinkingLevel
|
|
533
|
+
|
|
534
|
+
client = AIFactory.get_ai_completions_client(thinking="high") # or "thinking": "high" in models.json
|
|
535
|
+
client.thinking_level = AIThinkingLevel.LOW # change the default for later calls
|
|
536
|
+
|
|
537
|
+
text = client.send_prompt("...", other_params=params_with(thinking=AIThinkingLevel.MAX))
|
|
538
|
+
turn = client.send_conversation(system, messages, provider_options={"thinking_level": "off"})
|
|
539
|
+
result = client.send_structured_output(prompt, response_model=Summary,
|
|
540
|
+
provider_options={"thinking_level": "low"})
|
|
541
|
+
|
|
542
|
+
turn.thinking.requested, turn.thinking.applied, turn.thinking.exact # AIThinkingResolution
|
|
543
|
+
client.last_thinking # for send_prompt and friends
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
Precedence: the call (`other_params.thinking` on the text methods; the
|
|
547
|
+
reserved `thinking_level` key in `provider_options` on `send_conversation`
|
|
548
|
+
and `send_structured_output`, stripped before the options reach the
|
|
549
|
+
provider), then the client (`thinking_level`, set by the `thinking=`
|
|
550
|
+
factory argument), then `completions.thinking` in `models.json`, then
|
|
551
|
+
`medium`. The level has no `.env` key: it is a model selection, and those
|
|
552
|
+
live in the file. The factory applies one level to the primary,
|
|
553
|
+
its fallbacks, and every router tier, so a request that fails over keeps its
|
|
554
|
+
level.
|
|
555
|
+
|
|
556
|
+
`client.capabilities.thinking` lists the standard levels the model honors
|
|
557
|
+
exactly (`None` when the model has no thinking control). A requested level
|
|
558
|
+
outside that list resolves to the nearest one the model supports, and a tie
|
|
559
|
+
goes to the lower level, since lower thinking is cheaper and the caller
|
|
560
|
+
asked for no more. The library records both levels: on `result.thinking` and
|
|
561
|
+
`client.last_thinking` (`requested`, `applied`, `exact`, and a `reason`
|
|
562
|
+
such as `unsupported_level`, `no_thinking_control`, `forced_tool_choice`,
|
|
563
|
+
`assistant_prefill`, or `max_tokens_too_small`), on every completions
|
|
564
|
+
observability event (`thinking_requested`, `thinking_applied`,
|
|
565
|
+
`thinking_exact`), and in an `ai_thinking_event` log line when they differ.
|
|
566
|
+
|
|
567
|
+
How each engine applies a level:
|
|
568
|
+
|
|
569
|
+
| Engine | Models | `off` | `minimal` | `low` / `medium` / `high` / `max` |
|
|
570
|
+
|---|---|---|---|---|
|
|
571
|
+
| `claude`, Bedrock Claude | Fable 5 / 5.1, Opus 5.5 (always on) | nearest: `low` | nearest: `low` | `output_config.effort` |
|
|
572
|
+
| `claude`, Bedrock Claude | Opus 5, Haiku 5.5, Sonnet 5, Opus 4.6 to 4.8, Sonnet 4.6 | `thinking.type: disabled` | nearest: `off` | `thinking.type: adaptive` + `output_config.effort` |
|
|
573
|
+
| `claude`, Bedrock Claude | Sonnet 5.5 | `thinking.type: between_tools` | nearest: `off` | `thinking.type: adaptive` + `output_config.effort` |
|
|
574
|
+
| `claude`, Bedrock Claude | Haiku 4.5, Sonnet 4.5, Opus 4.5, Opus 4.1, Sonnet 4, Opus 4, 3.7 Sonnet | omit `thinking` | nearest: `off` | `thinking.budget_tokens` 1024 / 4096 / 16384 / 32768, capped at `max_tokens` less 1024 kept for the answer |
|
|
575
|
+
| `claude`, Bedrock Claude | Claude 3 Opus and Haiku, 3.5 Sonnet and Haiku | no control | no control | no control |
|
|
576
|
+
| `openai`, `openai-responses` | GPT-5.1 and later, GPT-6 | `reasoning_effort: none` (not on GPT-6 Astra) | nearest | `low` / `medium` / `high` / `xhigh` |
|
|
577
|
+
| `openai`, `openai-responses` | GPT-5, GPT-5 mini, nano, pro, codex, and dated snapshots | nearest: `minimal` | `minimal` | `low` / `medium` / `high` / nearest: `high` |
|
|
578
|
+
| `openai`, `openai-responses` | o-series | nearest: `low` | nearest: `low` | `low` / `medium` / `high` / nearest: `high` |
|
|
579
|
+
| `openai`, `openai-compatible` | GPT-4.1, GPT-4o, uncataloged | no control | no control | no control |
|
|
580
|
+
| `google-gemini` | Gemini 3 Flash and later | nearest: `minimal` | `thinking_level: MINIMAL` | `LOW` / `MEDIUM` / `HIGH` / nearest: `HIGH` |
|
|
581
|
+
| `google-gemini` | Gemini 3 Pro | nearest: `low` | nearest: `low` | `LOW` / nearest / `HIGH` / nearest: `HIGH` |
|
|
582
|
+
| `google-gemini` | Gemini 2.5 Flash, Flash Lite | `thinking_budget: 0` | nearest: `off` | `thinking_budget` 1024 / 8192 / 16384 / 24576 |
|
|
583
|
+
| `google-gemini` | Gemini 2.5 Pro | nearest: `low` | nearest: `low` | `thinking_budget` 1024 / 8192 / 16384 / 32768 |
|
|
584
|
+
| Bedrock | Nova, DeepSeek, Qwen, GLM | no control | no control | no control |
|
|
585
|
+
|
|
586
|
+
On Bedrock the Claude fields travel under `additionalModelRequestFields`.
|
|
587
|
+
The engines merge these fields before `provider_options`, so a caller's own
|
|
588
|
+
`thinking`, `reasoning_effort`, or `thinking_config` still wins. Three
|
|
589
|
+
request shapes turn thinking off on models that allow it, with the reason
|
|
590
|
+
recorded: a forced `tool_choice` (Claude rejects the pair), an assistant
|
|
591
|
+
prefill (Bedrock's `strict_schema_prompt` on pre-4.6 Claude), and a
|
|
592
|
+
`max_tokens` with no room for the 1024-token minimum budget after 1024
|
|
593
|
+
tokens are kept for the answer. Bedrock's `send_prompt` and streaming
|
|
594
|
+
defaults of 256 tokens rise to 4096 on budget-era Claude models when the
|
|
595
|
+
caller sets no cap, so the configured level is applied.
|
|
596
|
+
|
|
597
|
+
The tables follow each provider's documentation as of October 2026 and the
|
|
598
|
+
live test (`poetry run pytest -m nonmock tests/test_thinking_levels_nonmock.py -q`)
|
|
599
|
+
exercises the configured models. When a model rejects a value the table
|
|
600
|
+
lists, the provider returns its own 400; correct the table entry.
|
|
601
|
+
|
|
513
602
|
### Structured output extraction (send_structured_output)
|
|
514
603
|
|
|
515
604
|
`send_structured_output` is the single-shot extraction call: prose in, a parsed
|
|
@@ -695,7 +784,7 @@ Engine retry behavior:
|
|
|
695
784
|
`AWS_MAX_ATTEMPTS` environment variable.
|
|
696
785
|
|
|
697
786
|
Every engine accepts `retry_policy` at the constructor, through configuration
|
|
698
|
-
(`
|
|
787
|
+
(`completions.retry_policy: "none"` in `models.json`), or per call
|
|
699
788
|
(`provider_options={"retry_policy": "none"}`; on Bedrock the per-call
|
|
700
789
|
override collapses that call's schedule). HTTP-level failures raise
|
|
701
790
|
`AiProviderRequestError`, whose `status_code` attribute lets caller backoff
|
|
@@ -897,7 +986,7 @@ Anthropic serves none.
|
|
|
897
986
|
whose capabilities declare `supports_async` (currently `voyage`); others
|
|
898
987
|
raise `AiProviderCapabilityUnsupportedError`.
|
|
899
988
|
- The `voyage` engine honors `retry_policy="none"` (constructor or
|
|
900
|
-
`
|
|
989
|
+
`completions.retry_policy`) and wraps provider failures in
|
|
901
990
|
`AiProviderRequestError` carrying `status_code`, matching the completions
|
|
902
991
|
clients.
|
|
903
992
|
|
|
@@ -923,7 +1012,7 @@ Every embeddings client exposes a `capabilities` descriptor stating which input
|
|
|
923
1012
|
types the configured model supports. Text-only models raise
|
|
924
1013
|
`AiProviderCapabilityUnsupportedError` from `generate_embeddings_multimodal`.
|
|
925
1014
|
Google `gemini-embedding-2` embeds interleaved text, images, video, audio, and
|
|
926
|
-
PDFs into one vector space (set `
|
|
1015
|
+
PDFs into one vector space (set `embeddings.model` to `gemini-embedding-2`):
|
|
927
1016
|
|
|
928
1017
|
```python
|
|
929
1018
|
from ai_api_unified import (
|
|
@@ -932,7 +1021,7 @@ from ai_api_unified import (
|
|
|
932
1021
|
SupportedDataType,
|
|
933
1022
|
)
|
|
934
1023
|
|
|
935
|
-
client = AIFactory.get_ai_embedding_client() #
|
|
1024
|
+
client = AIFactory.get_ai_embedding_client() # embeddings.model: gemini-embedding-2
|
|
936
1025
|
|
|
937
1026
|
if SupportedDataType.IMAGE in client.capabilities.supported_data_types:
|
|
938
1027
|
with open("bicycle.png", "rb") as file_image:
|
|
@@ -1010,9 +1099,11 @@ print(result.job.status, result.artifacts[0].file_path)
|
|
|
1010
1099
|
|
|
1011
1100
|
Environment:
|
|
1012
1101
|
|
|
1102
|
+
```json
|
|
1103
|
+
"videos": {"engine": "google-gemini", "model": "veo-3.1-lite-generate-preview"}
|
|
1104
|
+
```
|
|
1105
|
+
|
|
1013
1106
|
```dotenv
|
|
1014
|
-
VIDEO_ENGINE=google-gemini
|
|
1015
|
-
VIDEO_MODEL_NAME=veo-3.1-lite-generate-preview
|
|
1016
1107
|
GOOGLE_GEMINI_API_KEY=...
|
|
1017
1108
|
GOOGLE_AUTH_METHOD=api_key
|
|
1018
1109
|
```
|
|
@@ -1046,9 +1137,11 @@ for reference only; use the `google-gemini` or `nova-reel` video engine.
|
|
|
1046
1137
|
|
|
1047
1138
|
Environment:
|
|
1048
1139
|
|
|
1140
|
+
```json
|
|
1141
|
+
"videos": {"engine": "openai", "model": "sora-2"}
|
|
1142
|
+
```
|
|
1143
|
+
|
|
1049
1144
|
```dotenv
|
|
1050
|
-
VIDEO_ENGINE=openai
|
|
1051
|
-
VIDEO_MODEL_NAME=sora-2
|
|
1052
1145
|
OPENAI_API_KEY=...
|
|
1053
1146
|
```
|
|
1054
1147
|
|
|
@@ -1094,7 +1187,7 @@ with open(f"out{voice.default_audio_format.file_extension}", "wb") as output_fil
|
|
|
1094
1187
|
`get_default_voice()` and `default_audio_format` supply the provider's own
|
|
1095
1188
|
defaults. Use `stream_audio(text)` for the streaming variant.
|
|
1096
1189
|
|
|
1097
|
-
`AIFactory.get_ai_voice_client()` reads `
|
|
1190
|
+
`AIFactory.get_ai_voice_client()` reads `voice.engine` from `models.json` and accepts an
|
|
1098
1191
|
explicit `voice_engine` override, matching the other capability constructors.
|
|
1099
1192
|
It delegates to `AIVoiceFactory.create()`, which remains supported and callable
|
|
1100
1193
|
with no arguments.
|
|
@@ -1107,7 +1200,7 @@ originating caller id unless the application sets one through the
|
|
|
1107
1200
|
observability context.
|
|
1108
1201
|
|
|
1109
1202
|
The OpenAI voice client shares `AIOpenAIBase` with the other OpenAI-backed
|
|
1110
|
-
capabilities, so it honors `
|
|
1203
|
+
capabilities, so it honors `completions.retry_policy` and `OPENAI_BASE_URL_OVERRIDE`,
|
|
1111
1204
|
and accepts the same `retry_policy` and `base_url` constructor arguments:
|
|
1112
1205
|
|
|
1113
1206
|
```python
|
|
@@ -1122,8 +1215,8 @@ Only the `openai` voice engine accepts these; passing them to `google`,
|
|
|
1122
1215
|
`azure`, or `elevenlabs` raises `AiProviderCapabilityUnsupportedError` rather
|
|
1123
1216
|
than ignoring them.
|
|
1124
1217
|
|
|
1125
|
-
A blank
|
|
1126
|
-
|
|
1218
|
+
A blank `completions.retry_policy` is treated as unconfigured and falls back
|
|
1219
|
+
to `default` across every OpenAI-backed capability.
|
|
1127
1220
|
|
|
1128
1221
|
## Classification
|
|
1129
1222
|
|
|
@@ -1138,7 +1231,7 @@ prompt and parsing an answer. The model router (a later section) is one consumer
|
|
|
1138
1231
|
```python
|
|
1139
1232
|
from ai_api_unified import AIFactory, AIClassificationChoice
|
|
1140
1233
|
|
|
1141
|
-
client = AIFactory.get_ai_classifier_client() #
|
|
1234
|
+
client = AIFactory.get_ai_classifier_client() # classifier.engine from models.json
|
|
1142
1235
|
result = client.classify_choice(
|
|
1143
1236
|
"My invoice shows two charges for one order.",
|
|
1144
1237
|
[
|
|
@@ -1208,8 +1301,8 @@ logger; it is never a silent zero.
|
|
|
1208
1301
|
|
|
1209
1302
|
| Setting | Meaning |
|
|
1210
1303
|
| ------- | ------- |
|
|
1211
|
-
| `
|
|
1212
|
-
| `
|
|
1304
|
+
| `classifier.engine` (`models.json`) | Engine token. When unset, `openai-decisions` is used if `OPENAI_API_KEY` is set; otherwise the factory raises. |
|
|
1305
|
+
| `classifier.model` | Model override; blank falls back to `completions.model` for structured-output engines, then the engine's default (`gpt-6-luna` for `openai-decisions`). |
|
|
1213
1306
|
| `AIFactory.get_ai_classifier_client(classifier_engine=, model_name=, base_url=)` | Arguments override the settings. |
|
|
1214
1307
|
|
|
1215
1308
|
Exports: `AIBaseClassifier`, `AIClassifierCapabilitiesBase`,
|
|
@@ -1238,20 +1331,21 @@ so it acts only on failures another model could serve.
|
|
|
1238
1331
|
|
|
1239
1332
|
### Setting up a chain
|
|
1240
1333
|
|
|
1241
|
-
From
|
|
1242
|
-
|
|
1243
|
-
```bash
|
|
1244
|
-
# The primary stays where it always was.
|
|
1245
|
-
COMPLETIONS_ENGINE=claude
|
|
1246
|
-
COMPLETIONS_MODEL_NAME=claude-opus-5
|
|
1247
|
-
|
|
1248
|
-
# Ordered engine:model pairs to try after the primary fails.
|
|
1249
|
-
COMPLETIONS_FALLBACKS=openai:gpt-5.6-luna,google-gemini:gemini-3.7-flash
|
|
1334
|
+
From `models.json`, which is the usual way:
|
|
1250
1335
|
|
|
1251
|
-
|
|
1252
|
-
|
|
1336
|
+
```json
|
|
1337
|
+
"completions": {
|
|
1338
|
+
"engine": "claude",
|
|
1339
|
+
"model": "claude-opus-5",
|
|
1340
|
+
"fallbacks": ["openai:gpt-5.6-luna", "google-gemini:gemini-3.7-flash"],
|
|
1341
|
+
"fallback_on": "unavailable,rate_limited,quota_exhausted,auth_rejected"
|
|
1342
|
+
}
|
|
1253
1343
|
```
|
|
1254
1344
|
|
|
1345
|
+
`fallbacks` lists the `engine:model` pairs to try, in order, after the
|
|
1346
|
+
primary fails; `fallback_on` names the failures that trigger a move (the
|
|
1347
|
+
value above is the default).
|
|
1348
|
+
|
|
1255
1349
|
Or in code, which overrides the settings:
|
|
1256
1350
|
|
|
1257
1351
|
```python
|
|
@@ -1270,7 +1364,7 @@ client = AIFactory.get_ai_completions_client(
|
|
|
1270
1364
|
|
|
1271
1365
|
Rules for the chain:
|
|
1272
1366
|
|
|
1273
|
-
- The split in `
|
|
1367
|
+
- The split in each `fallbacks` entry is on the first colon only, so Bedrock
|
|
1274
1368
|
ids keep theirs: `bedrock:amazon.nova-lite-v1:0`. A pair with no model
|
|
1275
1369
|
(`google-gemini`) uses that engine's default model.
|
|
1276
1370
|
- Each fallback engine needs its own credentials and optional extra, exactly
|
|
@@ -1310,11 +1404,12 @@ Rules for the chain:
|
|
|
1310
1404
|
| `AUTH_REJECTED` | yes | 401/403, a revoked or invalid API key, a disabled organization, expired AWS credentials |
|
|
1311
1405
|
| `MODEL_UNAVAILABLE` | no | 404, an unknown or retired model id, a model not offered in the region |
|
|
1312
1406
|
| `TIMEOUT` | no | the call's `request_timeout_seconds` passed with no answer |
|
|
1313
|
-
| `
|
|
1407
|
+
| `INVALID_OUTPUT` | no | a structured answer that is not valid JSON, not a JSON object, or fails the response model (`StructuredOutputValidationError`) |
|
|
1408
|
+
| `None` | never | a safety refusal, request validation before any call |
|
|
1314
1409
|
|
|
1315
1410
|
`MODEL_UNAVAILABLE` is off by default because an unknown model id usually
|
|
1316
1411
|
means a configuration typo, and a working fallback would hide it. Turn it on
|
|
1317
|
-
with `fallback_on`
|
|
1412
|
+
with `fallback_on` when you want an unknown or
|
|
1318
1413
|
retired model to fail over.
|
|
1319
1414
|
|
|
1320
1415
|
`TIMEOUT` is off by default because the deadline is yours, and a failover
|
|
@@ -1336,6 +1431,15 @@ often a configuration error too; the `failover` and `served_by_fallback` log
|
|
|
1336
1431
|
events (below) record it while traffic keeps flowing. Remove
|
|
1337
1432
|
`auth_rejected` from `fallback_on` to make it fail loudly instead.
|
|
1338
1433
|
|
|
1434
|
+
`INVALID_OUTPUT` is off by default because the provider billed the bad answer, and
|
|
1435
|
+
failing over buys a second billed answer on another model: a spending
|
|
1436
|
+
decision. The error is `StructuredOutputValidationError`, a `ValueError` (so
|
|
1437
|
+
existing handlers keep working) that carries the discarded answer's `usage`,
|
|
1438
|
+
`raw_text`, and `finish_reason`. Turn the reason on to let the chain try the
|
|
1439
|
+
next model, and the router escalate, on bad output; each discarded answer
|
|
1440
|
+
then shows on the request's cost report as a wasted line item (see
|
|
1441
|
+
"Per-request cost").
|
|
1442
|
+
|
|
1339
1443
|
A safety refusal is never a trigger: retrying a
|
|
1340
1444
|
refused request on another model is a policy decision, not a reliability one,
|
|
1341
1445
|
and it belongs in your code if you want it.
|
|
@@ -1443,8 +1547,8 @@ across the chain.
|
|
|
1443
1547
|
|
|
1444
1548
|
| Setting | Meaning |
|
|
1445
1549
|
| ------- | ------- |
|
|
1446
|
-
| `
|
|
1447
|
-
| `
|
|
1550
|
+
| `completions.fallbacks` (`models.json`) | Ordered `engine:model` pairs tried after the primary fails. Empty or absent means no chain. |
|
|
1551
|
+
| `completions.fallback_on` | `AiFallbackReason` values (a list, or comma-separated) that trigger a move. Default `unavailable,rate_limited,quota_exhausted,auth_rejected`. |
|
|
1448
1552
|
| `fallbacks=` (factory argument) | Same as the setting, as `(engine, model)` tuples or `AIFallbackCandidate` objects; overrides the setting. `[]` turns the chain off. |
|
|
1449
1553
|
| `fallback_on=` (factory argument) | Same as the setting, as reasons or their values; overrides the setting. |
|
|
1450
1554
|
|
|
@@ -1502,9 +1606,8 @@ can read.
|
|
|
1502
1606
|
}
|
|
1503
1607
|
```
|
|
1504
1608
|
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
are, and call the factory as usual:
|
|
1609
|
+
Put it in the `router` section of `models.json`, keep `completions.engine`
|
|
1610
|
+
and `completions.model` as they are, and call the factory as usual:
|
|
1508
1611
|
|
|
1509
1612
|
```python
|
|
1510
1613
|
client = AIFactory.get_ai_completions_client() # an AiRouterCompletions
|
|
@@ -1513,17 +1616,16 @@ print(client.last_tier, client.last_decision)
|
|
|
1513
1616
|
```
|
|
1514
1617
|
|
|
1515
1618
|
In code, pass `router=AIRouterConfig(...)` instead. The configured
|
|
1516
|
-
completions LLM, with its `
|
|
1619
|
+
completions LLM, with its `completions.fallbacks` chain, is the **default
|
|
1517
1620
|
route**: where calls go when no classifier is available, when the
|
|
1518
1621
|
classifier fails, or when `default_tier` is unset.
|
|
1519
1622
|
|
|
1520
1623
|
### The classifier
|
|
1521
1624
|
|
|
1522
1625
|
The router uses a classification engine (see Classification). Resolution
|
|
1523
|
-
order: `
|
|
1524
|
-
`classifier
|
|
1525
|
-
|
|
1526
|
-
set). Set either to `none` to route without a classifier; if the
|
|
1626
|
+
order: the router's own `classifier` entry, then the top-level
|
|
1627
|
+
`classifier` section of `models.json`, then `openai-decisions` when
|
|
1628
|
+
`OPENAI_API_KEY` is set. Set either to `none` to route without a classifier; if the
|
|
1527
1629
|
classifier cannot be built, the router logs `classifier_unavailable` once
|
|
1528
1630
|
and degrades to the default route, and never raises at startup.
|
|
1529
1631
|
|
|
@@ -1587,8 +1689,8 @@ cost by tier needs no extra work. The classifier call itself carries
|
|
|
1587
1689
|
|
|
1588
1690
|
| Setting | Meaning |
|
|
1589
1691
|
| ------- | ------- |
|
|
1590
|
-
| `
|
|
1591
|
-
| `
|
|
1692
|
+
| `router` (`models.json`) | `tiers`, optional `classifier`, `default_tier`, `on_exhausted`, `fallback_on`. |
|
|
1693
|
+
| `classifier` (`models.json`) | The classifier the router uses when its own `classifier` entry is absent; `{"engine": "none"}` routes without one. |
|
|
1592
1694
|
| `AIFactory.get_ai_completions_client(router=AIRouterConfig(...))` | Code-first configuration; overrides the setting. |
|
|
1593
1695
|
|
|
1594
1696
|
Exports: `AiRouterCompletions`, `AIRouterConfig`, `AIRouterTier`,
|
|
@@ -1644,7 +1746,7 @@ the results of the most recent check.
|
|
|
1644
1746
|
|
|
1645
1747
|
| Setting | Meaning |
|
|
1646
1748
|
| ------- | ------- |
|
|
1647
|
-
| `
|
|
1749
|
+
| `model_check.mode` (`models.json`) | `prune` (default): drop missing models with a warning. `strict`: raise on any missing model. `off`: skip the check. `model_check.timeout_seconds` bounds each probe (default 10). |
|
|
1648
1750
|
|
|
1649
1751
|
A client with only a primary and no chain or router runs no check: the
|
|
1650
1752
|
engine fails on its own at build.
|
|
@@ -1683,7 +1785,7 @@ the value may also be inline JSON).
|
|
|
1683
1785
|
{
|
|
1684
1786
|
"allow": ["claude:*", "google-gemini:*", "openai-decisions:*"],
|
|
1685
1787
|
"deny": ["openai:gpt-4*", "bedrock"],
|
|
1686
|
-
"completions": {"engine": "google-gemini", "model": "gemini-2.5-flash",
|
|
1788
|
+
"completions": {"engine": "google-gemini", "model": "gemini-2.5-flash", "thinking": "medium",
|
|
1687
1789
|
"fallbacks": ["claude:claude-sonnet-5"],
|
|
1688
1790
|
"fallback_on": "unavailable,rate_limited,quota_exhausted,auth_rejected"},
|
|
1689
1791
|
"classifier": {"engine": "openai-decisions", "model": "gpt-6-luna"},
|
|
@@ -1705,19 +1807,17 @@ poetry run python scripts/migrate2json.py # writes ./models.json
|
|
|
1705
1807
|
poetry run python scripts/migrate2json.py --comment-env # and comments the moved keys out of .env
|
|
1706
1808
|
```
|
|
1707
1809
|
|
|
1708
|
-
It places each selection
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
touches secrets.
|
|
1810
|
+
It places each selection in its section, embeds a router file the old
|
|
1811
|
+
`.env` pointed at, fills structural defaults from `models_sample.json`,
|
|
1812
|
+
leaves the allow and deny lists empty, and never touches secrets.
|
|
1712
1813
|
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
without editing the file. Secrets (API keys, AWS credentials), `AWS_REGION`,
|
|
1814
|
+
The old selection keys are deprecated. One left in `.env` or the process
|
|
1815
|
+
environment is still accepted and overrides the file, so a deployment does
|
|
1816
|
+
not break, and the library logs one warning per key naming the
|
|
1817
|
+
`models.json` field and the migrator command
|
|
1818
|
+
(`ai_config_event=deprecated_selection_key`). Precedence, highest first: an
|
|
1819
|
+
explicit factory argument, a deprecated environment key, the file, the
|
|
1820
|
+
library default. Secrets (API keys, AWS credentials), `AWS_REGION`,
|
|
1721
1821
|
base-URL overrides, and `AI_MIDDLEWARE_CONFIG_PATH` stay in `.env`.
|
|
1722
1822
|
|
|
1723
1823
|
### Allow and deny lists
|
|
@@ -1738,43 +1838,43 @@ classifier, and the embeddings, image, and video selections.
|
|
|
1738
1838
|
model, raises `ValueError` naming the pattern. Runtime never calls a
|
|
1739
1839
|
denied model.
|
|
1740
1840
|
|
|
1741
|
-
###
|
|
1841
|
+
### Engine selectors
|
|
1842
|
+
|
|
1843
|
+
There is no implicit default provider. Set the engine for each capability
|
|
1844
|
+
you use in `models.json`:
|
|
1845
|
+
|
|
1846
|
+
| `models.json` field | Valid values |
|
|
1847
|
+
| ------------------- | ------------ |
|
|
1848
|
+
| `completions.engine` | `openai`, `openai-responses`, `openai-compatible`, `claude`, `google-gemini`, `bedrock`, Bedrock-routed aliases such as `nova`, `anthropic`, `llama`, `mistral`, `cohere`, `ai21`, `rerank` |
|
|
1849
|
+
| `embeddings.engine` | `openai`, `titan`, `google-gemini`, `voyage` |
|
|
1850
|
+
| `classifier.engine` | `openai-decisions`, `openai`, `openai-responses`, `openai-compatible`, `claude`, `google-gemini`, `bedrock`, `anthropic` (see Classification) |
|
|
1851
|
+
| `images.engine` | `openai`, `google-gemini`, `nova-canvas`, `bedrock`, `nova` |
|
|
1852
|
+
| `videos.engine` | `openai`, `google-gemini`, `bedrock`, `nova`, `nova-reel` |
|
|
1853
|
+
| `voice.engine` | `openai`, `google`, `azure`, `elevenlabs` |
|
|
1854
|
+
|
|
1855
|
+
### Model selections
|
|
1742
1856
|
|
|
1743
|
-
|
|
1744
|
-
|
|
1857
|
+
Every section of `models.json` is documented in
|
|
1858
|
+
[`models_sample.json`](models_sample.json). The fields, by section:
|
|
1745
1859
|
|
|
1746
|
-
|
|
|
1747
|
-
|
|
|
1748
|
-
| `
|
|
1749
|
-
| `
|
|
1750
|
-
| `
|
|
1751
|
-
| `
|
|
1752
|
-
| `
|
|
1753
|
-
| `
|
|
1860
|
+
| Section | Fields |
|
|
1861
|
+
| ------- | ------ |
|
|
1862
|
+
| `completions` | `engine`, `model`, `fallbacks` (see Model fallback), `fallback_on`, `retry_policy`, `thinking` (see Thinking levels) |
|
|
1863
|
+
| `classifier` | `engine`, `model`, `base_url`, `timeout_seconds`, `instructions` |
|
|
1864
|
+
| `router` | `tiers`, `classifier`, `default_tier`, `on_exhausted`, `fallback_on` (see Model routing) |
|
|
1865
|
+
| `model_check` | `mode` (`prune`, `strict`, `off`), `timeout_seconds` (see Configured-model check) |
|
|
1866
|
+
| `embeddings` | `engine`, `model` (`gemini-embedding-2` enables multimodal embeddings), `dimensions` (`0` keeps the provider default) |
|
|
1867
|
+
| `images` | `engine`, `model` |
|
|
1868
|
+
| `videos` | `engine`, `model`, `output_dir`, `poll_interval_seconds`, `timeout_seconds` |
|
|
1869
|
+
| `voice` | `engine`, `tts_model`, `language` |
|
|
1870
|
+
| `geo_residency` | `US`, `USA`, or `United States` normalize to US routing where supported |
|
|
1754
1871
|
|
|
1755
|
-
###
|
|
1872
|
+
### Settings that stay in `.env`
|
|
1756
1873
|
|
|
1757
1874
|
| Environment variable | Notes |
|
|
1758
1875
|
| --------------------------- | ------------------------------------------------------------------------------------------- |
|
|
1759
|
-
| `COMPLETIONS_MODEL_NAME` | Optional completions model override |
|
|
1760
|
-
| `COMPLETIONS_FALLBACKS` | Optional ordered `engine:model` pairs tried after the primary fails (see Model fallback) |
|
|
1761
|
-
| `COMPLETIONS_FALLBACK_ON` | Optional comma-separated `AiFallbackReason` values that trigger a fallback; default `unavailable,rate_limited,quota_exhausted,auth_rejected` |
|
|
1762
1876
|
| `AI_MODELS_CONFIG` | Path to `models.json`/`.yaml`, or inline JSON; unset uses `./models.json` when present; blank turns the file off |
|
|
1763
|
-
| `COMPLETIONS_ROUTER_CONFIG` | Override of the file's `router` section: a JSON file path or inline JSON mapping tiers to `engine:model` chains (see Model routing) |
|
|
1764
|
-
| `COMPLETIONS_ROUTER_CLASSIFIER` | Optional `engine:model` for the router's classifier, or `none`; default is the classification factory's resolution |
|
|
1765
|
-
| `AI_MODEL_CHECK` | Optional: `prune` (default), `strict`, or `off` for the configured-model check (see Configured-model check) |
|
|
1766
|
-
| `AI_MODEL_CHECK_TIMEOUT_SECONDS` | Optional per-probe timeout for the configured-model check (default 10) |
|
|
1767
|
-
| `EMBEDDING_MODEL_NAME` | Optional embeddings model override. `gemini-embedding-2` enables multimodal embeddings. |
|
|
1768
|
-
| `CLASSIFIER_MODEL_NAME` | Optional classification model override; blank falls back to `COMPLETIONS_MODEL_NAME`, then the engine default |
|
|
1769
|
-
| `IMAGE_MODEL_NAME` | Optional image model override |
|
|
1770
|
-
| `VIDEO_MODEL_NAME` | Optional video model override |
|
|
1771
|
-
| `VIDEO_OUTPUT_DIR` | Optional local output directory for materialized video artifacts |
|
|
1772
|
-
| `VIDEO_POLL_INTERVAL_SECONDS` | Optional default poll interval for video job waits |
|
|
1773
|
-
| `VIDEO_TIMEOUT_SECONDS` | Optional default timeout for blocking video generation |
|
|
1774
1877
|
| `BEDROCK_VIDEO_OUTPUT_S3_URI` | Required for Nova Reel unless provided per request |
|
|
1775
|
-
| `DEFAULT_GEMINI_TTS_MODEL` | Optional Google voice model override |
|
|
1776
|
-
| `EMBEDDING_DIMENSIONS` | Optional embeddings dimension override. Leave unset for provider defaults. |
|
|
1777
|
-
| `AI_API_GEO_RESIDENCY` | Optional geo hint. `US`, `USA`, or `United States` normalize to US routing where supported. |
|
|
1778
1878
|
| `AI_MIDDLEWARE_CONFIG_PATH` | Optional YAML config path for observability and PII middleware |
|
|
1779
1879
|
| `ANTHROPIC_BASE_URL_OVERRIDE` | Optional API base-URL override for the `claude` engine (https required) |
|
|
1780
1880
|
| `OPENAI_BASE_URL_OVERRIDE` | Optional API base-URL override for the OpenAI engines (https required) |
|
|
@@ -1863,15 +1963,8 @@ Required:
|
|
|
1863
1963
|
|
|
1864
1964
|
- `OPENAI_API_KEY`
|
|
1865
1965
|
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
- `OPENAI_BASE_URL`
|
|
1869
|
-
- `COMPLETIONS_MODEL_NAME`
|
|
1870
|
-
- `EMBEDDING_MODEL_NAME`
|
|
1871
|
-
- `IMAGE_MODEL_NAME`
|
|
1872
|
-
- `VIDEO_MODEL_NAME`
|
|
1873
|
-
- `EMBEDDING_DIMENSIONS`
|
|
1874
|
-
- `AI_API_GEO_RESIDENCY`
|
|
1966
|
+
Optional: `OPENAI_BASE_URL_OVERRIDE`. Models and dimensions are selected in
|
|
1967
|
+
`models.json`.
|
|
1875
1968
|
|
|
1876
1969
|
#### Anthropic (native Claude API)
|
|
1877
1970
|
|
|
@@ -1879,9 +1972,7 @@ Required for the `claude` completions engine:
|
|
|
1879
1972
|
|
|
1880
1973
|
- `ANTHROPIC_API_KEY`
|
|
1881
1974
|
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
- `COMPLETIONS_MODEL_NAME` (defaults to `claude-opus-5`)
|
|
1975
|
+
The model is `completions.model` in `models.json` (default `claude-opus-5`).
|
|
1885
1976
|
|
|
1886
1977
|
Claude via Amazon Bedrock (the `anthropic` engine) does not use
|
|
1887
1978
|
`ANTHROPIC_API_KEY`; it authenticates with AWS credentials like the other
|
|
@@ -1899,15 +1990,8 @@ Required:
|
|
|
1899
1990
|
- `AWS_REGION`
|
|
1900
1991
|
- standard AWS credentials in environment or runtime IAM context
|
|
1901
1992
|
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
- `COMPLETIONS_MODEL_NAME`
|
|
1905
|
-
- `EMBEDDING_MODEL_NAME`
|
|
1906
|
-
- `IMAGE_MODEL_NAME`
|
|
1907
|
-
- `VIDEO_MODEL_NAME`
|
|
1908
|
-
- `BEDROCK_VIDEO_OUTPUT_S3_URI`
|
|
1909
|
-
- `EMBEDDING_DIMENSIONS`
|
|
1910
|
-
- `AI_API_GEO_RESIDENCY`
|
|
1993
|
+
Optional: `BEDROCK_VIDEO_OUTPUT_S3_URI` (Nova Reel). Models and dimensions
|
|
1994
|
+
are selected in `models.json`.
|
|
1911
1995
|
|
|
1912
1996
|
If your AWS profile uses `aws login` (a `login_session` profile), install
|
|
1913
1997
|
`botocore[crt]` as well; botocore needs it to read those credentials.
|
|
@@ -1917,9 +2001,11 @@ If your AWS profile uses `aws login` (a `login_session` profile), install
|
|
|
1917
2001
|
AWS hosts these models itself, so requests go to AWS under your AWS account
|
|
1918
2002
|
and never reach DeepSeek, Alibaba, or Z.ai.
|
|
1919
2003
|
|
|
2004
|
+
```json
|
|
2005
|
+
"completions": {"engine": "bedrock", "model": "deepseek.v3.2"}
|
|
2006
|
+
```
|
|
2007
|
+
|
|
1920
2008
|
```dotenv
|
|
1921
|
-
COMPLETIONS_ENGINE=bedrock
|
|
1922
|
-
COMPLETIONS_MODEL_NAME=deepseek.v3.2
|
|
1923
2009
|
AWS_REGION=us-east-1
|
|
1924
2010
|
AWS_PROFILE=<your profile>
|
|
1925
2011
|
```
|
|
@@ -2001,7 +2087,7 @@ Required:
|
|
|
2001
2087
|
Optional:
|
|
2002
2088
|
|
|
2003
2089
|
- `MICROSOFT_COGNITIVE_SERVICES_ENDPOINT`
|
|
2004
|
-
- `
|
|
2090
|
+
- `voice.language` in `models.json`
|
|
2005
2091
|
|
|
2006
2092
|
#### ElevenLabs
|
|
2007
2093
|
|
|
@@ -2207,6 +2293,63 @@ capability.supports_org_id, capability.supports_org_name, capability.requirement
|
|
|
2207
2293
|
# iam:ListAccountAliases; identity falls back to id-only without it")
|
|
2208
2294
|
```
|
|
2209
2295
|
|
|
2296
|
+
#### Per-request cost
|
|
2297
|
+
|
|
2298
|
+
A request's total cost includes every call it made: classification, failed
|
|
2299
|
+
attempts, fallbacks, and escalations. One routed structured-output request
|
|
2300
|
+
can be four provider calls: the classifier, a 529 on the first tier, a
|
|
2301
|
+
fallback on the same tier, and an escalation that answers. Each call still
|
|
2302
|
+
emits its own `ai_api_call_cost` event; the request adds one total on top.
|
|
2303
|
+
|
|
2304
|
+
Every client keeps the report for the last request it ran as the outermost
|
|
2305
|
+
call:
|
|
2306
|
+
|
|
2307
|
+
```python
|
|
2308
|
+
result = client.send_structured_output(prompt, response_model=Summary)
|
|
2309
|
+
report = client.last_request_cost # AIRequestCostReport
|
|
2310
|
+
report.total_usd # Decimal, every priced call
|
|
2311
|
+
report.classifier_usd # the router's classifier call
|
|
2312
|
+
report.wasted_usd # billed answers nobody used
|
|
2313
|
+
report.billed_calls, report.failed_calls # 3 and 1 in the example above
|
|
2314
|
+
report.served_by # "claude:claude-opus-5"
|
|
2315
|
+
for item in report.calls: # AIRequestCostLineItem, in call order
|
|
2316
|
+
item.role, item.route_tier, item.outcome, item.usd_cost, item.served
|
|
2317
|
+
```
|
|
2318
|
+
|
|
2319
|
+
A failed attempt is a line item with no cost and no tokens; it still counts
|
|
2320
|
+
as a call. A failed attempt the provider billed, a structured answer that
|
|
2321
|
+
could not be parsed or validated (`StructuredOutputValidationError`) or a
|
|
2322
|
+
truncated one (`StructuredResponseTokenLimitError`), keeps the usage the
|
|
2323
|
+
exception carries, so the report prices it and counts it in `billed_calls`
|
|
2324
|
+
and `wasted_usd`; its per-call cost event carries `outcome: "error"`. A model
|
|
2325
|
+
with no registry pricing counts as billed but unpriced
|
|
2326
|
+
(`unpriced_calls`), so `total_usd` understates the request by its cost.
|
|
2327
|
+
The ledger records calls whether or not the observability middleware is on.
|
|
2328
|
+
|
|
2329
|
+
To roll several library calls into one logical request, such as a workflow
|
|
2330
|
+
step, open the scope yourself. Nested scopes join the outer one, and the
|
|
2331
|
+
clients inside keep no report of their own:
|
|
2332
|
+
|
|
2333
|
+
```python
|
|
2334
|
+
from ai_api_unified import request_cost_scope
|
|
2335
|
+
|
|
2336
|
+
with request_cost_scope("summarize-contract") as scope:
|
|
2337
|
+
router.send_structured_output(prompt, response_model=Summary)
|
|
2338
|
+
router.send_prompt("One-line title for this summary.")
|
|
2339
|
+
scope.report.total_usd # both calls, every attempt behind them
|
|
2340
|
+
```
|
|
2341
|
+
|
|
2342
|
+
When `emit_cost` is on, the outermost scope emits one `ai_api_request_cost`
|
|
2343
|
+
event on the cost topic as it closes (normally or on an exception):
|
|
2344
|
+
`request_id`, `operation`, `status`, `caller_id`, `session_id`,
|
|
2345
|
+
`workflow_id`, `served_by`, `served_by_tier`, `served_by_method`,
|
|
2346
|
+
`total_usd`, `classifier_usd`, `wasted_usd`, `billed_calls`,
|
|
2347
|
+
`failed_calls`, `unpriced_calls`, `elapsed_ms`, and `calls` (a compact list
|
|
2348
|
+
with `call_id`, `role`, `model`, `outcome`, `usd_cost`, `served`). The
|
|
2349
|
+
`request_id` is the router's `route_id` when routed, and every per-call
|
|
2350
|
+
event under the request carries it as `tag_request_id`, so the per-call
|
|
2351
|
+
events and the total join on one key.
|
|
2352
|
+
|
|
2210
2353
|
### PII Redaction
|
|
2211
2354
|
|
|
2212
2355
|
PII redaction is optional and requires one of:
|
|
@@ -2365,12 +2508,12 @@ git push origin v<version>
|
|
|
2365
2508
|
|
|
2366
2509
|
## Troubleshooting
|
|
2367
2510
|
|
|
2368
|
-
- `
|
|
2511
|
+
- An engine `must be configured explicitly` error: set that capability's `engine` in `models.json`.
|
|
2369
2512
|
- `AiProviderDependencyUnavailableError`: install the extra for the selected provider.
|
|
2370
2513
|
- Google auth errors: the OSS default is `GOOGLE_AUTH_METHOD=api_key`. If you switch to `service_account`, point `GOOGLE_APPLICATION_CREDENTIALS` at a valid local JSON credential file. Set `GOOGLE_PROJECT_ID` and `GOOGLE_LOCATION` when required.
|
|
2371
|
-
- Unexpected embeddings dimensions: leave `
|
|
2514
|
+
- Unexpected embeddings dimensions: leave `embeddings.dimensions` at `0` unless you intentionally want a non-default size.
|
|
2372
2515
|
- Google image generation or TTS failures in live tests often come from account or service state: a disabled cloud API, or missing paid-plan access. Check those before suspecting a library bug.
|
|
2373
|
-
- `
|
|
2516
|
+
- `geo_residency: "US"` is a best-effort routing hint. Only providers that expose regional routing controls can honor it directly.
|
|
2374
2517
|
|
|
2375
2518
|
## License
|
|
2376
2519
|
|