lumen-llm 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.
- checksums.yaml +7 -0
- data/AGENTS.md +45 -0
- data/CHANGELOG.md +7 -0
- data/CONTRIBUTING.md +50 -0
- data/LICENSE.txt +22 -0
- data/README.md +203 -0
- data/SECURITY.md +22 -0
- data/examples/inputs/translator.json +5 -0
- data/examples/templates/translator.yml +19 -0
- data/lib/lumen.rb +2 -0
- data/lib/lumen_llm/configuration.rb +39 -0
- data/lib/lumen_llm/errors.rb +8 -0
- data/lib/lumen_llm/parser.rb +13 -0
- data/lib/lumen_llm/provider_registry.rb +19 -0
- data/lib/lumen_llm/providers/open_router/client.rb +79 -0
- data/lib/lumen_llm/providers/open_router/parser.rb +28 -0
- data/lib/lumen_llm/railtie.rb +15 -0
- data/lib/lumen_llm/runner.rb +50 -0
- data/lib/lumen_llm/stores/memory_store.rb +52 -0
- data/lib/lumen_llm/stores/null_store.rb +24 -0
- data/lib/lumen_llm/stores/redis_store.rb +55 -0
- data/lib/lumen_llm/template.rb +36 -0
- data/lib/lumen_llm/template_loader.rb +36 -0
- data/lib/lumen_llm/version.rb +4 -0
- data/lib/lumen_llm.rb +57 -0
- data/skills/lumen-llm-debugging/SKILL.md +35 -0
- data/skills/lumen-llm-setup/SKILL.md +41 -0
- data/skills/lumen-llm-template-authoring/SKILL.md +37 -0
- metadata +134 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 68399d42a44e4e0cfbdce9d294278dbe2a0aaa17d91795c0c8dc5faf0e3be502
|
|
4
|
+
data.tar.gz: 31d3eb2f3b8280fc97c35775587534f4cc6716cc7ddc788bfbc38d0c59e0f972
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 38ae5b9ee3f02ff592a74df1e88ceef5815ac5ecdbf3243e756dc24623311343abc499accea0d0d6625d9b048b3dec077b166a7efe5c653701b46516b18cff77
|
|
7
|
+
data.tar.gz: 530d17a9d585f593f2369074900b994239c64eb204b86d34936bfb5625a6dc0fe0f988bba45edafceff4d054a4fcf671b2dc198edaf085628446e3ff0d596ab5
|
data/AGENTS.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
This repo contains the `lumen-llm` Ruby gem.
|
|
4
|
+
|
|
5
|
+
## Ground Rules
|
|
6
|
+
|
|
7
|
+
- Keep runtime dependencies at zero.
|
|
8
|
+
- Keep Ruby syntax compatible with Ruby 2.3.
|
|
9
|
+
- Keep Rails support optional and compatible with Rails 4+.
|
|
10
|
+
- Use `LumenLLM::` as the canonical public namespace.
|
|
11
|
+
- Keep `Lumen::` as a compatibility alias.
|
|
12
|
+
- Do not add agent loops, tool calls, streaming, persistence, provider SDKs, or schema validation in v1.
|
|
13
|
+
|
|
14
|
+
## Development Commands
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
bin/setup
|
|
18
|
+
bin/test
|
|
19
|
+
ruby -Ilib:test test/runner_test.rb
|
|
20
|
+
bin/debug-template examples/templates/translator.yml examples/inputs/translator.json
|
|
21
|
+
gem build lumen-llm.gemspec
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Testing Expectations
|
|
25
|
+
|
|
26
|
+
- Use Minitest.
|
|
27
|
+
- Keep the Minitest development dependency below `5.16` while Ruby 2.3 is supported.
|
|
28
|
+
- Do not make real network calls in tests.
|
|
29
|
+
- Prefer fake providers, fake transports, and in-memory stores.
|
|
30
|
+
- Test public behavior instead of private implementation.
|
|
31
|
+
- Run `bin/test` before handing off changes.
|
|
32
|
+
|
|
33
|
+
## Compatibility Checklist
|
|
34
|
+
|
|
35
|
+
Before changing code, check that the change:
|
|
36
|
+
|
|
37
|
+
- avoids Ruby 2.4+ only syntax unless the support floor changes;
|
|
38
|
+
- does not assume Rails constants exist;
|
|
39
|
+
- does not assume ActiveSupport helpers exist;
|
|
40
|
+
- does not assume Redis is installed;
|
|
41
|
+
- does not require OpenRouter credentials in tests.
|
|
42
|
+
|
|
43
|
+
## Skills
|
|
44
|
+
|
|
45
|
+
Agent-facing skills live in `skills/`. Each skill must be a self-contained directory with a `SKILL.md` file containing YAML frontmatter with `name` and `description`.
|
data/CHANGELOG.md
ADDED
data/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Thanks for helping improve `lumen-llm`.
|
|
4
|
+
|
|
5
|
+
## Development
|
|
6
|
+
|
|
7
|
+
Set up the project:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
bin/setup
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Run the test suite:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
bin/test
|
|
17
|
+
bundle exec rake test
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Check template rendering:
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
bin/debug-template examples/templates/translator.yml examples/inputs/translator.json
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Check gem packaging:
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
gem build --strict --output /tmp/lumen-llm-0.1.0.gem lumen-llm.gemspec
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Compatibility Rules
|
|
33
|
+
|
|
34
|
+
- Keep runtime dependencies at zero.
|
|
35
|
+
- Keep Ruby syntax compatible with Ruby 2.3.
|
|
36
|
+
- Keep Rails support optional and compatible with Rails 4+.
|
|
37
|
+
- Use `LumenLLM::` as the canonical public namespace.
|
|
38
|
+
- Keep `Lumen::` as a compatibility alias.
|
|
39
|
+
- Do not add agent loops, tool calls, streaming, persistence, provider SDKs, or schema validation in v1.
|
|
40
|
+
|
|
41
|
+
## Tests
|
|
42
|
+
|
|
43
|
+
- Use Minitest.
|
|
44
|
+
- Do not make real network calls in tests.
|
|
45
|
+
- Prefer fake providers, fake transports, and in-memory stores.
|
|
46
|
+
- Test public behavior instead of private implementation.
|
|
47
|
+
|
|
48
|
+
## Releases
|
|
49
|
+
|
|
50
|
+
Only maintainers publish releases. Normal pushes and pull requests run CI only. RubyGems releases are triggered by `v*` tags through GitHub Actions and RubyGems Trusted Publishing.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dong Xu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
data/README.md
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# lumen-llm
|
|
2
|
+
|
|
3
|
+
[](https://github.com/uxgnod/lumen-llm/actions/workflows/ci.yml)
|
|
4
|
+
[](https://rubygems.org/gems/lumen-llm)
|
|
5
|
+
|
|
6
|
+
`lumen-llm` is a tiny Ruby LLM prompt runner for old Ruby and Rails apps.
|
|
7
|
+
|
|
8
|
+
It provides the small set of features extracted from Lumen:
|
|
9
|
+
|
|
10
|
+
- YAML prompt templates
|
|
11
|
+
- simple `{{variable}}` interpolation
|
|
12
|
+
- single-call OpenRouter chat completions
|
|
13
|
+
- JSON or text response parsing
|
|
14
|
+
- optional cache and usage stores
|
|
15
|
+
- optional Rails 4+ defaults
|
|
16
|
+
|
|
17
|
+
It intentionally does not provide agents, tool calls, streaming, vector search, provider SDKs, persistence, or schema validation.
|
|
18
|
+
|
|
19
|
+
## Requirements
|
|
20
|
+
|
|
21
|
+
- Ruby `>= 2.3`
|
|
22
|
+
- Rails `>= 4` is optional
|
|
23
|
+
- No runtime gem dependencies
|
|
24
|
+
|
|
25
|
+
HTTP uses Ruby stdlib `net/http`. JSON, YAML, logger, URI, digest, and date are also stdlib.
|
|
26
|
+
|
|
27
|
+
Ruby 2.3 and 2.4 are end-of-life runtimes. This gem supports them for legacy Rails apps, but new applications should use a maintained Ruby when possible.
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
Install directly:
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
gem install lumen-llm
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Or add it to your Gemfile:
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
gem "lumen-llm"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Then:
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
require "lumen_llm"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Configuration
|
|
50
|
+
|
|
51
|
+
For a plain Ruby app:
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
LumenLLM.configure do |config|
|
|
55
|
+
config.template_path = File.expand_path("lumen_templates", __dir__)
|
|
56
|
+
config.openrouter_api_key = ENV["OPENROUTER_API_KEY"]
|
|
57
|
+
config.openrouter_referer = "https://example.com"
|
|
58
|
+
config.openrouter_title = "MyApp"
|
|
59
|
+
end
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
For Rails 4+:
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
# config/initializers/lumen_llm.rb
|
|
66
|
+
LumenLLM.configure do |config|
|
|
67
|
+
config.template_path = Rails.root.join("lumen_templates").to_s
|
|
68
|
+
config.logger = Rails.logger
|
|
69
|
+
config.openrouter_api_key = ENV["OPENROUTER_API_KEY"]
|
|
70
|
+
config.openrouter_referer = "https://www.example.com"
|
|
71
|
+
config.openrouter_title = "MyRailsApp"
|
|
72
|
+
end
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The Railtie sets `template_path` and `logger` defaults when Rails is present, but explicit configuration is recommended.
|
|
76
|
+
|
|
77
|
+
## Templates
|
|
78
|
+
|
|
79
|
+
Create `lumen_templates/translator.yml`:
|
|
80
|
+
|
|
81
|
+
```yaml
|
|
82
|
+
key: translator
|
|
83
|
+
model: openai/gpt-5-mini
|
|
84
|
+
provider: openrouter
|
|
85
|
+
output_type: json
|
|
86
|
+
|
|
87
|
+
system_prompt: |
|
|
88
|
+
You are a professional localization assistant.
|
|
89
|
+
Return only valid JSON. Do not use markdown.
|
|
90
|
+
|
|
91
|
+
user_prompt: |
|
|
92
|
+
Translate "{{source_text}}" into these target languages:
|
|
93
|
+
{{target_languages_json}}
|
|
94
|
+
|
|
95
|
+
Return a JSON object using the same language codes as keys.
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Run it:
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
result = LumenLLM.run(
|
|
102
|
+
"translator",
|
|
103
|
+
input: {
|
|
104
|
+
source_text: "Save changes",
|
|
105
|
+
target_languages_json: { "de" => "German", "fr" => "French" }.to_json
|
|
106
|
+
}
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
puts result["de"]
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Compatibility Alias
|
|
113
|
+
|
|
114
|
+
The canonical namespace is `LumenLLM`.
|
|
115
|
+
|
|
116
|
+
For migrations from the original Rails-internal Lumen library, this gem also exposes:
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
Lumen.run("translator", input: { ... })
|
|
120
|
+
Lumen::TemplateLoader.load("translator")
|
|
121
|
+
Lumen::Runner.new(template: template, input: input)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Stores
|
|
125
|
+
|
|
126
|
+
By default, `lumen-llm` uses `LumenLLM::Stores::NullStore`, which does not cache or record stats.
|
|
127
|
+
|
|
128
|
+
Use memory store in tests or simple scripts:
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
LumenLLM.configure do |config|
|
|
132
|
+
config.store = LumenLLM::Stores::MemoryStore.new
|
|
133
|
+
end
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Use Redis by passing an existing Redis-like client:
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
redis = Redis.new(url: ENV["REDIS_URL"])
|
|
140
|
+
|
|
141
|
+
LumenLLM.configure do |config|
|
|
142
|
+
config.store = LumenLLM::Stores::RedisStore.new(redis)
|
|
143
|
+
end
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
`lumen-llm` does not depend on the `redis` gem. Your app owns that dependency.
|
|
147
|
+
|
|
148
|
+
## Force Refresh
|
|
149
|
+
|
|
150
|
+
```ruby
|
|
151
|
+
LumenLLM.run("translator", input: input, force: true)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
`force: true` bypasses the configured cache for that call.
|
|
155
|
+
|
|
156
|
+
## Development Workflow
|
|
157
|
+
|
|
158
|
+
```sh
|
|
159
|
+
bin/setup
|
|
160
|
+
bin/test
|
|
161
|
+
bundle exec rake test
|
|
162
|
+
bin/debug-template examples/templates/translator.yml examples/inputs/translator.json
|
|
163
|
+
gem build --strict --output /tmp/lumen-llm-0.1.0.gem lumen-llm.gemspec
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
The test suite uses Minitest and no real network calls.
|
|
167
|
+
Minitest is capped below `5.16` because newer Minitest releases require Ruby 2.6+.
|
|
168
|
+
|
|
169
|
+
## Contributing
|
|
170
|
+
|
|
171
|
+
Bug reports and small pull requests are welcome. Please read [CONTRIBUTING.md](CONTRIBUTING.md) before proposing changes, especially the Ruby 2.3 compatibility rules and the no-runtime-dependency constraint.
|
|
172
|
+
|
|
173
|
+
## Security
|
|
174
|
+
|
|
175
|
+
Please do not report security vulnerabilities in public issues. See [SECURITY.md](SECURITY.md) for the private reporting process.
|
|
176
|
+
|
|
177
|
+
## Agent Skills
|
|
178
|
+
|
|
179
|
+
The `skills/` directory contains Agent Skills compatible with the open `SKILL.md` format:
|
|
180
|
+
|
|
181
|
+
- `skills/lumen-llm-setup`
|
|
182
|
+
- `skills/lumen-llm-template-authoring`
|
|
183
|
+
- `skills/lumen-llm-debugging`
|
|
184
|
+
|
|
185
|
+
They help coding agents install, configure, test, and debug this gem without guessing project conventions.
|
|
186
|
+
|
|
187
|
+
## Release
|
|
188
|
+
|
|
189
|
+
Maintainers release through GitHub Actions and RubyGems Trusted Publishing.
|
|
190
|
+
Normal pushes and pull requests only run checks; only `v*` tags trigger the release workflow.
|
|
191
|
+
|
|
192
|
+
Build locally before tagging:
|
|
193
|
+
|
|
194
|
+
```sh
|
|
195
|
+
gem build --strict --output /tmp/lumen-llm-0.1.0.gem lumen-llm.gemspec
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
After CI passes and RubyGems trusted publishing is configured, publish by pushing a version tag:
|
|
199
|
+
|
|
200
|
+
```sh
|
|
201
|
+
git tag v0.1.0
|
|
202
|
+
git push origin v0.1.0
|
|
203
|
+
```
|
data/SECURITY.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported Versions
|
|
4
|
+
|
|
5
|
+
Security fixes are provided for the latest released version of `lumen-llm`.
|
|
6
|
+
|
|
7
|
+
## Reporting a Vulnerability
|
|
8
|
+
|
|
9
|
+
Please do not open a public issue for a security vulnerability.
|
|
10
|
+
|
|
11
|
+
Email `uxgnod@gmail.com` with:
|
|
12
|
+
|
|
13
|
+
- a description of the issue;
|
|
14
|
+
- affected versions, if known;
|
|
15
|
+
- reproduction steps or proof-of-concept details;
|
|
16
|
+
- any suggested mitigation.
|
|
17
|
+
|
|
18
|
+
We will acknowledge the report, investigate privately, and publish a fixed release when needed.
|
|
19
|
+
|
|
20
|
+
## Security Notes
|
|
21
|
+
|
|
22
|
+
`lumen-llm` intentionally keeps runtime dependencies at zero and does not include provider SDKs, persistence, agent loops, tool calls, or streaming in v1. Tests must not make real network calls or require OpenRouter credentials.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
key: translator
|
|
2
|
+
model: openai/gpt-5-mini
|
|
3
|
+
provider: openrouter
|
|
4
|
+
output_type: json
|
|
5
|
+
|
|
6
|
+
system_prompt: |
|
|
7
|
+
You are a professional localization assistant for software products.
|
|
8
|
+
Translate short UI copy naturally and concisely.
|
|
9
|
+
Preserve placeholders, numbers, HTML tags, and product names.
|
|
10
|
+
Return only valid JSON. Do not use markdown.
|
|
11
|
+
|
|
12
|
+
user_prompt: |
|
|
13
|
+
Translate the following source text into the target languages.
|
|
14
|
+
|
|
15
|
+
Source text: "{{source_text}}"
|
|
16
|
+
Target languages: {{target_languages_json}}
|
|
17
|
+
|
|
18
|
+
Return a JSON object using the exact same language codes from target languages as keys.
|
|
19
|
+
|
data/lib/lumen.rb
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
require "logger"
|
|
2
|
+
|
|
3
|
+
module LumenLLM
|
|
4
|
+
class Configuration
|
|
5
|
+
attr_accessor :template_path,
|
|
6
|
+
:openrouter_api_key,
|
|
7
|
+
:openrouter_referer,
|
|
8
|
+
:openrouter_title,
|
|
9
|
+
:store,
|
|
10
|
+
:provider_registry,
|
|
11
|
+
:cache_ttl,
|
|
12
|
+
:http_open_timeout,
|
|
13
|
+
:http_read_timeout
|
|
14
|
+
|
|
15
|
+
attr_writer :logger
|
|
16
|
+
|
|
17
|
+
def initialize
|
|
18
|
+
@template_path = nil
|
|
19
|
+
@logger = nil
|
|
20
|
+
@openrouter_api_key = ENV["OPENROUTER_API_KEY"]
|
|
21
|
+
@openrouter_referer = ENV["OPENROUTER_REFERER"] || "https://example.com"
|
|
22
|
+
@openrouter_title = ENV["OPENROUTER_TITLE"] || "lumen-llm"
|
|
23
|
+
@store = nil
|
|
24
|
+
@provider_registry = nil
|
|
25
|
+
@cache_ttl = 3600
|
|
26
|
+
@http_open_timeout = 10
|
|
27
|
+
@http_read_timeout = 60
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def logger
|
|
31
|
+
@logger ||= Logger.new($stderr)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def logger_configured?
|
|
35
|
+
!@logger.nil?
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module LumenLLM
|
|
2
|
+
module Parser
|
|
3
|
+
def self.extract(provider:, raw:, output_type:)
|
|
4
|
+
case provider.to_s
|
|
5
|
+
when "openrouter"
|
|
6
|
+
Providers::OpenRouter::Parser.extract(raw, output_type)
|
|
7
|
+
else
|
|
8
|
+
raise ParserError, "No parser defined for provider: #{provider}"
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module LumenLLM
|
|
2
|
+
class ProviderRegistry
|
|
3
|
+
def initialize
|
|
4
|
+
@providers = {}
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def register(name, provider = nil, &block)
|
|
8
|
+
@providers[name.to_s] = block || provider
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def fetch(name)
|
|
12
|
+
provider = @providers[name.to_s]
|
|
13
|
+
raise ConfigurationError, "Unknown LLM provider: #{name}" unless provider
|
|
14
|
+
|
|
15
|
+
provider.respond_to?(:call) ? provider.call : provider
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "net/http"
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module LumenLLM
|
|
6
|
+
module Providers
|
|
7
|
+
module OpenRouter
|
|
8
|
+
class Client
|
|
9
|
+
ENDPOINT = "https://openrouter.ai/api/v1/chat/completions"
|
|
10
|
+
|
|
11
|
+
def initialize(api_key: nil, referer: nil, title: nil, transport: nil, open_timeout: nil, read_timeout: nil)
|
|
12
|
+
config = LumenLLM.configuration
|
|
13
|
+
@api_key = api_key || config.openrouter_api_key || ENV["OPENROUTER_API_KEY"]
|
|
14
|
+
@referer = referer || config.openrouter_referer
|
|
15
|
+
@title = title || config.openrouter_title
|
|
16
|
+
@transport = transport || NetHTTPTransport.new
|
|
17
|
+
@open_timeout = open_timeout || config.http_open_timeout
|
|
18
|
+
@read_timeout = read_timeout || config.http_read_timeout
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def chat(messages, model:)
|
|
22
|
+
raise ConfigurationError, "OpenRouter API key is not configured" if blank?(@api_key)
|
|
23
|
+
|
|
24
|
+
body = {
|
|
25
|
+
:model => model,
|
|
26
|
+
:messages => messages
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
response = @transport.request(
|
|
30
|
+
URI.parse(ENDPOINT),
|
|
31
|
+
headers,
|
|
32
|
+
body.to_json,
|
|
33
|
+
@open_timeout,
|
|
34
|
+
@read_timeout
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
unless success?(response)
|
|
38
|
+
raise ProviderError, "OpenRouter API Error: #{response.code} - #{response.body}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
JSON.parse(response.body)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def headers
|
|
47
|
+
{
|
|
48
|
+
"Authorization" => "Bearer #{@api_key}",
|
|
49
|
+
"Content-Type" => "application/json",
|
|
50
|
+
"HTTP-Referer" => @referer,
|
|
51
|
+
"X-Title" => @title
|
|
52
|
+
}
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def success?(response)
|
|
56
|
+
response.code.to_i >= 200 && response.code.to_i < 300
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def blank?(value)
|
|
60
|
+
value.nil? || value.to_s.strip == ""
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
class NetHTTPTransport
|
|
65
|
+
def request(uri, headers, body, open_timeout, read_timeout)
|
|
66
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
67
|
+
http.use_ssl = uri.scheme == "https"
|
|
68
|
+
http.open_timeout = open_timeout
|
|
69
|
+
http.read_timeout = read_timeout
|
|
70
|
+
|
|
71
|
+
request = Net::HTTP::Post.new(uri.request_uri, headers)
|
|
72
|
+
request.body = body
|
|
73
|
+
http.request(request)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
module LumenLLM
|
|
4
|
+
module Providers
|
|
5
|
+
module OpenRouter
|
|
6
|
+
class Parser
|
|
7
|
+
def self.extract(raw, output_type = "text")
|
|
8
|
+
content = raw.dig("choices", 0, "message", "content")
|
|
9
|
+
raise ParserError, "Missing content in response." unless content
|
|
10
|
+
|
|
11
|
+
clean = content.gsub(/```json|```/, "").strip
|
|
12
|
+
|
|
13
|
+
case output_type.to_s
|
|
14
|
+
when "json"
|
|
15
|
+
begin
|
|
16
|
+
JSON.parse(clean)
|
|
17
|
+
rescue JSON::ParserError
|
|
18
|
+
raise ParserError, "Invalid JSON output:\n#{clean}"
|
|
19
|
+
end
|
|
20
|
+
else
|
|
21
|
+
clean
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
require "lumen_llm"
|
|
2
|
+
|
|
3
|
+
if defined?(Rails::Railtie)
|
|
4
|
+
module LumenLLM
|
|
5
|
+
class Railtie < Rails::Railtie
|
|
6
|
+
initializer "lumen_llm.configure" do |app|
|
|
7
|
+
LumenLLM.configure do |config|
|
|
8
|
+
config.template_path ||= app.root.join("lumen_templates").to_s
|
|
9
|
+
config.logger = Rails.logger unless config.logger_configured?
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
require "digest/sha1"
|
|
2
|
+
require "json"
|
|
3
|
+
|
|
4
|
+
module LumenLLM
|
|
5
|
+
class Runner
|
|
6
|
+
def initialize(template:, input:, store: nil, provider_registry: nil)
|
|
7
|
+
@template = template
|
|
8
|
+
@input = input
|
|
9
|
+
@store = store || LumenLLM.configuration.store || Stores::NullStore.new
|
|
10
|
+
@provider_registry = provider_registry || LumenLLM.configuration.provider_registry || LumenLLM.provider_registry
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def run(force: false)
|
|
14
|
+
cache_key = "lumen:#{@template.key}:#{Digest::SHA1.hexdigest(@input.to_json)}"
|
|
15
|
+
|
|
16
|
+
raw = if force
|
|
17
|
+
log("[LumenLLM::Runner] FORCE REFRESH: #{cache_key}")
|
|
18
|
+
generate
|
|
19
|
+
else
|
|
20
|
+
@store.cache(cache_key, :ttl => LumenLLM.configuration.cache_ttl) { generate }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
Parser.extract(
|
|
24
|
+
provider: @template.provider,
|
|
25
|
+
raw: raw,
|
|
26
|
+
output_type: @template.output_type
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def generate
|
|
33
|
+
payload = @template.render(@input)
|
|
34
|
+
response = @provider_registry.fetch(payload[:provider]).chat(payload[:messages], model: payload[:model])
|
|
35
|
+
track_stats(@template.key, response)
|
|
36
|
+
response
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def track_stats(key, response)
|
|
40
|
+
usage = response["usage"] || {}
|
|
41
|
+
@store.incr_stat("prompt_tokens:#{key}", usage["prompt_tokens"]) if usage["prompt_tokens"]
|
|
42
|
+
@store.incr_stat("completion_tokens:#{key}", usage["completion_tokens"]) if usage["completion_tokens"]
|
|
43
|
+
@store.track_usage(key, response["model"], usage)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def log(message)
|
|
47
|
+
LumenLLM.configuration.logger.info(message)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
require "date"
|
|
2
|
+
|
|
3
|
+
module LumenLLM
|
|
4
|
+
module Stores
|
|
5
|
+
class MemoryStore
|
|
6
|
+
def initialize
|
|
7
|
+
@values = {}
|
|
8
|
+
@stats = Hash.new(0)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def cache(key, _options = {})
|
|
12
|
+
return @values[key] if @values.key?(key)
|
|
13
|
+
|
|
14
|
+
@values[key] = yield
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def incr_stat(metric, amount = 1)
|
|
18
|
+
@stats["stats:#{metric}"] += amount.to_i
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def stat(key)
|
|
22
|
+
@stats["stats:#{key}"].to_i
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def all_stats(pattern = "stats:*")
|
|
26
|
+
prefix = pattern.sub(/\*$/, "")
|
|
27
|
+
result = {}
|
|
28
|
+
@stats.each do |key, value|
|
|
29
|
+
result[key] = value if key.index(prefix) == 0
|
|
30
|
+
end
|
|
31
|
+
result
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def track_usage(template_key, model, usage)
|
|
35
|
+
date = Date.today.strftime("%Y-%m-%d")
|
|
36
|
+
[
|
|
37
|
+
"usage:total",
|
|
38
|
+
"usage:#{template_key}",
|
|
39
|
+
"usage:model:#{model}",
|
|
40
|
+
"usage:#{template_key}:model:#{model}",
|
|
41
|
+
"usage:date:#{date}",
|
|
42
|
+
"usage:#{template_key}:date:#{date}"
|
|
43
|
+
].each do |prefix|
|
|
44
|
+
incr_stat("#{prefix}:prompt_tokens", usage["prompt_tokens"])
|
|
45
|
+
incr_stat("#{prefix}:completion_tokens", usage["completion_tokens"])
|
|
46
|
+
incr_stat("#{prefix}:total_tokens", usage["total_tokens"])
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module LumenLLM
|
|
2
|
+
module Stores
|
|
3
|
+
class NullStore
|
|
4
|
+
def cache(_key, _options = {})
|
|
5
|
+
yield
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def incr_stat(_metric, _amount = 1)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def stat(_key)
|
|
12
|
+
0
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def all_stats(_pattern = "stats:*")
|
|
16
|
+
{}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def track_usage(_template_key, _model, _usage)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "date"
|
|
3
|
+
|
|
4
|
+
module LumenLLM
|
|
5
|
+
module Stores
|
|
6
|
+
class RedisStore
|
|
7
|
+
def initialize(redis)
|
|
8
|
+
@redis = redis
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def cache(key, options = {})
|
|
12
|
+
ttl = options[:ttl] || 3600
|
|
13
|
+
value = @redis.get(key)
|
|
14
|
+
return JSON.parse(value) if value
|
|
15
|
+
|
|
16
|
+
result = yield
|
|
17
|
+
@redis.setex(key, ttl, result.to_json)
|
|
18
|
+
result
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def incr_stat(metric, amount = 1)
|
|
22
|
+
@redis.incrby("stats:#{metric}", amount.to_i)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def stat(key)
|
|
26
|
+
@redis.get("stats:#{key}").to_i
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def all_stats(pattern = "stats:*")
|
|
30
|
+
result = {}
|
|
31
|
+
@redis.keys(pattern).each do |key|
|
|
32
|
+
result[key] = @redis.get(key).to_i
|
|
33
|
+
end
|
|
34
|
+
result
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def track_usage(template_key, model, usage)
|
|
38
|
+
date = Date.today.strftime("%Y-%m-%d")
|
|
39
|
+
[
|
|
40
|
+
"usage:total",
|
|
41
|
+
"usage:#{template_key}",
|
|
42
|
+
"usage:model:#{model}",
|
|
43
|
+
"usage:#{template_key}:model:#{model}",
|
|
44
|
+
"usage:date:#{date}",
|
|
45
|
+
"usage:#{template_key}:date:#{date}"
|
|
46
|
+
].each do |prefix|
|
|
47
|
+
incr_stat("#{prefix}:prompt_tokens", usage["prompt_tokens"])
|
|
48
|
+
incr_stat("#{prefix}:completion_tokens", usage["completion_tokens"])
|
|
49
|
+
incr_stat("#{prefix}:total_tokens", usage["total_tokens"])
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module LumenLLM
|
|
2
|
+
class Template
|
|
3
|
+
attr_reader :key, :system_prompt, :user_prompt, :model, :provider, :output_type
|
|
4
|
+
|
|
5
|
+
def initialize(key:, system_prompt:, user_prompt:, model:, provider: :openrouter, output_type: :json)
|
|
6
|
+
@key = key.to_s
|
|
7
|
+
@system_prompt = system_prompt.to_s
|
|
8
|
+
@user_prompt = user_prompt.to_s
|
|
9
|
+
@model = model.to_s
|
|
10
|
+
@provider = provider.to_s
|
|
11
|
+
@output_type = output_type.to_s
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def render(input)
|
|
15
|
+
{
|
|
16
|
+
:provider => provider,
|
|
17
|
+
:model => model,
|
|
18
|
+
:messages => [
|
|
19
|
+
{ :role => "system", :content => interpolate(system_prompt, input) },
|
|
20
|
+
{ :role => "user", :content => interpolate(user_prompt, input) }
|
|
21
|
+
]
|
|
22
|
+
}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def interpolate(str, input)
|
|
28
|
+
rendered = str.dup
|
|
29
|
+
input.each do |key, value|
|
|
30
|
+
rendered = rendered.gsub("{{#{key}}}", value.to_s)
|
|
31
|
+
end
|
|
32
|
+
rendered
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
require "yaml"
|
|
2
|
+
|
|
3
|
+
module LumenLLM
|
|
4
|
+
class TemplateLoader
|
|
5
|
+
def self.load(key, path: nil)
|
|
6
|
+
new(path || LumenLLM.configuration.template_path).load(key)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def initialize(path)
|
|
10
|
+
@path = path
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def load(key)
|
|
14
|
+
raise ConfigurationError, "template_path is not configured" if blank?(@path)
|
|
15
|
+
|
|
16
|
+
template_file = File.join(@path.to_s, "#{key}.yml")
|
|
17
|
+
raise TemplateNotFoundError, "Template not found: #{key}" unless File.exist?(template_file)
|
|
18
|
+
|
|
19
|
+
config = YAML.load_file(template_file)
|
|
20
|
+
Template.new(
|
|
21
|
+
key: config["key"] || key,
|
|
22
|
+
system_prompt: config["system_prompt"],
|
|
23
|
+
user_prompt: config["user_prompt"],
|
|
24
|
+
model: config["model"],
|
|
25
|
+
provider: config["provider"] || "openrouter",
|
|
26
|
+
output_type: config["output_type"] || "text"
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def blank?(value)
|
|
33
|
+
value.nil? || value.to_s.strip == ""
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
data/lib/lumen_llm.rb
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
require "lumen_llm/version"
|
|
2
|
+
require "lumen_llm/errors"
|
|
3
|
+
require "lumen_llm/configuration"
|
|
4
|
+
require "lumen_llm/template"
|
|
5
|
+
require "lumen_llm/template_loader"
|
|
6
|
+
require "lumen_llm/provider_registry"
|
|
7
|
+
require "lumen_llm/stores/null_store"
|
|
8
|
+
require "lumen_llm/stores/memory_store"
|
|
9
|
+
require "lumen_llm/stores/redis_store"
|
|
10
|
+
require "lumen_llm/providers/open_router/client"
|
|
11
|
+
require "lumen_llm/providers/open_router/parser"
|
|
12
|
+
require "lumen_llm/parser"
|
|
13
|
+
require "lumen_llm/runner"
|
|
14
|
+
|
|
15
|
+
module LumenLLM
|
|
16
|
+
class << self
|
|
17
|
+
def configuration
|
|
18
|
+
@configuration ||= Configuration.new
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def configure
|
|
22
|
+
yield(configuration)
|
|
23
|
+
configuration
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def reset_configuration!
|
|
27
|
+
@configuration = Configuration.new
|
|
28
|
+
@provider_registry = nil
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def provider_registry
|
|
32
|
+
@provider_registry ||= default_provider_registry
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def run(template_key, input: {}, force: false, store: nil, provider_registry: nil)
|
|
36
|
+
template = TemplateLoader.load(template_key)
|
|
37
|
+
Runner.new(
|
|
38
|
+
template: template,
|
|
39
|
+
input: input,
|
|
40
|
+
store: store,
|
|
41
|
+
provider_registry: provider_registry
|
|
42
|
+
).run(force: force)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def default_provider_registry
|
|
48
|
+
registry = ProviderRegistry.new
|
|
49
|
+
registry.register("openrouter") { Providers::OpenRouter::Client.new }
|
|
50
|
+
registry
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
Lumen = LumenLLM unless defined?(::Lumen)
|
|
56
|
+
|
|
57
|
+
require "lumen_llm/railtie" if defined?(Rails)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: lumen-llm-debugging
|
|
3
|
+
description: Use when debugging lumen-llm failures involving template loading, OpenRouter requests, JSON parsing, cache behavior, or Rails integration.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# lumen-llm Debugging
|
|
7
|
+
|
|
8
|
+
Use this skill when a user reports a failing `LumenLLM.run` call.
|
|
9
|
+
|
|
10
|
+
## Checklist
|
|
11
|
+
|
|
12
|
+
1. Confirm the template path and template key.
|
|
13
|
+
2. Render the template locally with `bin/debug-template`.
|
|
14
|
+
3. Check `OPENROUTER_API_KEY` or explicit `config.openrouter_api_key`.
|
|
15
|
+
4. Confirm the model name is available through OpenRouter.
|
|
16
|
+
5. If JSON parsing fails, inspect the raw response content for markdown fences or explanations.
|
|
17
|
+
6. If cache seems stale, retry with `force: true`.
|
|
18
|
+
7. If Rails integration fails, require `lumen_llm` and inspect the initializer.
|
|
19
|
+
|
|
20
|
+
## Useful Commands
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
bin/test
|
|
24
|
+
bin/debug-template examples/templates/translator.yml examples/inputs/translator.json
|
|
25
|
+
ruby -Ilib -e 'require "lumen_llm"; p LumenLLM.configuration'
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Common Causes
|
|
29
|
+
|
|
30
|
+
- Missing template path.
|
|
31
|
+
- Template filename does not match the key passed to `LumenLLM.run`.
|
|
32
|
+
- The model returned non-JSON while `output_type: json` was set.
|
|
33
|
+
- Redis was assumed to exist but no store was configured.
|
|
34
|
+
- Rails app used `Settings.openrouter`; migrate to explicit `LumenLLM.configure`.
|
|
35
|
+
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: lumen-llm-setup
|
|
3
|
+
description: Use when installing, configuring, or migrating the lumen-llm Ruby gem in a Ruby or Rails app, especially Rails 4/5 apps that need a lightweight OpenRouter LLM runner.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# lumen-llm Setup
|
|
7
|
+
|
|
8
|
+
Use this skill when a user asks to add `lumen-llm`, migrate from an internal `Lumen` module, or configure OpenRouter credentials.
|
|
9
|
+
|
|
10
|
+
## Workflow
|
|
11
|
+
|
|
12
|
+
1. Inspect the app Ruby and Rails versions before editing.
|
|
13
|
+
2. Add `gem "lumen-llm"` to the Gemfile.
|
|
14
|
+
3. Add `require "lumen_llm"` only when the app does not use Bundler autorequire.
|
|
15
|
+
4. For Rails, create `config/initializers/lumen_llm.rb`.
|
|
16
|
+
5. Configure `template_path`, `logger`, `openrouter_api_key`, `openrouter_referer`, and `openrouter_title`.
|
|
17
|
+
6. Choose a store:
|
|
18
|
+
- no store for default no-cache behavior;
|
|
19
|
+
- `MemoryStore` for local scripts and tests;
|
|
20
|
+
- `RedisStore` with an app-owned Redis client for production cache/stats.
|
|
21
|
+
7. Add a template under `lumen_templates/`.
|
|
22
|
+
8. Verify with a fake provider or a single manual OpenRouter call.
|
|
23
|
+
|
|
24
|
+
## Rails Initializer Pattern
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
LumenLLM.configure do |config|
|
|
28
|
+
config.template_path = Rails.root.join("lumen_templates").to_s
|
|
29
|
+
config.logger = Rails.logger
|
|
30
|
+
config.openrouter_api_key = ENV["OPENROUTER_API_KEY"]
|
|
31
|
+
config.openrouter_referer = "https://www.example.com"
|
|
32
|
+
config.openrouter_title = "MyRailsApp"
|
|
33
|
+
end
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Migration Notes
|
|
37
|
+
|
|
38
|
+
- Prefer new calls as `LumenLLM.run(...)`.
|
|
39
|
+
- Existing `Lumen.run(...)`, `Lumen::TemplateLoader`, and `Lumen::Runner` are supported through the compatibility alias.
|
|
40
|
+
- Do not depend on app-specific `Settings`; pass values through `LumenLLM.configure`.
|
|
41
|
+
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: lumen-llm-template-authoring
|
|
3
|
+
description: Use when creating or reviewing lumen-llm YAML prompt templates, especially templates that require JSON output and Ruby 2.3/Rails 4 compatibility.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# lumen-llm Template Authoring
|
|
7
|
+
|
|
8
|
+
Use this skill when writing templates under `lumen_templates/` or `examples/templates/`.
|
|
9
|
+
|
|
10
|
+
## Template Shape
|
|
11
|
+
|
|
12
|
+
```yaml
|
|
13
|
+
key: translator
|
|
14
|
+
model: openai/gpt-5-mini
|
|
15
|
+
provider: openrouter
|
|
16
|
+
output_type: json
|
|
17
|
+
|
|
18
|
+
system_prompt: |
|
|
19
|
+
Return only valid JSON.
|
|
20
|
+
|
|
21
|
+
user_prompt: |
|
|
22
|
+
Input: {{input_json}}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Rules
|
|
26
|
+
|
|
27
|
+
- Use `{{variable_name}}` placeholders.
|
|
28
|
+
- Pass structured data as JSON strings from Ruby.
|
|
29
|
+
- Set `output_type: json` only when the model is instructed to return raw JSON.
|
|
30
|
+
- Tell the model not to wrap JSON in markdown.
|
|
31
|
+
- Preserve placeholders and product names when translating UI copy.
|
|
32
|
+
- Keep templates app-specific; the gem should only ship generic examples.
|
|
33
|
+
|
|
34
|
+
## Test Pattern
|
|
35
|
+
|
|
36
|
+
Use `LumenLLM::TemplateLoader.load` to load the template and `template.render(input)` to verify the rendered messages. Use a fake provider for full runner tests.
|
|
37
|
+
|
metadata
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: lumen-llm
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Dong Xu
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: minitest
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '5.10'
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '5.16'
|
|
22
|
+
type: :development
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - ">="
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: '5.10'
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '5.16'
|
|
32
|
+
- !ruby/object:Gem::Dependency
|
|
33
|
+
name: mutex_m
|
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - ">="
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: '0.1'
|
|
39
|
+
- - "<"
|
|
40
|
+
- !ruby/object:Gem::Version
|
|
41
|
+
version: '0.2'
|
|
42
|
+
type: :development
|
|
43
|
+
prerelease: false
|
|
44
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
45
|
+
requirements:
|
|
46
|
+
- - ">="
|
|
47
|
+
- !ruby/object:Gem::Version
|
|
48
|
+
version: '0.1'
|
|
49
|
+
- - "<"
|
|
50
|
+
- !ruby/object:Gem::Version
|
|
51
|
+
version: '0.2'
|
|
52
|
+
- !ruby/object:Gem::Dependency
|
|
53
|
+
name: rake
|
|
54
|
+
requirement: !ruby/object:Gem::Requirement
|
|
55
|
+
requirements:
|
|
56
|
+
- - ">="
|
|
57
|
+
- !ruby/object:Gem::Version
|
|
58
|
+
version: '12.3'
|
|
59
|
+
- - "<"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '14'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - ">="
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '12.3'
|
|
69
|
+
- - "<"
|
|
70
|
+
- !ruby/object:Gem::Version
|
|
71
|
+
version: '14'
|
|
72
|
+
description: lumen-llm provides YAML prompt templates, single-call OpenRouter chat
|
|
73
|
+
completion, JSON/text parsing, optional cache stores, and Rails 4+ integration without
|
|
74
|
+
runtime gem dependencies.
|
|
75
|
+
email:
|
|
76
|
+
- uxgnod@gmail.com
|
|
77
|
+
executables: []
|
|
78
|
+
extensions: []
|
|
79
|
+
extra_rdoc_files: []
|
|
80
|
+
files:
|
|
81
|
+
- AGENTS.md
|
|
82
|
+
- CHANGELOG.md
|
|
83
|
+
- CONTRIBUTING.md
|
|
84
|
+
- LICENSE.txt
|
|
85
|
+
- README.md
|
|
86
|
+
- SECURITY.md
|
|
87
|
+
- examples/inputs/translator.json
|
|
88
|
+
- examples/templates/translator.yml
|
|
89
|
+
- lib/lumen.rb
|
|
90
|
+
- lib/lumen_llm.rb
|
|
91
|
+
- lib/lumen_llm/configuration.rb
|
|
92
|
+
- lib/lumen_llm/errors.rb
|
|
93
|
+
- lib/lumen_llm/parser.rb
|
|
94
|
+
- lib/lumen_llm/provider_registry.rb
|
|
95
|
+
- lib/lumen_llm/providers/open_router/client.rb
|
|
96
|
+
- lib/lumen_llm/providers/open_router/parser.rb
|
|
97
|
+
- lib/lumen_llm/railtie.rb
|
|
98
|
+
- lib/lumen_llm/runner.rb
|
|
99
|
+
- lib/lumen_llm/stores/memory_store.rb
|
|
100
|
+
- lib/lumen_llm/stores/null_store.rb
|
|
101
|
+
- lib/lumen_llm/stores/redis_store.rb
|
|
102
|
+
- lib/lumen_llm/template.rb
|
|
103
|
+
- lib/lumen_llm/template_loader.rb
|
|
104
|
+
- lib/lumen_llm/version.rb
|
|
105
|
+
- skills/lumen-llm-debugging/SKILL.md
|
|
106
|
+
- skills/lumen-llm-setup/SKILL.md
|
|
107
|
+
- skills/lumen-llm-template-authoring/SKILL.md
|
|
108
|
+
homepage: https://github.com/uxgnod/lumen-llm
|
|
109
|
+
licenses:
|
|
110
|
+
- MIT
|
|
111
|
+
metadata:
|
|
112
|
+
source_code_uri: https://github.com/uxgnod/lumen-llm
|
|
113
|
+
changelog_uri: https://github.com/uxgnod/lumen-llm/blob/main/CHANGELOG.md
|
|
114
|
+
bug_tracker_uri: https://github.com/uxgnod/lumen-llm/issues
|
|
115
|
+
allowed_push_host: https://rubygems.org
|
|
116
|
+
rubygems_mfa_required: 'true'
|
|
117
|
+
rdoc_options: []
|
|
118
|
+
require_paths:
|
|
119
|
+
- lib
|
|
120
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
121
|
+
requirements:
|
|
122
|
+
- - ">="
|
|
123
|
+
- !ruby/object:Gem::Version
|
|
124
|
+
version: '2.3'
|
|
125
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
126
|
+
requirements:
|
|
127
|
+
- - ">="
|
|
128
|
+
- !ruby/object:Gem::Version
|
|
129
|
+
version: '0'
|
|
130
|
+
requirements: []
|
|
131
|
+
rubygems_version: 3.6.9
|
|
132
|
+
specification_version: 4
|
|
133
|
+
summary: A tiny Ruby/Rails-friendly LLM prompt runner for OpenRouter.
|
|
134
|
+
test_files: []
|