activeagent 1.0.0.rc1 → 1.0.1

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 (191) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +102 -1
  3. data/lib/active_agent/providers/_base_provider.rb +94 -82
  4. data/lib/active_agent/providers/anthropic/_types.rb +2 -2
  5. data/lib/active_agent/providers/anthropic/options.rb +4 -6
  6. data/lib/active_agent/providers/anthropic/request.rb +157 -78
  7. data/lib/active_agent/providers/anthropic/transforms.rb +482 -0
  8. data/lib/active_agent/providers/anthropic_provider.rb +159 -59
  9. data/lib/active_agent/providers/common/messages/_types.rb +46 -3
  10. data/lib/active_agent/providers/common/messages/assistant.rb +20 -4
  11. data/lib/active_agent/providers/common/responses/base.rb +118 -70
  12. data/lib/active_agent/providers/common/usage.rb +385 -0
  13. data/lib/active_agent/providers/concerns/instrumentation.rb +263 -0
  14. data/lib/active_agent/providers/concerns/previewable.rb +39 -5
  15. data/lib/active_agent/providers/concerns/tool_choice_clearing.rb +62 -0
  16. data/lib/active_agent/providers/log_subscriber.rb +64 -246
  17. data/lib/active_agent/providers/mock_provider.rb +23 -23
  18. data/lib/active_agent/providers/ollama/chat/request.rb +214 -35
  19. data/lib/active_agent/providers/ollama/chat/transforms.rb +135 -0
  20. data/lib/active_agent/providers/ollama/embedding/request.rb +160 -47
  21. data/lib/active_agent/providers/ollama/embedding/transforms.rb +160 -0
  22. data/lib/active_agent/providers/ollama_provider.rb +0 -1
  23. data/lib/active_agent/providers/open_ai/_base.rb +3 -2
  24. data/lib/active_agent/providers/open_ai/chat/_types.rb +13 -1
  25. data/lib/active_agent/providers/open_ai/chat/request.rb +132 -186
  26. data/lib/active_agent/providers/open_ai/chat/transforms.rb +444 -0
  27. data/lib/active_agent/providers/open_ai/chat_provider.rb +95 -36
  28. data/lib/active_agent/providers/open_ai/embedding/_types.rb +13 -2
  29. data/lib/active_agent/providers/open_ai/embedding/request.rb +38 -70
  30. data/lib/active_agent/providers/open_ai/embedding/transforms.rb +88 -0
  31. data/lib/active_agent/providers/open_ai/responses/_types.rb +1 -7
  32. data/lib/active_agent/providers/open_ai/responses/request.rb +116 -135
  33. data/lib/active_agent/providers/open_ai/responses/transforms.rb +363 -0
  34. data/lib/active_agent/providers/open_ai/responses_provider.rb +115 -30
  35. data/lib/active_agent/providers/open_ai_provider.rb +0 -3
  36. data/lib/active_agent/providers/open_router/_types.rb +27 -1
  37. data/lib/active_agent/providers/open_router/options.rb +49 -1
  38. data/lib/active_agent/providers/open_router/request.rb +252 -66
  39. data/lib/active_agent/providers/open_router/requests/_types.rb +0 -1
  40. data/lib/active_agent/providers/open_router/requests/messages/_types.rb +37 -40
  41. data/lib/active_agent/providers/open_router/requests/messages/content/file.rb +19 -3
  42. data/lib/active_agent/providers/open_router/requests/messages/content/files/details.rb +15 -4
  43. data/lib/active_agent/providers/open_router/requests/plugin.rb +19 -3
  44. data/lib/active_agent/providers/open_router/requests/plugins/pdf_config.rb +30 -8
  45. data/lib/active_agent/providers/open_router/requests/prediction.rb +17 -0
  46. data/lib/active_agent/providers/open_router/requests/provider_preferences/max_price.rb +41 -7
  47. data/lib/active_agent/providers/open_router/requests/provider_preferences.rb +60 -19
  48. data/lib/active_agent/providers/open_router/requests/response_format.rb +30 -2
  49. data/lib/active_agent/providers/open_router/transforms.rb +164 -0
  50. data/lib/active_agent/providers/open_router_provider.rb +23 -0
  51. data/lib/active_agent/version.rb +1 -1
  52. metadata +17 -160
  53. data/lib/active_agent/generation_provider/open_router/types.rb +0 -505
  54. data/lib/active_agent/generation_provider/xai_provider.rb +0 -144
  55. data/lib/active_agent/providers/anthropic/requests/_types.rb +0 -190
  56. data/lib/active_agent/providers/anthropic/requests/container_params.rb +0 -19
  57. data/lib/active_agent/providers/anthropic/requests/content/base.rb +0 -21
  58. data/lib/active_agent/providers/anthropic/requests/content/sources/base.rb +0 -22
  59. data/lib/active_agent/providers/anthropic/requests/context_management_config.rb +0 -18
  60. data/lib/active_agent/providers/anthropic/requests/messages/_types.rb +0 -189
  61. data/lib/active_agent/providers/anthropic/requests/messages/assistant.rb +0 -23
  62. data/lib/active_agent/providers/anthropic/requests/messages/base.rb +0 -63
  63. data/lib/active_agent/providers/anthropic/requests/messages/content/_types.rb +0 -143
  64. data/lib/active_agent/providers/anthropic/requests/messages/content/base.rb +0 -21
  65. data/lib/active_agent/providers/anthropic/requests/messages/content/document.rb +0 -26
  66. data/lib/active_agent/providers/anthropic/requests/messages/content/image.rb +0 -23
  67. data/lib/active_agent/providers/anthropic/requests/messages/content/redacted_thinking.rb +0 -21
  68. data/lib/active_agent/providers/anthropic/requests/messages/content/search_result.rb +0 -27
  69. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/_types.rb +0 -171
  70. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/base.rb +0 -22
  71. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/document_base64.rb +0 -25
  72. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/document_file.rb +0 -23
  73. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/document_text.rb +0 -25
  74. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/document_url.rb +0 -23
  75. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/image_base64.rb +0 -27
  76. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/image_file.rb +0 -23
  77. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/image_url.rb +0 -23
  78. data/lib/active_agent/providers/anthropic/requests/messages/content/text.rb +0 -22
  79. data/lib/active_agent/providers/anthropic/requests/messages/content/thinking.rb +0 -23
  80. data/lib/active_agent/providers/anthropic/requests/messages/content/tool_result.rb +0 -24
  81. data/lib/active_agent/providers/anthropic/requests/messages/content/tool_use.rb +0 -28
  82. data/lib/active_agent/providers/anthropic/requests/messages/user.rb +0 -21
  83. data/lib/active_agent/providers/anthropic/requests/metadata.rb +0 -18
  84. data/lib/active_agent/providers/anthropic/requests/response_format.rb +0 -22
  85. data/lib/active_agent/providers/anthropic/requests/thinking_config/_types.rb +0 -60
  86. data/lib/active_agent/providers/anthropic/requests/thinking_config/base.rb +0 -20
  87. data/lib/active_agent/providers/anthropic/requests/thinking_config/disabled.rb +0 -16
  88. data/lib/active_agent/providers/anthropic/requests/thinking_config/enabled.rb +0 -20
  89. data/lib/active_agent/providers/anthropic/requests/tool_choice/_types.rb +0 -78
  90. data/lib/active_agent/providers/anthropic/requests/tool_choice/any.rb +0 -17
  91. data/lib/active_agent/providers/anthropic/requests/tool_choice/auto.rb +0 -17
  92. data/lib/active_agent/providers/anthropic/requests/tool_choice/base.rb +0 -20
  93. data/lib/active_agent/providers/anthropic/requests/tool_choice/none.rb +0 -16
  94. data/lib/active_agent/providers/anthropic/requests/tool_choice/tool.rb +0 -20
  95. data/lib/active_agent/providers/ollama/chat/requests/_types.rb +0 -3
  96. data/lib/active_agent/providers/ollama/chat/requests/messages/_types.rb +0 -116
  97. data/lib/active_agent/providers/ollama/chat/requests/messages/assistant.rb +0 -19
  98. data/lib/active_agent/providers/ollama/chat/requests/messages/user.rb +0 -19
  99. data/lib/active_agent/providers/ollama/embedding/requests/_types.rb +0 -83
  100. data/lib/active_agent/providers/ollama/embedding/requests/options.rb +0 -104
  101. data/lib/active_agent/providers/open_ai/chat/requests/_types.rb +0 -229
  102. data/lib/active_agent/providers/open_ai/chat/requests/audio.rb +0 -24
  103. data/lib/active_agent/providers/open_ai/chat/requests/messages/_types.rb +0 -123
  104. data/lib/active_agent/providers/open_ai/chat/requests/messages/assistant.rb +0 -42
  105. data/lib/active_agent/providers/open_ai/chat/requests/messages/base.rb +0 -78
  106. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/_types.rb +0 -133
  107. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/audio.rb +0 -35
  108. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/base.rb +0 -24
  109. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/file.rb +0 -26
  110. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/files/_types.rb +0 -60
  111. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/files/details.rb +0 -41
  112. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/image.rb +0 -37
  113. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/refusal.rb +0 -25
  114. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/text.rb +0 -25
  115. data/lib/active_agent/providers/open_ai/chat/requests/messages/developer.rb +0 -25
  116. data/lib/active_agent/providers/open_ai/chat/requests/messages/function.rb +0 -25
  117. data/lib/active_agent/providers/open_ai/chat/requests/messages/system.rb +0 -25
  118. data/lib/active_agent/providers/open_ai/chat/requests/messages/tool.rb +0 -26
  119. data/lib/active_agent/providers/open_ai/chat/requests/messages/user.rb +0 -32
  120. data/lib/active_agent/providers/open_ai/chat/requests/prediction.rb +0 -46
  121. data/lib/active_agent/providers/open_ai/chat/requests/response_format.rb +0 -53
  122. data/lib/active_agent/providers/open_ai/chat/requests/stream_options.rb +0 -24
  123. data/lib/active_agent/providers/open_ai/chat/requests/tool_choice.rb +0 -26
  124. data/lib/active_agent/providers/open_ai/chat/requests/tools/_types.rb +0 -5
  125. data/lib/active_agent/providers/open_ai/chat/requests/tools/base.rb +0 -22
  126. data/lib/active_agent/providers/open_ai/chat/requests/tools/custom_tool.rb +0 -41
  127. data/lib/active_agent/providers/open_ai/chat/requests/tools/function_tool.rb +0 -51
  128. data/lib/active_agent/providers/open_ai/chat/requests/web_search_options.rb +0 -45
  129. data/lib/active_agent/providers/open_ai/embedding/requests/_types.rb +0 -49
  130. data/lib/active_agent/providers/open_ai/responses/requests/_types.rb +0 -231
  131. data/lib/active_agent/providers/open_ai/responses/requests/conversation.rb +0 -23
  132. data/lib/active_agent/providers/open_ai/responses/requests/inputs/_types.rb +0 -264
  133. data/lib/active_agent/providers/open_ai/responses/requests/inputs/assistant_message.rb +0 -22
  134. data/lib/active_agent/providers/open_ai/responses/requests/inputs/base.rb +0 -89
  135. data/lib/active_agent/providers/open_ai/responses/requests/inputs/code_interpreter_tool_call.rb +0 -30
  136. data/lib/active_agent/providers/open_ai/responses/requests/inputs/computer_tool_call.rb +0 -28
  137. data/lib/active_agent/providers/open_ai/responses/requests/inputs/computer_tool_call_output.rb +0 -33
  138. data/lib/active_agent/providers/open_ai/responses/requests/inputs/content/_types.rb +0 -207
  139. data/lib/active_agent/providers/open_ai/responses/requests/inputs/content/base.rb +0 -22
  140. data/lib/active_agent/providers/open_ai/responses/requests/inputs/content/input_audio.rb +0 -26
  141. data/lib/active_agent/providers/open_ai/responses/requests/inputs/content/input_file.rb +0 -28
  142. data/lib/active_agent/providers/open_ai/responses/requests/inputs/content/input_image.rb +0 -28
  143. data/lib/active_agent/providers/open_ai/responses/requests/inputs/content/input_text.rb +0 -25
  144. data/lib/active_agent/providers/open_ai/responses/requests/inputs/custom_tool_call.rb +0 -28
  145. data/lib/active_agent/providers/open_ai/responses/requests/inputs/custom_tool_call_output.rb +0 -27
  146. data/lib/active_agent/providers/open_ai/responses/requests/inputs/developer_message.rb +0 -20
  147. data/lib/active_agent/providers/open_ai/responses/requests/inputs/file_search_tool_call.rb +0 -25
  148. data/lib/active_agent/providers/open_ai/responses/requests/inputs/function_call_output.rb +0 -32
  149. data/lib/active_agent/providers/open_ai/responses/requests/inputs/function_tool_call.rb +0 -28
  150. data/lib/active_agent/providers/open_ai/responses/requests/inputs/image_gen_tool_call.rb +0 -27
  151. data/lib/active_agent/providers/open_ai/responses/requests/inputs/input_message.rb +0 -31
  152. data/lib/active_agent/providers/open_ai/responses/requests/inputs/item_reference.rb +0 -23
  153. data/lib/active_agent/providers/open_ai/responses/requests/inputs/local_shell_tool_call.rb +0 -26
  154. data/lib/active_agent/providers/open_ai/responses/requests/inputs/local_shell_tool_call_output.rb +0 -33
  155. data/lib/active_agent/providers/open_ai/responses/requests/inputs/mcp_approval_request.rb +0 -30
  156. data/lib/active_agent/providers/open_ai/responses/requests/inputs/mcp_approval_response.rb +0 -28
  157. data/lib/active_agent/providers/open_ai/responses/requests/inputs/mcp_list_tools.rb +0 -29
  158. data/lib/active_agent/providers/open_ai/responses/requests/inputs/mcp_tool_call.rb +0 -35
  159. data/lib/active_agent/providers/open_ai/responses/requests/inputs/output_message.rb +0 -35
  160. data/lib/active_agent/providers/open_ai/responses/requests/inputs/reasoning.rb +0 -33
  161. data/lib/active_agent/providers/open_ai/responses/requests/inputs/system_message.rb +0 -20
  162. data/lib/active_agent/providers/open_ai/responses/requests/inputs/tool_call_base.rb +0 -27
  163. data/lib/active_agent/providers/open_ai/responses/requests/inputs/tool_message.rb +0 -23
  164. data/lib/active_agent/providers/open_ai/responses/requests/inputs/user_message.rb +0 -20
  165. data/lib/active_agent/providers/open_ai/responses/requests/inputs/web_search_tool_call.rb +0 -24
  166. data/lib/active_agent/providers/open_ai/responses/requests/prompt_reference.rb +0 -23
  167. data/lib/active_agent/providers/open_ai/responses/requests/reasoning.rb +0 -23
  168. data/lib/active_agent/providers/open_ai/responses/requests/stream_options.rb +0 -20
  169. data/lib/active_agent/providers/open_ai/responses/requests/text/_types.rb +0 -89
  170. data/lib/active_agent/providers/open_ai/responses/requests/text/base.rb +0 -22
  171. data/lib/active_agent/providers/open_ai/responses/requests/text/json_object.rb +0 -20
  172. data/lib/active_agent/providers/open_ai/responses/requests/text/json_schema.rb +0 -48
  173. data/lib/active_agent/providers/open_ai/responses/requests/text/plain.rb +0 -20
  174. data/lib/active_agent/providers/open_ai/responses/requests/text.rb +0 -41
  175. data/lib/active_agent/providers/open_ai/responses/requests/tool_choice.rb +0 -26
  176. data/lib/active_agent/providers/open_ai/responses/requests/tools/_types.rb +0 -112
  177. data/lib/active_agent/providers/open_ai/responses/requests/tools/base.rb +0 -25
  178. data/lib/active_agent/providers/open_ai/responses/requests/tools/code_interpreter_tool.rb +0 -23
  179. data/lib/active_agent/providers/open_ai/responses/requests/tools/computer_tool.rb +0 -27
  180. data/lib/active_agent/providers/open_ai/responses/requests/tools/custom_tool.rb +0 -28
  181. data/lib/active_agent/providers/open_ai/responses/requests/tools/file_search_tool.rb +0 -27
  182. data/lib/active_agent/providers/open_ai/responses/requests/tools/function_tool.rb +0 -29
  183. data/lib/active_agent/providers/open_ai/responses/requests/tools/image_generation_tool.rb +0 -37
  184. data/lib/active_agent/providers/open_ai/responses/requests/tools/local_shell_tool.rb +0 -21
  185. data/lib/active_agent/providers/open_ai/responses/requests/tools/mcp_tool.rb +0 -41
  186. data/lib/active_agent/providers/open_ai/responses/requests/tools/web_search_preview_tool.rb +0 -24
  187. data/lib/active_agent/providers/open_ai/responses/requests/tools/web_search_tool.rb +0 -25
  188. data/lib/active_agent/providers/open_ai/schema.yml +0 -65937
  189. data/lib/active_agent/providers/open_router/requests/message.rb +0 -1
  190. data/lib/active_agent/providers/open_router/requests/messages/assistant.rb +0 -20
  191. data/lib/active_agent/providers/open_router/requests/messages/user.rb +0 -30
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3a2f8f345b5f6a3f9d2717f24542a0dd0a75ae22a7b3601ebe77b9ff8ba3bbc1
4
- data.tar.gz: efe4dc5f9c4d10e4f1454bf274080d42cd88c5af5488195fa98cafd5cbadb812
3
+ metadata.gz: 6ee3a092c5c836febf3c2e3045a1c1b2cd448edc18f3e76e4ee1ebc181f895b8
4
+ data.tar.gz: 9850ab912eedaac0a57f9a954648d096d6aa70ad470ff9aa7c84a18183ab76be
5
5
  SHA512:
