dspy-anthropic 1.0.4 → 1.0.6

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: 2b563f892087546b395c4c279b09684339170eb112898941d3129ac48f804cbf
4
- data.tar.gz: 54f15263e44abd8b6feca18a9569973ee952c6dd9373e4302db9ed66e46811c3
3
+ metadata.gz: b8d68b501f25927a814cc35c0af2c7f0e5f3b5bd1bb03f3b48df61111977bdee
4
+ data.tar.gz: 58af722a168077708cde9ab723d4004c5cf47204adad3e75ece1a15fd25455ce
5
5
  SHA512:
6
- metadata.gz: e78bbfb62abdd747b2dbf0d31da401078eeab71c09a6411d78097769baae4a8d96f1c142617741b203031ba9494e6b99581d5c4a9dbc81e1c40fe8ca221bffa7
7
- data.tar.gz: 9369dadfb40f324a0f27cf8cdb912245ec214a0e13c676865b561a8886f4a12b0059c4e3b0b67b75b923df7e4a3604462b48d0b8a7807508e1641e0f044173e5
6
+ metadata.gz: c070133fcdac4bfc561c37084bcce94b31a7564ae3e8b872b80593684e105f71dbe80a1c84e06f0cc7971649bbc8d5c66a9d3333f739ed643a5e04f59afffca3
7
+ data.tar.gz: 1da253c16a11737bb777d49639906ef09afe7b62188d36937bfd405582acc3de318169bfb7b95b41220ae042988f73079028d77f0cb63429f20c9b0793b63813
data/README.md CHANGED
@@ -6,95 +6,16 @@
6
6
  [![Documentation](https://img.shields.io/badge/docs-oss.vicente.services%2Fdspy.rb-blue)](https://oss.vicente.services/dspy.rb/)
7
7
  [![Discord](https://img.shields.io/discord/1161519468141355160?label=discord&logo=discord&logoColor=white)](https://discord.gg/zWBhrMqn)
8
8
 
9
- **Build reliable LLM applications in idiomatic Ruby using composable, type-safe modules.**
9
+ **Build typed agents and model-backed programs in Ruby.**
10
10
 
11
- DSPy.rb is the Ruby port of Stanford's [DSPy](https://dspy.ai). Instead of wrestling with brittle prompt strings, you define typed signatures and let the framework handle the rest. Prompts become functions. LLM calls become predictable.
11
+ DSPy.rb brings [DSPy](https://dspy.ai)'s signature, module, agent, and optimizer model to Ruby, with Sorbet types and Ruby-native integrations. A signature declares a task as typed inputs and outputs. A module chooses how to execute it. Ruby composes modules into programs, while `ReAct` adds a bounded loop when the model should choose the next action. DSPy.rb builds the provider request and validates the returned shape.
12
12
 
13
- ```ruby
14
- require 'dspy'
15
-
16
- DSPy.configure do |c|
17
- c.lm = DSPy::LM.new('openai/gpt-4o-mini', api_key: ENV['OPENAI_API_KEY'])
18
- end
19
-
20
- class Summarize < DSPy::Signature
21
- description "Summarize the given text in one sentence."
22
-
23
- input do
24
- const :text, String
25
- end
26
-
27
- output do
28
- const :summary, String
29
- end
30
- end
31
-
32
- summarizer = DSPy::Predict.new(Summarize)
33
- result = summarizer.call(text: "DSPy.rb brings structured LLM programming to Ruby...")
34
- puts result.summary
35
- ```
36
-
37
- That's it. No prompt templates. No JSON parsing. No prayer-based error handling.
38
-
39
- ## Installation
40
-
41
- ```ruby
42
- # Gemfile
43
- gem 'dspy'
44
- gem 'dspy-openai' # For OpenAI, OpenRouter, or Ollama
45
- # gem 'dspy-anthropic' # For Claude
46
- # gem 'dspy-gemini' # For Gemini
47
- # gem 'dspy-ruby_llm' # For 12+ providers via RubyLLM
48
- ```
49
-
50
- ```bash
51
- bundle install
52
- ```
53
-
54
- ## Quick Start
55
-
56
- ### Configure Your LLM
57
-
58
- ```ruby
59
- # OpenAI
60
- DSPy.configure do |c|
61
- c.lm = DSPy::LM.new('openai/gpt-4o-mini',
62
- api_key: ENV['OPENAI_API_KEY'],
63
- structured_outputs: true)
64
- end
65
-
66
- # Anthropic Claude
67
- DSPy.configure do |c|
68
- c.lm = DSPy::LM.new('anthropic/claude-sonnet-4-20250514',
69
- api_key: ENV['ANTHROPIC_API_KEY'])
70
- end
71
-
72
- # Google Gemini
73
- DSPy.configure do |c|
74
- c.lm = DSPy::LM.new('gemini/gemini-2.5-flash',
75
- api_key: ENV['GEMINI_API_KEY'])
76
- end
77
-
78
- # Ollama (local, free)
79
- DSPy.configure do |c|
80
- c.lm = DSPy::LM.new('ollama/llama3.2')
81
- end
82
-
83
- # OpenRouter (200+ models)
84
- DSPy.configure do |c|
85
- c.lm = DSPy::LM.new('openrouter/deepseek/deepseek-chat-v3.1:free',
86
- api_key: ENV['OPENROUTER_API_KEY'])
87
- end
88
- ```
89
-
90
- ### Define a Signature
13
+ The `1.x` series is the current stable release line.
91
14
 
92
- Signatures are typed contracts for LLM operations. Define inputs, outputs, and let DSPy handle the prompt:
15
+ In a configured application, the task contract and call look like this:
93
16
 
94
17
  ```ruby
95
18
  class Classify < DSPy::Signature
96
- description "Classify sentiment of a given sentence."
97
-
98
19
  class Sentiment < T::Enum
99
20
  enums do
100
21
  Positive = new('positive')
@@ -104,7 +25,7 @@ class Classify < DSPy::Signature
104
25
  end
105
26
 
106
27
  input do
107
- const :sentence, String, description: 'The sentence to analyze'
28
+ const :sentence, String
108
29
  end
109
30
 
110
31
  output do
@@ -114,107 +35,39 @@ class Classify < DSPy::Signature
114
35
  end
115
36
 
116
37
  classifier = DSPy::Predict.new(Classify)
117
- result = classifier.call(sentence: "This book was super fun to read!")
118
-
119
- result.sentiment # => #<Sentiment::Positive>
120
- result.confidence # => 0.92
38
+ result = classifier.call(sentence: "This book was fun to read!")
121
39
  ```
122
40
 
123
- ### Chain of Thought
124
-
125
- For complex reasoning, use `ChainOfThought` to get step-by-step explanations:
126
-
127
- ```ruby
128
- solver = DSPy::ChainOfThought.new(MathProblem)
129
- result = solver.call(problem: "If a train travels 120km in 2 hours, what's its speed?")
130
-
131
- result.reasoning # => "Speed = Distance / Time = 120km / 2h = 60km/h"
132
- result.answer # => "60 km/h"
133
- ```
41
+ Define a typed task contract instead of maintaining an output template. Receive validated Ruby values instead of parsing provider JSON. Handle configuration, transport, and validation errors explicitly.
134
42
 
135
- ### ReAct Agents
136
-
137
- Build agents that use tools to accomplish tasks:
138
-
139
- ```ruby
140
- class SearchTool < DSPy::Tools::Base
141
- tool_name "search"
142
- tool_description "Search for information"
143
-
144
- sig { params(query: String).returns(String) }
145
- def call(query:)
146
- # Your search implementation
147
- "Result 1, Result 2"
148
- end
149
- end
150
-
151
- agent = DSPy::ReAct.new(ResearchTask, tools: [SearchTool.new], max_iterations: 5)
152
- result = agent.call(question: "What's the latest on Ruby 3.4?")
153
- ```
43
+ ## Start Here
154
44
 
155
- ## What's Included
45
+ The [Quick Start](https://oss.vicente.services/dspy.rb/getting-started/quick-start/) is the complete supported path: install the core and provider gems, configure a key, save a program, and run it.
156
46
 
157
- **Core Modules**: Predict, ChainOfThought, ReAct agents, and composable pipelines.
47
+ Use [Installation](https://oss.vicente.services/dspy.rb/getting-started/installation/) to choose a provider. The [package and capability matrix](https://oss.vicente.services/dspy.rb/getting-started/packages/) records exact gem names, require behavior, support labels, and model or SDK boundaries.
158
48
 
159
- **Type Safety**: Sorbet-based runtime validation. Enums, unions, nested structs—all work.
49
+ ## Mental Model
160
50
 
161
- **Multimodal**: Image analysis with `DSPy::Image` for vision-capable models.
51
+ - A `DSPy::Signature` defines the task contract.
52
+ - `DSPy::Predict` and other modules choose an execution strategy.
53
+ - Ordinary Ruby owns fixed sequencing, branching, persistence, permissions, and failure policy.
54
+ - Results cross a runtime validation boundary; validation establishes shape, not factual correctness.
162
55
 
163
- **Observability**: Zero-config Langfuse integration via OpenTelemetry. Non-blocking, production-ready.
164
-
165
- **Optimization**: MIPROv2 (Bayesian optimization) and GEPA (genetic evolution) for prompt tuning.
166
-
167
- **Provider Support**: OpenAI, Anthropic, Gemini, Ollama, and OpenRouter via official SDKs.
168
-
169
- ## Documentation
170
-
171
- **[Full Documentation](https://oss.vicente.services/dspy.rb/)** — Getting started, core concepts, advanced patterns.
172
-
173
- **[llms.txt](https://oss.vicente.services/dspy.rb/llms.txt)** — LLM-friendly reference for AI assistants.
174
-
175
- ### Claude Skill
176
-
177
- A [Claude Skill](https://github.com/vicentereig/dspy-rb-skill) is available to help you build DSPy.rb applications:
178
-
179
- ```bash
180
- # Claude Code — install from the vicentereig/engineering marketplace
181
- claude install-skill vicentereig/engineering --skill dspy-rb
182
- ```
183
-
184
- For Claude.ai Pro/Max, download the [skill ZIP](https://github.com/vicentereig/dspy-rb-skill/archive/refs/heads/main.zip) and upload via Settings > Skills.
185
-
186
- ## Examples
187
-
188
- The [examples/](examples/) directory has runnable code for common patterns:
189
-
190
- - Sentiment classification
191
- - ReAct agents with tools
192
- - Image analysis
193
- - Prompt optimization
194
-
195
- ```bash
196
- bundle exec ruby examples/basic_search_agent.rb
197
- ```
56
+ ### ReAct Agents
198
57
 
199
- ## Optional Gems
58
+ Use `ReAct` when the model needs to choose among typed Ruby tools inside an iteration bound. The application still owns tool authorization, side effects, budgets, and errors. See [Predictors](https://oss.vicente.services/dspy.rb/core-concepts/predictors/) and [Toolsets](https://oss.vicente.services/dspy.rb/core-concepts/toolsets/).
200
59
 
201
- DSPy.rb ships sibling gems for features with heavier dependencies. Add them as needed:
60
+ ## Explore
202
61
 
203
- | Gem | What it does |
204
- | --- | --- |
205
- | `dspy-datasets` | Dataset helpers, Parquet/Polars tooling |
206
- | `dspy-evals` | Evaluation harness with metrics and callbacks |
207
- | `dspy-miprov2` | Bayesian optimization for prompt tuning |
208
- | `dspy-gepa` | Genetic-Pareto prompt evolution |
209
- | `dspy-o11y-langfuse` | Auto-configure Langfuse tracing |
210
- | `dspy-code_act` | Think-Code-Observe agents |
211
- | `dspy-deep_search` | Production DeepSearch with Exa |
62
+ - [Documentation](https://oss.vicente.services/dspy.rb/) task-oriented guides and reference
63
+ - [Examples](examples/) repository demos indexed by capability and prerequisites
64
+ - [llms.txt](https://oss.vicente.services/dspy.rb/llms.txt) generated reference for AI assistants
212
65
 
213
- See [the full list](https://oss.vicente.services/dspy.rb/getting-started/installation/) in the docs.
66
+ Provider adapters, optimizers, observability exporters, datasets, and code-executing agents are separate packages when they add dependencies. Check the package matrix before selecting one; package availability does not guarantee uniform provider or model behavior.
214
67
 
215
68
  ## Contributing
216
69
 
217
- Feedback is invaluable. If you encounter issues, [open an issue](https://github.com/vicentereig/dspy.rb/issues). For suggestions, [start a discussion](https://github.com/vicentereig/dspy.rb/discussions).
70
+ For bugs, [open an issue](https://github.com/vicentereig/dspy.rb/issues). For suggestions, [start a discussion](https://github.com/vicentereig/dspy.rb/discussions).
218
71
 
219
72
  Want to contribute code? Reach out: hey at vicente.services
220
73
 
@@ -3,20 +3,29 @@
3
3
  require 'anthropic'
4
4
  require 'dspy/lm/vision_models'
5
5
  require 'dspy/lm/adapter'
6
-
7
- require 'dspy/anthropic/guardrails'
8
- DSPy::Anthropic::Guardrails.ensure_anthropic_installed!
6
+ require 'dspy/anthropic/lm/model_capabilities'
9
7
 
10
8
  module DSPy
11
9
  module Anthropic
12
10
  module LM
13
11
  module Adapters
14
12
  class AnthropicAdapter < DSPy::LM::Adapter
15
- def initialize(model:, api_key:, structured_outputs: true)
13
+ # Distinguishes "temperature not passed" from "temperature: nil" so the
14
+ # implicit default (see #effective_temperature) can be model/reasoning-aware
15
+ # while still letting callers force an explicit value, including nil.
16
+ NOT_SET = Object.new.freeze
17
+
18
+ def initialize(model:, api_key:, structured_outputs: true, reasoning: nil, temperature: NOT_SET, max_tokens: 4096)
16
19
  super(model: model, api_key: api_key)
17
20
  validate_api_key!(api_key, 'anthropic')
18
21
  @client = ::Anthropic::Client.new(api_key: api_key)
19
22
  @structured_outputs_enabled = structured_outputs
23
+ @capability = DSPy::Anthropic::LM::ModelCapabilities.for(model)
24
+ @max_tokens = max_tokens
25
+ @temperature_explicit = !temperature.equal?(NOT_SET)
26
+ @temperature = @temperature_explicit ? temperature : nil
27
+ @thinking_param = build_thinking_param(reasoning)
28
+ @effort_param = build_effort_param(reasoning)
20
29
  end
21
30
 
22
31
  def chat(messages:, signature: nil, **extra_params, &block)
@@ -25,32 +34,48 @@ module DSPy
25
34
  # Validate vision support if images are present
26
35
  if contains_images?(normalized_messages)
27
36
  DSPy::LM::VisionModels.validate_vision_support!('anthropic', model)
28
- # Convert messages to Anthropic format with proper image handling
37
+ end
38
+
39
+ # Convert multimodal messages to Anthropic format
40
+ if contains_media?(normalized_messages)
29
41
  normalized_messages = format_multimodal_messages(normalized_messages, 'anthropic')
30
42
  end
31
43
 
32
44
  # Anthropic requires system message to be separate from messages
33
45
  system_message, user_messages = extract_system_message(normalized_messages)
34
46
 
35
- # Extract Beta API parameters if present
36
47
  output_format = extra_params.delete(:output_format)
37
- betas = extra_params.delete(:betas)
48
+ extra_params.delete(:betas) # deprecated Beta API param; ignored if a caller still passes it
38
49
 
39
50
  # Check if this is a tool use request
40
51
  has_tools = extra_params.key?(:tools) && !extra_params[:tools].empty?
41
52
 
42
- # Apply JSON prefilling if needed for better Claude JSON compliance (but not for tool use or Beta API)
43
- unless has_tools || output_format || contains_images?(normalized_messages)
53
+ # Apply JSON prefilling if needed for better Claude JSON compliance (but not for tool use or structured output)
54
+ unless has_tools || output_format || contains_media?(normalized_messages)
44
55
  user_messages = prepare_messages_for_json(user_messages, system_message)
45
56
  end
46
57
 
58
+ # A per-call `temperature:` (passed to #chat, e.g. via a strategy or a
59
+ # direct caller) takes priority over the adapter's own default/effective
60
+ # temperature — restores pre-#256 override semantics. `nil` here means
61
+ # "omit the key", same as the constructor-level sentinel.
62
+ per_call_temperature_given = extra_params.key?(:temperature)
63
+ per_call_temperature = extra_params.delete(:temperature)
64
+
47
65
  request_params = {
48
66
  model: model,
49
67
  messages: user_messages,
50
- max_tokens: 4096, # Required for Anthropic
51
- temperature: 0.0 # DSPy default for deterministic responses
68
+ max_tokens: @max_tokens
52
69
  }.merge(extra_params)
53
70
 
71
+ temperature = per_call_temperature_given ? per_call_temperature : effective_temperature
72
+ request_params[:temperature] = temperature unless temperature.nil?
73
+
74
+ request_params[:thinking] = @thinking_param if @thinking_param
75
+
76
+ output_config = build_output_config(output_format)
77
+ request_params[:output_config] = output_config if output_config
78
+
54
79
  # Add system message if present
55
80
  request_params[:system] = system_message if system_message
56
81
 
@@ -61,23 +86,17 @@ module DSPy
61
86
 
62
87
  begin
63
88
  if block_given?
89
+ # Anthropic::Resources::Messages#stream does not accept or
90
+ # yield to a block itself -- it returns a MessageStream that
91
+ # the caller consumes via #each/#text. Passing a `do...end`
92
+ # block directly to #stream (the pre-#259 approach) is
93
+ # silently discarded by Ruby, so the block body never runs
94
+ # and `content` never accumulates anything.
64
95
  content = ""
65
- if output_format
66
- @client.beta.messages.stream(**request_params, output_format: output_format, betas: betas) do |chunk|
67
- if chunk.respond_to?(:delta) && chunk.delta.respond_to?(:text)
68
- chunk_text = chunk.delta.text
69
- content += chunk_text
70
- block.call(chunk)
71
- end
72
- end
73
- else
74
- @client.messages.stream(**request_params) do |chunk|
75
- if chunk.respond_to?(:delta) && chunk.delta.respond_to?(:text)
76
- chunk_text = chunk.delta.text
77
- content += chunk_text
78
- block.call(chunk)
79
- end
80
- end
96
+ stream = @client.messages.stream(**request_params)
97
+ stream.text.each do |text_fragment|
98
+ content += text_fragment
99
+ block.call(text_fragment)
81
100
  end
82
101
 
83
102
  # Create typed metadata for streaming response
@@ -92,11 +111,7 @@ module DSPy
92
111
  metadata: metadata
93
112
  )
94
113
  else
95
- response = if output_format
96
- @client.beta.messages.create(**request_params, output_format: output_format, betas: betas)
97
- else
98
- @client.messages.create(**request_params)
99
- end
114
+ response = @client.messages.create(**request_params)
100
115
 
101
116
  if response.respond_to?(:error) && response.error
102
117
  raise DSPy::LM::AdapterError, "Anthropic API error: #{response.error}"
@@ -168,6 +183,117 @@ module DSPy
168
183
 
169
184
  private
170
185
 
186
+ # Merges structured-output format and reasoning effort into the single
187
+ # `output_config` shape Anthropic expects (replaces the deprecated
188
+ # `output_format`/`betas` Beta API params).
189
+ def build_output_config(output_format)
190
+ config = {}
191
+ config[:format] = output_format if output_format
192
+ config[:effort] = @effort_param if @effort_param
193
+ config.empty? ? nil : config
194
+ end
195
+
196
+ # `nil` reasoning => no `thinking` param at all (classic behavior).
197
+ #
198
+ # Effort-only reasoning (.low/.medium/.high/.xhigh/.max) normally
199
+ # returns nil here too — #build_effort_param/#build_output_config
200
+ # handle `output_config.effort` on their own. The one exception is
201
+ # "opt-in adaptive" model families (Opus 4.7/4.8, Opus/Sonnet 4.6):
202
+ # per Anthropic's docs, those models run *without* thinking unless
203
+ # `thinking: { type: "adaptive" }` is explicitly set, regardless of
204
+ # `output_config.effort`. Without this, `DSPy::Reasoning.high` would
205
+ # silently not enable any actual reasoning on those models — see
206
+ # PR #257 review.
207
+ def build_thinking_param(reasoning)
208
+ return nil if reasoning.nil?
209
+
210
+ if reasoning.budget_tokens
211
+ validate_manual_budget!(reasoning.budget_tokens)
212
+ { type: :enabled, budget_tokens: reasoning.budget_tokens }
213
+ elsif reasoning.adaptive
214
+ validate_adaptive!
215
+ { type: :adaptive }
216
+ elsif reasoning.disabled
217
+ validate_thinking_disable!
218
+ { type: :disabled }
219
+ elsif reasoning.effort && @capability.adaptive_thinking == :opt_in
220
+ { type: :adaptive }
221
+ end
222
+ end
223
+
224
+ def build_effort_param(reasoning)
225
+ return nil unless reasoning&.effort
226
+
227
+ validate_effort!(reasoning.effort)
228
+ reasoning.effort.serialize.to_sym
229
+ end
230
+
231
+ def validate_manual_budget!(budget_tokens)
232
+ if @capability.manual_budget == false
233
+ raise DSPy::LM::ConfigurationError,
234
+ "#{model} does not support manual thinking budgets (DSPy::Reasoning.budget). " \
235
+ "This model only supports adaptive thinking; use DSPy::Reasoning.adaptive or an effort tier instead."
236
+ end
237
+
238
+ if budget_tokens < 1024
239
+ raise DSPy::LM::ConfigurationError,
240
+ "budget_tokens must be >= 1024 (Anthropic's documented minimum), got #{budget_tokens}."
241
+ end
242
+
243
+ if budget_tokens >= @max_tokens
244
+ raise DSPy::LM::ConfigurationError,
245
+ "budget_tokens (#{budget_tokens}) must be less than max_tokens (#{@max_tokens}). " \
246
+ "Increase max_tokens: when constructing DSPy::LM, or lower the requested budget."
247
+ end
248
+ end
249
+
250
+ def validate_adaptive!
251
+ if @capability.adaptive_thinking == false
252
+ raise DSPy::LM::ConfigurationError,
253
+ "#{model} does not support adaptive thinking. Use DSPy::Reasoning.budget(tokens) instead."
254
+ end
255
+ end
256
+
257
+ def validate_thinking_disable!
258
+ unless @capability.thinking_disable
259
+ raise DSPy::LM::ConfigurationError,
260
+ "#{model} cannot disable extended thinking: it is always on for this model family."
261
+ end
262
+ end
263
+
264
+ def validate_effort!(effort)
265
+ unless @capability.effort
266
+ raise DSPy::LM::ConfigurationError,
267
+ "#{model} does not support DSPy::Reasoning effort tiers (output_config.effort). " \
268
+ "This model predates Anthropic's effort parameter."
269
+ end
270
+
271
+ if effort == DSPy::Reasoning::Effort::XHigh && !@capability.xhigh_effort
272
+ raise DSPy::LM::ConfigurationError, "#{model} does not support DSPy::Reasoning.xhigh."
273
+ end
274
+
275
+ if effort == DSPy::Reasoning::Effort::Max && !@capability.max_effort
276
+ raise DSPy::LM::ConfigurationError, "#{model} does not support DSPy::Reasoning.max."
277
+ end
278
+ end
279
+
280
+ # Three-state temperature: explicit value (including nil) always wins;
281
+ # otherwise omit for models that reject non-default sampling params, or
282
+ # while extended thinking is actually engaged (Anthropic: "Thinking isn't
283
+ # compatible with temperature"); otherwise fall back to DSPy's classic
284
+ # deterministic default.
285
+ def effective_temperature
286
+ return @temperature if @temperature_explicit
287
+ return nil if @capability.fixed_sampling
288
+ return nil if thinking_active?
289
+
290
+ 0.0
291
+ end
292
+
293
+ def thinking_active?
294
+ !@thinking_param.nil? && @thinking_param[:type] != :disabled
295
+ end
296
+
171
297
  # Enhanced JSON extraction specifically for Claude models
172
298
  # Handles multiple patterns of markdown-wrapped JSON responses
173
299
  def extract_json_from_response(content)
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DSPy
4
+ module Anthropic
5
+ module LM
6
+ # Per-model-family reasoning/sampling capabilities.
7
+ #
8
+ # Anthropic ships new Claude model generations every few months, each with
9
+ # different `thinking`/`effort`/sampling-parameter support. Rather than
10
+ # scatter model-name regexes through the adapter, this registry centralizes
11
+ # them so validation logic can ask a single question ("does this model
12
+ # support X?") instead of re-deriving it from the model string each time.
13
+ #
14
+ # Source (verified 2026-07-09):
15
+ # - https://platform.claude.com/docs/en/build-with-claude/adaptive-thinking
16
+ # - https://platform.claude.com/docs/en/build-with-claude/effort
17
+ #
18
+ # Unrecognized models (including future Anthropic releases not yet added
19
+ # here) fall back to +DEFAULT+: the classic, pre-adaptive-thinking
20
+ # behavior this gem has always assumed (manual `budget_tokens` only, no
21
+ # named effort tiers, unrestricted `temperature`). This is a deliberately
22
+ # conservative choice: it keeps existing behavior for models we don't
23
+ # know about, at the cost of `DSPy::Reasoning` effort tiers not working
24
+ # on brand-new models until this registry is updated.
25
+ module ModelCapabilities
26
+ Capability = Struct.new(
27
+ :adaptive_thinking, # :always_on | :default_on | :opt_in | false
28
+ :manual_budget, # true | :deprecated | false
29
+ :thinking_disable, # true | false — supports `thinking: {type: 'disabled'}`
30
+ :effort, # true | false — supports `output_config.effort` at all
31
+ :xhigh_effort, # true | false
32
+ :max_effort, # true | false
33
+ :fixed_sampling, # true => rejects non-default temperature/top_p/top_k
34
+ keyword_init: true
35
+ ) do
36
+ def to_h
37
+ {
38
+ adaptive_thinking: adaptive_thinking,
39
+ manual_budget: manual_budget,
40
+ thinking_disable: thinking_disable,
41
+ effort: effort,
42
+ xhigh_effort: xhigh_effort,
43
+ max_effort: max_effort,
44
+ fixed_sampling: fixed_sampling
45
+ }
46
+ end
47
+ end
48
+ private_constant :Capability
49
+
50
+ FABLE_MYTHOS_5 = Capability.new(
51
+ adaptive_thinking: :always_on, manual_budget: false, thinking_disable: false,
52
+ effort: true, xhigh_effort: true, max_effort: true, fixed_sampling: true
53
+ ).freeze
54
+
55
+ # fixed_sampling: true per PR #257 review (vicentereig, 2026-07-11) —
56
+ # Anthropic documents Mythos Preview as rejecting non-default
57
+ # temperature/top_p/top_k, same failure class as #256.
58
+ MYTHOS_PREVIEW = Capability.new(
59
+ adaptive_thinking: :default_on, manual_budget: true, thinking_disable: false,
60
+ effort: true, xhigh_effort: false, max_effort: true, fixed_sampling: true
61
+ ).freeze
62
+
63
+ OPUS_4_7_OR_4_8 = Capability.new(
64
+ adaptive_thinking: :opt_in, manual_budget: false, thinking_disable: true,
65
+ effort: true, xhigh_effort: true, max_effort: true, fixed_sampling: true
66
+ ).freeze
67
+
68
+ SONNET_5 = Capability.new(
69
+ adaptive_thinking: :default_on, manual_budget: false, thinking_disable: true,
70
+ effort: true, xhigh_effort: true, max_effort: true, fixed_sampling: true
71
+ ).freeze
72
+
73
+ OPUS_OR_SONNET_4_6 = Capability.new(
74
+ adaptive_thinking: :opt_in, manual_budget: :deprecated, thinking_disable: true,
75
+ effort: true, xhigh_effort: false, max_effort: true, fixed_sampling: false
76
+ ).freeze
77
+
78
+ OPUS_4_5 = Capability.new(
79
+ adaptive_thinking: false, manual_budget: true, thinking_disable: true,
80
+ effort: true, xhigh_effort: false, max_effort: false, fixed_sampling: false
81
+ ).freeze
82
+
83
+ # Conservative fallback for any model not explicitly listed above,
84
+ # including older Claude models and models Anthropic ships after this
85
+ # file is written. Matches this gem's pre-#256 behavior exactly.
86
+ DEFAULT = Capability.new(
87
+ adaptive_thinking: false, manual_budget: true, thinking_disable: true,
88
+ effort: false, xhigh_effort: false, max_effort: false, fixed_sampling: false
89
+ ).freeze
90
+
91
+ # Ordered [pattern, capability] pairs. Patterns are matched with
92
+ # `String#match?` against the bare model name (i.e. without the
93
+ # "anthropic/" provider prefix DSPy::LM strips before constructing the
94
+ # adapter). `\b` after the version number allows dated suffixes
95
+ # (e.g. "claude-sonnet-5-20260315") while still rejecting unrelated
96
+ # models that merely share a numeric prefix (e.g. "claude-sonnet-50").
97
+ FAMILIES = [
98
+ [/\Aclaude-(fable|mythos)-5\b/, FABLE_MYTHOS_5],
99
+ [/\Aclaude-mythos-preview\b/, MYTHOS_PREVIEW],
100
+ [/\Aclaude-opus-4-[78]\b/, OPUS_4_7_OR_4_8],
101
+ [/\Aclaude-sonnet-5\b/, SONNET_5],
102
+ [/\Aclaude-(opus|sonnet)-4-6\b/, OPUS_OR_SONNET_4_6],
103
+ [/\Aclaude-opus-4-5\b/, OPUS_4_5]
104
+ ].freeze
105
+
106
+ def self.for(model)
107
+ _, capability = FAMILIES.find { |pattern, _| pattern.match?(model) }
108
+ capability || DEFAULT
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module DSPy
4
4
  module Anthropic
5
- VERSION = '1.0.4'
5
+ VERSION = '1.0.6'
6
6
  end
7
7
  end
@@ -3,7 +3,4 @@
3
3
  require 'dspy/anthropic/version'
4
4
  require 'dspy/anthropic/errors'
5
5
 
6
- require 'dspy/anthropic/guardrails'
7
- DSPy::Anthropic::Guardrails.ensure_anthropic_installed!
8
-
9
6
  require 'dspy/anthropic/lm/adapters/anthropic_adapter'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dspy-anthropic
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.4
4
+ version: 1.0.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vicente Reig Rincón de Arellano
@@ -15,28 +15,40 @@ dependencies:
15
15
  requirements:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: '0.30'
18
+ version: 1.0.2
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '2.0'
19
22
  type: :runtime
20
23
  prerelease: false
21
24
  version_requirements: !ruby/object:Gem::Requirement
22
25
  requirements:
23
26
  - - ">="
24
27
  - !ruby/object:Gem::Version
25
- version: '0.30'
28
+ version: 1.0.2
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '2.0'
26
32
  - !ruby/object:Gem::Dependency
27
33
  name: anthropic
28
34
  requirement: !ruby/object:Gem::Requirement
29
35
  requirements:
30
36
  - - ">="
31
37
  - !ruby/object:Gem::Version
32
- version: '0'
38
+ version: 1.28.0
39
+ - - "<"
40
+ - !ruby/object:Gem::Version
41
+ version: '2.0'
33
42
  type: :runtime
34
43
  prerelease: false
35
44
  version_requirements: !ruby/object:Gem::Requirement
36
45
  requirements:
37
46
  - - ">="
38
47
  - !ruby/object:Gem::Version
39
- version: '0'
48
+ version: 1.28.0
49
+ - - "<"
50
+ - !ruby/object:Gem::Version
51
+ version: '2.0'
40
52
  description: Provides the AnthropicAdapter so Claude-compatible providers can be added
41
53
  to DSPy.rb projects independently of the core gem.
42
54
  email:
@@ -49,8 +61,8 @@ files:
49
61
  - README.md
50
62
  - lib/dspy/anthropic.rb
51
63
  - lib/dspy/anthropic/errors.rb
52
- - lib/dspy/anthropic/guardrails.rb
53
64
  - lib/dspy/anthropic/lm/adapters/anthropic_adapter.rb
65
+ - lib/dspy/anthropic/lm/model_capabilities.rb
54
66
  - lib/dspy/anthropic/lm/schema_converter.rb
55
67
  - lib/dspy/anthropic/version.rb
56
68
  homepage: https://github.com/vicentereig/dspy.rb
@@ -1,24 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'dspy/lm/errors'
4
-
5
- module DSPy
6
- module Anthropic
7
- class Guardrails
8
- SUPPORTED_ANTHROPIC_VERSIONS = "~> 1.12".freeze
9
-
10
- def self.ensure_anthropic_installed!
11
- require 'anthropic'
12
-
13
- spec = Gem.loaded_specs["anthropic"]
14
- unless spec && Gem::Requirement.new(SUPPORTED_ANTHROPIC_VERSIONS).satisfied_by?(spec.version)
15
- msg = <<~MSG
16
- DSPY requires the `anthropic` gem #{SUPPORTED_ANTHROPIC_VERSIONS}.
17
- Please install or upgrade it with `bundle add anthropic --version "#{SUPPORTED_ANTHROPIC_VERSIONS}"`.
18
- MSG
19
- raise DSPy::LM::UnsupportedVersionError, msg
20
- end
21
- end
22
- end
23
- end
24
- end