callm-toolkit 0.1.2__tar.gz → 0.3.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.
Files changed (75) hide show
  1. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/CHANGELOG.md +31 -1
  2. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/PKG-INFO +7 -5
  3. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/README.md +5 -3
  4. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/pyproject.toml +1 -1
  5. callm_toolkit-0.3.0/src/callm/__about__.py +1 -0
  6. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/src/callm/cli.py +1 -1
  7. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/src/callm/decorator.py +5 -1
  8. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/src/callm/interception.py +142 -3
  9. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/src/callm/middleware/telemetry.py +1 -1
  10. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/src/callm/pricing.py +5 -2
  11. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/src/callm/providers/__init__.py +2 -0
  12. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/src/callm/providers/base.py +8 -1
  13. callm_toolkit-0.3.0/src/callm/providers/openai_responses.py +283 -0
  14. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/src/callm/providers/registry.py +2 -0
  15. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/src/callm/types.py +5 -1
  16. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/tests/helpers.py +44 -0
  17. callm_toolkit-0.3.0/tests/test_parse.py +123 -0
  18. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/tests/test_regressions.py +30 -0
  19. callm_toolkit-0.3.0/tests/test_responses.py +298 -0
  20. callm_toolkit-0.1.2/src/callm/__about__.py +0 -1
  21. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/.gitignore +0 -0
  22. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/LICENSE +0 -0
  23. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/benchmarks/run.py +0 -0
  24. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/examples/async_budgets.py +0 -0
  25. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/examples/demo.tape +0 -0
  26. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/examples/offline_demo.py +0 -0
  27. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/examples/quickstart.py +0 -0
  28. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/examples/structured_output.py +0 -0
  29. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/examples/ticket_triage.py +0 -0
  30. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/src/callm/__init__.py +0 -0
  31. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/src/callm/__main__.py +0 -0
  32. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/src/callm/api.py +0 -0
  33. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/src/callm/budgets.py +0 -0
  34. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/src/callm/classify.py +0 -0
  35. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/src/callm/config.py +0 -0
  36. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/src/callm/data/__init__.py +0 -0
  37. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/src/callm/data/pricing.json +0 -0
  38. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/src/callm/embeddings.py +0 -0
  39. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/src/callm/errors.py +0 -0
  40. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/src/callm/middleware/__init__.py +0 -0
  41. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/src/callm/middleware/cache.py +0 -0
  42. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/src/callm/middleware/cost.py +0 -0
  43. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/src/callm/middleware/fallback.py +0 -0
  44. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/src/callm/middleware/retry.py +0 -0
  45. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/src/callm/middleware/security.py +0 -0
  46. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/src/callm/middleware/validator.py +0 -0
  47. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/src/callm/pipeline.py +0 -0
  48. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/src/callm/providers/anthropic.py +0 -0
  49. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/src/callm/providers/google.py +0 -0
  50. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/src/callm/providers/openai.py +0 -0
  51. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/src/callm/py.typed +0 -0
  52. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/src/callm/reports.py +0 -0
  53. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/src/callm/security/__init__.py +0 -0
  54. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/src/callm/security/injection.py +0 -0
  55. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/src/callm/security/pii.py +0 -0
  56. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/src/callm/storage/__init__.py +0 -0
  57. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/src/callm/storage/base.py +0 -0
  58. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/src/callm/storage/memory.py +0 -0
  59. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/src/callm/storage/redis.py +0 -0
  60. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/src/callm/storage/sqlite.py +0 -0
  61. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/src/callm/validation.py +0 -0
  62. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/tests/conftest.py +0 -0
  63. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/tests/test_api.py +0 -0
  64. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/tests/test_cache.py +0 -0
  65. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/tests/test_cli.py +0 -0
  66. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/tests/test_core.py +0 -0
  67. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/tests/test_cost.py +0 -0
  68. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/tests/test_decorator.py +0 -0
  69. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/tests/test_extras.py +0 -0
  70. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/tests/test_fallback.py +0 -0
  71. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/tests/test_providers.py +0 -0
  72. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/tests/test_retry.py +0 -0
  73. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/tests/test_security.py +0 -0
  74. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/tests/test_telemetry.py +0 -0
  75. {callm_toolkit-0.1.2 → callm_toolkit-0.3.0}/tests/test_validation.py +0 -0