6
- metadata.gz: ab6e83518446995be3984dc347524314d831de5650c263e0338f2cff4382cd111196145f2155b206fe651d930054ac3cf166b05cee9aa89fe907fbc4c7d7ed5f
7
- data.tar.gz: bbff91deb2aba1feccb49edbf71664c3569595357fc7d3c1aea9555d95f07c2edb102f24927fe2132eb35ddc5bc2a8b8d0f52eb1b0f40ca9ca308b59263b68a6
6
+ metadata.gz: 199061594f115a823037504f84daf5ebc79b084c8a835b3e67f5427a19ffe33bbe208bb627dc26b1abf977c4d35a2977defcfb59e9128e60d94c6a3dc0ce5259
7
+ data.tar.gz: 661bf403014e2a0d1156614249b9fcdc352b8124e63457071ce75399ac69d089cf2d43d8b571e41614d811a5e6dc4ebc9072dfe38d912bc122672bf9823bb331
data/CHANGELOG.md CHANGED
@@ -5,11 +5,82 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [1.0.0] - Unreleased
8
+ ## [1.0.0] - 2025-11-21
9
9
 
10
10
  Major refactor with breaking changes. Complete provider rewrite. New modular architecture.
11
11
 
12
12
  **Requirements:** Ruby 3.1+, Rails 7.0+/8.0+/8.1+
