Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,19 @@ jobs:
fail-fast: false
matrix:
ruby: ["3.4"]
gemfile:
- "gemfiles/anthropic_1.12.gemfile"
- "gemfiles/anthropic_1.14.gemfile"
- "gemfiles/openai_0.34.gemfile"
- "gemfiles/openai_0.35.gemfile"
include:
- gemfile: "gemfiles/anthropic_1.12.gemfile"
test-glob: "test/integration/anthropic/**/*_test.rb test/providers/usage_test.rb test/docs/providers/anthropic_examples_test.rb"
- gemfile: "gemfiles/anthropic_1.14.gemfile"
test-glob: "test/integration/anthropic/**/*_test.rb test/providers/usage_test.rb test/docs/providers/anthropic_examples_test.rb"
- gemfile: "gemfiles/anthropic_latest.gemfile"
test-glob: "test/integration/anthropic/**/*_test.rb test/providers/usage_test.rb test/docs/providers/anthropic_examples_test.rb"
- gemfile: "gemfiles/openai_0.34.gemfile"
test-glob: "test/integration/open_ai/**/*_test.rb"
- gemfile: "gemfiles/openai_0.35.gemfile"
test-glob: "test/integration/open_ai/**/*_test.rb"
- gemfile: "gemfiles/openai_latest.gemfile"
test-glob: "test/integration/open_ai/**/*_test.rb test/providers/usage_test.rb test/docs/providers/open_ai_examples_test.rb"
steps:
- uses: actions/checkout@v6
- name: Install system deps
Expand All @@ -111,4 +119,4 @@ jobs:
env:
RAILS_ENV: test
RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }}
run: bin/test
run: bundle exec ruby -Itest ${{ matrix.test-glob }}
4 changes: 2 additions & 2 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
nodejs 24.7.0
ruby 3.4.8
nodejs latest
ruby latest
12 changes: 9 additions & 3 deletions activeagent.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,16 @@ Gem::Specification.new do |spec|
spec.add_development_dependency "puma"
spec.add_development_dependency "sqlite3"
spec.add_development_dependency "minitest", "~> 5.0"
spec.add_development_dependency "vcr"
spec.add_development_dependency "webmock"
# Older vcr 6.3.x breaks on Ruby 3.5+/4.0 with a CGI.parse NameError. Keep a
# floor so no gemfile resolves back to the broken version.
spec.add_development_dependency "vcr", ">= 6.4"
spec.add_development_dependency "webmock", ">= 3.26"
# vcr calls CGI.parse but doesn't declare `cgi` itself. Depend on it directly
# so it's installed on Ruby 3.5+/4.0 where `cgi` is no longer a default gem.
spec.add_development_dependency "cgi"

spec.add_development_dependency "dotenv"
# dotenv >= 3.2 avoids the activation conflict older 3.1.x hits under Ruby 4.0.
spec.add_development_dependency "dotenv", ">= 3.2"
spec.add_development_dependency "pry"
spec.add_development_dependency "pry-byebug"
spec.add_development_dependency "pry-doc"
Expand Down
11 changes: 11 additions & 0 deletions bin/test
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@ unless ENV["CI"]
Dotenv.load(".env.test")
end

# When running locally with CI=true, still load env vars from .env.test
# so VCR cassettes can replay without needing real API keys at client init.
if ENV["CI"] && !ENV["OPENAI_API_KEY"] && !ENV["OPEN_AI_API_KEY"] && !ENV["OPEN_AI_ACCESS_TOKEN"]
begin
require "dotenv"
Dotenv.load(".env.test")
rescue LoadError
# dotenv not available
end
end

ENV["RAILS_ENV"] = "test"

