swarm_sdk 2.0.1 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/swarm_sdk/agent/builder.rb +7 -2
- data/lib/swarm_sdk/agent/definition.rb +57 -34
- data/lib/swarm_sdk/claude_code_agent_adapter.rb +205 -0
- data/lib/swarm_sdk/configuration.rb +4 -2
- data/lib/swarm_sdk/markdown_parser.rb +30 -1
- data/lib/swarm_sdk/model_aliases.json +5 -0
- data/lib/swarm_sdk/models.json +1 -0
- data/lib/swarm_sdk/models.rb +120 -0
- data/lib/swarm_sdk/swarm/builder.rb +77 -4
- data/lib/swarm_sdk/version.rb +1 -1
- data/lib/swarm_sdk.rb +0 -30
- metadata +5 -1
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: f2cedd1197d0fea1e52b40531ecb6d6f030524436c3164c109bce37da1db883b
         | 
| 4 | 
            +
              data.tar.gz: ac77a97bd54374a9049de888e0dfc4a1b1c99d5a4fdc2c01fabe20e54a7cd83b
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 9d1dfdd64d1247e15a3fea9ce56b0d28657e85a5f9d706d1a2ce9c72700226cf443dcdc691596710d2e948816a3fb5d93fe1520f3bc798df78a2b6a91e9a2085
         | 
| 7 | 
            +
              data.tar.gz: b54ae2918f3cb04eb2a49a80faecc2aac1a878cd14828cc3287705eae22ba4da09780c8e38a54c5d048cf4eb1fec75e523dc609192758f6ad60294467d68df01
         | 
| @@ -163,13 +163,14 @@ module SwarmSDK | |
| 163 163 | 
             
                    @description = text
         | 
| 164 164 | 
             
                  end
         | 
| 165 165 |  | 
| 166 | 
            -
                  #  | 
| 166 | 
            +
                  # Set or add tools
         | 
| 167 167 | 
             
                  #
         | 
| 168 168 | 
             
                  # Uses Set internally to automatically deduplicate tool names across multiple calls.
         | 
| 169 169 | 
             
                  # This allows calling tools() multiple times without worrying about duplicates.
         | 
| 170 170 | 
             
                  #
         | 
| 171 171 | 
             
                  # @param tool_names [Array<Symbol>] Tool names to add
         | 
| 172 172 | 
             
                  # @param include_default [Boolean] Whether to include default tools (Read, Grep, etc.)
         | 
| 173 | 
            +
                  # @param replace [Boolean] If true, replaces existing tools instead of merging (default: false)
         | 
| 173 174 | 
             
                  #
         | 
| 174 175 | 
             
                  # @example Basic usage with defaults
         | 
| 175 176 | 
             
                  #   tools :Grep, :Read  # include_default: true is implicit
         | 
| @@ -181,7 +182,11 @@ module SwarmSDK | |
| 181 182 | 
             
                  #   tools :Read
         | 
| 182 183 | 
             
                  #   tools :Write, :Edit  # @tools now contains Set[:Read, :Write, :Edit]
         | 
| 183 184 | 
             
                  #   tools :Read          # Still Set[:Read, :Write, :Edit] - no duplicate
         | 
| 184 | 
            -
                   | 
| 185 | 
            +
                  #
         | 
| 186 | 
            +
                  # @example Replace tools (for markdown overrides)
         | 
| 187 | 
            +
                  #   tools :Read, :Write, replace: true  # Replaces all existing tools
         | 
| 188 | 
            +
                  def tools(*tool_names, include_default: true, replace: false)
         | 
| 189 | 
            +
                    @tools = Set.new if replace
         | 
| 185 190 | 
             
                    @tools.merge(tool_names.map(&:to_sym))
         | 
| 186 191 | 
             
                    @include_default_tools = include_default
         | 
| 187 192 | 
             
                  end
         | 
| @@ -73,12 +73,9 @@ module SwarmSDK | |
| 73 73 | 
             
                    @timeout = config[:timeout] || DEFAULT_TIMEOUT
         | 
| 74 74 | 
             
                    @bypass_permissions = config[:bypass_permissions] || false
         | 
| 75 75 | 
             
                    @max_concurrent_tools = config[:max_concurrent_tools]
         | 
| 76 | 
            -
                    #  | 
| 77 | 
            -
                     | 
| 78 | 
            -
             | 
| 79 | 
            -
                    else
         | 
| 80 | 
            -
                      (base_url ? true : false)
         | 
| 81 | 
            -
                    end
         | 
| 76 | 
            +
                    # Always assume model exists - SwarmSDK validates models separately using models.json
         | 
| 77 | 
            +
                    # This prevents RubyLLM from trying to validate models in its registry
         | 
| 78 | 
            +
                    @assume_model_exists = true
         | 
| 82 79 |  | 
| 83 80 | 
             
                    # include_default_tools defaults to true if not specified
         | 
| 84 81 | 
             
                    @include_default_tools = config.key?(:include_default_tools) ? config[:include_default_tools] : true
         | 
| @@ -168,45 +165,53 @@ module SwarmSDK | |
| 168 165 |  | 
| 169 166 | 
             
                  private
         | 
| 170 167 |  | 
| 171 | 
            -
                  # Validate that model exists in  | 
| 168 | 
            +
                  # Validate that model exists in SwarmSDK's model registry
         | 
| 169 | 
            +
                  #
         | 
| 170 | 
            +
                  # Uses SwarmSDK's static models.json instead of RubyLLM's dynamic registry.
         | 
| 171 | 
            +
                  # This provides stable, offline model validation without network calls.
         | 
| 172 | 
            +
                  #
         | 
| 173 | 
            +
                  # Process:
         | 
| 174 | 
            +
                  # 1. Try to find model directly in models.json
         | 
| 175 | 
            +
                  # 2. If not found, try to resolve as alias and find again
         | 
| 176 | 
            +
                  # 3. If still not found, return warning with suggestions
         | 
| 172 177 | 
             
                  #
         | 
| 173 178 | 
             
                  # @return [Hash, nil] Warning hash if model not found, nil otherwise
         | 
| 174 179 | 
             
                  def validate_model
         | 
| 175 | 
            -
                    # Try  | 
| 176 | 
            -
                     | 
| 177 | 
            -
             | 
| 180 | 
            +
                    # Try direct lookup first
         | 
| 181 | 
            +
                    model_data = SwarmSDK::Models.all.find { |m| (m["id"] || m[:id]) == @model }
         | 
| 182 | 
            +
             | 
| 183 | 
            +
                    # If not found, try alias resolution
         | 
| 184 | 
            +
                    unless model_data
         | 
| 185 | 
            +
                      resolved_id = SwarmSDK::Models.resolve_alias(@model)
         | 
| 186 | 
            +
                      # Only search again if alias was different
         | 
| 187 | 
            +
                      if resolved_id != @model
         | 
| 188 | 
            +
                        model_data = SwarmSDK::Models.all.find { |m| (m["id"] || m[:id]) == resolved_id }
         | 
| 189 | 
            +
                      end
         | 
| 190 | 
            +
                    end
         | 
| 191 | 
            +
             | 
| 192 | 
            +
                    if model_data
         | 
| 193 | 
            +
                      nil # Model exists (either directly or via alias)
         | 
| 194 | 
            +
                    else
         | 
| 195 | 
            +
                      # Model not found - return warning with suggestions
         | 
| 196 | 
            +
                      {
         | 
| 197 | 
            +
                        type: :model_not_found,
         | 
| 198 | 
            +
                        agent: @name,
         | 
| 199 | 
            +
                        model: @model,
         | 
| 200 | 
            +
                        error_message: "Unknown model: #{@model}",
         | 
| 201 | 
            +
                        suggestions: SwarmSDK::Models.suggest_similar(@model),
         | 
| 202 | 
            +
                      }
         | 
| 203 | 
            +
                    end
         | 
| 178 204 | 
             
                  rescue StandardError => e
         | 
| 179 | 
            -
                    #  | 
| 205 | 
            +
                    # Return warning on error
         | 
| 180 206 | 
             
                    {
         | 
| 181 207 | 
             
                      type: :model_not_found,
         | 
| 182 208 | 
             
                      agent: @name,
         | 
| 183 209 | 
             
                      model: @model,
         | 
| 184 210 | 
             
                      error_message: e.message,
         | 
| 185 | 
            -
                      suggestions:  | 
| 211 | 
            +
                      suggestions: [],
         | 
| 186 212 | 
             
                    }
         | 
| 187 213 | 
             
                  end
         | 
| 188 214 |  | 
| 189 | 
            -
                  # Suggest similar models when a model is not found
         | 
| 190 | 
            -
                  #
         | 
| 191 | 
            -
                  # @return [Array<Hash>] Up to 3 similar models with their info
         | 
| 192 | 
            -
                  def suggest_similar_models
         | 
| 193 | 
            -
                    normalized_query = @model.to_s.downcase.gsub(/[.\-_]/, "")
         | 
| 194 | 
            -
             | 
| 195 | 
            -
                    RubyLLM.models.all.select do |model_info|
         | 
| 196 | 
            -
                      normalized_id = model_info.id.downcase.gsub(/[.\-_]/, "")
         | 
| 197 | 
            -
                      normalized_id.include?(normalized_query) ||
         | 
| 198 | 
            -
                        model_info.name&.downcase&.gsub(/[.\-_]/, "")&.include?(normalized_query)
         | 
| 199 | 
            -
                    end.first(3).map do |model_info|
         | 
| 200 | 
            -
                      {
         | 
| 201 | 
            -
                        id: model_info.id,
         | 
| 202 | 
            -
                        name: model_info.name,
         | 
| 203 | 
            -
                        context_window: model_info.context_window,
         | 
| 204 | 
            -
                      }
         | 
| 205 | 
            -
                    end
         | 
| 206 | 
            -
                  rescue StandardError
         | 
| 207 | 
            -
                    []
         | 
| 208 | 
            -
                  end
         | 
| 209 | 
            -
             | 
| 210 215 | 
             
                  def build_full_system_prompt(custom_prompt)
         | 
| 211 216 | 
             
                    # If coding_agent is false (default), return custom prompt with optional TODO/Scratchpad info
         | 
| 212 217 | 
             
                    # If coding_agent is true, include full base prompt for coding tasks
         | 
| @@ -253,9 +258,27 @@ module SwarmSDK | |
| 253 258 |  | 
| 254 259 | 
             
                  def render_non_coding_base_prompt
         | 
| 255 260 | 
             
                    # Simplified base prompt for non-coding agents
         | 
| 256 | 
            -
                    #  | 
| 261 | 
            +
                    # Includes environment info, TODO, and Scratchpad tool information
         | 
| 257 262 | 
             
                    # Does not steer towards coding tasks
         | 
| 263 | 
            +
                    cwd = @directory || Dir.pwd
         | 
| 264 | 
            +
                    platform = RUBY_PLATFORM
         | 
| 265 | 
            +
                    os_version = begin
         | 
| 266 | 
            +
                      %x(uname -sr 2>/dev/null).strip
         | 
| 267 | 
            +
                    rescue
         | 
| 268 | 
            +
                      RUBY_PLATFORM
         | 
| 269 | 
            +
                    end
         | 
| 270 | 
            +
                    date = Time.now.strftime("%Y-%m-%d")
         | 
| 271 | 
            +
             | 
| 258 272 | 
             
                    <<~PROMPT.strip
         | 
| 273 | 
            +
                      # Environment
         | 
| 274 | 
            +
             | 
| 275 | 
            +
                      <env>
         | 
| 276 | 
            +
                      Working directory: #{cwd}
         | 
| 277 | 
            +
                      Platform: #{platform}
         | 
| 278 | 
            +
                      OS Version: #{os_version}
         | 
| 279 | 
            +
                      Today's date: #{date}
         | 
| 280 | 
            +
                      </env>
         | 
| 281 | 
            +
             | 
| 259 282 | 
             
                      # Task Management
         | 
| 260 283 |  | 
| 261 284 | 
             
                      You have access to the TodoWrite tool to help you manage and plan tasks. Use this tool to track your progress and give visibility into your work.
         | 
| @@ -0,0 +1,205 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module SwarmSDK
         | 
| 4 | 
            +
              # Adapter for converting Claude Code agent markdown files to SwarmSDK format
         | 
| 5 | 
            +
              #
         | 
