Class: Aidp::Providers::Anthropic

Inherits:
Base
  • Object
show all
Includes:
DebugMixin
Defined in:
lib/aidp/providers/anthropic.rb

Constant Summary collapse

MODEL_PATTERN =

Model name pattern for Anthropic Claude models

/^claude-[\d.-]+-(?:opus|sonnet|haiku)(?:-\d{8})?$/i

Constants included from DebugMixin

DebugMixin::DEBUG_BASIC, DebugMixin::DEBUG_OFF, DebugMixin::DEBUG_VERBOSE

Constants inherited from Base

Base::ACTIVITY_STATES, Base::DEFAULT_STUCK_TIMEOUT, Base::TIER_TIMEOUT_MULTIPLIERS, Base::TIMEOUT_ARCHITECTURE_ANALYSIS, Base::TIMEOUT_DEFAULT, Base::TIMEOUT_DOCUMENTATION_ANALYSIS, Base::TIMEOUT_FUNCTIONALITY_ANALYSIS, Base::TIMEOUT_IMPLEMENTATION, Base::TIMEOUT_QUICK_MODE, Base::TIMEOUT_REFACTORING_RECOMMENDATIONS, Base::TIMEOUT_REPOSITORY_ANALYSIS, Base::TIMEOUT_STATIC_ANALYSIS, Base::TIMEOUT_TEST_ANALYSIS

Constants included from MessageDisplay

MessageDisplay::COLOR_MAP

Instance Attribute Summary collapse

Attributes inherited from Base

#activity_state, #last_activity_time, #start_time, #step_name, #stuck_timeout

Class Method Summary collapse

Instance Method Summary collapse

Methods included from DebugMixin

#debug_basic?, #debug_command, #debug_enabled?, #debug_error, #debug_execute_command, #debug_level, #debug_log, #debug_logger, #debug_provider, #debug_step, #debug_timing, #debug_verbose?, included, shared_logger

Methods inherited from Base

#activity_summary, #configure, discover_models_from_registry, #execution_time, #harness_config, #harness_health_status, #harness_healthy?, #harness_metrics, #harness_mode?, #initialize, #mark_completed, #mark_failed, #record_activity, #record_harness_request, #send_with_harness, #set_harness_context, #set_job_context, #setup_activity_monitoring, #stuck?, #supports_activity_monitoring?, #time_since_last_activity, #update_activity_state

Methods included from Adapter

#classify_error, #dangerous_mode=, #dangerous_mode_enabled?, #error_metadata, #health_status, #logging_metadata, #redact_secrets, #retryable_error?, #validate_config

Methods included from MessageDisplay

#display_message, included, #message_display_prompt

Constructor Details

This class inherits a constructor from Aidp::Providers::Base

Instance Attribute Details

#modelObject (readonly)

Returns the value of attribute model.



13
14
15
# File 'lib/aidp/providers/anthropic.rb', line 13

def model
  @model
end

Class Method Details

.available?Boolean

Returns:

  • (Boolean)


23
24
25
# File 'lib/aidp/providers/anthropic.rb', line 23

def self.available?
  !!Aidp::Util.which("claude")
end

.check_model_deprecation(model_name) ⇒ String?

Check if a model is deprecated and return replacement

Parameters:

  • model_name (String)

    The model name to check

Returns:

  • (String, nil)

    Replacement model name if deprecated, nil otherwise



316
317
318
# File 'lib/aidp/providers/anthropic.rb', line 316

def self.check_model_deprecation(model_name)
  deprecation_cache.replacement_for(provider: "anthropic", model_id: model_name)
end

.classify_provider_error(error_message) ⇒ Hash

Classify provider error using string matching

ZFC EXCEPTION: Cannot use AI to classify provider errors because:

  1. The failing provider IS the AI we’d use for classification (circular dependency)

  2. Provider may be rate-limited, down, or misconfigured

  3. Error classification must work even when AI unavailable

This is a legitimate exception to ZFC principles per LLM_STYLE_GUIDE: “Structural safety checks” are allowed in code when AI cannot be used.

Parameters:

  • error_message (String)

    The error message to classify

Returns:

  • (Hash)

    Classification result with :type, :is_deprecation, :is_rate_limit, :confidence



233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
# File 'lib/aidp/providers/anthropic.rb', line 233