# Default to rails8.gemfile for local development.
Expand Down
2 changes: 1 addition & 1 deletion docs/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Providers connect your agents to AI services through a unified interface. Switch
class RubyLLMAgent < ApplicationAgent
generate_with :ruby_llm, model: "gpt-4o-mini"
# Works with any model RubyLLM supports:
# generate_with :ruby_llm, model: "claude-sonnet-4-5-20250929"
# generate_with :ruby_llm, model: "claude-sonnet-5"
# generate_with :ruby_llm, model: "gemini-2.0-flash"
end
```
Expand Down
6 changes: 3 additions & 3 deletions docs/providers/ruby_llm.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ RubyLLM automatically resolves which provider to use based on the model ID. Any
| Provider | Example Models |
|----------|---------------|
| **OpenAI** | `gpt-4o`, `gpt-4o-mini`, `gpt-4.1` |
| **Anthropic** | `claude-sonnet-4-5-20250929`, `claude-haiku-4-5` |
| **Anthropic** | `claude-sonnet-5`, `claude-haiku-4-5` |
| **Google Gemini** | `gemini-2.0-flash`, `gemini-1.5-pro` |
| **AWS Bedrock** | Bedrock-hosted models |
| **Azure OpenAI** | Azure-hosted OpenAI models |
Expand All @@ -70,7 +70,7 @@ Switch providers by changing the model:
class FlexibleAgent < ApplicationAgent
# Any of these work with the same provider config:
generate_with :ruby_llm, model: "gpt-4o-mini"
# generate_with :ruby_llm, model: "claude-sonnet-4-5-20250929"
# generate_with :ruby_llm, model: "claude-sonnet-5"
# generate_with :ruby_llm, model: "gemini-2.0-flash"
end
```
Expand All @@ -79,7 +79,7 @@ end

### Required Parameters

- **`model`** - Model identifier (e.g., "gpt-4o-mini", "claude-sonnet-4-5-20250929")
- **`model`** - Model identifier (e.g., "gpt-4o-mini", "claude-sonnet-5")

### Sampling Parameters

Expand Down
2 changes: 2 additions & 0 deletions gemfiles/anthropic_1.12.gemfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
source "https://rubygems.org"

ENV["ANTHROPIC_GEM_VERSION"] = "1.12"

gem "anthropic", "~> 1.12.0"
gem "minitest", "~> 5.0"
gem "rails", "~> 8.0.0"
Expand Down
2 changes: 2 additions & 0 deletions gemfiles/anthropic_1.14.gemfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
source "https://rubygems.org"

ENV["ANTHROPIC_GEM_VERSION"] = "1.14"

gem "anthropic", "~> 1.14.0"
gem "minitest", "~> 5.0"
gem "rails", "~> 8.0.0"
Expand Down
8 changes: 8 additions & 0 deletions gemfiles/anthropic_latest.gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
source "https://rubygems.org"

gem "anthropic", "~> 1.55.0"
gem "minitest", "~> 5.0"
gem "rails", "~> 8.0.0"
gem "sqlite3", "~> 2.0"

gemspec path: ".."
8 changes: 8 additions & 0 deletions gemfiles/openai_latest.gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
source "https://rubygems.org"

gem "minitest", "~> 5.0"
gem "openai", "~> 0.69.0"
gem "rails", "~> 8.0.0"
gem "sqlite3", "~> 2.0"

gemspec path: ".."
50 changes: 43 additions & 7 deletions lib/active_agent/providers/anthropic/transforms.rb
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,8 @@ def normalize_tool_choice(tool_choice)
# → { format: { type: "json_schema", schema: {...} } }
#
# Notes:
# - Anthropic requires `additionalProperties: false` on all object schemas.
# This is auto-injected into any object schemas that don't have it set.
# - Anthropic does not use OpenAI's `name` or `strict` fields in output_config.format.
# - json_object is not handled here; it remains prompt-emulated.
# - text is not handled here; Anthropic returns plain text by default.
Expand All @@ -184,11 +186,15 @@ def normalize_response_format(format)
format_hash = format.deep_symbolize_keys