| 6 | 
            +
              # Claude Code agent files use a different syntax and conventions than SwarmSDK:
         | 
| 7 | 
            +
              # - Tools are comma-separated strings instead of arrays
         | 
| 8 | 
            +
              # - Model shortcuts like 'sonnet', 'opus', 'haiku' instead of full model IDs
         | 
| 9 | 
            +
              # - Tool permissions like 'Write(src/**)' instead of SwarmSDK's permission system
         | 
| 10 | 
            +
              # - Required 'name' field in frontmatter
         | 
| 11 | 
            +
              #
         | 
| 12 | 
            +
              # This adapter:
         | 
| 13 | 
            +
              # - Detects Claude Code format by checking frontmatter markers
         | 
| 14 | 
            +
              # - Converts tools from comma-separated strings to arrays
         | 
| 15 | 
            +
              # - Maps model shortcuts to canonical model IDs
         | 
| 16 | 
            +
              # - Strips unsupported tool permission syntax with warnings
         | 
| 17 | 
            +
              # - Sets coding_agent: true by default
         | 
| 18 | 
            +
              # - Warns about unsupported fields
         | 
| 19 | 
            +
              #
         | 
| 20 | 
            +
              # @example Parse a Claude Code agent file
         | 
| 21 | 
            +
              #   content = File.read('.claude/agents/reviewer.md')
         | 
| 22 | 
            +
              #   config = ClaudeCodeAgentAdapter.parse(content, :reviewer)
         | 
| 23 | 
            +
              #   agent = Agent::Definition.new(:reviewer, config)
         | 
| 24 | 
            +
              #
         | 
| 25 | 
            +
              class ClaudeCodeAgentAdapter
         | 
| 26 | 
            +
                # Fields supported in Claude Code agent frontmatter
         | 
| 27 | 
            +
                SUPPORTED_FIELDS = ["name", "description", "tools", "model"].freeze
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                # SwarmSDK documentation URL for reference
         | 
| 30 | 
            +
                SWARM_SDK_DOCS_URL = "https://github.com/parruda/claude-swarm/blob/main/docs/v2/README.md"
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                # Pattern to detect tool permission syntax like Write(src/**)
         | 
| 33 | 
            +
                TOOL_PERMISSION_PATTERN = /^([A-Za-z_]+)\([^)]+\)$/
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                class << self
         | 
| 36 | 
            +
                  # Detect if content appears to be in Claude Code agent format
         | 
| 37 | 
            +
                  #
         | 
| 38 | 
            +
                  # Detection is based on tools field type:
         | 
| 39 | 
            +
                  # - Claude Code: tools is a comma-separated string (e.g., "Read, Write, Bash")
         | 
| 40 | 
            +
                  # - SwarmSDK: tools is an array (e.g., [Read, Write, Bash])
         | 
| 41 | 
            +
                  #
         | 
| 42 | 
            +
                  # Note: The 'name' field alone is not sufficient since SwarmSDK also supports it
         | 
| 43 | 
            +
                  #
         | 
| 44 | 
            +
                  # @param content [String] Markdown content with YAML frontmatter
         | 
| 45 | 
            +
                  # @return [Boolean] true if content appears to be Claude Code format
         | 
| 46 | 
            +
                  def claude_code_format?(content)
         | 
| 47 | 
            +
                    return false unless content =~ /\A---\s*\n(.*?)\n---\s*\n/m
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                    frontmatter_yaml = Regexp.last_match(1)
         | 
| 50 | 
            +
                    frontmatter = YAML.safe_load(frontmatter_yaml, permitted_classes: [Symbol], aliases: true)
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                    return false unless frontmatter.is_a?(Hash)
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                    # Only detect as Claude Code if tools field is a comma-separated string
         | 
| 55 | 
            +
                    # This is the most reliable indicator since SwarmSDK always uses arrays
         | 
| 56 | 
            +
                    frontmatter.key?("tools") && frontmatter["tools"].is_a?(String)
         | 
| 57 | 
            +
                  rescue Psych::SyntaxError
         | 
| 58 | 
            +
                    false
         | 
| 59 | 
            +
                  end
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                  # Parse Claude Code agent markdown and convert to SwarmSDK format
         | 
| 62 | 
            +
                  #
         | 
| 63 | 
            +
                  # @param content [String] Markdown content with YAML frontmatter
         | 
| 64 | 
            +
                  # @param agent_name [Symbol, String] Name of the agent
         | 
| 65 | 
            +
                  # @param inherit_model [String, nil] Model to use when frontmatter has 'inherit'
         | 
| 66 | 
            +
                  # @return [Hash] Configuration hash suitable for Agent::Definition.new
         | 
| 67 | 
            +
                  # @raise [ConfigurationError] if content format is invalid
         | 
| 68 | 
            +
                  def parse(content, agent_name, inherit_model: nil)
         | 
| 69 | 
            +
                    new(inherit_model: inherit_model).parse(content, agent_name)
         | 
| 70 | 
            +
                  end
         | 
| 71 | 
            +
                end
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                # Initialize adapter with optional context
         | 
| 74 | 
            +
                #
         | 
| 75 | 
            +
                # @param inherit_model [String, nil] Model to use when frontmatter has 'inherit'
         | 
| 76 | 
            +
                def initialize(inherit_model: nil)
         | 
| 77 | 
            +
                  @inherit_model = inherit_model
         | 
| 78 | 
            +
                  @warnings = []
         | 
| 79 | 
            +
                end
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                # Parse Claude Code agent content
         | 
| 82 | 
            +
                #
         | 
| 83 | 
            +
                # @param content [String] Markdown content with YAML frontmatter
         | 
| 84 | 
            +
                # @param agent_name [Symbol, String] Name of the agent
         | 
| 85 | 
            +
                # @return [Hash] Configuration hash for Agent::Definition
         | 
| 86 | 
            +
                # @raise [ConfigurationError] if format is invalid
         | 
| 87 | 
            +
                def parse(content, agent_name)
         | 
| 88 | 
            +
                  unless content =~ /\A---\s*\n(.*?)\n---\s*\n(.*)\z/m
         | 
| 89 | 
            +
                    raise ConfigurationError, "Invalid Claude Code agent format. Expected YAML frontmatter followed by prompt content."
         | 
| 90 | 
            +
                  end
         | 
| 91 | 
            +
             | 
| 92 | 
            +
                  frontmatter_yaml = Regexp.last_match(1)
         | 
| 93 | 
            +
                  prompt_content = Regexp.last_match(2).strip
         | 
| 94 | 
            +
             | 
| 95 | 
            +
                  frontmatter = YAML.safe_load(frontmatter_yaml, permitted_classes: [Symbol], aliases: true)
         | 
| 96 | 
            +
             | 
| 97 | 
            +
                  unless frontmatter.is_a?(Hash)
         | 
| 98 | 
            +
                    raise ConfigurationError, "Invalid frontmatter format in Claude Code agent file"
         | 
| 99 | 
            +
                  end
         | 
| 100 | 
            +
             | 
| 101 | 
            +
                  config = build_config(frontmatter, prompt_content, agent_name)
         | 
| 102 | 
            +
                  emit_warnings(agent_name)
         | 
| 103 | 
            +
                  config
         | 
| 104 | 
            +
                end
         | 
| 105 | 
            +
             | 
| 106 | 
            +
                private
         | 
| 107 | 
            +
             | 
| 108 | 
            +
                # Build SwarmSDK configuration from Claude Code frontmatter
         | 
| 109 | 
            +
                def build_config(frontmatter, prompt_content, agent_name)
         | 
| 110 | 
            +
                  warn_unknown_fields(frontmatter)
         | 
| 111 | 
            +
             | 
| 112 | 
            +
                  config = {
         | 
| 113 | 
            +
                    description: frontmatter["description"],
         | 
| 114 | 
            +
                    system_prompt: prompt_content,
         | 
| 115 | 
            +
                    coding_agent: true, # Default for Claude Code agents
         | 
| 116 | 
            +
                  }
         | 
| 117 | 
            +
             | 
| 118 | 
            +
                  # Parse tools if present
         | 
| 119 | 
            +
                  if frontmatter["tools"]
         | 
| 120 | 
            +
                    config[:tools] = parse_tools(frontmatter["tools"])
         | 
| 121 | 
            +
                  end
         | 
| 122 | 
            +
             | 
| 123 | 
            +
                  # Parse model if present
         | 
| 124 | 
            +
                  if frontmatter["model"]
         | 
| 125 | 
            +
                    config[:model] = resolve_model(frontmatter["model"])
         | 
| 126 | 
            +
                  end
         | 
| 127 | 
            +
             | 
| 128 | 
            +
                  config
         | 
| 129 | 
            +
                end
         | 
| 130 | 
            +
             | 
| 131 | 
            +
                # Parse tools field - handles both comma-separated string and array
         | 
| 132 | 
            +
                #
         | 
| 133 | 
            +
                # @param tools_field [String, Array] Tools from frontmatter
         | 
| 134 | 
            +
                # @return [Array<String>] Array of tool names
         | 
| 135 | 
            +
                def parse_tools(tools_field)
         | 
| 136 | 
            +
                  tools_array = if tools_field.is_a?(String)
         | 
| 137 | 
            +
                    tools_field.split(",").map(&:strip)
         | 
| 138 | 
            +
                  else
         | 
| 139 | 
            +
                    Array(tools_field).map(&:to_s)
         | 
| 140 | 
            +
                  end
         | 
| 141 | 
            +
             | 
| 142 | 
            +
                  # Clean tool permissions and collect warnings
         | 
| 143 | 
            +
                  tools_array.map { |tool| clean_tool_permissions(tool) }.compact
         | 
| 144 | 
            +
                end
         | 
| 145 | 
            +
             | 
| 146 | 
            +
                # Strip tool permission syntax and warn if detected
         | 
| 147 | 
            +
                #
         | 
| 148 | 
            +
                # @param tool_string [String] Tool name, possibly with permissions like 'Write(src/**)'
         | 
| 149 | 
            +
                # @return [String, nil] Clean tool name, or nil if invalid
         | 
| 150 | 
            +
                def clean_tool_permissions(tool_string)
         | 
| 151 | 
            +
                  if tool_string =~ TOOL_PERMISSION_PATTERN
         | 
| 152 | 
            +
                    tool_name = Regexp.last_match(1)
         | 
| 153 | 
            +
                    @warnings << "Tool permission syntax '#{tool_string}' detected in agent file. SwarmSDK supports permissions but uses different syntax. Using '#{tool_name}' without restrictions for now. See SwarmSDK documentation for permission configuration: #{SWARM_SDK_DOCS_URL}"
         | 
| 154 | 
            +
                    tool_name
         | 
| 155 | 
            +
                  else
         | 
| 156 | 
            +
                    tool_string
         | 
| 157 | 
            +
                  end
         | 
| 158 | 
            +
                end
         | 
| 159 | 
            +
             | 
| 160 | 
            +
                # Resolve model shortcuts to canonical model IDs
         | 
| 161 | 
            +
                #
         | 
| 162 | 
            +
                # Uses SwarmSDK::Models.resolve_alias to map shortcuts like 'sonnet'
         | 
| 163 | 
            +
                # to the latest model IDs from model_aliases.json.
         | 
| 164 | 
            +
                #
         | 
| 165 | 
            +
                # @param model_field [String] Model from frontmatter
         | 
| 166 | 
            +
                # @return [String, Symbol] Canonical model ID or :inherit symbol
         | 
| 167 | 
            +
                def resolve_model(model_field)
         | 
| 168 | 
            +
                  model_str = model_field.to_s.strip
         | 
| 169 | 
            +
             | 
| 170 | 
            +
                  # Handle 'inherit' keyword
         | 
| 171 | 
            +
                  return :inherit if model_str == "inherit"
         | 
| 172 | 
            +
             | 
| 173 | 
            +
                  # Resolve using SwarmSDK model aliases
         | 
| 174 | 
            +
                  # This maps 'sonnet' → 'claude-sonnet-4-5-20250929', etc.
         | 
| 175 | 
            +
                  SwarmSDK::Models.resolve_alias(model_str)
         | 
| 176 | 
            +
                end
         | 
| 177 | 
            +
             | 
| 178 | 
            +
                # Warn about unknown frontmatter fields
         | 
| 179 | 
            +
                def warn_unknown_fields(frontmatter)
         | 
| 180 | 
            +
                  unknown_fields = frontmatter.keys - SUPPORTED_FIELDS
         | 