13
+ ## What's Changed
14
+ * Major Framework Refactor: ActiveAgent v1.0.0 by @sirwolfgang in https://github.com/activeagents/activeagent/pull/259
15
+ * Add API gem version testing and fix Anthropic 1.14.0 compatibility by @sirwolfgang in https://github.com/activeagents/activeagent/pull/265
16
+ * Fix version compatiblity issue for vitepress by @sirwolfgang in https://github.com/activeagents/activeagent/pull/266
17
+ * Add missing API Keys by @sirwolfgang in https://github.com/activeagents/activeagent/pull/267
18
+ * Fix website links by @sirwolfgang in https://github.com/activeagents/activeagent/pull/268
19
+ * chore: remove `standard` from dev dependencies by @okuramasafumi in https://github.com/activeagents/activeagent/pull/272
20
+ * Add thread safety tests by @sirwolfgang in https://github.com/activeagents/activeagent/pull/275
21
+ * Refactor: Leverage Native Gem Types Across All Providers by @sirwolfgang in https://github.com/activeagents/activeagent/pull/271
22
+ * Improved Usage Tracking by @sirwolfgang in https://github.com/activeagents/activeagent/pull/274
23
+
24
+ ## New Contributors
25
+ * @okuramasafumi made their first contribution in https://github.com/activeagents/activeagent/pull/272
26
+
27
+ **Full Changelog**: https://github.com/activeagents/activeagent/compare/v0.6.3...v1.0.0
28
+
29
+ ### Added
30
+
31
+ **Universal Tools Format**
32
+ ```ruby
33
+ # Single format works across all providers (Anthropic, OpenAI, OpenRouter, Ollama, Mock)
34
+ tools: [{
35
+ name: "get_weather",
36
+ description: "Get current weather",
37
+ parameters: {
38
+ type: "object",
39
+ properties: {
40
+ location: { type: "string", description: "City and state" }
41
+ },
42
+ required: ["location"]
43
+ }
44
+ }]
45
+
46
+ # Tool choice normalization
47
+ tool_choice: "auto" # Let model decide
48
+ tool_choice: "required" # Force tool use
49
+ tool_choice: { name: "get_weather" } # Force specific tool
50
+ ```
51
+
52
+ Automatic conversion to provider-specific formats. Old formats still work (backward compatible).
53
+
54
+ **Model Context Protocol (MCP) Support**
55
+ ```ruby
56
+ # Universal MCP format works across providers (Anthropic, OpenAI)
57
+ class MyAgent < ActiveAgent::Base
58
+ generate_with :anthropic, model: "claude-haiku-4-5"
59
+
60
+ def research
61
+ prompt(
62
+ message: "Research AI developments",
63
+ mcps: [{
64
+ name: "github",
65
+ url: "https://api.githubcopilot.com/mcp/",
66
+ authorization: ENV["GITHUB_MCP_TOKEN"]
67
+ }]
68
+ )
69
+ end
70
+ end
71
+ ```
72
+
73
+ - Common format: `{name: "server", url: "https://...", authorization: "token"}`
74
+ - Auto-converts to provider native formats
75
+ - Anthropic: Beta API support, up to 20 servers per request
76
+ - OpenAI: Responses API with pre-built connectors (Dropbox, Google Drive, etc.)
77
+ - Backwards compatible: accepts both `mcps` and `mcp_servers` parameters
78
+ - Comprehensive documentation with tested examples
79
+ - Full VCR test coverage with real MCP endpoints
80
+
81
+ ### Changed
82
+
83
+ - Shared `ToolChoiceClearing` concern eliminates duplication across providers
13
84
 
