swarm_sdk 2.4.4 → 2.4.5

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2357a0a9317288666f8e26f001e2d3b42402870d0e7e03560b6ff14109b36f56
4
- data.tar.gz: 0c11addc8f0578cfd796cda4795dfb51f2f4b134280eab2601e600b0b90670c8
3
+ metadata.gz: a0fcbfaece7a208cfd4ef7afedecda776309b3368ffea0f0800cce3fb1c2ba0f
4
+ data.tar.gz: f41125b70b3e4d83f21132a2231e1ae56f4b2dc35c00c28ef16a51e756fb93b5
5
5
  SHA512:
6
- metadata.gz: '029817745549dfcd04234c2b4e9f809e139fc222cf717f366d09c5cca6d7b7e2ae50a6f2c52058380a2a1e860e86526618ca06de464589f1d10eb21bebe1bb04'
7
- data.tar.gz: 2455fab120ec48edeb80adfa0bfa5ede016f09a49eb4ab2dc2e48490432d4b282255999f5540cb494267d4fe46c8d2f72e94ea7fcc37059db58f533ebd0bd834
6
+ metadata.gz: 6bc58ad13914191b180912d6ad8d7df187a43a696a5ef6db8f627cd213ab847ac3370d02a4e40e26df9b6bfa882ad3c5aa4adaccdc6c7eab1f3c10179768b97b
7
+ data.tar.gz: 0dcbe3bdbbf6a1443ab33268fd7968c50f874f7de688ca4edce8f7adb53ff3a00ab1910ae363f2b6c07a35401020f8602f3ca3f3ac81f95b6087fd76862e4914
@@ -170,11 +170,16 @@ module SwarmSDK
170
170
 
171
171
  # Fetch real model info for accurate context tracking
172
172
  #
173
+ # Uses SwarmSDK::Models for model lookup (reads from models.json).
174
+ # Falls back to RubyLLM.models if not found in SwarmSDK.
175
+ #
173
176
  # @param model_id [String] Model ID to lookup
174
177
  def fetch_real_model_info(model_id)
175
178
  @model_lookup_error = nil
176
179
  @real_model_info = begin
177
- RubyLLM.models.find(model_id)
180
+ # Try SwarmSDK::Models first (reads from local models.json)
181
+ # Returns ModelInfo object with method access (context_window, etc.)
182
+ SwarmSDK::Models.find(model_id) || RubyLLM.models.find(model_id)
178
183
  rescue StandardError => e
179
184
  suggestions = suggest_similar_models(model_id)