if format_hash[:type].to_s == "json_schema"
schema = format_hash[:json_schema]&.dig(:schema)
if schema
schema = inject_additional_properties(schema.deep_dup)
end
{
format: {
type: "json_schema",
schema: format_hash[:json_schema]&.dig(:schema)
}
schema: schema
}.compact
}
elsif format_hash[:type].to_s == "json_object"
# json_object is not handled here; it remains prompt-emulated.
Expand All @@ -201,16 +207,45 @@ def normalize_response_format(format)
format_hash
end
when Symbol, String
if format.to_s == "json_schema"
{ format: { type: "json_schema" } }
else
nil
end
# Bare :json_schema without a schema cannot use native output_config.
# Anthropic requires a valid JSON Schema — return nil to fall back.
nil
else
format
end
end

# Recursively injects `additionalProperties: false` into all object schemas.
#
# Anthropic's structured output API requires this field on all object types.
# Only inserts when not already explicitly set by the user.
#
# @param schema [Hash] JSON Schema hash
# @return [Hash] schema with additionalProperties injected
def inject_additional_properties(schema)
return schema unless schema.is_a?(Hash)

if schema[:type] == "object" && !schema.key?(:additionalProperties)
schema[:additionalProperties] = false
end

# Recurse into nested schemas
schema[:properties]&.each_value { |prop| inject_additional_properties(prop) }
schema[:items]&.then { |items| inject_additional_properties(items) }
schema[:anyOf]&.each { |s| inject_additional_properties(s) }
schema[:oneOf]&.each { |s| inject_additional_properties(s) }
schema[:allOf]&.each { |s| inject_additional_properties(s) }

if schema[:definitions]
schema[:definitions].each_value { |defn| inject_additional_properties(defn) }
end
if schema[:"$defs"]
schema[:"$defs"].each_value { |defn| inject_additional_properties(defn) }
end

schema
end

# Merges consecutive same-role messages into single messages with multiple content blocks.
#
# Required by Anthropic API - consecutive messages with the same role must be combined.
Expand Down Expand Up @@ -489,6 +524,7 @@ def cleanup_serialized_request(hash, defaults, gem_object = nil)
msg.delete(:model)
msg.delete(:stop_reason)
msg.delete(:stop_sequence)
msg.delete(:stop_details)
msg.delete(:type)
msg.delete(:usage)
end
Expand Down
4 changes: 2 additions & 2 deletions lib/active_agent/providers/bedrock_provider.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ module Providers
# bedrock:
# service: "Bedrock"
# aws_region: "eu-west-2"
# model: "eu.anthropic.claude-sonnet-4-5-20250929-v1:0"
# model: "eu.anthropic.claude-sonnet-5-v1:0"
#
# @example Agent usage
# class SummaryAgent < ApplicationAgent
# generate_with :bedrock, model: "eu.anthropic.claude-sonnet-4-5-20250929-v1:0"
# generate_with :bedrock, model: "eu.anthropic.claude-sonnet-5-v1:0"
#
# def summarize
# prompt(message: params[:message])
Expand Down
2 changes: 1 addition & 1 deletion test/docs/actions/embeddings_examples_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ class EmbeddingAgent < ApplicationAgent
VCR.use_cassette("docs/agents/embeddings_examples/mixing_providers") do
# region mixing_providers
class HybridAgent < ApplicationAgent
generate_with :anthropic, model: "claude-3-5-sonnet-20241022"
generate_with :anthropic, model: "claude-sonnet-5"
embed_with :openai, model: "text-embedding-3-small"
end

Expand Down
8 changes: 4 additions & 4 deletions test/docs/actions/messages_examples_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def chat

# region message_keyword_agent
class MessageKeywordAgent < ApplicationAgent
generate_with :anthropic, model: "claude-3-5-haiku-20241022"
generate_with :anthropic, model: "claude-haiku-4-5"

