ai-lib-python 0.7.1__tar.gz → 0.8.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/PKG-INFO +12 -1
  2. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/README.md +8 -0
  3. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/pyproject.toml +10 -2
  4. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/__init__.py +1 -1
  5. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/batch/collector.py +4 -1
  6. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/cache/manager.py +1 -0
  7. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/client/__init__.py +2 -2
  8. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/client/builder.py +33 -0
  9. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/client/cancel.py +7 -3
  10. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/client/core.py +97 -14
  11. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/computer_use/__init__.py +0 -1
  12. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/drivers/anthropic.py +10 -0
  13. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/drivers/gemini.py +11 -1
  14. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/drivers/openai.py +16 -12
  15. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/embeddings/client.py +9 -5
  16. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/guardrails/__init__.py +3 -3
  17. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/guardrails/base.py +10 -10
  18. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/guardrails/filters.py +28 -50
  19. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/guardrails/validators.py +12 -12
  20. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/multimodal/__init__.py +5 -0
  21. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/pipeline/accumulate.py +2 -2
  22. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/pipeline/base.py +3 -3
  23. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/pipeline/event_map.py +1 -1
  24. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/pipeline/fan_out.py +3 -3
  25. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/pipeline/select.py +1 -1
  26. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/protocol/loader.py +18 -6
  27. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/protocol/manifest.py +3 -1
  28. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/protocol/v2/capabilities.py +27 -1
  29. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/protocol/v2/manifest.py +45 -9
  30. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/registry/__init__.py +1 -2
  31. ai_lib_python-0.8.2/src/ai_lib_python/rerank/__init__.py +20 -0
  32. ai_lib_python-0.8.2/src/ai_lib_python/rerank/client.py +144 -0
  33. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/resilience/executor.py +9 -5
  34. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/resilience/fallback.py +3 -3
  35. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/resilience/preflight.py +15 -27
  36. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/resilience/retry.py +5 -4
  37. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/resilience/signals.py +7 -7
  38. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/structured/json_mode.py +6 -2
  39. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/structured/schema.py +10 -4
  40. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/structured/validator.py +18 -8
  41. ai_lib_python-0.8.2/src/ai_lib_python/stt/__init__.py +22 -0
  42. ai_lib_python-0.8.2/src/ai_lib_python/stt/client.py +171 -0
  43. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/telemetry/health.py +1 -1
  44. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/telemetry/logger.py +4 -1
  45. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/transport/auth.py +2 -2
  46. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/transport/http.py +22 -3
  47. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/transport/pool.py +1 -0
  48. ai_lib_python-0.8.2/src/ai_lib_python/tts/__init__.py +22 -0
  49. ai_lib_python-0.8.2/src/ai_lib_python/tts/client.py +164 -0
  50. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/types/message.py +25 -5
  51. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/types/tool.py +19 -1
  52. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/.gitignore +0 -0
  53. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/LICENSE-APACHE +0 -0
  54. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/LICENSE-MIT +0 -0
  55. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/_features.py +0 -0
  56. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/batch/__init__.py +0 -0
  57. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/batch/executor.py +0 -0
  58. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/cache/__init__.py +0 -0
  59. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/cache/backends.py +0 -0
  60. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/cache/key.py +0 -0
  61. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/client/response.py +0 -0
  62. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/drivers/__init__.py +0 -0
  63. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/embeddings/__init__.py +0 -0
  64. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/embeddings/types.py +0 -0
  65. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/embeddings/vectors.py +0 -0
  66. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/errors/__init__.py +0 -0
  67. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/errors/base.py +0 -0
  68. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/errors/classification.py +0 -0
  69. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/errors/standard_codes.py +0 -0
  70. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/mcp/__init__.py +0 -0
  71. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/pipeline/__init__.py +0 -0
  72. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/pipeline/decode.py +0 -0
  73. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/plugins/__init__.py +0 -0
  74. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/plugins/base.py +0 -0
  75. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/plugins/hooks.py +0 -0
  76. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/plugins/middleware.py +0 -0
  77. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/plugins/registry.py +0 -0
  78. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/protocol/__init__.py +0 -0
  79. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/protocol/v2/__init__.py +0 -0
  80. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/protocol/validator.py +0 -0
  81. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/py.typed +0 -0
  82. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/resilience/__init__.py +0 -0
  83. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/resilience/backpressure.py +0 -0
  84. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/resilience/circuit_breaker.py +0 -0
  85. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/resilience/rate_limiter.py +0 -0
  86. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/routing/__init__.py +0 -0
  87. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/routing/manager.py +0 -0
  88. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/routing/strategy.py +0 -0
  89. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/routing/types.py +0 -0
  90. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/structured/__init__.py +0 -0
  91. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/telemetry/__init__.py +0 -0
  92. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/telemetry/exporters/__init__.py +0 -0
  93. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/telemetry/exporters/prometheus.py +0 -0
  94. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/telemetry/feedback.py +0 -0
  95. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/telemetry/metrics.py +0 -0
  96. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/telemetry/tracer.py +0 -0
  97. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/tokens/__init__.py +0 -0
  98. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/tokens/counter.py +0 -0
  99. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/tokens/estimator.py +0 -0
  100. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/transport/__init__.py +0 -0
  101. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/types/__init__.py +0 -0
  102. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/types/events.py +0 -0
  103. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/utils/__init__.py +0 -0
  104. {ai_lib_python-0.7.1 → ai_lib_python-0.8.2}/src/ai_lib_python/utils/tool_call_assembler.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ai-lib-python
