Class: Vangrail::Provider

Inherits:
Object
  • Object
show all
Defined in:
lib/vangrail/provider.rb

Overview

Where the model-backed rails call, and what they may ask for there.

Every endpoint this gem talks to is OpenAI-compatible, so the differences that matter are not protocol at all. They are: how a credential resolves, whether the endpoint is up, and which model roles it can actually serve. A local proxy has a key sitting in a constant and may need starting; a shared gateway resolves a token from three places and is either up or not; neither necessarily hosts a safety classifier.

That last point drives a real decision rather than a label. model(:guard) returning nil means the provider has no classifier, and the builder puts a policy rail on the input side instead of pretending a classifier is there.

provider = Vangrail::Provider.resolve          # from the environment
provider.chat(:judge)                                # => Chat, ready to ask

Constant Summary collapse

ROLES =
%i[guard judge].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name:, base_url:, models: {}, key_resolver: nil, guard_preset: nil, local: false, probe: nil) ⇒ Provider

Returns a new instance of Provider.



105
106
107
108
109
110
111
112
113
114
# File 'lib/vangrail/provider.rb', line 105

def initialize(name:, base_url:, models: {}, key_resolver: nil, guard_preset: nil,
               local: false, probe: nil)
  @name = name.to_s
  @base_url = base_url.to_s.sub(/\/+\z/, '')
  @models = models
  @key_resolver = key_resolver
  @guard_preset = guard_preset
  @local = local
  @probe = probe
end

Instance Attribute Details

#base_urlObject (readonly)

Returns the value of attribute base_url.



103
104
105
# File 'lib/vangrail/provider.rb', line 103

def base_url
  @base_url
end

#guard_presetObject (readonly)

Returns the value of attribute guard_preset.



103
104
105
# File 'lib/vangrail/provider.rb', line 103

def guard_preset
  @guard_preset
end

#localObject (readonly)

Returns the value of attribute local.



103
104
105
# File 'lib/vangrail/provider.rb', line 103

def local
  @local
end

#modelsObject (readonly)

Returns the value of attribute models.



103
104
105
# File 'lib/vangrail/provider.rb', line 103

def models
  @models
end

#nameObject (readonly)

Returns the value of attribute name.



103
104
105
# File 'lib/vangrail/provider.rb', line 103

def name
  @name
end

Class Method Details

.[](name) ⇒ Object



36
37
38
# File 'lib/vangrail/provider.rb', line 36

def [](name)
  registry[name.to_s]
end

.from_env_pair(env) ⇒ Object

An endpoint given directly, which is how anything unregistered is used.



85
86
87
88
89
90
91
92
93
94
95
# File 'lib/vangrail/provider.rb', line 85

def from_env_pair(env)
  base = present(env['GUARDRAILS_API_BASE'])
  return nil unless base

  new(
    name: 'env',
    base_url: base,
    key_resolver: -> { present(env['GUARDRAILS_API_KEY']) },
    models: { judge: present(env['GUARDRAILS_JUDGE_MODEL']), guard: present(env['GUARDRAILS_MODEL']) }
  )
end

.gateway_in(env) ⇒ Object

A gateway described by the environment this call was handed, rather than by the one the registry happened to be installed from. Resolution is then a function of (registry, env), which is what a caller passing an env hash is entitled to assume.



75
76
77
78
79
80
81
82
# File 'lib/vangrail/provider.rb', line 75

def gateway_in(env)
  return nil if env.equal?(ENV)

  spec = Providers::Gateway.from_environment(env)
  spec && Providers::Gateway.provider(spec, env)
rescue NameError
  nil
end

.namesObject



40
41
42
# File 'lib/vangrail/provider.rb', line 40

def names
  registry.keys
end

.present(value) ⇒ Object



97
98
99
100
# File 'lib/vangrail/provider.rb', line 97

def present(value)
  s = value.to_s.strip
  s.empty? ? nil : s
end

.register(provider) ⇒ Object



31
32
33
34
# File 'lib/vangrail/provider.rb', line 31

def register(provider)
  registry[provider.name] = provider
  provider
end