@@ -6,6 +6,34 @@ All notable changes to this project are documented here. The format follows
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.3.0] - 2026-10-02
10
+
11
+ ### Added
12
+
13
+ - OpenAI structured outputs: `client.chat.completions.parse(...)`, sync and async, is now
14
+ intercepted. PII masking, budgets, caching, retries, cost and telemetry apply, and the
15
+ caller still gets a `ParsedChatCompletion` with `.parsed`, also on cache hits. Different
16
+ `response_format` models never share a cache entry.
17
+
18
+ ### Fixed
19
+
20
+ - Storing a `ParsedChatCompletion` for the cache no longer prints pydantic serializer
21
+ warnings.
22
+
23
+ ## [0.2.0] - 2026-09-29
24
+
25
+ ### Added
26
+
27
+ - OpenAI Responses API support: `client.responses.create(...)`, sync and async, is intercepted
28
+ like `chat.completions.create`. `instructions`, string and item `input` and tool outputs get
29
+ PII masking and injection checks; budgets, caching, output validation, retries, cost and
30
+ telemetry apply; fallbacks to other providers return a real `Response` object. The adapter is
31
+ also available as the `openai-responses` provider for `callm.complete` and fallback chains.
32
+ - Inside a callm function, SDK helpers callm does not intercept yet (`responses.parse/stream`,
33
+ `chat.completions.parse/stream`, `beta.chat.completions.parse/stream`, Anthropic
34
+ `messages.stream`) now log a one-time warning naming the protections that do not apply,
35
+ instead of silently skipping PII masking, budgets and telemetry.
36
+
9
37
  ## [0.1.2] - 2026-09-27
10
38
 
11
39
  ### Fixed
@@ -86,7 +114,9 @@ Initial release.
86
114
  budgets) from enclosing decorators and shields apply to inner calls.
87
115
  - `callm` CLI: `stats`, `calls`, `cache`, `pricing`, `telemetry`, `info`.
88
116
 
89
- [Unreleased]: https://github.com/TanbirRamim/callm/compare/v0.1.1...HEAD
117
+ [Unreleased]: https://github.com/TanbirRamim/callm/compare/v0.3.0...HEAD
118
+ [0.3.0]: https://github.com/TanbirRamim/callm/compare/v0.2.0...v0.3.0
119
+ [0.2.0]: https://github.com/TanbirRamim/callm/compare/v0.1.2...v0.2.0
90
120
  [0.1.2]: https://github.com/TanbirRamim/callm/compare/v0.1.1...v0.1.2
91
121
  [0.1.1]: https://github.com/TanbirRamim/callm/compare/v0.1.0...v0.1.1
92
122
  [0.1.0]: https://github.com/TanbirRamim/callm/releases/tag/v0.1.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: callm-toolkit
3
- Version: 0.1.2
3
+ Version: 0.3.0
4
4
  Summary: The production toolkit for LLM calls: caching, retries, fallback, cost tracking, security and structured output in one decorator.
5
5
  Project-URL: Homepage, https://github.com/TanbirRamim/callm
6
6
  Project-URL: Documentation, https://tanbirramim.github.io/callm
@@ -10,7 +10,7 @@ Project-URL: Changelog, https://github.com/TanbirRamim/callm/blob/main/CHANGELOG
10
10
  Author: Tanbir Hossain Ramim
11
11
  License-Expression: MIT
12
12
  License-File: LICENSE
13
- Keywords: anthropic,caching,fallback,gemini,llm,openai,pii,production,prompt-injection,pydantic,retry
13
+ Keywords: anthropic,budget,caching,cost-tracking,fallback,gemini,llm,llmops,openai,pii,production,prompt-injection,pydantic,retry,telemetry
14
14
  Classifier: Development Status :: 4 - Beta
15
15
  Classifier: Intended Audience :: Developers
16
16
  Classifier: Operating System :: OS Independent