def self.classify_provider_error(error_message)
  msg_lower = error_message.downcase

  # Use simple string.include? checks (not regex) to avoid ReDoS vulnerabilities
  is_rate_limit = msg_lower.include?("rate limit") || msg_lower.include?("session limit")
  is_deprecation = msg_lower.include?("deprecat") || msg_lower.include?("end-of-life")
  is_auth_error = msg_lower.include?("auth") && (msg_lower.include?("expired") || msg_lower.include?("invalid"))

  # Determine primary type
  type = if is_rate_limit
    "rate_limit"
  elsif is_deprecation
    "deprecation"
  elsif is_auth_error
    "auth_error"
  else
    "other"
  end

  Aidp.log_debug("anthropic", "Provider error classification",
    type: type,
    is_rate_limit: is_rate_limit,
    is_deprecation: is_deprecation,
    is_auth_error: is_auth_error)

  {
    type: type,
    is_rate_limit: is_rate_limit,
    is_deprecation: is_deprecation,
    is_auth_error: is_auth_error,
    confidence: 0.85, # Good confidence for clear error messages
    reasoning: "Pattern-based classification (ZFC exception: circular dependency)"
  }
end

.deprecation_cacheObject

Get deprecation cache instance (lazy loaded)



19
20
21
# File 'lib/aidp/providers/anthropic.rb', line 19

def self.deprecation_cache
  @deprecation_cache ||= Aidp::Harness::DeprecationCache.new
end

.discover_modelsArray<Hash>

Discover available models from Claude CLI

Returns:

  • (Array<Hash>)

    Array of discovered models



60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/aidp/providers/anthropic.rb', line 60

def self.discover_models
  return [] unless available?

  begin
    require "open3"
    output, _, status = Open3.capture3("claude", "models", "list", {timeout: 10})
    return [] unless status.success?

    parse_models_list(output)
  rescue => e
    Aidp.log_debug("anthropic_provider", "discovery failed", error: e.message)
    []
  end
end

.find_replacement_model(deprecated_model, provider_name: "anthropic") ⇒ String?

Find replacement model for deprecated one using RubyLLM registry

Parameters:

  • deprecated_model (String)

    The deprecated model name

  • provider_name (String) (defaults to: "anthropic")

    Provider name for registry lookup

Returns:

  • (String, nil)

    Latest model in the same family, or configured replacement



324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
# File 'lib/aidp/providers/anthropic.rb', line 324

def self.find_replacement_model(deprecated_model, provider_name: "anthropic")
  # First check the deprecation cache for explicit replacement
  replacement = deprecation_cache.replacement_for(provider: provider_name, model_id: deprecated_model)
  return replacement if replacement

  # Try to find latest model in same family using registry
  require_relative "../harness/ruby_llm_registry" unless defined?(Aidp::Harness::RubyLLMRegistry)

  begin
    registry = Aidp::Harness::RubyLLMRegistry.new

    # Search for non-deprecated models in the same family
    # Prefer models without "latest" suffix, sorted by ID (newer dates first)
    models = registry.models_for_provider(provider_name)
    candidates = models.select { |m| m.include?("sonnet") && !deprecation_cache.deprecated?(provider: provider_name, model_id: m) }

    # Prioritize: versioned models over -latest, newer versions first
    versioned = candidates.reject { |m| m.end_with?("-latest") }.sort.reverse
    latest_models = candidates.select { |m| m.end_with?("-latest") }

    replacement = versioned.first || latest_models.first

    if replacement
      Aidp.log_info("anthropic", "Found replacement model",
        deprecated: deprecated_model,
        replacement: replacement)
    end

    replacement
  rescue => e
    Aidp.log_error("anthropic", "Failed to find replacement model",
      deprecated: deprecated_model,
      error: e.message)
    nil
  end
end

.firewall_requirementsObject

Get firewall requirements for Anthropic provider



76
77
78
79
80
81
82
83
84
85
# File 'lib/aidp/providers/anthropic.rb', line 76

def self.firewall_requirements
  {
    domains: [
      "api.anthropic.com",
      "claude.ai",
      "console.anthropic.com"
    ],
    ip_ranges: []
  }
end

.model_family(provider_model_name) ⇒ String

Normalize a provider-specific model name to its model family

Anthropic uses date-versioned models (e.g., “claude-3-5-sonnet-20241022”). This method strips the date suffix to get the family name.

Parameters:

  • provider_model_name (String)

    The versioned model name

Returns:

  • (String)

    The model family name (e.g., “claude-3-5-sonnet”)



34
35
36
37
# File 'lib/aidp/providers/anthropic.rb', line 34

def self.model_family(provider_model_name)
  # Strip date suffix: "claude-3-5-sonnet-20241022" → "claude-3-5-sonnet"
  provider_model_name.sub(/-\d{8}$/, "")
