OpenRouter Provider
OpenRouter provides access to multiple AI models through a unified API, with advanced features like fallback models, multimodal support, and PDF processing.
Configuration
Configure OpenRouter in your agent:
class OpenRouterAgent < ApplicationAgent
layout "agent"
generate_with :open_router, model: "qwen/qwen3-30b-a3b:free", instructions: "You're a basic Open Router agent."
endFeatures
Structured Output Support
OpenRouter supports structured output for compatible models (like OpenAI's GPT-4o and GPT-4o-mini), allowing you to receive responses in a predefined JSON schema format. This is particularly useful for data extraction tasks.
Compatible Models
Models that support both vision capabilities AND structured output:
openai/gpt-4oopenai/gpt-4o-miniopenai/gpt-4-turbo(structured output only, no vision)openai/gpt-3.5-turbovariants (structured output only, no vision)
Using Structured Output
Define your schema and pass it to the prompt method:
class OpenRouterAgent < ApplicationAgent
generate_with :open_router, model: "openai/gpt-4o-mini"
def analyze_image
@image_url = params[:image_url]
prompt(
message: build_image_message,
output_schema: image_analysis_schema
)
end
private
def image_analysis_schema
{
name: "image_analysis",
strict: true,
schema: {
type: "object",
properties: {
description: { type: "string" },
objects: {
type: "array",
items: {
type: "object",
properties: {
name: { type: "string" },
position: { type: "string" },
color: { type: "string" }
},
required: ["name", "position", "color"],
additionalProperties: false
}
},
scene_type: {
type: "string",
enum: ["indoor", "outdoor", "abstract", "document", "photo", "illustration"]
}
},
required: ["description", "objects", "scene_type"],
additionalProperties: false
}
}
end
endTIP
When using strict: true with OpenAI models, all properties defined in your schema must be included in the required array. This ensures deterministic responses.
For more comprehensive structured output examples, including receipt data extraction and document parsing, see the Data Extraction Agent documentation.
Multimodal Support
OpenRouter supports vision-capable models for image analysis:
require "test_helper"
require "base64"
require "active_agent/action_prompt/message"
class OpenRouterIntegrationTest < ActiveSupport::TestCase
setup do
@agent = OpenRouterIntegrationAgent.new
end
test "analyzes image with structured output schema" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_image_analysis_structured") do
# Use the sales chart image URL for structured analysis
image_url = "https://raw.githubusercontent.com/activeagents/activeagent/refs/heads/main/test/fixtures/images/sales_chart.png"
prompt = OpenRouterIntegrationAgent.with(image_url: image_url).analyze_image
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
# When output_schema is present, content is already parsed
result = response.message.content
# Verify the structure matches our schema
assert result.key?("description")
assert result.key?("objects")
assert result.key?("scene_type")
assert result.key?("primary_colors")
assert result["objects"].is_a?(Array)
assert [ "indoor", "outdoor", "abstract", "document", "photo", "illustration" ].include?(result["scene_type"])
end
end
test "analyzes remote image URL without structured output" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_remote_image_basic") do
# Use a landscape image URL for basic analysis
image_url = "https://picsum.photos/400/300"
# For now, just use analyze_image without the structured output schema
# We'll get a natural language description instead of JSON
prompt = OpenRouterIntegrationAgent.with(image_url: image_url).analyze_image
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
assert response.message.content.is_a?(String)
assert response.message.content.length > 10
# Since analyze_image uses structured output, we'll get JSON
# Just verify we got a response
# In the future, we could add a simple_analyze action without schema
# Generate documentation example
doc_example_output(response)
end
end
test "extracts receipt data with structured output from local file" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_receipt_extraction_local") do
# Use the test receipt image - file exists, no conditional needed
receipt_path = Rails.root.join("..", "..", "test", "fixtures", "images", "test_receipt.png")
prompt = OpenRouterIntegrationAgent.with(image_path: receipt_path).extract_receipt_data
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
# When output_schema is present, content is already parsed
result = response.message.content
assert_equal result["merchant"]["name"], "Corner Mart"
assert_equal result["total"]["amount"], 14.83
assert_equal result["items"].size, 4
result["items"].each do |item|
assert item.key?("name")
assert item.key?("quantity")
assert item.key?("price")
end
assert_equal result["items"][0], { "name"=>"Milk", "quantity"=>1, "price"=>3.49 }
assert_equal result["items"][1], { "name"=>"Bread", "quantity"=>1, "price"=>2.29 }
assert_equal result["items"][2], { "name"=>"Apples", "quantity"=>1, "price"=>5.1 }
assert_equal result["items"][3], { "name"=>"Eggs", "quantity"=>1, "price"=>2.99 }
# Generate documentation example
doc_example_output(response)
end
end
test "handles base64 encoded images with sales chart" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_base64_sales_chart") do
# Use the sales chart image
chart_path = Rails.root.join("..", "..", "test", "fixtures", "images", "sales_chart.png")
prompt = OpenRouterIntegrationAgent.with(image_path: chart_path).analyze_image
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
assert_includes response.message.content, "(Q1, Q2, Q3, Q4), with varying heights indicating different sales amounts"
# Generate documentation example
doc_example_output(response)
end
end
test "processes PDF document from local file" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_pdf_local") do
# Use the sample resume PDF
pdf_path = Rails.root.join("..", "..", "test", "fixtures", "files", "sample_resume.pdf")
# Read and encode the PDF as base64 - OpenRouter accepts PDFs as image_url with data URL
pdf_data = Base64.strict_encode64(File.read(pdf_path))
prompt = OpenRouterIntegrationAgent.with(
pdf_data: pdf_data,
prompt_text: "Extract information from this document and return as JSON",
output_schema: :resume_schema
).analyze_pdf
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
assert response.message.content.present?
# When output_schema is present, content is already parsed
result = response.message.content
assert_equal result["name"], "John Doe"
assert_equal result["email"], "john.doe@example.com"
assert_equal result["phone"], "(555) 123-4567"
assert_equal result["education"].first, { "degree"=>"BS Computer Science", "institution"=>"Stanford University", "year"=>2020 }
assert_equal result["experience"].first, { "job_title"=>"Senior Software Engineer", "company"=>"TechCorp", "duration"=>"2020-2024" }
# Generate documentation example
doc_example_output(response)
end
end
# endregion pdf_processing_local
test "processes PDF from remote URL of resume no plugins" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_pdf_remote_no_plugin") do
pdf_url = "https://docs.activeagents.ai/sample_resume.pdf"
prompt = OpenRouterIntegrationAgent.with(
pdf_url: pdf_url,
prompt_text: "Analyze the PDF",
output_schema: :resume_schema,
skip_plugin: true
).analyze_pdf
# Remote URLs are not supported without a PDF engine plugin
# OpenAI: Inputs by file URL are not supported for chat completions. Use the ResponsesAPI for this option.
# https://platform.openai.com/docs/guides/pdf-files#file-urls
# Accept either the OpenAI error directly or our wrapped error
# Suppress ruby-openai gem's error output to STDERR
error = assert_raises(ActiveAgent::GenerationProvider::Base::GenerationProviderError, OpenAI::Error) do
prompt.generate_now
end
# Check the error message regardless of which error type was raised
error_message = error.message
assert_match(/Missing required parameter.*file_id/, error_message)
assert_match(/Provider returned error|invalid_request_error/, error_message)
end
end
# region pdf_native_support
test "processes PDF with native model support" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_pdf_native") do
# Test with a model that might have native PDF support
# Using the native engine (charged as input tokens)
pdf_path = Rails.root.join("..", "..", "test", "fixtures", "files", "sample_resume.pdf")
pdf_data = Base64.strict_encode64(File.read(pdf_path))
prompt = OpenRouterIntegrationAgent.with(
pdf_data: pdf_data,
prompt_text: "Analyze this PDF document",
pdf_engine: "native" # Use native engine (charged as input tokens)
).analyze_pdf
# First verify the prompt has the plugins in options
assert prompt.options[:plugins].present?, "Plugins should be present in prompt options"
assert prompt.options[:fallback_models].present?, "Fallback models should be present in prompt options"
assert_equal "file-parser", prompt.options[:plugins][0][:id]
assert_equal "native", prompt.options[:plugins][0][:pdf][:engine]
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
assert response.message.content.present?
assert_includes response.message.content, "John Doe"
# Generate documentation example
doc_example_output(response)
end
end
# endregion pdf_native_support
test "processes PDF without any plugin for models with built-in support" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_pdf_no_plugin") do
# Test without any plugin - for models that have built-in PDF support
pdf_url = "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
prompt = OpenRouterIntegrationAgent.with(
pdf_url: pdf_url,
prompt_text: "Analyze this PDF document",
skip_plugin: true # Don't use any plugin
).analyze_pdf
# Verify no plugins are included when skip_plugin is true
assert_empty prompt.options[:plugins], "Should not have plugins when skip_plugin is true"
response = prompt.generate_now
raw_response = response.raw_response
assert_equal "Google", raw_response["provider"]
assert_not_nil response
assert_not_nil response.message
assert response.message.content.present?
# Generate documentation example
doc_example_output(response)
end
end
test "processes scanned PDF with OCR engine" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_pdf_ocr") do
# Test with the mistral-ocr engine for scanned documents
# Using a simple PDF that should be processable
pdf_url = "https://docs.activeagents.ai/sample_resume.pdf"
prompt = OpenRouterIntegrationAgent.with(
pdf_url: pdf_url,
prompt_text: "Extract text from this PDF.",
output_schema: :resume_schema,
pdf_engine: "mistral-ocr" # OCR engine for text extraction
).analyze_pdf
# Verify OCR engine is specified
assert prompt.options[:plugins].present?, "Should have plugins for OCR"
assert_equal "mistral-ocr", prompt.options[:plugins][0][:pdf][:engine]
response = prompt.generate_now
# MUST return valid JSON - no fallback allowed
raw_response = response.raw_response
# When output_schema is present, content is already parsed
result = response.message.content
assert_equal result["name"], "John Doe"
assert_equal result["email"], "john.doe@example.com"
assert_equal result["phone"], "(555) 123-4567"
assert_equal result["education"], [ { "degree"=>"BS Computer Science", "institution"=>"Stanford University", "year"=>2020 } ]
assert_equal result["experience"], [ { "job_title"=>"Senior Software Engineer", "company"=>"TechCorp", "duration"=>"2020-2024" } ]
# Generate documentation example
doc_example_output(response)
end
end
test "uses fallback models when primary fails" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_fallback_models") do
prompt = OpenRouterIntegrationAgent.test_fallback
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
# Check metadata for fallback usage
if response.respond_to?(:metadata) && response.metadata
# Should use one of the fallback models, not the primary
possible_models = [ "openai/gpt-3.5-turbo-0301", "openai/gpt-3.5-turbo", "openai/gpt-4o-mini" ]
assert possible_models.include?(response.metadata[:model_used])
assert response.metadata[:provider].present?
end
# The response should still work (2+2=4)
assert response.message.content.include?("4")
# Generate documentation example
doc_example_output(response)
end
end
test "applies transforms for long content" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_transforms") do
# Generate a very long text
long_text = "Lorem ipsum dolor sit amet. " * 1000
prompt = OpenRouterIntegrationAgent.with(text: long_text).process_long_text
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
assert response.message.content.present?
# The summary should be much shorter than the original
assert response.message.content.length < long_text.length / 10
# Generate documentation example
doc_example_output(response)
end
end
test "tracks usage and costs" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_cost_tracking") do
prompt = OpenRouterIntegrationAgent.with(message: "Hello").prompt_context
response = prompt.generate_now
assert_not_nil response
# Check for usage information
if response.respond_to?(:usage) && response.usage
assert response.usage["prompt_tokens"].is_a?(Integer)
assert response.usage["completion_tokens"].is_a?(Integer)
assert response.usage["total_tokens"].is_a?(Integer)
end
# Check for metadata with model information from OpenRouter
if response.respond_to?(:metadata) && response.metadata
assert response.metadata[:model_used].present?
assert response.metadata[:provider].present?
# Verify we're using the expected model (gpt-4o-mini)
assert_equal "openai/gpt-4o-mini", response.metadata[:model_used]
end
# Generate documentation example
doc_example_output(response)
end
end
test "includes OpenRouter headers in requests" do
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o",
"app_name" => "TestApp",
"site_url" => "https://test.example.com"
)
# Get the headers that would be sent
headers = provider.send(:openrouter_headers)
assert_equal "https://test.example.com", headers["HTTP-Referer"]
assert_equal "TestApp", headers["X-Title"]
end
test "builds provider preferences correctly" do
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o",
"enable_fallbacks" => true,
"provider" => {
"order" => [ "OpenAI", "Anthropic" ],
"require_parameters" => true,
"data_collection" => "deny"
}
)
prefs = provider.send(:build_provider_preferences)
assert_equal [ "OpenAI", "Anthropic" ], prefs[:order]
assert_equal true, prefs[:require_parameters]
assert_equal true, prefs[:allow_fallbacks]
assert_equal "deny", prefs[:data_collection]
end
test "configures data collection policies" do
# Test deny all data collection
provider_deny = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o",
"data_collection" => "deny"
)
prefs_deny = provider_deny.send(:build_provider_preferences)
assert_equal "deny", prefs_deny[:data_collection]
# Test allow all data collection (default)
provider_allow = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o"
)
prefs_allow = provider_allow.send(:build_provider_preferences)
assert_equal "allow", prefs_allow[:data_collection]
# Test selective provider data collection
provider_selective = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o",
"data_collection" => [ "OpenAI", "Google" ]
)
prefs_selective = provider_selective.send(:build_provider_preferences)
assert_equal [ "OpenAI", "Google" ], prefs_selective[:data_collection]
end
test "handles multimodal content correctly" do
# Create a message with multimodal content
message = ActiveAgent::ActionPrompt::Message.new(
content: [
{ type: "text", text: "What's in this image?" },
{ type: "image_url", image_url: { url: "https://example.com/image.jpg" } }
],
role: :user
)
prompt = ActiveAgent::ActionPrompt::Prompt.new(
messages: [ message ]
)
assert prompt.multimodal?
end
test "converts file type to image_url for OpenRouter PDF support" do
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o"
)
# Test file type conversion
file_item = {
type: "file",
file: {
file_data: "data:application/pdf;base64,JVBERi0xLj..."
}
}
formatted = provider.send(:format_content_item, file_item)
assert_equal "image_url", formatted[:type]
assert_equal "data:application/pdf;base64,JVBERi0xLj...", formatted[:image_url][:url]
end
test "respects configuration hierarchy for site_url" do
# Test with explicit site_url config
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o",
"site_url" => "https://configured.example.com"
)
assert_equal "https://configured.example.com", provider.instance_variable_get(:@site_url)
# Test with default_url_options in config
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o",
"default_url_options" => {
"host" => "fromconfig.example.com"
}
)
assert_equal "fromconfig.example.com", provider.instance_variable_get(:@site_url)
end
test "handles rate limit information in metadata" do
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o"
)
# Create a mock response
prompt = ActiveAgent::ActionPrompt::Prompt.new(message: "test")
response = ActiveAgent::GenerationProvider::Response.new(prompt: prompt)
headers = {
"x-provider" => "OpenAI",
"x-model" => "gpt-4o",
"x-ratelimit-requests-limit" => "100",
"x-ratelimit-requests-remaining" => "99",
"x-ratelimit-tokens-limit" => "10000",
"x-ratelimit-tokens-remaining" => "9500"
}
provider.send(:add_openrouter_metadata, response, headers)
assert_equal "100", response.metadata[:ratelimit][:requests_limit]
assert_equal "99", response.metadata[:ratelimit][:requests_remaining]
assert_equal "10000", response.metadata[:ratelimit][:tokens_limit]
assert_equal "9500", response.metadata[:ratelimit][:tokens_remaining]
end
test "includes plugins parameter when passed in options" do
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o"
)
# Create a prompt with plugins option
prompt = ActiveAgent::ActionPrompt::Prompt.new(
message: "test",
options: {
plugins: [
{
id: "file-parser",
pdf: {
engine: "pdf-text"
}
}
]
}
)
# Set the prompt on the provider
provider.instance_variable_set(:@prompt, prompt)
# Build parameters and verify plugins are included
parameters = provider.send(:build_openrouter_parameters)
assert_not_nil parameters[:plugins]
assert_equal 1, parameters[:plugins].size
assert_equal "file-parser", parameters[:plugins][0][:id]
assert_equal "pdf-text", parameters[:plugins][0][:pdf][:engine]
end
endImage Analysis with Structured Output
Receipt Data Extraction with Structured Output
Extract structured data from receipts and documents using OpenRouter's structured output capabilities. This example demonstrates how to parse receipt images and extract specific fields like merchant information, items, and totals.
Test Implementation
require "test_helper"
require "base64"
require "active_agent/action_prompt/message"
class OpenRouterIntegrationTest < ActiveSupport::TestCase
setup do
@agent = OpenRouterIntegrationAgent.new
end
test "analyzes image with structured output schema" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_image_analysis_structured") do
# Use the sales chart image URL for structured analysis
image_url = "https://raw.githubusercontent.com/activeagents/activeagent/refs/heads/main/test/fixtures/images/sales_chart.png"
prompt = OpenRouterIntegrationAgent.with(image_url: image_url).analyze_image
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
# When output_schema is present, content is already parsed
result = response.message.content
# Verify the structure matches our schema
assert result.key?("description")
assert result.key?("objects")
assert result.key?("scene_type")
assert result.key?("primary_colors")
assert result["objects"].is_a?(Array)
assert [ "indoor", "outdoor", "abstract", "document", "photo", "illustration" ].include?(result["scene_type"])
end
end
test "analyzes remote image URL without structured output" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_remote_image_basic") do
# Use a landscape image URL for basic analysis
image_url = "https://picsum.photos/400/300"
# For now, just use analyze_image without the structured output schema
# We'll get a natural language description instead of JSON
prompt = OpenRouterIntegrationAgent.with(image_url: image_url).analyze_image
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
assert response.message.content.is_a?(String)
assert response.message.content.length > 10
# Since analyze_image uses structured output, we'll get JSON
# Just verify we got a response
# In the future, we could add a simple_analyze action without schema
# Generate documentation example
doc_example_output(response)
end
end
test "extracts receipt data with structured output from local file" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_receipt_extraction_local") do
# Use the test receipt image - file exists, no conditional needed
receipt_path = Rails.root.join("..", "..", "test", "fixtures", "images", "test_receipt.png")
prompt = OpenRouterIntegrationAgent.with(image_path: receipt_path).extract_receipt_data
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
# When output_schema is present, content is already parsed
result = response.message.content
assert_equal result["merchant"]["name"], "Corner Mart"
assert_equal result["total"]["amount"], 14.83
assert_equal result["items"].size, 4
result["items"].each do |item|
assert item.key?("name")
assert item.key?("quantity")
assert item.key?("price")
end
assert_equal result["items"][0], { "name"=>"Milk", "quantity"=>1, "price"=>3.49 }
assert_equal result["items"][1], { "name"=>"Bread", "quantity"=>1, "price"=>2.29 }
assert_equal result["items"][2], { "name"=>"Apples", "quantity"=>1, "price"=>5.1 }
assert_equal result["items"][3], { "name"=>"Eggs", "quantity"=>1, "price"=>2.99 }
# Generate documentation example
doc_example_output(response)
end
end
test "handles base64 encoded images with sales chart" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_base64_sales_chart") do
# Use the sales chart image
chart_path = Rails.root.join("..", "..", "test", "fixtures", "images", "sales_chart.png")
prompt = OpenRouterIntegrationAgent.with(image_path: chart_path).analyze_image
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
assert_includes response.message.content, "(Q1, Q2, Q3, Q4), with varying heights indicating different sales amounts"
# Generate documentation example
doc_example_output(response)
end
end
test "processes PDF document from local file" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_pdf_local") do
# Use the sample resume PDF
pdf_path = Rails.root.join("..", "..", "test", "fixtures", "files", "sample_resume.pdf")
# Read and encode the PDF as base64 - OpenRouter accepts PDFs as image_url with data URL
pdf_data = Base64.strict_encode64(File.read(pdf_path))
prompt = OpenRouterIntegrationAgent.with(
pdf_data: pdf_data,
prompt_text: "Extract information from this document and return as JSON",
output_schema: :resume_schema
).analyze_pdf
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
assert response.message.content.present?
# When output_schema is present, content is already parsed
result = response.message.content
assert_equal result["name"], "John Doe"
assert_equal result["email"], "john.doe@example.com"
assert_equal result["phone"], "(555) 123-4567"
assert_equal result["education"].first, { "degree"=>"BS Computer Science", "institution"=>"Stanford University", "year"=>2020 }
assert_equal result["experience"].first, { "job_title"=>"Senior Software Engineer", "company"=>"TechCorp", "duration"=>"2020-2024" }
# Generate documentation example
doc_example_output(response)
end
end
# endregion pdf_processing_local
test "processes PDF from remote URL of resume no plugins" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_pdf_remote_no_plugin") do
pdf_url = "https://docs.activeagents.ai/sample_resume.pdf"
prompt = OpenRouterIntegrationAgent.with(
pdf_url: pdf_url,
prompt_text: "Analyze the PDF",
output_schema: :resume_schema,
skip_plugin: true
).analyze_pdf
# Remote URLs are not supported without a PDF engine plugin
# OpenAI: Inputs by file URL are not supported for chat completions. Use the ResponsesAPI for this option.
# https://platform.openai.com/docs/guides/pdf-files#file-urls
# Accept either the OpenAI error directly or our wrapped error
# Suppress ruby-openai gem's error output to STDERR
error = assert_raises(ActiveAgent::GenerationProvider::Base::GenerationProviderError, OpenAI::Error) do
prompt.generate_now
end
# Check the error message regardless of which error type was raised
error_message = error.message
assert_match(/Missing required parameter.*file_id/, error_message)
assert_match(/Provider returned error|invalid_request_error/, error_message)
end
end
# region pdf_native_support
test "processes PDF with native model support" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_pdf_native") do
# Test with a model that might have native PDF support
# Using the native engine (charged as input tokens)
pdf_path = Rails.root.join("..", "..", "test", "fixtures", "files", "sample_resume.pdf")
pdf_data = Base64.strict_encode64(File.read(pdf_path))
prompt = OpenRouterIntegrationAgent.with(
pdf_data: pdf_data,
prompt_text: "Analyze this PDF document",
pdf_engine: "native" # Use native engine (charged as input tokens)
).analyze_pdf
# First verify the prompt has the plugins in options
assert prompt.options[:plugins].present?, "Plugins should be present in prompt options"
assert prompt.options[:fallback_models].present?, "Fallback models should be present in prompt options"
assert_equal "file-parser", prompt.options[:plugins][0][:id]
assert_equal "native", prompt.options[:plugins][0][:pdf][:engine]
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
assert response.message.content.present?
assert_includes response.message.content, "John Doe"
# Generate documentation example
doc_example_output(response)
end
end
# endregion pdf_native_support
test "processes PDF without any plugin for models with built-in support" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_pdf_no_plugin") do
# Test without any plugin - for models that have built-in PDF support
pdf_url = "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
prompt = OpenRouterIntegrationAgent.with(
pdf_url: pdf_url,
prompt_text: "Analyze this PDF document",
skip_plugin: true # Don't use any plugin
).analyze_pdf
# Verify no plugins are included when skip_plugin is true
assert_empty prompt.options[:plugins], "Should not have plugins when skip_plugin is true"
response = prompt.generate_now
raw_response = response.raw_response
assert_equal "Google", raw_response["provider"]
assert_not_nil response
assert_not_nil response.message
assert response.message.content.present?
# Generate documentation example
doc_example_output(response)
end
end
test "processes scanned PDF with OCR engine" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_pdf_ocr") do
# Test with the mistral-ocr engine for scanned documents
# Using a simple PDF that should be processable
pdf_url = "https://docs.activeagents.ai/sample_resume.pdf"
prompt = OpenRouterIntegrationAgent.with(
pdf_url: pdf_url,
prompt_text: "Extract text from this PDF.",
output_schema: :resume_schema,
pdf_engine: "mistral-ocr" # OCR engine for text extraction
).analyze_pdf
# Verify OCR engine is specified
assert prompt.options[:plugins].present?, "Should have plugins for OCR"
assert_equal "mistral-ocr", prompt.options[:plugins][0][:pdf][:engine]
response = prompt.generate_now
# MUST return valid JSON - no fallback allowed
raw_response = response.raw_response
# When output_schema is present, content is already parsed
result = response.message.content
assert_equal result["name"], "John Doe"
assert_equal result["email"], "john.doe@example.com"
assert_equal result["phone"], "(555) 123-4567"
assert_equal result["education"], [ { "degree"=>"BS Computer Science", "institution"=>"Stanford University", "year"=>2020 } ]
assert_equal result["experience"], [ { "job_title"=>"Senior Software Engineer", "company"=>"TechCorp", "duration"=>"2020-2024" } ]
# Generate documentation example
doc_example_output(response)
end
end
test "uses fallback models when primary fails" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_fallback_models") do
prompt = OpenRouterIntegrationAgent.test_fallback
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
# Check metadata for fallback usage
if response.respond_to?(:metadata) && response.metadata
# Should use one of the fallback models, not the primary
possible_models = [ "openai/gpt-3.5-turbo-0301", "openai/gpt-3.5-turbo", "openai/gpt-4o-mini" ]
assert possible_models.include?(response.metadata[:model_used])
assert response.metadata[:provider].present?
end
# The response should still work (2+2=4)
assert response.message.content.include?("4")
# Generate documentation example
doc_example_output(response)
end
end
test "applies transforms for long content" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_transforms") do
# Generate a very long text
long_text = "Lorem ipsum dolor sit amet. " * 1000
prompt = OpenRouterIntegrationAgent.with(text: long_text).process_long_text
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
assert response.message.content.present?
# The summary should be much shorter than the original
assert response.message.content.length < long_text.length / 10
# Generate documentation example
doc_example_output(response)
end
end
test "tracks usage and costs" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_cost_tracking") do
prompt = OpenRouterIntegrationAgent.with(message: "Hello").prompt_context
response = prompt.generate_now
assert_not_nil response
# Check for usage information
if response.respond_to?(:usage) && response.usage
assert response.usage["prompt_tokens"].is_a?(Integer)
assert response.usage["completion_tokens"].is_a?(Integer)
assert response.usage["total_tokens"].is_a?(Integer)
end
# Check for metadata with model information from OpenRouter
if response.respond_to?(:metadata) && response.metadata
assert response.metadata[:model_used].present?
assert response.metadata[:provider].present?
# Verify we're using the expected model (gpt-4o-mini)
assert_equal "openai/gpt-4o-mini", response.metadata[:model_used]
end
# Generate documentation example
doc_example_output(response)
end
end
test "includes OpenRouter headers in requests" do
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o",
"app_name" => "TestApp",
"site_url" => "https://test.example.com"
)
# Get the headers that would be sent
headers = provider.send(:openrouter_headers)
assert_equal "https://test.example.com", headers["HTTP-Referer"]
assert_equal "TestApp", headers["X-Title"]
end
test "builds provider preferences correctly" do
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o",
"enable_fallbacks" => true,
"provider" => {
"order" => [ "OpenAI", "Anthropic" ],
"require_parameters" => true,
"data_collection" => "deny"
}
)
prefs = provider.send(:build_provider_preferences)
assert_equal [ "OpenAI", "Anthropic" ], prefs[:order]
assert_equal true, prefs[:require_parameters]
assert_equal true, prefs[:allow_fallbacks]
assert_equal "deny", prefs[:data_collection]
end
test "configures data collection policies" do
# Test deny all data collection
provider_deny = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o",
"data_collection" => "deny"
)
prefs_deny = provider_deny.send(:build_provider_preferences)
assert_equal "deny", prefs_deny[:data_collection]
# Test allow all data collection (default)
provider_allow = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o"
)
prefs_allow = provider_allow.send(:build_provider_preferences)
assert_equal "allow", prefs_allow[:data_collection]
# Test selective provider data collection
provider_selective = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o",
"data_collection" => [ "OpenAI", "Google" ]
)
prefs_selective = provider_selective.send(:build_provider_preferences)
assert_equal [ "OpenAI", "Google" ], prefs_selective[:data_collection]
end
test "handles multimodal content correctly" do
# Create a message with multimodal content
message = ActiveAgent::ActionPrompt::Message.new(
content: [
{ type: "text", text: "What's in this image?" },
{ type: "image_url", image_url: { url: "https://example.com/image.jpg" } }
],
role: :user
)
prompt = ActiveAgent::ActionPrompt::Prompt.new(
messages: [ message ]
)
assert prompt.multimodal?
end
test "converts file type to image_url for OpenRouter PDF support" do
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o"
)
# Test file type conversion
file_item = {
type: "file",
file: {
file_data: "data:application/pdf;base64,JVBERi0xLj..."
}
}
formatted = provider.send(:format_content_item, file_item)
assert_equal "image_url", formatted[:type]
assert_equal "data:application/pdf;base64,JVBERi0xLj...", formatted[:image_url][:url]
end
test "respects configuration hierarchy for site_url" do
# Test with explicit site_url config
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o",
"site_url" => "https://configured.example.com"
)
assert_equal "https://configured.example.com", provider.instance_variable_get(:@site_url)
# Test with default_url_options in config
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o",
"default_url_options" => {
"host" => "fromconfig.example.com"
}
)
assert_equal "fromconfig.example.com", provider.instance_variable_get(:@site_url)
end
test "handles rate limit information in metadata" do
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o"
)
# Create a mock response
prompt = ActiveAgent::ActionPrompt::Prompt.new(message: "test")
response = ActiveAgent::GenerationProvider::Response.new(prompt: prompt)
headers = {
"x-provider" => "OpenAI",
"x-model" => "gpt-4o",
"x-ratelimit-requests-limit" => "100",
"x-ratelimit-requests-remaining" => "99",
"x-ratelimit-tokens-limit" => "10000",
"x-ratelimit-tokens-remaining" => "9500"
}
provider.send(:add_openrouter_metadata, response, headers)
assert_equal "100", response.metadata[:ratelimit][:requests_limit]
assert_equal "99", response.metadata[:ratelimit][:requests_remaining]
assert_equal "10000", response.metadata[:ratelimit][:tokens_limit]
assert_equal "9500", response.metadata[:ratelimit][:tokens_remaining]
end
test "includes plugins parameter when passed in options" do
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o"
)
# Create a prompt with plugins option
prompt = ActiveAgent::ActionPrompt::Prompt.new(
message: "test",
options: {
plugins: [
{
id: "file-parser",
pdf: {
engine: "pdf-text"
}
}
]
}
)
# Set the prompt on the provider
provider.instance_variable_set(:@prompt, prompt)
# Build parameters and verify plugins are included
parameters = provider.send(:build_openrouter_parameters)
assert_not_nil parameters[:plugins]
assert_equal 1, parameters[:plugins].size
assert_equal "file-parser", parameters[:plugins][0][:id]
assert_equal "pdf-text", parameters[:plugins][0][:pdf][:engine]
end
endReceipt Schema Definition
def receipt_schema
{
name: "receipt_data",
strict: true,
schema: {
type: "object",
properties: {
merchant: {
type: "object",
properties: {
name: { type: "string" },
address: { type: "string" }
},
required: [ "name", "address" ],
additionalProperties: false
},
date: { type: "string" },
total: {
type: "object",
properties: {
amount: { type: "number" },
currency: { type: "string" }
},
required: [ "amount", "currency" ],
additionalProperties: false
},
items: {
type: "array",
items: {
type: "object",
properties: {
name: { type: "string" },
quantity: { type: "integer" },
price: { type: "number" }
},
required: [ "name", "price", "quantity" ],
additionalProperties: false
}
},
tax: { type: "number" },
subtotal: { type: "number" }
},
required: [ "merchant", "total", "items", "date", "tax", "subtotal" ],
additionalProperties: false
}
}
endThe receipt schema ensures consistent extraction of:
- Merchant name and address
- Individual line items with names and prices
- Subtotal, tax, and total amounts
- Currency information
Receipt Extraction Example Output
TIP
This example uses structured output to ensure the receipt data is returned in a consistent JSON format. For more examples of structured data extraction from various document types, see the Data Extraction Agent documentation.
PDF Processing
OpenRouter supports PDF processing with various engines:
require "test_helper"
require "base64"
require "active_agent/action_prompt/message"
class OpenRouterIntegrationTest < ActiveSupport::TestCase
setup do
@agent = OpenRouterIntegrationAgent.new
end
test "analyzes image with structured output schema" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_image_analysis_structured") do
# Use the sales chart image URL for structured analysis
image_url = "https://raw.githubusercontent.com/activeagents/activeagent/refs/heads/main/test/fixtures/images/sales_chart.png"
prompt = OpenRouterIntegrationAgent.with(image_url: image_url).analyze_image
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
# When output_schema is present, content is already parsed
result = response.message.content
# Verify the structure matches our schema
assert result.key?("description")
assert result.key?("objects")
assert result.key?("scene_type")
assert result.key?("primary_colors")
assert result["objects"].is_a?(Array)
assert [ "indoor", "outdoor", "abstract", "document", "photo", "illustration" ].include?(result["scene_type"])
end
end
test "analyzes remote image URL without structured output" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_remote_image_basic") do
# Use a landscape image URL for basic analysis
image_url = "https://picsum.photos/400/300"
# For now, just use analyze_image without the structured output schema
# We'll get a natural language description instead of JSON
prompt = OpenRouterIntegrationAgent.with(image_url: image_url).analyze_image
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
assert response.message.content.is_a?(String)
assert response.message.content.length > 10
# Since analyze_image uses structured output, we'll get JSON
# Just verify we got a response
# In the future, we could add a simple_analyze action without schema
# Generate documentation example
doc_example_output(response)
end
end
test "extracts receipt data with structured output from local file" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_receipt_extraction_local") do
# Use the test receipt image - file exists, no conditional needed
receipt_path = Rails.root.join("..", "..", "test", "fixtures", "images", "test_receipt.png")
prompt = OpenRouterIntegrationAgent.with(image_path: receipt_path).extract_receipt_data
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
# When output_schema is present, content is already parsed
result = response.message.content
assert_equal result["merchant"]["name"], "Corner Mart"
assert_equal result["total"]["amount"], 14.83
assert_equal result["items"].size, 4
result["items"].each do |item|
assert item.key?("name")
assert item.key?("quantity")
assert item.key?("price")
end
assert_equal result["items"][0], { "name"=>"Milk", "quantity"=>1, "price"=>3.49 }
assert_equal result["items"][1], { "name"=>"Bread", "quantity"=>1, "price"=>2.29 }
assert_equal result["items"][2], { "name"=>"Apples", "quantity"=>1, "price"=>5.1 }
assert_equal result["items"][3], { "name"=>"Eggs", "quantity"=>1, "price"=>2.99 }
# Generate documentation example
doc_example_output(response)
end
end
test "handles base64 encoded images with sales chart" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_base64_sales_chart") do
# Use the sales chart image
chart_path = Rails.root.join("..", "..", "test", "fixtures", "images", "sales_chart.png")
prompt = OpenRouterIntegrationAgent.with(image_path: chart_path).analyze_image
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
assert_includes response.message.content, "(Q1, Q2, Q3, Q4), with varying heights indicating different sales amounts"
# Generate documentation example
doc_example_output(response)
end
end
test "processes PDF document from local file" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_pdf_local") do
# Use the sample resume PDF
pdf_path = Rails.root.join("..", "..", "test", "fixtures", "files", "sample_resume.pdf")
# Read and encode the PDF as base64 - OpenRouter accepts PDFs as image_url with data URL
pdf_data = Base64.strict_encode64(File.read(pdf_path))
prompt = OpenRouterIntegrationAgent.with(
pdf_data: pdf_data,
prompt_text: "Extract information from this document and return as JSON",
output_schema: :resume_schema
).analyze_pdf
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
assert response.message.content.present?
# When output_schema is present, content is already parsed
result = response.message.content
assert_equal result["name"], "John Doe"
assert_equal result["email"], "john.doe@example.com"
assert_equal result["phone"], "(555) 123-4567"
assert_equal result["education"].first, { "degree"=>"BS Computer Science", "institution"=>"Stanford University", "year"=>2020 }
assert_equal result["experience"].first, { "job_title"=>"Senior Software Engineer", "company"=>"TechCorp", "duration"=>"2020-2024" }
# Generate documentation example
doc_example_output(response)
end
end
# endregion pdf_processing_local
test "processes PDF from remote URL of resume no plugins" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_pdf_remote_no_plugin") do
pdf_url = "https://docs.activeagents.ai/sample_resume.pdf"
prompt = OpenRouterIntegrationAgent.with(
pdf_url: pdf_url,
prompt_text: "Analyze the PDF",
output_schema: :resume_schema,
skip_plugin: true
).analyze_pdf
# Remote URLs are not supported without a PDF engine plugin
# OpenAI: Inputs by file URL are not supported for chat completions. Use the ResponsesAPI for this option.
# https://platform.openai.com/docs/guides/pdf-files#file-urls
# Accept either the OpenAI error directly or our wrapped error
# Suppress ruby-openai gem's error output to STDERR
error = assert_raises(ActiveAgent::GenerationProvider::Base::GenerationProviderError, OpenAI::Error) do
prompt.generate_now
end
# Check the error message regardless of which error type was raised
error_message = error.message
assert_match(/Missing required parameter.*file_id/, error_message)
assert_match(/Provider returned error|invalid_request_error/, error_message)
end
end
# region pdf_native_support
test "processes PDF with native model support" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_pdf_native") do
# Test with a model that might have native PDF support
# Using the native engine (charged as input tokens)
pdf_path = Rails.root.join("..", "..", "test", "fixtures", "files", "sample_resume.pdf")
pdf_data = Base64.strict_encode64(File.read(pdf_path))
prompt = OpenRouterIntegrationAgent.with(
pdf_data: pdf_data,
prompt_text: "Analyze this PDF document",
pdf_engine: "native" # Use native engine (charged as input tokens)
).analyze_pdf
# First verify the prompt has the plugins in options
assert prompt.options[:plugins].present?, "Plugins should be present in prompt options"
assert prompt.options[:fallback_models].present?, "Fallback models should be present in prompt options"
assert_equal "file-parser", prompt.options[:plugins][0][:id]
assert_equal "native", prompt.options[:plugins][0][:pdf][:engine]
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
assert response.message.content.present?
assert_includes response.message.content, "John Doe"
# Generate documentation example
doc_example_output(response)
end
end
# endregion pdf_native_support
test "processes PDF without any plugin for models with built-in support" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_pdf_no_plugin") do
# Test without any plugin - for models that have built-in PDF support
pdf_url = "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
prompt = OpenRouterIntegrationAgent.with(
pdf_url: pdf_url,
prompt_text: "Analyze this PDF document",
skip_plugin: true # Don't use any plugin
).analyze_pdf
# Verify no plugins are included when skip_plugin is true
assert_empty prompt.options[:plugins], "Should not have plugins when skip_plugin is true"
response = prompt.generate_now
raw_response = response.raw_response
assert_equal "Google", raw_response["provider"]
assert_not_nil response
assert_not_nil response.message
assert response.message.content.present?
# Generate documentation example
doc_example_output(response)
end
end
test "processes scanned PDF with OCR engine" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_pdf_ocr") do
# Test with the mistral-ocr engine for scanned documents
# Using a simple PDF that should be processable
pdf_url = "https://docs.activeagents.ai/sample_resume.pdf"
prompt = OpenRouterIntegrationAgent.with(
pdf_url: pdf_url,
prompt_text: "Extract text from this PDF.",
output_schema: :resume_schema,
pdf_engine: "mistral-ocr" # OCR engine for text extraction
).analyze_pdf
# Verify OCR engine is specified
assert prompt.options[:plugins].present?, "Should have plugins for OCR"
assert_equal "mistral-ocr", prompt.options[:plugins][0][:pdf][:engine]
response = prompt.generate_now
# MUST return valid JSON - no fallback allowed
raw_response = response.raw_response
# When output_schema is present, content is already parsed
result = response.message.content
assert_equal result["name"], "John Doe"
assert_equal result["email"], "john.doe@example.com"
assert_equal result["phone"], "(555) 123-4567"
assert_equal result["education"], [ { "degree"=>"BS Computer Science", "institution"=>"Stanford University", "year"=>2020 } ]
assert_equal result["experience"], [ { "job_title"=>"Senior Software Engineer", "company"=>"TechCorp", "duration"=>"2020-2024" } ]
# Generate documentation example
doc_example_output(response)
end
end
test "uses fallback models when primary fails" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_fallback_models") do
prompt = OpenRouterIntegrationAgent.test_fallback
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
# Check metadata for fallback usage
if response.respond_to?(:metadata) && response.metadata
# Should use one of the fallback models, not the primary
possible_models = [ "openai/gpt-3.5-turbo-0301", "openai/gpt-3.5-turbo", "openai/gpt-4o-mini" ]
assert possible_models.include?(response.metadata[:model_used])
assert response.metadata[:provider].present?
end
# The response should still work (2+2=4)
assert response.message.content.include?("4")
# Generate documentation example
doc_example_output(response)
end
end
test "applies transforms for long content" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_transforms") do
# Generate a very long text
long_text = "Lorem ipsum dolor sit amet. " * 1000
prompt = OpenRouterIntegrationAgent.with(text: long_text).process_long_text
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
assert response.message.content.present?
# The summary should be much shorter than the original
assert response.message.content.length < long_text.length / 10
# Generate documentation example
doc_example_output(response)
end
end
test "tracks usage and costs" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_cost_tracking") do
prompt = OpenRouterIntegrationAgent.with(message: "Hello").prompt_context
response = prompt.generate_now
assert_not_nil response
# Check for usage information
if response.respond_to?(:usage) && response.usage
assert response.usage["prompt_tokens"].is_a?(Integer)
assert response.usage["completion_tokens"].is_a?(Integer)
assert response.usage["total_tokens"].is_a?(Integer)
end
# Check for metadata with model information from OpenRouter
if response.respond_to?(:metadata) && response.metadata
assert response.metadata[:model_used].present?
assert response.metadata[:provider].present?
# Verify we're using the expected model (gpt-4o-mini)
assert_equal "openai/gpt-4o-mini", response.metadata[:model_used]
end
# Generate documentation example
doc_example_output(response)
end
end
test "includes OpenRouter headers in requests" do
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o",
"app_name" => "TestApp",
"site_url" => "https://test.example.com"
)
# Get the headers that would be sent
headers = provider.send(:openrouter_headers)
assert_equal "https://test.example.com", headers["HTTP-Referer"]
assert_equal "TestApp", headers["X-Title"]
end
test "builds provider preferences correctly" do
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o",
"enable_fallbacks" => true,
"provider" => {
"order" => [ "OpenAI", "Anthropic" ],
"require_parameters" => true,
"data_collection" => "deny"
}
)
prefs = provider.send(:build_provider_preferences)
assert_equal [ "OpenAI", "Anthropic" ], prefs[:order]
assert_equal true, prefs[:require_parameters]
assert_equal true, prefs[:allow_fallbacks]
assert_equal "deny", prefs[:data_collection]
end
test "configures data collection policies" do
# Test deny all data collection
provider_deny = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o",
"data_collection" => "deny"
)
prefs_deny = provider_deny.send(:build_provider_preferences)
assert_equal "deny", prefs_deny[:data_collection]
# Test allow all data collection (default)
provider_allow = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o"
)
prefs_allow = provider_allow.send(:build_provider_preferences)
assert_equal "allow", prefs_allow[:data_collection]
# Test selective provider data collection
provider_selective = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o",
"data_collection" => [ "OpenAI", "Google" ]
)
prefs_selective = provider_selective.send(:build_provider_preferences)
assert_equal [ "OpenAI", "Google" ], prefs_selective[:data_collection]
end
test "handles multimodal content correctly" do
# Create a message with multimodal content
message = ActiveAgent::ActionPrompt::Message.new(
content: [
{ type: "text", text: "What's in this image?" },
{ type: "image_url", image_url: { url: "https://example.com/image.jpg" } }
],
role: :user
)
prompt = ActiveAgent::ActionPrompt::Prompt.new(
messages: [ message ]
)
assert prompt.multimodal?
end
test "converts file type to image_url for OpenRouter PDF support" do
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o"
)
# Test file type conversion
file_item = {
type: "file",
file: {
file_data: "data:application/pdf;base64,JVBERi0xLj..."
}
}
formatted = provider.send(:format_content_item, file_item)
assert_equal "image_url", formatted[:type]
assert_equal "data:application/pdf;base64,JVBERi0xLj...", formatted[:image_url][:url]
end
test "respects configuration hierarchy for site_url" do
# Test with explicit site_url config
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o",
"site_url" => "https://configured.example.com"
)
assert_equal "https://configured.example.com", provider.instance_variable_get(:@site_url)
# Test with default_url_options in config
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o",
"default_url_options" => {
"host" => "fromconfig.example.com"
}
)
assert_equal "fromconfig.example.com", provider.instance_variable_get(:@site_url)
end
test "handles rate limit information in metadata" do
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o"
)
# Create a mock response
prompt = ActiveAgent::ActionPrompt::Prompt.new(message: "test")
response = ActiveAgent::GenerationProvider::Response.new(prompt: prompt)
headers = {
"x-provider" => "OpenAI",
"x-model" => "gpt-4o",
"x-ratelimit-requests-limit" => "100",
"x-ratelimit-requests-remaining" => "99",
"x-ratelimit-tokens-limit" => "10000",
"x-ratelimit-tokens-remaining" => "9500"
}
provider.send(:add_openrouter_metadata, response, headers)
assert_equal "100", response.metadata[:ratelimit][:requests_limit]
assert_equal "99", response.metadata[:ratelimit][:requests_remaining]
assert_equal "10000", response.metadata[:ratelimit][:tokens_limit]
assert_equal "9500", response.metadata[:ratelimit][:tokens_remaining]
end
test "includes plugins parameter when passed in options" do
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o"
)
# Create a prompt with plugins option
prompt = ActiveAgent::ActionPrompt::Prompt.new(
message: "test",
options: {
plugins: [
{
id: "file-parser",
pdf: {
engine: "pdf-text"
}
}
]
}
)
# Set the prompt on the provider
provider.instance_variable_set(:@prompt, prompt)
# Build parameters and verify plugins are included
parameters = provider.send(:build_openrouter_parameters)
assert_not_nil parameters[:plugins]
assert_equal 1, parameters[:plugins].size
assert_equal "file-parser", parameters[:plugins][0][:id]
assert_equal "pdf-text", parameters[:plugins][0][:pdf][:engine]
end
endPDF Processing Example
PDF Processing Options
OpenRouter offers multiple PDF processing engines:
- Native Engine: Charged as input tokens, best for models with built-in PDF support
- Mistral OCR Engine: $2 per 1000 pages, optimized for scanned documents
- No Plugin: For models that have built-in PDF capabilities
Example with OCR engine:
test "processes PDF with native model support" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_pdf_native") do
# Test with a model that might have native PDF support
# Using the native engine (charged as input tokens)
pdf_path = Rails.root.join("..", "..", "test", "fixtures", "files", "sample_resume.pdf")
pdf_data = Base64.strict_encode64(File.read(pdf_path))
prompt = OpenRouterIntegrationAgent.with(
pdf_data: pdf_data,
prompt_text: "Analyze this PDF document",
pdf_engine: "native" # Use native engine (charged as input tokens)
).analyze_pdf
# First verify the prompt has the plugins in options
assert prompt.options[:plugins].present?, "Plugins should be present in prompt options"
assert prompt.options[:fallback_models].present?, "Fallback models should be present in prompt options"
assert_equal "file-parser", prompt.options[:plugins][0][:id]
assert_equal "native", prompt.options[:plugins][0][:pdf][:engine]
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
assert response.message.content.present?
assert_includes response.message.content, "John Doe"
# Generate documentation example
doc_example_output(response)
end
endOCR Processing Example
Fallback Models
Configure fallback models for improved reliability:
require "test_helper"
require "base64"
require "active_agent/action_prompt/message"
class OpenRouterIntegrationTest < ActiveSupport::TestCase
setup do
@agent = OpenRouterIntegrationAgent.new
end
test "analyzes image with structured output schema" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_image_analysis_structured") do
# Use the sales chart image URL for structured analysis
image_url = "https://raw.githubusercontent.com/activeagents/activeagent/refs/heads/main/test/fixtures/images/sales_chart.png"
prompt = OpenRouterIntegrationAgent.with(image_url: image_url).analyze_image
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
# When output_schema is present, content is already parsed
result = response.message.content
# Verify the structure matches our schema
assert result.key?("description")
assert result.key?("objects")
assert result.key?("scene_type")
assert result.key?("primary_colors")
assert result["objects"].is_a?(Array)
assert [ "indoor", "outdoor", "abstract", "document", "photo", "illustration" ].include?(result["scene_type"])
end
end
test "analyzes remote image URL without structured output" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_remote_image_basic") do
# Use a landscape image URL for basic analysis
image_url = "https://picsum.photos/400/300"
# For now, just use analyze_image without the structured output schema
# We'll get a natural language description instead of JSON
prompt = OpenRouterIntegrationAgent.with(image_url: image_url).analyze_image
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
assert response.message.content.is_a?(String)
assert response.message.content.length > 10
# Since analyze_image uses structured output, we'll get JSON
# Just verify we got a response
# In the future, we could add a simple_analyze action without schema
# Generate documentation example
doc_example_output(response)
end
end
test "extracts receipt data with structured output from local file" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_receipt_extraction_local") do
# Use the test receipt image - file exists, no conditional needed
receipt_path = Rails.root.join("..", "..", "test", "fixtures", "images", "test_receipt.png")
prompt = OpenRouterIntegrationAgent.with(image_path: receipt_path).extract_receipt_data
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
# When output_schema is present, content is already parsed
result = response.message.content
assert_equal result["merchant"]["name"], "Corner Mart"
assert_equal result["total"]["amount"], 14.83
assert_equal result["items"].size, 4
result["items"].each do |item|
assert item.key?("name")
assert item.key?("quantity")
assert item.key?("price")
end
assert_equal result["items"][0], { "name"=>"Milk", "quantity"=>1, "price"=>3.49 }
assert_equal result["items"][1], { "name"=>"Bread", "quantity"=>1, "price"=>2.29 }
assert_equal result["items"][2], { "name"=>"Apples", "quantity"=>1, "price"=>5.1 }
assert_equal result["items"][3], { "name"=>"Eggs", "quantity"=>1, "price"=>2.99 }
# Generate documentation example
doc_example_output(response)
end
end
test "handles base64 encoded images with sales chart" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_base64_sales_chart") do
# Use the sales chart image
chart_path = Rails.root.join("..", "..", "test", "fixtures", "images", "sales_chart.png")
prompt = OpenRouterIntegrationAgent.with(image_path: chart_path).analyze_image
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
assert_includes response.message.content, "(Q1, Q2, Q3, Q4), with varying heights indicating different sales amounts"
# Generate documentation example
doc_example_output(response)
end
end
test "processes PDF document from local file" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_pdf_local") do
# Use the sample resume PDF
pdf_path = Rails.root.join("..", "..", "test", "fixtures", "files", "sample_resume.pdf")
# Read and encode the PDF as base64 - OpenRouter accepts PDFs as image_url with data URL
pdf_data = Base64.strict_encode64(File.read(pdf_path))
prompt = OpenRouterIntegrationAgent.with(
pdf_data: pdf_data,
prompt_text: "Extract information from this document and return as JSON",
output_schema: :resume_schema
).analyze_pdf
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
assert response.message.content.present?
# When output_schema is present, content is already parsed
result = response.message.content
assert_equal result["name"], "John Doe"
assert_equal result["email"], "john.doe@example.com"
assert_equal result["phone"], "(555) 123-4567"
assert_equal result["education"].first, { "degree"=>"BS Computer Science", "institution"=>"Stanford University", "year"=>2020 }
assert_equal result["experience"].first, { "job_title"=>"Senior Software Engineer", "company"=>"TechCorp", "duration"=>"2020-2024" }
# Generate documentation example
doc_example_output(response)
end
end
# endregion pdf_processing_local
test "processes PDF from remote URL of resume no plugins" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_pdf_remote_no_plugin") do
pdf_url = "https://docs.activeagents.ai/sample_resume.pdf"
prompt = OpenRouterIntegrationAgent.with(
pdf_url: pdf_url,
prompt_text: "Analyze the PDF",
output_schema: :resume_schema,
skip_plugin: true
).analyze_pdf
# Remote URLs are not supported without a PDF engine plugin
# OpenAI: Inputs by file URL are not supported for chat completions. Use the ResponsesAPI for this option.
# https://platform.openai.com/docs/guides/pdf-files#file-urls
# Accept either the OpenAI error directly or our wrapped error
# Suppress ruby-openai gem's error output to STDERR
error = assert_raises(ActiveAgent::GenerationProvider::Base::GenerationProviderError, OpenAI::Error) do
prompt.generate_now
end
# Check the error message regardless of which error type was raised
error_message = error.message
assert_match(/Missing required parameter.*file_id/, error_message)
assert_match(/Provider returned error|invalid_request_error/, error_message)
end
end
# region pdf_native_support
test "processes PDF with native model support" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_pdf_native") do
# Test with a model that might have native PDF support
# Using the native engine (charged as input tokens)
pdf_path = Rails.root.join("..", "..", "test", "fixtures", "files", "sample_resume.pdf")
pdf_data = Base64.strict_encode64(File.read(pdf_path))
prompt = OpenRouterIntegrationAgent.with(
pdf_data: pdf_data,
prompt_text: "Analyze this PDF document",
pdf_engine: "native" # Use native engine (charged as input tokens)
).analyze_pdf
# First verify the prompt has the plugins in options
assert prompt.options[:plugins].present?, "Plugins should be present in prompt options"
assert prompt.options[:fallback_models].present?, "Fallback models should be present in prompt options"
assert_equal "file-parser", prompt.options[:plugins][0][:id]
assert_equal "native", prompt.options[:plugins][0][:pdf][:engine]
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
assert response.message.content.present?
assert_includes response.message.content, "John Doe"
# Generate documentation example
doc_example_output(response)
end
end
# endregion pdf_native_support
test "processes PDF without any plugin for models with built-in support" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_pdf_no_plugin") do
# Test without any plugin - for models that have built-in PDF support
pdf_url = "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
prompt = OpenRouterIntegrationAgent.with(
pdf_url: pdf_url,
prompt_text: "Analyze this PDF document",
skip_plugin: true # Don't use any plugin
).analyze_pdf
# Verify no plugins are included when skip_plugin is true
assert_empty prompt.options[:plugins], "Should not have plugins when skip_plugin is true"
response = prompt.generate_now
raw_response = response.raw_response
assert_equal "Google", raw_response["provider"]
assert_not_nil response
assert_not_nil response.message
assert response.message.content.present?
# Generate documentation example
doc_example_output(response)
end
end
test "processes scanned PDF with OCR engine" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_pdf_ocr") do
# Test with the mistral-ocr engine for scanned documents
# Using a simple PDF that should be processable
pdf_url = "https://docs.activeagents.ai/sample_resume.pdf"
prompt = OpenRouterIntegrationAgent.with(
pdf_url: pdf_url,
prompt_text: "Extract text from this PDF.",
output_schema: :resume_schema,
pdf_engine: "mistral-ocr" # OCR engine for text extraction
).analyze_pdf
# Verify OCR engine is specified
assert prompt.options[:plugins].present?, "Should have plugins for OCR"
assert_equal "mistral-ocr", prompt.options[:plugins][0][:pdf][:engine]
response = prompt.generate_now
# MUST return valid JSON - no fallback allowed
raw_response = response.raw_response
# When output_schema is present, content is already parsed
result = response.message.content
assert_equal result["name"], "John Doe"
assert_equal result["email"], "john.doe@example.com"
assert_equal result["phone"], "(555) 123-4567"
assert_equal result["education"], [ { "degree"=>"BS Computer Science", "institution"=>"Stanford University", "year"=>2020 } ]
assert_equal result["experience"], [ { "job_title"=>"Senior Software Engineer", "company"=>"TechCorp", "duration"=>"2020-2024" } ]
# Generate documentation example
doc_example_output(response)
end
end
test "uses fallback models when primary fails" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_fallback_models") do
prompt = OpenRouterIntegrationAgent.test_fallback
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
# Check metadata for fallback usage
if response.respond_to?(:metadata) && response.metadata
# Should use one of the fallback models, not the primary
possible_models = [ "openai/gpt-3.5-turbo-0301", "openai/gpt-3.5-turbo", "openai/gpt-4o-mini" ]
assert possible_models.include?(response.metadata[:model_used])
assert response.metadata[:provider].present?
end
# The response should still work (2+2=4)
assert response.message.content.include?("4")
# Generate documentation example
doc_example_output(response)
end
end
test "applies transforms for long content" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_transforms") do
# Generate a very long text
long_text = "Lorem ipsum dolor sit amet. " * 1000
prompt = OpenRouterIntegrationAgent.with(text: long_text).process_long_text
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
assert response.message.content.present?
# The summary should be much shorter than the original
assert response.message.content.length < long_text.length / 10
# Generate documentation example
doc_example_output(response)
end
end
test "tracks usage and costs" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_cost_tracking") do
prompt = OpenRouterIntegrationAgent.with(message: "Hello").prompt_context
response = prompt.generate_now
assert_not_nil response
# Check for usage information
if response.respond_to?(:usage) && response.usage
assert response.usage["prompt_tokens"].is_a?(Integer)
assert response.usage["completion_tokens"].is_a?(Integer)
assert response.usage["total_tokens"].is_a?(Integer)
end
# Check for metadata with model information from OpenRouter
if response.respond_to?(:metadata) && response.metadata
assert response.metadata[:model_used].present?
assert response.metadata[:provider].present?
# Verify we're using the expected model (gpt-4o-mini)
assert_equal "openai/gpt-4o-mini", response.metadata[:model_used]
end
# Generate documentation example
doc_example_output(response)
end
end
test "includes OpenRouter headers in requests" do
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o",
"app_name" => "TestApp",
"site_url" => "https://test.example.com"
)
# Get the headers that would be sent
headers = provider.send(:openrouter_headers)
assert_equal "https://test.example.com", headers["HTTP-Referer"]
assert_equal "TestApp", headers["X-Title"]
end
test "builds provider preferences correctly" do
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o",
"enable_fallbacks" => true,
"provider" => {
"order" => [ "OpenAI", "Anthropic" ],
"require_parameters" => true,
"data_collection" => "deny"
}
)
prefs = provider.send(:build_provider_preferences)
assert_equal [ "OpenAI", "Anthropic" ], prefs[:order]
assert_equal true, prefs[:require_parameters]
assert_equal true, prefs[:allow_fallbacks]
assert_equal "deny", prefs[:data_collection]
end
test "configures data collection policies" do
# Test deny all data collection
provider_deny = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o",
"data_collection" => "deny"
)
prefs_deny = provider_deny.send(:build_provider_preferences)
assert_equal "deny", prefs_deny[:data_collection]
# Test allow all data collection (default)
provider_allow = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o"
)
prefs_allow = provider_allow.send(:build_provider_preferences)
assert_equal "allow", prefs_allow[:data_collection]
# Test selective provider data collection
provider_selective = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o",
"data_collection" => [ "OpenAI", "Google" ]
)
prefs_selective = provider_selective.send(:build_provider_preferences)
assert_equal [ "OpenAI", "Google" ], prefs_selective[:data_collection]
end
test "handles multimodal content correctly" do
# Create a message with multimodal content
message = ActiveAgent::ActionPrompt::Message.new(
content: [
{ type: "text", text: "What's in this image?" },
{ type: "image_url", image_url: { url: "https://example.com/image.jpg" } }
],
role: :user
)
prompt = ActiveAgent::ActionPrompt::Prompt.new(
messages: [ message ]
)
assert prompt.multimodal?
end
test "converts file type to image_url for OpenRouter PDF support" do
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o"
)
# Test file type conversion
file_item = {
type: "file",
file: {
file_data: "data:application/pdf;base64,JVBERi0xLj..."
}
}
formatted = provider.send(:format_content_item, file_item)
assert_equal "image_url", formatted[:type]
assert_equal "data:application/pdf;base64,JVBERi0xLj...", formatted[:image_url][:url]
end
test "respects configuration hierarchy for site_url" do
# Test with explicit site_url config
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o",
"site_url" => "https://configured.example.com"
)
assert_equal "https://configured.example.com", provider.instance_variable_get(:@site_url)
# Test with default_url_options in config
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o",
"default_url_options" => {
"host" => "fromconfig.example.com"
}
)
assert_equal "fromconfig.example.com", provider.instance_variable_get(:@site_url)
end
test "handles rate limit information in metadata" do
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o"
)
# Create a mock response
prompt = ActiveAgent::ActionPrompt::Prompt.new(message: "test")
response = ActiveAgent::GenerationProvider::Response.new(prompt: prompt)
headers = {
"x-provider" => "OpenAI",
"x-model" => "gpt-4o",
"x-ratelimit-requests-limit" => "100",
"x-ratelimit-requests-remaining" => "99",
"x-ratelimit-tokens-limit" => "10000",
"x-ratelimit-tokens-remaining" => "9500"
}
provider.send(:add_openrouter_metadata, response, headers)
assert_equal "100", response.metadata[:ratelimit][:requests_limit]
assert_equal "99", response.metadata[:ratelimit][:requests_remaining]
assert_equal "10000", response.metadata[:ratelimit][:tokens_limit]
assert_equal "9500", response.metadata[:ratelimit][:tokens_remaining]
end
test "includes plugins parameter when passed in options" do
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o"
)
# Create a prompt with plugins option
prompt = ActiveAgent::ActionPrompt::Prompt.new(
message: "test",
options: {
plugins: [
{
id: "file-parser",
pdf: {
engine: "pdf-text"
}
}
]
}
)
# Set the prompt on the provider
provider.instance_variable_set(:@prompt, prompt)
# Build parameters and verify plugins are included
parameters = provider.send(:build_openrouter_parameters)
assert_not_nil parameters[:plugins]
assert_equal 1, parameters[:plugins].size
assert_equal "file-parser", parameters[:plugins][0][:id]
assert_equal "pdf-text", parameters[:plugins][0][:pdf][:engine]
end
endFallback Model Example
Content Transforms
Apply transforms for handling long content:
require "test_helper"
require "base64"
require "active_agent/action_prompt/message"
class OpenRouterIntegrationTest < ActiveSupport::TestCase
setup do
@agent = OpenRouterIntegrationAgent.new
end
test "analyzes image with structured output schema" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_image_analysis_structured") do
# Use the sales chart image URL for structured analysis
image_url = "https://raw.githubusercontent.com/activeagents/activeagent/refs/heads/main/test/fixtures/images/sales_chart.png"
prompt = OpenRouterIntegrationAgent.with(image_url: image_url).analyze_image
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
# When output_schema is present, content is already parsed
result = response.message.content
# Verify the structure matches our schema
assert result.key?("description")
assert result.key?("objects")
assert result.key?("scene_type")
assert result.key?("primary_colors")
assert result["objects"].is_a?(Array)
assert [ "indoor", "outdoor", "abstract", "document", "photo", "illustration" ].include?(result["scene_type"])
end
end
test "analyzes remote image URL without structured output" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_remote_image_basic") do
# Use a landscape image URL for basic analysis
image_url = "https://picsum.photos/400/300"
# For now, just use analyze_image without the structured output schema
# We'll get a natural language description instead of JSON
prompt = OpenRouterIntegrationAgent.with(image_url: image_url).analyze_image
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
assert response.message.content.is_a?(String)
assert response.message.content.length > 10
# Since analyze_image uses structured output, we'll get JSON
# Just verify we got a response
# In the future, we could add a simple_analyze action without schema
# Generate documentation example
doc_example_output(response)
end
end
test "extracts receipt data with structured output from local file" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_receipt_extraction_local") do
# Use the test receipt image - file exists, no conditional needed
receipt_path = Rails.root.join("..", "..", "test", "fixtures", "images", "test_receipt.png")
prompt = OpenRouterIntegrationAgent.with(image_path: receipt_path).extract_receipt_data
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
# When output_schema is present, content is already parsed
result = response.message.content
assert_equal result["merchant"]["name"], "Corner Mart"
assert_equal result["total"]["amount"], 14.83
assert_equal result["items"].size, 4
result["items"].each do |item|
assert item.key?("name")
assert item.key?("quantity")
assert item.key?("price")
end
assert_equal result["items"][0], { "name"=>"Milk", "quantity"=>1, "price"=>3.49 }
assert_equal result["items"][1], { "name"=>"Bread", "quantity"=>1, "price"=>2.29 }
assert_equal result["items"][2], { "name"=>"Apples", "quantity"=>1, "price"=>5.1 }
assert_equal result["items"][3], { "name"=>"Eggs", "quantity"=>1, "price"=>2.99 }
# Generate documentation example
doc_example_output(response)
end
end
test "handles base64 encoded images with sales chart" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_base64_sales_chart") do
# Use the sales chart image
chart_path = Rails.root.join("..", "..", "test", "fixtures", "images", "sales_chart.png")
prompt = OpenRouterIntegrationAgent.with(image_path: chart_path).analyze_image
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
assert_includes response.message.content, "(Q1, Q2, Q3, Q4), with varying heights indicating different sales amounts"
# Generate documentation example
doc_example_output(response)
end
end
test "processes PDF document from local file" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_pdf_local") do
# Use the sample resume PDF
pdf_path = Rails.root.join("..", "..", "test", "fixtures", "files", "sample_resume.pdf")
# Read and encode the PDF as base64 - OpenRouter accepts PDFs as image_url with data URL
pdf_data = Base64.strict_encode64(File.read(pdf_path))
prompt = OpenRouterIntegrationAgent.with(
pdf_data: pdf_data,
prompt_text: "Extract information from this document and return as JSON",
output_schema: :resume_schema
).analyze_pdf
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
assert response.message.content.present?
# When output_schema is present, content is already parsed
result = response.message.content
assert_equal result["name"], "John Doe"
assert_equal result["email"], "john.doe@example.com"
assert_equal result["phone"], "(555) 123-4567"
assert_equal result["education"].first, { "degree"=>"BS Computer Science", "institution"=>"Stanford University", "year"=>2020 }
assert_equal result["experience"].first, { "job_title"=>"Senior Software Engineer", "company"=>"TechCorp", "duration"=>"2020-2024" }
# Generate documentation example
doc_example_output(response)
end
end
# endregion pdf_processing_local
test "processes PDF from remote URL of resume no plugins" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_pdf_remote_no_plugin") do
pdf_url = "https://docs.activeagents.ai/sample_resume.pdf"
prompt = OpenRouterIntegrationAgent.with(
pdf_url: pdf_url,
prompt_text: "Analyze the PDF",
output_schema: :resume_schema,
skip_plugin: true
).analyze_pdf
# Remote URLs are not supported without a PDF engine plugin
# OpenAI: Inputs by file URL are not supported for chat completions. Use the ResponsesAPI for this option.
# https://platform.openai.com/docs/guides/pdf-files#file-urls
# Accept either the OpenAI error directly or our wrapped error
# Suppress ruby-openai gem's error output to STDERR
error = assert_raises(ActiveAgent::GenerationProvider::Base::GenerationProviderError, OpenAI::Error) do
prompt.generate_now
end
# Check the error message regardless of which error type was raised
error_message = error.message
assert_match(/Missing required parameter.*file_id/, error_message)
assert_match(/Provider returned error|invalid_request_error/, error_message)
end
end
# region pdf_native_support
test "processes PDF with native model support" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_pdf_native") do
# Test with a model that might have native PDF support
# Using the native engine (charged as input tokens)
pdf_path = Rails.root.join("..", "..", "test", "fixtures", "files", "sample_resume.pdf")
pdf_data = Base64.strict_encode64(File.read(pdf_path))
prompt = OpenRouterIntegrationAgent.with(
pdf_data: pdf_data,
prompt_text: "Analyze this PDF document",
pdf_engine: "native" # Use native engine (charged as input tokens)
).analyze_pdf
# First verify the prompt has the plugins in options
assert prompt.options[:plugins].present?, "Plugins should be present in prompt options"
assert prompt.options[:fallback_models].present?, "Fallback models should be present in prompt options"
assert_equal "file-parser", prompt.options[:plugins][0][:id]
assert_equal "native", prompt.options[:plugins][0][:pdf][:engine]
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
assert response.message.content.present?
assert_includes response.message.content, "John Doe"
# Generate documentation example
doc_example_output(response)
end
end
# endregion pdf_native_support
test "processes PDF without any plugin for models with built-in support" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_pdf_no_plugin") do
# Test without any plugin - for models that have built-in PDF support
pdf_url = "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
prompt = OpenRouterIntegrationAgent.with(
pdf_url: pdf_url,
prompt_text: "Analyze this PDF document",
skip_plugin: true # Don't use any plugin
).analyze_pdf
# Verify no plugins are included when skip_plugin is true
assert_empty prompt.options[:plugins], "Should not have plugins when skip_plugin is true"
response = prompt.generate_now
raw_response = response.raw_response
assert_equal "Google", raw_response["provider"]
assert_not_nil response
assert_not_nil response.message
assert response.message.content.present?
# Generate documentation example
doc_example_output(response)
end
end
test "processes scanned PDF with OCR engine" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_pdf_ocr") do
# Test with the mistral-ocr engine for scanned documents
# Using a simple PDF that should be processable
pdf_url = "https://docs.activeagents.ai/sample_resume.pdf"
prompt = OpenRouterIntegrationAgent.with(
pdf_url: pdf_url,
prompt_text: "Extract text from this PDF.",
output_schema: :resume_schema,
pdf_engine: "mistral-ocr" # OCR engine for text extraction
).analyze_pdf
# Verify OCR engine is specified
assert prompt.options[:plugins].present?, "Should have plugins for OCR"
assert_equal "mistral-ocr", prompt.options[:plugins][0][:pdf][:engine]
response = prompt.generate_now
# MUST return valid JSON - no fallback allowed
raw_response = response.raw_response
# When output_schema is present, content is already parsed
result = response.message.content
assert_equal result["name"], "John Doe"
assert_equal result["email"], "john.doe@example.com"
assert_equal result["phone"], "(555) 123-4567"
assert_equal result["education"], [ { "degree"=>"BS Computer Science", "institution"=>"Stanford University", "year"=>2020 } ]
assert_equal result["experience"], [ { "job_title"=>"Senior Software Engineer", "company"=>"TechCorp", "duration"=>"2020-2024" } ]
# Generate documentation example
doc_example_output(response)
end
end
test "uses fallback models when primary fails" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_fallback_models") do
prompt = OpenRouterIntegrationAgent.test_fallback
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
# Check metadata for fallback usage
if response.respond_to?(:metadata) && response.metadata
# Should use one of the fallback models, not the primary
possible_models = [ "openai/gpt-3.5-turbo-0301", "openai/gpt-3.5-turbo", "openai/gpt-4o-mini" ]
assert possible_models.include?(response.metadata[:model_used])
assert response.metadata[:provider].present?
end
# The response should still work (2+2=4)
assert response.message.content.include?("4")
# Generate documentation example
doc_example_output(response)
end
end
test "applies transforms for long content" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_transforms") do
# Generate a very long text
long_text = "Lorem ipsum dolor sit amet. " * 1000
prompt = OpenRouterIntegrationAgent.with(text: long_text).process_long_text
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
assert response.message.content.present?
# The summary should be much shorter than the original
assert response.message.content.length < long_text.length / 10
# Generate documentation example
doc_example_output(response)
end
end
test "tracks usage and costs" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_cost_tracking") do
prompt = OpenRouterIntegrationAgent.with(message: "Hello").prompt_context
response = prompt.generate_now
assert_not_nil response
# Check for usage information
if response.respond_to?(:usage) && response.usage
assert response.usage["prompt_tokens"].is_a?(Integer)
assert response.usage["completion_tokens"].is_a?(Integer)
assert response.usage["total_tokens"].is_a?(Integer)
end
# Check for metadata with model information from OpenRouter
if response.respond_to?(:metadata) && response.metadata
assert response.metadata[:model_used].present?
assert response.metadata[:provider].present?
# Verify we're using the expected model (gpt-4o-mini)
assert_equal "openai/gpt-4o-mini", response.metadata[:model_used]
end
# Generate documentation example
doc_example_output(response)
end
end
test "includes OpenRouter headers in requests" do
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o",
"app_name" => "TestApp",
"site_url" => "https://test.example.com"
)
# Get the headers that would be sent
headers = provider.send(:openrouter_headers)
assert_equal "https://test.example.com", headers["HTTP-Referer"]
assert_equal "TestApp", headers["X-Title"]
end
test "builds provider preferences correctly" do
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o",
"enable_fallbacks" => true,
"provider" => {
"order" => [ "OpenAI", "Anthropic" ],
"require_parameters" => true,
"data_collection" => "deny"
}
)
prefs = provider.send(:build_provider_preferences)
assert_equal [ "OpenAI", "Anthropic" ], prefs[:order]
assert_equal true, prefs[:require_parameters]
assert_equal true, prefs[:allow_fallbacks]
assert_equal "deny", prefs[:data_collection]
end
test "configures data collection policies" do
# Test deny all data collection
provider_deny = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o",
"data_collection" => "deny"
)
prefs_deny = provider_deny.send(:build_provider_preferences)
assert_equal "deny", prefs_deny[:data_collection]
# Test allow all data collection (default)
provider_allow = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o"
)
prefs_allow = provider_allow.send(:build_provider_preferences)
assert_equal "allow", prefs_allow[:data_collection]
# Test selective provider data collection
provider_selective = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o",
"data_collection" => [ "OpenAI", "Google" ]
)
prefs_selective = provider_selective.send(:build_provider_preferences)
assert_equal [ "OpenAI", "Google" ], prefs_selective[:data_collection]
end
test "handles multimodal content correctly" do
# Create a message with multimodal content
message = ActiveAgent::ActionPrompt::Message.new(
content: [
{ type: "text", text: "What's in this image?" },
{ type: "image_url", image_url: { url: "https://example.com/image.jpg" } }
],
role: :user
)
prompt = ActiveAgent::ActionPrompt::Prompt.new(
messages: [ message ]
)
assert prompt.multimodal?
end
test "converts file type to image_url for OpenRouter PDF support" do
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o"
)
# Test file type conversion
file_item = {
type: "file",
file: {
file_data: "data:application/pdf;base64,JVBERi0xLj..."
}
}
formatted = provider.send(:format_content_item, file_item)
assert_equal "image_url", formatted[:type]
assert_equal "data:application/pdf;base64,JVBERi0xLj...", formatted[:image_url][:url]
end
test "respects configuration hierarchy for site_url" do
# Test with explicit site_url config
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o",
"site_url" => "https://configured.example.com"
)
assert_equal "https://configured.example.com", provider.instance_variable_get(:@site_url)
# Test with default_url_options in config
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o",
"default_url_options" => {
"host" => "fromconfig.example.com"
}
)
assert_equal "fromconfig.example.com", provider.instance_variable_get(:@site_url)
end
test "handles rate limit information in metadata" do
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o"
)
# Create a mock response
prompt = ActiveAgent::ActionPrompt::Prompt.new(message: "test")
response = ActiveAgent::GenerationProvider::Response.new(prompt: prompt)
headers = {
"x-provider" => "OpenAI",
"x-model" => "gpt-4o",
"x-ratelimit-requests-limit" => "100",
"x-ratelimit-requests-remaining" => "99",
"x-ratelimit-tokens-limit" => "10000",
"x-ratelimit-tokens-remaining" => "9500"
}
provider.send(:add_openrouter_metadata, response, headers)
assert_equal "100", response.metadata[:ratelimit][:requests_limit]
assert_equal "99", response.metadata[:ratelimit][:requests_remaining]
assert_equal "10000", response.metadata[:ratelimit][:tokens_limit]
assert_equal "9500", response.metadata[:ratelimit][:tokens_remaining]
end
test "includes plugins parameter when passed in options" do
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o"
)
# Create a prompt with plugins option
prompt = ActiveAgent::ActionPrompt::Prompt.new(
message: "test",
options: {
plugins: [
{
id: "file-parser",
pdf: {
engine: "pdf-text"
}
}
]
}
)
# Set the prompt on the provider
provider.instance_variable_set(:@prompt, prompt)
# Build parameters and verify plugins are included
parameters = provider.send(:build_openrouter_parameters)
assert_not_nil parameters[:plugins]
assert_equal 1, parameters[:plugins].size
assert_equal "file-parser", parameters[:plugins][0][:id]
assert_equal "pdf-text", parameters[:plugins][0][:pdf][:engine]
end
endTransform Example
Usage and Cost Tracking
Track token usage and costs for OpenRouter requests:
require "test_helper"
require "base64"
require "active_agent/action_prompt/message"
class OpenRouterIntegrationTest < ActiveSupport::TestCase
setup do
@agent = OpenRouterIntegrationAgent.new
end
test "analyzes image with structured output schema" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_image_analysis_structured") do
# Use the sales chart image URL for structured analysis
image_url = "https://raw.githubusercontent.com/activeagents/activeagent/refs/heads/main/test/fixtures/images/sales_chart.png"
prompt = OpenRouterIntegrationAgent.with(image_url: image_url).analyze_image
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
# When output_schema is present, content is already parsed
result = response.message.content
# Verify the structure matches our schema
assert result.key?("description")
assert result.key?("objects")
assert result.key?("scene_type")
assert result.key?("primary_colors")
assert result["objects"].is_a?(Array)
assert [ "indoor", "outdoor", "abstract", "document", "photo", "illustration" ].include?(result["scene_type"])
end
end
test "analyzes remote image URL without structured output" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_remote_image_basic") do
# Use a landscape image URL for basic analysis
image_url = "https://picsum.photos/400/300"
# For now, just use analyze_image without the structured output schema
# We'll get a natural language description instead of JSON
prompt = OpenRouterIntegrationAgent.with(image_url: image_url).analyze_image
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
assert response.message.content.is_a?(String)
assert response.message.content.length > 10
# Since analyze_image uses structured output, we'll get JSON
# Just verify we got a response
# In the future, we could add a simple_analyze action without schema
# Generate documentation example
doc_example_output(response)
end
end
test "extracts receipt data with structured output from local file" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_receipt_extraction_local") do
# Use the test receipt image - file exists, no conditional needed
receipt_path = Rails.root.join("..", "..", "test", "fixtures", "images", "test_receipt.png")
prompt = OpenRouterIntegrationAgent.with(image_path: receipt_path).extract_receipt_data
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
# When output_schema is present, content is already parsed
result = response.message.content
assert_equal result["merchant"]["name"], "Corner Mart"
assert_equal result["total"]["amount"], 14.83
assert_equal result["items"].size, 4
result["items"].each do |item|
assert item.key?("name")
assert item.key?("quantity")
assert item.key?("price")
end
assert_equal result["items"][0], { "name"=>"Milk", "quantity"=>1, "price"=>3.49 }
assert_equal result["items"][1], { "name"=>"Bread", "quantity"=>1, "price"=>2.29 }
assert_equal result["items"][2], { "name"=>"Apples", "quantity"=>1, "price"=>5.1 }
assert_equal result["items"][3], { "name"=>"Eggs", "quantity"=>1, "price"=>2.99 }
# Generate documentation example
doc_example_output(response)
end
end
test "handles base64 encoded images with sales chart" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_base64_sales_chart") do
# Use the sales chart image
chart_path = Rails.root.join("..", "..", "test", "fixtures", "images", "sales_chart.png")
prompt = OpenRouterIntegrationAgent.with(image_path: chart_path).analyze_image
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
assert_includes response.message.content, "(Q1, Q2, Q3, Q4), with varying heights indicating different sales amounts"
# Generate documentation example
doc_example_output(response)
end
end
test "processes PDF document from local file" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_pdf_local") do
# Use the sample resume PDF
pdf_path = Rails.root.join("..", "..", "test", "fixtures", "files", "sample_resume.pdf")
# Read and encode the PDF as base64 - OpenRouter accepts PDFs as image_url with data URL
pdf_data = Base64.strict_encode64(File.read(pdf_path))
prompt = OpenRouterIntegrationAgent.with(
pdf_data: pdf_data,
prompt_text: "Extract information from this document and return as JSON",
output_schema: :resume_schema
).analyze_pdf
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
assert response.message.content.present?
# When output_schema is present, content is already parsed
result = response.message.content
assert_equal result["name"], "John Doe"
assert_equal result["email"], "john.doe@example.com"
assert_equal result["phone"], "(555) 123-4567"
assert_equal result["education"].first, { "degree"=>"BS Computer Science", "institution"=>"Stanford University", "year"=>2020 }
assert_equal result["experience"].first, { "job_title"=>"Senior Software Engineer", "company"=>"TechCorp", "duration"=>"2020-2024" }
# Generate documentation example
doc_example_output(response)
end
end
# endregion pdf_processing_local
test "processes PDF from remote URL of resume no plugins" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_pdf_remote_no_plugin") do
pdf_url = "https://docs.activeagents.ai/sample_resume.pdf"
prompt = OpenRouterIntegrationAgent.with(
pdf_url: pdf_url,
prompt_text: "Analyze the PDF",
output_schema: :resume_schema,
skip_plugin: true
).analyze_pdf
# Remote URLs are not supported without a PDF engine plugin
# OpenAI: Inputs by file URL are not supported for chat completions. Use the ResponsesAPI for this option.
# https://platform.openai.com/docs/guides/pdf-files#file-urls
# Accept either the OpenAI error directly or our wrapped error
# Suppress ruby-openai gem's error output to STDERR
error = assert_raises(ActiveAgent::GenerationProvider::Base::GenerationProviderError, OpenAI::Error) do
prompt.generate_now
end
# Check the error message regardless of which error type was raised
error_message = error.message
assert_match(/Missing required parameter.*file_id/, error_message)
assert_match(/Provider returned error|invalid_request_error/, error_message)
end
end
# region pdf_native_support
test "processes PDF with native model support" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_pdf_native") do
# Test with a model that might have native PDF support
# Using the native engine (charged as input tokens)
pdf_path = Rails.root.join("..", "..", "test", "fixtures", "files", "sample_resume.pdf")
pdf_data = Base64.strict_encode64(File.read(pdf_path))
prompt = OpenRouterIntegrationAgent.with(
pdf_data: pdf_data,
prompt_text: "Analyze this PDF document",
pdf_engine: "native" # Use native engine (charged as input tokens)
).analyze_pdf
# First verify the prompt has the plugins in options
assert prompt.options[:plugins].present?, "Plugins should be present in prompt options"
assert prompt.options[:fallback_models].present?, "Fallback models should be present in prompt options"
assert_equal "file-parser", prompt.options[:plugins][0][:id]
assert_equal "native", prompt.options[:plugins][0][:pdf][:engine]
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
assert response.message.content.present?
assert_includes response.message.content, "John Doe"
# Generate documentation example
doc_example_output(response)
end
end
# endregion pdf_native_support
test "processes PDF without any plugin for models with built-in support" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_pdf_no_plugin") do
# Test without any plugin - for models that have built-in PDF support
pdf_url = "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
prompt = OpenRouterIntegrationAgent.with(
pdf_url: pdf_url,
prompt_text: "Analyze this PDF document",
skip_plugin: true # Don't use any plugin
).analyze_pdf
# Verify no plugins are included when skip_plugin is true
assert_empty prompt.options[:plugins], "Should not have plugins when skip_plugin is true"
response = prompt.generate_now
raw_response = response.raw_response
assert_equal "Google", raw_response["provider"]
assert_not_nil response
assert_not_nil response.message
assert response.message.content.present?
# Generate documentation example
doc_example_output(response)
end
end
test "processes scanned PDF with OCR engine" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_pdf_ocr") do
# Test with the mistral-ocr engine for scanned documents
# Using a simple PDF that should be processable
pdf_url = "https://docs.activeagents.ai/sample_resume.pdf"
prompt = OpenRouterIntegrationAgent.with(
pdf_url: pdf_url,
prompt_text: "Extract text from this PDF.",
output_schema: :resume_schema,
pdf_engine: "mistral-ocr" # OCR engine for text extraction
).analyze_pdf
# Verify OCR engine is specified
assert prompt.options[:plugins].present?, "Should have plugins for OCR"
assert_equal "mistral-ocr", prompt.options[:plugins][0][:pdf][:engine]
response = prompt.generate_now
# MUST return valid JSON - no fallback allowed
raw_response = response.raw_response
# When output_schema is present, content is already parsed
result = response.message.content
assert_equal result["name"], "John Doe"
assert_equal result["email"], "john.doe@example.com"
assert_equal result["phone"], "(555) 123-4567"
assert_equal result["education"], [ { "degree"=>"BS Computer Science", "institution"=>"Stanford University", "year"=>2020 } ]
assert_equal result["experience"], [ { "job_title"=>"Senior Software Engineer", "company"=>"TechCorp", "duration"=>"2020-2024" } ]
# Generate documentation example
doc_example_output(response)
end
end
test "uses fallback models when primary fails" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_fallback_models") do
prompt = OpenRouterIntegrationAgent.test_fallback
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
# Check metadata for fallback usage
if response.respond_to?(:metadata) && response.metadata
# Should use one of the fallback models, not the primary
possible_models = [ "openai/gpt-3.5-turbo-0301", "openai/gpt-3.5-turbo", "openai/gpt-4o-mini" ]
assert possible_models.include?(response.metadata[:model_used])
assert response.metadata[:provider].present?
end
# The response should still work (2+2=4)
assert response.message.content.include?("4")
# Generate documentation example
doc_example_output(response)
end
end
test "applies transforms for long content" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_transforms") do
# Generate a very long text
long_text = "Lorem ipsum dolor sit amet. " * 1000
prompt = OpenRouterIntegrationAgent.with(text: long_text).process_long_text
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
assert response.message.content.present?
# The summary should be much shorter than the original
assert response.message.content.length < long_text.length / 10
# Generate documentation example
doc_example_output(response)
end
end
test "tracks usage and costs" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_cost_tracking") do
prompt = OpenRouterIntegrationAgent.with(message: "Hello").prompt_context
response = prompt.generate_now
assert_not_nil response
# Check for usage information
if response.respond_to?(:usage) && response.usage
assert response.usage["prompt_tokens"].is_a?(Integer)
assert response.usage["completion_tokens"].is_a?(Integer)
assert response.usage["total_tokens"].is_a?(Integer)
end
# Check for metadata with model information from OpenRouter
if response.respond_to?(:metadata) && response.metadata
assert response.metadata[:model_used].present?
assert response.metadata[:provider].present?
# Verify we're using the expected model (gpt-4o-mini)
assert_equal "openai/gpt-4o-mini", response.metadata[:model_used]
end
# Generate documentation example
doc_example_output(response)
end
end
test "includes OpenRouter headers in requests" do
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o",
"app_name" => "TestApp",
"site_url" => "https://test.example.com"
)
# Get the headers that would be sent
headers = provider.send(:openrouter_headers)
assert_equal "https://test.example.com", headers["HTTP-Referer"]
assert_equal "TestApp", headers["X-Title"]
end
test "builds provider preferences correctly" do
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o",
"enable_fallbacks" => true,
"provider" => {
"order" => [ "OpenAI", "Anthropic" ],
"require_parameters" => true,
"data_collection" => "deny"
}
)
prefs = provider.send(:build_provider_preferences)
assert_equal [ "OpenAI", "Anthropic" ], prefs[:order]
assert_equal true, prefs[:require_parameters]
assert_equal true, prefs[:allow_fallbacks]
assert_equal "deny", prefs[:data_collection]
end
test "configures data collection policies" do
# Test deny all data collection
provider_deny = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o",
"data_collection" => "deny"
)
prefs_deny = provider_deny.send(:build_provider_preferences)
assert_equal "deny", prefs_deny[:data_collection]
# Test allow all data collection (default)
provider_allow = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o"
)
prefs_allow = provider_allow.send(:build_provider_preferences)
assert_equal "allow", prefs_allow[:data_collection]
# Test selective provider data collection
provider_selective = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o",
"data_collection" => [ "OpenAI", "Google" ]
)
prefs_selective = provider_selective.send(:build_provider_preferences)
assert_equal [ "OpenAI", "Google" ], prefs_selective[:data_collection]
end
test "handles multimodal content correctly" do
# Create a message with multimodal content
message = ActiveAgent::ActionPrompt::Message.new(
content: [
{ type: "text", text: "What's in this image?" },
{ type: "image_url", image_url: { url: "https://example.com/image.jpg" } }
],
role: :user
)
prompt = ActiveAgent::ActionPrompt::Prompt.new(
messages: [ message ]
)
assert prompt.multimodal?
end
test "converts file type to image_url for OpenRouter PDF support" do
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o"
)
# Test file type conversion
file_item = {
type: "file",
file: {
file_data: "data:application/pdf;base64,JVBERi0xLj..."
}
}
formatted = provider.send(:format_content_item, file_item)
assert_equal "image_url", formatted[:type]
assert_equal "data:application/pdf;base64,JVBERi0xLj...", formatted[:image_url][:url]
end
test "respects configuration hierarchy for site_url" do
# Test with explicit site_url config
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o",
"site_url" => "https://configured.example.com"
)
assert_equal "https://configured.example.com", provider.instance_variable_get(:@site_url)
# Test with default_url_options in config
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o",
"default_url_options" => {
"host" => "fromconfig.example.com"
}
)
assert_equal "fromconfig.example.com", provider.instance_variable_get(:@site_url)
end
test "handles rate limit information in metadata" do
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o"
)
# Create a mock response
prompt = ActiveAgent::ActionPrompt::Prompt.new(message: "test")
response = ActiveAgent::GenerationProvider::Response.new(prompt: prompt)
headers = {
"x-provider" => "OpenAI",
"x-model" => "gpt-4o",
"x-ratelimit-requests-limit" => "100",
"x-ratelimit-requests-remaining" => "99",
"x-ratelimit-tokens-limit" => "10000",
"x-ratelimit-tokens-remaining" => "9500"
}
provider.send(:add_openrouter_metadata, response, headers)
assert_equal "100", response.metadata[:ratelimit][:requests_limit]
assert_equal "99", response.metadata[:ratelimit][:requests_remaining]
assert_equal "10000", response.metadata[:ratelimit][:tokens_limit]
assert_equal "9500", response.metadata[:ratelimit][:tokens_remaining]
end
test "includes plugins parameter when passed in options" do
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o"
)
# Create a prompt with plugins option
prompt = ActiveAgent::ActionPrompt::Prompt.new(
message: "test",
options: {
plugins: [
{
id: "file-parser",
pdf: {
engine: "pdf-text"
}
}
]
}
)
# Set the prompt on the provider
provider.instance_variable_set(:@prompt, prompt)
# Build parameters and verify plugins are included
parameters = provider.send(:build_openrouter_parameters)
assert_not_nil parameters[:plugins]
assert_equal 1, parameters[:plugins].size
assert_equal "file-parser", parameters[:plugins][0][:id]
assert_equal "pdf-text", parameters[:plugins][0][:pdf][:engine]
end
endUsage Tracking Example
Provider Preferences
Configure provider preferences for routing and data collection:
require "test_helper"
require "base64"
require "active_agent/action_prompt/message"
class OpenRouterIntegrationTest < ActiveSupport::TestCase
setup do
@agent = OpenRouterIntegrationAgent.new
end
test "analyzes image with structured output schema" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_image_analysis_structured") do
# Use the sales chart image URL for structured analysis
image_url = "https://raw.githubusercontent.com/activeagents/activeagent/refs/heads/main/test/fixtures/images/sales_chart.png"
prompt = OpenRouterIntegrationAgent.with(image_url: image_url).analyze_image
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
# When output_schema is present, content is already parsed
result = response.message.content
# Verify the structure matches our schema
assert result.key?("description")
assert result.key?("objects")
assert result.key?("scene_type")
assert result.key?("primary_colors")
assert result["objects"].is_a?(Array)
assert [ "indoor", "outdoor", "abstract", "document", "photo", "illustration" ].include?(result["scene_type"])
end
end
test "analyzes remote image URL without structured output" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_remote_image_basic") do
# Use a landscape image URL for basic analysis
image_url = "https://picsum.photos/400/300"
# For now, just use analyze_image without the structured output schema
# We'll get a natural language description instead of JSON
prompt = OpenRouterIntegrationAgent.with(image_url: image_url).analyze_image
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
assert response.message.content.is_a?(String)
assert response.message.content.length > 10
# Since analyze_image uses structured output, we'll get JSON
# Just verify we got a response
# In the future, we could add a simple_analyze action without schema
# Generate documentation example
doc_example_output(response)
end
end
test "extracts receipt data with structured output from local file" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_receipt_extraction_local") do
# Use the test receipt image - file exists, no conditional needed
receipt_path = Rails.root.join("..", "..", "test", "fixtures", "images", "test_receipt.png")
prompt = OpenRouterIntegrationAgent.with(image_path: receipt_path).extract_receipt_data
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
# When output_schema is present, content is already parsed
result = response.message.content
assert_equal result["merchant"]["name"], "Corner Mart"
assert_equal result["total"]["amount"], 14.83
assert_equal result["items"].size, 4
result["items"].each do |item|
assert item.key?("name")
assert item.key?("quantity")
assert item.key?("price")
end
assert_equal result["items"][0], { "name"=>"Milk", "quantity"=>1, "price"=>3.49 }
assert_equal result["items"][1], { "name"=>"Bread", "quantity"=>1, "price"=>2.29 }
assert_equal result["items"][2], { "name"=>"Apples", "quantity"=>1, "price"=>5.1 }
assert_equal result["items"][3], { "name"=>"Eggs", "quantity"=>1, "price"=>2.99 }
# Generate documentation example
doc_example_output(response)
end
end
test "handles base64 encoded images with sales chart" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_base64_sales_chart") do
# Use the sales chart image
chart_path = Rails.root.join("..", "..", "test", "fixtures", "images", "sales_chart.png")
prompt = OpenRouterIntegrationAgent.with(image_path: chart_path).analyze_image
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
assert_includes response.message.content, "(Q1, Q2, Q3, Q4), with varying heights indicating different sales amounts"
# Generate documentation example
doc_example_output(response)
end
end
test "processes PDF document from local file" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_pdf_local") do
# Use the sample resume PDF
pdf_path = Rails.root.join("..", "..", "test", "fixtures", "files", "sample_resume.pdf")
# Read and encode the PDF as base64 - OpenRouter accepts PDFs as image_url with data URL
pdf_data = Base64.strict_encode64(File.read(pdf_path))
prompt = OpenRouterIntegrationAgent.with(
pdf_data: pdf_data,
prompt_text: "Extract information from this document and return as JSON",
output_schema: :resume_schema
).analyze_pdf
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
assert response.message.content.present?
# When output_schema is present, content is already parsed
result = response.message.content
assert_equal result["name"], "John Doe"
assert_equal result["email"], "john.doe@example.com"
assert_equal result["phone"], "(555) 123-4567"
assert_equal result["education"].first, { "degree"=>"BS Computer Science", "institution"=>"Stanford University", "year"=>2020 }
assert_equal result["experience"].first, { "job_title"=>"Senior Software Engineer", "company"=>"TechCorp", "duration"=>"2020-2024" }
# Generate documentation example
doc_example_output(response)
end
end
# endregion pdf_processing_local
test "processes PDF from remote URL of resume no plugins" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_pdf_remote_no_plugin") do
pdf_url = "https://docs.activeagents.ai/sample_resume.pdf"
prompt = OpenRouterIntegrationAgent.with(
pdf_url: pdf_url,
prompt_text: "Analyze the PDF",
output_schema: :resume_schema,
skip_plugin: true
).analyze_pdf
# Remote URLs are not supported without a PDF engine plugin
# OpenAI: Inputs by file URL are not supported for chat completions. Use the ResponsesAPI for this option.
# https://platform.openai.com/docs/guides/pdf-files#file-urls
# Accept either the OpenAI error directly or our wrapped error
# Suppress ruby-openai gem's error output to STDERR
error = assert_raises(ActiveAgent::GenerationProvider::Base::GenerationProviderError, OpenAI::Error) do
prompt.generate_now
end
# Check the error message regardless of which error type was raised
error_message = error.message
assert_match(/Missing required parameter.*file_id/, error_message)
assert_match(/Provider returned error|invalid_request_error/, error_message)
end
end
# region pdf_native_support
test "processes PDF with native model support" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_pdf_native") do
# Test with a model that might have native PDF support
# Using the native engine (charged as input tokens)
pdf_path = Rails.root.join("..", "..", "test", "fixtures", "files", "sample_resume.pdf")
pdf_data = Base64.strict_encode64(File.read(pdf_path))
prompt = OpenRouterIntegrationAgent.with(
pdf_data: pdf_data,
prompt_text: "Analyze this PDF document",
pdf_engine: "native" # Use native engine (charged as input tokens)
).analyze_pdf
# First verify the prompt has the plugins in options
assert prompt.options[:plugins].present?, "Plugins should be present in prompt options"
assert prompt.options[:fallback_models].present?, "Fallback models should be present in prompt options"
assert_equal "file-parser", prompt.options[:plugins][0][:id]
assert_equal "native", prompt.options[:plugins][0][:pdf][:engine]
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
assert response.message.content.present?
assert_includes response.message.content, "John Doe"
# Generate documentation example
doc_example_output(response)
end
end
# endregion pdf_native_support
test "processes PDF without any plugin for models with built-in support" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_pdf_no_plugin") do
# Test without any plugin - for models that have built-in PDF support
pdf_url = "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
prompt = OpenRouterIntegrationAgent.with(
pdf_url: pdf_url,
prompt_text: "Analyze this PDF document",
skip_plugin: true # Don't use any plugin
).analyze_pdf
# Verify no plugins are included when skip_plugin is true
assert_empty prompt.options[:plugins], "Should not have plugins when skip_plugin is true"
response = prompt.generate_now
raw_response = response.raw_response
assert_equal "Google", raw_response["provider"]
assert_not_nil response
assert_not_nil response.message
assert response.message.content.present?
# Generate documentation example
doc_example_output(response)
end
end
test "processes scanned PDF with OCR engine" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_pdf_ocr") do
# Test with the mistral-ocr engine for scanned documents
# Using a simple PDF that should be processable
pdf_url = "https://docs.activeagents.ai/sample_resume.pdf"
prompt = OpenRouterIntegrationAgent.with(
pdf_url: pdf_url,
prompt_text: "Extract text from this PDF.",
output_schema: :resume_schema,
pdf_engine: "mistral-ocr" # OCR engine for text extraction
).analyze_pdf
# Verify OCR engine is specified
assert prompt.options[:plugins].present?, "Should have plugins for OCR"
assert_equal "mistral-ocr", prompt.options[:plugins][0][:pdf][:engine]
response = prompt.generate_now
# MUST return valid JSON - no fallback allowed
raw_response = response.raw_response
# When output_schema is present, content is already parsed
result = response.message.content
assert_equal result["name"], "John Doe"
assert_equal result["email"], "john.doe@example.com"
assert_equal result["phone"], "(555) 123-4567"
assert_equal result["education"], [ { "degree"=>"BS Computer Science", "institution"=>"Stanford University", "year"=>2020 } ]
assert_equal result["experience"], [ { "job_title"=>"Senior Software Engineer", "company"=>"TechCorp", "duration"=>"2020-2024" } ]
# Generate documentation example
doc_example_output(response)
end
end
test "uses fallback models when primary fails" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_fallback_models") do
prompt = OpenRouterIntegrationAgent.test_fallback
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
# Check metadata for fallback usage
if response.respond_to?(:metadata) && response.metadata
# Should use one of the fallback models, not the primary
possible_models = [ "openai/gpt-3.5-turbo-0301", "openai/gpt-3.5-turbo", "openai/gpt-4o-mini" ]
assert possible_models.include?(response.metadata[:model_used])
assert response.metadata[:provider].present?
end
# The response should still work (2+2=4)
assert response.message.content.include?("4")
# Generate documentation example
doc_example_output(response)
end
end
test "applies transforms for long content" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_transforms") do
# Generate a very long text
long_text = "Lorem ipsum dolor sit amet. " * 1000
prompt = OpenRouterIntegrationAgent.with(text: long_text).process_long_text
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
assert response.message.content.present?
# The summary should be much shorter than the original
assert response.message.content.length < long_text.length / 10
# Generate documentation example
doc_example_output(response)
end
end
test "tracks usage and costs" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_cost_tracking") do
prompt = OpenRouterIntegrationAgent.with(message: "Hello").prompt_context
response = prompt.generate_now
assert_not_nil response
# Check for usage information
if response.respond_to?(:usage) && response.usage
assert response.usage["prompt_tokens"].is_a?(Integer)
assert response.usage["completion_tokens"].is_a?(Integer)
assert response.usage["total_tokens"].is_a?(Integer)
end
# Check for metadata with model information from OpenRouter
if response.respond_to?(:metadata) && response.metadata
assert response.metadata[:model_used].present?
assert response.metadata[:provider].present?
# Verify we're using the expected model (gpt-4o-mini)
assert_equal "openai/gpt-4o-mini", response.metadata[:model_used]
end
# Generate documentation example
doc_example_output(response)
end
end
test "includes OpenRouter headers in requests" do
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o",
"app_name" => "TestApp",
"site_url" => "https://test.example.com"
)
# Get the headers that would be sent
headers = provider.send(:openrouter_headers)
assert_equal "https://test.example.com", headers["HTTP-Referer"]
assert_equal "TestApp", headers["X-Title"]
end
test "builds provider preferences correctly" do
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o",
"enable_fallbacks" => true,
"provider" => {
"order" => [ "OpenAI", "Anthropic" ],
"require_parameters" => true,
"data_collection" => "deny"
}
)
prefs = provider.send(:build_provider_preferences)
assert_equal [ "OpenAI", "Anthropic" ], prefs[:order]
assert_equal true, prefs[:require_parameters]
assert_equal true, prefs[:allow_fallbacks]
assert_equal "deny", prefs[:data_collection]
end
test "configures data collection policies" do
# Test deny all data collection
provider_deny = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o",
"data_collection" => "deny"
)
prefs_deny = provider_deny.send(:build_provider_preferences)
assert_equal "deny", prefs_deny[:data_collection]
# Test allow all data collection (default)
provider_allow = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o"
)
prefs_allow = provider_allow.send(:build_provider_preferences)
assert_equal "allow", prefs_allow[:data_collection]
# Test selective provider data collection
provider_selective = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o",
"data_collection" => [ "OpenAI", "Google" ]
)
prefs_selective = provider_selective.send(:build_provider_preferences)
assert_equal [ "OpenAI", "Google" ], prefs_selective[:data_collection]
end
test "handles multimodal content correctly" do
# Create a message with multimodal content
message = ActiveAgent::ActionPrompt::Message.new(
content: [
{ type: "text", text: "What's in this image?" },
{ type: "image_url", image_url: { url: "https://example.com/image.jpg" } }
],
role: :user
)
prompt = ActiveAgent::ActionPrompt::Prompt.new(
messages: [ message ]
)
assert prompt.multimodal?
end
test "converts file type to image_url for OpenRouter PDF support" do
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o"
)
# Test file type conversion
file_item = {
type: "file",
file: {
file_data: "data:application/pdf;base64,JVBERi0xLj..."
}
}
formatted = provider.send(:format_content_item, file_item)
assert_equal "image_url", formatted[:type]
assert_equal "data:application/pdf;base64,JVBERi0xLj...", formatted[:image_url][:url]
end
test "respects configuration hierarchy for site_url" do
# Test with explicit site_url config
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o",
"site_url" => "https://configured.example.com"
)
assert_equal "https://configured.example.com", provider.instance_variable_get(:@site_url)
# Test with default_url_options in config
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o",
"default_url_options" => {
"host" => "fromconfig.example.com"
}
)
assert_equal "fromconfig.example.com", provider.instance_variable_get(:@site_url)
end
test "handles rate limit information in metadata" do
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o"
)
# Create a mock response
prompt = ActiveAgent::ActionPrompt::Prompt.new(message: "test")
response = ActiveAgent::GenerationProvider::Response.new(prompt: prompt)
headers = {
"x-provider" => "OpenAI",
"x-model" => "gpt-4o",
"x-ratelimit-requests-limit" => "100",
"x-ratelimit-requests-remaining" => "99",
"x-ratelimit-tokens-limit" => "10000",
"x-ratelimit-tokens-remaining" => "9500"
}
provider.send(:add_openrouter_metadata, response, headers)
assert_equal "100", response.metadata[:ratelimit][:requests_limit]
assert_equal "99", response.metadata[:ratelimit][:requests_remaining]
assert_equal "10000", response.metadata[:ratelimit][:tokens_limit]
assert_equal "9500", response.metadata[:ratelimit][:tokens_remaining]
end
test "includes plugins parameter when passed in options" do
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o"
)
# Create a prompt with plugins option
prompt = ActiveAgent::ActionPrompt::Prompt.new(
message: "test",
options: {
plugins: [
{
id: "file-parser",
pdf: {
engine: "pdf-text"
}
}
]
}
)
# Set the prompt on the provider
provider.instance_variable_set(:@prompt, prompt)
# Build parameters and verify plugins are included
parameters = provider.send(:build_openrouter_parameters)
assert_not_nil parameters[:plugins]
assert_equal 1, parameters[:plugins].size
assert_equal "file-parser", parameters[:plugins][0][:id]
assert_equal "pdf-text", parameters[:plugins][0][:pdf][:engine]
end
endData Collection Policies
OpenRouter supports configuring data collection policies to control which providers can collect and use your data for training. According to the OpenRouter documentation, you can configure this in three ways:
- Allow all providers (default): All providers can collect data
- Deny all providers: No providers can collect data
- Selective providers: Only specified providers can collect data
Configuration Examples
require "test_helper"
require "base64"
require "active_agent/action_prompt/message"
class OpenRouterIntegrationTest < ActiveSupport::TestCase
setup do
@agent = OpenRouterIntegrationAgent.new
end
test "analyzes image with structured output schema" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_image_analysis_structured") do
# Use the sales chart image URL for structured analysis
image_url = "https://raw.githubusercontent.com/activeagents/activeagent/refs/heads/main/test/fixtures/images/sales_chart.png"
prompt = OpenRouterIntegrationAgent.with(image_url: image_url).analyze_image
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
# When output_schema is present, content is already parsed
result = response.message.content
# Verify the structure matches our schema
assert result.key?("description")
assert result.key?("objects")
assert result.key?("scene_type")
assert result.key?("primary_colors")
assert result["objects"].is_a?(Array)
assert [ "indoor", "outdoor", "abstract", "document", "photo", "illustration" ].include?(result["scene_type"])
end
end
test "analyzes remote image URL without structured output" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_remote_image_basic") do
# Use a landscape image URL for basic analysis
image_url = "https://picsum.photos/400/300"
# For now, just use analyze_image without the structured output schema
# We'll get a natural language description instead of JSON
prompt = OpenRouterIntegrationAgent.with(image_url: image_url).analyze_image
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
assert response.message.content.is_a?(String)
assert response.message.content.length > 10
# Since analyze_image uses structured output, we'll get JSON
# Just verify we got a response
# In the future, we could add a simple_analyze action without schema
# Generate documentation example
doc_example_output(response)
end
end
test "extracts receipt data with structured output from local file" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_receipt_extraction_local") do
# Use the test receipt image - file exists, no conditional needed
receipt_path = Rails.root.join("..", "..", "test", "fixtures", "images", "test_receipt.png")
prompt = OpenRouterIntegrationAgent.with(image_path: receipt_path).extract_receipt_data
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
# When output_schema is present, content is already parsed
result = response.message.content
assert_equal result["merchant"]["name"], "Corner Mart"
assert_equal result["total"]["amount"], 14.83
assert_equal result["items"].size, 4
result["items"].each do |item|
assert item.key?("name")
assert item.key?("quantity")
assert item.key?("price")
end
assert_equal result["items"][0], { "name"=>"Milk", "quantity"=>1, "price"=>3.49 }
assert_equal result["items"][1], { "name"=>"Bread", "quantity"=>1, "price"=>2.29 }
assert_equal result["items"][2], { "name"=>"Apples", "quantity"=>1, "price"=>5.1 }
assert_equal result["items"][3], { "name"=>"Eggs", "quantity"=>1, "price"=>2.99 }
# Generate documentation example
doc_example_output(response)
end
end
test "handles base64 encoded images with sales chart" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_base64_sales_chart") do
# Use the sales chart image
chart_path = Rails.root.join("..", "..", "test", "fixtures", "images", "sales_chart.png")
prompt = OpenRouterIntegrationAgent.with(image_path: chart_path).analyze_image
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
assert_includes response.message.content, "(Q1, Q2, Q3, Q4), with varying heights indicating different sales amounts"
# Generate documentation example
doc_example_output(response)
end
end
test "processes PDF document from local file" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_pdf_local") do
# Use the sample resume PDF
pdf_path = Rails.root.join("..", "..", "test", "fixtures", "files", "sample_resume.pdf")
# Read and encode the PDF as base64 - OpenRouter accepts PDFs as image_url with data URL
pdf_data = Base64.strict_encode64(File.read(pdf_path))
prompt = OpenRouterIntegrationAgent.with(
pdf_data: pdf_data,
prompt_text: "Extract information from this document and return as JSON",
output_schema: :resume_schema
).analyze_pdf
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
assert response.message.content.present?
# When output_schema is present, content is already parsed
result = response.message.content
assert_equal result["name"], "John Doe"
assert_equal result["email"], "john.doe@example.com"
assert_equal result["phone"], "(555) 123-4567"
assert_equal result["education"].first, { "degree"=>"BS Computer Science", "institution"=>"Stanford University", "year"=>2020 }
assert_equal result["experience"].first, { "job_title"=>"Senior Software Engineer", "company"=>"TechCorp", "duration"=>"2020-2024" }
# Generate documentation example
doc_example_output(response)
end
end
# endregion pdf_processing_local
test "processes PDF from remote URL of resume no plugins" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_pdf_remote_no_plugin") do
pdf_url = "https://docs.activeagents.ai/sample_resume.pdf"
prompt = OpenRouterIntegrationAgent.with(
pdf_url: pdf_url,
prompt_text: "Analyze the PDF",
output_schema: :resume_schema,
skip_plugin: true
).analyze_pdf
# Remote URLs are not supported without a PDF engine plugin
# OpenAI: Inputs by file URL are not supported for chat completions. Use the ResponsesAPI for this option.
# https://platform.openai.com/docs/guides/pdf-files#file-urls
# Accept either the OpenAI error directly or our wrapped error
# Suppress ruby-openai gem's error output to STDERR
error = assert_raises(ActiveAgent::GenerationProvider::Base::GenerationProviderError, OpenAI::Error) do
prompt.generate_now
end
# Check the error message regardless of which error type was raised
error_message = error.message
assert_match(/Missing required parameter.*file_id/, error_message)
assert_match(/Provider returned error|invalid_request_error/, error_message)
end
end
# region pdf_native_support
test "processes PDF with native model support" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_pdf_native") do
# Test with a model that might have native PDF support
# Using the native engine (charged as input tokens)
pdf_path = Rails.root.join("..", "..", "test", "fixtures", "files", "sample_resume.pdf")
pdf_data = Base64.strict_encode64(File.read(pdf_path))
prompt = OpenRouterIntegrationAgent.with(
pdf_data: pdf_data,
prompt_text: "Analyze this PDF document",
pdf_engine: "native" # Use native engine (charged as input tokens)
).analyze_pdf
# First verify the prompt has the plugins in options
assert prompt.options[:plugins].present?, "Plugins should be present in prompt options"
assert prompt.options[:fallback_models].present?, "Fallback models should be present in prompt options"
assert_equal "file-parser", prompt.options[:plugins][0][:id]
assert_equal "native", prompt.options[:plugins][0][:pdf][:engine]
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
assert response.message.content.present?
assert_includes response.message.content, "John Doe"
# Generate documentation example
doc_example_output(response)
end
end
# endregion pdf_native_support
test "processes PDF without any plugin for models with built-in support" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_pdf_no_plugin") do
# Test without any plugin - for models that have built-in PDF support
pdf_url = "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
prompt = OpenRouterIntegrationAgent.with(
pdf_url: pdf_url,
prompt_text: "Analyze this PDF document",
skip_plugin: true # Don't use any plugin
).analyze_pdf
# Verify no plugins are included when skip_plugin is true
assert_empty prompt.options[:plugins], "Should not have plugins when skip_plugin is true"
response = prompt.generate_now
raw_response = response.raw_response
assert_equal "Google", raw_response["provider"]
assert_not_nil response
assert_not_nil response.message
assert response.message.content.present?
# Generate documentation example
doc_example_output(response)
end
end
test "processes scanned PDF with OCR engine" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_pdf_ocr") do
# Test with the mistral-ocr engine for scanned documents
# Using a simple PDF that should be processable
pdf_url = "https://docs.activeagents.ai/sample_resume.pdf"
prompt = OpenRouterIntegrationAgent.with(
pdf_url: pdf_url,
prompt_text: "Extract text from this PDF.",
output_schema: :resume_schema,
pdf_engine: "mistral-ocr" # OCR engine for text extraction
).analyze_pdf
# Verify OCR engine is specified
assert prompt.options[:plugins].present?, "Should have plugins for OCR"
assert_equal "mistral-ocr", prompt.options[:plugins][0][:pdf][:engine]
response = prompt.generate_now
# MUST return valid JSON - no fallback allowed
raw_response = response.raw_response
# When output_schema is present, content is already parsed
result = response.message.content
assert_equal result["name"], "John Doe"
assert_equal result["email"], "john.doe@example.com"
assert_equal result["phone"], "(555) 123-4567"
assert_equal result["education"], [ { "degree"=>"BS Computer Science", "institution"=>"Stanford University", "year"=>2020 } ]
assert_equal result["experience"], [ { "job_title"=>"Senior Software Engineer", "company"=>"TechCorp", "duration"=>"2020-2024" } ]
# Generate documentation example
doc_example_output(response)
end
end
test "uses fallback models when primary fails" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_fallback_models") do
prompt = OpenRouterIntegrationAgent.test_fallback
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
# Check metadata for fallback usage
if response.respond_to?(:metadata) && response.metadata
# Should use one of the fallback models, not the primary
possible_models = [ "openai/gpt-3.5-turbo-0301", "openai/gpt-3.5-turbo", "openai/gpt-4o-mini" ]
assert possible_models.include?(response.metadata[:model_used])
assert response.metadata[:provider].present?
end
# The response should still work (2+2=4)
assert response.message.content.include?("4")
# Generate documentation example
doc_example_output(response)
end
end
test "applies transforms for long content" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_transforms") do
# Generate a very long text
long_text = "Lorem ipsum dolor sit amet. " * 1000
prompt = OpenRouterIntegrationAgent.with(text: long_text).process_long_text
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
assert response.message.content.present?
# The summary should be much shorter than the original
assert response.message.content.length < long_text.length / 10
# Generate documentation example
doc_example_output(response)
end
end
test "tracks usage and costs" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_cost_tracking") do
prompt = OpenRouterIntegrationAgent.with(message: "Hello").prompt_context
response = prompt.generate_now
assert_not_nil response
# Check for usage information
if response.respond_to?(:usage) && response.usage
assert response.usage["prompt_tokens"].is_a?(Integer)
assert response.usage["completion_tokens"].is_a?(Integer)
assert response.usage["total_tokens"].is_a?(Integer)
end
# Check for metadata with model information from OpenRouter
if response.respond_to?(:metadata) && response.metadata
assert response.metadata[:model_used].present?
assert response.metadata[:provider].present?
# Verify we're using the expected model (gpt-4o-mini)
assert_equal "openai/gpt-4o-mini", response.metadata[:model_used]
end
# Generate documentation example
doc_example_output(response)
end
end
test "includes OpenRouter headers in requests" do
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o",
"app_name" => "TestApp",
"site_url" => "https://test.example.com"
)
# Get the headers that would be sent
headers = provider.send(:openrouter_headers)
assert_equal "https://test.example.com", headers["HTTP-Referer"]
assert_equal "TestApp", headers["X-Title"]
end
test "builds provider preferences correctly" do
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o",
"enable_fallbacks" => true,
"provider" => {
"order" => [ "OpenAI", "Anthropic" ],
"require_parameters" => true,
"data_collection" => "deny"
}
)
prefs = provider.send(:build_provider_preferences)
assert_equal [ "OpenAI", "Anthropic" ], prefs[:order]
assert_equal true, prefs[:require_parameters]
assert_equal true, prefs[:allow_fallbacks]
assert_equal "deny", prefs[:data_collection]
end
test "configures data collection policies" do
# Test deny all data collection
provider_deny = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o",
"data_collection" => "deny"
)
prefs_deny = provider_deny.send(:build_provider_preferences)
assert_equal "deny", prefs_deny[:data_collection]
# Test allow all data collection (default)
provider_allow = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o"
)
prefs_allow = provider_allow.send(:build_provider_preferences)
assert_equal "allow", prefs_allow[:data_collection]
# Test selective provider data collection
provider_selective = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o",
"data_collection" => [ "OpenAI", "Google" ]
)
prefs_selective = provider_selective.send(:build_provider_preferences)
assert_equal [ "OpenAI", "Google" ], prefs_selective[:data_collection]
end
test "handles multimodal content correctly" do
# Create a message with multimodal content
message = ActiveAgent::ActionPrompt::Message.new(
content: [
{ type: "text", text: "What's in this image?" },
{ type: "image_url", image_url: { url: "https://example.com/image.jpg" } }
],
role: :user
)
prompt = ActiveAgent::ActionPrompt::Prompt.new(
messages: [ message ]
)
assert prompt.multimodal?
end
test "converts file type to image_url for OpenRouter PDF support" do
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o"
)
# Test file type conversion
file_item = {
type: "file",
file: {
file_data: "data:application/pdf;base64,JVBERi0xLj..."
}
}
formatted = provider.send(:format_content_item, file_item)
assert_equal "image_url", formatted[:type]
assert_equal "data:application/pdf;base64,JVBERi0xLj...", formatted[:image_url][:url]
end
test "respects configuration hierarchy for site_url" do
# Test with explicit site_url config
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o",
"site_url" => "https://configured.example.com"
)
assert_equal "https://configured.example.com", provider.instance_variable_get(:@site_url)
# Test with default_url_options in config
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o",
"default_url_options" => {
"host" => "fromconfig.example.com"
}
)
assert_equal "fromconfig.example.com", provider.instance_variable_get(:@site_url)
end
test "handles rate limit information in metadata" do
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o"
)
# Create a mock response
prompt = ActiveAgent::ActionPrompt::Prompt.new(message: "test")
response = ActiveAgent::GenerationProvider::Response.new(prompt: prompt)
headers = {
"x-provider" => "OpenAI",
"x-model" => "gpt-4o",
"x-ratelimit-requests-limit" => "100",
"x-ratelimit-requests-remaining" => "99",
"x-ratelimit-tokens-limit" => "10000",
"x-ratelimit-tokens-remaining" => "9500"
}
provider.send(:add_openrouter_metadata, response, headers)
assert_equal "100", response.metadata[:ratelimit][:requests_limit]
assert_equal "99", response.metadata[:ratelimit][:requests_remaining]
assert_equal "10000", response.metadata[:ratelimit][:tokens_limit]
assert_equal "9500", response.metadata[:ratelimit][:tokens_remaining]
end
test "includes plugins parameter when passed in options" do
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o"
)
# Create a prompt with plugins option
prompt = ActiveAgent::ActionPrompt::Prompt.new(
message: "test",
options: {
plugins: [
{
id: "file-parser",
pdf: {
engine: "pdf-text"
}
}
]
}
)
# Set the prompt on the provider
provider.instance_variable_set(:@prompt, prompt)
# Build parameters and verify plugins are included
parameters = provider.send(:build_openrouter_parameters)
assert_not_nil parameters[:plugins]
assert_equal 1, parameters[:plugins].size
assert_equal "file-parser", parameters[:plugins][0][:id]
assert_equal "pdf-text", parameters[:plugins][0][:pdf][:engine]
end
endReal-World Example: Privacy-Focused Agent
Here's a complete example of an agent configured to handle sensitive data with strict privacy controls:
generate_with :open_router,
model: "openai/gpt-4o-mini",
data_collection: "deny", # Prevent all providers from collecting data
enable_fallbacks: true,
fallback_models: [ "openai/gpt-3.5-turbo" ]Processing sensitive financial data:
def analyze_financial_data
@data = params[:financial_data]
@analysis_type = params[:analysis_type] || "summary"
prompt(
message: build_financial_message,
instructions: "You are analyzing sensitive financial data. Ensure privacy and confidentiality."
)
endSelective provider data collection for medical records:
def process_medical_records
# Only allow specific trusted providers to collect data
prompt(
message: "Analyze the following medical record: #{params[:record]}",
instructions: "Handle medical data with utmost privacy",
options: {
provider: {
data_collection: [ "OpenAI" ] # Only OpenAI can collect this data
}
}
)
endYou can configure data collection at multiple levels:
# In config/active_agent.yml
development:
open_router:
api_key: <%= Rails.application.credentials.dig(:open_router, :api_key) %>
model: openai/gpt-4o
data_collection: deny # Deny all providers from collecting data
require_parameters: true # Require model providers to support all specified parameters
# Or allow specific providers only
production:
open_router:
api_key: <%= Rails.application.credentials.dig(:open_router, :api_key) %>
model: openai/gpt-4o
data_collection: ["OpenAI", "Google"] # Only these providers can collect data
require_parameters: false # Allow fallback to providers that don't support all parameters
# In your agent configuration
class PrivacyFocusedAgent < ApplicationAgent
generate_with :open_router,
model: "openai/gpt-4o",
data_collection: "deny", # Override for this specific agent
require_parameters: true # Ensure all parameters are supported
endPrivacy Considerations
When handling sensitive data, consider setting data_collection: "deny" to ensure your data is not used for model training. This is especially important for:
- Personal information
- Proprietary business data
- Medical or financial records
- Confidential communications
TIP
The data_collection parameter respects OpenRouter's provider compliance requirements. Providers that don't comply with your data collection policy will be automatically excluded from the routing pool.
Headers and Site Configuration
OpenRouter supports custom headers for tracking and attribution:
require "test_helper"
require "base64"
require "active_agent/action_prompt/message"
class OpenRouterIntegrationTest < ActiveSupport::TestCase
setup do
@agent = OpenRouterIntegrationAgent.new
end
test "analyzes image with structured output schema" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_image_analysis_structured") do
# Use the sales chart image URL for structured analysis
image_url = "https://raw.githubusercontent.com/activeagents/activeagent/refs/heads/main/test/fixtures/images/sales_chart.png"
prompt = OpenRouterIntegrationAgent.with(image_url: image_url).analyze_image
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
# When output_schema is present, content is already parsed
result = response.message.content
# Verify the structure matches our schema
assert result.key?("description")
assert result.key?("objects")
assert result.key?("scene_type")
assert result.key?("primary_colors")
assert result["objects"].is_a?(Array)
assert [ "indoor", "outdoor", "abstract", "document", "photo", "illustration" ].include?(result["scene_type"])
end
end
test "analyzes remote image URL without structured output" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_remote_image_basic") do
# Use a landscape image URL for basic analysis
image_url = "https://picsum.photos/400/300"
# For now, just use analyze_image without the structured output schema
# We'll get a natural language description instead of JSON
prompt = OpenRouterIntegrationAgent.with(image_url: image_url).analyze_image
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
assert response.message.content.is_a?(String)
assert response.message.content.length > 10
# Since analyze_image uses structured output, we'll get JSON
# Just verify we got a response
# In the future, we could add a simple_analyze action without schema
# Generate documentation example
doc_example_output(response)
end
end
test "extracts receipt data with structured output from local file" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_receipt_extraction_local") do
# Use the test receipt image - file exists, no conditional needed
receipt_path = Rails.root.join("..", "..", "test", "fixtures", "images", "test_receipt.png")
prompt = OpenRouterIntegrationAgent.with(image_path: receipt_path).extract_receipt_data
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
# When output_schema is present, content is already parsed
result = response.message.content
assert_equal result["merchant"]["name"], "Corner Mart"
assert_equal result["total"]["amount"], 14.83
assert_equal result["items"].size, 4
result["items"].each do |item|
assert item.key?("name")
assert item.key?("quantity")
assert item.key?("price")
end
assert_equal result["items"][0], { "name"=>"Milk", "quantity"=>1, "price"=>3.49 }
assert_equal result["items"][1], { "name"=>"Bread", "quantity"=>1, "price"=>2.29 }
assert_equal result["items"][2], { "name"=>"Apples", "quantity"=>1, "price"=>5.1 }
assert_equal result["items"][3], { "name"=>"Eggs", "quantity"=>1, "price"=>2.99 }
# Generate documentation example
doc_example_output(response)
end
end
test "handles base64 encoded images with sales chart" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_base64_sales_chart") do
# Use the sales chart image
chart_path = Rails.root.join("..", "..", "test", "fixtures", "images", "sales_chart.png")
prompt = OpenRouterIntegrationAgent.with(image_path: chart_path).analyze_image
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
assert_includes response.message.content, "(Q1, Q2, Q3, Q4), with varying heights indicating different sales amounts"
# Generate documentation example
doc_example_output(response)
end
end
test "processes PDF document from local file" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_pdf_local") do
# Use the sample resume PDF
pdf_path = Rails.root.join("..", "..", "test", "fixtures", "files", "sample_resume.pdf")
# Read and encode the PDF as base64 - OpenRouter accepts PDFs as image_url with data URL
pdf_data = Base64.strict_encode64(File.read(pdf_path))
prompt = OpenRouterIntegrationAgent.with(
pdf_data: pdf_data,
prompt_text: "Extract information from this document and return as JSON",
output_schema: :resume_schema
).analyze_pdf
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
assert response.message.content.present?
# When output_schema is present, content is already parsed
result = response.message.content
assert_equal result["name"], "John Doe"
assert_equal result["email"], "john.doe@example.com"
assert_equal result["phone"], "(555) 123-4567"
assert_equal result["education"].first, { "degree"=>"BS Computer Science", "institution"=>"Stanford University", "year"=>2020 }
assert_equal result["experience"].first, { "job_title"=>"Senior Software Engineer", "company"=>"TechCorp", "duration"=>"2020-2024" }
# Generate documentation example
doc_example_output(response)
end
end
# endregion pdf_processing_local
test "processes PDF from remote URL of resume no plugins" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_pdf_remote_no_plugin") do
pdf_url = "https://docs.activeagents.ai/sample_resume.pdf"
prompt = OpenRouterIntegrationAgent.with(
pdf_url: pdf_url,
prompt_text: "Analyze the PDF",
output_schema: :resume_schema,
skip_plugin: true
).analyze_pdf
# Remote URLs are not supported without a PDF engine plugin
# OpenAI: Inputs by file URL are not supported for chat completions. Use the ResponsesAPI for this option.
# https://platform.openai.com/docs/guides/pdf-files#file-urls
# Accept either the OpenAI error directly or our wrapped error
# Suppress ruby-openai gem's error output to STDERR
error = assert_raises(ActiveAgent::GenerationProvider::Base::GenerationProviderError, OpenAI::Error) do
prompt.generate_now
end
# Check the error message regardless of which error type was raised
error_message = error.message
assert_match(/Missing required parameter.*file_id/, error_message)
assert_match(/Provider returned error|invalid_request_error/, error_message)
end
end
# region pdf_native_support
test "processes PDF with native model support" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_pdf_native") do
# Test with a model that might have native PDF support
# Using the native engine (charged as input tokens)
pdf_path = Rails.root.join("..", "..", "test", "fixtures", "files", "sample_resume.pdf")
pdf_data = Base64.strict_encode64(File.read(pdf_path))
prompt = OpenRouterIntegrationAgent.with(
pdf_data: pdf_data,
prompt_text: "Analyze this PDF document",
pdf_engine: "native" # Use native engine (charged as input tokens)
).analyze_pdf
# First verify the prompt has the plugins in options
assert prompt.options[:plugins].present?, "Plugins should be present in prompt options"
assert prompt.options[:fallback_models].present?, "Fallback models should be present in prompt options"
assert_equal "file-parser", prompt.options[:plugins][0][:id]
assert_equal "native", prompt.options[:plugins][0][:pdf][:engine]
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
assert response.message.content.present?
assert_includes response.message.content, "John Doe"
# Generate documentation example
doc_example_output(response)
end
end
# endregion pdf_native_support
test "processes PDF without any plugin for models with built-in support" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_pdf_no_plugin") do
# Test without any plugin - for models that have built-in PDF support
pdf_url = "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
prompt = OpenRouterIntegrationAgent.with(
pdf_url: pdf_url,
prompt_text: "Analyze this PDF document",
skip_plugin: true # Don't use any plugin
).analyze_pdf
# Verify no plugins are included when skip_plugin is true
assert_empty prompt.options[:plugins], "Should not have plugins when skip_plugin is true"
response = prompt.generate_now
raw_response = response.raw_response
assert_equal "Google", raw_response["provider"]
assert_not_nil response
assert_not_nil response.message
assert response.message.content.present?
# Generate documentation example
doc_example_output(response)
end
end
test "processes scanned PDF with OCR engine" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_pdf_ocr") do
# Test with the mistral-ocr engine for scanned documents
# Using a simple PDF that should be processable
pdf_url = "https://docs.activeagents.ai/sample_resume.pdf"
prompt = OpenRouterIntegrationAgent.with(
pdf_url: pdf_url,
prompt_text: "Extract text from this PDF.",
output_schema: :resume_schema,
pdf_engine: "mistral-ocr" # OCR engine for text extraction
).analyze_pdf
# Verify OCR engine is specified
assert prompt.options[:plugins].present?, "Should have plugins for OCR"
assert_equal "mistral-ocr", prompt.options[:plugins][0][:pdf][:engine]
response = prompt.generate_now
# MUST return valid JSON - no fallback allowed
raw_response = response.raw_response
# When output_schema is present, content is already parsed
result = response.message.content
assert_equal result["name"], "John Doe"
assert_equal result["email"], "john.doe@example.com"
assert_equal result["phone"], "(555) 123-4567"
assert_equal result["education"], [ { "degree"=>"BS Computer Science", "institution"=>"Stanford University", "year"=>2020 } ]
assert_equal result["experience"], [ { "job_title"=>"Senior Software Engineer", "company"=>"TechCorp", "duration"=>"2020-2024" } ]
# Generate documentation example
doc_example_output(response)
end
end
test "uses fallback models when primary fails" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_fallback_models") do
prompt = OpenRouterIntegrationAgent.test_fallback
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
# Check metadata for fallback usage
if response.respond_to?(:metadata) && response.metadata
# Should use one of the fallback models, not the primary
possible_models = [ "openai/gpt-3.5-turbo-0301", "openai/gpt-3.5-turbo", "openai/gpt-4o-mini" ]
assert possible_models.include?(response.metadata[:model_used])
assert response.metadata[:provider].present?
end
# The response should still work (2+2=4)
assert response.message.content.include?("4")
# Generate documentation example
doc_example_output(response)
end
end
test "applies transforms for long content" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_transforms") do
# Generate a very long text
long_text = "Lorem ipsum dolor sit amet. " * 1000
prompt = OpenRouterIntegrationAgent.with(text: long_text).process_long_text
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
assert response.message.content.present?
# The summary should be much shorter than the original
assert response.message.content.length < long_text.length / 10
# Generate documentation example
doc_example_output(response)
end
end
test "tracks usage and costs" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_cost_tracking") do
prompt = OpenRouterIntegrationAgent.with(message: "Hello").prompt_context
response = prompt.generate_now
assert_not_nil response
# Check for usage information
if response.respond_to?(:usage) && response.usage
assert response.usage["prompt_tokens"].is_a?(Integer)
assert response.usage["completion_tokens"].is_a?(Integer)
assert response.usage["total_tokens"].is_a?(Integer)
end
# Check for metadata with model information from OpenRouter
if response.respond_to?(:metadata) && response.metadata
assert response.metadata[:model_used].present?
assert response.metadata[:provider].present?
# Verify we're using the expected model (gpt-4o-mini)
assert_equal "openai/gpt-4o-mini", response.metadata[:model_used]
end
# Generate documentation example
doc_example_output(response)
end
end
test "includes OpenRouter headers in requests" do
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o",
"app_name" => "TestApp",
"site_url" => "https://test.example.com"
)
# Get the headers that would be sent
headers = provider.send(:openrouter_headers)
assert_equal "https://test.example.com", headers["HTTP-Referer"]
assert_equal "TestApp", headers["X-Title"]
end
test "builds provider preferences correctly" do
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o",
"enable_fallbacks" => true,
"provider" => {
"order" => [ "OpenAI", "Anthropic" ],
"require_parameters" => true,
"data_collection" => "deny"
}
)
prefs = provider.send(:build_provider_preferences)
assert_equal [ "OpenAI", "Anthropic" ], prefs[:order]
assert_equal true, prefs[:require_parameters]
assert_equal true, prefs[:allow_fallbacks]
assert_equal "deny", prefs[:data_collection]
end
test "configures data collection policies" do
# Test deny all data collection
provider_deny = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o",
"data_collection" => "deny"
)
prefs_deny = provider_deny.send(:build_provider_preferences)
assert_equal "deny", prefs_deny[:data_collection]
# Test allow all data collection (default)
provider_allow = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o"
)
prefs_allow = provider_allow.send(:build_provider_preferences)
assert_equal "allow", prefs_allow[:data_collection]
# Test selective provider data collection
provider_selective = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o",
"data_collection" => [ "OpenAI", "Google" ]
)
prefs_selective = provider_selective.send(:build_provider_preferences)
assert_equal [ "OpenAI", "Google" ], prefs_selective[:data_collection]
end
test "handles multimodal content correctly" do
# Create a message with multimodal content
message = ActiveAgent::ActionPrompt::Message.new(
content: [
{ type: "text", text: "What's in this image?" },
{ type: "image_url", image_url: { url: "https://example.com/image.jpg" } }
],
role: :user
)
prompt = ActiveAgent::ActionPrompt::Prompt.new(
messages: [ message ]
)
assert prompt.multimodal?
end
test "converts file type to image_url for OpenRouter PDF support" do
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o"
)
# Test file type conversion
file_item = {
type: "file",
file: {
file_data: "data:application/pdf;base64,JVBERi0xLj..."
}
}
formatted = provider.send(:format_content_item, file_item)
assert_equal "image_url", formatted[:type]
assert_equal "data:application/pdf;base64,JVBERi0xLj...", formatted[:image_url][:url]
end
test "respects configuration hierarchy for site_url" do
# Test with explicit site_url config
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o",
"site_url" => "https://configured.example.com"
)
assert_equal "https://configured.example.com", provider.instance_variable_get(:@site_url)
# Test with default_url_options in config
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o",
"default_url_options" => {
"host" => "fromconfig.example.com"
}
)
assert_equal "fromconfig.example.com", provider.instance_variable_get(:@site_url)
end
test "handles rate limit information in metadata" do
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o"
)
# Create a mock response
prompt = ActiveAgent::ActionPrompt::Prompt.new(message: "test")
response = ActiveAgent::GenerationProvider::Response.new(prompt: prompt)
headers = {
"x-provider" => "OpenAI",
"x-model" => "gpt-4o",
"x-ratelimit-requests-limit" => "100",
"x-ratelimit-requests-remaining" => "99",
"x-ratelimit-tokens-limit" => "10000",
"x-ratelimit-tokens-remaining" => "9500"
}
provider.send(:add_openrouter_metadata, response, headers)
assert_equal "100", response.metadata[:ratelimit][:requests_limit]
assert_equal "99", response.metadata[:ratelimit][:requests_remaining]
assert_equal "10000", response.metadata[:ratelimit][:tokens_limit]
assert_equal "9500", response.metadata[:ratelimit][:tokens_remaining]
end
test "includes plugins parameter when passed in options" do
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o"
)
# Create a prompt with plugins option
prompt = ActiveAgent::ActionPrompt::Prompt.new(
message: "test",
options: {
plugins: [
{
id: "file-parser",
pdf: {
engine: "pdf-text"
}
}
]
}
)
# Set the prompt on the provider
provider.instance_variable_set(:@prompt, prompt)
# Build parameters and verify plugins are included
parameters = provider.send(:build_openrouter_parameters)
assert_not_nil parameters[:plugins]
assert_equal 1, parameters[:plugins].size
assert_equal "file-parser", parameters[:plugins][0][:id]
assert_equal "pdf-text", parameters[:plugins][0][:pdf][:engine]
end
endModel Capabilities Detection
The provider automatically detects model capabilities:
require "test_helper"
require "base64"
require "active_agent/action_prompt/message"
class OpenRouterIntegrationTest < ActiveSupport::TestCase
setup do
@agent = OpenRouterIntegrationAgent.new
end
test "analyzes image with structured output schema" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_image_analysis_structured") do
# Use the sales chart image URL for structured analysis
image_url = "https://raw.githubusercontent.com/activeagents/activeagent/refs/heads/main/test/fixtures/images/sales_chart.png"
prompt = OpenRouterIntegrationAgent.with(image_url: image_url).analyze_image
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
# When output_schema is present, content is already parsed
result = response.message.content
# Verify the structure matches our schema
assert result.key?("description")
assert result.key?("objects")
assert result.key?("scene_type")
assert result.key?("primary_colors")
assert result["objects"].is_a?(Array)
assert [ "indoor", "outdoor", "abstract", "document", "photo", "illustration" ].include?(result["scene_type"])
end
end
test "analyzes remote image URL without structured output" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_remote_image_basic") do
# Use a landscape image URL for basic analysis
image_url = "https://picsum.photos/400/300"
# For now, just use analyze_image without the structured output schema
# We'll get a natural language description instead of JSON
prompt = OpenRouterIntegrationAgent.with(image_url: image_url).analyze_image
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
assert response.message.content.is_a?(String)
assert response.message.content.length > 10
# Since analyze_image uses structured output, we'll get JSON
# Just verify we got a response
# In the future, we could add a simple_analyze action without schema
# Generate documentation example
doc_example_output(response)
end
end
test "extracts receipt data with structured output from local file" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_receipt_extraction_local") do
# Use the test receipt image - file exists, no conditional needed
receipt_path = Rails.root.join("..", "..", "test", "fixtures", "images", "test_receipt.png")
prompt = OpenRouterIntegrationAgent.with(image_path: receipt_path).extract_receipt_data
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
# When output_schema is present, content is already parsed
result = response.message.content
assert_equal result["merchant"]["name"], "Corner Mart"
assert_equal result["total"]["amount"], 14.83
assert_equal result["items"].size, 4
result["items"].each do |item|
assert item.key?("name")
assert item.key?("quantity")
assert item.key?("price")
end
assert_equal result["items"][0], { "name"=>"Milk", "quantity"=>1, "price"=>3.49 }
assert_equal result["items"][1], { "name"=>"Bread", "quantity"=>1, "price"=>2.29 }
assert_equal result["items"][2], { "name"=>"Apples", "quantity"=>1, "price"=>5.1 }
assert_equal result["items"][3], { "name"=>"Eggs", "quantity"=>1, "price"=>2.99 }
# Generate documentation example
doc_example_output(response)
end
end
test "handles base64 encoded images with sales chart" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_base64_sales_chart") do
# Use the sales chart image
chart_path = Rails.root.join("..", "..", "test", "fixtures", "images", "sales_chart.png")
prompt = OpenRouterIntegrationAgent.with(image_path: chart_path).analyze_image
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
assert_includes response.message.content, "(Q1, Q2, Q3, Q4), with varying heights indicating different sales amounts"
# Generate documentation example
doc_example_output(response)
end
end
test "processes PDF document from local file" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_pdf_local") do
# Use the sample resume PDF
pdf_path = Rails.root.join("..", "..", "test", "fixtures", "files", "sample_resume.pdf")
# Read and encode the PDF as base64 - OpenRouter accepts PDFs as image_url with data URL
pdf_data = Base64.strict_encode64(File.read(pdf_path))
prompt = OpenRouterIntegrationAgent.with(
pdf_data: pdf_data,
prompt_text: "Extract information from this document and return as JSON",
output_schema: :resume_schema
).analyze_pdf
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
assert response.message.content.present?
# When output_schema is present, content is already parsed
result = response.message.content
assert_equal result["name"], "John Doe"
assert_equal result["email"], "john.doe@example.com"
assert_equal result["phone"], "(555) 123-4567"
assert_equal result["education"].first, { "degree"=>"BS Computer Science", "institution"=>"Stanford University", "year"=>2020 }
assert_equal result["experience"].first, { "job_title"=>"Senior Software Engineer", "company"=>"TechCorp", "duration"=>"2020-2024" }
# Generate documentation example
doc_example_output(response)
end
end
# endregion pdf_processing_local
test "processes PDF from remote URL of resume no plugins" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_pdf_remote_no_plugin") do
pdf_url = "https://docs.activeagents.ai/sample_resume.pdf"
prompt = OpenRouterIntegrationAgent.with(
pdf_url: pdf_url,
prompt_text: "Analyze the PDF",
output_schema: :resume_schema,
skip_plugin: true
).analyze_pdf
# Remote URLs are not supported without a PDF engine plugin
# OpenAI: Inputs by file URL are not supported for chat completions. Use the ResponsesAPI for this option.
# https://platform.openai.com/docs/guides/pdf-files#file-urls
# Accept either the OpenAI error directly or our wrapped error
# Suppress ruby-openai gem's error output to STDERR
error = assert_raises(ActiveAgent::GenerationProvider::Base::GenerationProviderError, OpenAI::Error) do
prompt.generate_now
end
# Check the error message regardless of which error type was raised
error_message = error.message
assert_match(/Missing required parameter.*file_id/, error_message)
assert_match(/Provider returned error|invalid_request_error/, error_message)
end
end
# region pdf_native_support
test "processes PDF with native model support" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_pdf_native") do
# Test with a model that might have native PDF support
# Using the native engine (charged as input tokens)
pdf_path = Rails.root.join("..", "..", "test", "fixtures", "files", "sample_resume.pdf")
pdf_data = Base64.strict_encode64(File.read(pdf_path))
prompt = OpenRouterIntegrationAgent.with(
pdf_data: pdf_data,
prompt_text: "Analyze this PDF document",
pdf_engine: "native" # Use native engine (charged as input tokens)
).analyze_pdf
# First verify the prompt has the plugins in options
assert prompt.options[:plugins].present?, "Plugins should be present in prompt options"
assert prompt.options[:fallback_models].present?, "Fallback models should be present in prompt options"
assert_equal "file-parser", prompt.options[:plugins][0][:id]
assert_equal "native", prompt.options[:plugins][0][:pdf][:engine]
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
assert response.message.content.present?
assert_includes response.message.content, "John Doe"
# Generate documentation example
doc_example_output(response)
end
end
# endregion pdf_native_support
test "processes PDF without any plugin for models with built-in support" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_pdf_no_plugin") do
# Test without any plugin - for models that have built-in PDF support
pdf_url = "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
prompt = OpenRouterIntegrationAgent.with(
pdf_url: pdf_url,
prompt_text: "Analyze this PDF document",
skip_plugin: true # Don't use any plugin
).analyze_pdf
# Verify no plugins are included when skip_plugin is true
assert_empty prompt.options[:plugins], "Should not have plugins when skip_plugin is true"
response = prompt.generate_now
raw_response = response.raw_response
assert_equal "Google", raw_response["provider"]
assert_not_nil response
assert_not_nil response.message
assert response.message.content.present?
# Generate documentation example
doc_example_output(response)
end
end
test "processes scanned PDF with OCR engine" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_pdf_ocr") do
# Test with the mistral-ocr engine for scanned documents
# Using a simple PDF that should be processable
pdf_url = "https://docs.activeagents.ai/sample_resume.pdf"
prompt = OpenRouterIntegrationAgent.with(
pdf_url: pdf_url,
prompt_text: "Extract text from this PDF.",
output_schema: :resume_schema,
pdf_engine: "mistral-ocr" # OCR engine for text extraction
).analyze_pdf
# Verify OCR engine is specified
assert prompt.options[:plugins].present?, "Should have plugins for OCR"
assert_equal "mistral-ocr", prompt.options[:plugins][0][:pdf][:engine]
response = prompt.generate_now
# MUST return valid JSON - no fallback allowed
raw_response = response.raw_response
# When output_schema is present, content is already parsed
result = response.message.content
assert_equal result["name"], "John Doe"
assert_equal result["email"], "john.doe@example.com"
assert_equal result["phone"], "(555) 123-4567"
assert_equal result["education"], [ { "degree"=>"BS Computer Science", "institution"=>"Stanford University", "year"=>2020 } ]
assert_equal result["experience"], [ { "job_title"=>"Senior Software Engineer", "company"=>"TechCorp", "duration"=>"2020-2024" } ]
# Generate documentation example
doc_example_output(response)
end
end
test "uses fallback models when primary fails" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_fallback_models") do
prompt = OpenRouterIntegrationAgent.test_fallback
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
# Check metadata for fallback usage
if response.respond_to?(:metadata) && response.metadata
# Should use one of the fallback models, not the primary
possible_models = [ "openai/gpt-3.5-turbo-0301", "openai/gpt-3.5-turbo", "openai/gpt-4o-mini" ]
assert possible_models.include?(response.metadata[:model_used])
assert response.metadata[:provider].present?
end
# The response should still work (2+2=4)
assert response.message.content.include?("4")
# Generate documentation example
doc_example_output(response)
end
end
test "applies transforms for long content" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_transforms") do
# Generate a very long text
long_text = "Lorem ipsum dolor sit amet. " * 1000
prompt = OpenRouterIntegrationAgent.with(text: long_text).process_long_text
response = prompt.generate_now
assert_not_nil response
assert_not_nil response.message
assert response.message.content.present?
# The summary should be much shorter than the original
assert response.message.content.length < long_text.length / 10
# Generate documentation example
doc_example_output(response)
end
end
test "tracks usage and costs" do
skip "Requires actual OpenRouter API key and credits" unless has_openrouter_credentials?
VCR.use_cassette("openrouter_cost_tracking") do
prompt = OpenRouterIntegrationAgent.with(message: "Hello").prompt_context
response = prompt.generate_now
assert_not_nil response
# Check for usage information
if response.respond_to?(:usage) && response.usage
assert response.usage["prompt_tokens"].is_a?(Integer)
assert response.usage["completion_tokens"].is_a?(Integer)
assert response.usage["total_tokens"].is_a?(Integer)
end
# Check for metadata with model information from OpenRouter
if response.respond_to?(:metadata) && response.metadata
assert response.metadata[:model_used].present?
assert response.metadata[:provider].present?
# Verify we're using the expected model (gpt-4o-mini)
assert_equal "openai/gpt-4o-mini", response.metadata[:model_used]
end
# Generate documentation example
doc_example_output(response)
end
end
test "includes OpenRouter headers in requests" do
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o",
"app_name" => "TestApp",
"site_url" => "https://test.example.com"
)
# Get the headers that would be sent
headers = provider.send(:openrouter_headers)
assert_equal "https://test.example.com", headers["HTTP-Referer"]
assert_equal "TestApp", headers["X-Title"]
end
test "builds provider preferences correctly" do
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o",
"enable_fallbacks" => true,
"provider" => {
"order" => [ "OpenAI", "Anthropic" ],
"require_parameters" => true,
"data_collection" => "deny"
}
)
prefs = provider.send(:build_provider_preferences)
assert_equal [ "OpenAI", "Anthropic" ], prefs[:order]
assert_equal true, prefs[:require_parameters]
assert_equal true, prefs[:allow_fallbacks]
assert_equal "deny", prefs[:data_collection]
end
test "configures data collection policies" do
# Test deny all data collection
provider_deny = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o",
"data_collection" => "deny"
)
prefs_deny = provider_deny.send(:build_provider_preferences)
assert_equal "deny", prefs_deny[:data_collection]
# Test allow all data collection (default)
provider_allow = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o"
)
prefs_allow = provider_allow.send(:build_provider_preferences)
assert_equal "allow", prefs_allow[:data_collection]
# Test selective provider data collection
provider_selective = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o",
"data_collection" => [ "OpenAI", "Google" ]
)
prefs_selective = provider_selective.send(:build_provider_preferences)
assert_equal [ "OpenAI", "Google" ], prefs_selective[:data_collection]
end
test "handles multimodal content correctly" do
# Create a message with multimodal content
message = ActiveAgent::ActionPrompt::Message.new(
content: [
{ type: "text", text: "What's in this image?" },
{ type: "image_url", image_url: { url: "https://example.com/image.jpg" } }
],
role: :user
)
prompt = ActiveAgent::ActionPrompt::Prompt.new(
messages: [ message ]
)
assert prompt.multimodal?
end
test "converts file type to image_url for OpenRouter PDF support" do
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o"
)
# Test file type conversion
file_item = {
type: "file",
file: {
file_data: "data:application/pdf;base64,JVBERi0xLj..."
}
}
formatted = provider.send(:format_content_item, file_item)
assert_equal "image_url", formatted[:type]
assert_equal "data:application/pdf;base64,JVBERi0xLj...", formatted[:image_url][:url]
end
test "respects configuration hierarchy for site_url" do
# Test with explicit site_url config
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o",
"site_url" => "https://configured.example.com"
)
assert_equal "https://configured.example.com", provider.instance_variable_get(:@site_url)
# Test with default_url_options in config
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o",
"default_url_options" => {
"host" => "fromconfig.example.com"
}
)
assert_equal "fromconfig.example.com", provider.instance_variable_get(:@site_url)
end
test "handles rate limit information in metadata" do
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o"
)
# Create a mock response
prompt = ActiveAgent::ActionPrompt::Prompt.new(message: "test")
response = ActiveAgent::GenerationProvider::Response.new(prompt: prompt)
headers = {
"x-provider" => "OpenAI",
"x-model" => "gpt-4o",
"x-ratelimit-requests-limit" => "100",
"x-ratelimit-requests-remaining" => "99",
"x-ratelimit-tokens-limit" => "10000",
"x-ratelimit-tokens-remaining" => "9500"
}
provider.send(:add_openrouter_metadata, response, headers)
assert_equal "100", response.metadata[:ratelimit][:requests_limit]
assert_equal "99", response.metadata[:ratelimit][:requests_remaining]
assert_equal "10000", response.metadata[:ratelimit][:tokens_limit]
assert_equal "9500", response.metadata[:ratelimit][:tokens_remaining]
end
test "includes plugins parameter when passed in options" do
provider = ActiveAgent::GenerationProvider::OpenRouterProvider.new(
"model" => "openai/gpt-4o"
)
# Create a prompt with plugins option
prompt = ActiveAgent::ActionPrompt::Prompt.new(
message: "test",
options: {
plugins: [
{
id: "file-parser",
pdf: {
engine: "pdf-text"
}
}
]
}
)
# Set the prompt on the provider
provider.instance_variable_set(:@prompt, prompt)
# Build parameters and verify plugins are included
parameters = provider.send(:build_openrouter_parameters)
assert_not_nil parameters[:plugins]
assert_equal 1, parameters[:plugins].size
assert_equal "file-parser", parameters[:plugins][0][:id]
assert_equal "pdf-text", parameters[:plugins][0][:pdf][:engine]
end
endImportant Notes
Model Compatibility
When using OpenRouter's advanced features, ensure your chosen model supports the required capabilities:
- Structured Output: Requires models like
openai/gpt-4o,openai/gpt-4o-mini, or other OpenAI models with structured output support - Vision/Image Analysis: Requires vision-capable models like GPT-4o, Claude 3, or Gemini Pro Vision
- PDF Processing: May require specific plugins or engines depending on the model and document type
For tasks requiring both vision and structured output (like receipt extraction), use models that support both capabilities, such as:
openai/gpt-4oopenai/gpt-4o-mini
See Also
- Data Extraction Agent - Comprehensive examples of structured data extraction
- Generation Provider Overview - Understanding provider architecture
- OpenRouter API Documentation - Official OpenRouter documentation