| 181 | 
            +
             | 
| 182 | 
            +
                  unknown_fields.each do |field|
         | 
| 183 | 
            +
                    @warnings << case field
         | 
| 184 | 
            +
                    when "hooks"
         | 
| 185 | 
            +
                      "Hooks configuration detected in agent frontmatter. SwarmSDK handles hooks at the swarm level. See: #{SWARM_SDK_DOCS_URL}"
         | 
| 186 | 
            +
                    else
         | 
| 187 | 
            +
                      "Unknown field '#{field}' in Claude Code agent file. Ignoring. Supported fields: #{SUPPORTED_FIELDS.join(", ")}"
         | 
| 188 | 
            +
                    end
         | 
| 189 | 
            +
                  end
         | 
| 190 | 
            +
                end
         | 
| 191 | 
            +
             | 
| 192 | 
            +
                # Emit all collected warnings via LogCollector
         | 
| 193 | 
            +
                def emit_warnings(agent_name)
         | 
| 194 | 
            +
                  return if @warnings.empty?
         | 
| 195 | 
            +
             | 
| 196 | 
            +
                  @warnings.each do |warning|
         | 
| 197 | 
            +
                    LogCollector.emit(
         | 
| 198 | 
            +
                      type: "claude_code_conversion_warning",
         | 
| 199 | 
            +
                      agent: agent_name,
         | 
| 200 | 
            +
                      message: warning,
         | 
| 201 | 
            +
                    )
         | 
| 202 | 
            +
                  end
         | 
| 203 | 
            +
                end
         | 
| 204 | 
            +
              end
         | 
| 205 | 
            +
            end
         | 
| @@ -238,8 +238,10 @@ module SwarmSDK | |
| 238 238 | 
             
                  # Parse markdown and merge with YAML config
         | 
| 239 239 | 
             
                  agent_def_from_file = MarkdownParser.parse(content, name)
         | 
| 240 240 |  | 
| 241 | 
            -
                  # Merge:  | 
| 242 | 
            -
                   | 
| 241 | 
            +
                  # Merge: YAML config overrides markdown file (YAML takes precedence)
         | 
| 242 | 
            +
                  # This allows YAML to override any settings from the markdown file
         | 
| 243 | 
            +
                  final_config = agent_def_from_file.to_h.compact.merge(merged_config.compact)
         | 
| 244 | 
            +
             | 
| 243 245 | 
             
                  Agent::Definition.new(name, final_config)
         | 
| 244 246 | 
             
                rescue StandardError => e
         | 
| 245 247 | 
             
                  raise ConfigurationError, "Error loading agent '#{name}' from file '#{file_path}': #{e.message}"
         | 
| @@ -1,12 +1,41 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 2 |  | 
| 3 3 | 
             
            module SwarmSDK
         | 
| 4 | 
            +
              # Parser for agent markdown files with YAML frontmatter
         | 
| 5 | 
            +
              #
         | 
| 6 | 
            +
              # Supports two formats:
         | 
| 7 | 
            +
              # 1. SwarmSDK format - YAML frontmatter with array-based tools
         | 
| 8 | 
            +
              # 2. Claude Code format - Detected and converted via ClaudeCodeAgentAdapter
         | 
| 9 | 
            +
              #
         | 
| 10 | 
            +
              # Format detection is automatic based on frontmatter structure.
         | 
| 4 11 | 
             
              class MarkdownParser
         | 
| 5 12 | 
             
                FRONTMATTER_PATTERN = /\A---\s*\n(.*?)\n---\s*\n(.*)\z/m
         | 
| 6 13 |  | 
| 7 14 | 
             
                class << self
         | 
| 15 | 
            +
                  # Parse markdown content into an Agent::Definition
         | 
| 16 | 
            +
                  #
         | 
| 17 | 
            +
                  # Automatically detects format (SwarmSDK or Claude Code) and routes
         | 
| 18 | 
            +
                  # to appropriate parser.
         | 
| 19 | 
            +
                  #
         | 
| 20 | 
            +
                  # @param content [String] Markdown content with YAML frontmatter
         | 
| 21 | 
            +
                  # @param agent_name [Symbol, String, nil] Name of the agent
         | 
| 22 | 
            +
                  # @return [Agent::Definition] Parsed agent definition
         | 
| 23 | 
            +
                  # @raise [ConfigurationError] if format is invalid
         | 
| 8 24 | 
             
                  def parse(content, agent_name = nil)
         | 
| 9 | 
            -
                     | 
| 25 | 
            +
                    # Detect Claude Code format and route to adapter
         | 
| 26 | 
            +
                    if ClaudeCodeAgentAdapter.claude_code_format?(content)
         | 
| 27 | 
            +
                      config = ClaudeCodeAgentAdapter.parse(content, agent_name)
         | 
| 28 | 
            +
                      # For Claude Code format, agent_name parameter is required since
         | 
| 29 | 
            +
                      # the 'name' field in frontmatter is Claude Code specific and not used
         | 
| 30 | 
            +
                      unless agent_name
         | 
| 31 | 
            +
                        raise ConfigurationError, "Agent name must be provided when parsing Claude Code format"
         | 
| 32 | 
            +
                      end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                      Agent::Definition.new(agent_name.to_sym, config)
         | 
| 35 | 
            +
                    else
         | 
| 36 | 
            +
                      # Use standard SwarmSDK format parsing
         | 
| 37 | 
            +
                      new(content, agent_name).parse
         | 
| 38 | 
            +
                    end
         | 
| 10 39 | 
             
                  end
         | 
| 11 40 | 
             
                end
         | 
