Module: Vangrail::Providers::Gateway

Defined in:
lib/vangrail/providers/gateway.rb

Overview

A shared OpenAI-compatible gateway, described by configuration rather than compiled in.

Institutions and vendors each run one, on their own hostname, with their own credential source and their own model names. None of that is knowledge a general-purpose gem should carry: a hostname in this source is an endpoint every installation inherits whether it can reach it or not, and a credential path is worse, because it says where somebody's secrets live.

So a gateway is registered by the application that has one:

Vangrail::Providers.register_gateway(
Vangrail::Providers::Gateway::Spec.new(
  name: 'hub',
  base_url: 'https://gateway.example/api/v0',
  models: { judge: 'some/instruct-model', guard: 'some/guard-model' },
  guard_preset: :apriel_guard,
  key_env: 'HUB_API_KEY',
  key_file: File.join(Dir.home, '.config', 'hub', 'api_key'),
  pass_entry: 'hub/token'
)
)

or by environment, so a deployment needs no code at all:

GUARDRAILS_GATEWAY_NAME, _API_BASE, _API_KEY, _MODEL, _JUDGE_MODEL,
_GUARD_PRESET, _KEY_FILE, _PASS_ENTRY

Credentials resolve in one order, most explicit first: the environment variable, then a key file, then pass. Nothing is cached across a reset!, so a test can point the lookups at nothing and mean it.

Defined Under Namespace

Classes: Spec

Constant Summary collapse

ENV_PREFIX =
'GUARDRAILS_GATEWAY'

Class Method Summary collapse

Class Method Details

.from_env(spec, env) ⇒ Object



69
70
71
72
73
# File 'lib/vangrail/providers/gateway.rb', line 69

def from_env(spec, env)
  return nil unless spec.key_env

  present(env[spec.key_env])
end

.from_environment(env = ENV) ⇒ Object

A gateway described entirely by environment, or nil when none is.



106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/vangrail/providers/gateway.rb', line 106

def from_environment(env = ENV)
  base = present(env["#{ENV_PREFIX}_API_BASE"])
  return nil unless base

  Spec.new(
    name: present(env["#{ENV_PREFIX}_NAME"]) || 'gateway',
    base_url: base,
    models: {
      judge: present(env["#{ENV_PREFIX}_JUDGE_MODEL"]) || present(env["#{ENV_PREFIX}_MODEL"]),
      guard: present(env["#{ENV_PREFIX}_GUARD_MODEL"]),
    }.compact,
    guard_preset: present(env["#{ENV_PREFIX}_GUARD_PRESET"])&.to_sym,
    key_env: "#{ENV_PREFIX}_API_KEY",
    file_env: "#{ENV_PREFIX}_KEY_FILE",
    pass_env: "#{ENV_PREFIX}_PASS_ENTRY",
    key_file: present(env["#{ENV_PREFIX}_KEY_FILE"]),
    pass_entry: present(env["#{ENV_PREFIX}_PASS_ENTRY"]),
  )
end

.from_file(spec, env) ⇒ Object

The override replaces the configured path rather than being tried before it. Pointing it at a file that does not exist has to mean "no key here": otherwise there is no way to run without credentials on a machine that happens to have some, which is exactly what a test needs to do.



79
80
81
82
83
84
85
86
# File 'lib/vangrail/providers/gateway.rb', line 79

def from_file(spec, env)
  var = spec.file_env || (spec.key_env && "#{spec.key_env}_FILE")
  override = var && present(env[var])
  path = override || spec.key_file
  return nil unless path && File.file?(path)

  present(File.read(path).lines.first)
end

.from_pass(spec, env = ENV) ⇒ Object

stdin is closed for the child: gpg reads stdin, and a helper that inherits a caller's stdin can consume input the caller still needs or block forever on a pinentry prompt.



91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/vangrail/providers/gateway.rb', line 91

def from_pass(spec, env = ENV)
  entry = (spec.pass_env && present(env[spec.pass_env])) || spec.pass_entry
  return nil unless entry

  out = IO.popen(['pass', 'show', entry], in: File::NULL, err: File::NULL, &:read)
  return nil unless $?&.success?

  present(out.to_s.lines.first)
# A missing `pass` binary, or one that cannot be executed. A failing
# lookup is already `$?` not succeeding; anything else is a bug.
rescue Errno::ENOENT, Errno::EACCES
  nil
end

.present(value) ⇒ Object



126
127
128
129
# File 'lib/vangrail/providers/gateway.rb', line 126

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

.provider(spec, env = ENV) ⇒ Object

Builds a Provider from a Spec. The token is resolved lazily and memoized per spec, so pass runs at most once per process.



52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/vangrail/providers/gateway.rb', line 52

def provider(spec, env = ENV)
  tokens = {}
  Provider.new(
    name: spec.name,
    base_url: spec.base_url,
    models: spec.models || {},
    guard_preset: spec.guard_preset,
    key_resolver: lambda do
      tokens[spec.name] ||= token(spec, env)
    end,
  )
end

.token(spec, env = ENV) ⇒ Object



65
66
67
# File 'lib/vangrail/providers/gateway.rb', line 65

def token(spec, env = ENV)
  from_env(spec, env) || from_file(spec, env) || from_pass(spec, env)
end