activeagent 1.0.0.rc1 → 1.0.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +31 -1
- data/lib/active_agent/providers/_base_provider.rb +92 -82
- data/lib/active_agent/providers/anthropic/_types.rb +2 -2
- data/lib/active_agent/providers/anthropic/request.rb +135 -81
- data/lib/active_agent/providers/anthropic/transforms.rb +353 -0
- data/lib/active_agent/providers/anthropic_provider.rb +96 -53
- data/lib/active_agent/providers/common/messages/_types.rb +37 -1
- data/lib/active_agent/providers/common/responses/base.rb +118 -70
- data/lib/active_agent/providers/common/usage.rb +385 -0
- data/lib/active_agent/providers/concerns/instrumentation.rb +263 -0
- data/lib/active_agent/providers/log_subscriber.rb +64 -246
- data/lib/active_agent/providers/mock_provider.rb +23 -23
- data/lib/active_agent/providers/ollama/chat/request.rb +214 -35
- data/lib/active_agent/providers/ollama/chat/transforms.rb +135 -0
- data/lib/active_agent/providers/ollama/embedding/request.rb +160 -47
- data/lib/active_agent/providers/ollama/embedding/transforms.rb +160 -0
- data/lib/active_agent/providers/ollama_provider.rb +0 -1
- data/lib/active_agent/providers/open_ai/_base.rb +3 -2
- data/lib/active_agent/providers/open_ai/chat/_types.rb +13 -1
- data/lib/active_agent/providers/open_ai/chat/request.rb +132 -186
- data/lib/active_agent/providers/open_ai/chat/transforms.rb +364 -0
- data/lib/active_agent/providers/open_ai/chat_provider.rb +57 -36
- data/lib/active_agent/providers/open_ai/embedding/_types.rb +13 -2
- data/lib/active_agent/providers/open_ai/embedding/request.rb +38 -70
- data/lib/active_agent/providers/open_ai/embedding/transforms.rb +88 -0
- data/lib/active_agent/providers/open_ai/responses/_types.rb +1 -7
- data/lib/active_agent/providers/open_ai/responses/request.rb +100 -134
- data/lib/active_agent/providers/open_ai/responses/transforms.rb +228 -0
- data/lib/active_agent/providers/open_ai/responses_provider.rb +77 -30
- data/lib/active_agent/providers/open_ai_provider.rb +0 -3
- data/lib/active_agent/providers/open_router/_types.rb +27 -1
- data/lib/active_agent/providers/open_router/options.rb +49 -1
- data/lib/active_agent/providers/open_router/request.rb +232 -66
- data/lib/active_agent/providers/open_router/requests/_types.rb +0 -1
- data/lib/active_agent/providers/open_router/requests/messages/_types.rb +37 -40
- data/lib/active_agent/providers/open_router/requests/messages/content/file.rb +19 -3
- data/lib/active_agent/providers/open_router/requests/messages/content/files/details.rb +15 -4
- data/lib/active_agent/providers/open_router/requests/plugin.rb +19 -3
- data/lib/active_agent/providers/open_router/requests/plugins/pdf_config.rb +30 -8
- data/lib/active_agent/providers/open_router/requests/prediction.rb +17 -0
- data/lib/active_agent/providers/open_router/requests/provider_preferences/max_price.rb +41 -7
- data/lib/active_agent/providers/open_router/requests/provider_preferences.rb +60 -19
- data/lib/active_agent/providers/open_router/requests/response_format.rb +30 -2
- data/lib/active_agent/providers/open_router/transforms.rb +134 -0
- data/lib/active_agent/providers/open_router_provider.rb +9 -0
- data/lib/active_agent/version.rb +1 -1
- metadata +15 -159
- data/lib/active_agent/generation_provider/open_router/types.rb +0 -505
- data/lib/active_agent/generation_provider/xai_provider.rb +0 -144
- data/lib/active_agent/providers/anthropic/requests/_types.rb +0 -190
- data/lib/active_agent/providers/anthropic/requests/container_params.rb +0 -19
- data/lib/active_agent/providers/anthropic/requests/content/base.rb +0 -21
- data/lib/active_agent/providers/anthropic/requests/content/sources/base.rb +0 -22
- data/lib/active_agent/providers/anthropic/requests/context_management_config.rb +0 -18
- data/lib/active_agent/providers/anthropic/requests/messages/_types.rb +0 -189
- data/lib/active_agent/providers/anthropic/requests/messages/assistant.rb +0 -23
- data/lib/active_agent/providers/anthropic/requests/messages/base.rb +0 -63
- data/lib/active_agent/providers/anthropic/requests/messages/content/_types.rb +0 -143
- data/lib/active_agent/providers/anthropic/requests/messages/content/base.rb +0 -21
- data/lib/active_agent/providers/anthropic/requests/messages/content/document.rb +0 -26
- data/lib/active_agent/providers/anthropic/requests/messages/content/image.rb +0 -23
- data/lib/active_agent/providers/anthropic/requests/messages/content/redacted_thinking.rb +0 -21
- data/lib/active_agent/providers/anthropic/requests/messages/content/search_result.rb +0 -27
- data/lib/active_agent/providers/anthropic/requests/messages/content/sources/_types.rb +0 -171
- data/lib/active_agent/providers/anthropic/requests/messages/content/sources/base.rb +0 -22
- data/lib/active_agent/providers/anthropic/requests/messages/content/sources/document_base64.rb +0 -25
- data/lib/active_agent/providers/anthropic/requests/messages/content/sources/document_file.rb +0 -23
- data/lib/active_agent/providers/anthropic/requests/messages/content/sources/document_text.rb +0 -25
- data/lib/active_agent/providers/anthropic/requests/messages/content/sources/document_url.rb +0 -23
- data/lib/active_agent/providers/anthropic/requests/messages/content/sources/image_base64.rb +0 -27
- data/lib/active_agent/providers/anthropic/requests/messages/content/sources/image_file.rb +0 -23
- data/lib/active_agent/providers/anthropic/requests/messages/content/sources/image_url.rb +0 -23
- data/lib/active_agent/providers/anthropic/requests/messages/content/text.rb +0 -22
- data/lib/active_agent/providers/anthropic/requests/messages/content/thinking.rb +0 -23
- data/lib/active_agent/providers/anthropic/requests/messages/content/tool_result.rb +0 -24
- data/lib/active_agent/providers/anthropic/requests/messages/content/tool_use.rb +0 -28
- data/lib/active_agent/providers/anthropic/requests/messages/user.rb +0 -21
- data/lib/active_agent/providers/anthropic/requests/metadata.rb +0 -18
- data/lib/active_agent/providers/anthropic/requests/response_format.rb +0 -22
- data/lib/active_agent/providers/anthropic/requests/thinking_config/_types.rb +0 -60
- data/lib/active_agent/providers/anthropic/requests/thinking_config/base.rb +0 -20
- data/lib/active_agent/providers/anthropic/requests/thinking_config/disabled.rb +0 -16
- data/lib/active_agent/providers/anthropic/requests/thinking_config/enabled.rb +0 -20
- data/lib/active_agent/providers/anthropic/requests/tool_choice/_types.rb +0 -78
- data/lib/active_agent/providers/anthropic/requests/tool_choice/any.rb +0 -17
- data/lib/active_agent/providers/anthropic/requests/tool_choice/auto.rb +0 -17
- data/lib/active_agent/providers/anthropic/requests/tool_choice/base.rb +0 -20
- data/lib/active_agent/providers/anthropic/requests/tool_choice/none.rb +0 -16
- data/lib/active_agent/providers/anthropic/requests/tool_choice/tool.rb +0 -20
- data/lib/active_agent/providers/ollama/chat/requests/_types.rb +0 -3
- data/lib/active_agent/providers/ollama/chat/requests/messages/_types.rb +0 -116
- data/lib/active_agent/providers/ollama/chat/requests/messages/assistant.rb +0 -19
- data/lib/active_agent/providers/ollama/chat/requests/messages/user.rb +0 -19
- data/lib/active_agent/providers/ollama/embedding/requests/_types.rb +0 -83
- data/lib/active_agent/providers/ollama/embedding/requests/options.rb +0 -104
- data/lib/active_agent/providers/open_ai/chat/requests/_types.rb +0 -229
- data/lib/active_agent/providers/open_ai/chat/requests/audio.rb +0 -24
- data/lib/active_agent/providers/open_ai/chat/requests/messages/_types.rb +0 -123
- data/lib/active_agent/providers/open_ai/chat/requests/messages/assistant.rb +0 -42
- data/lib/active_agent/providers/open_ai/chat/requests/messages/base.rb +0 -78
- data/lib/active_agent/providers/open_ai/chat/requests/messages/content/_types.rb +0 -133
- data/lib/active_agent/providers/open_ai/chat/requests/messages/content/audio.rb +0 -35
- data/lib/active_agent/providers/open_ai/chat/requests/messages/content/base.rb +0 -24
- data/lib/active_agent/providers/open_ai/chat/requests/messages/content/file.rb +0 -26
- data/lib/active_agent/providers/open_ai/chat/requests/messages/content/files/_types.rb +0 -60
- data/lib/active_agent/providers/open_ai/chat/requests/messages/content/files/details.rb +0 -41
- data/lib/active_agent/providers/open_ai/chat/requests/messages/content/image.rb +0 -37
- data/lib/active_agent/providers/open_ai/chat/requests/messages/content/refusal.rb +0 -25
- data/lib/active_agent/providers/open_ai/chat/requests/messages/content/text.rb +0 -25
- data/lib/active_agent/providers/open_ai/chat/requests/messages/developer.rb +0 -25
- data/lib/active_agent/providers/open_ai/chat/requests/messages/function.rb +0 -25
- data/lib/active_agent/providers/open_ai/chat/requests/messages/system.rb +0 -25
- data/lib/active_agent/providers/open_ai/chat/requests/messages/tool.rb +0 -26
- data/lib/active_agent/providers/open_ai/chat/requests/messages/user.rb +0 -32
- data/lib/active_agent/providers/open_ai/chat/requests/prediction.rb +0 -46
- data/lib/active_agent/providers/open_ai/chat/requests/response_format.rb +0 -53
- data/lib/active_agent/providers/open_ai/chat/requests/stream_options.rb +0 -24
- data/lib/active_agent/providers/open_ai/chat/requests/tool_choice.rb +0 -26
- data/lib/active_agent/providers/open_ai/chat/requests/tools/_types.rb +0 -5
- data/lib/active_agent/providers/open_ai/chat/requests/tools/base.rb +0 -22
- data/lib/active_agent/providers/open_ai/chat/requests/tools/custom_tool.rb +0 -41
- data/lib/active_agent/providers/open_ai/chat/requests/tools/function_tool.rb +0 -51
- data/lib/active_agent/providers/open_ai/chat/requests/web_search_options.rb +0 -45
- data/lib/active_agent/providers/open_ai/embedding/requests/_types.rb +0 -49
- data/lib/active_agent/providers/open_ai/responses/requests/_types.rb +0 -231
- data/lib/active_agent/providers/open_ai/responses/requests/conversation.rb +0 -23
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/_types.rb +0 -264
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/assistant_message.rb +0 -22
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/base.rb +0 -89
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/code_interpreter_tool_call.rb +0 -30
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/computer_tool_call.rb +0 -28
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/computer_tool_call_output.rb +0 -33
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/content/_types.rb +0 -207
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/content/base.rb +0 -22
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/content/input_audio.rb +0 -26
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/content/input_file.rb +0 -28
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/content/input_image.rb +0 -28
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/content/input_text.rb +0 -25
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/custom_tool_call.rb +0 -28
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/custom_tool_call_output.rb +0 -27
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/developer_message.rb +0 -20
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/file_search_tool_call.rb +0 -25
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/function_call_output.rb +0 -32
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/function_tool_call.rb +0 -28
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/image_gen_tool_call.rb +0 -27
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/input_message.rb +0 -31
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/item_reference.rb +0 -23
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/local_shell_tool_call.rb +0 -26
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/local_shell_tool_call_output.rb +0 -33
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/mcp_approval_request.rb +0 -30
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/mcp_approval_response.rb +0 -28
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/mcp_list_tools.rb +0 -29
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/mcp_tool_call.rb +0 -35
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/output_message.rb +0 -35
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/reasoning.rb +0 -33
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/system_message.rb +0 -20
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/tool_call_base.rb +0 -27
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/tool_message.rb +0 -23
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/user_message.rb +0 -20
- data/lib/active_agent/providers/open_ai/responses/requests/inputs/web_search_tool_call.rb +0 -24
- data/lib/active_agent/providers/open_ai/responses/requests/prompt_reference.rb +0 -23
- data/lib/active_agent/providers/open_ai/responses/requests/reasoning.rb +0 -23
- data/lib/active_agent/providers/open_ai/responses/requests/stream_options.rb +0 -20
- data/lib/active_agent/providers/open_ai/responses/requests/text/_types.rb +0 -89
- data/lib/active_agent/providers/open_ai/responses/requests/text/base.rb +0 -22
- data/lib/active_agent/providers/open_ai/responses/requests/text/json_object.rb +0 -20
- data/lib/active_agent/providers/open_ai/responses/requests/text/json_schema.rb +0 -48
- data/lib/active_agent/providers/open_ai/responses/requests/text/plain.rb +0 -20
- data/lib/active_agent/providers/open_ai/responses/requests/text.rb +0 -41
- data/lib/active_agent/providers/open_ai/responses/requests/tool_choice.rb +0 -26
- data/lib/active_agent/providers/open_ai/responses/requests/tools/_types.rb +0 -112
- data/lib/active_agent/providers/open_ai/responses/requests/tools/base.rb +0 -25
- data/lib/active_agent/providers/open_ai/responses/requests/tools/code_interpreter_tool.rb +0 -23
- data/lib/active_agent/providers/open_ai/responses/requests/tools/computer_tool.rb +0 -27
- data/lib/active_agent/providers/open_ai/responses/requests/tools/custom_tool.rb +0 -28
- data/lib/active_agent/providers/open_ai/responses/requests/tools/file_search_tool.rb +0 -27
- data/lib/active_agent/providers/open_ai/responses/requests/tools/function_tool.rb +0 -29
- data/lib/active_agent/providers/open_ai/responses/requests/tools/image_generation_tool.rb +0 -37
- data/lib/active_agent/providers/open_ai/responses/requests/tools/local_shell_tool.rb +0 -21
- data/lib/active_agent/providers/open_ai/responses/requests/tools/mcp_tool.rb +0 -41
- data/lib/active_agent/providers/open_ai/responses/requests/tools/web_search_preview_tool.rb +0 -24
- data/lib/active_agent/providers/open_ai/responses/requests/tools/web_search_tool.rb +0 -25
- data/lib/active_agent/providers/open_ai/schema.yml +0 -65937
- data/lib/active_agent/providers/open_router/requests/message.rb +0 -1
- data/lib/active_agent/providers/open_router/requests/messages/assistant.rb +0 -20
- data/lib/active_agent/providers/open_router/requests/messages/user.rb +0 -30
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_agent/providers/common/model"
|
|
4
|
+
|
|
5
|
+
module ActiveAgent
|
|
6
|
+
module Providers
|
|
7
|
+
module Common
|
|
8
|
+
# Normalizes token usage statistics across AI providers.
|
|
9
|
+
#
|
|
10
|
+
# Providers return usage data in different formats with different field names.
|
|
11
|
+
# This model normalizes them into a consistent structure, automatically calculating
|
|
12
|
+
# +total_tokens+ if not provided.
|
|
13
|
+
#
|
|
14
|
+
# @example Accessing normalized usage data
|
|
15
|
+
# usage = response.normalized_usage
|
|
16
|
+
# usage.input_tokens #=> 100
|
|
17
|
+
# usage.output_tokens #=> 25
|
|
18
|
+
# usage.total_tokens #=> 125
|
|
19
|
+
# usage.cached_tokens #=> 20 (if available)
|
|
20
|
+
#
|
|
21
|
+
# @example Provider-specific details
|
|
22
|
+
# usage.provider_details #=> { "completion_tokens_details" => {...}, ... }
|
|
23
|
+
# usage.duration_ms #=> 5000 (for Ollama)
|
|
24
|
+
# usage.service_tier #=> "standard" (for Anthropic)
|
|
25
|
+
#
|
|
26
|
+
# @see https://platform.openai.com/docs/api-reference/chat/object OpenAI Chat Completion
|
|
27
|
+
# @see https://docs.anthropic.com/en/api/messages Anthropic Messages API
|
|
28
|
+
# @see https://github.com/ollama/ollama/blob/main/docs/api.md Ollama API
|
|
29
|
+
class Usage < BaseModel
|
|
30
|
+
# @!attribute [rw] input_tokens
|
|
31
|
+
# Normalized from:
|
|
32
|
+
# - OpenAI Chat/Embeddings: prompt_tokens
|
|
33
|
+
# - OpenAI Responses API: input_tokens
|
|
34
|
+
# - Anthropic: input_tokens
|
|
35
|
+
# - Ollama: prompt_eval_count
|
|
36
|
+
# - OpenRouter: prompt_tokens
|
|
37
|
+
#
|
|
38
|
+
# @return [Integer]
|
|
39
|
+
attribute :input_tokens, :integer, default: 0
|
|
40
|
+
|
|
41
|
+
# @!attribute [rw] output_tokens
|
|
42
|
+
# Normalized from:
|
|
43
|
+
# - OpenAI Chat: completion_tokens
|
|
44
|
+
# - OpenAI Responses API: output_tokens
|
|
45
|
+
# - Anthropic: output_tokens
|
|
46
|
+
# - Ollama: eval_count
|
|
47
|
+
# - OpenRouter: completion_tokens
|
|
48
|
+
# - OpenAI Embeddings: 0 (no output tokens)
|
|
49
|
+
#
|
|
50
|
+
# @return [Integer]
|
|
51
|
+
attribute :output_tokens, :integer, default: 0
|
|
52
|
+
|
|
53
|
+
# @!attribute [rw] total_tokens
|
|
54
|
+
# Automatically calculated as input_tokens + output_tokens if not provided by provider.
|
|
55
|
+
#
|
|
56
|
+
# @return [Integer]
|
|
57
|
+
attribute :total_tokens, :integer
|
|
58
|
+
|
|
59
|
+
# @!attribute [rw] cached_tokens
|
|
60
|
+
# Available from:
|
|
61
|
+
# - OpenAI: prompt_tokens_details.cached_tokens or input_tokens_details.cached_tokens
|
|
62
|
+
# - Anthropic: cache_read_input_tokens
|
|
63
|
+
#
|
|
64
|
+
# @return [Integer, nil]
|
|
65
|
+
attribute :cached_tokens, :integer
|
|
66
|
+
|
|
67
|
+
# @!attribute [rw] reasoning_tokens
|
|
68
|
+
# Available from:
|
|
69
|
+
# - OpenAI Chat: completion_tokens_details.reasoning_tokens
|
|
70
|
+
# - OpenAI Responses: output_tokens_details.reasoning_tokens
|
|
71
|
+
#
|
|
72
|
+
# @return [Integer, nil]
|
|
73
|
+
attribute :reasoning_tokens, :integer
|
|
74
|
+
|
|
75
|
+
# @!attribute [rw] audio_tokens
|
|
76
|
+
# Available from:
|
|
77
|
+
# - OpenAI: sum of prompt_tokens_details.audio_tokens and completion_tokens_details.audio_tokens
|
|
78
|
+
#
|
|
79
|
+
# @return [Integer, nil]
|
|
80
|
+
attribute :audio_tokens, :integer
|
|
81
|
+
|
|
82
|
+
# @!attribute [rw] cache_creation_tokens
|
|
83
|
+
# Available from:
|
|
84
|
+
# - Anthropic: cache_creation_input_tokens
|
|
85
|
+
#
|
|
86
|
+
# @return [Integer, nil]
|
|
87
|
+
attribute :cache_creation_tokens, :integer
|
|
88
|
+
|
|
89
|
+
# @!attribute [rw] service_tier
|
|
90
|
+
# Available from:
|
|
91
|
+
# - Anthropic: service_tier ("standard", "priority", "batch")
|
|
92
|
+
#
|
|
93
|
+
# @return [String, nil]
|
|
94
|
+
attribute :service_tier, :string
|
|
95
|
+
|
|
96
|
+
# @!attribute [rw] duration_ms
|
|
97
|
+
# Available from:
|
|
98
|
+
# - Ollama: total_duration (converted from nanoseconds)
|
|
99
|
+
#
|
|
100
|
+
# @return [Integer, nil]
|
|
101
|
+
attribute :duration_ms, :integer
|
|
102
|
+
|
|
103
|
+
# @!attribute [rw] provider_details
|
|
104
|
+
# Preserves provider-specific information that doesn't fit the normalized structure.
|
|
105
|
+
# Useful for debugging or provider-specific features.
|
|
106
|
+
#
|
|
107
|
+
# @return [Hash]
|
|
108
|
+
attribute :provider_details, default: -> { {} }
|
|
109
|
+
|
|
110
|
+
# Automatically calculates total_tokens if not provided.
|
|
111
|
+
#
|
|
112
|
+
# @param attributes [Hash]
|
|
113
|
+
# @option attributes [Integer] :input_tokens
|
|
114
|
+
# @option attributes [Integer] :output_tokens
|
|
115
|
+
# @option attributes [Integer] :total_tokens (calculated if not provided)
|
|
116
|
+
# @option attributes [Integer] :cached_tokens
|
|
117
|
+
# @option attributes [Integer] :reasoning_tokens
|
|
118
|
+
# @option attributes [Integer] :audio_tokens
|
|
119
|
+
# @option attributes [Integer] :cache_creation_tokens
|
|
120
|
+
# @option attributes [String] :service_tier
|
|
121
|
+
# @option attributes [Integer] :duration_ms
|
|
122
|
+
# @option attributes [Hash] :provider_details
|
|
123
|
+
def initialize(attributes = {})
|
|
124
|
+
super
|
|
125
|
+
# Calculate total_tokens if not provided
|
|
126
|
+
self.total_tokens ||= (input_tokens || 0) + (output_tokens || 0)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Sums all token counts from two Usage objects.
|
|
130
|
+
#
|
|
131
|
+
# @param other [Usage]
|
|
132
|
+
# @return [Usage]
|
|
133
|
+
#
|
|
134
|
+
# @example
|
|
135
|
+
# usage1 = Usage.new(input_tokens: 100, output_tokens: 50)
|
|
136
|
+
# usage2 = Usage.new(input_tokens: 75, output_tokens: 25)
|
|
137
|
+
# combined = usage1 + usage2
|
|
138
|
+
# combined.input_tokens #=> 175
|
|
139
|
+
# combined.output_tokens #=> 75
|
|
140
|
+
# combined.total_tokens #=> 250
|
|
141
|
+
def +(other)
|
|
142
|
+
return self unless other
|
|
143
|
+
|
|
144
|
+
self.class.new(
|
|
145
|
+
input_tokens: self.input_tokens + other.input_tokens,
|
|
146
|
+
output_tokens: self.output_tokens + other.output_tokens,
|
|
147
|
+
total_tokens: self.total_tokens + other.total_tokens,
|
|
148
|
+
cached_tokens: sum_optional(self.cached_tokens, other.cached_tokens),
|
|
149
|
+
cache_creation_tokens: sum_optional(self.cache_creation_tokens, other.cache_creation_tokens),
|
|
150
|
+
reasoning_tokens: sum_optional(self.reasoning_tokens, other.reasoning_tokens),
|
|
151
|
+
audio_tokens: sum_optional(self.audio_tokens, other.audio_tokens)
|
|
152
|
+
)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Creates a Usage object from OpenAI Chat Completion usage data.
|
|
156
|
+
#
|
|
157
|
+
# @param usage_hash [Hash]
|
|
158
|
+
# @return [Usage]
|
|
159
|
+
#
|
|
160
|
+
# @example
|
|
161
|
+
# Usage.from_openai_chat({
|
|
162
|
+
# "prompt_tokens" => 100,
|
|
163
|
+
# "completion_tokens" => 25,
|
|
164
|
+
# "total_tokens" => 125,
|
|
165
|
+
# "prompt_tokens_details" => { "cached_tokens" => 20 },
|
|
166
|
+
# "completion_tokens_details" => { "reasoning_tokens" => 3 }
|
|
167
|
+
# })
|
|
168
|
+
def self.from_openai_chat(usage_hash)
|
|
169
|
+
return nil unless usage_hash
|
|
170
|
+
|
|
171
|
+
usage = usage_hash.deep_symbolize_keys
|
|
172
|
+
prompt_details = usage[:prompt_tokens_details] || {}
|
|
173
|
+
completion_details = usage[:completion_tokens_details] || {}
|
|
174
|
+
|
|
175
|
+
audio_sum = [
|
|
176
|
+
prompt_details[:audio_tokens],
|
|
177
|
+
completion_details[:audio_tokens]
|
|
178
|
+
].compact.sum
|
|
179
|
+
|
|
180
|
+
new(
|
|
181
|
+
**usage.slice(:total_tokens),
|
|
182
|
+
input_tokens: usage[:prompt_tokens] || 0,
|
|
183
|
+
output_tokens: usage[:completion_tokens] || 0,
|
|
184
|
+
cached_tokens: prompt_details[:cached_tokens],
|
|
185
|
+
reasoning_tokens: completion_details[:reasoning_tokens],
|
|
186
|
+
audio_tokens: audio_sum > 0 ? audio_sum : nil,
|
|
187
|
+
provider_details: usage.slice(:prompt_tokens_details, :completion_tokens_details).compact
|
|
188
|
+
)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Creates a Usage object from OpenAI Embedding API usage data.
|
|
192
|
+
#
|
|
193
|
+
# @param usage_hash [Hash]
|
|
194
|
+
# @return [Usage]
|
|
195
|
+
#
|
|
196
|
+
# @example
|
|
197
|
+
# Usage.from_openai_embedding({
|
|
198
|
+
# "prompt_tokens" => 8,
|
|
199
|
+
# "total_tokens" => 8
|
|
200
|
+
# })
|
|
201
|
+
def self.from_openai_embedding(usage_hash)
|
|
202
|
+
return nil unless usage_hash
|
|
203
|
+
|
|
204
|
+
usage = usage_hash.deep_symbolize_keys
|
|
205
|
+
|
|
206
|
+
new(
|
|
207
|
+
**usage.slice(:total_tokens),
|
|
208
|
+
input_tokens: usage[:prompt_tokens] || 0,
|
|
209
|
+
output_tokens: 0, # Embeddings don't generate output tokens
|
|
210
|
+
provider_details: usage.except(:prompt_tokens, :total_tokens)
|
|
211
|
+
)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Creates a Usage object from OpenAI Responses API usage data.
|
|
215
|
+
#
|
|
216
|
+
# @param usage_hash [Hash]
|
|
217
|
+
# @return [Usage]
|
|
218
|
+
#
|
|
219
|
+
# @example
|
|
220
|
+
# Usage.from_openai_responses({
|
|
221
|
+
# "input_tokens" => 150,
|
|
222
|
+
# "output_tokens" => 75,
|
|
223
|
+
# "total_tokens" => 225,
|
|
224
|
+
# "input_tokens_details" => { "cached_tokens" => 50 },
|
|
225
|
+
# "output_tokens_details" => { "reasoning_tokens" => 10 }
|
|
226
|
+
# })
|
|
227
|
+
def self.from_openai_responses(usage_hash)
|
|
228
|
+
return nil unless usage_hash
|
|
229
|
+
|
|
230
|
+
usage = usage_hash.deep_symbolize_keys
|
|
231
|
+
input_details = usage[:input_tokens_details] || {}
|
|
232
|
+
output_details = usage[:output_tokens_details] || {}
|
|
233
|
+
|
|
234
|
+
new(
|
|
235
|
+
**usage.slice(:input_tokens, :output_tokens, :total_tokens),
|
|
236
|
+
input_tokens: usage[:input_tokens] || 0,
|
|
237
|
+
output_tokens: usage[:output_tokens] || 0,
|
|
238
|
+
cached_tokens: input_details[:cached_tokens],
|
|
239
|
+
reasoning_tokens: output_details[:reasoning_tokens],
|
|
240
|
+
provider_details: usage.slice(:input_tokens_details, :output_tokens_details).compact
|
|
241
|
+
)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Creates a Usage object from Anthropic usage data.
|
|
245
|
+
#
|
|
246
|
+
# @param usage_hash [Hash]
|
|
247
|
+
# @return [Usage]
|
|
248
|
+
#
|
|
249
|
+
# @example
|
|
250
|
+
# Usage.from_anthropic({
|
|
251
|
+
# "input_tokens" => 2095,
|
|
252
|
+
# "output_tokens" => 503,
|
|
253
|
+
# "cache_read_input_tokens" => 1500,
|
|
254
|
+
# "cache_creation_input_tokens" => 2051,
|
|
255
|
+
# "service_tier" => "standard"
|
|
256
|
+
# })
|
|
257
|
+
def self.from_anthropic(usage_hash)
|
|
258
|
+
return nil unless usage_hash
|
|
259
|
+
|
|
260
|
+
usage = usage_hash.deep_symbolize_keys
|
|
261
|
+
|
|
262
|
+
new(
|
|
263
|
+
**usage.slice(:input_tokens, :output_tokens, :service_tier),
|
|
264
|
+
input_tokens: usage[:input_tokens] || 0,
|
|
265
|
+
output_tokens: usage[:output_tokens] || 0,
|
|
266
|
+
cached_tokens: usage[:cache_read_input_tokens],
|
|
267
|
+
cache_creation_tokens: usage[:cache_creation_input_tokens],
|
|
268
|
+
provider_details: usage.slice(:cache_creation, :server_tool_use).compact
|
|
269
|
+
)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Creates a Usage object from Ollama usage data.
|
|
273
|
+
#
|
|
274
|
+
# @param usage_hash [Hash]
|
|
275
|
+
# @return [Usage]
|
|
276
|
+
#
|
|
277
|
+
# @example
|
|
278
|
+
# Usage.from_ollama({
|
|
279
|
+
# "prompt_eval_count" => 50,
|
|
280
|
+
# "eval_count" => 25,
|
|
281
|
+
# "total_duration" => 5000000000,
|
|
282
|
+
# "load_duration" => 1000000000
|
|
283
|
+
# })
|
|
284
|
+
def self.from_ollama(usage_hash)
|
|
285
|
+
return nil unless usage_hash
|
|
286
|
+
|
|
287
|
+
usage = usage_hash.deep_symbolize_keys
|
|
288
|
+
|
|
289
|
+
new(
|
|
290
|
+
input_tokens: usage[:prompt_eval_count] || 0,
|
|
291
|
+
output_tokens: usage[:eval_count] || 0,
|
|
292
|
+
duration_ms: convert_nanoseconds_to_ms(usage[:total_duration]),
|
|
293
|
+
provider_details: {
|
|
294
|
+
load_duration_ms: convert_nanoseconds_to_ms(usage[:load_duration]),
|
|
295
|
+
prompt_eval_duration_ms: convert_nanoseconds_to_ms(usage[:prompt_eval_duration]),
|
|
296
|
+
eval_duration_ms: convert_nanoseconds_to_ms(usage[:eval_duration]),
|
|
297
|
+
tokens_per_second: calculate_tokens_per_second(usage[:eval_count], usage[:eval_duration])
|
|
298
|
+
}.compact
|
|
299
|
+
)
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# Creates a Usage object from OpenRouter usage data.
|
|
303
|
+
#
|
|
304
|
+
# OpenRouter uses the same format as OpenAI Chat Completion.
|
|
305
|
+
#
|
|
306
|
+
# @param usage_hash [Hash]
|
|
307
|
+
# @return [Usage]
|
|
308
|
+
#
|
|
309
|
+
# @example
|
|
310
|
+
# Usage.from_openrouter({
|
|
311
|
+
# "prompt_tokens" => 14,
|
|
312
|
+
# "completion_tokens" => 4,
|
|
313
|
+
# "total_tokens" => 18
|
|
314
|
+
# })
|
|
315
|
+
def self.from_openrouter(usage_hash)
|
|
316
|
+
from_openai_chat(usage_hash)
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# Auto-detects the provider format and creates a normalized Usage object.
|
|
320
|
+
#
|
|
321
|
+
# @note Detection is based on hash structure rather than native gem types
|
|
322
|
+
# because we cannot force-load all provider gems. This allows the framework
|
|
323
|
+
# to work with only the gems the user has installed.
|
|
324
|
+
#
|
|
325
|
+
# @param usage_hash [Hash]
|
|
326
|
+
# @return [Usage, nil]
|
|
327
|
+
#
|
|
328
|
+
# @example
|
|
329
|
+
# Usage.from_provider_usage(some_usage_hash)
|
|
330
|
+
def self.from_provider_usage(usage_hash)
|
|
331
|
+
return nil unless usage_hash.is_a?(Hash)
|
|
332
|
+
|
|
333
|
+
usage = usage_hash.deep_symbolize_keys
|
|
334
|
+
|
|
335
|
+
# Detect Ollama by presence of nanosecond duration fields
|
|
336
|
+
if usage.key?(:total_duration)
|
|
337
|
+
from_ollama(usage_hash)
|
|
338
|
+
# Detect Anthropic by presence of cache_creation or service_tier
|
|
339
|
+
elsif usage.key?(:cache_creation) || usage.key?(:service_tier)
|
|
340
|
+
from_anthropic(usage_hash)
|
|
341
|
+
# Detect OpenAI Responses API by input_tokens/output_tokens with details
|
|
342
|
+
elsif usage.key?(:input_tokens) && usage.key?(:input_tokens_details)
|
|
343
|
+
from_openai_responses(usage_hash)
|
|
344
|
+
# Detect OpenAI Chat/OpenRouter by prompt_tokens/completion_tokens
|
|
345
|
+
elsif usage.key?(:completion_tokens)
|
|
346
|
+
from_openai_chat(usage_hash)
|
|
347
|
+
# Detect OpenAI Embedding by prompt_tokens without completion_tokens
|
|
348
|
+
elsif usage.key?(:prompt_tokens)
|
|
349
|
+
from_openai_embedding(usage_hash)
|
|
350
|
+
# Default to raw initialization
|
|
351
|
+
else
|
|
352
|
+
new(usage_hash)
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
private
|
|
357
|
+
|
|
358
|
+
# @param a [Integer, nil]
|
|
359
|
+
# @param b [Integer, nil]
|
|
360
|
+
# @return [Integer, nil] nil if both inputs are nil
|
|
361
|
+
def sum_optional(a, b)
|
|
362
|
+
return nil if a.nil? && b.nil?
|
|
363
|
+
(a || 0) + (b || 0)
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
# @param nanoseconds [Integer, nil]
|
|
367
|
+
# @return [Integer, nil]
|
|
368
|
+
def self.convert_nanoseconds_to_ms(nanoseconds)
|
|
369
|
+
return nil unless nanoseconds
|
|
370
|
+
|
|
371
|
+
(nanoseconds / 1_000_000.0).round
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# @param tokens [Integer, nil]
|
|
375
|
+
# @param duration_ns [Integer, nil]
|
|
376
|
+
# @return [Float, nil]
|
|
377
|
+
def self.calculate_tokens_per_second(tokens, duration_ns)
|
|
378
|
+
return nil unless tokens && duration_ns && duration_ns > 0
|
|
379
|
+
|
|
380
|
+
(tokens.to_f / (duration_ns / 1_000_000_000.0)).round(2)
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
end
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveAgent
|
|
4
|
+
module Providers
|
|
5
|
+
# Builds instrumentation event payloads for ActiveSupport::Notifications.
|
|
6
|
+
#
|
|
7
|
+
# Extracts request parameters and response metadata for monitoring, debugging,
|
|
8
|
+
# and APM integration (New Relic, DataDog, etc.).
|
|
9
|
+
#
|
|
10
|
+
# == Event Payloads
|
|
11
|
+
#
|
|
12
|
+
# Top-Level Events (overall request lifecycle):
|
|
13
|
+
#
|
|
14
|
+
# prompt.active_agent::
|
|
15
|
+
# Initial: `{ model:, temperature:, max_tokens:, message_count:, has_tools:, stream: }`
|
|
16
|
+
# Final: `{ usage: { input_tokens:, output_tokens:, total_tokens: }, finish_reason:, response_model:, response_id: }`
|
|
17
|
+
# Note: Usage is cumulative across all API calls in multi-turn conversations
|
|
18
|
+
#
|
|
19
|
+
# embed.active_agent::
|
|
20
|
+
# Initial: `{ model:, input_size:, encoding_format:, dimensions: }`
|
|
21
|
+
# Final: `{ usage: { input_tokens:, total_tokens: }, embedding_count:, response_model:, response_id: }`
|
|
22
|
+
#
|
|
23
|
+
# Provider-Level Events (per API call):
|
|
24
|
+
#
|
|
25
|
+
# prompt.provider.active_agent::
|
|
26
|
+
# Initial: `{ model:, temperature:, max_tokens:, message_count:, has_tools:, stream: }`
|
|
27
|
+
# Final: `{ usage: { input_tokens:, output_tokens:, total_tokens: }, finish_reason:, response_model:, response_id: }`
|
|
28
|
+
# Note: Usage is per individual API call
|
|
29
|
+
#
|
|
30
|
+
# embed.provider.active_agent::
|
|
31
|
+
# Initial: `{ model:, input_size:, encoding_format:, dimensions: }`
|
|
32
|
+
# Final: `{ usage: { input_tokens:, total_tokens: }, embedding_count:, response_model:, response_id: }`
|
|
33
|
+
module Instrumentation
|
|
34
|
+
extend ActiveSupport::Concern
|
|
35
|
+
|
|
36
|
+
# Builds and merges payload data for prompt instrumentation events.
|
|
37
|
+
#
|
|
38
|
+
# Populates both request parameters and response metadata for top-level and
|
|
39
|
+
# provider-level events. Usage data (tokens) is CRITICAL for APM cost tracking
|
|
40
|
+
# and performance monitoring.
|
|
41
|
+
#
|
|
42
|
+
# @param payload [Hash] instrumentation payload to merge into
|
|
43
|
+
# @param request [Request] request object with parameters
|
|
44
|
+
# @param response [Common::PromptResponse] completed response with normalized data
|
|
45
|
+
# @return [void]
|
|
46
|
+
def instrumentation_prompt_payload(payload, request, response)
|
|
47
|
+
# message_count: prefer the request/input messages (pre-call), fall back to
|
|
48
|
+
# response messages only if the request doesn't expose messages. New Relic
|
|
49
|
+
# expects parameters[:messages] to be the request messages and computes
|
|
50
|
+
# total message counts by adding response choices to that count.
|
|
51
|
+
message_count = safe_access(request, :messages)&.size
|
|
52
|
+
message_count = safe_access(response, :messages)&.size if message_count.nil?
|
|
53
|
+
|
|
54
|
+
payload.merge!(trace_id: trace_id, message_count: message_count || 0, stream: !!safe_access(request, :stream))
|
|
55
|
+
|
|
56
|
+
# Common parameters: prefer response-normalized values, then request
|
|
57
|
+
payload[:model] = safe_access(response, :model) || safe_access(request, :model)
|
|
58
|
+
payload[:temperature] = safe_access(request, :temperature)
|
|
59
|
+
payload[:max_tokens] = safe_access(request, :max_tokens)
|
|
60
|
+
payload[:top_p] = safe_access(request, :top_p)
|
|
61
|
+
|
|
62
|
+
# Tools / instructions
|
|
63
|
+
if (tools_val = safe_access(request, :tools))
|
|
64
|
+
payload[:has_tools] = tools_val.respond_to?(:present?) ? tools_val.present? : !!tools_val
|
|
65
|
+
payload[:tool_count] = tools_val&.size || 0
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
if (instr_val = safe_access(request, :instructions))
|
|
69
|
+
payload[:has_instructions] = instr_val.respond_to?(:present?) ? instr_val.present? : !!instr_val
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Usage (normalized)
|
|
73
|
+
if response.usage
|
|
74
|
+
usage = response.usage
|
|
75
|
+
payload[:usage] = {
|
|
76
|
+
input_tokens: usage.input_tokens,
|
|
77
|
+
output_tokens: usage.output_tokens,
|
|
78
|
+
total_tokens: usage.total_tokens
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
payload[:usage][:cached_tokens] = usage.cached_tokens if usage.cached_tokens
|
|
82
|
+
payload[:usage][:cache_creation_tokens] = usage.cache_creation_tokens if usage.cache_creation_tokens
|
|
83
|
+
payload[:usage][:reasoning_tokens] = usage.reasoning_tokens if usage.reasoning_tokens
|
|
84
|
+
payload[:usage][:audio_tokens] = usage.audio_tokens if usage.audio_tokens
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Response metadata
|
|
88
|
+
payload[:finish_reason] = safe_access(response, :finish_reason) || response.finish_reason
|
|
89
|
+
payload[:response_model] = safe_access(response, :model) || response.model
|
|
90
|
+
payload[:response_id] = safe_access(response, :id) || response.id
|
|
91
|
+
|
|
92
|
+
# Build messages list: prefer request messages; if unavailable use prior
|
|
93
|
+
# response messages (all but the final generated message).
|
|
94
|
+
if (req_msgs = safe_access(request, :messages)).is_a?(Array)
|
|
95
|
+
payload[:messages] = req_msgs.map { |m| extract_message_hash(m, false) }
|
|
96
|
+
else
|
|
97
|
+
prior = safe_access(response, :messages)
|
|
98
|
+
prior = prior[0...-1] if prior.is_a?(Array) && prior.size > 1
|
|
99
|
+
if prior.is_a?(Array) && prior.any?
|
|
100
|
+
payload[:messages] = prior.map { |m| extract_message_hash(m, false) }
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Build a parameters hash that mirrors what New Relic's OpenAI
|
|
105
|
+
# instrumentation expects. This makes it easy for APM adapters to
|
|
106
|
+
# map our provider payload to their LLM event constructors.
|
|
107
|
+
parameters = {}
|
|
108
|
+
parameters[:model] = payload[:model] if payload[:model]
|
|
109
|
+
parameters[:max_tokens] = payload[:max_tokens] if payload[:max_tokens]
|
|
110
|
+
parameters[:temperature] = payload[:temperature] if payload[:temperature]
|
|
111
|
+
parameters[:top_p] = payload[:top_p] if payload[:top_p]
|
|
112
|
+
parameters[:stream] = payload[:stream]
|
|
113
|
+
parameters[:messages] = payload[:messages] if payload[:messages]
|
|
114
|
+
|
|
115
|
+
# Include tools/instructions where available — New Relic ignores unknown keys,
|
|
116
|
+
# but having them here makes the parameter shape closer to OpenAI's.
|
|
117
|
+
parameters[:tools] = begin request.tools rescue nil end if begin request.tools rescue nil end
|
|
118
|
+
parameters[:instructions] = begin request.instructions rescue nil end if begin request.instructions rescue nil end
|
|
119
|
+
|
|
120
|
+
payload[:parameters] = parameters
|
|
121
|
+
|
|
122
|
+
# Attach raw response (provider-specific) so downstream APM integrations
|
|
123
|
+
# can inspect the provider response if needed. Use the normalized raw_response
|
|
124
|
+
# available on the Common::Response when possible.
|
|
125
|
+
begin
|
|
126
|
+
payload[:response_raw] = response.raw_response if response.respond_to?(:raw_response) && response.raw_response
|
|
127
|
+
rescue StandardError
|
|
128
|
+
# ignore
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
private
|
|
133
|
+
|
|
134
|
+
# Safely attempt to call a method or lookup a key on an object. We avoid
|
|
135
|
+
# probing with `respond_to?` to prevent ActiveModel attribute casting side
|
|
136
|
+
# effects; instead we attempt the call and rescue failures.
|
|
137
|
+
def safe_access(obj, name)
|
|
138
|
+
return nil if obj.nil?
|
|
139
|
+
|
|
140
|
+
begin
|
|
141
|
+
return obj.public_send(name)
|
|
142
|
+
rescue StandardError
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
begin
|
|
146
|
+
return obj[name]
|
|
147
|
+
rescue StandardError
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
begin
|
|
151
|
+
return obj[name.to_s]
|
|
152
|
+
rescue StandardError
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
nil
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# NOTE: message access is handled via `safe_access(obj, :messages)` to
|
|
159
|
+
# avoid duplicating guarded lookup logic.
|
|
160
|
+
|
|
161
|
+
# Extract a simple hash from a provider message object or hash-like value.
|
|
162
|
+
def extract_message_hash(msg, is_response = false)
|
|
163
|
+
role = begin
|
|
164
|
+
if msg.respond_to?(:[])
|
|
165
|
+
begin msg[:role] rescue (begin msg["role"] rescue nil end) end
|
|
166
|
+
elsif msg.respond_to?(:role)
|
|
167
|
+
msg.role
|
|
168
|
+
elsif msg.respond_to?(:type)
|
|
169
|
+
msg.type
|
|
170
|
+
end
|
|
171
|
+
rescue StandardError
|
|
172
|
+
begin msg.role rescue msg.type rescue nil end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
content = begin
|
|
176
|
+
if msg.respond_to?(:[])
|
|
177
|
+
begin msg[:content] rescue (begin msg["content"] rescue nil end) end
|
|
178
|
+
elsif msg.respond_to?(:content)
|
|
179
|
+
msg.content
|
|
180
|
+
elsif msg.respond_to?(:text)
|
|
181
|
+
msg.text
|
|
182
|
+
elsif msg.respond_to?(:to_h)
|
|
183
|
+
begin msg.to_h[:content] rescue (begin msg.to_h["content"] rescue nil end) end
|
|
184
|
+
elsif msg.respond_to?(:to_s)
|
|
185
|
+
msg.to_s
|
|
186
|
+
end
|
|
187
|
+
rescue StandardError
|
|
188
|
+
begin msg.to_s rescue nil end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
{ role: role, content: content, is_response: is_response }
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Builds and merges payload data for embed instrumentation events.
|
|
195
|
+
#
|
|
196
|
+
# Embeddings typically only report input tokens (no output tokens).
|
|
197
|
+
#
|
|
198
|
+
# @param payload [Hash] instrumentation payload to merge into
|
|
199
|
+
# @param request [Request] request object with parameters
|
|
200
|
+
# @param response [Common::EmbedResponse] completed response with normalized data
|
|
201
|
+
# @return [void]
|
|
202
|
+
def instrumentation_embed_payload(payload, request, response)
|
|
203
|
+
# Add request parameters
|
|
204
|
+
payload[:trace_id] = trace_id
|
|
205
|
+
payload[:model] = request.model if request.respond_to?(:model)
|
|
206
|
+
|
|
207
|
+
# Add input size if available
|
|
208
|
+
if request.respond_to?(:input)
|
|
209
|
+
begin
|
|
210
|
+
input = request.input
|
|
211
|
+
if input.is_a?(String)
|
|
212
|
+
payload[:input_size] = 1
|
|
213
|
+
elsif input.is_a?(Array)
|
|
214
|
+
payload[:input_size] = input.size
|
|
215
|
+
end
|
|
216
|
+
rescue # OpenAI throws errors this for some reason when you try to look at the input.
|
|
217
|
+
payload[:input_size] = request[:input].size
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Expose embedding input content similarly to message content.
|
|
222
|
+
# Use guarded access to avoid provider-specific errors.
|
|
223
|
+
begin
|
|
224
|
+
if (emb_input = safe_access(request, :input))
|
|
225
|
+
# Keep the raw input (string or array) in the payload so APM adapters
|
|
226
|
+
# can inspect it. This matches how we include message content.
|
|
227
|
+
payload[:input] = emb_input
|
|
228
|
+
end
|
|
229
|
+
rescue StandardError
|
|
230
|
+
# ignore
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Add encoding format if available (OpenAI)
|
|
234
|
+
payload[:encoding_format] = request.encoding_format if request.respond_to?(:encoding_format)
|
|
235
|
+
|
|
236
|
+
# Add dimensions if available (OpenAI)
|
|
237
|
+
payload[:dimensions] = request.dimensions if request.respond_to?(:dimensions)
|
|
238
|
+
|
|
239
|
+
# Add response data
|
|
240
|
+
payload[:embedding_count] = response.data&.size || 0
|
|
241
|
+
|
|
242
|
+
# Add usage data if available (CRITICAL for APM integration)
|
|
243
|
+
# Embeddings typically only have input tokens
|
|
244
|
+
if response.usage
|
|
245
|
+
payload[:usage] = {
|
|
246
|
+
input_tokens: response.usage.input_tokens,
|
|
247
|
+
total_tokens: response.usage.total_tokens
|
|
248
|
+
}
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Add response metadata directly from response object
|
|
252
|
+
payload[:response_model] = response.model
|
|
253
|
+
payload[:response_id] = response.id
|
|
254
|
+
|
|
255
|
+
# Build a parameters hash for embeddings to match New Relic's shape.
|
|
256
|
+
emb_params = {}
|
|
257
|
+
emb_params[:model] = payload[:model] if payload[:model]
|
|
258
|
+
emb_params[:input] = payload[:input] if payload.key?(:input)
|
|
259
|
+
payload[:parameters] = emb_params unless emb_params.empty?
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|