dspy-anthropic 1.0.5 → 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: 85982867f2b7d1d926d83ab4cecffa9c4610570b60f6b395a023b333a5b80a1f
4
- data.tar.gz: 73520e61cb128b3ef403a7f8640f1b1ccb9fbaaed6c4a0278a7ca26a2170e387
3
+ metadata.gz: b8d68b501f25927a814cc35c0af2c7f0e5f3b5bd1bb03f3b48df61111977bdee
4
+ data.tar.gz: 58af722a168077708cde9ab723d4004c5cf47204adad3e75ece1a15fd25455ce
5
5
  SHA512:
6
- metadata.gz: 93a9d89923aa1409a35dd4bbba9c9c62c3ce0ed4f58b22931747d843678093f19a301e391b96768611b02c305fe43f15de29d26151014f711d619629624f621a
7
- data.tar.gz: 82d5cb3944ef321fc8b6b5bc0986433e3d88d98db5b368b69048f054503bf83c6acb0e18911a450e1272effc86ba65d802d98faf6e1b35e64f85c3649b46324d
6
+ metadata.gz: c070133fcdac4bfc561c37084bcce94b31a7564ae3e8b872b80593684e105f71dbe80a1c84e06f0cc7971649bbc8d5c66a9d3333f739ed643a5e04f59afffca3
7
+ data.tar.gz: 1da253c16a11737bb777d49639906ef09afe7b62188d36937bfd405582acc3de318169bfb7b95b41220ae042988f73079028d77f0cb63429f20c9b0793b63813
data/README.md CHANGED
@@ -6,96 +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.
12
- The `1.x` line is the stable release track for production Ruby LLM applications.
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.
13
12
 
14
- ```ruby
15
- require 'dspy'
16
-
17
- DSPy.configure do |c|
18
- c.lm = DSPy::LM.new('openai/gpt-4o-mini', api_key: ENV['OPENAI_API_KEY'])
19
- end
20
-
21
- class Summarize < DSPy::Signature
22
- description "Summarize the given text in one sentence."
23
-
24
- input do
25
- const :text, String
26
- end
27
-
28
- output do
29
- const :summary, String
30
- end
31
- end
32
-
33
- summarizer = DSPy::Predict.new(Summarize)
34
- result = summarizer.call(text: "DSPy.rb brings structured LLM programming to Ruby...")
35
- puts result.summary
36
- ```
37
-
38
- That's it. No prompt templates. No JSON parsing. No prayer-based error handling.
39
-
40
- ## Installation
41
-
42
- ```ruby
43
- # Gemfile
44
- gem 'dspy'
45
- gem 'dspy-openai' # For OpenAI, OpenRouter, or Ollama
46
- # gem 'dspy-anthropic' # For Claude
47
- # gem 'dspy-gemini' # For Gemini
48
- # gem 'dspy-ruby_llm' # For 12+ providers via RubyLLM
49
- ```
50
-
51
- ```bash
52
- bundle install
53
- ```
54
-
55
- ## Quick Start
56
-
57
- ### Configure Your LLM
58
-
59
- ```ruby
60
- # OpenAI
61
- DSPy.configure do |c|
62
- c.lm = DSPy::LM.new('openai/gpt-4o-mini',
63
- api_key: ENV['OPENAI_API_KEY'],
64
- structured_outputs: true)
65
- end
66
-
67
- # Anthropic Claude
68
- DSPy.configure do |c|
69
- c.lm = DSPy::LM.new('anthropic/claude-sonnet-4-20250514',
70
- api_key: ENV['ANTHROPIC_API_KEY'])
71
- end
72
-
73
- # Google Gemini
74
- DSPy.configure do |c|
75
- c.lm = DSPy::LM.new('gemini/gemini-2.5-flash',
76
- api_key: ENV['GEMINI_API_KEY'])
77
- end
78
-
79
- # Ollama (local, free)
80
- DSPy.configure do |c|
81
- c.lm = DSPy::LM.new('ollama/llama3.2')
82
- end
83
-
84
- # OpenRouter (200+ models)
85
- DSPy.configure do |c|
86
- c.lm = DSPy::LM.new('openrouter/deepseek/deepseek-chat-v3.1:free',
87
- api_key: ENV['OPENROUTER_API_KEY'])
88
- end
89
- ```
90
-
91
- ### Define a Signature
13
+ The `1.x` series is the current stable release line.
92
14
 
93
- 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:
94
16
 
95
17
  ```ruby
96
18
  class Classify < DSPy::Signature
97
- description "Classify sentiment of a given sentence."
98
-
99
19
  class Sentiment < T::Enum
100
20
  enums do
101
21
  Positive = new('positive')
@@ -105,7 +25,7 @@ class Classify < DSPy::Signature
105
25
  end
106
26
 
107
27
  input do
108
- const :sentence, String, description: 'The sentence to analyze'
28
+ const :sentence, String
109
29
  end
110
30
 
111
31
  output do
@@ -115,107 +35,39 @@ class Classify < DSPy::Signature
115
35
  end
116
36
 
117
37
  classifier = DSPy::Predict.new(Classify)
118
- result = classifier.call(sentence: "This book was super fun to read!")
119
-
120
- result.sentiment # => #<Sentiment::Positive>
121
- result.confidence # => 0.92
38
+ result = classifier.call(sentence: "This book was fun to read!")
122
39
  ```
123
40
 
124
- ### Chain of Thought
125
-
126
- For complex reasoning, use `ChainOfThought` to get step-by-step explanations:
127
-
128
- ```ruby
129
- solver = DSPy::ChainOfThought.new(MathProblem)
130
- result = solver.call(problem: "If a train travels 120km in 2 hours, what's its speed?")
131
-
132
- result.reasoning # => "Speed = Distance / Time = 120km / 2h = 60km/h"
133
- result.answer # => "60 km/h"
134
- ```
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.
135
42
 