180
185
  @model_lookup_error = {
@@ -74,8 +74,8 @@ module SwarmSDK
74
74
  model_info = SwarmSDK::Models.find(message.model_id)
75
75
  return zero_cost unless model_info
76
76
 
77
- # Extract pricing from SwarmSDK's models.json structure
78
- pricing = model_info["pricing"] || model_info[:pricing]
77
+ # Extract pricing from SwarmSDK's ModelInfo (method access for top-level, Hash for nested)
78
+ pricing = model_info.pricing
79
79
  return zero_cost unless pricing
80
80
 
81
81
  text_pricing = pricing["text_tokens"] || pricing[:text_tokens]
@@ -84,6 +84,35 @@ module SwarmSDK
84
84
  limit - cumulative_total_tokens
85
85
  end
86
86
 
87
+ # Calculate cumulative input cost based on tokens and model pricing
88
+ #
89
+ # @return [Float] Total input cost in dollars
90
+ def cumulative_input_cost
91
+ pricing = model_pricing
92
+ return 0.0 unless pricing
93
+
94
+ input_price = pricing["input_per_million"] || pricing[:input_per_million] || 0.0
95
+ (cumulative_input_tokens / 1_000_000.0) * input_price
96
+ end
97
+
98
+ # Calculate cumulative output cost based on tokens and model pricing
99
+ #
100
+ # @return [Float] Total output cost in dollars
101
+ def cumulative_output_cost
102
+ pricing = model_pricing
103
+ return 0.0 unless pricing
104
+
105
+ output_price = pricing["output_per_million"] || pricing[:output_per_million] || 0.0
106
+ (cumulative_output_tokens / 1_000_000.0) * output_price
107
+ end
108
+
109
+ # Calculate cumulative total cost (input + output)
110
+ #
111
+ # @return [Float] Total cost in dollars
112
+ def cumulative_total_cost
113
+ cumulative_input_cost + cumulative_output_cost
114
+ end
115
+
87
116
  # Compact the conversation history to reduce token usage
88
117
  #
89
118
  # @param options [Hash] Compression options
@@ -92,6 +121,25 @@ module SwarmSDK
92
121
  compactor = ContextCompactor.new(self, options)
93
122
  compactor.compact
94
123
  end
124
+
125
+ private
126
+
127
+ # Get pricing info for the current model
128
+ #
129
+ # Extracts standard text token pricing from model info.
130
+ #
131
+ # @return [Hash, nil] Pricing hash with input_per_million and output_per_million
132
+ def model_pricing
133
+ return unless @real_model_info&.pricing
134
+
135
+ pricing = @real_model_info.pricing
136
+ text_pricing = pricing["text_tokens"] || pricing[:text_tokens]
137
+ return unless text_pricing
138
+
139
+ text_pricing["standard"] || text_pricing[:standard]
140
+ rescue StandardError
141
+ nil
142
+ end
95
143
  end
96
144
  end
97
145
  end
@@ -5,7 +5,7 @@
5
5
  "provider": "anthropic",
6
6
  "family": "claude-haiku-4-5",
7
7
  "created_at": null,
8
- "context_window": null,
8
+ "context_window": 200000,
9
9
  "max_output_tokens": 64000,
10
10
  "knowledge_cutoff": null,
11
11
  "modalities": {
@@ -36,7 +36,7 @@
36
36
  "provider": "anthropic",
37
37
  "family": "claude-haiku-4-5",
38
38
  "created_at": null,
39
- "context_window": null,
39
+ "context_window": 200000,
40
40
  "max_output_tokens": 64000,
41
41
  "knowledge_cutoff": null,
42
42
  "modalities": {
@@ -18,16 +18,57 @@ module SwarmSDK
18
18
  MODELS_JSON_PATH = File.expand_path("models.json", __dir__)
19
19
  ALIASES_JSON_PATH = File.expand_path("model_aliases.json", __dir__)
20
20
 
21
+ # Model information wrapper providing method access to model data
22
+ #
23
+ # Wraps the raw Hash from models.json to provide RubyLLM::Model::Info-like
24
+ # interface for compatibility with code expecting method access.
25
+ #
26
+ # @example
27
+ # model = SwarmSDK::Models.find("claude-sonnet-4-5-20250929")
28
+ # model.context_window #=> 200000
29
+ # model.id #=> "claude-sonnet-4-5-20250929"
30
+ class ModelInfo
31
+ attr_reader :id,
32
+ :name,
33
+ :provider,
34
+ :family,
35
+ :context_window,
36
+ :max_output_tokens,
37
+ :knowledge_cutoff,
38
+ :modalities,
39
+ :capabilities,
40
+ :pricing,
41
+ :metadata
42
+
43
+ # Create a ModelInfo from a Hash
44
+ #
45
+ # @param data [Hash] Model data from models.json
46
+ def initialize(data)
47
+ @id = data["id"] || data[:id]
48
+ @name = data["name"] || data[:name]
49
+ @provider = data["provider"] || data[:provider]
50
+ @family = data["family"] || data[:family]
51
+ @context_window = data["context_window"] || data[:context_window]
52
+ @max_output_tokens = data["max_output_tokens"] || data[:max_output_tokens]
53
+ @knowledge_cutoff = data["knowledge_cutoff"] || data[:knowledge_cutoff]
54
+ @modalities = data["modalities"] || data[:modalities]
55
+ @capabilities = data["capabilities"] || data[:capabilities]
56
+ @pricing = data["pricing"] || data[:pricing]
57
+ @metadata = data["metadata"] || data[:metadata]
58
+ end
59
+ end
60
+
21
61
  class << self
22
62
  # Find a model by ID or alias
23
63
  #
24
64
  # @param model_id [String] Model ID or alias to find
25
- # @return [Hash, nil] Model data or nil if not found
65
+ # @return [ModelInfo, nil] Model info or nil if not found
26
66
  def find(model_id)
27
67
  # Check if it's an alias first
28
68
  resolved_id = resolve_alias(model_id)
29
69
 
30
- all.find { |m| m["id"] == resolved_id || m[:id] == resolved_id }
70
+ model_hash = all.find { |m| m["id"] == resolved_id || m[:id] == resolved_id }
71
+ model_hash ? ModelInfo.new(model_hash) : nil
31
72
  end
32
73
 
33
74
  # Resolve a model alias to full model ID
@@ -109,6 +109,58 @@ module SwarmSDK
109
109
  @logs.map { |entry| entry[:agent] }.compact.uniq.map(&:to_sym)
110
110
  end
111
111
 
112
+ # Get per-agent usage breakdown from logs
113
+ #
114
+ # Aggregates context usage, tokens, and cost for each agent from their
115
+ # final agent_stop or agent_step events. Each agent's entry includes:
116
+ # - input_tokens, output_tokens, total_tokens
117
+ # - context_limit, usage_percentage, tokens_remaining
118
+ # - input_cost, output_cost, total_cost
119
+ #
120
+ # @return [Hash{Symbol => Hash}] Per-agent usage breakdown
121
+ #
122
+ # @example
123
+ # result.per_agent_usage[:backend]
124
+ # # => {
125
+ # # input_tokens: 15000,
126
+ # # output_tokens: 5000,
127
+ # # total_tokens: 20000,
128
+ # # context_limit: 200000,
129
+ # # usage_percentage: "10.0%",
130
+ # # tokens_remaining: 180000,
131
+ # # input_cost: 0.045,
132
+ # # output_cost: 0.075,
133
+ # # total_cost: 0.12
134
+ # # }
135
+ def per_agent_usage
136
+ # Find the last usage entry for each agent
137
+ agent_entries = {}
138
+
139
+ @logs.each do |entry|
140
+ next unless entry[:usage] && entry[:agent]
141
+ next unless entry[:type] == "agent_step" || entry[:type] == "agent_stop"
142
+
143
+ agent_name = entry[:agent].to_sym
144
+ agent_entries[agent_name] = entry[:usage]
145
+ end
146
+
147
+ # Build breakdown from final usage entries
148
+ agent_entries.transform_values do |usage|
149
+ {
150
+ input_tokens: usage[:cumulative_input_tokens] || 0,
151
+ output_tokens: usage[:cumulative_output_tokens] || 0,
152
+ total_tokens: usage[:cumulative_total_tokens] || 0,
153
+ cached_tokens: usage[:cumulative_cached_tokens] || 0,
154
+ context_limit: usage[:context_limit],
155
+ usage_percentage: usage[:tokens_used_percentage],
156
+ tokens_remaining: usage[:tokens_remaining],
157
+ input_cost: usage[:input_cost] || 0.0,
158
+ output_cost: usage[:output_cost] || 0.0,
159
+ total_cost: usage[:total_cost] || 0.0,
160
+ }
161
+ end
162
+ end
163
+
112
164
  # Count total LLM requests made
113
165
  # Each LLM API call produces either agent_step (tool calls) or agent_stop (final answer)
114
166
  def llm_requests
@@ -77,6 +77,7 @@ module SwarmSDK
77
77
  total_cost: result.total_cost,
78
78
  total_tokens: result.total_tokens,
79
79
  agents_involved: result.agents_involved,
80
+ per_agent_usage: result.per_agent_usage,
80
81
  result: result,
81
82
  timestamp: Time.now.utc.iso8601,
82
83
  },
@@ -208,6 +208,7 @@ module SwarmSDK
208
208
  total_cost: context.metadata[:total_cost],
209
209
  total_tokens: context.metadata[:total_tokens],
210
210
  agents_involved: context.metadata[:agents_involved],
211
+ per_agent_usage: context.metadata[:per_agent_usage],
211
212
  timestamp: context.metadata[:timestamp],
212
213
  )
213
214
  end
@@ -366,6 +366,47 @@ module SwarmSDK
366
366
  @agent_definitions.keys
367
367
  end
368
368
 
369
+ # Get context usage breakdown for all agents
370
+ #
371
+ # Returns per-agent context statistics including tokens used, context limit,
372
+ # usage percentage, and cost. Useful for monitoring context window consumption
373
+ # across the swarm.
374
+ #
375
+ # @return [Hash{Symbol => Hash}] Per-agent context breakdown
376
+ #
377
+ # @example
378
+ # breakdown = swarm.context_breakdown
379
+ # breakdown[:backend]
380
+ # # => {
381
+ # # input_tokens: 15000,
382
+ # # output_tokens: 5000,
383
+ # # total_tokens: 20000,
384
+ # # cached_tokens: 2000,
385
+ # # context_limit: 200000,
386
+ # # usage_percentage: 10.0,
387
+ # # tokens_remaining: 180000,
388
+ # # input_cost: 0.045,
389
+ # # output_cost: 0.075,
390
+ # # total_cost: 0.12
391
+ # # }
392
+ def context_breakdown
393
+ initialize_agents unless @agents_initialized
394
+
395
+ breakdown = {}
396
+
397
+ # Include primary agents
398
+ @agents.each do |name, chat|
399
+ breakdown[name] = build_agent_context_info(chat)
400
+ end
401
+
402
+ # Include delegation instances
403
+ @delegation_instances.each do |instance_name, chat|
404
+ breakdown[instance_name.to_sym] = build_agent_context_info(chat)
405
+ end
406
+
407
+ breakdown
408
+ end
409
+
369
410
  # Implement Snapshotable interface
370
411
  def primary_agents
371
412
  @agents
@@ -546,6 +587,29 @@ module SwarmSDK
546
587
  end
547
588
  end
548
589
 
590
+ # Build context info hash for an agent chat instance
591
+ #
592
+ # @param chat [Agent::Chat] Agent chat instance with TokenTracking
593
+ # @return [Hash] Context usage information
594
+ def build_agent_context_info(chat)
595
+ return {} unless chat.respond_to?(:cumulative_input_tokens)
596
+
597
+ {
598
+ input_tokens: chat.cumulative_input_tokens,
599
+ output_tokens: chat.cumulative_output_tokens,
600
+ total_tokens: chat.cumulative_total_tokens,
601
+ cached_tokens: chat.cumulative_cached_tokens,
602
+ cache_creation_tokens: chat.cumulative_cache_creation_tokens,
603
+ effective_input_tokens: chat.effective_input_tokens,
604
+ context_limit: chat.context_limit,
605
+ usage_percentage: chat.context_usage_percentage,
606
+ tokens_remaining: chat.tokens_remaining,
607
+ input_cost: chat.cumulative_input_cost,
608
+ output_cost: chat.cumulative_output_cost,
609
+ total_cost: chat.cumulative_total_cost,
610
+ }
611
+ end
612
+
549
613
  # Validate that observer agent exists
550
614
  #
551
615
  # @param agent_name [Symbol] Name of the observer agent
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SwarmSDK
4
- VERSION = "2.4.4"
4
+ VERSION = "2.4.5"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: swarm_sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.4.4
4
+ version: 2.4.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Paulo Arruda