| 12 41 |  | 
| @@ -0,0 +1 @@ | |
| 1 | 
            +
            [{"name":"Claude Sonnet 4.5","id":"claude-sonnet-4-5-20250929","provider":"anthropic","family":"claude-sonnet-4-5","context_window":200000,"max_output_tokens":64000,"modalities":{"input":["image","text"],"output":["text"]},"capabilities":["function_calling"],"pricing":{"text_tokens":{"standard":{"input_per_million":3.0,"cached_input_per_million":3.75,"output_per_million":15.0},"batch":{"input_per_million":null,"output_per_million":null}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"Claude Haiku 4.5","id":"claude-haiku-4-5-20251001","provider":"anthropic","family":"claude-haiku-4-5","context_window":200000,"max_output_tokens":64000,"modalities":{"input":["image","text"],"output":["text"]},"capabilities":["function_calling"],"pricing":{"text_tokens":{"standard":{"input_per_million":1.0,"cached_input_per_million":1.25,"output_per_million":5.0},"batch":{"input_per_million":null,"output_per_million":null}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"Claude Opus 4.1","id":"claude-opus-4-1-20250805","provider":"anthropic","family":"claude-opus-4-1","context_window":200000,"max_output_tokens":32000,"modalities":{"input":["image","text"],"output":["text"]},"capabilities":["function_calling"],"pricing":{"text_tokens":{"standard":{"input_per_million":15.0,"cached_input_per_million":18.75,"output_per_million":75.0},"batch":{"input_per_million":null,"output_per_million":null}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"Gemini 2.5 Pro","id":"gemini-2.5-pro","provider":"gemini","family":"gemini-2.5-pro","context_window":1048576,"max_output_tokens":65536,"modalities":{"input":["audio","text"],"output":["text"]},"capabilities":["batch","function_calling","structured_output"],"pricing":{"text_tokens":{"standard":{"input_per_million":1.25,"cached_input_per_million":0.125,"output_per_million":10.0},"batch":{"input_per_million":0.625,"output_per_million":5.0}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"Gemini 2.5 Flash","id":"gemini-2.5-flash","provider":"gemini","family":"gemini-2.5-flash","context_window":1048576,"max_output_tokens":65536,"modalities":{"input":["audio","image","text"],"output":["text"]},"capabilities":["batch","function_calling","structured_output"],"pricing":{"text_tokens":{"standard":{"input_per_million":0.3,"cached_input_per_million":0.03,"output_per_million":2.5},"batch":{"input_per_million":0.15,"output_per_million":1.25}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"Gemini 2.5 Flash-Lite","id":"gemini-2.5-flash-lite","provider":"gemini","family":"gemini-2.5-flash-lite","context_window":1048576,"max_output_tokens":65536,"modalities":{"input":["audio","image","text"],"output":["text"]},"capabilities":["batch","function_calling","structured_output"],"pricing":{"text_tokens":{"standard":{"input_per_million":0.3,"cached_input_per_million":0.03,"output_per_million":2.5},"batch":{"input_per_million":0.15,"output_per_million":1.25}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"Gemini 2.0 Flash","id":"gemini-2.0-flash","provider":"gemini","family":"gemini-2.0-flash","context_window":1048576,"max_output_tokens":8192,"modalities":{"input":["audio","image","text"],"output":["text"]},"capabilities":["batch","function_calling","structured_output"],"pricing":{"text_tokens":{"standard":{"input_per_million":0.1,"cached_input_per_million":0.025,"output_per_million":0.4},"batch":{"input_per_million":0.05,"output_per_million":0.2}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"Gemini 2.0 Flash","id":"gemini-2.0-flash-001","provider":"gemini","family":"gemini-2.0-flash","context_window":1048576,"max_output_tokens":8192,"modalities":{"input":["audio","image","text"],"output":["text"]},"capabilities":["batch","function_calling","structured_output"],"pricing":{"text_tokens":{"standard":{"input_per_million":0.1,"cached_input_per_million":0.025,"output_per_million":0.4},"batch":{"input_per_million":0.05,"output_per_million":0.2}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"Gemini 2.0 Flash","id":"gemini-2.0-flash-exp","provider":"gemini","family":"gemini-2.0-flash","context_window":1048576,"max_output_tokens":8192,"modalities":{"input":["audio","image","text"],"output":["text"]},"capabilities":["batch","function_calling","structured_output"],"pricing":{"text_tokens":{"standard":{"input_per_million":0.1,"cached_input_per_million":0.025,"output_per_million":0.4},"batch":{"input_per_million":0.05,"output_per_million":0.2}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"Gemini 2.0 Flash-Lite","id":"gemini-2.0-flash-lite","provider":"gemini","family":"gemini-2.0-flash-lite","context_window":1048576,"max_output_tokens":8192,"modalities":{"input":["audio","image","text"],"output":["text"]},"capabilities":["batch","function_calling","structured_output"],"pricing":{"text_tokens":{"standard":{"input_per_million":0.1,"cached_input_per_million":0.025,"output_per_million":0.4},"batch":{"input_per_million":0.05,"output_per_million":0.2}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"Gemini 2.0 Flash-Lite","id":"gemini-2.0-flash-lite-001","provider":"gemini","family":"gemini-2.0-flash-lite","context_window":1048576,"max_output_tokens":8192,"modalities":{"input":["audio","image","text"],"output":["text"]},"capabilities":["batch","function_calling","structured_output"],"pricing":{"text_tokens":{"standard":{"input_per_million":0.1,"cached_input_per_million":0.025,"output_per_million":0.4},"batch":{"input_per_million":0.05,"output_per_million":0.2}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"deepseek-chat","id":"deepseek-chat","provider":"deepseek","family":"deepseek-chat","context_window":128000,"max_output_tokens":8000,"modalities":{"input":["text"],"output":["text"]},"capabilities":["function_calling","structured_output"],"pricing":{"text_tokens":{"standard":{"input_per_million":0.28,"cached_input_per_million":0.028,"output_per_million":0.42},"batch":{"input_per_million":null,"output_per_million":null}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"deepseek-reasoner","id":"deepseek-reasoner","provider":"deepseek","family":"deepseek-reasoner","context_window":null,"max_output_tokens":64000,"modalities":{"input":["text"],"output":["text"]},"capabilities":["function_calling","structured_output"],"pricing":{"text_tokens":{"standard":{"input_per_million":null,"cached_input_per_million":null,"output_per_million":null},"batch":{"input_per_million":null,"output_per_million":null}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"GPT-5","family":"gpt-5","provider":"openai","id":"gpt-5-2025-08-07","context_window":400000,"max_output_tokens":128000,"modalities":{"input":["image","text"],"output":["embeddings","text"]},"capabilities":["batch","function_calling","structured_output"],"pricing":{"text_tokens":{"standard":{"input_per_million":1.25,"cached_input_per_million":0.125,"output_per_million":10.0},"batch":{"input_per_million":0.625,"output_per_million":5.0}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"GPT-5","family":"gpt-5","provider":"openai","id":"gpt-5","context_window":400000,"max_output_tokens":128000,"modalities":{"input":["image","text"],"output":["embeddings","text"]},"capabilities":["batch","function_calling","structured_output"],"pricing":{"text_tokens":{"standard":{"input_per_million":1.25,"cached_input_per_million":0.125,"output_per_million":10.0},"batch":{"input_per_million":0.625,"output_per_million":5.0}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"GPT-5 mini","family":"gpt-5-mini","provider":"openai","id":"gpt-5-mini-2025-08-07","context_window":400000,"max_output_tokens":128000,"modalities":{"input":["image","text"],"output":["embeddings","text"]},"capabilities":["batch","function_calling","structured_output"],"pricing":{"text_tokens":{"standard":{"input_per_million":0.25,"cached_input_per_million":0.025,"output_per_million":2.0},"batch":{"input_per_million":0.125,"output_per_million":1.0}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"GPT-5 mini","family":"gpt-5-mini","provider":"openai","id":"gpt-5-mini","context_window":400000,"max_output_tokens":128000,"modalities":{"input":["image","text"],"output":["embeddings","text"]},"capabilities":["batch","function_calling","structured_output"],"pricing":{"text_tokens":{"standard":{"input_per_million":0.25,"cached_input_per_million":0.025,"output_per_million":2.0},"batch":{"input_per_million":0.125,"output_per_million":1.0}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"GPT-5 nano","family":"gpt-5-nano","provider":"openai","id":"gpt-5-nano-2025-08-07","context_window":400000,"max_output_tokens":128000,"modalities":{"input":["image","text"],"output":["embeddings","text"]},"capabilities":["batch","function_calling","structured_output"],"pricing":{"text_tokens":{"standard":{"input_per_million":0.05,"cached_input_per_million":0.005,"output_per_million":0.4},"batch":{"input_per_million":0.025,"output_per_million":0.2}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"GPT-5 nano","family":"gpt-5-nano","provider":"openai","id":"gpt-5-nano","context_window":400000,"max_output_tokens":128000,"modalities":{"input":["image","text"],"output":["embeddings","text"]},"capabilities":["batch","function_calling","structured_output"],"pricing":{"text_tokens":{"standard":{"input_per_million":0.05,"cached_input_per_million":0.005,"output_per_million":0.4},"batch":{"input_per_million":0.025,"output_per_million":0.2}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"GPT-5 pro","family":"gpt-5-pro","provider":"openai","id":"gpt-5-pro-2025-10-06","context_window":400000,"max_output_tokens":272000,"modalities":{"input":["image","text"],"output":["embeddings","text"]},"capabilities":["batch","function_calling","structured_output"],"pricing":{"text_tokens":{"standard":{"input_per_million":15.0,"cached_input_per_million":null,"output_per_million":120.0},"batch":{"input_per_million":7.5,"output_per_million":60.0}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"GPT-5 pro","family":"gpt-5-pro","provider":"openai","id":"gpt-5-pro","context_window":400000,"max_output_tokens":272000,"modalities":{"input":["image","text"],"output":["embeddings","text"]},"capabilities":["batch","function_calling","structured_output"],"pricing":{"text_tokens":{"standard":{"input_per_million":15.0,"cached_input_per_million":null,"output_per_million":120.0},"batch":{"input_per_million":7.5,"output_per_million":60.0}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"GPT-4.1","family":"gpt-4.1","provider":"openai","id":"gpt-4.1-2025-04-14","context_window":1047576,"max_output_tokens":32768,"modalities":{"input":["image","text"],"output":["embeddings","text"]},"capabilities":["batch","function_calling","structured_output"],"pricing":{"text_tokens":{"standard":{"input_per_million":2.0,"cached_input_per_million":0.5,"output_per_million":8.0},"batch":{"input_per_million":1.0,"output_per_million":4.0}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"GPT-4.1","family":"gpt-4.1","provider":"openai","id":"gpt-4.1","context_window":1047576,"max_output_tokens":32768,"modalities":{"input":["image","text"],"output":["embeddings","text"]},"capabilities":["batch","function_calling","structured_output"],"pricing":{"text_tokens":{"standard":{"input_per_million":2.0,"cached_input_per_million":0.5,"output_per_million":8.0},"batch":{"input_per_million":1.0,"output_per_million":4.0}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"gpt-oss-120b","family":"gpt-oss-120b","provider":"openai","id":"gpt-oss-120b","context_window":131072,"max_output_tokens":131072,"modalities":{"input":["text"],"output":["embeddings","text"]},"capabilities":["batch","function_calling","structured_output"],"pricing":{"text_tokens":{"standard":{"input_per_million":null,"cached_input_per_million":null,"output_per_million":null},"batch":{"input_per_million":null,"output_per_million":null}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"gpt-oss-20b","family":"gpt-oss-20b","provider":"openai","id":"gpt-oss-20b","context_window":131072,"max_output_tokens":131072,"modalities":{"input":["text"],"output":["embeddings","text"]},"capabilities":["batch","function_calling","structured_output"],"pricing":{"text_tokens":{"standard":{"input_per_million":null,"cached_input_per_million":null,"output_per_million":null},"batch":{"input_per_million":null,"output_per_million":null}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"Sora 2","family":"sora-2","provider":"openai","id":"sora-2","context_window":null,"max_output_tokens":null,"modalities":{"input":["image","text"],"output":["audio","embeddings"]},"capabilities":[],"pricing":{"text_tokens":{"standard":{"input_per_million":null,"cached_input_per_million":null,"output_per_million":null},"batch":{"input_per_million":null,"output_per_million":null}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"Sora 2 Pro","family":"sora-2-pro","provider":"openai","id":"sora-2-pro","context_window":null,"max_output_tokens":null,"modalities":{"input":["image","text"],"output":["audio","embeddings"]},"capabilities":[],"pricing":{"text_tokens":{"standard":{"input_per_million":null,"cached_input_per_million":null,"output_per_million":null},"batch":{"input_per_million":null,"output_per_million":null}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"o3-deep-research","family":"o3-deep-research","provider":"openai","id":"o3-deep-research-2025-06-26","context_window":200000,"max_output_tokens":100000,"modalities":{"input":["image","text"],"output":["embeddings","text"]},"capabilities":["batch"],"pricing":{"text_tokens":{"standard":{"input_per_million":10.0,"cached_input_per_million":2.5,"output_per_million":40.0},"batch":{"input_per_million":5.0,"output_per_million":20.0}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"o3-deep-research","family":"o3-deep-research","provider":"openai","id":"o3-deep-research","context_window":200000,"max_output_tokens":100000,"modalities":{"input":["image","text"],"output":["embeddings","text"]},"capabilities":["batch"],"pricing":{"text_tokens":{"standard":{"input_per_million":10.0,"cached_input_per_million":2.5,"output_per_million":40.0},"batch":{"input_per_million":5.0,"output_per_million":20.0}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"o4-mini-deep-research","family":"o4-mini-deep-research","provider":"openai","id":"o4-mini-deep-research-2025-06-26","context_window":200000,"max_output_tokens":100000,"modalities":{"input":["image","text"],"output":["embeddings","text"]},"capabilities":["batch"],"pricing":{"text_tokens":{"standard":{"input_per_million":2.0,"cached_input_per_million":0.5,"output_per_million":8.0},"batch":{"input_per_million":1.0,"output_per_million":4.0}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"o4-mini-deep-research","family":"o4-mini-deep-research","provider":"openai","id":"o4-mini-deep-research","context_window":200000,"max_output_tokens":100000,"modalities":{"input":["image","text"],"output":["embeddings","text"]},"capabilities":["batch"],"pricing":{"text_tokens":{"standard":{"input_per_million":2.0,"cached_input_per_million":0.5,"output_per_million":8.0},"batch":{"input_per_million":1.0,"output_per_million":4.0}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"GPT Image 1","family":"gpt-image-1","provider":"openai","id":"gpt-image-1","context_window":null,"max_output_tokens":null,"modalities":{"input":["image","text"],"output":["embeddings","image"]},"capabilities":[],"pricing":{"text_tokens":{"standard":{"input_per_million":5.0,"cached_input_per_million":1.25,"output_per_million":40.0},"batch":{"input_per_million":null,"output_per_million":null}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"gpt-image-1-mini","family":"gpt-image-1-mini","provider":"openai","id":"gpt-image-1-mini","context_window":null,"max_output_tokens":null,"modalities":{"input":["image","text"],"output":["embeddings","image"]},"capabilities":[],"pricing":{"text_tokens":{"standard":{"input_per_million":2.0,"cached_input_per_million":0.2,"output_per_million":8.0},"batch":{"input_per_million":null,"output_per_million":null}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"DALL·E 3","family":"dall-e-3","provider":"openai","id":"dall-e-3","context_window":null,"max_output_tokens":null,"modalities":{"input":["text"],"output":["embeddings","image"]},"capabilities":[],"pricing":{"text_tokens":{"standard":{"input_per_million":null,"cached_input_per_million":null,"output_per_million":null},"batch":{"input_per_million":null,"output_per_million":null}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"GPT-4o mini TTS","family":"gpt-4o-mini-tts","provider":"openai","id":"gpt-4o-mini-tts","context_window":2000,"max_output_tokens":null,"modalities":{"input":["text"],"output":["audio","embeddings"]},"capabilities":[],"pricing":{"text_tokens":{"standard":{"input_per_million":0.6,"cached_input_per_million":null,"output_per_million":12.0},"batch":{"input_per_million":null,"output_per_million":null}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"GPT-4o Transcribe","family":"gpt-4o-transcribe","provider":"openai","id":"gpt-4o-transcribe","context_window":16000,"max_output_tokens":2000,"modalities":{"input":["audio","text"],"output":["embeddings","text"]},"capabilities":[],"pricing":{"text_tokens":{"standard":{"input_per_million":2.5,"cached_input_per_million":null,"output_per_million":10.0},"batch":{"input_per_million":null,"output_per_million":null}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"GPT-4o mini Transcribe","family":"gpt-4o-mini-transcribe","provider":"openai","id":"gpt-4o-mini-transcribe","context_window":16000,"max_output_tokens":2000,"modalities":{"input":["audio","text"],"output":["embeddings","text"]},"capabilities":[],"pricing":{"text_tokens":{"standard":{"input_per_million":1.25,"cached_input_per_million":null,"output_per_million":5.0},"batch":{"input_per_million":null,"output_per_million":null}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"gpt-realtime","family":"gpt-realtime","provider":"openai","id":"gpt-realtime-2025-08-28","context_window":32000,"max_output_tokens":4096,"modalities":{"input":["audio","image","text"],"output":["audio","embeddings","text"]},"capabilities":["function_calling"],"pricing":{"text_tokens":{"standard":{"input_per_million":4.0,"cached_input_per_million":0.5,"output_per_million":16.0},"batch":{"input_per_million":null,"output_per_million":null}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"gpt-realtime","family":"gpt-realtime","provider":"openai","id":"gpt-realtime","context_window":32000,"max_output_tokens":4096,"modalities":{"input":["audio","image","text"],"output":["audio","embeddings","text"]},"capabilities":["function_calling"],"pricing":{"text_tokens":{"standard":{"input_per_million":4.0,"cached_input_per_million":0.5,"output_per_million":16.0},"batch":{"input_per_million":null,"output_per_million":null}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"gpt-audio","family":"gpt-audio","provider":"openai","id":"gpt-audio-2025-08-28","context_window":128000,"max_output_tokens":16384,"modalities":{"input":["audio","text"],"output":["audio","embeddings","text"]},"capabilities":["function_calling"],"pricing":{"text_tokens":{"standard":{"input_per_million":2.5,"cached_input_per_million":null,"output_per_million":10.0},"batch":{"input_per_million":null,"output_per_million":null}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"gpt-audio","family":"gpt-audio","provider":"openai","id":"gpt-audio","context_window":128000,"max_output_tokens":16384,"modalities":{"input":["audio","text"],"output":["audio","embeddings","text"]},"capabilities":["function_calling"],"pricing":{"text_tokens":{"standard":{"input_per_million":2.5,"cached_input_per_million":null,"output_per_million":10.0},"batch":{"input_per_million":null,"output_per_million":null}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"gpt-realtime-mini","family":"gpt-realtime-mini","provider":"openai","id":"gpt-realtime-mini-2025-10-06","context_window":32000,"max_output_tokens":4096,"modalities":{"input":["audio","image","text"],"output":["audio","embeddings","text"]},"capabilities":["function_calling"],"pricing":{"text_tokens":{"standard":{"input_per_million":0.6,"cached_input_per_million":0.06,"output_per_million":2.4},"batch":{"input_per_million":null,"output_per_million":null}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"gpt-realtime-mini","family":"gpt-realtime-mini","provider":"openai","id":"gpt-realtime-mini","context_window":32000,"max_output_tokens":4096,"modalities":{"input":["audio","image","text"],"output":["audio","embeddings","text"]},"capabilities":["function_calling"],"pricing":{"text_tokens":{"standard":{"input_per_million":0.6,"cached_input_per_million":0.06,"output_per_million":2.4},"batch":{"input_per_million":null,"output_per_million":null}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"gpt-audio-mini","family":"gpt-audio-mini","provider":"openai","id":"gpt-audio-mini-2025-10-06","context_window":128000,"max_output_tokens":16384,"modalities":{"input":["audio","text"],"output":["audio","embeddings","text"]},"capabilities":["function_calling"],"pricing":{"text_tokens":{"standard":{"input_per_million":0.6,"cached_input_per_million":null,"output_per_million":2.4},"batch":{"input_per_million":null,"output_per_million":null}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"gpt-audio-mini","family":"gpt-audio-mini","provider":"openai","id":"gpt-audio-mini","context_window":128000,"max_output_tokens":16384,"modalities":{"input":["audio","text"],"output":["audio","embeddings","text"]},"capabilities":["function_calling"],"pricing":{"text_tokens":{"standard":{"input_per_million":0.6,"cached_input_per_million":null,"output_per_million":2.4},"batch":{"input_per_million":null,"output_per_million":null}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"GPT-5 Chat","family":"gpt-5-chat-latest","provider":"openai","id":"gpt-5-chat-latest","context_window":128000,"max_output_tokens":16384,"modalities":{"input":["image","text"],"output":["embeddings","text"]},"capabilities":["function_calling","structured_output"],"pricing":{"text_tokens":{"standard":{"input_per_million":1.25,"cached_input_per_million":0.125,"output_per_million":10.0},"batch":{"input_per_million":null,"output_per_million":null}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"ChatGPT-4o","family":"chatgpt-4o-latest","provider":"openai","id":"chatgpt-4o-latest","context_window":128000,"max_output_tokens":16384,"modalities":{"input":["image","text"],"output":["embeddings","text"]},"capabilities":[],"pricing":{"text_tokens":{"standard":{"input_per_million":5.0,"cached_input_per_million":null,"output_per_million":15.0},"batch":{"input_per_million":null,"output_per_million":null}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"GPT-5-Codex","family":"gpt-5-codex","provider":"openai","id":"gpt-5-codex","context_window":400000,"max_output_tokens":128000,"modalities":{"input":["image","text"],"output":["embeddings","text"]},"capabilities":["function_calling","structured_output"],"pricing":{"text_tokens":{"standard":{"input_per_million":1.25,"cached_input_per_million":0.125,"output_per_million":10.0},"batch":{"input_per_million":null,"output_per_million":null}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"o3-pro","family":"o3-pro","provider":"openai","id":"o3-pro-2025-06-10","context_window":200000,"max_output_tokens":100000,"modalities":{"input":["image","text"],"output":["embeddings","text"]},"capabilities":["batch","function_calling","structured_output"],"pricing":{"text_tokens":{"standard":{"input_per_million":20.0,"cached_input_per_million":null,"output_per_million":80.0},"batch":{"input_per_million":10.0,"output_per_million":40.0}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"o3-pro","family":"o3-pro","provider":"openai","id":"o3-pro","context_window":200000,"max_output_tokens":100000,"modalities":{"input":["image","text"],"output":["embeddings","text"]},"capabilities":["batch","function_calling","structured_output"],"pricing":{"text_tokens":{"standard":{"input_per_million":20.0,"cached_input_per_million":null,"output_per_million":80.0},"batch":{"input_per_million":10.0,"output_per_million":40.0}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"o3","family":"o3","provider":"openai","id":"o3-2025-04-16","context_window":200000,"max_output_tokens":100000,"modalities":{"input":["image","text"],"output":["embeddings","text"]},"capabilities":["batch","function_calling","structured_output"],"pricing":{"text_tokens":{"standard":{"input_per_million":2.0,"cached_input_per_million":0.5,"output_per_million":8.0},"batch":{"input_per_million":1.0,"output_per_million":4.0}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"o3","family":"o3","provider":"openai","id":"o3","context_window":200000,"max_output_tokens":100000,"modalities":{"input":["image","text"],"output":["embeddings","text"]},"capabilities":["batch","function_calling","structured_output"],"pricing":{"text_tokens":{"standard":{"input_per_million":2.0,"cached_input_per_million":0.5,"output_per_million":8.0},"batch":{"input_per_million":1.0,"output_per_million":4.0}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"o4-mini","family":"o4-mini","provider":"openai","id":"o4-mini-2025-04-16","context_window":200000,"max_output_tokens":100000,"modalities":{"input":["image","text"],"output":["embeddings","text"]},"capabilities":["batch","function_calling","structured_output"],"pricing":{"text_tokens":{"standard":{"input_per_million":1.1,"cached_input_per_million":0.275,"output_per_million":4.4},"batch":{"input_per_million":0.55,"output_per_million":2.2}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"o4-mini","family":"o4-mini","provider":"openai","id":"o4-mini","context_window":200000,"max_output_tokens":100000,"modalities":{"input":["image","text"],"output":["embeddings","text"]},"capabilities":["batch","function_calling","structured_output"],"pricing":{"text_tokens":{"standard":{"input_per_million":1.1,"cached_input_per_million":0.275,"output_per_million":4.4},"batch":{"input_per_million":0.55,"output_per_million":2.2}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"GPT-4.1 mini","family":"gpt-4.1-mini","provider":"openai","id":"gpt-4.1-mini-2025-04-14","context_window":1047576,"max_output_tokens":32768,"modalities":{"input":["image","text"],"output":["embeddings","text"]},"capabilities":["batch","function_calling","structured_output"],"pricing":{"text_tokens":{"standard":{"input_per_million":0.4,"cached_input_per_million":0.1,"output_per_million":1.6},"batch":{"input_per_million":0.2,"output_per_million":0.8}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"GPT-4.1 mini","family":"gpt-4.1-mini","provider":"openai","id":"gpt-4.1-mini","context_window":1047576,"max_output_tokens":32768,"modalities":{"input":["image","text"],"output":["embeddings","text"]},"capabilities":["batch","function_calling","structured_output"],"pricing":{"text_tokens":{"standard":{"input_per_million":0.4,"cached_input_per_million":0.1,"output_per_million":1.6},"batch":{"input_per_million":0.2,"output_per_million":0.8}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"GPT-4.1 nano","family":"gpt-4.1-nano","provider":"openai","id":"gpt-4.1-nano-2025-04-14","context_window":1047576,"max_output_tokens":32768,"modalities":{"input":["image","text"],"output":["embeddings","text"]},"capabilities":["batch","function_calling","structured_output"],"pricing":{"text_tokens":{"standard":{"input_per_million":0.1,"cached_input_per_million":0.025,"output_per_million":0.4},"batch":{"input_per_million":0.05,"output_per_million":0.2}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"GPT-4.1 nano","family":"gpt-4.1-nano","provider":"openai","id":"gpt-4.1-nano","context_window":1047576,"max_output_tokens":32768,"modalities":{"input":["image","text"],"output":["embeddings","text"]},"capabilities":["batch","function_calling","structured_output"],"pricing":{"text_tokens":{"standard":{"input_per_million":0.1,"cached_input_per_million":0.025,"output_per_million":0.4},"batch":{"input_per_million":0.05,"output_per_million":0.2}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"o1-pro","family":"o1-pro","provider":"openai","id":"o1-pro-2025-03-19","context_window":200000,"max_output_tokens":100000,"modalities":{"input":["image","text"],"output":["embeddings","text"]},"capabilities":["batch","function_calling","structured_output"],"pricing":{"text_tokens":{"standard":{"input_per_million":150.0,"cached_input_per_million":null,"output_per_million":600.0},"batch":{"input_per_million":75.0,"output_per_million":300.0}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"o1-pro","family":"o1-pro","provider":"openai","id":"o1-pro","context_window":200000,"max_output_tokens":100000,"modalities":{"input":["image","text"],"output":["embeddings","text"]},"capabilities":["batch","function_calling","structured_output"],"pricing":{"text_tokens":{"standard":{"input_per_million":150.0,"cached_input_per_million":null,"output_per_million":600.0},"batch":{"input_per_million":75.0,"output_per_million":300.0}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"computer-use-preview","family":"computer-use-preview","provider":"openai","id":"computer-use-preview-2025-03-11","context_window":8192,"max_output_tokens":1024,"modalities":{"input":["image","text"],"output":["embeddings","text"]},"capabilities":["batch","function_calling"],"pricing":{"text_tokens":{"standard":{"input_per_million":3.0,"cached_input_per_million":null,"output_per_million":12.0},"batch":{"input_per_million":1.5,"output_per_million":6.0}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"computer-use-preview","family":"computer-use-preview","provider":"openai","id":"computer-use-preview","context_window":8192,"max_output_tokens":1024,"modalities":{"input":["image","text"],"output":["embeddings","text"]},"capabilities":["batch","function_calling"],"pricing":{"text_tokens":{"standard":{"input_per_million":3.0,"cached_input_per_million":null,"output_per_million":12.0},"batch":{"input_per_million":1.5,"output_per_million":6.0}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"GPT-4o mini Search Preview","family":"gpt-4o-mini-search-preview","provider":"openai","id":"gpt-4o-mini-search-preview-2025-03-11","context_window":128000,"max_output_tokens":16384,"modalities":{"input":["text"],"output":["embeddings","text"]},"capabilities":["structured_output"],"pricing":{"text_tokens":{"standard":{"input_per_million":0.15,"cached_input_per_million":null,"output_per_million":0.6},"batch":{"input_per_million":null,"output_per_million":null}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"GPT-4o mini Search Preview","family":"gpt-4o-mini-search-preview","provider":"openai","id":"gpt-4o-mini-search-preview","context_window":128000,"max_output_tokens":16384,"modalities":{"input":["text"],"output":["embeddings","text"]},"capabilities":["structured_output"],"pricing":{"text_tokens":{"standard":{"input_per_million":0.15,"cached_input_per_million":null,"output_per_million":0.6},"batch":{"input_per_million":null,"output_per_million":null}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"GPT-4o Search Preview","family":"gpt-4o-search-preview","provider":"openai","id":"gpt-4o-search-preview-2025-03-11","context_window":128000,"max_output_tokens":16384,"modalities":{"input":["text"],"output":["embeddings","text"]},"capabilities":["structured_output"],"pricing":{"text_tokens":{"standard":{"input_per_million":2.5,"cached_input_per_million":null,"output_per_million":10.0},"batch":{"input_per_million":null,"output_per_million":null}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"GPT-4o Search Preview","family":"gpt-4o-search-preview","provider":"openai","id":"gpt-4o-search-preview","context_window":128000,"max_output_tokens":16384,"modalities":{"input":["text"],"output":["embeddings","text"]},"capabilities":["structured_output"],"pricing":{"text_tokens":{"standard":{"input_per_million":2.5,"cached_input_per_million":null,"output_per_million":10.0},"batch":{"input_per_million":null,"output_per_million":null}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"GPT-4.5 Preview (Deprecated)","family":"gpt-4.5-preview","provider":"openai","id":"gpt-4.5-preview-2025-02-27","context_window":128000,"max_output_tokens":16384,"modalities":{"input":["image","text"],"output":["embeddings","text"]},"capabilities":["batch","function_calling","structured_output"],"pricing":{"text_tokens":{"standard":{"input_per_million":75.0,"cached_input_per_million":37.5,"output_per_million":150.0},"batch":{"input_per_million":37.5,"output_per_million":75.0}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"GPT-4.5 Preview (Deprecated)","family":"gpt-4.5-preview","provider":"openai","id":"gpt-4.5-preview","context_window":128000,"max_output_tokens":16384,"modalities":{"input":["image","text"],"output":["embeddings","text"]},"capabilities":["batch","function_calling","structured_output"],"pricing":{"text_tokens":{"standard":{"input_per_million":75.0,"cached_input_per_million":37.5,"output_per_million":150.0},"batch":{"input_per_million":37.5,"output_per_million":75.0}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"o3-mini","family":"o3-mini","provider":"openai","id":"o3-mini-2025-01-31","context_window":200000,"max_output_tokens":100000,"modalities":{"input":["text"],"output":["embeddings","text"]},"capabilities":["batch","function_calling","structured_output"],"pricing":{"text_tokens":{"standard":{"input_per_million":1.1,"cached_input_per_million":0.55,"output_per_million":4.4},"batch":{"input_per_million":0.55,"output_per_million":2.2}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"o3-mini","family":"o3-mini","provider":"openai","id":"o3-mini","context_window":200000,"max_output_tokens":100000,"modalities":{"input":["text"],"output":["embeddings","text"]},"capabilities":["batch","function_calling","structured_output"],"pricing":{"text_tokens":{"standard":{"input_per_million":1.1,"cached_input_per_million":0.55,"output_per_million":4.4},"batch":{"input_per_million":0.55,"output_per_million":2.2}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"GPT-4o mini Audio","family":"gpt-4o-mini-audio-preview","provider":"openai","id":"gpt-4o-mini-audio-preview-2024-12-17","context_window":128000,"max_output_tokens":16384,"modalities":{"input":["audio","text"],"output":["audio","embeddings","text"]},"capabilities":["function_calling"],"pricing":{"text_tokens":{"standard":{"input_per_million":0.15,"cached_input_per_million":null,"output_per_million":0.6},"batch":{"input_per_million":null,"output_per_million":null}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"GPT-4o mini Audio","family":"gpt-4o-mini-audio-preview","provider":"openai","id":"gpt-4o-mini-audio-preview","context_window":128000,"max_output_tokens":16384,"modalities":{"input":["audio","text"],"output":["audio","embeddings","text"]},"capabilities":["function_calling"],"pricing":{"text_tokens":{"standard":{"input_per_million":0.15,"cached_input_per_million":null,"output_per_million":0.6},"batch":{"input_per_million":null,"output_per_million":null}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"GPT-4o mini Realtime","family":"gpt-4o-mini-realtime-preview","provider":"openai","id":"gpt-4o-mini-realtime-preview-2024-12-17","context_window":16000,"max_output_tokens":4096,"modalities":{"input":["audio","text"],"output":["audio","embeddings","text"]},"capabilities":["function_calling"],"pricing":{"text_tokens":{"standard":{"input_per_million":0.6,"cached_input_per_million":0.3,"output_per_million":2.4},"batch":{"input_per_million":null,"output_per_million":null}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"GPT-4o mini Realtime","family":"gpt-4o-mini-realtime-preview","provider":"openai","id":"gpt-4o-mini-realtime-preview","context_window":16000,"max_output_tokens":4096,"modalities":{"input":["audio","text"],"output":["audio","embeddings","text"]},"capabilities":["function_calling"],"pricing":{"text_tokens":{"standard":{"input_per_million":0.6,"cached_input_per_million":0.3,"output_per_million":2.4},"batch":{"input_per_million":null,"output_per_million":null}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"o1","family":"o1","provider":"openai","id":"o1-2024-12-17","context_window":200000,"max_output_tokens":100000,"modalities":{"input":["image","text"],"output":["embeddings","text"]},"capabilities":["batch","function_calling","structured_output"],"pricing":{"text_tokens":{"standard":{"input_per_million":15.0,"cached_input_per_million":7.5,"output_per_million":60.0},"batch":{"input_per_million":7.5,"output_per_million":30.0}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"o1","family":"o1","provider":"openai","id":"o1","context_window":200000,"max_output_tokens":100000,"modalities":{"input":["image","text"],"output":["embeddings","text"]},"capabilities":["batch","function_calling","structured_output"],"pricing":{"text_tokens":{"standard":{"input_per_million":15.0,"cached_input_per_million":7.5,"output_per_million":60.0},"batch":{"input_per_million":7.5,"output_per_million":30.0}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"omni-moderation","family":"omni-moderation-latest","provider":"openai","id":"omni-moderation-latest","context_window":null,"max_output_tokens":null,"modalities":{"input":["image","text"],"output":["embeddings","text"]},"capabilities":[],"pricing":{"text_tokens":{"standard":{"input_per_million":0.0,"cached_input_per_million":null,"output_per_million":null},"batch":{"input_per_million":null,"output_per_million":null}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"o1-mini","family":"o1-mini","provider":"openai","id":"o1-mini-2024-09-12","context_window":128000,"max_output_tokens":65536,"modalities":{"input":["text"],"output":["embeddings","text"]},"capabilities":[],"pricing":{"text_tokens":{"standard":{"input_per_million":1.1,"cached_input_per_million":0.55,"output_per_million":4.4},"batch":{"input_per_million":null,"output_per_million":null}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"o1-mini","family":"o1-mini","provider":"openai","id":"o1-mini","context_window":128000,"max_output_tokens":65536,"modalities":{"input":["text"],"output":["embeddings","text"]},"capabilities":[],"pricing":{"text_tokens":{"standard":{"input_per_million":1.1,"cached_input_per_million":0.55,"output_per_million":4.4},"batch":{"input_per_million":null,"output_per_million":null}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"o1 Preview","family":"o1-preview","provider":"openai","id":"o1-preview-2024-09-12","context_window":128000,"max_output_tokens":32768,"modalities":{"input":["text"],"output":["embeddings","text"]},"capabilities":["function_calling","structured_output"],"pricing":{"text_tokens":{"standard":{"input_per_million":15.0,"cached_input_per_million":7.5,"output_per_million":60.0},"batch":{"input_per_million":null,"output_per_million":null}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"o1 Preview","family":"o1-preview","provider":"openai","id":"o1-preview","context_window":128000,"max_output_tokens":32768,"modalities":{"input":["text"],"output":["embeddings","text"]},"capabilities":["function_calling","structured_output"],"pricing":{"text_tokens":{"standard":{"input_per_million":15.0,"cached_input_per_million":7.5,"output_per_million":60.0},"batch":{"input_per_million":null,"output_per_million":null}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"GPT-4o","family":"gpt-4o","provider":"openai","id":"gpt-4o-2024-08-06","context_window":128000,"max_output_tokens":16384,"modalities":{"input":["image","text"],"output":["embeddings","text"]},"capabilities":["batch","function_calling","structured_output"],"pricing":{"text_tokens":{"standard":{"input_per_million":2.5,"cached_input_per_million":1.25,"output_per_million":10.0},"batch":{"input_per_million":1.25,"output_per_million":5.0}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"GPT-4o","family":"gpt-4o","provider":"openai","id":"gpt-4o","context_window":128000,"max_output_tokens":16384,"modalities":{"input":["image","text"],"output":["embeddings","text"]},"capabilities":["batch","function_calling","structured_output"],"pricing":{"text_tokens":{"standard":{"input_per_million":2.5,"cached_input_per_million":1.25,"output_per_million":10.0},"batch":{"input_per_million":1.25,"output_per_million":5.0}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"GPT-4o Audio","family":"gpt-4o-audio-preview","provider":"openai","id":"gpt-4o-audio-preview","context_window":128000,"max_output_tokens":16384,"modalities":{"input":["audio","text"],"output":["audio","embeddings","text"]},"capabilities":["function_calling"],"pricing":{"text_tokens":{"standard":{"input_per_million":2.5,"cached_input_per_million":null,"output_per_million":10.0},"batch":{"input_per_million":null,"output_per_million":null}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"GPT-4o mini","family":"gpt-4o-mini","provider":"openai","id":"gpt-4o-mini-2024-07-18","context_window":128000,"max_output_tokens":16384,"modalities":{"input":["image","text"],"output":["embeddings","text"]},"capabilities":["batch","function_calling","structured_output"],"pricing":{"text_tokens":{"standard":{"input_per_million":0.15,"cached_input_per_million":0.075,"output_per_million":0.6},"batch":{"input_per_million":0.075,"output_per_million":0.3}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"GPT-4o mini","family":"gpt-4o-mini","provider":"openai","id":"gpt-4o-mini","context_window":128000,"max_output_tokens":16384,"modalities":{"input":["image","text"],"output":["embeddings","text"]},"capabilities":["batch","function_calling","structured_output"],"pricing":{"text_tokens":{"standard":{"input_per_million":0.15,"cached_input_per_million":0.075,"output_per_million":0.6},"batch":{"input_per_million":0.075,"output_per_million":0.3}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"GPT-4o Realtime","family":"gpt-4o-realtime-preview","provider":"openai","id":"gpt-4o-realtime-preview","context_window":32000,"max_output_tokens":4096,"modalities":{"input":["audio","text"],"output":["audio","embeddings","text"]},"capabilities":["function_calling"],"pricing":{"text_tokens":{"standard":{"input_per_million":5.0,"cached_input_per_million":2.5,"output_per_million":20.0},"batch":{"input_per_million":null,"output_per_million":null}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"GPT-4 Turbo","family":"gpt-4-turbo","provider":"openai","id":"gpt-4-turbo-2024-04-09","context_window":128000,"max_output_tokens":4096,"modalities":{"input":["image","text"],"output":["embeddings","text"]},"capabilities":["batch","function_calling"],"pricing":{"text_tokens":{"standard":{"input_per_million":10.0,"cached_input_per_million":null,"output_per_million":30.0},"batch":{"input_per_million":5.0,"output_per_million":15.0}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"GPT-4 Turbo","family":"gpt-4-turbo","provider":"openai","id":"gpt-4-turbo","context_window":128000,"max_output_tokens":4096,"modalities":{"input":["image","text"],"output":["embeddings","text"]},"capabilities":["batch","function_calling"],"pricing":{"text_tokens":{"standard":{"input_per_million":10.0,"cached_input_per_million":null,"output_per_million":30.0},"batch":{"input_per_million":5.0,"output_per_million":15.0}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"babbage-002","family":"babbage-002","provider":"openai","id":"babbage-002","context_window":null,"max_output_tokens":16384,"modalities":{"input":["text"],"output":["embeddings","text"]},"capabilities":[],"pricing":{"text_tokens":{"standard":{"input_per_million":0.4,"cached_input_per_million":null,"output_per_million":0.4},"batch":{"input_per_million":null,"output_per_million":null}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"codex-mini-latest","family":"codex-mini-latest","provider":"openai","id":"codex-mini-latest","context_window":200000,"max_output_tokens":100000,"modalities":{"input":["image","text"],"output":["embeddings","text"]},"capabilities":["function_calling","structured_output"],"pricing":{"text_tokens":{"standard":{"input_per_million":1.5,"cached_input_per_million":0.375,"output_per_million":6.0},"batch":{"input_per_million":null,"output_per_million":null}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"DALL·E 2","family":"dall-e-2","provider":"openai","id":"dall-e-2","context_window":null,"max_output_tokens":null,"modalities":{"input":["text"],"output":["embeddings","image"]},"capabilities":[],"pricing":{"text_tokens":{"standard":{"input_per_million":null,"cached_input_per_million":null,"output_per_million":null},"batch":{"input_per_million":null,"output_per_million":null}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"davinci-002","family":"davinci-002","provider":"openai","id":"davinci-002","context_window":null,"max_output_tokens":16384,"modalities":{"input":["text"],"output":["embeddings","text"]},"capabilities":[],"pricing":{"text_tokens":{"standard":{"input_per_million":2.0,"cached_input_per_million":null,"output_per_million":2.0},"batch":{"input_per_million":null,"output_per_million":null}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"GPT-3.5 Turbo","family":"gpt-3.5-turbo","provider":"openai","id":"gpt-3.5-turbo","context_window":16385,"max_output_tokens":4096,"modalities":{"input":["text"],"output":["embeddings","text"]},"capabilities":["batch"],"pricing":{"text_tokens":{"standard":{"input_per_million":0.5,"cached_input_per_million":null,"output_per_million":1.5},"batch":{"input_per_million":0.25,"output_per_million":0.75}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"GPT-4","family":"gpt-4","provider":"openai","id":"gpt-4-0613","context_window":8192,"max_output_tokens":8192,"modalities":{"input":["text"],"output":["embeddings","text"]},"capabilities":["batch"],"pricing":{"text_tokens":{"standard":{"input_per_million":30.0,"cached_input_per_million":null,"output_per_million":60.0},"batch":{"input_per_million":15.0,"output_per_million":30.0}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"GPT-4","family":"gpt-4","provider":"openai","id":"gpt-4","context_window":8192,"max_output_tokens":8192,"modalities":{"input":["text"],"output":["embeddings","text"]},"capabilities":["batch"],"pricing":{"text_tokens":{"standard":{"input_per_million":30.0,"cached_input_per_million":null,"output_per_million":60.0},"batch":{"input_per_million":15.0,"output_per_million":30.0}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"GPT-4 Turbo Preview","family":"gpt-4-turbo-preview","provider":"openai","id":"gpt-4-turbo-preview","context_window":128000,"max_output_tokens":4096,"modalities":{"input":["text"],"output":["embeddings","text"]},"capabilities":[],"pricing":{"text_tokens":{"standard":{"input_per_million":10.0,"cached_input_per_million":null,"output_per_million":30.0},"batch":{"input_per_million":null,"output_per_million":null}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"gpt-4o-transcribe-diarize","family":"gpt-4o-transcribe-diarize","provider":"openai","id":"gpt-4o-transcribe-diarize","context_window":16000,"max_output_tokens":2000,"modalities":{"input":["audio","text"],"output":["embeddings","text"]},"capabilities":[],"pricing":{"text_tokens":{"standard":{"input_per_million":2.5,"cached_input_per_million":null,"output_per_million":10.0},"batch":{"input_per_million":null,"output_per_million":null}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"text-embedding-3-large","family":"text-embedding-3-large","provider":"openai","id":"text-embedding-3-large","context_window":null,"max_output_tokens":null,"modalities":{"input":["text"],"output":["embeddings","text"]},"capabilities":["batch"],"pricing":{"text_tokens":{"standard":{"input_per_million":0.13,"cached_input_per_million":null,"output_per_million":null},"batch":{"input_per_million":0.065,"output_per_million":null}},"embeddings":{"standard":{"input_per_million":0.13},"batch":{"input_per_million":0.065}}}},{"name":"text-embedding-3-small","family":"text-embedding-3-small","provider":"openai","id":"text-embedding-3-small","context_window":null,"max_output_tokens":null,"modalities":{"input":["text"],"output":["embeddings","text"]},"capabilities":["batch","structured_output"],"pricing":{"text_tokens":{"standard":{"input_per_million":0.02,"cached_input_per_million":null,"output_per_million":null},"batch":{"input_per_million":0.01,"output_per_million":null}},"embeddings":{"standard":{"input_per_million":0.02},"batch":{"input_per_million":0.01}}}},{"name":"text-embedding-ada-002","family":"text-embedding-ada-002","provider":"openai","id":"text-embedding-ada-002","context_window":null,"max_output_tokens":null,"modalities":{"input":["text"],"output":["embeddings","text"]},"capabilities":["batch"],"pricing":{"text_tokens":{"standard":{"input_per_million":0.1,"cached_input_per_million":null,"output_per_million":null},"batch":{"input_per_million":0.05,"output_per_million":null}},"embeddings":{"standard":{"input_per_million":0.1},"batch":{"input_per_million":0.05}}}},{"name":"text-moderation","family":"text-moderation-latest","provider":"openai","id":"text-moderation-latest","context_window":null,"max_output_tokens":32768,"modalities":{"input":["text"],"output":["embeddings","text"]},"capabilities":[],"pricing":{"text_tokens":{"standard":{"input_per_million":0.0,"cached_input_per_million":null,"output_per_million":0.0},"batch":{"input_per_million":null,"output_per_million":null}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"text-moderation-stable","family":"text-moderation-stable","provider":"openai","id":"text-moderation-stable","context_window":null,"max_output_tokens":32768,"modalities":{"input":["text"],"output":["embeddings","text"]},"capabilities":[],"pricing":{"text_tokens":{"standard":{"input_per_million":0.0,"cached_input_per_million":null,"output_per_million":0.0},"batch":{"input_per_million":null,"output_per_million":null}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"TTS-1","family":"tts-1","provider":"openai","id":"tts-1","context_window":null,"max_output_tokens":null,"modalities":{"input":["text"],"output":["audio","embeddings"]},"capabilities":[],"pricing":{"text_tokens":{"standard":{"input_per_million":15.0,"cached_input_per_million":null,"output_per_million":null},"batch":{"input_per_million":null,"output_per_million":null}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"TTS-1 HD","family":"tts-1-hd","provider":"openai","id":"tts-1-hd","context_window":null,"max_output_tokens":null,"modalities":{"input":["text"],"output":["audio","embeddings"]},"capabilities":[],"pricing":{"text_tokens":{"standard":{"input_per_million":null,"cached_input_per_million":null,"output_per_million":30.0},"batch":{"input_per_million":null,"output_per_million":null}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}},{"name":"Whisper","family":"whisper-1","provider":"openai","id":"whisper-1","context_window":null,"max_output_tokens":null,"modalities":{"input":["audio"],"output":["audio","embeddings","text"]},"capabilities":[],"pricing":{"text_tokens":{"standard":{"input_per_million":0.006,"cached_input_per_million":null,"output_per_million":null},"batch":{"input_per_million":null,"output_per_million":null}},"embeddings":{"standard":{"input_per_million":null},"batch":{"input_per_million":null}}}}]
         | 
| @@ -0,0 +1,120 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module SwarmSDK
         | 
| 4 | 
            +
              # Models provides model validation and suggestion functionality
         | 
| 5 | 
            +
              #
         | 
| 6 | 
            +
              # Uses static JSON files:
         | 
| 7 | 
            +
              # - models.json: Curated model list from Parsera
         | 
| 8 | 
            +
              # - model_aliases.json: Shortcuts mapping to latest models
         | 
| 9 | 
            +
              #
         | 
| 10 | 
            +
              # This avoids network calls, API key requirements, and RubyLLM
         | 
| 11 | 
            +
              # registry manipulation.
         | 
| 12 | 
            +
              #
         | 
| 13 | 
            +
              # @example
         | 
| 14 | 
            +
              #   model = SwarmSDK::Models.find("claude-sonnet-4-5-20250929")
         | 
| 15 | 
            +
              #   model = SwarmSDK::Models.find("sonnet")  # Uses alias
         | 
| 16 | 
            +
              #   suggestions = SwarmSDK::Models.suggest_similar("anthropic:claude-sonnet-4-5")
         | 
| 17 | 
            +
              class Models
         | 
| 18 | 
            +
                MODELS_JSON_PATH = File.expand_path("models.json", __dir__)
         | 
| 19 | 
            +
                ALIASES_JSON_PATH = File.expand_path("model_aliases.json", __dir__)
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                class << self
         | 
| 22 | 
            +
                  # Find a model by ID or alias
         | 
| 23 | 
            +
                  #
         | 
| 24 | 
            +
                  # @param model_id [String] Model ID or alias to find
         | 
| 25 | 
            +
                  # @return [Hash, nil] Model data or nil if not found
         | 
| 26 | 
            +
                  def find(model_id)
         | 
| 27 | 
            +
                    # Check if it's an alias first
         | 
| 28 | 
            +
                    resolved_id = resolve_alias(model_id)
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                    all.find { |m| m["id"] == resolved_id || m[:id] == resolved_id }
         | 
| 31 | 
            +
                  end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                  # Resolve a model alias to full model ID
         | 
| 34 | 
            +
                  #
         | 
| 35 | 
            +
                  # @param model_id [String] Model ID or alias
         | 
| 36 | 
            +
                  # @return [String] Resolved model ID (or original if not an alias)
         | 
| 37 | 
            +
                  def resolve_alias(model_id)
         | 
| 38 | 
            +
                    aliases[model_id.to_s] || model_id
         | 
| 39 | 
            +
                  end
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                  # Suggest similar models for a given query
         | 
| 42 | 
            +
                  #
         | 
| 43 | 
            +
                  # Strips provider prefixes and normalizes for fuzzy matching.
         | 
| 44 | 
            +
                  #
         | 
| 45 | 
            +
                  # @param query [String] Model ID to match against
         | 
| 46 | 
            +
                  # @param limit [Integer] Maximum number of suggestions
         | 
| 47 | 
            +
                  # @return [Array<Hash>] Up to `limit` similar models
         | 
| 48 | 
            +
                  def suggest_similar(query, limit: 3)
         | 
| 49 | 
            +
                    # Strip provider prefix (e.g., "anthropic:claude-sonnet-4-5" → "claude-sonnet-4-5")
         | 
| 50 | 
            +
                    query_without_prefix = query.to_s.sub(/^[^:]+:/, "")
         | 
| 51 | 
            +
                    normalized_query = query_without_prefix.downcase.gsub(/[.\-_]/, "")
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                    matches = all.select do |model|
         | 
| 54 | 
            +
                      model_id = (model["id"] || model[:id]).to_s
         | 
| 55 | 
            +
                      model_name = (model["name"] || model[:name]).to_s
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                      normalized_id = model_id.downcase.gsub(/[.\-_]/, "")
         | 
| 58 | 
            +
                      normalized_name = model_name.downcase.gsub(/[.\-_]/, "")
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                      normalized_id.include?(normalized_query) || normalized_name.include?(normalized_query)
         | 
| 61 | 
            +
                    end.first(limit)
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                    matches.map do |m|
         | 
| 64 | 
            +
                      {
         | 
| 65 | 
            +
                        id: m["id"] || m[:id],
         | 
| 66 | 
            +
                        name: m["name"] || m[:name],
         | 
| 67 | 
            +
                        context_window: m["context_window"] || m[:context_window],
         | 
| 68 | 
            +
                      }
         | 
| 69 | 
            +
                    end
         | 
| 70 | 
            +
                  end
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                  # Get all models
         | 
| 73 | 
            +
                  #
         | 
| 74 | 
            +
                  # @return [Array<Hash>] All models from models.json
         | 
| 75 | 
            +
                  def all
         | 
| 76 | 
            +
                    @models ||= load_models
         | 
| 77 | 
            +
                  end
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                  # Get all aliases
         | 
| 80 | 
            +
                  #
         | 
| 81 | 
            +
                  # @return [Hash] Alias mappings
         | 
| 82 | 
            +
                  def aliases
         | 
| 83 | 
            +
                    @aliases ||= load_aliases
         | 
| 84 | 
            +
                  end
         | 
| 85 | 
            +
             | 
| 86 | 
            +
                  # Reload models and aliases from JSON files
         | 
| 87 | 
            +
                  #
         | 
| 88 | 
            +
                  # @return [Array<Hash>] Loaded models
         | 
| 89 | 
            +
                  def reload!
         | 
| 90 | 
            +
                    @models = load_models
         | 
| 91 | 
            +
                    @aliases = load_aliases
         | 
| 92 | 
            +
                    @models
         | 
| 93 | 
            +
                  end
         | 
| 94 | 
            +
             | 
| 95 | 
            +
                  private
         | 
| 96 | 
            +
             | 
| 97 | 
            +
                  # Load models from JSON file
         | 
| 98 | 
            +
                  #
         | 
| 99 | 
            +
                  # @return [Array<Hash>] Models array
         | 
| 100 | 
            +
                  def load_models
         | 
| 101 | 
            +
                    JSON.parse(File.read(MODELS_JSON_PATH))
         | 
| 102 | 
            +
                  rescue StandardError => e
         | 
| 103 | 
            +
                    # Log error and return empty array
         | 
| 104 | 
            +
                    RubyLLM.logger.error("Failed to load SwarmSDK models.json: #{e.class} - #{e.message}")
         | 
| 105 | 
            +
                    []
         | 
| 106 | 
            +
                  end
         | 
| 107 | 
            +
             | 
| 108 | 
            +
                  # Load aliases from JSON file
         | 
| 109 | 
            +
                  #
         | 
| 110 | 
            +
                  # @return [Hash] Alias mappings
         | 
| 111 | 
            +
                  def load_aliases
         | 
| 112 | 
            +
                    JSON.parse(File.read(ALIASES_JSON_PATH))
         | 
| 113 | 
            +
                  rescue StandardError => e
         | 
| 114 | 
            +
                    # Log error and return empty hash
         | 
| 115 | 
            +
                    RubyLLM.logger.debug("Failed to load SwarmSDK model_aliases.json: #{e.class} - #{e.message}")
         | 
| 116 | 
            +
                    {}
         | 
| 117 | 
            +
                  end
         | 
| 118 | 
            +
                end
         | 
| 119 | 
            +
              end
         | 
| 120 | 
            +
            end
         | 
| @@ -94,16 +94,19 @@ module SwarmSDK | |
| 94 94 | 
             
                  #     You build APIs.
         | 
| 95 95 | 
             
                  #   MD
         | 
| 96 96 | 
             
                  def agent(name, content = nil, &block)
         | 
| 97 | 
            -
                    # Case 1: agent :name, <<~MD ( | 
| 98 | 
            -
                    if content.is_a?(String) &&  | 
| 97 | 
            +
                    # Case 1: agent :name, <<~MD do ... end (markdown + overrides)
         | 
| 98 | 
            +
                    if content.is_a?(String) && block_given? && markdown_content?(content)
         | 
| 99 | 
            +
                      load_agent_from_markdown_with_overrides(content, name, &block)
         | 
| 100 | 
            +
                    # Case 2: agent :name, <<~MD (markdown only)
         | 
| 101 | 
            +
                    elsif content.is_a?(String) && !block_given? && markdown_content?(content)
         | 
| 99 102 | 
             
                      load_agent_from_markdown(content, name)
         | 
| 100 | 
            -
                    # Case  | 
| 103 | 
            +
                    # Case 3: agent :name do ... end (inline DSL)
         | 
| 101 104 | 
             
                    elsif block_given?
         | 
| 102 105 | 
             
                      builder = Agent::Builder.new(name)
         | 
| 103 106 | 
             
                      builder.instance_eval(&block)
         | 
| 104 107 | 
             
                      @agents[name] = builder
         | 
| 105 108 | 
             
                    else
         | 
| 106 | 
            -
                      raise ArgumentError, "Invalid agent definition. Use: agent :name { ... } OR agent :name, <<~MD ... MD"
         | 
| 109 | 
            +
                      raise ArgumentError, "Invalid agent definition. Use: agent :name { ... } OR agent :name, <<~MD ... MD OR agent :name, <<~MD do ... end"
         | 
| 107 110 | 
             
                    end
         | 
| 108 111 | 
             
                  end
         | 
| 109 112 |  | 
| @@ -225,6 +228,76 @@ module SwarmSDK | |
| 225 228 | 
             
                    @agents[definition.name] = { __file_config__: definition.to_h }
         | 
| 226 229 | 
             
                  end
         | 
| 227 230 |  | 
| 231 | 
            +
                  # Load an agent from markdown content with DSL overrides
         | 
| 232 | 
            +
                  #
         | 
| 233 | 
            +
                  # This allows loading from a file and then overriding specific settings:
         | 
| 234 | 
            +
                  #   agent :reviewer, File.read("reviewer.md") do
         | 
| 235 | 
            +
                  #     provider :openai
         | 
| 236 | 
            +
                  #     model "gpt-4o"
         | 
| 237 | 
            +
                  #   end
         | 
| 238 | 
            +
                  #
         | 
| 239 | 
            +
                  # @param content [String] Markdown content with frontmatter
         | 
| 240 | 
            +
                  # @param name_override [Symbol, nil] Optional name to override frontmatter name
         | 
| 241 | 
            +
                  # @yield Block with DSL overrides
         | 
| 242 | 
            +
                  # @return [void]
         | 
| 243 | 
            +
                  def load_agent_from_markdown_with_overrides(content, name_override = nil, &block)
         | 
| 244 | 
            +
                    # Parse markdown content first
         | 
| 245 | 
            +
                    definition = MarkdownParser.parse(content, name_override)
         | 
| 246 | 
            +
             | 
| 247 | 
            +
                    # Create a builder with the markdown config
         | 
| 248 | 
            +
                    builder = Agent::Builder.new(definition.name)
         | 
| 249 | 
            +
             | 
| 250 | 
            +
                    # Apply markdown settings to builder (these become the base)
         | 
| 251 | 
            +
                    apply_definition_to_builder(builder, definition.to_h)
         | 
| 252 | 
            +
             | 
| 253 | 
            +
                    # Apply DSL overrides (these override the markdown settings)
         | 
| 254 | 
            +
                    builder.instance_eval(&block)
         | 
| 255 | 
            +
             | 
| 256 | 
            +
                    # Store the builder (not file config) so overrides are preserved
         | 
| 257 | 
            +
                    @agents[definition.name] = builder
         | 
| 258 | 
            +
                  end
         | 
| 259 | 
            +
             | 
| 260 | 
            +
                  # Apply agent definition hash to a builder
         | 
| 261 | 
            +
                  #
         | 
| 262 | 
            +
                  # @param builder [Agent::Builder] Builder to configure
         | 
| 263 | 
            +
                  # @param config [Hash] Configuration hash from definition
         | 
| 264 | 
            +
                  # @return [void]
         | 
| 265 | 
            +
                  def apply_definition_to_builder(builder, config)
         | 
| 266 | 
            +
                    builder.description(config[:description]) if config[:description]
         | 
| 267 | 
            +
                    builder.model(config[:model]) if config[:model]
         | 
| 268 | 
            +
                    builder.provider(config[:provider]) if config[:provider]
         | 
| 269 | 
            +
                    builder.base_url(config[:base_url]) if config[:base_url]
         | 
| 270 | 
            +
                    builder.api_version(config[:api_version]) if config[:api_version]
         | 
| 271 | 
            +
                    builder.context_window(config[:context_window]) if config[:context_window]
         | 
| 272 | 
            +
                    builder.system_prompt(config[:system_prompt]) if config[:system_prompt]
         | 
| 273 | 
            +
                    builder.directory(config[:directory]) if config[:directory]
         | 
| 274 | 
            +
                    builder.timeout(config[:timeout]) if config[:timeout]
         | 
| 275 | 
            +
                    builder.parameters(config[:parameters]) if config[:parameters]
         | 
| 276 | 
            +
                    builder.headers(config[:headers]) if config[:headers]
         | 
| 277 | 
            +
                    builder.coding_agent(config[:coding_agent]) unless config[:coding_agent].nil?
         | 
| 278 | 
            +
                    # Don't apply assume_model_exists from markdown - let DSL overrides or auto-enable handle it
         | 
| 279 | 
            +
                    # builder.assume_model_exists(config[:assume_model_exists]) unless config[:assume_model_exists].nil?
         | 
| 280 | 
            +
                    builder.bypass_permissions(config[:bypass_permissions]) if config[:bypass_permissions]
         | 
| 281 | 
            +
                    builder.include_default_tools(config[:include_default_tools]) unless config[:include_default_tools].nil?
         | 
| 282 | 
            +
             | 
| 283 | 
            +
                    # Add tools from markdown
         | 
| 284 | 
            +
                    if config[:tools]&.any?
         | 
| 285 | 
            +
                      # Extract tool names from the tools array (which may be hashes with permissions)
         | 
| 286 | 
            +
                      tool_names = config[:tools].map do |tool|
         | 
| 287 | 
            +
                        tool.is_a?(Hash) ? tool[:name] : tool
         | 
| 288 | 
            +
                      end
         | 
| 289 | 
            +
                      builder.tools(*tool_names)
         | 
| 290 | 
            +
                    end
         | 
| 291 | 
            +
             | 
| 292 | 
            +
                    # Add delegates_to
         | 
| 293 | 
            +
                    builder.delegates_to(*config[:delegates_to]) if config[:delegates_to]&.any?
         | 
| 294 | 
            +
             | 
| 295 | 
            +
                    # Add MCP servers
         | 
| 296 | 
            +
                    config[:mcp_servers]&.each do |server|
         | 
| 297 | 
            +
                      builder.mcp_server(server[:name], **server.except(:name))
         | 
| 298 | 
            +
                    end
         | 
| 299 | 
            +
                  end
         | 
| 300 | 
            +
             | 
| 228 301 | 
             
                  # Build a traditional single-swarm execution
         | 
| 229 302 | 
             
                  #
         | 
| 230 303 | 
             
                  # @return [Swarm] Configured swarm instance
         | 
    
        data/lib/swarm_sdk/version.rb
    CHANGED
    
    
    
        data/lib/swarm_sdk.rb
    CHANGED
    
    | @@ -41,36 +41,6 @@ module SwarmSDK | |
| 41 41 | 
             
              class StateError < Error; end
         | 
| 42 42 |  | 
| 43 43 | 
             
              class << self
         | 
| 44 | 
            -
                # Refresh RubyLLM model registry silently (without log output)
         | 
| 45 | 
            -
                #
         | 
| 46 | 
            -
                # By default, RubyLLM.models.refresh! outputs INFO level logs about
         | 
| 47 | 
            -
                # fetching models from providers. This method temporarily raises the
         | 
| 48 | 
            -
                # log level to suppress those messages, which is useful for CLI tools
         | 
| 49 | 
            -
                # that want clean output.
         | 
| 50 | 
            -
                #
         | 
| 51 | 
            -
                # If model refresh fails (e.g., missing API keys, invalid keys, network
         | 
| 52 | 
            -
                # unavailable), the error is silently caught and execution continues
         | 
| 53 | 
            -
                # using the bundled models.json. This allows SwarmSDK to work offline
         | 
| 54 | 
            -
                # and with dummy keys for local proxies.
         | 
| 55 | 
            -
                #
         | 
| 56 | 
            -
                # @example
         | 
| 57 | 
            -
                #   SwarmSDK.refresh_models_silently
         | 
| 58 | 
            -
                #
         | 
| 59 | 
            -
                # @return [void]
         | 
| 60 | 
            -
                def refresh_models_silently
         | 
| 61 | 
            -
                  original_level = RubyLLM.logger.level
         | 
| 62 | 
            -
                  RubyLLM.logger.level = Logger::ERROR
         | 
| 63 | 
            -
             | 
| 64 | 
            -
                  RubyLLM.models.refresh!
         | 
| 65 | 
            -
                rescue StandardError => e
         | 
| 66 | 
            -
                  # Silently ignore all refresh failures
         | 
| 67 | 
            -
                  # Models will use bundled models.json instead
         | 
| 68 | 
            -
                  RubyLLM.logger.debug("Model refresh skipped: #{e.class} - #{e.message}")
         | 
| 69 | 
            -
                  nil
         | 
| 70 | 
            -
                ensure
         | 
| 71 | 
            -
                  RubyLLM.logger.level = original_level
         | 
| 72 | 
            -
                end
         | 
| 73 | 
            -
             | 
| 74 44 | 
             
                # Main entry point for DSL
         | 
| 75 45 | 
             
                def build(&block)
         | 
| 76 46 | 
             
                  Swarm::Builder.build(&block)
         | 
    
        metadata
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: swarm_sdk
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 2.0. | 
| 4 | 
            +
              version: 2.0.2
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Paulo Arruda
         | 
| @@ -86,6 +86,7 @@ files: | |
| 86 86 | 
             
            - lib/swarm_sdk/agent/chat/system_reminder_injector.rb
         | 
| 87 87 | 
             
            - lib/swarm_sdk/agent/context.rb
         | 
| 88 88 | 
             
            - lib/swarm_sdk/agent/definition.rb
         | 
| 89 | 
            +
            - lib/swarm_sdk/claude_code_agent_adapter.rb
         | 
| 89 90 | 
             
            - lib/swarm_sdk/configuration.rb
         | 
| 90 91 | 
             
            - lib/swarm_sdk/context_compactor.rb
         | 
| 91 92 | 
             
            - lib/swarm_sdk/context_compactor/metrics.rb
         | 
| @@ -103,6 +104,9 @@ files: | |
| 103 104 | 
             
            - lib/swarm_sdk/log_collector.rb
         | 
| 104 105 | 
             
            - lib/swarm_sdk/log_stream.rb
         | 
| 105 106 | 
             
            - lib/swarm_sdk/markdown_parser.rb
         | 
| 107 | 
            +
            - lib/swarm_sdk/model_aliases.json
         | 
| 108 | 
            +
            - lib/swarm_sdk/models.json
         | 
| 109 | 
            +
            - lib/swarm_sdk/models.rb
         | 
| 106 110 | 
             
            - lib/swarm_sdk/node/agent_config.rb
         | 
| 107 111 | 
             
            - lib/swarm_sdk/node/builder.rb
         | 
| 108 112 | 
             
            - lib/swarm_sdk/node/transformer_executor.rb
         |