ruby_llm_mesh 0.1.0

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.
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "net/http"
5
+ require "uri"
6
+
7
+ module RubyLlmMesh
8
+ module Providers
9
+ class Base
10
+ attr_reader :config
11
+
12
+ def initialize(config = RubyLlmMesh.configuration)
13
+ @config = config
14
+ end
15
+
16
+ def name
17
+ raise NotImplementedError
18
+ end
19
+
20
+ def complete(prompt:, system: nil, model: nil, **options)
21
+ raise NotImplementedError
22
+ end
23
+
24
+ protected
25
+
26
+ def http_post(url, headers:, body:, timeout: nil)
27
+ uri = URI(url)
28
+ http = Net::HTTP.new(uri.host, uri.port)
29
+ http.use_ssl = uri.scheme == "https"
30
+ http.open_timeout = timeout || config.timeout
31
+ http.read_timeout = timeout || config.timeout
32
+
33
+ request = Net::HTTP::Post.new(uri)
34
+ headers.each { |k, v| request[k] = v }
35
+ request.body = JSON.generate(body)
36
+
37
+ started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
38
+ response = http.request(request)
39
+ latency_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000).round
40
+
41
+ [response, latency_ms]
42
+ rescue Net::OpenTimeout, Net::ReadTimeout, Timeout::Error => e
43
+ raise TimeoutError.new(e.message, provider: name)
44
+ rescue SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ETIMEDOUT => e
45
+ raise ProviderError.new(e.message, provider: name)
46
+ end
47
+
48
+ def parse_json(response)
49
+ JSON.parse(response.body)
50
+ rescue JSON::ParserError
51
+ { "raw" => response.body }
52
+ end
53
+
54
+ def raise_for_status!(response)
55
+ code = response.code.to_i
56
+ return if code.between?(200, 299)
57
+
58
+ body = response.body
59
+ message = "#{name} HTTP #{code}: #{body.to_s[0, 300]}"
60
+
61
+ case code
62
+ when 401, 403
63
+ raise AuthenticationError.new(message, provider: name, status: code, body: body)
64
+ when 429
65
+ raise RateLimitError.new(message, provider: name, status: code, body: body)
66
+ else
67
+ raise ProviderError.new(message, provider: name, status: code, body: body)
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLlmMesh
4
+ module Providers
5
+ # OpenAI-compatible local runtime (Ollama, LM Studio, vLLM, llama.cpp server, etc.)
6
+ class LocalNode < Base
7
+ def name
8
+ :local_node
9
+ end
10
+
11
+ def complete(prompt:, system: nil, model: nil, **options)
12
+ model ||= config.local_node_model
13
+ messages = []
14
+ messages << { role: "system", content: system } if system
15
+ messages << { role: "user", content: prompt }
16
+
17
+ body = {
18
+ model: model,
19
+ messages: messages,
20
+ stream: false,
21
+ temperature: options.fetch(:temperature, 0.7)
22
+ }
23
+ body[:options] = { num_predict: options[:max_tokens] } if options[:max_tokens]
24
+
25
+ response, latency_ms = http_post(
26
+ "#{config.local_node_base_url.chomp('/')}/v1/chat/completions",
27
+ headers: { "Content-Type" => "application/json" },
28
+ body: body
29
+ )
30
+
31
+ # Fallback to Ollama native chat API if OpenAI-compat endpoint is missing
32
+ if response.code.to_i == 404
33
+ response, latency_ms = http_post(
34
+ "#{config.local_node_base_url.chomp('/')}/api/chat",
35
+ headers: { "Content-Type" => "application/json" },
36
+ body: { model: model, messages: messages, stream: false }
37
+ )
38
+ raise_for_status!(response)
39
+ data = parse_json(response)
40
+ content = data.dig("message", "content").to_s
41
+ return Response.new(
42
+ content: content,
43
+ provider: name,
44
+ model: model,
45
+ usage: {},
46
+ raw: data,
47
+ latency_ms: latency_ms
48
+ )
49
+ end
50
+
51
+ raise_for_status!(response)
52
+ data = parse_json(response)
53
+ content = data.dig("choices", 0, "message", "content").to_s
54
+
55
+ Response.new(
56
+ content: content,
57
+ provider: name,
58
+ model: data["model"] || model,
59
+ usage: data["usage"] || {},
60
+ raw: data,
61
+ latency_ms: latency_ms
62
+ )
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLlmMesh
4
+ module Providers
5
+ class Openai < Base
6
+ def name
7
+ :openai
8
+ end
9
+
10
+ def complete(prompt:, system: nil, model: nil, **options)
11
+ api_key = config.openai_api_key
12
+ raise AuthenticationError.new("OPENAI_API_KEY is not configured", provider: name) if api_key.to_s.empty?
13
+
14
+ model ||= config.openai_model
15
+ messages = []
16
+ messages << { role: "system", content: system } if system
17
+ messages << { role: "user", content: prompt }
18
+
19
+ body = {
20
+ model: model,
21
+ messages: messages,
22
+ temperature: options.fetch(:temperature, 0.7)
23
+ }
24
+ body[:max_tokens] = options[:max_tokens] if options[:max_tokens]
25
+ body[:tools] = options[:tools] if options[:tools]
26
+
27
+ response, latency_ms = http_post(
28
+ "#{config.openai_base_url.chomp('/')}/chat/completions",
29
+ headers: {
30
+ "Authorization" => "Bearer #{api_key}",
31
+ "Content-Type" => "application/json"
32
+ },
33
+ body: body
34
+ )
35
+
36
+ raise_for_status!(response)
37
+ data = parse_json(response)
38
+ choice = data.dig("choices", 0, "message", "content").to_s
39
+
40
+ Response.new(
41
+ content: choice,
42
+ provider: name,
43
+ model: data["model"] || model,
44
+ usage: data["usage"] || {},
45
+ raw: data,
46
+ latency_ms: latency_ms
47
+ )
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLlmMesh
4
+ module Rag
5
+ # Lightweight text chunker with optional overlap for RAG pipelines.
6
+ class Chunker
7
+ DEFAULT_SIZE = 800
8
+ DEFAULT_OVERLAP = 100
9
+
10
+ def initialize(size: DEFAULT_SIZE, overlap: DEFAULT_OVERLAP, separator: /\n{2,}|\n|\s+/)
11
+ raise ArgumentError, "overlap must be less than size" if overlap >= size
12
+
13
+ @size = size
14
+ @overlap = overlap
15
+ @separator = separator
16
+ end
17
+
18
+ def chunk(text)
19
+ return [] if text.nil? || text.strip.empty?
20
+
21
+ paragraphs = text.to_s.split(@separator).map(&:strip).reject(&:empty?)
22
+ chunks = []
23
+ buffer = +""
24
+
25
+ paragraphs.each do |piece|
26
+ candidate = buffer.empty? ? piece : "#{buffer} #{piece}"
27
+ if candidate.length <= @size
28
+ buffer = candidate
29
+ else
30
+ chunks << buffer unless buffer.empty?
31
+ buffer = overlap_tail(buffer)
32
+ buffer = buffer.empty? ? piece : "#{buffer} #{piece}"
33
+ while buffer.length > @size
34
+ chunks << buffer[0, @size]
35
+ buffer = overlap_tail(buffer[0, @size]) + buffer[@size..]
36
+ end
37
+ end
38
+ end
39
+
40
+ chunks << buffer unless buffer.empty?
41
+ chunks
42
+ end
43
+
44
+ private
45
+
46
+ def overlap_tail(text)
47
+ return "" if @overlap.zero? || text.nil? || text.empty?
48
+
49
+ text[[text.length - @overlap, 0].max..]
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLlmMesh
4
+ module Rag
5
+ # Zero-dependency bag-of-words style embeddings for local prototyping.
6
+ # Swap for a real embedding provider in production.
7
+ class Embeddings
8
+ def initialize(dimensions: 256)
9
+ @dimensions = dimensions
10
+ end
11
+
12
+ def embed(text)
13
+ vector = Array.new(@dimensions, 0.0)
14
+ tokenize(text).each do |token|
15
+ index = stable_hash(token) % @dimensions
16
+ vector[index] += 1.0
17
+ end
18
+ normalize!(vector)
19
+ vector
20
+ end
21
+
22
+ def embed_many(texts)
23
+ Array(texts).map { |t| embed(t) }
24
+ end
25
+
26
+ def cosine_similarity(a, b)
27
+ raise ArgumentError, "vectors must match length" unless a.length == b.length
28
+
29
+ dot = 0.0
30
+ a.each_index { |i| dot += a[i] * b[i] }
31
+ dot
32
+ end
33
+
34
+ def top_k(query, documents, k: 3)
35
+ query_vec = embed(query)
36
+ scored = documents.map do |doc|
37
+ text = doc.is_a?(Hash) ? doc[:text] || doc["text"] : doc.to_s
38
+ score = cosine_similarity(query_vec, embed(text))
39
+ { document: doc, score: score }
40
+ end
41
+ scored.sort_by { |row| -row[:score] }.first(k)
42
+ end
43
+
44
+ private
45
+
46
+ def tokenize(text)
47
+ text.to_s.downcase.scan(/[a-z0-9_]+/)
48
+ end
49
+
50
+ def stable_hash(token)
51
+ # FNV-1a 32-bit
52
+ hash = 2166136261
53
+ token.each_byte do |byte|
54
+ hash ^= byte
55
+ hash = (hash * 16777619) & 0xffffffff
56
+ end
57
+ hash
58
+ end
59
+
60
+ def normalize!(vector)
61
+ norm = Math.sqrt(vector.sum { |v| v * v })
62
+ return vector if norm.zero?
63
+
64
+ vector.map! { |v| v / norm }
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLlmMesh
4
+ module Rag
5
+ # Helpers for structuring tool / function-calling schemas across providers.
6
+ module Tools
7
+ module_function
8
+
9
+ def define(name:, description:, parameters: {}, required: [])
10
+ {
11
+ name: name.to_s,
12
+ description: description.to_s,
13
+ parameters: {
14
+ type: "object",
15
+ properties: parameters,
16
+ required: Array(required).map(&:to_s)
17
+ }
18
+ }
19
+ end
20
+
21
+ def for_openai(tools)
22
+ Array(tools).map do |tool|
23
+ {
24
+ type: "function",
25
+ function: {
26
+ name: tool[:name] || tool["name"],
27
+ description: tool[:description] || tool["description"],
28
+ parameters: tool[:parameters] || tool["parameters"]
29
+ }
30
+ }
31
+ end
32
+ end
33
+
34
+ def for_anthropic(tools)
35
+ Array(tools).map do |tool|
36
+ {
37
+ name: tool[:name] || tool["name"],
38
+ description: tool[:description] || tool["description"],
39
+ input_schema: tool[:parameters] || tool["parameters"]
40
+ }
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLlmMesh
4
+ class Railtie < ::Rails::Railtie
5
+ initializer "ruby_llm_mesh.active_record" do
6
+ ActiveSupport.on_load(:active_record) do
7
+ require_relative "active_record/acts_as_ai_agent"
8
+ extend RubyLlmMesh::ActiveRecord::ActsAsAiAgent
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLlmMesh
4
+ class Response
5
+ attr_reader :content, :provider, :model, :usage, :raw, :latency_ms, :fallback_used
6
+
7
+ def initialize(content:, provider:, model: nil, usage: {}, raw: nil, latency_ms: nil, fallback_used: false)
8
+ @content = content
9
+ @provider = provider
10
+ @model = model
11
+ @usage = usage || {}
12
+ @raw = raw
13
+ @latency_ms = latency_ms
14
+ @fallback_used = fallback_used
15
+ end
16
+
17
+ def text
18
+ content
19
+ end
20
+
21
+ def to_s
22
+ content.to_s
23
+ end
24
+
25
+ def to_h
26
+ {
27
+ content: content,
28
+ provider: provider,
29
+ model: model,
30
+ usage: usage,
31
+ latency_ms: latency_ms,
32
+ fallback_used: fallback_used
33
+ }
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLlmMesh
4
+ class Router
5
+ PROVIDER_MAP = {
6
+ openai: Providers::Openai,
7
+ anthropic: Providers::Anthropic,
8
+ local_node: Providers::LocalNode
9
+ }.freeze
10
+
11
+ class << self
12
+ def circuit_breaker
13
+ @circuit_breaker ||= CircuitBreaker.new(
14
+ failure_threshold: RubyLlmMesh.configuration.circuit_failure_threshold,
15
+ reset_timeout: RubyLlmMesh.configuration.circuit_reset_timeout
16
+ )
17
+ end
18
+
19
+ def reset_circuit_breaker!
20
+ @circuit_breaker = nil
21
+ end
22
+ end
23
+
24
+ def initialize(config: RubyLlmMesh.configuration, circuit_breaker: self.class.circuit_breaker)
25
+ @config = config
26
+ @circuit_breaker = circuit_breaker
27
+ end
28
+
29
+ def complete(prompt:, providers: nil, fallback: nil, system: nil, model: nil, **options)
30
+ raise ArgumentError, "prompt is required" if prompt.nil? || prompt.to_s.strip.empty?
31
+
32
+ provider_list = Array(providers || @config.default_providers).map(&:to_sym)
33
+ raise ArgumentError, "providers list cannot be empty" if provider_list.empty?
34
+
35
+ use_fallback = fallback.nil? ? @config.fallback : fallback
36
+ errors = {}
37
+ attempted = 0
38
+
39
+ provider_list.each_with_index do |provider_name, index|
40
+ unless PROVIDER_MAP.key?(provider_name)
41
+ errors[provider_name] = ProviderError.new("Unknown provider: #{provider_name}", provider: provider_name)
42
+ next
43
+ end
44
+
45
+ unless @circuit_breaker.allow?(provider_name)
46
+ errors[provider_name] = CircuitOpenError.new(
47
+ "Circuit open for #{provider_name}",
48
+ provider: provider_name
49
+ )
50
+ log(:warn, "Skipping #{provider_name} — circuit open")
51
+ next
52
+ end
53
+
54
+ begin
55
+ attempted += 1
56
+ log(:info, "Routing to #{provider_name}")
57
+ provider = PROVIDER_MAP[provider_name].new(@config)
58
+ response = provider.complete(prompt: prompt, system: system, model: model, **options)
59
+ @circuit_breaker.record_success(provider_name)
60
+ return Response.new(
61
+ content: response.content,
62
+ provider: response.provider,
63
+ model: response.model,
64
+ usage: response.usage,
65
+ raw: response.raw,
66
+ latency_ms: response.latency_ms,
67
+ fallback_used: index.positive?
68
+ )
69
+ rescue ProviderError => e
70
+ @circuit_breaker.record_failure(provider_name)
71
+ errors[provider_name] = e
72
+ log(:error, "#{provider_name} failed: #{e.message}")
73
+ break unless use_fallback
74
+ end
75
+ end
76
+
77
+ raise AllProvidersFailedError, errors
78
+ end
79
+
80
+ private
81
+
82
+ def log(level, message)
83
+ logger = @config.logger
84
+ return unless logger
85
+
86
+ if logger.respond_to?(level)
87
+ logger.public_send(level, "[RubyLlmMesh] #{message}")
88
+ elsif logger.respond_to?(:call)
89
+ logger.call(level, message)
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLlmMesh
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "ruby_llm_mesh/version"
4
+ require_relative "ruby_llm_mesh/errors"
5
+ require_relative "ruby_llm_mesh/configuration"
6
+ require_relative "ruby_llm_mesh/response"
7
+ require_relative "ruby_llm_mesh/circuit_breaker"
8
+ require_relative "ruby_llm_mesh/providers/base"
9
+ require_relative "ruby_llm_mesh/providers/openai"
10
+ require_relative "ruby_llm_mesh/providers/anthropic"
11
+ require_relative "ruby_llm_mesh/providers/local_node"
12
+ require_relative "ruby_llm_mesh/router"
13
+ require_relative "ruby_llm_mesh/rag/chunker"
14
+ require_relative "ruby_llm_mesh/rag/embeddings"
15
+ require_relative "ruby_llm_mesh/rag/tools"
16
+
17
+ # Optional Rails integration — only load railtie when Rails is already present
18
+ if defined?(Rails::Railtie)
19
+ require_relative "ruby_llm_mesh/railtie"
20
+ end
21
+
22
+ module RubyLlmMesh
23
+ class << self
24
+ def complete(prompt:, providers: nil, fallback: nil, **options)
25
+ Router.new.complete(prompt: prompt, providers: providers, fallback: fallback, **options)
26
+ end
27
+
28
+ def chat(**)
29
+ complete(**)
30
+ end
31
+ end
32
+ end
33
+
34
+ # Friendly alias matching the public DSL from the project overview
35
+ module AiAgentRouter
36
+ def self.complete(...)
37
+ RubyLlmMesh.complete(...)
38
+ end
39
+
40
+ def self.configure(&)
41
+ RubyLlmMesh.configure(&)
42
+ end
43
+
44
+ def self.configuration
45
+ RubyLlmMesh.configuration
46
+ end
47
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/ruby_llm_mesh/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "ruby_llm_mesh"
7
+ spec.version = RubyLlmMesh::VERSION
8
+ spec.authors = ["theworker02"]
9
+ spec.email = ["theworker02@users.noreply.github.com"]
10
+
11
+ spec.summary = "Unified multi-provider AI routing with circuit-breaking and local fallback for Ruby & Rails"
12
+ spec.description = <<~DESC
13
+ ruby_llm_mesh (AiAgentRouter) is a drop-in Ruby gem that routes AI prompts across
14
+ OpenAI, Anthropic, and local node runtimes with automatic circuit-breaking,
15
+ fallback ladders, lightweight RAG helpers, and optional ActiveRecord hooks.
16
+ DESC
17
+ spec.homepage = "https://github.com/theworker02/ruby_llm_mesh"
18
+ spec.license = "MIT"
19
+ spec.required_ruby_version = ">= 3.1.0"
20
+
21
+ spec.metadata["homepage_uri"] = spec.homepage
22
+ spec.metadata["source_code_uri"] = "https://github.com/theworker02/ruby_llm_mesh"
23
+ spec.metadata["changelog_uri"] = "https://github.com/theworker02/ruby_llm_mesh/blob/main/CHANGELOG.md"
24
+ spec.metadata["documentation_uri"] = "https://theworker02.github.io/ruby_llm_mesh/"
25
+ spec.metadata["bug_tracker_uri"] = "https://github.com/theworker02/ruby_llm_mesh/issues"
26
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
27
+ spec.metadata["rubygems_mfa_required"] = "true"
28
+
29
+ spec.files = Dir.chdir(__dir__) do
30
+ `git ls-files -z`.split("\x0").reject do |f|
31
+ f.start_with?(*%w[test/ spec/ features/ .git .github docs/]) ||
32
+ f.end_with?(".gem")
33
+ end
34
+ end
35
+ # Ensure packaging works before the first commit
36
+ if spec.files.empty?
37
+ spec.files = Dir[
38
+ "lib/**/*",
39
+ "sig/**/*",
40
+ "exe/**/*",
41
+ "assets/**/*",
42
+ "LICENSE*",
43
+ "README*",
44
+ "CHANGELOG*",
45
+ "CODE_OF_CONDUCT*",
46
+ "*.gemspec"
47
+ ]
48
+ end
49
+
50
+ spec.bindir = "exe"
51
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
52
+ spec.require_paths = ["lib"]
53
+
54
+ spec.add_development_dependency "minitest", "~> 5.25"
55
+ spec.add_development_dependency "rake", "~> 13.2"
56
+ spec.add_development_dependency "webmock", "~> 3.24"
57
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLlmMesh
4
+ VERSION: String
5
+
6
+ class Configuration
7
+ attr_accessor default_providers: Array[Symbol]
8
+ attr_accessor fallback: bool
9
+ attr_accessor timeout: Integer
10
+ attr_accessor max_retries: Integer
11
+ attr_accessor openai_api_key: String?
12
+ attr_accessor openai_base_url: String
13
+ attr_accessor openai_model: String
14
+ attr_accessor anthropic_api_key: String?
15
+ attr_accessor anthropic_base_url: String
16
+ attr_accessor anthropic_model: String
17
+ attr_accessor local_node_base_url: String
18
+ attr_accessor local_node_model: String
19
+ attr_accessor circuit_failure_threshold: Integer
20
+ attr_accessor circuit_reset_timeout: Integer
21
+ attr_accessor logger: untyped
22
+ end
23
+
24
+ def self.configuration: () -> Configuration
25
+ def self.configure: () { (Configuration) -> void } -> void
26
+ def self.reset_configuration!: () -> void
27
+ def self.complete: (
28
+ prompt: String,
29
+ ?providers: Array[Symbol]?,
30
+ ?fallback: bool?,
31
+ **untyped
32
+ ) -> Response
33
+ def self.chat: (**untyped) -> Response
34
+
35
+ class Response
36
+ attr_reader content: String
37
+ attr_reader provider: Symbol
38
+ attr_reader model: String?
39
+ attr_reader usage: Hash[untyped, untyped]
40
+ attr_reader raw: untyped
41
+ attr_reader latency_ms: Integer?
42
+ attr_reader fallback_used: bool
43
+
44
+ def initialize: (
45
+ content: String,
46
+ provider: Symbol,
47
+ ?model: String?,
48
+ ?usage: Hash[untyped, untyped],
49
+ ?raw: untyped,
50
+ ?latency_ms: Integer?,
51
+ ?fallback_used: bool
52
+ ) -> void
53
+ def text: () -> String
54
+ def to_s: () -> String
55
+ def to_h: () -> Hash[Symbol, untyped]
56
+ end
57
+
58
+ class Router
59
+ def complete: (
60
+ prompt: String,
61
+ ?providers: Array[Symbol]?,
62
+ ?fallback: bool?,
63
+ ?system: String?,
64
+ ?model: String?,
65
+ **untyped
66
+ ) -> Response
67
+ end
68
+
69
+ class CircuitBreaker
70
+ def allow?: (Symbol) -> bool
71
+ def record_success: (Symbol) -> void
72
+ def record_failure: (Symbol) -> void
73
+ def state_for: (Symbol) -> Symbol
74
+ def reset!: (?Symbol?) -> void
75
+ end
76
+ end
77
+
78
+ module AiAgentRouter
79
+ def self.complete: (**untyped) -> RubyLlmMesh::Response
80
+ def self.configure: () { (RubyLlmMesh::Configuration) -> void } -> void
81
+ def self.configuration: () -> RubyLlmMesh::Configuration
82
+ end