136
- ### ReAct Agents
137
-
138
- Build agents that use tools to accomplish tasks:
139
-
140
- ```ruby
141
- class SearchTool < DSPy::Tools::Base
142
- tool_name "search"
143
- tool_description "Search for information"
144
-
145
- sig { params(query: String).returns(String) }
146
- def call(query:)
147
- # Your search implementation
148
- "Result 1, Result 2"
149
- end
150
- end
151
-
152
- agent = DSPy::ReAct.new(ResearchTask, tools: [SearchTool.new], max_iterations: 5)
153
- result = agent.call(question: "What's the latest on Ruby 3.4?")
154
- ```
43
+ ## Start Here
155
44
 
156
- ## 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.
157
46
 
158
- **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.
159
48
 
160
- **Type Safety**: Sorbet-based runtime validation. Enums, unions, nested structs—all work.
49
+ ## Mental Model
161
50
 
162
- **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.
163
55
 
164
- **Observability**: Zero-config Langfuse integration via OpenTelemetry. Non-blocking, production-ready.
165
-
166
- **Optimization**: MIPROv2 (Bayesian optimization) and GEPA (genetic evolution) for prompt tuning.
167
-
168
- **Provider Support**: OpenAI, Anthropic, Gemini, Ollama, and OpenRouter via official SDKs.
169
-
170
- ## Documentation
171
-
172
- **[Full Documentation](https://oss.vicente.services/dspy.rb/)** — Getting started, core concepts, advanced patterns.
173
-
174
- **[llms.txt](https://oss.vicente.services/dspy.rb/llms.txt)** — LLM-friendly reference for AI assistants.
175
-
176
- ### Claude Skill
177
-
178
- A [Claude Skill](https://github.com/vicentereig/dspy-rb-skill) is available to help you build DSPy.rb applications:
179
-
180
- ```bash
181
- # Claude Code — install from the vicentereig/engineering marketplace
182
- claude install-skill vicentereig/engineering --skill dspy-rb
183
- ```
184
-
185
- 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.
186
-
187
- ## Examples
188
-
189
- The [examples/](examples/) directory has runnable code for common patterns:
190
-
191
- - Sentiment classification
192
- - ReAct agents with tools
193
- - Image analysis
194
- - Prompt optimization
195
-
196
- ```bash
197
- bundle exec ruby examples/basic_search_agent.rb
198
- ```
56
+ ### ReAct Agents
199
57
 
200
- ## 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/).
201
59
 
202
- DSPy.rb ships sibling gems for features with heavier dependencies. Add them as needed:
60
+ ## Explore
203
61
 
204
- | Gem | What it does |
205
- | --- | --- |
206
- | `dspy-datasets` | Dataset helpers, Parquet/Polars tooling |
207
- | `dspy-evals` | Evaluation harness with metrics and callbacks |
208
- | `dspy-miprov2` | Bayesian optimization for prompt tuning |
209
- | `dspy-gepa` | Genetic-Pareto prompt evolution |
210
- | `dspy-o11y-langfuse` | Auto-configure Langfuse tracing |
211
- | `dspy-code_act` | Think-Code-Observe agents |
212
- | `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
213
65
 
214
- 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.
215
67
 
216
68
  ## Contributing
217
69
 
218
- 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).
219
71
 
220
72
  Want to contribute code? Reach out: hey at vicente.services
221
73
 
@@ -3,17 +3,29 @@
3
3
  require 'anthropic'
4
4
  require 'dspy/lm/vision_models'
5
5
  require 'dspy/lm/adapter'
6
+ require 'dspy/anthropic/lm/model_capabilities'
6
7
 
7
8
  module DSPy
8
9
  module Anthropic
9
10
  module LM
10
11
  module Adapters
11
12
  class AnthropicAdapter < DSPy::LM::Adapter
12
- 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)
13
19
  super(model: model, api_key: api_key)
14
20
  validate_api_key!(api_key, 'anthropic')
15
21
  @client = ::Anthropic::Client.new(api_key: api_key)
16
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)
17
29
  end
18
30
 
19
31
  def chat(messages:, signature: nil, **extra_params, &block)
@@ -32,25 +44,38 @@ module DSPy
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)
53
+ # Apply JSON prefilling if needed for better Claude JSON compliance (but not for tool use or structured output)
43
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.5'
5
+ VERSION = '1.0.6'
6
6
  end
7
7
  end
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.5
4
+ version: 1.0.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vicente Reig Rincón de Arellano
@@ -15,7 +15,7 @@ dependencies:
15
15
  requirements:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: 0.30.1
18
+ version: 1.0.2
19
19
  - - "<"
20
20
  - !ruby/object:Gem::Version
21
21
  version: '2.0'
@@ -25,7 +25,7 @@ dependencies:
25
25
  requirements:
26
26
  - - ">="
27
27
  - !ruby/object:Gem::Version
28
- version: 0.30.1
28
+ version: 1.0.2
29
29
  - - "<"
30
30
  - !ruby/object:Gem::Version
31
31
  version: '2.0'
@@ -62,6 +62,7 @@ files:
62
62
  - lib/dspy/anthropic.rb
63
63
  - lib/dspy/anthropic/errors.rb
64
64
  - lib/dspy/anthropic/lm/adapters/anthropic_adapter.rb
65
+ - lib/dspy/anthropic/lm/model_capabilities.rb
65
66
  - lib/dspy/anthropic/lm/schema_converter.rb
66
67
  - lib/dspy/anthropic/version.rb
67
68
  homepage: https://github.com/vicentereig/dspy.rb