def chat
prompt(message: "Explain quantum computing")
Expand Down Expand Up @@ -62,7 +62,7 @@ def chat_single

# region image_agent
class ImageAgent < ApplicationAgent
generate_with :anthropic, model: "claude-3-5-haiku-20241022"
generate_with :anthropic, model: "claude-haiku-4-5"

def analyze_url
prompt(
Expand Down Expand Up @@ -109,7 +109,7 @@ def summarize_url

# region document_base64_agent
class DocumentBase64Agent < ApplicationAgent
generate_with :anthropic, model: "claude-3-5-haiku-20241022"
generate_with :anthropic, model: "claude-haiku-4-5"

def extract_base64
file_path = Rails.root.join("../fixtures/files/sample_resume.pdf")
Expand All @@ -136,7 +136,7 @@ def chat

# region system_messages_agent
class SystemMessagesAgent < ApplicationAgent
generate_with :anthropic, model: "claude-3-5-haiku-20241022"
generate_with :anthropic, model: "claude-haiku-4-5"

def chat
prompt(
Expand Down
2 changes: 1 addition & 1 deletion test/docs/actions/structured_output_examples_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def extract

# region anthropic_json_agent
class AnthropicAgent < ApplicationAgent
generate_with :anthropic, model: "claude-3-5-sonnet-latest"
generate_with :anthropic, model: "claude-haiku-4-5"

def extract
prompt(
Expand Down
4 changes: 2 additions & 2 deletions test/docs/actions/tools_examples_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ def get_current_weather(location:, unit: "fahrenheit")
class AnthropicBasicExample < ActiveSupport::TestCase
# region anthropic_basic_function
class WeatherAgent < ApplicationAgent
generate_with :anthropic, model: "claude-sonnet-4-20250514"
generate_with :anthropic, model: "claude-haiku-4-5"

def weather_update
prompt(
Expand Down Expand Up @@ -267,7 +267,7 @@ def check_weather
# region cross_provider_anthropic
class AnthropicAgent < ApplicationAgent
include WeatherTool
generate_with :anthropic, model: "claude-sonnet-4-20250514"
generate_with :anthropic, model: "claude-haiku-4-5"

def check_weather
prompt(message: "What's the weather?", tools: [ WEATHER_TOOL ])
Expand Down
2 changes: 1 addition & 1 deletion test/docs/actions/usage_examples_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ def chat
end

class AnthropicUsageAgent < ApplicationAgent
generate_with :anthropic, model: "claude-3-5-haiku-20241022"
generate_with :anthropic, model: "claude-haiku-4-5"

def chat
prompt(message: params[:message])
Expand Down
3 changes: 2 additions & 1 deletion test/docs/providers/anthropic_examples_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ def extract_colors
# => { colors: ["red", "blue", "yellow"] }
# endregion response_format_json_object_example

assert_equal({ colors: [ "red", "blue", "yellow" ] }, colors)
assert colors.key?(:colors)
assert colors[:colors].is_a?(Array)
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion test/dummy/app/agents/providers/anthropic_agent.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ module Providers
# response.message.content #=> "Hi! How can I help you today?"
# region agent
class AnthropicAgent < ApplicationAgent
generate_with :anthropic, model: "claude-sonnet-4-5-20250929"
generate_with :anthropic, model: "claude-sonnet-5"

# @return [ActiveAgent::Generation]
def ask
Expand Down
2 changes: 1 addition & 1 deletion test/dummy/app/agents/providers/bedrock_agent.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ module Providers
# response.message.content #=> "Hi! How can I help you today?"
# region agent
class BedrockAgent < ApplicationAgent
generate_with :bedrock, model: "eu.anthropic.claude-sonnet-4-5-20250929-v1:0"
generate_with :bedrock, model: "eu.anthropic.claude-sonnet-5-v1:0"

# @return [ActiveAgent::Generation]
def ask
Expand Down
Loading