3
- Version: 0.7.1
3
+ Version: 0.8.2
4
4
  Summary: Official Python Runtime for AI-Protocol - The canonical Pythonic implementation for unified AI model interaction
5
5
  Project-URL: Homepage, https://github.com/hiddenpath/ai-lib-python
6
6
  Project-URL: Documentation, https://github.com/hiddenpath/ai-lib-python#readme
@@ -57,13 +57,16 @@ Requires-Dist: tiktoken>=0.5; extra == 'full'
57
57
  Requires-Dist: watchdog>=3.0; extra == 'full'
58
58
  Provides-Extra: jupyter
59
59
  Requires-Dist: ipywidgets>=8.0; extra == 'jupyter'
60
+ Provides-Extra: reranking
60
61
  Provides-Extra: structured
62
+ Provides-Extra: stt
61
63
  Provides-Extra: telemetry
62
64
  Requires-Dist: opentelemetry-api>=1.20; extra == 'telemetry'
63
65
  Requires-Dist: opentelemetry-exporter-otlp>=1.20; extra == 'telemetry'
64
66
  Requires-Dist: opentelemetry-sdk>=1.20; extra == 'telemetry'
65
67
  Provides-Extra: tokenizer
66
68
  Requires-Dist: tiktoken>=0.5; extra == 'tokenizer'
69
+ Provides-Extra: tts
67
70
  Provides-Extra: vision
68
71
  Requires-Dist: pillow>=10.0; extra == 'vision'
69
72
  Description-Content-Type: text/markdown
@@ -186,8 +189,13 @@ docker-compose up -d
186
189
 
187
190
  # Run tests with mock
188
191
  MOCK_HTTP_URL=http://localhost:4010 MOCK_MCP_URL=http://localhost:4010/mcp pytest tests/ -v
192
+
193
+ # Run only mock E2E tests (chat, streaming, tools, MCP)
194
+ MOCK_HTTP_URL=http://localhost:4010 pytest tests/integration/test_mock_chat_e2e.py tests/integration/test_mcp_bridge_e2e.py -v
189
195
  ```
190
196
 
197
+ With proxy: set `NO_PROXY` to include the mock server IP (e.g. `NO_PROXY=192.168.2.13,localhost,127.0.0.1`).
198
+
191
199
  Or in code: `AiClient.create("openai/gpt-4o", base_url="http://localhost:4010")`
192
200
 
193
201
  ## 📦 Installation
@@ -209,6 +217,9 @@ pip install ai-lib-python[embeddings] # Embedding generation
209
217
  pip install ai-lib-python[structured] # Structured output / JSON mode
210
218
  pip install ai-lib-python[batch] # Batch processing
211
219
  pip install ai-lib-python[agentic] # Agent workflow support
220
+ pip install ai-lib-python[stt] # Speech-to-Text (STT)
221
+ pip install ai-lib-python[tts] # Text-to-Speech (TTS)
222
+ pip install ai-lib-python[reranking] # Document reranking
212
223
 
213
224
  # Infrastructure extras
214
225
  pip install ai-lib-python[telemetry] # OpenTelemetry integration
@@ -116,8 +116,13 @@ docker-compose up -d
116
116
 
117
117
  # Run tests with mock
118
118
  MOCK_HTTP_URL=http://localhost:4010 MOCK_MCP_URL=http://localhost:4010/mcp pytest tests/ -v
119
+
120
+ # Run only mock E2E tests (chat, streaming, tools, MCP)
121
+ MOCK_HTTP_URL=http://localhost:4010 pytest tests/integration/test_mock_chat_e2e.py tests/integration/test_mcp_bridge_e2e.py -v
119
122
  ```
