dspy 0.12.0 → 0.13.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d40d9681a87073c6c4f5531025891bf871bee086434ee131e15a4391cba7b893
4
- data.tar.gz: 1a1a4ce935e7cdf4ce9c06617f124ed8858d7074a3f6bec4484d6db24dbfa232
3
+ metadata.gz: d80d5b0166fe5a101e4918ffee13a70dec6ca67b493cf2e68dff1c18b2df36c1
4
+ data.tar.gz: 1687fe88d41c5d4627592ff5e98f87ca9f40186870386291b1ead091f51235da
5
5
  SHA512:
6
- metadata.gz: 2cdd3ee7e867344b4e45dee7fd0d73dc875ec2c730a2cf75e5f620a19c535f0aac50cc6c88dfb7beb3e075b147e7467ccd4e55a7724f49f3ac111eb080fd88e0
7
- data.tar.gz: f5c5ed5dd3b6aa5401c20e31fbbd9638eb1d8c0f3b7fe5adb2e2b69bbb3b051d0ecd277d6e2f3b4d4896fd19f39a89345c9503d97106a3b8e005b1f55a3e5b2b
6
+ metadata.gz: 7bedebf2e58243bedcf8003d25b4f55789a7e4a611f9f1997a322e93db553fe117fde155415c5a607a65b49f61f2a640c7899a0fd4793bcdf9e8672602f54755
7
+ data.tar.gz: cdf39e605a7550e94334c5bad13ccc5af2a255b7039559b56b918e1ba3706e671c52960558459a0c96edab318c68bc9828b1eb03530b47daa896766a2cb7aa7d
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative '../lm/usage'
4
+
3
5
  module DSPy
4
6
  module Instrumentation
5
7
  # Utility for extracting token usage from different LM adapters
@@ -9,6 +11,18 @@ module DSPy
9
11
 
10
12
  # Extract actual token usage from API responses
11
13
  def extract_token_usage(response, provider)
14
+ return {} unless response&.usage
15
+
16
+ # Handle Usage struct
17
+ if response.usage.is_a?(DSPy::LM::Usage) || response.usage.is_a?(DSPy::LM::OpenAIUsage)
18
+ return {
19
+ input_tokens: response.usage.input_tokens,
20
+ output_tokens: response.usage.output_tokens,
21
+ total_tokens: response.usage.total_tokens
22
+ }
23
+ end
24
+
25
+ # Fallback to legacy hash handling
12
26
  case provider.to_s.downcase
13
27
  when 'openai'
14
28
  extract_openai_tokens(response)
@@ -27,11 +41,12 @@ module DSPy
27
41
  usage = response.usage
28
42
  return {} unless usage.is_a?(Hash)
29
43
 
44
+ # Handle both symbol and string keys for VCR compatibility
30
45
  {
31
46
  input_tokens: usage[:prompt_tokens] || usage['prompt_tokens'],
32
47
  output_tokens: usage[:completion_tokens] || usage['completion_tokens'],
33
48
  total_tokens: usage[:total_tokens] || usage['total_tokens']
34
- }
49
+ }.compact # Remove nil values
35
50
  end
36
51
 
37
52
  def extract_anthropic_tokens(response)
@@ -40,6 +55,7 @@ module DSPy
40
55
  usage = response.usage
41
56
  return {} unless usage.is_a?(Hash)
42
57
 
58
+ # Handle both symbol and string keys for VCR compatibility
43
59
  input_tokens = usage[:input_tokens] || usage['input_tokens'] || 0
44
60
  output_tokens = usage[:output_tokens] || usage['output_tokens'] || 0
45
61
 
@@ -47,7 +63,7 @@ module DSPy
47
63
  input_tokens: input_tokens,
48
64
  output_tokens: output_tokens,
49
65
  total_tokens: input_tokens + output_tokens
50
- }
66
+ }.compact # Remove nil values
51
67
  end
52
68
  end
53
69
  end
@@ -63,9 +63,12 @@ module DSPy
63
63
  content = response.content.first.text if response.content.is_a?(Array) && response.content.first