end

.provider_model_name(family_name) ⇒ String

Convert a model family name to the provider’s preferred model name

Returns the family name as-is. Users can configure specific versions in aidp.yml.

Parameters:

  • family_name (String)

    The model family name

Returns:

  • (String)

    The model name (same as family for flexibility)



45
46
47
# File 'lib/aidp/providers/anthropic.rb', line 45

def self.provider_model_name(family_name)
  family_name
end

.supports_model_family?(family_name) ⇒ Boolean

Check if this provider supports a given model family

Parameters:

  • family_name (String)

    The model family name

Returns:

  • (Boolean)

    True if it matches Claude model pattern



53
54
55
# File 'lib/aidp/providers/anthropic.rb', line 53

def self.supports_model_family?(family_name)
  MODEL_PATTERN.match?(family_name)
end

Instance Method Details

#available?Boolean

Returns:

  • (Boolean)


196
197
198
# File 'lib/aidp/providers/anthropic.rb', line 196

def available?
  self.class.available?
end

#capabilitiesObject

ProviderAdapter interface methods



202
203
204
205
206
207
208
209
210
211
# File 'lib/aidp/providers/anthropic.rb', line 202

def capabilities
  {
    reasoning_tiers: ["mini", "standard", "thinking"],
    context_window: 200_000,
    supports_json_mode: true,
    supports_tool_use: true,
    supports_vision: false,
    supports_file_upload: true
  }
end

#dangerous_mode_flagsObject



217
218
219
# File 'lib/aidp/providers/anthropic.rb', line 217

def dangerous_mode_flags
  ["--dangerously-skip-permissions"]
end

#display_nameObject



173
174
175
# File 'lib/aidp/providers/anthropic.rb', line 173

def display_name
  "Anthropic Claude CLI"
end

#error_patternsObject

Error patterns for classify_error method (legacy support) NOTE: ZFC-based classification preferred - see classify_error_with_zfc These patterns serve as fallback when ZFC is unavailable



271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
# File 'lib/aidp/providers/anthropic.rb', line 271

def error_patterns
  {
    rate_limited: [
      /rate.?limit/i,
      /too.?many.?requests/i,
      /429/,
      /overloaded/i
    ],
    auth_expired: [
      /oauth.*token.*expired/i,
      /authentication.*error/i,
      /invalid.*api.*key/i,
      /unauthorized/i,
      /401/
    ],
    quota_exceeded: [
      /quota.*exceeded/i,
      /usage.*limit/i,
      /credit.*exhausted/i
    ],
    transient: [
      /timeout/i,
      /connection.*reset/i,
      /temporary.*error/i,
      /service.*unavailable/i,
      /503/,
      /502/,
      /504/
    ],
    permanent: [
      /invalid.*model/i,
      /unsupported.*operation/i,
      /not.*found/i,
      /404/,
      /bad.*request/i,
      /400/,
      /model.*deprecated/i,
      /end-of-life/i
    ]
  }
end

#fetch_mcp_serversObject



181
182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/aidp/providers/anthropic.rb', line 181

def fetch_mcp_servers
  return [] unless self.class.available?

  begin
    # Use claude mcp list command
    result = debug_execute_command("claude", args: ["mcp", "list"], timeout: 5)
    return [] unless result.exit_status == 0

    parse_claude_mcp_output(result.out)
  rescue => e
    debug_log("Failed to fetch MCP servers via Claude CLI: #{e.message}", level: :debug)
    []
  end
end

#nameObject



169
170
171
# File 'lib/aidp/providers/anthropic.rb', line 169

def name
  "anthropic"
end

#send_message(prompt:, session: nil, options: {}) ⇒ Object



361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
# File 'lib/aidp/providers/anthropic.rb', line 361