@@ -364,8 +364,8 @@ def summarize(): ─────────► │ 1. Telemetry cost, la
364
364
  ```
365
365
 
366
366
  1. `@callm` sets a scope (a `ContextVar`) while your function runs.
367
- 2. The official SDK methods (`chat.completions.create`, `messages.create`,
368
- `models.generate_content`, sync and async) are instrumented on first use. Outside a callm
367
+ 2. The official SDK methods (`chat.completions.create` and `.parse`, OpenAI's `responses.create`,
368
+ `messages.create`, `models.generate_content`, sync and async) are instrumented on first use. Outside a callm
369
369
  scope they call straight through, so importing callm never changes other code.
370
370
  3. Inside a scope, the SDK call is converted into a provider-neutral request and sent through
371
371
  the middleware chain. Each layer is independent and only enabled when configured.
@@ -473,7 +473,7 @@ Measured offline with the real OpenAI SDK against a fake server
473
473
 
474
474
  ## Contributing
475
475
 
476
- Contributions are welcome — see [CONTRIBUTING.md](https://github.com/TanbirRamim/callm/blob/main/CONTRIBUTING.md). The test suite runs
476
+ Contributions are welcome. The [good first issues](https://github.com/TanbirRamim/callm/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) are small and scoped, and every pull request gets a review. See [CONTRIBUTING.md](https://github.com/TanbirRamim/callm/blob/main/CONTRIBUTING.md). The test suite runs
477
477
  entirely offline against the real provider SDKs with mocked HTTP transports:
478
478
 
479
479
  ```bash
@@ -481,6 +481,8 @@ uv sync
481
481
  uv run pytest
482
482
  ```
483
483
 
484
+ If callm is useful to you, a star helps other people find it.
485
+
484
486
  ## License
485
487
 
486
488
  [MIT](https://github.com/TanbirRamim/callm/blob/main/LICENSE)
@@ -305,8 +305,8 @@ def summarize(): ─────────► │ 1. Telemetry cost, la
305
305
  ```
306
306
 
307
307
  1. `@callm` sets a scope (a `ContextVar`) while your function runs.
308
- 2. The official SDK methods (`chat.completions.create`, `messages.create`,
309
- `models.generate_content`, sync and async) are instrumented on first use. Outside a callm
308
+ 2. The official SDK methods (`chat.completions.create` and `.parse`, OpenAI's `responses.create`,
309
+ `messages.create`, `models.generate_content`, sync and async) are instrumented on first use. Outside a callm
310
310
  scope they call straight through, so importing callm never changes other code.
311
311
  3. Inside a scope, the SDK call is converted into a provider-neutral request and sent through
312
312
  the middleware chain. Each layer is independent and only enabled when configured.
@@ -414,7 +414,7 @@ Measured offline with the real OpenAI SDK against a fake server
414
414
 
415
415
  ## Contributing
416
416
 