14
85
  ### Breaking Changes
15
86
 
@@ -148,6 +219,35 @@ response = MyAgent.embed(inputs: ["Text 1", "Text 2"]).embed_now
148
219
  vectors = response.data.map { |d| d[:embedding] }
149
220
  ```
150
221
 
222
+ **Normalized Usage Statistics**
223
+ ```ruby
224
+ response = MyAgent.prompt("Hello").generate_now
225
+
226
+ # Works across all providers
227
+ response.usage.input_tokens
228
+ response.usage.output_tokens
229
+ response.usage.total_tokens
230
+
231
+ # Provider-specific fields when available
232
+ response.usage.cached_tokens # OpenAI, Anthropic
233
+ response.usage.reasoning_tokens # OpenAI o1 models
234
+ response.usage.service_tier # Anthropic
235
+ ```
236
+
237
+ **Enhanced Instrumentation for APM Integration**
238
+ - Unified event structure: `prompt.active_agent` and `embed.active_agent` (top-level) plus `prompt.provider.active_agent` and `embed.provider.active_agent` (per-API-call)
239
+ - Event payloads include comprehensive data for monitoring tools (New Relic, DataDog, etc.):
240
+ - Request parameters: `model`, `temperature`, `max_tokens`, `top_p`, `stream`, `message_count`, `has_tools`
241
+ - Usage data: `input_tokens`, `output_tokens`, `total_tokens`, `cached_tokens`, `reasoning_tokens`, `audio_tokens`, `cache_creation_tokens` (critical for cost tracking)
242
+ - Response metadata: `finish_reason`, `response_model`, `response_id`, `embedding_count`
243
+ - Top-level events report cumulative usage across all API calls in multi-turn conversations
244
+ - Provider-level events report per-call usage for granular tracking
245
+
246
+ **Multi-Turn Usage Tracking**
247
+ - `response.usage` now returns cumulative token counts across all API calls during tool calling
248
+ - New `response.usages` array contains individual usage objects from each API call
249
+ - `Usage` objects support addition: `usage1 + usage2` for combining statistics
250
+
151
251
  **Provider Enhancements**
152
252
  - OpenAI Responses API: `api: :responses` or `api: :chat`
153
253
  - Anthropic JSON object mode with automatic extraction
@@ -195,6 +295,7 @@ vectors = response.data.map { |d| d[:embedding] }
195
295
  - Template rendering without blocks
196
296
  - Schema generator key symbolization
197
297
  - Rails 8.0 and 8.1 compatibility
298
+ - Usage extraction across OpenAI/Anthropic response formats
198
299
 
199
300
  ### Removed
200
301
 
@@ -2,21 +2,22 @@ require "active_support/delegation"
2
2
 
3
3
  require_relative "common/response"
4
4
  require_relative "concerns/exception_handler"
5
+ require_relative "concerns/instrumentation"
5
6
  require_relative "concerns/previewable"
7
+ require_relative "concerns/tool_choice_clearing"
6
8
 
7
- # Maps provider types to their gem dependencies.
8
9
  # @private
9
10
  GEM_LOADERS = {
10
11
  anthropic: [ "anthropic", "~> 1.12", "anthropic" ],
11
12
  openai: [ "openai", "~> 0.34", "openai" ]
12
13
  }
13
14
 
14
- # Loads and requires a provider's gem dependency.
15
+ # Requires a provider's gem dependency.
15
16
  #
16
17
  # @param type [Symbol] provider type (:anthropic, :openai)
17
- # @param file_name [String] provider file path for error context
18
+ # @param file_name [String] for error context
18
19
  # @return [void]
19
- # @raise [LoadError] when the required gem is not available
20
+ # @raise [LoadError] when required gem is not installed
20
21
  def require_gem!(type, file_name)
21
22
  gem_name, requirement, package_name = GEM_LOADERS.fetch(type)
22
23
  provider_name = file_name.split("/").last.delete_suffix(".rb").camelize
@@ -31,9 +32,8 @@ end
31
32
 
32
33
  module ActiveAgent
33
34
  module Providers
34
- # Base class for LLM provider integrations.
35
+ # Orchestrates LLM provider API requests, streaming, and multi-turn tool calling.
35
36
  #
36
- # Orchestrates API requests, streaming responses, and multi-turn tool calling.
37
37
  # Each provider (OpenAI, Anthropic, etc.) subclasses this to implement
38
38
  # provider-specific API interactions.
39
39
  #
@@ -44,41 +44,44 @@ module ActiveAgent
44
44
  extend ActiveSupport::Delegation
45
45
 
46
46
  include ExceptionHandler
47
+ include Instrumentation
47
48
  include Previewable
49
+ include ToolChoiceClearing
48
50
 
49
51
  class ProvidersError < StandardError; end
50
52
 
51
53
  attr_internal :options, :context, :trace_id, # Setup
52
54
  :request, :message_stack, # Runtime
53
55
  :stream_broadcaster, :streaming, # Callback (Streams)
54
- :tools_function # Callback (Tools)
56
+ :tools_function, # Callback (Tools)
57
+ :usage_stack # Usage Tracking
55
58
 
56
- # @return [String] provider name extracted from class name (e.g., "Anthropic", "OpenAI")
59
+ # @return [String] e.g., "Anthropic", "OpenAI"
57
60
  def self.service_name
58
61
  name.split("::").last.delete_suffix("Provider")
59
62
  end
60
63
 
61
- # @return [String] module-qualified provider name (e.g., "Anthropic", "OpenAI::Chat")
64
+ # @return [String] e.g., "Anthropic", "OpenAI::Chat"
62
65
  def self.tag_name
63
66
  name.delete_prefix("ActiveAgent::Providers::").delete_suffix("Provider")
64
67
  end
65
68
 
66
- # @return [Module] provider's namespace module (e.g., ActiveAgent::Providers::OpenAI)
69
+ # @return [Module] e.g., ActiveAgent::Providers::OpenAI
67
70
  def self.namespace
68
71
  "#{name.deconstantize}::#{service_name}".safe_constantize
69
72
  end
70
73
 
71
- # @return [Class] provider's options class
74
+ # @return [Class]
72
75
  def self.options_klass
73
76
  namespace::Options
74
77
  end
75
78
 
76
- # @return [ActiveModel::Type::Value] provider-specific request type for prompt casting/serialization
79
+ # @return [ActiveModel::Type::Value] for prompt casting/serialization
77
80
  def self.prompt_request_type
78
81
  namespace::RequestType.new
79
82
  end
80
83
 
81
- # @return [ActiveModel::Type::Value] provider-specific request type for embedding casting/serialization
84
+ # @return [ActiveModel::Type::Value] for embedding casting/serialization
82
85
  # @raise [NotImplementedError] when provider doesn't support embeddings
83
86
  def self.embed_request_type
84
87
  fail(NotImplementedError)
@@ -86,12 +89,10 @@ module ActiveAgent
86
89
 
87
90
  delegate :service_name, :tag_name, :namespace, :options_klass, :prompt_request_type, :embed_request_type, to: :class
88
91
 
89
- # Initializes a provider instance.
90
- #
91
92
  # @param kwargs [Hash] configuration and callbacks
92
93
  # @option kwargs [Symbol] :service validates against provider's service name
93
- # @option kwargs [Proc] :stream_broadcaster callback for streaming events (:open, :update, :close)
94
- # @option kwargs [Proc] :tools_function callback to execute tool/function calls
94
+ # @option kwargs [Proc] :stream_broadcaster for streaming events (:open, :update, :close)
95
+ # @option kwargs [Proc] :tools_function to execute tool/function calls
95
96
  # @raise [RuntimeError] when service name doesn't match provider
96
97
  def initialize(kwargs = {})
97
98
  assert_service!(kwargs.delete(:service))
@@ -107,21 +108,10 @@ module ActiveAgent
107
108
  self.options = options_klass.new(kwargs.extract!(*options_klass.keys))
108
109
  self.context = kwargs
109
110
  self.message_stack = []
111
+ self.usage_stack = []
110
112
  end
111
113
 
112
- # Executes a prompt request with error handling and instrumentation.
113
- #
114
- # @return [ActiveAgent::Providers::Common::PromptResponse]
115
- def prompt
116
- instrument("prompt_start.provider.active_agent") do
117
- self.request = prompt_request_type.cast(context.except(:trace_id))
118
- resolve_prompt
119
- end
120
- end
121
-
122
- # Generates a preview of the prompt without executing the API call.
123
- #
124
- # Casts context into a request object and renders it as markdown for inspection.
114
+ # Generates prompt preview without executing the API call.
125
115
  #
126
116
  # @return [String] markdown-formatted preview
127
117
  def preview
@@ -129,15 +119,31 @@ module ActiveAgent
129
119
  preview_prompt
130
120
  end
131
121
 
132
- # Executes an embedding request with error handling and instrumentation.
122
+ # Executes prompt request with error handling and instrumentation.
133
123
  #
134
- # Converts text into vector representations for semantic search and similarity operations.
124
+ # @return [ActiveAgent::Providers::Common::PromptResponse]
125
+ def prompt
126
+ self.request = prompt_request_type.cast(context.except(:trace_id))
127
+
128
+ instrument("prompt.active_agent") do |payload|
129
+ response = resolve_prompt
130
+ instrumentation_prompt_payload(payload, request, response)
131
+
132
+ response
133
+ end
134
+ end
135
+
136
+ # Executes embedding request with error handling and instrumentation.
135
137
  #
136
138
  # @return [ActiveAgent::Providers::Common::EmbedResponse]
137
139
  def embed
138
- instrument("embed_start.provider.active_agent") do
139
- self.request = embed_request_type.cast(context.except(:trace_id))
140
- resolve_embed
140
+ self.request = embed_request_type.cast(context.except(:trace_id))
141
+
142
+ instrument("embed.active_agent") do |payload|
143
+ response = resolve_embed
144
+ instrumentation_embed_payload(payload, request, response)
145
+
146
+ response
141
147
  end
142
148
  end
143
149
 
@@ -149,8 +155,6 @@ module ActiveAgent
149
155
  fail "Unexpected Service Name: #{name} != #{service_name}" if name && name != service_name
150
156
  end
151
157
 
152
- # Instruments an event for logging and metrics.
153
- #
154
158
  # @param name [String]
155
159
  # @param payload [Hash]
156
160
  # @yield block to instrument
@@ -160,34 +164,42 @@ module ActiveAgent
160
164
  ActiveSupport::Notifications.instrument(name, full_payload, &block)
161
165
  end
162
166
 
163
- # Orchestrates the complete prompt request lifecycle.
167
+ # Orchestrates complete prompt request lifecycle.
164
168
  #
165
- # Prepares request, executes API call, processes response, and handles
166
- # recursive tool/function calling until completion.
169
+ # Handles recursive tool/function calling until completion.
167
170
  #
168
171
  # @return [ActiveAgent::Providers::Common::PromptResponse]
169
172
  def resolve_prompt
170
- request = prepare_prompt_request
171
-
172
- instrument("request_prepared.provider.active_agent", message_count: request.messages.size)
173
-
174
- # @todo Validate Request
175
- api_parameters = api_request_build(request, prompt_request_type)
176
- api_response = instrument("api_call.provider.active_agent", streaming: api_parameters[:stream].present?) do
177
- with_exception_handling { api_prompt_execute(api_parameters) }
173
+ api_parameters = api_request_build(prepare_prompt_request, prompt_request_type)
174
+ api_response = instrument("prompt.provider.active_agent") do |payload|
175
+ raw_response = with_exception_handling { api_prompt_execute(api_parameters) }
176
+
177
+ # Instrumentation Context Building
178
+ # Normalize response for instrumentation (providers may return gem objects)
179
+ normalized_response = api_response_normalize(raw_response)
180
+ common_response = Common::PromptResponse.new(raw_response: normalized_response)
181
+ instrumentation_prompt_payload(payload, self.request, common_response)
182
+ usage_stack.push(common_response.usage) if common_response&.usage
183
+
184
+ raw_response
178
185
  end
179
186
 
180
- process_prompt_finished(api_response.as_json&.deep_symbolize_keys)
187
+ process_prompt_finished(api_response)
181
188
  end
182
189
 
183
- # Orchestrates the complete embedding request lifecycle.
190
+ # Orchestrates complete embedding request lifecycle.
184
191
  #
185
192
  # @return [ActiveAgent::Providers::Common::EmbedResponse]
186
193
  def resolve_embed
187
- # @todo Validate Request
188
- api_parameters = api_request_build(request, embed_request_type)
189
- api_response = instrument("embed_call.provider.active_agent") do
190
- with_exception_handling { api_embed_execute(api_parameters) }
194
+ api_parameters = api_request_build(self.request, embed_request_type)
195
+ api_response = instrument("embed.provider.active_agent") do |payload|
196
+ raw_response = with_exception_handling { api_embed_execute(api_parameters) }
197
+
198
+ # Instrumentation Context Building
199
+ common_response = Common::EmbedResponse.new(raw_response:)
200
+ instrumentation_embed_payload(payload, self.request, common_response)
201
+
202
+ raw_response
191
203
  end
192
204
 
193
205
  process_embed_finished(api_response)
@@ -195,7 +207,7 @@ module ActiveAgent
195
207
 
196
208
  # Prepares request for next iteration in multi-turn conversation.
197
209
  #
198
- # Appends accumulated messages from message stack and resets buffer for next cycle.
210
+ # Appends accumulated messages and resets buffer for next cycle.
199
211
  #
200
212
  # @return [Request]
201
213
  def prepare_prompt_request
@@ -205,11 +217,9 @@ module ActiveAgent
205
217
  self.request
206
218
  end
207
219
 
208
- # Builds API request parameters from request object.
209
- #
210
220
  # @param request [Request]
211
- # @param request_type [ActiveModel::Type::Value] type for serialization
212
- # @return [Hash]
221
+ # @param request_type [ActiveModel::Type::Value] for serialization
222
+ # @return [Hash] API request parameters
213
223
  def api_request_build(request, request_type)
214
224
  parameters = request_type.serialize(request)
215
225
  parameters[:stream] = process_stream if request.try(:stream)
@@ -221,7 +231,7 @@ module ActiveAgent
221
231
  parameters
222
232
  end
223
233
 
224
- # @return [Proc] invoked for each response chunk
234
+ # @return [Proc] for each response chunk
225
235
  def process_stream
226
236
  proc do |api_response_chunk|
227
237
  process_stream_chunk(api_response_chunk)
@@ -231,12 +241,10 @@ module ActiveAgent
231
241
  # Executes prompt request against provider's API.
232
242
  #
233
243
  # @abstract
234
- # @param request_parameters [Hash]
244
+ # @param parameters [Hash]
235
245
  # @return [Object] provider-specific API response
236
246
  # @raise [NotImplementedError]
237
247
  def api_prompt_execute(parameters)
238
- instrument("api_request.provider.active_agent", model: parameters[:model], streaming: !!parameters[:stream])
239
-
240
248
  unless parameters[:stream]
241
249
  api_prompt_executer.create(**parameters)
242
250
  else
@@ -257,6 +265,18 @@ module ActiveAgent
257
265
  fail NotImplementedError, "Subclass expected to implement"
258
266
  end
259
267
 
268
+ # Normalizes API response for instrumentation.
269
+ #
270
+ # Providers that return gem objects (like Anthropic::Models::Message) should
271
+ # override this to convert to a hash so usage data can be extracted.
272
+ # By default, returns the response as-is (for providers returning hashes).
273
+ #
274
+ # @param api_response [Object] provider-specific API response
275
+ # @return [Hash, Object] normalized response (preferably hash)
276
+ def api_response_normalize(api_response)
277
+ api_response
278
+ end
279
+
260
280
  # Executes embedding request against provider's API.
261
281
  #
262
282
  # @abstract
@@ -278,21 +298,21 @@ module ActiveAgent
278
298
 
279
299
  # Broadcasts stream open event.
280
300
  #
281
- # Fires once per request cycle even during multi-turn tool calling.
301
+ # Fires once per request cycle, even during multi-turn tool calling.
282
302
  #
283
303
  # @return [void]
284
304
  def broadcast_stream_open
285
305
  return if streaming
286
306
  self.streaming = true
287
307
 
288
- instrument("stream_open.provider.active_agent")
308
+ instrument("stream_open.active_agent")
289
309
  stream_broadcaster.call(nil, nil, :open)
290
310
  end
291
311
 
292
312
  # Broadcasts stream update with message content delta.
293
313
  #
294
314
  # @param message [Hash, Object]
295
- # @param delta [String, nil] incremental content chunk
315
+ # @param delta [String, nil]
296
316
  # @return [void]
297
317
  def broadcast_stream_update(message, delta = nil)
298
318
  stream_broadcaster.call(message, delta, :update)
@@ -300,35 +320,31 @@ module ActiveAgent
300
320
 
301
321
  # Broadcasts stream close event.
302
322
  #
303
- # Fires once per request cycle even during multi-turn tool calling.
323
+ # Fires once per request cycle, even during multi-turn tool calling.
304
324
  #
305
325
  # @return [void]
306
326
  def broadcast_stream_close
307
327
  return unless streaming
308
328
  self.streaming = false
309
329
 
310
- instrument("stream_close.provider.active_agent")
330
+ instrument("stream_close.active_agent")
311
331
  stream_broadcaster.call(message_stack.last, nil, :close)
312
332
  end
313
333
 
314
334
  # Processes completed API response and handles tool calling recursion.
315
335
  #
316
- # Extracts messages and function calls from the response. If tools were invoked,
317
- # executes them and recursively continues the prompt until completion.
336
+ # Extracts messages and function calls. If tools were invoked,
337
+ # executes them and recursively continues until completion.
318
338
  #
319
339
  # @param api_response [Object, nil] provider-specific response
320
340
  # @return [Common::PromptResponse, nil]
321
341
  def process_prompt_finished(api_response = nil)
322
342
  if (api_messages = process_prompt_finished_extract_messages(api_response))
323
- instrument("messages_extracted.provider.active_agent", message_count: api_messages.size)
324
343
  message_stack.push(*api_messages)
325
344
  end
326
345
 
327
346
  if (tool_calls = process_prompt_finished_extract_function_calls)&.any?
328
- instrument("tool_calls_processing.provider.active_agent", tool_count: tool_calls.size)
329
347
  process_function_calls(tool_calls)
330
-
331
- instrument("multi_turn_continue.provider.active_agent")
332
348
  resolve_prompt
333
349
  else
334
350
 
@@ -337,27 +353,25 @@ module ActiveAgent
337
353
  # as they continue to work.
338
354
  broadcast_stream_close
339
355
 
340
- instrument("prompt_complete.provider.active_agent", message_count: message_stack.size)
341
-
342
356
  # To convert the messages into common format we first need to merge the current
343
357
  # stack and then cast them to the provider type, so we can cast them out to common.
344
358
  messages = prompt_request_type.cast(
345
359
  messages: [ *request.messages, *message_stack ]
346
360
  ).messages
347
361
 
362
+ # Create response object with usage_stack array for multi-turn cumulative tracking.
348
363
  # This will returned as it closes up the recursive stack
349
364
  Common::PromptResponse.new(
350
365
  context:,
366
+ format: request.response_format,
367
+ messages:,
351
368
  raw_request: prompt_request_type.serialize(request),
352
369
  raw_response: api_response,
353
- messages:,
354
- format: request.response_format
370
+ usages: usage_stack
355
371
  )
356
372
  end
357
373
  end
358
374
 
359
- # Extracts messages from API response.
360
- #
361
375
  # @abstract
362
376
  # @param api_response [Object]
363
377
  # @return [Array<Message>, nil]
@@ -366,8 +380,6 @@ module ActiveAgent
366
380
  fail NotImplementedError, "Subclass expected to implement"
367
381
  end
368
382
 
369
- # Extracts tool/function calls from API response.
370
- #
371
383
  # @abstract
372
384
  # @return [Array<Hash>, nil]
373
385
  # @raise [NotImplementedError]
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "requests/_types"
4
-
5
3
  require_relative "options"
6
4
  require_relative "request"
7
5
 
@@ -11,6 +9,8 @@ module ActiveAgent
11
9
  # ActiveModel type for casting and serializing Anthropic Request objects.
12
10
  #
13
11
  # Handles conversion between Hash, Request, and serialized formats for API calls.
12
+ # The Request class now delegates to the official Anthropic gem model, eliminating
13
+ # the need for maintaining nested type definitions.
14
14
  class RequestType < ActiveModel::Type::Value
15
15
  # Casts input to Request object.
16
16
  #
@@ -28,15 +28,13 @@ module ActiveAgent
28
28
  end
29
29
 
30
30
  def serialize
31
- super.except(:anthropic_beta).tap do |hash|
32
- hash[:extra_headers] = extra_headers unless extra_headers.blank?
33
- end
31
+ super.except(:anthropic_beta)
34
32
  end
35
33
 
34
+ # Anthropic gem handles beta headers differently via client.beta
35
+ # rather than via extra_headers in request_options
36
36
  def extra_headers
37
- deep_compact(
38
- "anthropic-beta" => anthropic_beta.presence,
39
- )
37
+ {}
40
38
  end
41
39
 
42
40
  private