ai_api_unified 3.2.0__tar.gz → 3.6.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.6.0}/PKG-INFO +362 -189
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/README.md +361 -188
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/pyproject.toml +1 -1
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/__init__.py +21 -1
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/__version__.py +1 -1
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/ai_base.py +541 -73
- ai_api_unified-3.6.0/src/ai_api_unified/ai_completions_exceptions.py +107 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/ai_factory.py +112 -44
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/ai_models_config.py +36 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/ai_provider_exceptions.py +8 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/completions/ai_anthropic_completions.py +92 -25
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/completions/ai_bedrock_completions.py +152 -24
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/completions/ai_fallback_completions.py +89 -4
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/completions/ai_google_gemini_capabilities.py +2 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/completions/ai_google_gemini_completions.py +26 -23
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/completions/ai_openai_completions.py +34 -23
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/completions/ai_openai_responses_completions.py +22 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/completions/ai_router_completions.py +252 -172
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/middleware/__init__.py +12 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/middleware/observability.py +112 -29
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/middleware/observability_runtime.py +172 -51
- ai_api_unified-3.6.0/src/ai_api_unified/middleware/request_cost.py +736 -0
- ai_api_unified-3.6.0/src/ai_api_unified/thinking.py +545 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/util/ai_model_checker.py +104 -23
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/util/ai_model_probes.py +117 -41
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/util/env_settings.py +53 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/videos/ai_bedrock_videos.py +1 -1
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/videos/ai_google_gemini_videos.py +4 -3
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/videos/ai_openai_videos.py +3 -1
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.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.6.0}/LICENSE +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/ai_anthropic_base.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/ai_bedrock_base.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/ai_google_base.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/ai_openai_base.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/ai_provider_loader.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/ai_provider_registry.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/classification/__init__.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/classification/ai_openai_decisions_classifier.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/classification/ai_structured_output_classifier.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/completions/__init__.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/completions/ai_openai_compatible_completions.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/embeddings/__init__.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/embeddings/ai_google_gemini_embeddings.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/embeddings/ai_openai_embeddings.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/embeddings/ai_titan_embeddings.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/embeddings/ai_voyage_embeddings.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/images/__init__.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/images/ai_bedrock_images.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/images/ai_google_gemini_images.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/images/ai_openai_images.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/middleware/impl/__init__.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/middleware/impl/_presidio_redactor.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/middleware/impl/base_redactor.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/middleware/impl/custom_recognizer_factory.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/middleware/impl/noop_redactor.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/middleware/impl/presidio_log_control.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/middleware/middleware.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/middleware/middleware_config.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/middleware/pii_redactor.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/middleware/redaction_exceptions.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/pricing/__init__.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/pricing/model_pricing.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/pricing/pricing_registry.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/routing/__init__.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/routing/router_config.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/util/__init__.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/util/_lazy_pydub.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/util/package_metadata_validation.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/util/utils.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/videos/__init__.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/videos/frame_helpers.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/voice/__init__.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/voice/ai_voice_azure.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/voice/ai_voice_base.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/voice/ai_voice_elevenlabs.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/voice/ai_voice_google.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/voice/ai_voice_google_gemini_voices.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.0}/src/ai_api_unified/voice/ai_voice_openai.py +0 -0
- {ai_api_unified-3.2.0 → ai_api_unified-3.6.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.6.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.6.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
|
|
1334
|
+
From `models.json`, which is the usual way:
|
|
1242
1335
|
|
|
1243
|
-
```
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
# Optional. Which failures trigger a move; this is the default.
|
|
1252
|
-
COMPLETIONS_FALLBACK_ON=unavailable,rate_limited,quota_exhausted,auth_rejected
|
|
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,14 +1364,17 @@ 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
|
|
1277
1371
|
as it would if it were the primary.
|
|
1278
1372
|
- The factory checks every fallback engine token against the provider
|
|
1279
1373
|
registry when it builds the client, so a typo fails at startup, long
|
|
1280
|
-
before any outage.
|
|
1374
|
+
before any outage. The [configured-model check](#configured-model-check)
|
|
1375
|
+
then probes each fallback model with its engine's credentials and prunes,
|
|
1376
|
+
with a WARNING, any that does not exist or cannot be reached (no key,
|
|
1377
|
+
rejected key, extra not installed).
|
|
1281
1378
|
- Passing `fallbacks=[]` in code turns the chain off even when the setting
|
|
1282
1379
|
is present.
|
|
1283
1380
|
|
|
@@ -1288,11 +1385,13 @@ Rules for the chain:
|
|
|
1288
1385
|
2. If the engine raises `AiProviderRequestError` with a `fallback_reason` in
|
|
1289
1386
|
the configured set, the client logs a warning naming both candidates and
|
|
1290
1387
|
the reason, and sends the request to the next candidate.
|
|
1291
|
-
3. The client builds a fallback the first time a request needs it.
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1388
|
+
3. The client builds a fallback the first time a request needs it. The
|
|
1389
|
+
configured-model check has already pruned fallbacks that are missing or
|
|
1390
|
+
unreachable, so a build failure here is rare (an unsupported option,
|
|
1391
|
+
for example). A key revoked after startup fails the request instead,
|
|
1392
|
+
as `AUTH_REJECTED` (see step 5). When a build fails, the client logs it
|
|
1393
|
+
once and skips that candidate for the rest of the process, so it never
|
|
1394
|
+
blocks the rest of the chain.
|
|
1296
1395
|
4. The client skips, with a warning, a fallback that lacks a capability the
|
|
1297
1396
|
call needs (structured output, tool use, async, streaming).
|
|
1298
1397
|
5. After a `QUOTA_EXHAUSTED` or `AUTH_REJECTED` failure, the client skips
|
|
@@ -1310,11 +1409,12 @@ Rules for the chain:
|
|
|
1310
1409
|
| `AUTH_REJECTED` | yes | 401/403, a revoked or invalid API key, a disabled organization, expired AWS credentials |
|
|
1311
1410
|
| `MODEL_UNAVAILABLE` | no | 404, an unknown or retired model id, a model not offered in the region |
|
|
1312
1411
|
| `TIMEOUT` | no | the call's `request_timeout_seconds` passed with no answer |
|
|
1313
|
-
| `
|
|
1412
|
+
| `INVALID_OUTPUT` | no | a structured answer that is not valid JSON, not a JSON object, or fails the response model (`StructuredOutputValidationError`) |
|
|
1413
|
+
| `None` | never | a safety refusal, request validation before any call |
|
|
1314
1414
|
|
|
1315
1415
|
`MODEL_UNAVAILABLE` is off by default because an unknown model id usually
|
|
1316
1416
|
means a configuration typo, and a working fallback would hide it. Turn it on
|
|
1317
|
-
with `fallback_on`
|
|
1417
|
+
with `fallback_on` when you want an unknown or
|
|
1318
1418
|
retired model to fail over.
|
|
1319
1419
|
|
|
1320
1420
|
`TIMEOUT` is off by default because the deadline is yours, and a failover
|
|
@@ -1336,6 +1436,15 @@ often a configuration error too; the `failover` and `served_by_fallback` log
|
|
|
1336
1436
|
events (below) record it while traffic keeps flowing. Remove
|
|
1337
1437
|
`auth_rejected` from `fallback_on` to make it fail loudly instead.
|
|
1338
1438
|
|
|
1439
|
+
`INVALID_OUTPUT` is off by default because the provider billed the bad answer, and
|
|
1440
|
+
failing over buys a second billed answer on another model: a spending
|
|
1441
|
+
decision. The error is `StructuredOutputValidationError`, a `ValueError` (so
|
|
1442
|
+
existing handlers keep working) that carries the discarded answer's `usage`,
|
|
1443
|
+
`raw_text`, and `finish_reason`. Turn the reason on to let the chain try the
|
|
1444
|
+
next model, and the router escalate, on bad output; each discarded answer
|
|
1445
|
+
then shows on the request's cost report as a wasted line item (see
|
|
1446
|
+
"Per-request cost").
|
|
1447
|
+
|
|
1339
1448
|
A safety refusal is never a trigger: retrying a
|
|
1340
1449
|
refused request on another model is a policy decision, not a reliability one,
|
|
1341
1450
|
and it belongs in your code if you want it.
|
|
@@ -1443,8 +1552,8 @@ across the chain.
|
|
|
1443
1552
|
|
|
1444
1553
|
| Setting | Meaning |
|
|
1445
1554
|
| ------- | ------- |
|
|
1446
|
-
| `
|
|
1447
|
-
| `
|
|
1555
|
+
| `completions.fallbacks` (`models.json`) | Ordered `engine:model` pairs tried after the primary fails. Empty or absent means no chain. |
|
|
1556
|
+
| `completions.fallback_on` | `AiFallbackReason` values (a list, or comma-separated) that trigger a move. Default `unavailable,rate_limited,quota_exhausted,auth_rejected`. |
|
|
1448
1557
|
| `fallbacks=` (factory argument) | Same as the setting, as `(engine, model)` tuples or `AIFallbackCandidate` objects; overrides the setting. `[]` turns the chain off. |
|
|
1449
1558
|
| `fallback_on=` (factory argument) | Same as the setting, as reasons or their values; overrides the setting. |
|
|
1450
1559
|
|
|
@@ -1502,9 +1611,8 @@ can read.
|
|
|
1502
1611
|
}
|
|
1503
1612
|
```
|
|
1504
1613
|
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
are, and call the factory as usual:
|
|
1614
|
+
Put it in the `router` section of `models.json`, keep `completions.engine`
|
|
1615
|
+
and `completions.model` as they are, and call the factory as usual:
|
|
1508
1616
|
|
|
1509
1617
|
```python
|
|
1510
1618
|
client = AIFactory.get_ai_completions_client() # an AiRouterCompletions
|
|
@@ -1513,19 +1621,20 @@ print(client.last_tier, client.last_decision)
|
|
|
1513
1621
|
```
|
|
1514
1622
|
|
|
1515
1623
|
In code, pass `router=AIRouterConfig(...)` instead. The configured
|
|
1516
|
-
completions LLM, with its `
|
|
1624
|
+
completions LLM, with its `completions.fallbacks` chain, is the **default
|
|
1517
1625
|
route**: where calls go when no classifier is available, when the
|
|
1518
1626
|
classifier fails, or when `default_tier` is unset.
|
|
1519
1627
|
|
|
1520
1628
|
### The classifier
|
|
1521
1629
|
|
|
1522
1630
|
The router uses a classification engine (see Classification). Resolution
|
|
1523
|
-
order: `
|
|
1524
|
-
`classifier
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
classifier cannot be built, the router
|
|
1528
|
-
and degrades to the default route, and
|
|
1631
|
+
order: the router's own `classifier` entry, then the top-level
|
|
1632
|
+
`classifier` section of `models.json`, then `openai-decisions` when
|
|
1633
|
+
`OPENAI_API_KEY` is set. Set either to `none` to route without a classifier. The
|
|
1634
|
+
configured-model check probes the classifier model with the tiers; if it
|
|
1635
|
+
is missing or unreachable, or the classifier cannot be built, the router
|
|
1636
|
+
logs `classifier_unavailable` once and degrades to the default route, and
|
|
1637
|
+
never raises at startup.
|
|
1529
1638
|
|
|
1530
1639
|
The classifier reads the latest user message only, cut to the engine's
|
|
1531
1640
|
`max_input_chars`. The system prompt is left out: it is long, constant,
|
|
@@ -1548,9 +1657,10 @@ and `classifier.instructions` prepends your own guidance.
|
|
|
1548
1657
|
5. If the chain runs out of candidates on a trigger reason
|
|
1549
1658
|
(`fallback_on`), the router escalates to the next tier up and logs
|
|
1550
1659
|
`tier_exhausted`; with `on_exhausted: raise`, on a non-trigger error, or
|
|
1551
|
-
on the top tier, the error propagates
|
|
1552
|
-
|
|
1553
|
-
|
|
1660
|
+
on the top tier, the error propagates. Tier models that are missing or
|
|
1661
|
+
unreachable were pruned at startup by the configured-model check; if a
|
|
1662
|
+
tier's primary still cannot be built, the router logs
|
|
1663
|
+
`tier_unbuildable` once and skips the tier the same way.
|
|
1554
1664
|
|
|
1555
1665
|
### Seeing where a request went
|
|
1556
1666
|
|
|
@@ -1587,8 +1697,8 @@ cost by tier needs no extra work. The classifier call itself carries
|
|
|
1587
1697
|
|
|
1588
1698
|
| Setting | Meaning |
|
|
1589
1699
|
| ------- | ------- |
|
|
1590
|
-
| `
|
|
1591
|
-
| `
|
|
1700
|
+
| `router` (`models.json`) | `tiers`, optional `classifier`, `default_tier`, `on_exhausted`, `fallback_on`. |
|
|
1701
|
+
| `classifier` (`models.json`) | The classifier the router uses when its own `classifier` entry is absent; `{"engine": "none"}` routes without one. |
|
|
1592
1702
|
| `AIFactory.get_ai_completions_client(router=AIRouterConfig(...))` | Code-first configuration; overrides the setting. |
|
|
1593
1703
|
|
|
1594
1704
|
Exports: `AiRouterCompletions`, `AIRouterConfig`, `AIRouterTier`,
|
|
@@ -1596,13 +1706,33 @@ Exports: `AiRouterCompletions`, `AIRouterConfig`, `AIRouterTier`,
|
|
|
1596
1706
|
|
|
1597
1707
|
## Configured-model check
|
|
1598
1708
|
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1709
|
+
Every model the completions client may call is checked before the client
|
|
1710
|
+
is returned: the completions LLM, each fallback, each router tier
|
|
1711
|
+
candidate, and the router's classifier. The check asks two questions:
|
|
1712
|
+
does the provider serve this model, and can this process reach it with
|
|
1713
|
+
the credentials the engine will use? A typo, a retired id, a model not
|
|
1714
|
+
offered in your region, or a fallback whose key is missing then shows up
|
|
1715
|
+
at startup, not in the middle of a failover.
|
|
1716
|
+
|
|
1717
|
+
### When it runs
|
|
1718
|
+
|
|
1719
|
+
- On every `AIFactory.get_ai_completions_client()` call, with or without
|
|
1720
|
+
a chain or router. A lone primary is checked too.
|
|
1721
|
+
- Each model is probed at most once per process for a given engine,
|
|
1722
|
+
model id, base URL, and credentials. Later factory calls reuse the
|
|
1723
|
+
result and make no network call. A probe runs again only when one of
|
|
1724
|
+
those four changes: an edited `models.json` that names a new model or
|
|
1725
|
+
base URL, or a rotated key. Credentials enter the memo key as a
|
|
1726
|
+
one-way hash of the settings the probe reads (for example
|
|
1727
|
+
`ANTHROPIC_API_KEY` and `ANTHROPIC_BASE_URL_OVERRIDE`); the key itself
|
|
1728
|
+
is never stored. AWS credentials from an instance role or an SSO cache
|
|
1729
|
+
are not in the hash; set `AWS_PROFILE` or the key variables when you
|
|
1730
|
+
need a rotation to force a recheck, or call
|
|
1731
|
+
`AiModelChecker.reset_cache()`.
|
|
1732
|
+
- A probe that failed for a transient reason (timeout, 5xx, dropped
|
|
1733
|
+
connection) is not cached, so the next factory call probes that model
|
|
1734
|
+
again.
|
|
1735
|
+
- `models.json` itself is reloaded when its modification time changes.
|
|
1606
1736
|
|
|
1607
1737
|
### What it does
|
|
1608
1738
|
|
|
@@ -1611,32 +1741,37 @@ router's classifier.
|
|
|
1611
1741
|
the raw id, Bedrock `get_foundation_model` (or `get_inference_profile`
|
|
1612
1742
|
for `us.`, `eu.`, `apac.`, and `global.` ids) in `AWS_REGION`, an
|
|
1613
1743
|
OpenAI-compatible server's `/models` list, and the Decisions API's fixed
|
|
1614
|
-
model set.
|
|
1615
|
-
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1744
|
+
model set. Calls run in parallel, so startup pays one round trip.
|
|
1745
|
+
- Each model gets one status:
|
|
1746
|
+
|
|
1747
|
+
| Status | Meaning | Effect |
|
|
1748
|
+
| ------ | ------- | ------ |
|
|
1749
|
+
| `available` | The provider returned the model's metadata. | Kept. |
|
|
1750
|
+
| `missing` | The provider has no such model (404, or an id outside a fixed catalog). | Pruned, WARNING. |
|
|
1751
|
+
| `unreachable` | The engine cannot call the model: no key, a key rejected as invalid (401, Gemini `API_KEY_INVALID`, Bedrock `UnrecognizedClient` / `InvalidSignature` / `ExpiredToken`, no AWS credentials), no base URL for an OpenAI-compatible server, or the engine's extra not installed. | Pruned, WARNING. |
|
|
1752
|
+
| `unverifiable` | The probe cannot ask, but nothing says the call would fail: the engine default model (no id configured), a permission denial (403, Bedrock `AccessDenied`) that may cover only the catalog call, Vertex auth, or a server without a model catalog. | Kept, INFO. |
|
|
1753
|
+
| `error` | The probe failed for another reason (timeout, 5xx, network). | Kept, WARNING; not cached. |
|
|
1754
|
+
|
|
1755
|
+
- A pruned fallback leaves the chain: `configured model openai:gpt-nope
|
|
1756
|
+
is not available and will be ignored`, or `configured model
|
|
1757
|
+
claude:claude-haiku-4-5 cannot be reached and will be ignored`. A
|
|
1758
|
+
pruned tier candidate leaves its tier, and the router drops a tier left
|
|
1759
|
+
with no models. A request for a dropped tier (by the classifier, a
|
|
1760
|
+
`route` override, or `for_tier`) goes to the next **higher** surviving
|
|
1761
|
+
tier, never a lower one, and to the default route when nothing is
|
|
1762
|
+
above. A dropped `default_tier` moves the same way.
|
|
1627
1763
|
- The primary completions model cannot be pruned: it is the default
|
|
1628
|
-
route
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
- A
|
|
1632
|
-
|
|
1633
|
-
**unverifiable** and kept, logged at INFO. A probe error (network, 5xx)
|
|
1634
|
-
keeps the model and logs a WARNING. Neither counts as missing.
|
|
1764
|
+
route. A missing primary raises `ValueError`; an unreachable one raises
|
|
1765
|
+
`AiProviderConfigurationError` (the engine's own error comes first when
|
|
1766
|
+
its constructor already refuses, for example a missing extra).
|
|
1767
|
+
- A missing or unreachable classifier model logs `classifier_unavailable`,
|
|
1768
|
+
and the router degrades to its default route.
|
|
1635
1769
|
|
|
1636
1770
|
Log records go to `ai_api_unified.util.ai_model_checker` with an
|
|
1637
|
-
`ai_model_check_event` field: `model_missing` (WARNING),
|
|
1638
|
-
|
|
1639
|
-
|
|
1771
|
+
`ai_model_check_event` field: `model_missing` (WARNING),
|
|
1772
|
+
`model_unreachable` (WARNING), `model_check_error` (WARNING),
|
|
1773
|
+
`model_unverifiable` (INFO), `model_available` (DEBUG); the factory adds
|
|
1774
|
+
`router_tier_pruned` and `router_disabled` (WARNING) on
|
|
1640
1775
|
`ai_api_unified.ai_factory`. `AIFactory.last_model_check_report()` returns
|
|
1641
1776
|
the results of the most recent check.
|
|
1642
1777
|
|
|
@@ -1644,10 +1779,7 @@ the results of the most recent check.
|
|
|
1644
1779
|
|
|
1645
1780
|
| Setting | Meaning |
|
|
1646
1781
|
| ------- | ------- |
|
|
1647
|
-
| `
|
|
1648
|
-
|
|
1649
|
-
A client with only a primary and no chain or router runs no check: the
|
|
1650
|
-
engine fails on its own at build.
|
|
1782
|
+
| `model_check.mode` (`models.json`) | `prune` (default): drop missing and unreachable fallbacks and tier models with a warning. `strict`: raise on any of them. `off`: skip the check. `model_check.timeout_seconds` bounds each probe (default 10). |
|
|
1651
1783
|
|
|
1652
1784
|
### Checking your own configuration
|
|
1653
1785
|
|
|
@@ -1655,8 +1787,8 @@ engine fails on its own at build.
|
|
|
1655
1787
|
poetry run pytest -m nonmock tests/test_configured_models_nonmock.py -q -s --models-config ./models.json
|
|
1656
1788
|
```
|
|
1657
1789
|
|
|
1658
|
-
prints every model the file names with its status and fails on any
|
|
1659
|
-
|
|
1790
|
+
prints every model the file names with its status and fails on any that
|
|
1791
|
+
is missing or unreachable. To watch the pruning itself:
|
|
1660
1792
|
|
|
1661
1793
|
```bash
|
|
1662
1794
|
poetry run pytest -m nonmock tests/test_model_check_nonmock.py -q -s
|
|
@@ -1683,7 +1815,7 @@ the value may also be inline JSON).
|
|
|
1683
1815
|
{
|
|
1684
1816
|
"allow": ["claude:*", "google-gemini:*", "openai-decisions:*"],
|
|
1685
1817
|
"deny": ["openai:gpt-4*", "bedrock"],
|
|
1686
|
-
"completions": {"engine": "google-gemini", "model": "gemini-2.5-flash",
|
|
1818
|
+
"completions": {"engine": "google-gemini", "model": "gemini-2.5-flash", "thinking": "medium",
|
|
1687
1819
|
"fallbacks": ["claude:claude-sonnet-5"],
|
|
1688
1820
|
"fallback_on": "unavailable,rate_limited,quota_exhausted,auth_rejected"},
|
|
1689
1821
|
"classifier": {"engine": "openai-decisions", "model": "gpt-6-luna"},
|
|
@@ -1705,19 +1837,17 @@ poetry run python scripts/migrate2json.py # writes ./models.json
|
|
|
1705
1837
|
poetry run python scripts/migrate2json.py --comment-env # and comments the moved keys out of .env
|
|
1706
1838
|
```
|
|
1707
1839
|
|
|
1708
|
-
It places each selection
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
touches secrets.
|
|
1840
|
+
It places each selection in its section, embeds a router file the old
|
|
1841
|
+
`.env` pointed at, fills structural defaults from `models_sample.json`,
|
|
1842
|
+
leaves the allow and deny lists empty, and never touches secrets.
|
|
1712
1843
|
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
without editing the file. Secrets (API keys, AWS credentials), `AWS_REGION`,
|
|
1844
|
+
The old selection keys are deprecated. One left in `.env` or the process
|
|
1845
|
+
environment is still accepted and overrides the file, so a deployment does
|
|
1846
|
+
not break, and the library logs one warning per key naming the
|
|
1847
|
+
`models.json` field and the migrator command
|
|
1848
|
+
(`ai_config_event=deprecated_selection_key`). Precedence, highest first: an
|
|
1849
|
+
explicit factory argument, a deprecated environment key, the file, the
|
|
1850
|
+
library default. Secrets (API keys, AWS credentials), `AWS_REGION`,
|
|
1721
1851
|
base-URL overrides, and `AI_MIDDLEWARE_CONFIG_PATH` stay in `.env`.
|
|
1722
1852
|
|
|
1723
1853
|
### Allow and deny lists
|
|
@@ -1738,43 +1868,43 @@ classifier, and the embeddings, image, and video selections.
|
|
|
1738
1868
|
model, raises `ValueError` naming the pattern. Runtime never calls a
|
|
1739
1869
|
denied model.
|
|
1740
1870
|
|
|
1741
|
-
###
|
|
1871
|
+
### Engine selectors
|
|
1872
|
+
|
|
1873
|
+
There is no implicit default provider. Set the engine for each capability
|
|
1874
|
+
you use in `models.json`:
|
|
1875
|
+
|
|
1876
|
+
| `models.json` field | Valid values |
|
|
1877
|
+
| ------------------- | ------------ |
|
|
1878
|
+
| `completions.engine` | `openai`, `openai-responses`, `openai-compatible`, `claude`, `google-gemini`, `bedrock`, Bedrock-routed aliases such as `nova`, `anthropic`, `llama`, `mistral`, `cohere`, `ai21`, `rerank` |
|
|
1879
|
+
| `embeddings.engine` | `openai`, `titan`, `google-gemini`, `voyage` |
|
|
1880
|
+
| `classifier.engine` | `openai-decisions`, `openai`, `openai-responses`, `openai-compatible`, `claude`, `google-gemini`, `bedrock`, `anthropic` (see Classification) |
|
|
1881
|
+
| `images.engine` | `openai`, `google-gemini`, `nova-canvas`, `bedrock`, `nova` |
|
|
1882
|
+
| `videos.engine` | `openai`, `google-gemini`, `bedrock`, `nova`, `nova-reel` |
|
|
1883
|
+
| `voice.engine` | `openai`, `google`, `azure`, `elevenlabs` |
|
|
1884
|
+
|
|
1885
|
+
### Model selections
|
|
1742
1886
|
|
|
1743
|
-
|
|
1744
|
-
|
|
1887
|
+
Every section of `models.json` is documented in
|
|
1888
|
+
[`models_sample.json`](models_sample.json). The fields, by section:
|
|
1745
1889
|
|
|
1746
|
-
|
|
|
1747
|
-
|
|
|
1748
|
-
| `
|
|
1749
|
-
| `
|
|
1750
|
-
| `
|
|
1751
|
-
| `
|
|
1752
|
-
| `
|
|
1753
|
-
| `
|
|
1890
|
+
| Section | Fields |
|
|
1891
|
+
| ------- | ------ |
|
|
1892
|
+
| `completions` | `engine`, `model`, `fallbacks` (see Model fallback), `fallback_on`, `retry_policy`, `thinking` (see Thinking levels) |
|
|
1893
|
+
| `classifier` | `engine`, `model`, `base_url`, `timeout_seconds`, `instructions` |
|
|
1894
|
+
| `router` | `tiers`, `classifier`, `default_tier`, `on_exhausted`, `fallback_on` (see Model routing) |
|
|
1895
|
+
| `model_check` | `mode` (`prune`, `strict`, `off`), `timeout_seconds` (see Configured-model check) |
|
|
1896
|
+
| `embeddings` | `engine`, `model` (`gemini-embedding-2` enables multimodal embeddings), `dimensions` (`0` keeps the provider default) |
|
|
1897
|
+
| `images` | `engine`, `model` |
|
|
1898
|
+
| `videos` | `engine`, `model`, `output_dir`, `poll_interval_seconds`, `timeout_seconds` |
|
|
1899
|
+
| `voice` | `engine`, `tts_model`, `language` |
|
|
1900
|
+
| `geo_residency` | `US`, `USA`, or `United States` normalize to US routing where supported |
|
|
1754
1901
|
|
|
1755
|
-
###
|
|
1902
|
+
### Settings that stay in `.env`
|
|
1756
1903
|
|
|
1757
1904
|
| Environment variable | Notes |
|
|
1758
1905
|
| --------------------------- | ------------------------------------------------------------------------------------------- |
|
|
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
1906
|
| `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
1907
|
| `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
1908
|
| `AI_MIDDLEWARE_CONFIG_PATH` | Optional YAML config path for observability and PII middleware |
|
|
1779
1909
|
| `ANTHROPIC_BASE_URL_OVERRIDE` | Optional API base-URL override for the `claude` engine (https required) |
|
|
1780
1910
|
| `OPENAI_BASE_URL_OVERRIDE` | Optional API base-URL override for the OpenAI engines (https required) |
|
|
@@ -1863,15 +1993,8 @@ Required:
|
|
|
1863
1993
|
|
|
1864
1994
|
- `OPENAI_API_KEY`
|
|
1865
1995
|
|
|
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`
|
|
1996
|
+
Optional: `OPENAI_BASE_URL_OVERRIDE`. Models and dimensions are selected in
|
|
1997
|
+
`models.json`.
|
|
1875
1998
|
|
|
1876
1999
|
#### Anthropic (native Claude API)
|
|
1877
2000
|
|
|
@@ -1879,9 +2002,7 @@ Required for the `claude` completions engine:
|
|
|
1879
2002
|
|
|
1880
2003
|
- `ANTHROPIC_API_KEY`
|
|
1881
2004
|
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
- `COMPLETIONS_MODEL_NAME` (defaults to `claude-opus-5`)
|
|
2005
|
+
The model is `completions.model` in `models.json` (default `claude-opus-5`).
|
|
1885
2006
|
|
|
1886
2007
|
Claude via Amazon Bedrock (the `anthropic` engine) does not use
|
|
1887
2008
|
`ANTHROPIC_API_KEY`; it authenticates with AWS credentials like the other
|
|
@@ -1899,15 +2020,8 @@ Required:
|
|
|
1899
2020
|
- `AWS_REGION`
|
|
1900
2021
|
- standard AWS credentials in environment or runtime IAM context
|
|
1901
2022
|
|
|
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`
|
|
2023
|
+
Optional: `BEDROCK_VIDEO_OUTPUT_S3_URI` (Nova Reel). Models and dimensions
|
|
2024
|
+
are selected in `models.json`.
|
|
1911
2025
|
|
|
1912
2026
|
If your AWS profile uses `aws login` (a `login_session` profile), install
|
|
1913
2027
|
`botocore[crt]` as well; botocore needs it to read those credentials.
|
|
@@ -1917,9 +2031,11 @@ If your AWS profile uses `aws login` (a `login_session` profile), install
|
|
|
1917
2031
|
AWS hosts these models itself, so requests go to AWS under your AWS account
|
|
1918
2032
|
and never reach DeepSeek, Alibaba, or Z.ai.
|
|
1919
2033
|
|
|
2034
|
+
```json
|
|
2035
|
+
"completions": {"engine": "bedrock", "model": "deepseek.v3.2"}
|
|
2036
|
+
```
|
|
2037
|
+
|
|
1920
2038
|
```dotenv
|
|
1921
|
-
COMPLETIONS_ENGINE=bedrock
|
|
1922
|
-
COMPLETIONS_MODEL_NAME=deepseek.v3.2
|
|
1923
2039
|
AWS_REGION=us-east-1
|
|
1924
2040
|
AWS_PROFILE=<your profile>
|
|
1925
2041
|
```
|
|
@@ -2001,7 +2117,7 @@ Required:
|
|
|
2001
2117
|
Optional:
|
|
2002
2118
|
|
|
2003
2119
|
- `MICROSOFT_COGNITIVE_SERVICES_ENDPOINT`
|
|
2004
|
-
- `
|
|
2120
|
+
- `voice.language` in `models.json`
|
|
2005
2121
|
|
|
2006
2122
|
#### ElevenLabs
|
|
2007
2123
|
|
|
@@ -2207,6 +2323,63 @@ capability.supports_org_id, capability.supports_org_name, capability.requirement
|
|
|
2207
2323
|
# iam:ListAccountAliases; identity falls back to id-only without it")
|
|
2208
2324
|
```
|
|
2209
2325
|
|
|
2326
|
+
#### Per-request cost
|
|
2327
|
+
|
|
2328
|
+
A request's total cost includes every call it made: classification, failed
|
|
2329
|
+
attempts, fallbacks, and escalations. One routed structured-output request
|
|
2330
|
+
can be four provider calls: the classifier, a 529 on the first tier, a
|
|
2331
|
+
fallback on the same tier, and an escalation that answers. Each call still
|
|
2332
|
+
emits its own `ai_api_call_cost` event; the request adds one total on top.
|
|
2333
|
+
|
|
2334
|
+
Every client keeps the report for the last request it ran as the outermost
|
|
2335
|
+
call:
|
|
2336
|
+
|
|
2337
|
+
```python
|
|
2338
|
+
result = client.send_structured_output(prompt, response_model=Summary)
|
|
2339
|
+
report = client.last_request_cost # AIRequestCostReport
|
|
2340
|
+
report.total_usd # Decimal, every priced call
|
|
2341
|
+
report.classifier_usd # the router's classifier call
|
|
2342
|
+
report.wasted_usd # billed answers nobody used
|
|
2343
|
+
report.billed_calls, report.failed_calls # 3 and 1 in the example above
|
|
2344
|
+
report.served_by # "claude:claude-opus-5"
|
|
2345
|
+
for item in report.calls: # AIRequestCostLineItem, in call order
|
|
2346
|
+
item.role, item.route_tier, item.outcome, item.usd_cost, item.served
|
|
2347
|
+
```
|
|
2348
|
+
|
|
2349
|
+
A failed attempt is a line item with no cost and no tokens; it still counts
|
|
2350
|
+
as a call. A failed attempt the provider billed, a structured answer that
|
|
2351
|
+
could not be parsed or validated (`StructuredOutputValidationError`) or a
|
|
2352
|
+
truncated one (`StructuredResponseTokenLimitError`), keeps the usage the
|
|
2353
|
+
exception carries, so the report prices it and counts it in `billed_calls`
|
|
2354
|
+
and `wasted_usd`; its per-call cost event carries `outcome: "error"`. A model
|
|
2355
|
+
with no registry pricing counts as billed but unpriced
|
|
2356
|
+
(`unpriced_calls`), so `total_usd` understates the request by its cost.
|
|
2357
|
+
The ledger records calls whether or not the observability middleware is on.
|
|
2358
|
+
|
|
2359
|
+
To roll several library calls into one logical request, such as a workflow
|
|
2360
|
+
step, open the scope yourself. Nested scopes join the outer one, and the
|
|
2361
|
+
clients inside keep no report of their own:
|
|
2362
|
+
|
|
2363
|
+
```python
|
|
2364
|
+
from ai_api_unified import request_cost_scope
|
|
2365
|
+
|
|
2366
|
+
with request_cost_scope("summarize-contract") as scope:
|
|
2367
|
+
router.send_structured_output(prompt, response_model=Summary)
|
|
2368
|
+
router.send_prompt("One-line title for this summary.")
|
|
2369
|
+
scope.report.total_usd # both calls, every attempt behind them
|
|
2370
|
+
```
|
|
2371
|
+
|
|
2372
|
+
When `emit_cost` is on, the outermost scope emits one `ai_api_request_cost`
|
|
2373
|
+
event on the cost topic as it closes (normally or on an exception):
|
|
2374
|
+
`request_id`, `operation`, `status`, `caller_id`, `session_id`,
|
|
2375
|
+
`workflow_id`, `served_by`, `served_by_tier`, `served_by_method`,
|
|
2376
|
+
`total_usd`, `classifier_usd`, `wasted_usd`, `billed_calls`,
|
|
2377
|
+
`failed_calls`, `unpriced_calls`, `elapsed_ms`, and `calls` (a compact list
|
|
2378
|
+
with `call_id`, `role`, `model`, `outcome`, `usd_cost`, `served`). The
|
|
2379
|
+
`request_id` is the router's `route_id` when routed, and every per-call
|
|
2380
|
+
event under the request carries it as `tag_request_id`, so the per-call
|
|
2381
|
+
events and the total join on one key.
|
|
2382
|
+
|
|
2210
2383
|
### PII Redaction
|
|
2211
2384
|
|
|
2212
2385
|
PII redaction is optional and requires one of:
|
|
@@ -2365,12 +2538,12 @@ git push origin v<version>
|
|
|
2365
2538
|
|
|
2366
2539
|
## Troubleshooting
|
|
2367
2540
|
|
|
2368
|
-
- `
|
|
2541
|
+
- An engine `must be configured explicitly` error: set that capability's `engine` in `models.json`.
|
|
2369
2542
|
- `AiProviderDependencyUnavailableError`: install the extra for the selected provider.
|
|
2370
2543
|
- 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 `
|
|
2544
|
+
- Unexpected embeddings dimensions: leave `embeddings.dimensions` at `0` unless you intentionally want a non-default size.
|
|
2372
2545
|
- 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
|
-
- `
|
|
2546
|
+
- `geo_residency: "US"` is a best-effort routing hint. Only providers that expose regional routing controls can honor it directly.
|
|
2374
2547
|
|
|
2375
2548
|
## License
|
|
2376
2549
|
|