120
123
 
124
+ With proxy: set `NO_PROXY` to include the mock server IP (e.g. `NO_PROXY=192.168.2.13,localhost,127.0.0.1`).
125
+
121
126
  Or in code: `AiClient.create("openai/gpt-4o", base_url="http://localhost:4010")`
122
127
 
123
128
  ## 📦 Installation
@@ -139,6 +144,9 @@ pip install ai-lib-python[embeddings] # Embedding generation
139
144
  pip install ai-lib-python[structured] # Structured output / JSON mode
140
145
  pip install ai-lib-python[batch] # Batch processing
141
146
  pip install ai-lib-python[agentic] # Agent workflow support
147
+ pip install ai-lib-python[stt] # Speech-to-Text (STT)
148
+ pip install ai-lib-python[tts] # Text-to-Speech (TTS)
149
+ pip install ai-lib-python[reranking] # Document reranking
142
150
 
143
151
  # Infrastructure extras
144
152
  pip install ai-lib-python[telemetry] # OpenTelemetry integration
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ai-lib-python"
7
- version = "0.7.1"
7
+ version = "0.8.2"
8
8
  description = "Official Python Runtime for AI-Protocol - The canonical Pythonic implementation for unified AI model interaction"
9
9
  readme = "README.md"
10
10
  license = "MIT OR Apache-2.0"
@@ -43,6 +43,9 @@ embeddings = []
43
43
  structured = []
44
44
  batch = []
45
45
  agentic = []
46
+ stt = []
47
+ tts = []
48
+ reranking = []
46
49
 
47
50
  # Infrastructure extras