417
- Contributions are welcome — see [CONTRIBUTING.md](https://github.com/TanbirRamim/callm/blob/main/CONTRIBUTING.md). The test suite runs
417
+ Contributions are welcome. The [good first issues](https://github.com/TanbirRamim/callm/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) are small and scoped, and every pull request gets a review. See [CONTRIBUTING.md](https://github.com/TanbirRamim/callm/blob/main/CONTRIBUTING.md). The test suite runs
418
418
  entirely offline against the real provider SDKs with mocked HTTP transports:
419
419
 
420
420
  ```bash
@@ -422,6 +422,8 @@ uv sync
422
422
  uv run pytest
423
423
  ```
424
424
 
425
+ If callm is useful to you, a star helps other people find it.
426
+
425
427
  ## License
426
428
 
427
429
  [MIT](https://github.com/TanbirRamim/callm/blob/main/LICENSE)
@@ -11,7 +11,7 @@ license = "MIT"
11
11
  license-files = ["LICENSE"]
12
12
  requires-python = ">=3.10"
13
13
  authors = [{ name = "Tanbir Hossain Ramim" }]
14
- keywords = ["llm", "openai", "anthropic", "gemini", "caching", "retry", "fallback", "pii", "prompt-injection", "pydantic", "production"]
14
+ keywords = ["llm", "openai", "anthropic", "gemini", "caching", "retry", "fallback", "pii", "prompt-injection", "pydantic", "production", "llmops", "cost-tracking", "budget", "telemetry"]
15
15
  classifiers = [
16
16
  "Development Status :: 4 - Beta",
17
17
  "Intended Audience :: Developers",
@@ -0,0 +1 @@
1
+ __version__ = "0.3.0"
@@ -272,7 +272,7 @@ def _cmd_info(args: argparse.Namespace) -> int:
272
272
  "enabled": settings.enabled,
273
273
  "providers": provider_names(),
274
274
  "instrumented_methods": sorted(
275
- {f"{t.provider}: {t.cls}.{t.method}" for t in PATCH_TARGETS}
275
+ {f"{t.provider}: {t.cls}.{t.method}" for t in PATCH_TARGETS if not t.warn_only}
276
276
  ),
277
277
  "optional_packages": optional,
278
278
  }
@@ -42,7 +42,9 @@ def detect_provider(value: Any) -> str | None:
42
42
  """Which provider SDK produced ``value`` (a native response object), if any."""
43
43
  module = type(value).__module__ or ""
44
44
  if module.startswith("openai"):
45
- return "openai" if hasattr(value, "choices") else None
45
+ if hasattr(value, "choices"):
46
+ return "openai"
47
+ return "openai-responses" if hasattr(value, "output") and hasattr(value, "usage") else None
46
48
  if module.startswith("anthropic"):
47
49
  return "anthropic" if hasattr(value, "content") and hasattr(value, "usage") else None
48
50
  if module.startswith("google.genai"):
@@ -50,6 +52,8 @@ def detect_provider(value: Any) -> str | None:
50
52
  if isinstance(value, AttrDict):
51
53
  if "choices" in value:
52
54
  return "openai"
55
+ if value.get("object") == "response":
56
+ return "openai-responses"
53
57
  if value.get("type") == "message":
54
58
  return "anthropic"
55
59
  if "candidates" in value:
@@ -95,17 +95,60 @@ class PatchTarget:
95
95
  cls: str
96
96
  method: str
97
97
  is_async: bool
98
+ #: True for SDK methods callm does not intercept yet: inside @callm they only log a
99
+ #: one-time warning, so nobody assumes PII masking or budgets apply when they do not.
100
+ warn_only: bool = False
98
101
 
99
102
 
100
103
  PATCH_TARGETS: tuple[PatchTarget, ...] = (
101
104
  PatchTarget("openai", "openai.resources.chat.completions", "Completions", "create", False),
102
105
  PatchTarget("openai", "openai.resources.chat.completions", "AsyncCompletions", "create", True),
106
+ PatchTarget("openai", "openai.resources.chat.completions", "Completions", "parse", False),
107
+ PatchTarget("openai", "openai.resources.chat.completions", "AsyncCompletions", "parse", True),
108
+ PatchTarget("openai-responses", "openai.resources.responses", "Responses", "create", False),
109
+ PatchTarget("openai-responses", "openai.resources.responses", "AsyncResponses", "create", True),
103
110
  PatchTarget("anthropic", "anthropic.resources.messages", "Messages", "create", False),
104
111
  PatchTarget("anthropic", "anthropic.resources.messages", "AsyncMessages", "create", True),
105
112
  PatchTarget("google", "google.genai.models", "Models", "generate_content", False),
106
113
  PatchTarget("google", "google.genai.models", "AsyncModels", "generate_content", True),
114
+ *(
115
+ PatchTarget(provider, module, cls, method, cls.startswith("Async"), warn_only=True)
116
+ for provider, module, classes, methods in (
117
+ (
118
+ "openai-responses",
119
+ "openai.resources.responses",
120
+ ("Responses", "AsyncResponses"),
121
+ ("parse", "stream"),
122
+ ),
123
+ (
124
+ "openai",
125
+ "openai.resources.chat.completions",
126
+ ("Completions", "AsyncCompletions"),
127
+ ("stream",),
128
+ ),
129
+ (
130
+ "openai",
131
+ "openai.resources.beta.chat.completions",
132
+ ("Completions", "AsyncCompletions"),
133
+ ("parse", "stream"),
134
+ ),
135
+ (
136
+ "anthropic",
137
+ "anthropic.resources.messages",
138
+ ("Messages", "AsyncMessages"),
139
+ ("stream",),
140
+ ),
141
+ )
142
+ for cls in classes
143
+ for method in methods
144
+ ),
107
145
  )
108
- _ROOT_PACKAGES = {"openai": "openai", "anthropic": "anthropic", "google": "google.genai"}
146
+ _ROOT_PACKAGES = {
147
+ "openai": "openai",
148
+ "openai-responses": "openai",
149
+ "anthropic": "anthropic",
150
+ "google": "google.genai",
151
+ }
109
152
  _RAW_RESPONSE_HEADERS = frozenset({"x-stainless-raw-response", "x-stainless-streamed-raw-response"})
110
153
 
111
154
  _lock = threading.RLock()
@@ -160,6 +203,7 @@ def client_endpoint(resource: Any) -> str | None:
160
203
  # Where each patched resource lives on its SDK client, to rebuild it on a client copy.
161
204
  _RESOURCE_PATHS: dict[str, tuple[str, ...]] = {
162
205
  "openai": ("chat", "completions"),
206
+ "openai-responses": ("responses",),
163
207
  "anthropic": ("messages",),
164
208
  }
165
209
  _no_retry_resources: weakref.WeakKeyDictionary[Any, Any] = weakref.WeakKeyDictionary()
@@ -238,7 +282,99 @@ def clean_kwargs(kwargs: dict[str, Any]) -> dict[str, Any]:
238
282
  def _finish(target: PatchTarget, state: CallState, response: LLMResponse) -> Any:
239
283
  if state.request.stream:
240
284
  return response.raw
241
- return get_provider(target.provider).native_for(response)
285
+ native = get_provider(target.provider).native_for(response)
286
+ if target.provider == "openai" and target.method == "parse":
287
+ native = _as_parsed_completion(native, state.request.native_extra)
288
+ return native
289
+
290
+
291
+ def _as_parsed_completion(native: Any, extra: dict[str, Any]) -> Any:
292
+ """The ``ParsedChatCompletion`` that ``chat.completions.parse`` promises its caller.
293
+
294
+ A live call already returns one. A cache hit, or a response synthesized after a
295
+ fallback, is a plain ``ChatCompletion``, so run it through the SDK's own parser
296
+ with the caller's ``response_format`` and ``tools``.
297
+ """
298
+ if type(native).__name__.startswith("Parsed"):
299
+ return native
300
+ try:
301
+ import openai
302
+ from openai.lib._parsing import parse_chat_completion
303
+ except ImportError:
304
+ return native
305
+ not_given: Any = getattr(openai, "omit", None) or getattr(openai, "NOT_GIVEN", None)
306
+ response_format: Any = extra.get("response_format", not_given)
307
+ input_tools: Any = extra.get("tools", not_given)
308
+ try:
309
+ return parse_chat_completion(
310
+ response_format=response_format,
311
+ input_tools=input_tools,
312
+ chat_completion=native,
313
+ )
314
+ except (TypeError, AttributeError): # an AttrDict without the SDK types
315
+ logger.debug("callm: could not rebuild a ParsedChatCompletion", exc_info=True)
316
+ return native
317
+
318
+
319
+ _warned_methods: set[str] = set()
320
+ _SUPPORTED_ALTERNATIVE = {
321
+ "anthropic": "messages.create",
322
+ "openai-responses": "responses.create",
323
+ }
324
+
325
+
326
+ def _unprotected_features(config: CallConfig) -> str:
327
+ features = []
328
+ if config.pii is not None:
329
+ features.append("PII masking")
330
+ if config.injection is not None:
331
+ features.append("injection checks")
332
+ if config.max_cost is not None or config.budgets:
333
+ features.append("budgets")
334
+ if config.cache is not None:
335
+ features.append("caching")
336
+ if config.output_schema is not None:
337
+ features.append("output validation")
338
+ if config.fallback:
339
+ features.append("fallback")
340
+ if config.retry.max_retries:
341
+ features.append("callm retries")
342
+ features.append("cost tracking and telemetry")
343
+ return ", ".join(features)
344
+
345
+
346
+ def _make_warning_wrapper(target: PatchTarget, original: Callable[..., Any]) -> Callable[..., Any]:
347
+ """Pass-through wrapper for SDK methods callm does not intercept yet.
348
+
349
+ Plain (non-async) on purpose: async originals return their coroutine or stream manager
350
+ unchanged, so behaviour and return types stay exactly those of the SDK.
351
+ """
352
+ name = f"{target.provider} {target.cls}.{target.method}"
353
+ label = (
354
+ "responses." + target.method
355
+ if "responses" in target.module
356
+ else ("messages." if target.provider == "anthropic" else "chat.completions.")
357
+ + target.method
358
+ )
359
+
360
+ @functools.wraps(original)
361
+ def wrapper(self: Any, *args: Any, **kwargs: Any) -> Any:
362
+ ctx = ACTIVE.get()
363
+ if ctx is not None and not BYPASS.get() and name not in _warned_methods:
364
+ _warned_methods.add(name)
365
+ logger.warning(
366
+ "callm: %s (%s) is not intercepted by callm yet, so inside %s it runs without %s. "
367
+ "Use %s for now: https://tanbirramim.github.io/callm/faq/",
368
+ label,
369
+ target.provider,
370
+ ctx.name or "@callm",
371
+ _unprotected_features(ctx.config),
372
+ _SUPPORTED_ALTERNATIVE.get(target.provider, "chat.completions.create"),
373
+ )
374
+ return original(self, *args, **kwargs)
375
+
376
+ wrapper.__callm_original__ = original # type: ignore[attr-defined]
377
+ return wrapper
242
378
 
243
379
 
244
380
  def _make_sync_wrapper(target: PatchTarget, original: Callable[..., Any]) -> Callable[..., Any]:
@@ -304,7 +440,10 @@ def _patch_module(module: ModuleType, provider: str) -> None:
304
440
  if existing is not None: # already instrumented (e.g. module reloaded)
305
441
  _patched[target] = existing
306
442
  continue
307
- make = _make_async_wrapper if target.is_async else _make_sync_wrapper
443
+ if target.warn_only:
444
+ make = _make_warning_wrapper
445
+ else:
446
+ make = _make_async_wrapper if target.is_async else _make_sync_wrapper
308
447
  setattr(cls, target.method, make(target, original))
309
448
  _patched[target] = original
310
449
  logger.debug("callm: instrumented %s.%s.%s", target.module, target.cls, target.method)
@@ -103,7 +103,7 @@ def _export_span(record: CallRecord, start_ns: int | None) -> None:
103
103
  )
104
104
  attributes: dict[str, Any] = {
105
105
  "gen_ai.operation.name": "chat",
106
- "gen_ai.system": record.provider,
106
+ "gen_ai.system": "openai" if record.provider == "openai-responses" else record.provider,
107
107
  "gen_ai.request.model": record.model,
108
108
  "gen_ai.usage.input_tokens": record.input_tokens
109
109
  + record.cache_read_tokens
@@ -161,8 +161,11 @@ def _lookup(table: dict[str, ModelPrice], name: str) -> ModelPrice | None:
161
161
  return table[best] if best is not None else None
162
162
 
163
163
 
164
+ _PRICE_ALIASES = {"gemini": "google", "openai-responses": "openai"}
165
+
166
+
164
167
  def _provider_key(provider: str) -> str:
165
- return "google" if provider == "gemini" else provider
168
+ return _PRICE_ALIASES.get(provider, provider)
166
169
 
167
170
 
168
171
  def get_price(provider: str, model: str) -> ModelPrice | None:
@@ -235,7 +238,7 @@ def estimate_tokens(text: str, model: str | None = None, provider: str | None =
235
238
  """Estimate a token count. Uses ``tiktoken`` for OpenAI models when it is installed."""
236
239
  if not text:
237
240
  return 0
238
- if provider in (None, "openai") and model is not None:
241
+ if provider in (None, "openai", "openai-responses") and model is not None:
239
242
  encoder = _tiktoken_encoder(model)
240
243
  if encoder is not None:
241
244
  return len(encoder.encode(text, disallowed_special=()))
@@ -4,12 +4,14 @@ from callm.providers.anthropic import AnthropicProvider
4
4
  from callm.providers.base import Provider
5
5
  from callm.providers.google import GoogleProvider
6
6
  from callm.providers.openai import OpenAIProvider
7
+ from callm.providers.openai_responses import OpenAIResponsesProvider
7
8
  from callm.providers.registry import get_provider, parse_target, provider_names, register_provider
8
9
 
9
10
  __all__ = [
10
11
  "AnthropicProvider",
11
12
  "GoogleProvider",
12
13
  "OpenAIProvider",
14
+ "OpenAIResponsesProvider",
13
15
  "Provider",
14
16
  "get_provider",
15
17
  "parse_target",
@@ -61,7 +61,14 @@ def dump_json(obj: Any) -> dict[str, Any] | None:
61
61
  dump = getattr(obj, "model_dump", None)
62
62
  if callable(dump):
63
63
  try:
64
- result = dump(mode="json", exclude_none=True)
64
+ # warnings=False: ParsedChatCompletion types `parsed` loosely, so pydantic
65
+ # would warn on every call even though the dump is correct.
66
+ result = dump(mode="json", exclude_none=True, warnings=False)
67
+ except TypeError: # an object whose model_dump has no `warnings` option
68
+ try:
69
+ result = dump(mode="json", exclude_none=True)
70
+ except Exception:
71
+ return None
65
72
  except Exception:
66
73
  return None
67
74
  return result if isinstance(result, dict) else None