.registryObject

Presets by name, in the order resolve tries them.



27
28
29
# File 'lib/vangrail/provider.rb', line 27

def registry
  @registry ||= {}
end

.resolve(env = ENV) ⇒ Object

Picks a provider from the environment.

GUARDRAILS_PROVIDER=<name>   take this one, and fail loudly if it is
                           unknown rather than falling back
GUARDRAILS_API_BASE + key    an endpoint nobody registered
otherwise                    the first registered provider that is
                           actually available, in registration order

Returning nil is a legitimate answer: no endpoint is reachable, and the caller builds an engine with only the offline rails on it.



54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/vangrail/provider.rb', line 54

def resolve(env = ENV)
  candidates = registry.each_value.to_a + [gateway_in(env)].compact

  wanted = present(env['GUARDRAILS_PROVIDER'])
  if wanted
    found = candidates.find { |p| p.name == wanted }
    raise ConfigError, "unknown provider #{wanted.inspect}; known: #{names.join(', ')}" unless found

    return found.with_env(env)
  end

  explicit = from_env_pair(env)
  return explicit if explicit

  candidates.map { |p| p.with_env(env) }.find(&:available?)
end

Instance Method Details

#api_keyObject



134
135
136
137
138
# File 'lib/vangrail/provider.rb', line 134

def api_key
  return @api_key if defined?(@api_key)

  @api_key = @key_resolver&.call
end

#available?Boolean

Up, and holding a credential. A local endpoint is probed, because a proxy that is not running is the ordinary case rather than a failure.

Returns:

  • (Boolean)


152
153
154
155
156
157
# File 'lib/vangrail/provider.rb', line 152

def available?
  return false unless api_key || !credential_required?
  return true unless @probe

  @probe.call
end

#chat(role = :judge, **kwargs) ⇒ Object

Raises:



163
164
165
166
167
168
# File 'lib/vangrail/provider.rb', line 163

def chat(role = :judge, **kwargs)
  name = model(role)
  raise ConfigError, "provider #{self.name} has no #{role} model" unless name

  Chat.new(model: name, base_url: base_url, api_key: api_key, **kwargs)
end

#credential_required?Boolean

Returns:

  • (Boolean)


159
160
161
# File 'lib/vangrail/provider.rb', line 159

def credential_required?
  !@key_resolver.nil?
end

#guard?Boolean

Can this provider serve a safety classifier, as opposed to an instruct model answering a written policy.

Returns:

  • (Boolean)


146
147
148
# File 'lib/vangrail/provider.rb', line 146

def guard?
  !model(:guard).nil? && !guard_preset.nil?
end

#model(role) ⇒ Object



140
141
142
# File 'lib/vangrail/provider.rb', line 140

def model(role)
  models[role.to_sym]
end

#to_hObject



170
171
172
173
174
175
176
177
178
179
# File 'lib/vangrail/provider.rb', line 170

def to_h
  {
    'name' => name,
    'base_url' => base_url,
    'models' => models.transform_keys(&:to_s).compact,
    'guard_preset' => guard_preset&.to_s,
    'local' => local,
    'available' => available?
  }.compact
end

#to_sObject



181
182
183
# File 'lib/vangrail/provider.rb', line 181

def to_s
  "#{name} #{base_url}"
end

#with_env(env) ⇒ Object

A copy that reads overrides out of an environment. Providers are shared objects in a registry, so nothing mutates in place.



118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/vangrail/provider.rb', line 118

def with_env(env)
  overrides = {
    judge: self.class.present(env['GUARDRAILS_JUDGE_MODEL']),
    guard: self.class.present(env['GUARDRAILS_MODEL'])
  }.compact
  base = self.class.present(env["#{env_prefix}_API_BASE"]) || base_url
  key = self.class.present(env["#{env_prefix}_API_KEY"])
  return self if overrides.empty? && base == base_url && key.nil?

  self.class.new(
    name: name, base_url: base, models: models.merge(overrides),
    key_resolver: key ? -> { key } : @key_resolver,
    guard_preset: guard_preset, local: local, probe: @probe
  )
end