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 +4 -4
- data/README.md +23 -171
- data/lib/dspy/anthropic/lm/adapters/anthropic_adapter.rb +153 -27
- data/lib/dspy/anthropic/lm/model_capabilities.rb +113 -0
- data/lib/dspy/anthropic/version.rb +1 -1
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b8d68b501f25927a814cc35c0af2c7f0e5f3b5bd1bb03f3b48df61111977bdee
|
|
4
|
+
data.tar.gz: 58af722a168077708cde9ab723d4004c5cf47204adad3e75ece1a15fd25455ce
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c070133fcdac4bfc561c37084bcce94b31a7564ae3e8b872b80593684e105f71dbe80a1c84e06f0cc7971649bbc8d5c66a9d3333f739ed643a5e04f59afffca3
|
|
7
|
+
data.tar.gz: 1da253c16a11737bb777d49639906ef09afe7b62188d36937bfd405582acc3de318169bfb7b95b41220ae042988f73079028d77f0cb63429f20c9b0793b63813
|
data/README.md
CHANGED
|
@@ -6,96 +6,16 @@
|
|
|
6
6
|
[](https://oss.vicente.services/dspy.rb/)
|
|
7
7
|
[](https://discord.gg/zWBhrMqn)
|
|
8
8
|
|
|
9
|
-
**Build
|
|
9
|
+
**Build typed agents and model-backed programs in Ruby.**
|
|
10
10
|
|
|
11
|
-
DSPy.rb
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
49
|
+
## Mental Model
|
|
161
50
|
|
|
162
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
60
|
+
## Explore
|
|
203
61
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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 =
|
|
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
|
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
|
+
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.
|
|
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.
|
|
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
|