48
51
  telemetry = [
@@ -56,7 +59,7 @@ tokenizer = ["tiktoken>=0.5"]
56
59
  full = [
57
60
  "watchdog>=3.0",
58
61
  "keyring>=24.0",
59
- "ai-lib-python[vision,audio,embeddings,structured,batch,agentic,telemetry,tokenizer]",
62
+ "ai-lib-python[vision,audio,embeddings,structured,batch,agentic,stt,tts,reranking,telemetry,tokenizer]",
60
63
  ]
61
64
  jupyter = ["ipywidgets>=8.0"]
62
65
  dev = [
@@ -143,6 +146,10 @@ ignore = [
143
146
  "SIM105", # contextlib.suppress doesn't work with yield
144
147
  "ARG001", # unused function argument (reserved for future)
145
148
  "ARG003", # unused class method argument (reserved for future)
149
+ "RUF002", # allow bilingual module headers with full-width punctuation
150
+ "RUF012", # defer mutable class-default cleanup to focused refactor
151
+ "RUF022", # keep curated __all__ export order by domain grouping
152
+ "TC001", # keep runtime imports explicit in legacy driver modules
146
153
  ]
147
154
 
148
155
  [tool.ruff.lint.isort]
@@ -167,3 +174,4 @@ exclude_lines = [
167
174
  "if __name__ == .__main__.:",
168
175
  "@abstractmethod",
169
176
  ]
177
+
@@ -27,7 +27,7 @@ from ai_lib_python.types.message import (
27
27
  )
28
28
  from ai_lib_python.types.tool import ToolCall, ToolDefinition
29
29
 
30
- __version__ = "0.7.0"
30
+ __version__ = "0.7.5"
31
31
 
32
32
  __all__ = [
33
33
  # Client
@@ -191,7 +191,10 @@ class BatchCollector(Generic[T, R]):
191
191
 
192
192
  try:
193
193
  # Execute batch
194
- results = await self._executor(data_list)
194
+ executor = self._executor
195
+ if executor is None:
196
+ raise RuntimeError("No executor set")
197
+ results = await executor(data_list)
195
198
 
196
199
  # Resolve futures
197
200
  for request, result in zip(requests, results, strict=False):
@@ -130,6 +130,7 @@ class CacheManager:
130
130
  self._key_generator = CacheKeyGenerator()
131
131
  self._stats = CacheStats()
132
132
 
133
+ self._backend: CacheBackend
133
134
  if not self._config.enabled:
134
135
  self._backend = NullCache()
135
136
  else:
@@ -10,8 +10,8 @@ This module provides:
10
10
 
11
11
  from ai_lib_python.client.builder import AiClientBuilder, ChatRequestBuilder
12
12
  from ai_lib_python.client.cancel import (
13
- CancellableStream,
14
13
  CancelHandle,
14
+ CancellableStream,
15
15
  CancelReason,
16
16
  CancelState,
17
17
  CancelToken,
@@ -25,11 +25,11 @@ __all__ = [
25
25
  "AiClient",
26
26
  "AiClientBuilder",
27
27
  "CallStats",
28
- "CancellableStream",
29
28
  "CancelHandle",
30
29
  "CancelReason",
31
30
  "CancelState",
32
31
  "CancelToken",
32
+ "CancellableStream",
33
33
  "ChatRequestBuilder",
34
34
  "ChatResponse",
35
35
  "create_cancel_pair",
@@ -52,6 +52,7 @@ class AiClientBuilder:
52
52
  self._rate_limit_config: RateLimiterConfig | None = None
53
53
  self._circuit_breaker_config: CircuitBreakerConfig | None = None
54
54
  self._resilient_config: ResilientConfig | None = None
55
+ self._api_keys: dict[str, str] = {}
55
56
 
56
57
  def model(self, model_id: str) -> AiClientBuilder:
57
58
  """Set the model to use.
@@ -149,6 +150,37 @@ class AiClientBuilder:
149
150
  self._max_inflight = n
150
151
  return self
151
152
 
153
+ def retry(self, max_attempts: int = 3, backoff: float = 1.0) -> AiClientBuilder:
154
+ """Configure retry policy with simple parameters.
155
+
156
+ Args:
157
+ max_attempts: Maximum number of attempts (including initial)
158
+ backoff: Backoff base in seconds
159
+
160
+ Returns:
161
+ Self for chaining
162
+ """
163
+ from ai_lib_python.resilience import RetryConfig
164
+
165
+ self._retry_config = RetryConfig(
166
+ max_retries=max_attempts - 1,
167
+ min_delay_ms=int(backoff * 1000),
168
+ )
169
+ return self
170
+
171
+ def api_key_for(self, model_id: str, key: str) -> AiClientBuilder:
172
+ """Set API key for a specific fallback model.
173
+
174
+ Args:
175
+ model_id: Model identifier
176
+ key: API key to use for this model
177
+
178
+ Returns:
179
+ Self for chaining
180
+ """
181
+ self._api_keys[model_id] = key
182
+ return self
183
+
152
184
  def with_retry(self, config: RetryConfig) -> AiClientBuilder:
153
185
  """Configure retry policy.
154
186
 
@@ -252,6 +284,7 @@ class AiClientBuilder:
252
284
  timeout=self._timeout,
253
285
  hot_reload=self._hot_reload,
254
286
  resilient_config=resilient_config,
287
+ api_keys=self._api_keys,
255
288
  )
256
289
 
257
290
 
@@ -77,8 +77,12 @@ class CancelToken:
77
77
 
78
78
  def _start_timeout(self) -> None:
79
79
  """Start the timeout task."""
80
+ timeout = self._timeout
81
+ if timeout is None:
82
+ return
83
+
80
84
  async def timeout_handler() -> None:
81
- await asyncio.sleep(self._timeout) # type: ignore
85
+ await asyncio.sleep(timeout)
82
86
  if not self._state.cancelled:
83
87
  self.cancel(CancelReason.TIMEOUT)
84
88
 
@@ -125,7 +129,7 @@ class CancelToken:
125
129
  try:
126
130
  result = callback(reason)
127
131
  if asyncio.iscoroutine(result):
128
- _ = asyncio.create_task(result) # type: ignore # noqa: RUF006
132
+ _ = asyncio.create_task(result) # noqa: RUF006
129
133
  except Exception:
130
134
  pass
131
135
 
@@ -185,7 +189,7 @@ class CancelToken:
185
189
  try:
186
190
  result = callback(self._state.reason)
187
191
  if asyncio.iscoroutine(result):
188
- _ = asyncio.create_task(result) # type: ignore # noqa: RUF006
192
+ _ = asyncio.create_task(result) # noqa: RUF006
189
193
  except Exception:
190
194
  pass
191
195
  return self
@@ -56,6 +56,10 @@ class AiClient:
56
56
  model_id: str,
57
57
  fallbacks: list[str] | None = None,
58
58
  executor: ResilientExecutor | None = None,
59
+ loader: ProtocolLoader | None = None,
60
+ api_keys: dict[str, str] | None = None,
61
+ base_url_override: str | None = None,
62
+ timeout: float | None = None,
59
63
  ) -> None:
60
64
  """Initialize the client (internal use).
61
65
 
@@ -67,6 +71,10 @@ class AiClient:
67
71
  self._model_id = model_id
68
72
  self._fallbacks = fallbacks or []
69
73
  self._executor = executor
74
+ self._loader = loader
75
+ self._api_keys = api_keys or {}
76
+ self._base_url_override = base_url_override
77
+ self._timeout = timeout
70
78
 
71
79
  @classmethod
72
80
  async def create(
@@ -131,6 +139,7 @@ class AiClient:
131
139
  timeout: float | None = None,
132
140
  hot_reload: bool = False,
133
141
  resilient_config: ResilientConfig | None = None,
142
+ api_keys: dict[str, str] | None = None,
134
143
  ) -> AiClient:
135
144
  """Internal creation method.
136
145
 
@@ -181,9 +190,13 @@ class AiClient:
181
190
  manifest=manifest,
182
191
  transport=transport,
183
192
  pipeline=pipeline,
184
- model_id=model_id,
193
+ model_id=model, # Keep the full model name including provider
185
194
  fallbacks=fallbacks,
186
195
  executor=executor,
196
+ loader=loader,
197
+ api_keys=api_keys,
198
+ base_url_override=base_url_override,
199
+ timeout=timeout,
187
200
  )
188
201
 
189
202
  def chat(self) -> ChatRequestBuilder:
@@ -203,7 +216,7 @@ class AiClient:
203
216
  return ChatRequestBuilder(self)
204
217
 
205
218
  async def _execute_chat(self, builder: ChatRequestBuilder) -> ChatResponse:
206
- """Execute a non-streaming chat request.
219
+ """Execute a non-streaming chat request with fallback support.
207
220
 
208
221
  Args:
209
222
  builder: Configured request builder
@@ -211,19 +224,89 @@ class AiClient:
211
224
  Returns:
212
225
  ChatResponse with the completion
213
226
  """
214
- async def do_request() -> ChatResponse:
215
- payload = builder.build_payload()
216
- endpoint = self._manifest.get_chat_endpoint()
227
+ models_to_try = [self._model_id, *self._fallbacks]
228
+ last_error = None
217
229
 
218
- response = await self._transport.post(endpoint, json=payload)
219
- data = response.json()
220
-
221
- return self._parse_response(data)
222
-
223
- # Use executor if available for resilience
224
- if self._executor:
225
- return await self._executor.execute(do_request)
226
- return await do_request()
230
+ for model in models_to_try:
231
+ try:
232
+ # 1. Resolve manifest and transport for this model
233
+ if model == self._model_id:
234
+ manifest = self._manifest
235
+ transport = self._transport
236
+ pipeline = self._pipeline
237
+ else:
238
+ # Dynamic load for fallback
239
+ if not self._loader:
240
+ raise ValueError("ProtocolLoader missing for fallback")
241
+ manifest = await self._loader.load_model(model)
242
+
243
+ parts = model.split("/")
244
+ m_id = parts[1] if len(parts) >= 2 else model
245
+
246
+ # Resolve key for this model
247
+ m_key = self._api_keys.get(model)
248
+
249
+ from ai_lib_python.transport import HttpTransport
250
+ transport = HttpTransport(
251
+ manifest=manifest,
252
+ model_id=m_id,
253
+ api_key=m_key,
254
+ base_url_override=self._base_url_override,
255
+ timeout=self._timeout,
256
+ )
257
+ pipeline = Pipeline.from_manifest(manifest)
258
+
259
+ async def do_request(
260
+ m: ProtocolManifest = manifest,
261
+ t: HttpTransport = transport,
262
+ p: Pipeline = pipeline,
263
+ mid: str = model,
264
+ ) -> ChatResponse:
265
+ # Debug print for model being used
266
+ print(f"DEBUG: Executing request for model: {mid}, manifest ID: {m.id}")
267
+
268
+ # Update builder's client temporary context?
269
+ # Actually builder.build_payload() uses self._client._model_id
270
+ # This is tricky as builder is bound to the primary client.
271
+ # We need to temporarily override the client context in the builder.
272
+
273
+ # Create a temporary builder/payload
274
+ # For simplicity, we'll just manually build the payload here or
275
+ # temporarily swap self._model_id (hacky but it works for this pattern)
276
+ original_model_id = self._model_id
277
+ original_manifest = self._manifest
278
+ try:
279
+ self._model_id = mid
280
+ self._manifest = m
281
+ payload = builder.build_payload()
282
+ print(f"DEBUG: Payload model: {payload.get('model')}")
283
+ finally:
284
+ self._model_id = original_model_id
285
+ self._manifest = original_manifest
286
+
287
+ endpoint = m.get_chat_endpoint()
288
+ print(f"DEBUG: Endpoint: {endpoint}")
289
+ response = await t.post(endpoint, json=payload)
290
+ data = response.json()
291
+
292
+ # Parse using the correct pipeline
293
+ return self._parse_response(data)
294
+
295
+ # Use executor if available for resilience
296
+ if self._executor:
297
+ return await self._executor.execute(do_request)
298
+ return await do_request()
299
+
300
+ except Exception as e:
301
+ from ai_lib_python.errors import is_fallbackable
302
+ # Check if we should fallback
303
+ error_class = getattr(e, "error_class", None)
304
+ if model != models_to_try[-1] and (error_class is None or is_fallbackable(error_class)):
305
+ last_error = e
306
+ continue
307
+ raise e
308
+
309
+ raise last_error or RuntimeError("Fallback exhausted")
227
310
 
228
311
  async def _execute_chat_with_stats(
229
312
  self, builder: ChatRequestBuilder
@@ -14,7 +14,6 @@ from enum import Enum
14
14
  from typing import Any
15
15
  from urllib.parse import urlparse
16
16
 
17
-
18
17
  # ─── Normalized Action Types ────────────────────────────────────────────────
19
18
 
20
19
 
@@ -162,6 +162,16 @@ class AnthropicDriver(ProviderDriver):
162
162
  system_parts.append(m.content)
163
163
  continue
164
164
 
165
+ if role == "tool":
166
+ # Anthropic: tool results as user message with tool_result block
167
+ tool_id = getattr(m, "tool_call_id", None)
168
+ if tool_id and isinstance(m.content, str):
169
+ msgs.append({
170
+ "role": "user",
171
+ "content": [{"type": "tool_result", "tool_use_id": tool_id, "content": m.content}],
172
+ })
173
+ continue
174
+
165
175
  if isinstance(m.content, str):
166
176
  content: Any = [{"type": "text", "text": m.content}]
167
177
  else:
@@ -55,7 +55,7 @@ class GeminiDriver(ProviderDriver):
55
55
  def build_request(
56
56
  self,
57
57
  messages: list[Message],
58
- model: str,
58
+ _model: str,
59
59
  *,
60
60
  temperature: float | None = None,
61
61
  max_tokens: int | None = None,
@@ -161,6 +161,16 @@ class GeminiDriver(ProviderDriver):
161
161
  system_parts.append(m.content)
162
162
  continue
163
163
 
164
+ if role == "tool":
165
+ # Gemini: function_response with name (tool_call_id) and response
166
+ tool_id = getattr(m, "tool_call_id", None)
167
+ if tool_id and isinstance(m.content, str):
168
+ contents.append({
169
+ "role": "user",
170
+ "parts": [{"functionResponse": {"name": tool_id, "response": {"result": m.content}}}],
171
+ })
172
+ continue
173
+
164
174
  gemini_role = "model" if role == "assistant" else "user"
165
175
 
166
176
  if isinstance(m.content, str):
@@ -18,7 +18,7 @@ from ai_lib_python.drivers import (
18
18
  from ai_lib_python.protocol.v2.capabilities import Capability
19
19
  from ai_lib_python.protocol.v2.manifest import ApiStyle
20
20
  from ai_lib_python.types.events import StreamingEvent
21
- from ai_lib_python.types.message import ContentBlock, Message
21
+ from ai_lib_python.types.message import Message
22
22
 
23
23
 
24
24
  class OpenAiDriver(ProviderDriver):
@@ -120,14 +120,18 @@ class OpenAiDriver(ProviderDriver):
120
120
  # role is stored as str because model uses use_enum_values=True
121
121
  role = m.role if isinstance(m.role, str) else m.role.value
122
122
  if isinstance(m.content, str):
123
- return {"role": role, "content": m.content}
124
- # list[ContentBlock] → OpenAI content array
125
- blocks = []
126
- for b in m.content:
127
- if b.type == "text":
128
- blocks.append({"type": "text", "text": b.text})
129
- elif b.type == "image":
130
- blocks.append(b.model_dump(by_alias=True))
131
- else:
132
- blocks.append(b.model_dump(by_alias=True))
133
- return {"role": role, "content": blocks}
123
+ out: dict[str, Any] = {"role": role, "content": m.content}
124
+ else:
125
+ # list[ContentBlock] → OpenAI content array
126
+ blocks = []
127
+ for b in m.content:
128
+ if b.type == "text":
129
+ blocks.append({"type": "text", "text": b.text})
130
+ elif b.type == "image":
131
+ blocks.append(b.model_dump(by_alias=True))
132
+ else:
133
+ blocks.append(b.model_dump(by_alias=True))
134
+ out = {"role": role, "content": blocks}
135
+ if role == "tool" and getattr(m, "tool_call_id", None):
136
+ out["tool_call_id"] = m.tool_call_id
137
+ return out
@@ -193,9 +193,12 @@ class EmbeddingClient:
193
193
  Returns:
194
194
  Endpoint path
195
195
  """
196
- # Try to get from manifest, default to OpenAI-style
197
- if hasattr(self._manifest, "embedding_endpoint"):
198
- return self._manifest.embedding_endpoint
196
+ # Prefer manifest endpoint mapping when present.
197
+ embedding_cfg = self._manifest.endpoints.get("embeddings")
198
+ if isinstance(embedding_cfg, dict):
199
+ path = embedding_cfg.get("path")
200
+ if isinstance(path, str):
201
+ return path
199
202
  return "/v1/embeddings"
200
203
 
201
204
  @property
@@ -325,8 +328,9 @@ class EmbeddingClientBuilder:
325
328
  manifest = await loader.load_provider(provider_id)
326
329
 
327
330
  # Create transport
328
- transport = HttpTransport.from_manifest(
329
- manifest,
331
+ transport = HttpTransport(
332
+ manifest=manifest,
333
+ model_id=model_id,
330
334
  api_key=self._api_key,
331
335
  base_url_override=self._base_url,
332
336
  timeout=self._timeout,
@@ -7,14 +7,14 @@ both user inputs and AI model outputs to ensure safety and compliance.
7
7
  Core principle: All logic is operators, all configuration is protocol.
8
8
  """
9
9
 
10
- from ai_lib_python.guardrails.base import Guardrail, GuardrailViolation, GuardrailResult
10
+ from ai_lib_python.guardrails.base import Guardrail, GuardrailResult, GuardrailViolation
11
11
  from ai_lib_python.guardrails.filters import (
12
+ EmailFilter,
12
13
  KeywordFilter,
13
- RegexFilter,
14
14
  LengthFilter,
15
15
  ProfanityFilter,
16
+ RegexFilter,
16
17
  UrlFilter,
17
- EmailFilter,
18
18
  )
19
19
  from ai_lib_python.guardrails.validators import ContentValidator
20
20
 
@@ -6,7 +6,7 @@ from __future__ import annotations
6
6
 
7
7
  from dataclasses import dataclass, field
8
8
  from enum import Enum
9
- from typing import TYPE_CHECKING
9
+ from typing import TYPE_CHECKING, Any
10
10
 
11
11
  if TYPE_CHECKING:
12
12
  from collections.abc import Callable
@@ -29,14 +29,14 @@ class GuardrailViolation:
29
29
  message: str
30
30
  severity: GuardrailSeverity
31
31
  matched_text: str | None = None
32
- metadata: dict = field(default_factory=dict)
32
+ metadata: dict[str, Any] = field(default_factory=dict)
33
33
 
34
34
  def __post_init__(self) -> None:
35
35
  """Validate severity."""
36
36
  if not isinstance(self.severity, GuardrailSeverity):
37
37
  self.severity = GuardrailSeverity(self.severity)
38
38
 
39
- def to_dict(self) -> dict:
39
+ def to_dict(self) -> dict[str, Any]:
40
40
  """Convert violation to dictionary."""
41
41
  return {
42
42
  "rule_id": self.rule_id,
@@ -54,10 +54,10 @@ class GuardrailResult:
54
54
  is_safe: bool
55
55
  violations: list[GuardrailViolation] = field(default_factory=list)
56
56
  filtered_content: str | None = None
57
- metadata: dict = field(default_factory=dict)
57
+ metadata: dict[str, Any] = field(default_factory=dict)
58
58
 
59
59
  @classmethod
60
- def safe(cls, content: str | None = None) -> "GuardrailResult":
60
+ def safe(cls, content: str | None = None) -> GuardrailResult:
61
61
  """Create a safe result."""
62
62
  return cls(is_safe=True, violations=[], filtered_content=content)
63
63
 
@@ -66,7 +66,7 @@ class GuardrailResult:
66
66
  cls,
67
67
  violations: list[GuardrailViolation],
68
68
  filtered_content: str | None = None,
69
- ) -> "GuardrailResult":
69
+ ) -> GuardrailResult:
70
70
  """Create a violated result."""
71
71
  return cls(
72
72
  is_safe=False,
@@ -74,7 +74,7 @@ class GuardrailResult:
74
74
  filtered_content=filtered_content,
75
75
  )
76
76
 
77
- def to_dict(self) -> dict:
77
+ def to_dict(self) -> dict[str, Any]:
78
78
  """Convert result to dictionary."""
79
79
  return {
80
80
  "is_safe": self.is_safe,
@@ -303,7 +303,7 @@ class ConditionalGuardrail(Guardrail):
303
303
  self,
304
304
  rule_id: str,
305
305
  guardrail: Guardrail,
306
- condition: Callable[[dict], bool],
306
+ condition: Callable[[dict[str, Any]], bool],
307
307
  severity: GuardrailSeverity = GuardrailSeverity.WARNING,
308
308
  ) -> None:
309
309
  """Initialize conditional guardrail.
@@ -317,9 +317,9 @@ class ConditionalGuardrail(Guardrail):
317
317
  super().__init__(rule_id, severity)
318
318
  self._guardrail = guardrail
319
319
  self._condition = condition
320
- self._context: dict = {}
320
+ self._context: dict[str, Any] = {}
321
321
 
322
- def set_context(self, context: dict) -> None:
322
+ def set_context(self, context: dict[str, Any]) -> None:
323
323
  """Set the context for condition evaluation."""
324
324
  self._context = context
325
325