OmniAI

repository·main·Indexed 42 days ago

https://github.com/ksylvest/omniai

A unified Ruby API for integrating with multiple AI providers including Anthropic, OpenAI, Google, Mistral, DeepSeek, and Llama. It provides a consistent interface for chat (text, vision, tools, history), speech (TTS, STT), embeddings, and structured data via OmniAI::Schema. OmniAI also supports the Model Context Protocol (MCP) for building servers and includes a CLI for testing AI capabilities.

Tokens
14.8K
Snippets
63
Records
70
Agent score
54%

What's inside OmniAI

  1. Implement Tool Calling (Function Calling)

    main

    To allow an LLM to use tools, define a class inheriting from OmniAI::Tool. Use the description and parameter DSL to define the tool's interface. Pass an array of tool instances to the tools: option in client.chat.

    Tool Definition Pattern:

    • description "..." sets the tool's purpose.
    • parameter :name, :type, description: "..." defines inputs.
    • required %i[param1 param2] specifies mandatory parameters.
    • def execute(...) contains the logic to run when the tool is called.
    class WeatherTool < OmniAI::Tool
      description "Lookup the weather for a lat / lng."
    
      parameter :lat, :number, description: "The latitude of the location."
      parameter :lng, :number, description: "The longitude of the location."
      parameter :unit, :string, enum: %w[Celsius Fahrenheit], description: "The unit of measurement."
      required %i[lat lng]
    
      def execute(lat:, lng:, unit: "Celsius")
        "#{rand(20..50)}° #{unit} at lat=#{lat} lng=#{lng}"
      end
    end
    
    # Usage in chat
    client.chat(stream: $stdout, tools: [WeatherTool.new]) do |prompt|
      prompt.user 'What is the weather in "London" in Celsius?'
    end
  2. Build an MCP Server with OmniAI

    main

    OmniAI supports the Model Context Protocol (MCP) via the stdio transport. You can build an MCP server by defining tools (inheriting from OmniAI::Tool) and running an OmniAI::MCP::Server with an OmniAI::MCP::Transport::Stdio instance.

    class Weather < OmniAI::Tool
      description "Lookup the weather for a location"
      parameter :location, :string, description: "A location (e.g. 'London')."
      required %i[location]
    
      def execute(location:)
        location == 'London' ? 'Rainy' : 'Sunny'
      end
    end
    
    transport = OmniAI::MCP::Transport::Stdio.new
    mcp = OmniAI::MCP::Server.new(tools: [Weather.new])
    mcp.run(transport:)
  3. Implement and Use Tools in Chat

    main

    To provide tools to a model, define a class that specifies its description, parameters, and execution logic.

    When using tools, the client handles the tool-call chain. By default, only the final Response is returned. To capture intermediate steps (each round of tool calls), use the on_response: option.

    class WeatherTool
      description "Lookup the weather at a location."
    
      parameter :location, :string, description: "The location.", nullable: false
      parameter :unit, :string, enum: %w[Celsius Fahrenheit], description: "The unit.", nullable: true
      required %i[location unit]
    
      def execute(location:, unit: "Celsius")
        { temperature: "20°", humidity: 50 }
      end
    end
    
    # Using the tool
    client.chat('What is the weather in London in Celsius?', tools: [WeatherTool.new])
    
    # Capturing every round of the tool-call chain
    rounds = []
    client.chat('Weather in London?', tools: [WeatherTool.new], on_response: proc { |round| rounds << round })
  4. Initialize an OmniAI Client

    main

    OmniAI provides specialized gems for different AI providers. To use a specific provider, require its corresponding gem and initialize its client.

    Supported providers include:

    • Anthropic: OmniAI::Anthropic::Client
    • DeepSeek: OmniAI::DeepSeek::Client
    • Llama: OmniAI::Llama::Client
    • Google: OmniAI::Google::Client
    • Mistral: OmniAI::Mistral::Client
    • OpenAI: OmniAI::OpenAI::Client

    For OpenAI-compatible gateways (like LocalAI or Ollama), you can configure a custom host.

    require 'omniai/openai'
    
    # Standard OpenAI usage
    client = OmniAI::OpenAI::Client.new(api_key: ENV.fetch('OPENAI_API_KEY'))
    
    # Custom host (e.g., for LocalAI, Ollama, or private gateways)
    client = OmniAI::OpenAI::Client.new(
      api_key: ENV.fetch('OPENAI_API_KEY'),
      host: ENV.fetch('OPENAI_HOST', 'https://api.openai.com')
    )
  5. Install OmniAI and provider gems

    main

    To use OmniAI, install the main gem and the specific provider gem for the AI service you wish to use (e.g., Anthropic, OpenAI, Google, etc.).

    Install the core gem:

    gem install omniai

    Install provider-specific gems as needed:

    gem install omniai-anthropic
    gem install omniai-deepseek
    gem install omniai-mistral
    gem install omniai-google
    gem install omniai-openai
    gem install omniai
  6. Automatic tool naming with namify

    main

    OmniAI automatically generates a tool name from the class name using the namify method. This converts CamelCase class names into snake_case strings and removes the Tool suffix.

    Example mapping:

    • WeatherTool $\rightarrow$ weather
    • IBM::Watson::SearchTool $\rightarrow$ ibm_watson_search
  7. Configure Client Logging and Timeouts

    main

    You can customize client behavior by passing a logger or timeout settings during initialization.

    • Logging: Pass any Ruby Logger instance to log requests and responses.
    • Timeouts: Pass an integer for a total request/response timeout, or a hash for granular control over read, write, and connect durations.
    require 'omniai/openai'
    require 'logger'
    
    # Logging requests/responses
    logger = Logger.new(STDOUT)
    client = OmniAI::OpenAI::Client.new(logger: logger)
    
    # Simple integer timeout (seconds)
    client = OmniAI::OpenAI::Client.new(timeout: 8)
    
    # Granular timeout hash
    client = OmniAI::OpenAI::Client.new(timeout: {
      read: 2,
      write: 3,
      connect: 4
    })
  8. Implement a custom embedding provider

    main

    To add support for a new embedding provider, you must create a subclass of OmniAI::Embed. Your implementation must define the payload and path methods to specify how the request is structured and where it is sent.

    When implementing, you can also override context to provide serializers or deserializers specific to your provider, and params if the request requires additional query parameters.

    class MyProvider::Embed < OmniAI::Embed
      module Model
        MY_MODEL = "my-model-name"
      end
    
      protected
    
      # Returns the JSON body for the request
      def payload
        { input: @input, model: @model }
      end
    
      # Returns the API endpoint path
      def path
        "/v1/embeddings"
      end
    end
  9. Handle tool calls and errors in OmniAI::Chat

    main

    When using tools, OmniAI::Chat#process! automatically detects if the model requests a tool call. If tools are present, it executes them and continues the conversation to get a final response.

    Error Handling

    If a tool call fails or is misconfigured, the following errors may be raised:

    • OmniAI::Chat::ToolCallError: A general error raised for tool-call issues.
    • OmniAI::Chat::ToolCallMissingError: Raised when the model requests a tool that was not provided in the tools array.
  10. How OmniAI clients work (Abstract Base Class)

    main

    The OmniAI::Client class is an abstract base class designed to be subclassed by specific provider implementations (e.g., OmniAI::OpenAI::Client).

    When implementing a new provider, you should inherit from OmniAI::Client and override the core functional methods. The base class provides common attributes and a connection management system using the http gem.

    Core Attributes:

    • api_key: The authentication key for the provider.
    • logger: An optional logger for instrumentation.
    • host: An optional custom host URL (e.g., for local proxies).
    • timeout: An optional integer timeout for requests.

    Core Methods to Implement:

    • chat(prompt, model:, ...): For text/multimodal chat interactions.
    • transcribe(io, model:, ...): For speech-to-text tasks.
    • speak(input, model:, voice:, ...): For text-to-speech tasks.
    • embed(input, model:, ...): For generating vector embeddings.
    # Pattern for subclassing OmniAI::Client
    class OmniAI::MyProvider::Client < OmniAI::Client
      def initialize(api_key: ENV.fetch('MY_API_KEY'), logger: nil)
        super
      end
    
      def connection
        @connection ||= super.auth("Bearer: #{@api_key}")
      end
    
      def chat(prompt = nil, model:, **options)
        # Implementation logic here
      end
    end
  11. How OmniAI::Chat::Stream aggregates data

    main

    The OmniAI::Chat::Stream class transforms fragmented JSON chunks into a coherent response. It handles the following logic:

    1. Content Merging: It accumulates delta content into a single message["content"] string.
    2. Tool Call Merging: If the model uses tools, it merges tool_calls by index and concatenates the function["arguments"] strings, which are typically sent in fragments.
    3. Payload Structure: It converts a sequence of delta chunks like this:
      • { "id":"...", "choices": [{ "index": 0, "delta": { "role" :"assistant", "content":"" } }] }
      • { "id":"...", "choices": [{ "index": 0, "delta": { "content" :"A" } }] }
      • { "id":"...", "choices": [{ "index": 0, "delta": { "content" :"B" } }] }

    Into a single combined payload structure containing the full message and tool call data.

  12. Manage tool calls with OmniAI::Chat::ToolCallList

    main

    The OmniAI::Chat::ToolCallList class is a collection designed to handle merging multiple tool call arrays. This is particularly useful when working with LLMs that provide tool calls in chunks; the list allows you to combine these subsets using their indices to ensure a complete set of tool calls is reconstructed.

    Key features:

    • Merging: Use the + operator to combine two ToolCallList instances.
    • Serialization: Convert the list back into a raw array of hashes using serialize.
    • Deserialization: Reconstruct a ToolCallList from raw data using self.deserialize.
    • Enumerable: It includes the Enumerable module, allowing you to use standard methods like each, any?, and others.
    # Merging two lists
    list1 = OmniAI::Chat::ToolCallList.new(entries: [tool_call_a])
    list2 = OmniAI::Chat::ToolCallList.new(entries: [tool_call_b])
    combined_list = list1 + list2
    
    # Deserializing from data
    new_list = OmniAI::Chat::ToolCallList.deserialize(raw_data_array)
    
    # Serializing to data
    raw_array = new_list.serialize