def send_message(prompt:, session: nil, options: {})
  raise "claude CLI not available" unless self.class.available?

  # Check if current model is deprecated and warn
  if @model && (replacement = self.class.check_model_deprecation(@model))
    Aidp.log_warn("anthropic", "Using deprecated model",
      current: @model,
      replacement: replacement)
    debug_log("⚠️  Model #{@model} is deprecated. Consider upgrading to #{replacement}", level: :warn)
  end

  # Smart timeout calculation with tier awareness
  timeout_seconds = calculate_timeout(options)

  debug_provider("claude", "Starting execution", {timeout: timeout_seconds, tier: options[:tier]})
  debug_log("📝 Sending prompt to claude...", level: :info)

  # Build command arguments
  args = ["--print", "--output-format=text"]

  # Add model if specified
  if @model && !@model.empty?
    args << "--model" << @model
  end

  # Check if we should skip permissions (devcontainer support)
  if should_skip_permissions?
    args << "--dangerously-skip-permissions"
    debug_log("🔓 Running with elevated permissions (devcontainer mode)", level: :info)
  end

  begin
    result = debug_execute_command("claude", args: args, input: prompt, timeout: timeout_seconds)

    # Log the results
    debug_command("claude", args: args, input: prompt, output: result.out, error: result.err, exit_code: result.exit_status)

    if result.exit_status == 0
      result.out
    else
      # Detect issues in stdout/stderr (Claude sometimes prints to stdout)
      combined = [result.out, result.err].compact.join("\n")

      # Classify provider error using pattern matching
      # ZFC EXCEPTION: Cannot use AI to classify provider's own errors (circular dependency)
      error_classification = self.class.classify_provider_error(combined)

      Aidp.log_debug("anthropic_provider", "error_classified",
        exit_code: result.exit_status,
        type: error_classification[:type],
        confidence: error_classification[:confidence])

      # Check for model deprecation FIRST (before rate limiting)
      # Even if rate limited, we need to cache the deprecation for next run
      if error_classification[:is_deprecation]
        deprecated_model = @model
        Aidp.log_error("anthropic", "Model deprecation detected",
          model: deprecated_model,
          message: combined)

        # Try to find replacement
        replacement = deprecated_model ? self.class.find_replacement_model(deprecated_model) : nil

        # Record deprecation in cache for future runs
        if replacement
          self.class.deprecation_cache.add_deprecated_model(
            provider: "anthropic",
            model_id: deprecated_model,
            replacement: replacement,
            reason: combined.lines.first&.strip || "Model deprecated"
          )

          Aidp.log_info("anthropic", "Auto-upgrading to non-deprecated model",
            old_model: deprecated_model,
            new_model: replacement)
          debug_log("🔄 Upgrading from deprecated model #{deprecated_model} to #{replacement}", level: :info)

          # Update model and retry
          @model = replacement

          # Retry with new model (even if rate limited, we'll hit rate limit with new model)
          debug_log("🔄 Retrying with upgraded model: #{replacement}", level: :info)
          return send_message(prompt: prompt, session: session, options: options)
        else
          # Record deprecation even without replacement
          if deprecated_model
            self.class.deprecation_cache.add_deprecated_model(
              provider: "anthropic",
              model_id: deprecated_model,
              replacement: nil,
              reason: combined.lines.first&.strip || "Model deprecated"
            )
          end

          error_message = "Model '#{deprecated_model}' is deprecated and no replacement found.\n#{combined}"
          error = RuntimeError.new(error_message)
          debug_error(error, {exit_code: result.exit_status, stdout: result.out, stderr: result.err})
          raise error
        end
      end

      # Check for rate limit (after handling deprecation)
      if error_classification[:is_rate_limit]
        Aidp.log_debug("anthropic_provider", "rate_limit_detected",
          exit_code: result.exit_status,
          confidence: error_classification[:confidence],
          message: combined)
        notify_rate_limit(combined)
        error_message = "Rate limit reached for Claude CLI.\n#{combined}"
        error = RuntimeError.new(error_message)
        debug_error(error, {exit_code: result.exit_status, stdout: result.out, stderr: result.err})
        raise error
      end

      # Check for auth issues
      if combined.downcase.include?("oauth token has expired") || combined.downcase.include?("authentication_error")
        error_message = "Authentication error from Claude CLI: token expired or invalid.\n" \
                        "Run 'claude /login' or refresh credentials.\n" \
                        "Note: Model discovery requires valid authentication."
        error = RuntimeError.new(error_message)
        debug_error(error, {exit_code: result.exit_status, stdout: result.out, stderr: result.err})
        raise error
      end

      error_message = "claude failed with exit code #{result.exit_status}: #{result.err}"
      error = RuntimeError.new(error_message)
      debug_error(error, {exit_code: result.exit_status, stderr: result.err})
      raise error
    end
  rescue => e
    debug_error(e, {provider: "claude", prompt_length: prompt.length})
    raise
  end
end

#supports_dangerous_mode?Boolean

Returns:

  • (Boolean)


213
214
215
# File 'lib/aidp/providers/anthropic.rb', line 213

def supports_dangerous_mode?
  true
end

#supports_mcp?Boolean

Returns:

  • (Boolean)


177
178
179
# File 'lib/aidp/providers/anthropic.rb', line 177

def supports_mcp?
  true
end