locallingo 0.2.2 → 0.3.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/README.md +15 -2
- data/lib/locallingo/cli.rb +13 -0
- data/lib/locallingo/providers/ruby_llm.rb +54 -17
- data/lib/locallingo/settings.rb +31 -0
- data/lib/locallingo/version.rb +1 -1
- data/lib/locallingo.rb +18 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ff4c0e72d7b3eb6d699b7cff03d8e2b8a61efb8a1c94c16cc906f2843b4dc53c
|
|
4
|
+
data.tar.gz: c4419498f0f7de3b334efab4c89471eb3a6a27da543992b5171aa3b67760c3d6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7820e2f62b6784ae546cd7521303e434a0319099f1ac69fa7471aff9cb786ff3b844f1200ae711eab36df7e058b31742f03de231c5bd95d460889c1027209628
|
|
7
|
+
data.tar.gz: b727c0b6f89916f328f3c2b00831263d779b7a7139f478571d7b2e9015326ef8841873da7e72ddd6f963e7f0009dafe9cc4630c1d6883d091bb165672d70fc67
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,13 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
9
|
### Added
|
|
10
|
+
- `Locallingo.configure { |c| c.anthropic_api_key = ... }` — gem-level provider
|
|
11
|
+
credentials as Strings or lazy callables, for apps whose keys don't live in
|
|
12
|
+
ENV (Rails credentials, app config objects, vaults). Precedence:
|
|
13
|
+
`Locallingo.configure` → host `RubyLLM.configure` → ENV.
|
|
14
|
+
- `.locallingo.rb` setup file: the CLI loads it from the project root before
|
|
15
|
+
dispatch, so standalone `lingo` runs can configure credentials without
|
|
16
|
+
booting Rails.
|
|
10
17
|
- Initial extraction from the `bin/translate` toolchain into a standalone gem.
|
|
11
18
|
- `lingo` CLI with subcommands: `status`, `translate`, `validate`, `quality`,
|
|
12
19
|
`fix-quality`, `accept-edits`, `hash`, `sync`. Legacy `--flag` forms still work
|
data/README.md
CHANGED
|
@@ -32,8 +32,21 @@ group :development do
|
|
|
32
32
|
end
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
Provider credentials come from ENV (e.g. `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`)
|
|
36
|
+
or from Ruby, for apps whose keys live elsewhere (Rails credentials, an app
|
|
37
|
+
config object, a vault):
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
Locallingo.configure do |config|
|
|
41
|
+
config.anthropic_api_key = "sk-ant-..." # a String…
|
|
42
|
+
config.openai_api_key = -> { AppConf.openai_key } # …or a lazy callable
|
|
43
|
+
end
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Put that in a `.locallingo.rb` file next to `.locallingo.yml` and the `lingo`
|
|
47
|
+
CLI loads it on start — no Rails boot required. A key set via
|
|
48
|
+
`Locallingo.configure` wins over one set through `RubyLLM.configure`, which
|
|
49
|
+
wins over ENV. Locallingo never stores keys itself.
|
|
37
50
|
|
|
38
51
|
## Configuration
|
|
39
52
|
|
data/lib/locallingo/cli.rb
CHANGED
|
@@ -16,6 +16,11 @@ module Locallingo
|
|
|
16
16
|
class CLI
|
|
17
17
|
CLI_NAME = "lingo"
|
|
18
18
|
|
|
19
|
+
# Optional Ruby setup file loaded before dispatch — the hook for apps to
|
|
20
|
+
# configure credentials (via Locallingo.configure or RubyLLM.configure)
|
|
21
|
+
# without booting Rails.
|
|
22
|
+
SETUP_FILENAME = ".locallingo.rb"
|
|
23
|
+
|
|
19
24
|
# subcommand => the legacy flag it replaces
|
|
20
25
|
COMMANDS = {
|
|
21
26
|
"status" => "--status",
|
|
@@ -52,6 +57,7 @@ module Locallingo
|
|
|
52
57
|
def run
|
|
53
58
|
command = resolve_command
|
|
54
59
|
options = parse_options!
|
|
60
|
+
load_setup_file
|
|
55
61
|
config = Locallingo.configuration(root_path: Dir.pwd, package: options[:package])
|
|
56
62
|
dispatch(command, config, options)
|
|
57
63
|
rescue Locallingo::Error => e
|
|
@@ -61,6 +67,13 @@ module Locallingo
|
|
|
61
67
|
|
|
62
68
|
private
|
|
63
69
|
|
|
70
|
+
# An absent file is fine; errors in the file propagate loudly. `load` (not
|
|
71
|
+
# require) so repeated in-process invocations re-execute it.
|
|
72
|
+
def load_setup_file
|
|
73
|
+
path = File.join(Dir.pwd, SETUP_FILENAME)
|
|
74
|
+
load path if File.file?(path)
|
|
75
|
+
end
|
|
76
|
+
|
|
64
77
|
# Determine the subcommand, translating a leading legacy flag (with a
|
|
65
78
|
# deprecation notice) and defaulting to `status`.
|
|
66
79
|
def resolve_command
|
|
@@ -31,14 +31,14 @@ module Locallingo
|
|
|
31
31
|
@provider = provider.to_sym
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
-
# True when
|
|
35
|
-
#
|
|
36
|
-
# elsewhere) rather than
|
|
34
|
+
# True when a key for the configured provider is found in any source
|
|
35
|
+
# (Locallingo settings, host RubyLLM config, ENV). Unknown providers are
|
|
36
|
+
# assumed configured (RubyLLM may source the key elsewhere) rather than
|
|
37
|
+
# blocking.
|
|
37
38
|
def credentials?
|
|
38
|
-
|
|
39
|
-
return true unless env
|
|
39
|
+
return true unless CREDENTIAL_ENV.key?(provider)
|
|
40
40
|
|
|
41
|
-
!
|
|
41
|
+
!resolved_api_key.nil?
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
# Raise a precise error when the provider has no credentials.
|
|
@@ -46,8 +46,10 @@ module Locallingo
|
|
|
46
46
|
return if credentials?
|
|
47
47
|
|
|
48
48
|
raise MissingCredentialsError,
|
|
49
|
-
"No credentials for provider #{provider.inspect} " \
|
|
50
|
-
"
|
|
49
|
+
"No credentials for provider #{provider.inspect}. " \
|
|
50
|
+
"Set #{CREDENTIAL_ENV[provider]} in ENV, call " \
|
|
51
|
+
"Locallingo.configure { |c| c.#{provider}_api_key = ... }, or add a " \
|
|
52
|
+
".locallingo.rb setup file at the project root (loaded by the CLI)."
|
|
51
53
|
end
|
|
52
54
|
|
|
53
55
|
# Send +instructions+ (system prompt) + +payload+ (user message, JSON) to
|
|
@@ -70,20 +72,55 @@ module Locallingo
|
|
|
70
72
|
|
|
71
73
|
# RubyLLM does not read provider API keys from ENV on its own, so a
|
|
72
74
|
# standalone CLI run (no Rails initializer to call RubyLLM.configure)
|
|
73
|
-
# would raise "Missing configuration for <provider>".
|
|
74
|
-
#
|
|
75
|
-
#
|
|
75
|
+
# would raise "Missing configuration for <provider>". Push the resolved
|
|
76
|
+
# key into RubyLLM's config: an explicit `Locallingo.configure` key wins
|
|
77
|
+
# (re-resolved every chat so callables stay live), then a key the host
|
|
78
|
+
# app already set, then the ENV fallback.
|
|
76
79
|
def configure_credentials!
|
|
77
|
-
|
|
78
|
-
return unless env
|
|
80
|
+
return unless CREDENTIAL_ENV.key?(provider)
|
|
79
81
|
|
|
80
|
-
setting = "#{provider}_api_key"
|
|
81
82
|
config = ::RubyLLM.config
|
|
83
|
+
setting = key_setting
|
|
82
84
|
return unless config.respond_to?(setting) && config.respond_to?("#{setting}=")
|
|
83
|
-
return unless config.public_send(setting).to_s.strip.empty?
|
|
84
85
|
|
|
85
|
-
key =
|
|
86
|
-
config.public_send(
|
|
86
|
+
key = settings_api_key
|
|
87
|
+
return if key.nil? && !presence(config.public_send(setting)).nil?
|
|
88
|
+
|
|
89
|
+
key ||= env_api_key
|
|
90
|
+
config.public_send("#{setting}=", key) unless key.nil?
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Key resolution across sources, in precedence order. Used by
|
|
94
|
+
# #credentials? as a fail-fast check before any network call.
|
|
95
|
+
def resolved_api_key
|
|
96
|
+
settings_api_key || host_configured_api_key || env_api_key
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def settings_api_key
|
|
100
|
+
Locallingo.settings.api_key_for(provider)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# A key the host app set via RubyLLM.configure — only inspected when
|
|
104
|
+
# ruby_llm is already loaded (we never require it just to peek).
|
|
105
|
+
def host_configured_api_key
|
|
106
|
+
return nil unless defined?(::RubyLLM)
|
|
107
|
+
|
|
108
|
+
config = ::RubyLLM.config
|
|
109
|
+
return nil unless config.respond_to?(key_setting)
|
|
110
|
+
|
|
111
|
+
presence(config.public_send(key_setting))
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def env_api_key
|
|
115
|
+
env = CREDENTIAL_ENV[provider]
|
|
116
|
+
env ? presence(ENV.fetch(env, "")) : nil
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def key_setting = "#{provider}_api_key"
|
|
120
|
+
|
|
121
|
+
def presence(value)
|
|
122
|
+
key = value.to_s.strip
|
|
123
|
+
key.empty? ? nil : key
|
|
87
124
|
end
|
|
88
125
|
end
|
|
89
126
|
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Locallingo
|
|
4
|
+
# Code-level gem settings, configured via `Locallingo.configure`. Distinct
|
|
5
|
+
# from Locallingo::Configuration, which loads the `.locallingo.yml` project
|
|
6
|
+
# file — Settings holds what must never live in YAML: provider credentials.
|
|
7
|
+
#
|
|
8
|
+
# Locallingo.configure do |config|
|
|
9
|
+
# config.anthropic_api_key = ENV.fetch("MY_KEY") # a String…
|
|
10
|
+
# config.openai_api_key = -> { Vault.read("openai_key") } # …or a callable
|
|
11
|
+
# end
|
|
12
|
+
#
|
|
13
|
+
# Callables are resolved lazily on every use (never memoized), so keys can
|
|
14
|
+
# come from sources that aren't ready at configure time or that rotate.
|
|
15
|
+
class Settings
|
|
16
|
+
PROVIDERS = %i[openai anthropic gemini deepseek openrouter].freeze
|
|
17
|
+
|
|
18
|
+
attr_accessor(*PROVIDERS.map { |name| :"#{name}_api_key" })
|
|
19
|
+
|
|
20
|
+
# The usable key for +provider+: callables are called, results stripped,
|
|
21
|
+
# and blank or unknown-provider values come back as nil.
|
|
22
|
+
def api_key_for(provider)
|
|
23
|
+
return nil unless PROVIDERS.include?(provider.to_sym)
|
|
24
|
+
|
|
25
|
+
value = public_send("#{provider}_api_key")
|
|
26
|
+
value = value.call if value.respond_to?(:call)
|
|
27
|
+
key = value.to_s.strip
|
|
28
|
+
key.empty? ? nil : key
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
data/lib/locallingo/version.rb
CHANGED
data/lib/locallingo.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "locallingo/version"
|
|
4
|
+
require_relative "locallingo/settings"
|
|
4
5
|
require_relative "locallingo/configuration"
|
|
5
6
|
require_relative "locallingo/json_extraction"
|
|
6
7
|
require_relative "locallingo/key_flattener"
|
|
@@ -35,4 +36,21 @@ module Locallingo
|
|
|
35
36
|
def self.configuration(root_path: Dir.pwd, package: nil)
|
|
36
37
|
Configuration.load(root_path:, package:)
|
|
37
38
|
end
|
|
39
|
+
|
|
40
|
+
# Code-level settings (provider credentials) — see Locallingo::Settings.
|
|
41
|
+
def self.settings
|
|
42
|
+
@settings ||= Settings.new
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Configure the gem from Ruby — the credentials path for apps whose keys
|
|
46
|
+
# don't live in ENV (call it from a Rails initializer or a `.locallingo.rb`
|
|
47
|
+
# setup file next to `.locallingo.yml`; the CLI loads the latter on start).
|
|
48
|
+
def self.configure
|
|
49
|
+
yield settings
|
|
50
|
+
settings
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.reset_settings!
|
|
54
|
+
@settings = nil
|
|
55
|
+
end
|
|
38
56
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: locallingo
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mikael Henriksson
|
|
@@ -81,6 +81,7 @@ files:
|
|
|
81
81
|
- lib/locallingo/quality_checker.rb
|
|
82
82
|
- lib/locallingo/reporter.rb
|
|
83
83
|
- lib/locallingo/rubocop.rb
|
|
84
|
+
- lib/locallingo/settings.rb
|
|
84
85
|
- lib/locallingo/state_store.rb
|
|
85
86
|
- lib/locallingo/validators/duplicate_values.rb
|
|
86
87
|
- lib/locallingo/validators/manual_edits.rb
|