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."
end
Features
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-4o
openai/gpt-4o-mini
openai/gpt-4-turbo
(structured output only, no vision)openai/gpt-3.5-turbo
variants (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
end
TIP
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
end
Image Analysis with Structured Output
activeagent/test/agents/open_router_integration_test.rb:57
# Response object
#<ActiveAgent::GenerationProvider::Response:0x3d68
@message=#<ActiveAgent::ActionPrompt::Message:0x3d7c
@action_id=nil,
@action_name=nil,
@action_requested=false,
@charset="UTF-8",
@content="```json\n{\n \"description\": \"A scenic view of the Golden Gate Bridge partially obscured by fog, spanning across the water. The bridge is seen from an elevated perspective, showcasing its iconic towers and cables. A sailboat is visible on the water below.\",\n...",
@role=:assistant>
@prompt=#<ActiveAgent::ActionPrompt::Prompt:0x3d90 ...>
@content_type="application/json"
@raw_response={...}>
# Message content
response.message.content # => "```json\n{\n \"description\": \"A scenic view of the Golden Gate Bridge partially obscured by fog, spanning across the water. The bridge is seen from an elevated perspective, showcasing its iconic towers and cables. A sailboat is visible on the water below.\",\n \"objects\": [\n \"Golden Gate Bridge\",\n \"water\",\n \"fog\",\n \"sailboat\"\n ],\n \"scene_type\": \"landscape\",\n \"primary_colors\": [\n \"blue\",\n \"gray\",\n \"white\",\n \"brown\"\n ]\n}\n```"
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
end
Receipt 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
}
}
end
The 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
activeagent/test/agents/open_router_integration_test.rb:90
# Response object
#<ActiveAgent::GenerationProvider::Response:0x3d18
@message=#<ActiveAgent::ActionPrompt::Message:0x3d2c
@action_id=nil,
@action_name=nil,
@action_requested=false,
@charset="UTF-8",
@content={"merchant"=>{"name"=>"Corner Mart", "address"=>"123 Main St.\nCity, State 12345"}, "date"=>"", "total"=>{"amount"=>14.83, "currency"=>"USD"}, "items"=>[{"name"=>"Milk", "quantity"=>1, "price"=>3.49}, {"name"=>"Bread", "quantity"=>1, "price"=>2.29}, {"name"=>"Apples", "quantity"=>1, "price"=>5.1}, {"name"=>"Eggs", "quantity"=>1, "price"=>2.99}], "tax"=>0.96, "subtotal"=>13.87},
@role=:assistant>
@prompt=#<ActiveAgent::ActionPrompt::Prompt:0x3d40 ...>
@content_type="application/json"
@raw_response={...}>
# Message content
response.message.content # => {"merchant"=>{"name"=>"Corner Mart", "address"=>"123 Main St.\nCity, State 12345"}, "date"=>"", "total"=>{"amount"=>14.83, "currency"=>"USD"}, "items"=>[{"name"=>"Milk", "quantity"=>1, "price"=>3.49}, {"name"=>"Bread", "quantity"=>1, "price"=>2.29}, {"name"=>"Apples", "quantity"=>1, "price"=>5.1}, {"name"=>"Eggs", "quantity"=>1, "price"=>2.99}], "tax"=>0.96, "subtotal"=>13.87}
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
end
PDF Processing Example
activeagent/test/agents/open_router_integration_test.rb:144
# Response object
#<ActiveAgent::GenerationProvider::Response:0x3fac
@message=#<ActiveAgent::ActionPrompt::Message:0x3fc0
@action_id=nil,
@action_name=nil,
@action_requested=false,
@charset="UTF-8",
@content={"name"=>"John Doe", "email"=>"john.doe@example.com", "phone"=>"(555) 123-4567", "education"=>[{"degree"=>"BS Computer Science", "institution"=>"Stanford University", "year"=>2020}], "experience"=>[{"job_title"=>"Senior Software Engineer", "company"=>"TechCorp", "duration"=>"2020-2024"}]},
@role=:assistant>
@prompt=#<ActiveAgent::ActionPrompt::Prompt:0x3fd4 ...>
@content_type="application/json"
@raw_response={...}>
# Message content
response.message.content # => {"name"=>"John Doe", "email"=>"john.doe@example.com", "phone"=>"(555) 123-4567", "education"=>[{"degree"=>"BS Computer Science", "institution"=>"Stanford University", "year"=>2020}], "experience"=>[{"job_title"=>"Senior Software Engineer", "company"=>"TechCorp", "duration"=>"2020-2024"}]}
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
end
OCR Processing Example
activeagent/test/agents/open_router_integration_test.rb:273
# Response object
#<ActiveAgent::GenerationProvider::Response:0x3e1c
@message=#<ActiveAgent::ActionPrompt::Message:0x3e30
@action_id=nil,
@action_name=nil,
@action_requested=false,
@charset="UTF-8",
@content={"name"=>"John Doe", "email"=>"john.doe@example.com", "phone"=>"(555) 123-4567", "education"=>[{"degree"=>"BS Computer Science", "institution"=>"Stanford University", "year"=>2020}], "experience"=>[{"job_title"=>"Senior Software Engineer", "company"=>"TechCorp", "duration"=>"2020-2024"}]},
@role=:assistant>
@prompt=#<ActiveAgent::ActionPrompt::Prompt:0x3e44 ...>
@content_type="application/json"
@raw_response={...}>
# Message content
response.message.content # => {"name"=>"John Doe", "email"=>"john.doe@example.com", "phone"=>"(555) 123-4567", "education"=>[{"degree"=>"BS Computer Science", "institution"=>"Stanford University", "year"=>2020}], "experience"=>[{"job_title"=>"Senior Software Engineer", "company"=>"TechCorp", "duration"=>"2020-2024"}]}
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
end
Fallback Model Example
activeagent/test/agents/open_router_integration_test.rb:299
# Response object
#<ActiveAgent::GenerationProvider::Response:0x3ca0
@message=#<ActiveAgent::ActionPrompt::Message:0x3cb4
@action_id=nil,
@action_name=nil,
@action_requested=false,
@charset="UTF-8",
@content="4",
@role=:assistant>
@prompt=#<ActiveAgent::ActionPrompt::Prompt:0x3cc8 ...>
@content_type="text/plain"
@raw_response={...}>
# Message content
response.message.content # => "4"
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
end
Transform Example
activeagent/test/agents/open_router_integration_test.rb:321
# Response object
#<ActiveAgent::GenerationProvider::Response:0x3c50
@message=#<ActiveAgent::ActionPrompt::Message:0x3c64
@action_id=nil,
@action_name=nil,
@action_requested=false,
@charset="UTF-8",
@content="- The text consists predominantly of repetitive phrases of \"Lorem ipsum dolor sit amet,\" which is a placeholder text commonly used in design and printing.\n- It emphasizes the lack of specific content or context, serving primarily as a demonstration of typo...",
@role=:assistant>
@prompt=#<ActiveAgent::ActionPrompt::Prompt:0x3c78 ...>
@content_type="text/plain"
@raw_response={...}>
# Message content
response.message.content # => "- The text consists predominantly of repetitive phrases of \"Lorem ipsum dolor sit amet,\" which is a placeholder text commonly used in design and printing.\n- It emphasizes the lack of specific content or context, serving primarily as a demonstration of typography and layout.\n- The repetitive nature of the text suggests it is intended for testing visual elements rather than conveying meaningful information."
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
end
Usage Tracking Example
activeagent/test/agents/open_router_integration_test.rb:350
# Response object
#<ActiveAgent::GenerationProvider::Response:0x3ebc
@message=#<ActiveAgent::ActionPrompt::Message:0x3ed0
@action_id=nil,
@action_name=nil,
@action_requested=false,
@charset="UTF-8",
@content="Hello! How can I assist you today?",
@role=:assistant>
@prompt=#<ActiveAgent::ActionPrompt::Prompt:0x3ee4 ...>
@content_type="text/plain"
@raw_response={...}>
# Message content
response.message.content # => "Hello! How can I assist you today?"
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
end
Data 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
end
Real-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."
)
end
Selective 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
}
}
)
end
You 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
end
Privacy 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
end
Model 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
end
Important 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-4o
openai/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