64
64
  usage = response.usage
65
65
 
66
+ # Convert usage data to typed struct
67
+ usage_struct = UsageFactory.create('anthropic', usage)
68
+
66
69
  Response.new(
67
70
  content: content,
68
- usage: usage.respond_to?(:to_h) ? usage.to_h : usage,
71
+ usage: usage_struct,
69
72
  metadata: {
70
73
  provider: 'anthropic',
71
74
  model: model,
@@ -52,9 +52,12 @@ module DSPy
52
52
  raise AdapterError, "OpenAI refused to generate output: #{message.refusal}"
53
53
  end
54
54
 
55
+ # Convert usage data to typed struct
56
+ usage_struct = UsageFactory.create('openai', usage)
57
+
55
58
  Response.new(
56
59
  content: content,
57
- usage: usage.respond_to?(:to_h) ? usage.to_h : usage,
60
+ usage: usage_struct,
58
61
  metadata: {
59
62
  provider: 'openai',
60
63
  model: model,
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'usage'
4
+
3
5
  module DSPy
4
6
  class LM
5
7
  # Normalized response format for all LM providers
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sorbet-runtime'
4
+
5
+ module DSPy
6
+ class LM
7
+ # Base class for token usage information
8
+ class Usage < T::Struct
9
+ extend T::Sig
10
+
11
+ const :input_tokens, Integer
12
+ const :output_tokens, Integer
13
+ const :total_tokens, Integer
14
+
15
+ sig { returns(Hash) }
16
+ def to_h
17
+ {
18
+ input_tokens: input_tokens,
19
+ output_tokens: output_tokens,
20
+ total_tokens: total_tokens
21
+ }
22
+ end
23
+ end
24
+
25
+ # OpenAI-specific usage information with additional fields
26
+ class OpenAIUsage < T::Struct
27
+ extend T::Sig
28
+
29
+ const :input_tokens, Integer
30
+ const :output_tokens, Integer
31
+ const :total_tokens, Integer
32
+ const :prompt_tokens_details, T.nilable(T::Hash[Symbol, Integer]), default: nil
33
+ const :completion_tokens_details, T.nilable(T::Hash[Symbol, Integer]), default: nil
34
+
35
+ sig { returns(Hash) }
36
+ def to_h
37
+ base = {
38
+ input_tokens: input_tokens,
39
+ output_tokens: output_tokens,
40
+ total_tokens: total_tokens
41
+ }
42
+ base[:prompt_tokens_details] = prompt_tokens_details if prompt_tokens_details
43
+ base[:completion_tokens_details] = completion_tokens_details if completion_tokens_details
44
+ base
45
+ end
46
+ end
47
+
48
+ # Factory for creating appropriate usage objects
49
+ module UsageFactory
50
+ extend T::Sig
51
+
52
+ sig { params(provider: String, usage_data: T.untyped).returns(T.nilable(T.any(Usage, OpenAIUsage))) }
53
+ def self.create(provider, usage_data)
54
+ return nil if usage_data.nil?
55
+
56
+ # If already a Usage struct, return as-is
57
+ return usage_data if usage_data.is_a?(Usage)
58
+
59
+ # Handle test doubles by converting to hash
60
+ if usage_data.respond_to?(:to_h)
61
+ usage_data = usage_data.to_h
62
+ end
63
+
64
+ # Convert hash to appropriate struct
65
+ return nil unless usage_data.is_a?(Hash)
66
+
67
+ # Normalize keys to symbols
68
+ normalized = usage_data.transform_keys(&:to_sym)
69
+
70
+ case provider.to_s.downcase
71
+ when 'openai'
72
+ create_openai_usage(normalized)
73
+ when 'anthropic'
74
+ create_anthropic_usage(normalized)
75
+ else
76
+ create_generic_usage(normalized)
77
+ end
78
+ end
79
+
80
+ private
81
+
82
+ sig { params(data: T::Hash[Symbol, T.untyped]).returns(T.nilable(OpenAIUsage)) }
83
+ def self.create_openai_usage(data)
84
+ # OpenAI uses prompt_tokens/completion_tokens
85
+ input_tokens = data[:prompt_tokens] || data[:input_tokens] || 0
86
+ output_tokens = data[:completion_tokens] || data[:output_tokens] || 0
87
+ total_tokens = data[:total_tokens] || (input_tokens + output_tokens)
88
+
89
+ # Convert prompt_tokens_details and completion_tokens_details to hashes if needed
90
+ prompt_details = convert_to_hash(data[:prompt_tokens_details])
91
+ completion_details = convert_to_hash(data[:completion_tokens_details])
92
+
93
+ OpenAIUsage.new(
94
+ input_tokens: input_tokens,
95
+ output_tokens: output_tokens,
96
+ total_tokens: total_tokens,
97
+ prompt_tokens_details: prompt_details,
98
+ completion_tokens_details: completion_details
99
+ )
100
+ rescue => e
101
+ DSPy.logger.debug("Failed to create OpenAI usage: #{e.message}")
102
+ nil
103
+ end
104
+
105
+ sig { params(value: T.untyped).returns(T.nilable(T::Hash[Symbol, Integer])) }
106
+ def self.convert_to_hash(value)
107
+ return nil if value.nil?
108
+ return value if value.is_a?(Hash) && value.keys.all? { |k| k.is_a?(Symbol) }
109
+
110
+ # Convert object to hash if it responds to to_h
111
+ if value.respond_to?(:to_h)
112
+ hash = value.to_h
113
+ # Ensure symbol keys and integer values
114
+ hash.transform_keys(&:to_sym).transform_values(&:to_i)
115
+ else
116
+ nil
117
+ end
118
+ rescue
119
+ nil
120
+ end
121
+
122
+ sig { params(data: T::Hash[Symbol, T.untyped]).returns(T.nilable(Usage)) }
123
+ def self.create_anthropic_usage(data)
124
+ # Anthropic uses input_tokens/output_tokens
125
+ input_tokens = data[:input_tokens] || 0
126
+ output_tokens = data[:output_tokens] || 0
127
+ total_tokens = data[:total_tokens] || (input_tokens + output_tokens)
128
+
129
+ Usage.new(
130
+ input_tokens: input_tokens,
131
+ output_tokens: output_tokens,
132
+ total_tokens: total_tokens
133
+ )
134
+ rescue => e
135
+ DSPy.logger.debug("Failed to create Anthropic usage: #{e.message}")
136
+ nil
137
+ end
138
+
139
+ sig { params(data: T::Hash[Symbol, T.untyped]).returns(T.nilable(Usage)) }
140
+ def self.create_generic_usage(data)
141
+ # Generic fallback
142
+ input_tokens = data[:input_tokens] || data[:prompt_tokens] || 0
143
+ output_tokens = data[:output_tokens] || data[:completion_tokens] || 0
144
+ total_tokens = data[:total_tokens] || (input_tokens + output_tokens)
145
+
146
+ Usage.new(
147
+ input_tokens: input_tokens,
148
+ output_tokens: output_tokens,
149
+ total_tokens: total_tokens
150
+ )
151
+ rescue => e
152
+ DSPy.logger.debug("Failed to create generic usage: #{e.message}")
153
+ nil
154
+ end
155
+ end
156
+ end
157
+ end
data/lib/dspy/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DSPy
4
- VERSION = "0.12.0"
4
+ VERSION = "0.13.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dspy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.12.0
4
+ version: 0.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vicente Reig Rincón de Arellano
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-07-24 00:00:00.000000000 Z
11
+ date: 2025-07-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dry-configurable
@@ -186,6 +186,7 @@ files:
186
186
  - lib/dspy/lm/strategies/openai_structured_output_strategy.rb
187
187
  - lib/dspy/lm/strategy_selector.rb
188
188
  - lib/dspy/lm/structured_output_strategy.rb
189
+ - lib/dspy/lm/usage.rb
189
190
  - lib/dspy/memory.rb
190
191
  - lib/dspy/memory/embedding_engine.rb
191
192
  - lib/dspy/memory/in_memory_store.rb