diff --git a/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent.rb b/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent.rb index d27e253b..2fffcc22 100644 --- a/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent.rb +++ b/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent.rb @@ -71,7 +71,7 @@ class Agent < ApplicationRecord ].freeze # Available providers - PROVIDERS = %w[openai anthropic ollama openrouter].freeze + PROVIDERS = %w[openai anthropic ollama openrouter requesty].freeze # Returns the configuration as a hash for versioning def configuration_snapshot diff --git a/lib/active_agent/providers/requesty/_types.rb b/lib/active_agent/providers/requesty/_types.rb new file mode 100644 index 00000000..33bda61f --- /dev/null +++ b/lib/active_agent/providers/requesty/_types.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require_relative "../open_ai/chat/_types" +require_relative "options" + +module ActiveAgent + module Providers + module Requesty + # ActiveModel type for casting and serializing Requesty requests. + # + # Requesty is OpenAI-compatible, so requests use the same shape as the + # OpenAI Chat API. This delegates entirely to OpenAI::Chat::RequestType. + RequestType = ActiveAgent::Providers::OpenAI::Chat::RequestType + end + end +end diff --git a/lib/active_agent/providers/requesty/options.rb b/lib/active_agent/providers/requesty/options.rb new file mode 100644 index 00000000..706b24b0 --- /dev/null +++ b/lib/active_agent/providers/requesty/options.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require_relative "../open_ai/options" + +module ActiveAgent + module Providers + module Requesty + # Configuration options for the Requesty provider. + # + # Extends OpenAI::Options, overriding the base URL to point at Requesty's + # OpenAI-compatible gateway and resolving the API key from REQUESTY_API_KEY. + # Requesty does not use organization or project identifiers. + # + # @example Basic configuration + # options = Options.new(api_key: ENV["REQUESTY_API_KEY"]) + # + # @see https://docs.requesty.ai + # @see https://app.requesty.ai/api-keys Requesty API Keys + class Options < ActiveAgent::Providers::OpenAI::Options + # @!attribute base_url + # @return [String] API endpoint (default: "https://router.requesty.ai/v1") + attribute :base_url, :string, as: "https://router.requesty.ai/v1" + + private + + def resolve_api_key(kwargs) + kwargs[:api_key] || + kwargs[:access_token] || + ENV["REQUESTY_API_KEY"] || + ENV["REQUESTY_ACCESS_TOKEN"] + end + + # Not used as part of Requesty + def resolve_organization_id(kwargs) = nil + def resolve_project_id(kwargs) = nil + end + end + end +end diff --git a/lib/active_agent/providers/requesty_provider.rb b/lib/active_agent/providers/requesty_provider.rb new file mode 100644 index 00000000..bdc2a1fe --- /dev/null +++ b/lib/active_agent/providers/requesty_provider.rb @@ -0,0 +1,63 @@ +require_relative "_base_provider" + +require_gem!(:openai, __FILE__) + +require_relative "open_ai_provider" +require_relative "requesty/_types" + +module ActiveAgent + module Providers + # Provides access to Requesty's OpenAI-compatible LLM gateway. + # + # Extends the OpenAI provider to work with Requesty's OpenAI-compatible API, + # enabling access to multiple AI models through a single interface using the + # +provider/model+ naming convention (e.g. +openai/gpt-4o-mini+). + # + # Requesty is a plain OpenAI-compatible gateway: requests, responses and + # transforms are identical to the OpenAI Chat API, so this provider reuses + # OpenAI::Chat::RequestType and OpenAI::Chat::Transforms directly. The only + # Requesty-specific configuration is the base URL and API key, which live in + # Requesty::Options. + # + # @example Configuration in active_agent.yml + # requesty: + # service: "Requesty" + # api_key: <%= ENV["REQUESTY_API_KEY"] %> + # model: "openai/gpt-4o-mini" + # + # @see OpenAI::ChatProvider + # @see https://docs.requesty.ai + class RequestyProvider < OpenAI::ChatProvider + # @return [String] + def self.service_name + "Requesty" + end + + # @return [Class] + def self.options_klass + Requesty::Options + end + + # @return [ActiveModel::Type::Value] + def self.prompt_request_type + OpenAI::Chat::RequestType.new + end + + # @return [ActiveModel::Type::Value] + def self.embed_request_type + OpenAI::Embedding::RequestType.new + end + + protected + + # @see BaseProvider#api_response_normalize + # @param api_response [OpenAI::Models::ChatCompletion] + # @return [Hash] normalized response hash + def api_response_normalize(api_response) + return api_response unless api_response + + OpenAI::Chat::Transforms.gem_to_hash(api_response) + end + end + end +end diff --git a/test/providers/requesty/provider_loading_test.rb b/test/providers/requesty/provider_loading_test.rb new file mode 100644 index 00000000..6960ba68 --- /dev/null +++ b/test/providers/requesty/provider_loading_test.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require "test_helper" + +class RequestyProviderLoadingTest < ActiveSupport::TestCase + test "loads RequestyProvider via requesty_provider path" do + require "active_agent/providers/requesty_provider" + + assert defined?(ActiveAgent::Providers::RequestyProvider) + assert defined?(ActiveAgent::Providers::Requesty::Options) + end + + test "provider concern loads Requesty service correctly" do + # Simulate how the provider concern loads providers + service_name = "Requesty" + require "active_agent/providers/#{service_name.underscore}_provider" + + remaps = ActiveAgent::Provider::PROVIDER_SERVICE_NAMES_REMAPS + remapped = Hash.new(service_name).merge!(remaps)[service_name] + + assert_equal "Requesty", remapped + + provider_class = ActiveAgent::Providers.const_get("#{remapped.camelize}Provider") + assert_equal ActiveAgent::Providers::RequestyProvider, provider_class + end + + test "Requesty options default to the Requesty gateway and REQUESTY_API_KEY" do + require "active_agent/providers/requesty_provider" + + options = ActiveAgent::Providers::Requesty::Options.new(api_key: "rqsty-test") + + assert_equal "https://router.requesty.ai/v1", options.base_url + assert_equal "rqsty-test", options.api_key + end +end