ai_api_unified 2.35.0__tar.gz → 3.2.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-2.35.0 → ai_api_unified-3.2.0}/PKG-INFO +398 -3
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/README.md +397 -2
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/pyproject.toml +1 -1
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/__init__.py +22 -0
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/__version__.py +1 -1
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/ai_base.py +614 -1
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/ai_factory.py +679 -16
- ai_api_unified-3.2.0/src/ai_api_unified/ai_models_config.py +477 -0
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/ai_provider_registry.py +127 -0
- ai_api_unified-3.2.0/src/ai_api_unified/classification/__init__.py +6 -0
- ai_api_unified-3.2.0/src/ai_api_unified/classification/ai_openai_decisions_classifier.py +554 -0
- ai_api_unified-3.2.0/src/ai_api_unified/classification/ai_structured_output_classifier.py +438 -0
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/completions/ai_fallback_completions.py +2 -1
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/completions/ai_openai_completions.py +10 -2
- ai_api_unified-3.2.0/src/ai_api_unified/completions/ai_router_completions.py +1404 -0
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/pricing/pricing_registry.py +17 -0
- ai_api_unified-3.2.0/src/ai_api_unified/routing/__init__.py +23 -0
- ai_api_unified-3.2.0/src/ai_api_unified/routing/router_config.py +384 -0
- ai_api_unified-3.2.0/src/ai_api_unified/util/ai_model_checker.py +373 -0
- ai_api_unified-3.2.0/src/ai_api_unified/util/ai_model_probes.py +360 -0
- ai_api_unified-3.2.0/src/ai_api_unified/util/env_settings.py +168 -0
- ai_api_unified-2.35.0/src/ai_api_unified/util/env_settings.py +0 -93
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/LICENSE +0 -0
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/ai_anthropic_base.py +0 -0
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/ai_bedrock_base.py +0 -0
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/ai_completions_exceptions.py +0 -0
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/ai_google_base.py +0 -0
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/ai_openai_base.py +0 -0
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/ai_provider_exceptions.py +0 -0
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/ai_provider_loader.py +0 -0
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/completions/__init__.py +0 -0
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/completions/ai_anthropic_completions.py +0 -0
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/completions/ai_bedrock_completions.py +0 -0
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/completions/ai_google_gemini_capabilities.py +0 -0
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/completions/ai_google_gemini_completions.py +0 -0
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/completions/ai_openai_compatible_completions.py +0 -0
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/completions/ai_openai_responses_completions.py +0 -0
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/embeddings/__init__.py +0 -0
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/embeddings/ai_google_gemini_embeddings.py +0 -0
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/embeddings/ai_openai_embeddings.py +0 -0
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/embeddings/ai_titan_embeddings.py +0 -0
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/embeddings/ai_voyage_embeddings.py +0 -0
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/images/__init__.py +0 -0
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/images/ai_bedrock_images.py +0 -0
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/images/ai_google_gemini_images.py +0 -0
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/images/ai_openai_images.py +0 -0
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/middleware/__init__.py +0 -0
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/middleware/impl/__init__.py +0 -0
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/middleware/impl/_presidio_redactor.py +0 -0
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/middleware/impl/base_redactor.py +0 -0
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/middleware/impl/custom_recognizer_factory.py +0 -0
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/middleware/impl/noop_redactor.py +0 -0
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/middleware/impl/presidio_log_control.py +0 -0
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/middleware/middleware.py +0 -0
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/middleware/middleware_config.py +0 -0
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/middleware/observability.py +0 -0
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/middleware/observability_runtime.py +0 -0
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/middleware/pii_redactor.py +0 -0
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/middleware/redaction_exceptions.py +0 -0
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/pricing/__init__.py +0 -0
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/pricing/model_pricing.py +0 -0
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/util/__init__.py +0 -0
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/util/_lazy_pydub.py +0 -0
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/util/package_metadata_validation.py +0 -0
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/util/utils.py +0 -0
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/videos/__init__.py +0 -0
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/videos/ai_bedrock_videos.py +0 -0
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/videos/ai_google_gemini_videos.py +0 -0
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/videos/ai_openai_videos.py +0 -0
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/videos/frame_helpers.py +0 -0
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/voice/__init__.py +0 -0
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/voice/ai_voice_azure.py +0 -0
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/voice/ai_voice_base.py +0 -0
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/voice/ai_voice_elevenlabs.py +0 -0
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/voice/ai_voice_factory.py +0 -0
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/voice/ai_voice_google.py +0 -0
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/voice/ai_voice_google_gemini_voices.py +0 -0
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.0}/src/ai_api_unified/voice/ai_voice_openai.py +0 -0
- {ai_api_unified-2.35.0 → ai_api_unified-3.2.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: 2.
|
|
3
|
+
Version: 3.2.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 2.
|
|
80
|
+
# ai-api-unified 3.2.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/)
|
|
@@ -190,6 +190,7 @@ The public entry points are the stable base interfaces and factories:
|
|
|
190
190
|
| ----------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
|
|
191
191
|
| Completions | `AIBaseCompletions` | `openai`, `openai-responses`, `openai-compatible`, `claude`, `google-gemini`, Bedrock-routed aliases such as `nova`, `anthropic`, `llama`, `mistral`, `cohere`, `ai21`, `rerank` | `openai`, `anthropic`, `google_gemini`, `bedrock` |
|
|
192
192
|
| Embeddings | `AIBaseEmbeddings` | `openai`, `titan`, `google-gemini`, `voyage` | `openai`, `bedrock`, `google_gemini`, `voyage` |
|
|
193
|
+
| Classification | `AIBaseClassifier` | `openai-decisions`, and `openai`, `openai-responses`, `openai-compatible`, `claude`, `google-gemini`, `bedrock`, `anthropic` through structured output | `openai`, `anthropic`, `google_gemini`, `bedrock` |
|
|
193
194
|
| Images | `AIBaseImages` | `openai`, `google-gemini`, `nova-canvas` and Bedrock image aliases | `openai`, `google_gemini`, `bedrock` |
|
|
194
195
|
| Videos | `AIBaseVideos` | `openai`, `google-gemini`, `nova-reel` and Bedrock video aliases | `openai`, `google_gemini`, `bedrock` |
|
|
195
196
|
| Voice TTS | `AIVoiceBase` | `openai`, `google`, `azure`, `elevenlabs` | `openai`, `google_gemini`, `azure_tts`, `elevenlabs` |
|
|
@@ -900,6 +901,22 @@ Anthropic serves none.
|
|
|
900
901
|
`AiProviderRequestError` carrying `status_code`, matching the completions
|
|
901
902
|
clients.
|
|
902
903
|
|
|
904
|
+
|
|
905
|
+
### Classification
|
|
906
|
+
|
|
907
|
+
```python
|
|
908
|
+
from ai_api_unified import AIFactory, AIClassificationChoice
|
|
909
|
+
|
|
910
|
+
client = AIFactory.get_ai_classifier_client(classifier_engine="claude")
|
|
911
|
+
result = client.classify_choice(
|
|
912
|
+
"Summarize this ticket in one line.",
|
|
913
|
+
[AIClassificationChoice(value="easy"), AIClassificationChoice(value="hard")],
|
|
914
|
+
)
|
|
915
|
+
print(result.choice) # "easy"
|
|
916
|
+
```
|
|
917
|
+
|
|
918
|
+
See the Classification section for the question types and the engines that
|
|
919
|
+
support them.
|
|
903
920
|
### Multimodal Embeddings
|
|
904
921
|
|
|
905
922
|
Every embeddings client exposes a `capabilities` descriptor stating which input
|
|
@@ -1108,6 +1125,96 @@ than ignoring them.
|
|
|
1108
1125
|
A blank retry-policy setting (`COMPLETIONS_RETRY_POLICY=`) is treated as
|
|
1109
1126
|
unconfigured and falls back to `default` across every OpenAI-backed capability.
|
|
1110
1127
|
|
|
1128
|
+
## Classification
|
|
1129
|
+
|
|
1130
|
+
A classifier answers one narrow question about a piece of text: which of
|
|
1131
|
+
your options fits it, whether a condition holds, or where it sits on a
|
|
1132
|
+
scale. It stands beside completions and embeddings as its own capability,
|
|
1133
|
+
so you can use it for routing, triage, or moderation without writing a
|
|
1134
|
+
prompt and parsing an answer. The model router (a later section) is one consumer.
|
|
1135
|
+
|
|
1136
|
+
### Quickstart
|
|
1137
|
+
|
|
1138
|
+
```python
|
|
1139
|
+
from ai_api_unified import AIFactory, AIClassificationChoice
|
|
1140
|
+
|
|
1141
|
+
client = AIFactory.get_ai_classifier_client() # CLASSIFIER_ENGINE from env
|
|
1142
|
+
result = client.classify_choice(
|
|
1143
|
+
"My invoice shows two charges for one order.",
|
|
1144
|
+
[
|
|
1145
|
+
AIClassificationChoice(value="billing", description="Payments, invoices, refunds."),
|
|
1146
|
+
AIClassificationChoice(value="technical", description="Errors using the product."),
|
|
1147
|
+
AIClassificationChoice(value="other", description="Anything else."),
|
|
1148
|
+
],
|
|
1149
|
+
instructions="Which team should handle this message?",
|
|
1150
|
+
)
|
|
1151
|
+
print(result.choice, result.confidence, result.usage.input_tokens)
|
|
1152
|
+
```
|
|
1153
|
+
|
|
1154
|
+
Three question types share one result model, `AIClassificationResult`:
|
|
1155
|
+
|
|
1156
|
+
| Method | Question | Result fields |
|
|
1157
|
+
| ------ | -------- | ------------- |
|
|
1158
|
+
| `classify_choice(text, choices)` | Which option fits? | `choice`, `confidence`, `probabilities` |
|
|
1159
|
+
| `classify_predicate(text, instructions=...)` | Does the condition hold? | `probability` |
|
|
1160
|
+
| `classify_score(text, levels)` | Where on the scale, lowest level first? | `score` in [0, 1], `choice` (the level label) |
|
|
1161
|
+
|
|
1162
|
+
Every result also carries `refused` (the model declined), `usage`,
|
|
1163
|
+
`token_source`, `provider_engine`, `model_name`, and `latency_ms`. Async
|
|
1164
|
+
twins (`aclassify_choice` and so on) exist where `capabilities.supports_async`
|
|
1165
|
+
is True. `capabilities` reports which question types the engine answers,
|
|
1166
|
+
`max_choices`, `max_input_chars`, and `pricing`.
|
|
1167
|
+
|
|
1168
|
+
Before the call, the text goes through the PII middleware when it is
|
|
1169
|
+
enabled, then is cut to `capabilities.max_input_chars` (6,000 by default)
|
|
1170
|
+
keeping its head and tail: a label rarely needs more, and the provider
|
|
1171
|
+
bills every input token.
|
|
1172
|
+
|
|
1173
|
+
### Models that can classify
|
|
1174
|
+
|
|
1175
|
+
| Engine | How it classifies | Models |
|
|
1176
|
+
| ------ | ----------------- | ------ |
|
|
1177
|
+
| `openai-decisions` | OpenAI's Decisions API (`POST /v1/decisions`). Native choice, predicate, and score answers with probabilities. About ten times faster than a chat completion. Bills input tokens only, at $0.10 per 1M. | `gpt-6-luna` (public beta) |
|
|
1178
|
+
| `openai`, `openai-responses`, `openai-compatible` | Structured output (a one-field enum schema) | Every model the engine lists |
|
|
1179
|
+
| `claude` | Structured output | Every model the engine lists |
|
|
1180
|
+
| `google-gemini` | Structured output | Every model the engine lists |
|
|
1181
|
+
| `bedrock`, `anthropic` | Structured output | Models whose capabilities include `supports_structured_output` (Claude, DeepSeek, Qwen, GLM); Nova cannot classify |
|
|
1182
|
+
|
|
1183
|
+
Through structured output, a predicate answer is a hard yes or no
|
|
1184
|
+
(`probability` 1.0 or 0.0) and a score is the picked level's position on the
|
|
1185
|
+
scale; engines with native classification report real probabilities.
|
|
1186
|
+
Picking an engine without structured output fails at build with
|
|
1187
|
+
`AiProviderCapabilityUnsupportedError`.
|
|
1188
|
+
|
|
1189
|
+
### Cost and metering
|
|
1190
|
+
|
|
1191
|
+
Every classification call runs inside the same observability lifecycle as
|
|
1192
|
+
completions, so with observability on it emits the usual input, output,
|
|
1193
|
+
error, and cost events, with `operation` set to `classify_choice`,
|
|
1194
|
+
`classify_predicate`, or `classify_score`. The Decisions engine prices
|
|
1195
|
+
under the registry key `decisions/gpt-6-luna`, since its rates differ from
|
|
1196
|
+
the model's chat rates; when a response carries no `usage` block the engine
|
|
1197
|
+
estimates input tokens from the request text and reports
|
|
1198
|
+
`token_source="estimated"`. The completions engine a
|
|
1199
|
+
structured-output classifier wraps meters the call, and that call carries
|
|
1200
|
+
the tag `classifier_role` (`choice`, `predicate`, or `score`) so a dashboard can
|
|
1201
|
+
split classifier spend from the engine's other traffic. `token_source` on
|
|
1202
|
+
the result says where the counts came from: `provider`, `estimated`, or
|
|
1203
|
+
`none`. A call that could not be metered logs a WARNING with
|
|
1204
|
+
`ai_classifier_event=classifier_unmetered` on the `ai_api_unified.ai_base`
|
|
1205
|
+
logger; it is never a silent zero.
|
|
1206
|
+
|
|
1207
|
+
### Configuration
|
|
1208
|
+
|
|
1209
|
+
| Setting | Meaning |
|
|
1210
|
+
| ------- | ------- |
|
|
1211
|
+
| `CLASSIFIER_ENGINE` | Engine token. When unset, `openai-decisions` is used if `OPENAI_API_KEY` is set; otherwise the factory raises. |
|
|
1212
|
+
| `CLASSIFIER_MODEL_NAME` | Model override; blank falls back to `COMPLETIONS_MODEL_NAME` for structured-output engines, then the engine's default (`gpt-6-luna` for `openai-decisions`). |
|
|
1213
|
+
| `AIFactory.get_ai_classifier_client(classifier_engine=, model_name=, base_url=)` | Arguments override the settings. |
|
|
1214
|
+
|
|
1215
|
+
Exports: `AIBaseClassifier`, `AIClassifierCapabilitiesBase`,
|
|
1216
|
+
`AIClassificationChoice`, `AIClassificationLevel`, `AIClassificationResult`.
|
|
1217
|
+
|
|
1111
1218
|
## Model fallback
|
|
1112
1219
|
|
|
1113
1220
|
A fallback chain keeps completions flowing when a model cannot serve a
|
|
@@ -1358,16 +1465,289 @@ poetry run pytest -m nonmock tests/test_model_fallback_nonmock.py -q
|
|
|
1358
1465
|
It needs `ANTHROPIC_API_KEY` and `OPENAI_API_KEY` in `.env` and skips without
|
|
1359
1466
|
them.
|
|
1360
1467
|
|
|
1468
|
+
## Model routing
|
|
1469
|
+
|
|
1470
|
+
A router sends each request to a tier of models that matches how demanding
|
|
1471
|
+
the request is. A cheap classifier reads the latest user message, places it
|
|
1472
|
+
in one of your tiers (easy, medium, hard, or names of your own), and the
|
|
1473
|
+
request runs on that tier's model chain. A trivial question stops costing
|
|
1474
|
+
flagship rates; a hard one still gets the strongest model.
|
|
1475
|
+
|
|
1476
|
+
### Why it exists
|
|
1477
|
+
|
|
1478
|
+
One configured model serves every prompt, whatever its difficulty. Routing
|
|
1479
|
+
by complexity lowers cost and latency on the easy majority without giving
|
|
1480
|
+
up quality on the hard minority, and it does so in the library, so an
|
|
1481
|
+
application changes nothing but configuration.
|
|
1482
|
+
|
|
1483
|
+
### Setting up a router
|
|
1484
|
+
|
|
1485
|
+
Write the tiers as JSON, ascending capability. Each tier lists an ordered
|
|
1486
|
+
`engine:model` chain: the first entry is the tier's primary and the rest
|
|
1487
|
+
are its fallback chain. Tiers named `easy`, `medium`, and `hard` have
|
|
1488
|
+
built-in descriptions; any other name needs a `description` the classifier
|
|
1489
|
+
can read.
|
|
1490
|
+
|
|
1491
|
+
```json
|
|
1492
|
+
{
|
|
1493
|
+
"tiers": [
|
|
1494
|
+
{"name": "easy", "models": ["openai:gpt-4.1-mini", "google-gemini:gemini-2.5-flash-lite"]},
|
|
1495
|
+
{"name": "medium", "models": ["claude:claude-sonnet-5", "openai:gpt-5.6-luna"]},
|
|
1496
|
+
{"name": "hard", "description": "Multi-step reasoning, long synthesis, cross-file code, or anything where a wrong answer is costly.",
|
|
1497
|
+
"models": ["claude:claude-opus-5-5"]}
|
|
1498
|
+
],
|
|
1499
|
+
"classifier": {"engine": "openai-decisions", "model": "gpt-6-luna", "timeout_seconds": 5},
|
|
1500
|
+
"default_tier": "medium",
|
|
1501
|
+
"on_exhausted": "escalate"
|
|
1502
|
+
}
|
|
1503
|
+
```
|
|
1504
|
+
|
|
1505
|
+
Point `COMPLETIONS_ROUTER_CONFIG` at the file (or set it to the JSON
|
|
1506
|
+
itself), keep `COMPLETIONS_ENGINE` and `COMPLETIONS_MODEL_NAME` as they
|
|
1507
|
+
are, and call the factory as usual:
|
|
1508
|
+
|
|
1509
|
+
```python
|
|
1510
|
+
client = AIFactory.get_ai_completions_client() # an AiRouterCompletions
|
|
1511
|
+
print(client.send_prompt("What is 2 + 2?")) # routed to the easy tier
|
|
1512
|
+
print(client.last_tier, client.last_decision)
|
|
1513
|
+
```
|
|
1514
|
+
|
|
1515
|
+
In code, pass `router=AIRouterConfig(...)` instead. The configured
|
|
1516
|
+
completions LLM, with its `COMPLETIONS_FALLBACKS` chain, is the **default
|
|
1517
|
+
route**: where calls go when no classifier is available, when the
|
|
1518
|
+
classifier fails, or when `default_tier` is unset.
|
|
1519
|
+
|
|
1520
|
+
### The classifier
|
|
1521
|
+
|
|
1522
|
+
The router uses a classification engine (see Classification). Resolution
|
|
1523
|
+
order: `COMPLETIONS_ROUTER_CLASSIFIER` (`engine:model`), then the config's
|
|
1524
|
+
`classifier`, then the classification factory's own resolution
|
|
1525
|
+
(`CLASSIFIER_ENGINE`, then `openai-decisions` when `OPENAI_API_KEY` is
|
|
1526
|
+
set). Set either to `none` to route without a classifier; if the
|
|
1527
|
+
classifier cannot be built, the router logs `classifier_unavailable` once
|
|
1528
|
+
and degrades to the default route, and never raises at startup.
|
|
1529
|
+
|
|
1530
|
+
The classifier reads the latest user message only, cut to the engine's
|
|
1531
|
+
`max_input_chars`. The system prompt is left out: it is long, constant,
|
|
1532
|
+
and carries no complexity signal. The tier descriptions carry the policy,
|
|
1533
|
+
and `classifier.instructions` prepends your own guidance.
|
|
1534
|
+
|
|
1535
|
+
### What happens on a request
|
|
1536
|
+
|
|
1537
|
+
1. A per-call `route` option or `for_tier()` pins the tier (method
|
|
1538
|
+
`override`).
|
|
1539
|
+
2. A history that already holds engine-shaped entries (tool turns) skips
|
|
1540
|
+
the classifier and stays in a tier whose engines share that family,
|
|
1541
|
+
preferring the tier that served the previous turn (method `pinned`).
|
|
1542
|
+
3. Otherwise the classifier names a tier (method `classifier`). No
|
|
1543
|
+
classifier, an error, a refusal, or an unknown tier name routes to
|
|
1544
|
+
`default_tier` or the default route (method `default`) and logs
|
|
1545
|
+
`classifier_failed`.
|
|
1546
|
+
4. The call runs on the tier's chain, which fails over inside the tier as
|
|
1547
|
+
any chain does.
|
|
1548
|
+
5. If the chain runs out of candidates on a trigger reason
|
|
1549
|
+
(`fallback_on`), the router escalates to the next tier up and logs
|
|
1550
|
+
`tier_exhausted`; with `on_exhausted: raise`, on a non-trigger error, or
|
|
1551
|
+
on the top tier, the error propagates; if a tier's primary cannot be
|
|
1552
|
+
built, the router logs `tier_unbuildable` once and skips the tier the
|
|
1553
|
+
same way.
|
|
1554
|
+
|
|
1555
|
+
### Seeing where a request went
|
|
1556
|
+
|
|
1557
|
+
Every call logs a `routed` record at INFO before it runs, on the logger
|
|
1558
|
+
`ai_api_unified.completions.ai_router_completions`, with an
|
|
1559
|
+
`ai_router_event` field so a log processor can count and alert without
|
|
1560
|
+
parsing the message:
|
|
1561
|
+
|
|
1562
|
+
| Level | `ai_router_event` | When |
|
|
1563
|
+
| ----- | ----------------- | ---- |
|
|
1564
|
+
| INFO | `routed` | Every call. Fields: `route_id`, `tier`, `tier_index`, `method`, `operation`, `classifier_engine`, `classifier_model`, `confidence`, `probabilities`, `classifier_latency_ms`, `classifier_input_tokens`, `classifier_token_source`, `classifier_usd_cost`, `caller_id`, `session_id`, `workflow_id`. |
|
|
1565
|
+
| WARNING | `classifier_unavailable` | Once at build: no classifier, with `reason` and `routed_to`. |
|
|
1566
|
+
| WARNING | `classifier_failed` | The classifier raised, refused, or named no tier; the call went to `routed_to`. Fields: `error`, `fallback_reason`, `latency_ms`. |
|
|
1567
|
+
| ERROR | `tier_exhausted` | A tier's chain ran out of candidates on a trigger reason; `escalate_to` names the next tier. |
|
|
1568
|
+
| ERROR | `tier_unbuildable` | A tier's primary could not be built; the tier is out for this process. |
|
|
1569
|
+
|
|
1570
|
+
The router also pushes `route_id`, `route_tier`, and `route_method` onto
|
|
1571
|
+
the observability context around the downstream call, so the engine's own
|
|
1572
|
+
input, output, and cost events carry them as `tag_route_tier` and so on:
|
|
1573
|
+
cost by tier needs no extra work. The classifier call itself carries
|
|
1574
|
+
`tag_router_role=classifier`.
|
|
1575
|
+
|
|
1576
|
+
### Per-call control
|
|
1577
|
+
|
|
1578
|
+
- `provider_options={"route": "hard"}` pins one structured-output or
|
|
1579
|
+
conversation call to a tier; `"default"` pins it to the default route.
|
|
1580
|
+
The router removes the key before options reach the engine.
|
|
1581
|
+
- `client.for_tier("hard")` returns that tier's chain client, for
|
|
1582
|
+
`send_prompt` and streaming, which take no `provider_options`.
|
|
1583
|
+
- `client.last_tier`, `client.last_decision`, and `client.last_route_client`
|
|
1584
|
+
report the most recent call.
|
|
1585
|
+
|
|
1586
|
+
### Configuration reference
|
|
1587
|
+
|
|
1588
|
+
| Setting | Meaning |
|
|
1589
|
+
| ------- | ------- |
|
|
1590
|
+
| `COMPLETIONS_ROUTER_CONFIG` | Path to a JSON file, or inline JSON, with `tiers`, optional `classifier`, `default_tier`, `on_exhausted`, `fallback_on`. |
|
|
1591
|
+
| `COMPLETIONS_ROUTER_CLASSIFIER` | `engine:model` for the router's classifier, or `none`. Overrides the config's `classifier`. |
|
|
1592
|
+
| `AIFactory.get_ai_completions_client(router=AIRouterConfig(...))` | Code-first configuration; overrides the setting. |
|
|
1593
|
+
|
|
1594
|
+
Exports: `AiRouterCompletions`, `AIRouterConfig`, `AIRouterTier`,
|
|
1595
|
+
`AIRouterClassifierConfig`, `load_router_config`.
|
|
1596
|
+
|
|
1597
|
+
## Configured-model check
|
|
1598
|
+
|
|
1599
|
+
A fallback chain or a router names models the process expects to call
|
|
1600
|
+
later. A typo, a retired id, or a model not offered in your region would
|
|
1601
|
+
otherwise surface only when that model is finally tried, in the middle of
|
|
1602
|
+
a failover. So whenever you configure a chain or a router, the factory
|
|
1603
|
+
verifies every configured model before it builds the client: the
|
|
1604
|
+
completions LLM, each chain candidate, each tier candidate, and the
|
|
1605
|
+
router's classifier.
|
|
1606
|
+
|
|
1607
|
+
### What it does
|
|
1608
|
+
|
|
1609
|
+
- One free metadata call per model, never a completion: OpenAI
|
|
1610
|
+
`models.retrieve`, Anthropic `models.retrieve`, Gemini `models.get` on
|
|
1611
|
+
the raw id, Bedrock `get_foundation_model` (or `get_inference_profile`
|
|
1612
|
+
for `us.`, `eu.`, `apac.`, and `global.` ids) in `AWS_REGION`, an
|
|
1613
|
+
OpenAI-compatible server's `/models` list, and the Decisions API's fixed
|
|
1614
|
+
model set.
|
|
1615
|
+
- Calls run in parallel, and the checker probes each `(engine, model,
|
|
1616
|
+
base_url)` once per process however many clients, chains, or tiers name
|
|
1617
|
+
it. Startup
|
|
1618
|
+
pays one round trip, and later factory calls pay nothing.
|
|
1619
|
+
- The factory prunes a model the provider reports **missing** from the
|
|
1620
|
+
effective configuration with a WARNING: `configured model openai:gpt-nope
|
|
1621
|
+
is not available and will be ignored`. A chain drops the candidate; a
|
|
1622
|
+
router tier drops the candidate, and the router drops a tier left with no
|
|
1623
|
+
models. A request for a dropped tier (by the classifier, a `route`
|
|
1624
|
+
override, or `for_tier`) goes to the next **higher** surviving tier, never
|
|
1625
|
+
a lower one, and to the default route when nothing is above. A dropped
|
|
1626
|
+
`default_tier` moves the same way.
|
|
1627
|
+
- The primary completions model cannot be pruned: it is the default
|
|
1628
|
+
route, so when it is missing the factory raises.
|
|
1629
|
+
- A missing classifier model logs `classifier_unavailable`, and the router
|
|
1630
|
+
degrades to its default route.
|
|
1631
|
+
- A model the probe cannot ask about (no credentials, no extra installed,
|
|
1632
|
+
credentials rejected, a server with no model catalog, Vertex auth) is
|
|
1633
|
+
**unverifiable** and kept, logged at INFO. A probe error (network, 5xx)
|
|
1634
|
+
keeps the model and logs a WARNING. Neither counts as missing.
|
|
1635
|
+
|
|
1636
|
+
Log records go to `ai_api_unified.util.ai_model_checker` with an
|
|
1637
|
+
`ai_model_check_event` field: `model_missing` (WARNING), `model_check_error`
|
|
1638
|
+
(WARNING), `model_unverifiable` (INFO), `model_available` (DEBUG); the
|
|
1639
|
+
factory adds `router_tier_pruned` and `router_disabled` (WARNING) on
|
|
1640
|
+
`ai_api_unified.ai_factory`. `AIFactory.last_model_check_report()` returns
|
|
1641
|
+
the results of the most recent check.
|
|
1642
|
+
|
|
1643
|
+
### Configuration
|
|
1644
|
+
|
|
1645
|
+
| Setting | Meaning |
|
|
1646
|
+
| ------- | ------- |
|
|
1647
|
+
| `AI_MODEL_CHECK` | `prune` (default): drop missing models with a warning. `strict`: raise on any missing model. `off`: skip the check. |
|
|
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.
|
|
1651
|
+
|
|
1652
|
+
### Checking your own configuration
|
|
1653
|
+
|
|
1654
|
+
```bash
|
|
1655
|
+
poetry run pytest -m nonmock tests/test_configured_models_nonmock.py -q -s --models-config ./models.json
|
|
1656
|
+
```
|
|
1657
|
+
|
|
1658
|
+
prints every model the file names with its status and fails on any the
|
|
1659
|
+
provider reports missing. To watch the pruning itself:
|
|
1660
|
+
|
|
1661
|
+
```bash
|
|
1662
|
+
poetry run pytest -m nonmock tests/test_model_check_nonmock.py -q -s
|
|
1663
|
+
```
|
|
1664
|
+
|
|
1665
|
+
reads `tests/fixtures/models_missing_test.json`, whose chain and easy tier
|
|
1666
|
+
each list an id no provider serves, and proves both are reported missing
|
|
1667
|
+
and pruned with the warning while the configuration still builds; with
|
|
1668
|
+
`--models-config ./models.json` it does the same against your file,
|
|
1669
|
+
injecting a bogus id in memory.
|
|
1670
|
+
|
|
1361
1671
|
## Configuration
|
|
1362
1672
|
|
|
1673
|
+
### models.json: one file for model selections
|
|
1674
|
+
|
|
1675
|
+
Engines, models, the fallback chain, the router, the classifier, the
|
|
1676
|
+
configured-model check, and the allow/deny policy live in one file,
|
|
1677
|
+
`models.json` (or `.yaml`). Copy `models_sample.json`, which sets every
|
|
1678
|
+
section, and point `AI_MODELS_CONFIG` at it (or leave it unset: the library
|
|
1679
|
+
picks up `./models.json` when it exists; a blank value turns the file off;
|
|
1680
|
+
the value may also be inline JSON).
|
|
1681
|
+
|
|
1682
|
+
```json
|
|
1683
|
+
{
|
|
1684
|
+
"allow": ["claude:*", "google-gemini:*", "openai-decisions:*"],
|
|
1685
|
+
"deny": ["openai:gpt-4*", "bedrock"],
|
|
1686
|
+
"completions": {"engine": "google-gemini", "model": "gemini-2.5-flash",
|
|
1687
|
+
"fallbacks": ["claude:claude-sonnet-5"],
|
|
1688
|
+
"fallback_on": "unavailable,rate_limited,quota_exhausted,auth_rejected"},
|
|
1689
|
+
"classifier": {"engine": "openai-decisions", "model": "gpt-6-luna"},
|
|
1690
|
+
"router": {"tiers": [{"name": "easy", "models": ["google-gemini:gemini-2.5-flash-lite"]},
|
|
1691
|
+
{"name": "hard", "models": ["claude:claude-opus-5-5"]}],
|
|
1692
|
+
"default_tier": "easy"},
|
|
1693
|
+
"model_check": {"mode": "prune", "timeout_seconds": 10},
|
|
1694
|
+
"embeddings": {"engine": "google-gemini", "model": "gemini-embedding-001"},
|
|
1695
|
+
"images": {"engine": "google-gemini", "model": "gemini-3.1-flash-image"},
|
|
1696
|
+
"videos": {"engine": "google-gemini", "model": "veo-3.1-lite-generate-preview"},
|
|
1697
|
+
"voice": {"engine": "google", "tts_model": "gemini-2.5-pro-tts", "language": "en-US"}
|
|
1698
|
+
}
|
|
1699
|
+
```
|
|
1700
|
+
|
|
1701
|
+
To convert an existing `.env`, run
|
|
1702
|
+
|
|
1703
|
+
```bash
|
|
1704
|
+
poetry run python scripts/migrate2json.py # writes ./models.json
|
|
1705
|
+
poetry run python scripts/migrate2json.py --comment-env # and comments the moved keys out of .env
|
|
1706
|
+
```
|
|
1707
|
+
|
|
1708
|
+
It places each selection key in its section, embeds the router file named
|
|
1709
|
+
by `COMPLETIONS_ROUTER_CONFIG`, fills structural defaults from
|
|
1710
|
+
`models_sample.json`, leaves the allow and deny lists empty, and never
|
|
1711
|
+
touches secrets.
|
|
1712
|
+
|
|
1713
|
+
Each section stands in for the `.env` keys of the same capability
|
|
1714
|
+
(`completions.model` for `COMPLETIONS_MODEL_NAME`, `embeddings.engine` for
|
|
1715
|
+
`EMBEDDING_ENGINE`, `router` for `COMPLETIONS_ROUTER_CONFIG`, and so on),
|
|
1716
|
+
so nothing else in the library changes. Precedence, highest first: an
|
|
1717
|
+
explicit factory argument, a process environment variable, a `.env` value,
|
|
1718
|
+
the file, the library default. Every key in the tables below still works
|
|
1719
|
+
and overrides the file when set, which lets a deployment pin one value
|
|
1720
|
+
without editing the file. Secrets (API keys, AWS credentials), `AWS_REGION`,
|
|
1721
|
+
base-URL overrides, and `AI_MIDDLEWARE_CONFIG_PATH` stay in `.env`.
|
|
1722
|
+
|
|
1723
|
+
### Allow and deny lists
|
|
1724
|
+
|
|
1725
|
+
`allow` and `deny` hold `engine:model` globs: `claude:*`, `*:gpt-4*`,
|
|
1726
|
+
`openai-decisions:gpt-6-luna`; a bare engine token (`bedrock`) means
|
|
1727
|
+
`bedrock:*`. Deny wins over allow, and an empty allow list allows every
|
|
1728
|
+
model. The policy applies wherever a model enters: the completions LLM,
|
|
1729
|
+
each fallback candidate, each router tier candidate, the router's
|
|
1730
|
+
classifier, and the embeddings, image, and video selections.
|
|
1731
|
+
|
|
1732
|
+
- The factory prunes a denied fallback, tier, or classifier model the way
|
|
1733
|
+
it prunes a missing one (see Configured-model check) and warns
|
|
1734
|
+
`configured fallback model openai:gpt-4o is denied by policy pattern 'openai:gpt-4*' and will be ignored`
|
|
1735
|
+
(`ai_model_policy_event=model_denied`). A tier left with no models
|
|
1736
|
+
routes to the next higher surviving tier.
|
|
1737
|
+
- A denied primary, or an explicit `model_name=` argument for a denied
|
|
1738
|
+
model, raises `ValueError` naming the pattern. Runtime never calls a
|
|
1739
|
+
denied model.
|
|
1740
|
+
|
|
1363
1741
|
### Required Engine Selectors
|
|
1364
1742
|
|
|
1365
|
-
There is no implicit default provider. Set the selector for each capability
|
|
1743
|
+
There is no implicit default provider. Set the selector for each capability
|
|
1744
|
+
you use, in `models.json` or as a variable:
|
|
1366
1745
|
|
|
1367
1746
|
| Environment variable | Valid values |
|
|
1368
1747
|
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
|
|
1369
1748
|
| `COMPLETIONS_ENGINE` | `openai`, `openai-responses`, `openai-compatible`, `claude`, `google-gemini`, `bedrock`, Bedrock-routed aliases such as `nova`, `anthropic`, `llama`, `mistral`, `cohere`, `ai21`, `rerank` |
|
|
1370
1749
|
| `EMBEDDING_ENGINE` | `openai`, `titan`, `google-gemini` |
|
|
1750
|
+
| `CLASSIFIER_ENGINE` | `openai-decisions`, `openai`, `openai-responses`, `openai-compatible`, `claude`, `google-gemini`, `bedrock`, `anthropic` (see Classification) |
|
|
1371
1751
|
| `IMAGE_ENGINE` | `openai`, `google-gemini`, `nova-canvas`, `bedrock`, `nova` |
|
|
1372
1752
|
| `VIDEO_ENGINE` | `openai`, `google-gemini`, `bedrock`, `nova`, `nova-reel` |
|
|
1373
1753
|
| `AI_VOICE_ENGINE` | `openai`, `google`, `azure`, `elevenlabs` |
|
|
@@ -1379,7 +1759,13 @@ There is no implicit default provider. Set the selector for each capability you
|
|
|
1379
1759
|
| `COMPLETIONS_MODEL_NAME` | Optional completions model override |
|
|
1380
1760
|
| `COMPLETIONS_FALLBACKS` | Optional ordered `engine:model` pairs tried after the primary fails (see Model fallback) |
|
|
1381
1761
|
| `COMPLETIONS_FALLBACK_ON` | Optional comma-separated `AiFallbackReason` values that trigger a fallback; default `unavailable,rate_limited,quota_exhausted,auth_rejected` |
|
|
1762
|
+
| `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) |
|
|
1382
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 |
|
|
1383
1769
|
| `IMAGE_MODEL_NAME` | Optional image model override |
|
|
1384
1770
|
| `VIDEO_MODEL_NAME` | Optional video model override |
|
|
1385
1771
|
| `VIDEO_OUTPUT_DIR` | Optional local output directory for materialized video artifacts |
|
|
@@ -1927,6 +2313,15 @@ Live provider tests:
|
|
|
1927
2313
|
poetry run pytest -m nonmock -s -vv
|
|
1928
2314
|
```
|
|
1929
2315
|
|
|
2316
|
+
Tests never read your own `models.json`: the suite blanks `AI_MODELS_CONFIG`.
|
|
2317
|
+
Live tests that need a models config read `tests/fixtures/models_test.json`,
|
|
2318
|
+
whose hard tier lists a model its own deny list rejects so the policy tests
|
|
2319
|
+
prove the deny list bites. To run them against any file:
|
|
2320
|
+
|
|
2321
|
+
```bash
|
|
2322
|
+
poetry run pytest -m nonmock tests/test_model_policy_nonmock.py tests/test_configured_models_nonmock.py -q -s --models-config ./models.json
|
|
2323
|
+
```
|
|
2324
|
+
|
|
1930
2325
|
Notes for live tests:
|
|
1931
2326
|
|
|
1932
2327
|
- install the matching provider extras first
|