ruby_llm-providers-apfel 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/.flayignore +1 -0
- data/.github/workflows/ci.yml +32 -0
- data/.github/workflows/gitleaks.yml +22 -0
- data/.github/workflows/release.yml +36 -0
- data/.overcommit.yml +31 -0
- data/.rspec +2 -0
- data/.rubocop.yml +29 -0
- data/Archspec.rb +14 -0
- data/LICENSE +21 -0
- data/README.md +85 -0
- data/lib/ruby_llm/providers/apfel/connection_guard.rb +28 -0
- data/lib/ruby_llm/providers/apfel/version.rb +15 -0
- data/lib/ruby_llm/providers/apfel.rb +52 -0
- data/spec/ruby_llm/chat_schema_spec.rb +30 -0
- data/spec/ruby_llm/chat_spec.rb +35 -0
- data/spec/ruby_llm/chat_streaming_spec.rb +22 -0
- data/spec/ruby_llm/chat_tools_spec.rb +30 -0
- data/spec/ruby_llm/embedding_spec.rb +49 -0
- data/spec/ruby_llm/image_spec.rb +23 -0
- data/spec/ruby_llm/models_spec.rb +11 -0
- data/spec/ruby_llm/moderation_spec.rb +22 -0
- data/spec/ruby_llm/providers/apfel_spec.rb +32 -0
- data/spec/ruby_llm/rerank_spec.rb +23 -0
- data/spec/ruby_llm/speech_spec.rb +25 -0
- data/spec/ruby_llm/video_spec.rb +27 -0
- data/spec/spec_helper.rb +26 -0
- data/spec/support/models.rb +19 -0
- data/spec/support/rubyllm_configuration.rb +14 -0
- data/spec/support/vcr_configuration.rb +16 -0
- metadata +90 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 691f6aca0f671994fe947ed562e4b35720453513152f8b5d959023266679bc97
|
|
4
|
+
data.tar.gz: 87e2c3d286eb1e31575cfc661a66f95015a36fc5f5a39ed02404e82925a86043
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 6db0f5d2633e80c9897733b133b77fdd472dfad063449a4b10361788ae2b7192649f8a440922a926c1404025b7654301c2291bbb8c9124fbd352d0831c742d3c
|
|
7
|
+
data.tar.gz: 3178067325ab285c5b62e8bd1e78ed32c65990b9fbd509ed4b8a0cabcf06dedebe83f4db8b21850da37b71a43498dd3d69265ceb30a100cd498c6e77c1e7a835
|
data/.flayignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
spec/support/streaming_error_helpers.rb
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
branches: ["main"]
|
|
6
|
+
push:
|
|
7
|
+
branches: ["main"]
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
test:
|
|
14
|
+
name: Ruby ${{ matrix.ruby-version }}
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
strategy:
|
|
17
|
+
fail-fast: false
|
|
18
|
+
matrix:
|
|
19
|
+
ruby-version: ["3.1", "3.2", "3.3", "3.4", "4.0"]
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v6
|
|
22
|
+
|
|
23
|
+
- uses: ruby/setup-ruby@v1
|
|
24
|
+
with:
|
|
25
|
+
ruby-version: ${{ matrix.ruby-version }}
|
|
26
|
+
bundler-cache: true
|
|
27
|
+
|
|
28
|
+
- run: bundle exec rake
|
|
29
|
+
if: matrix.ruby-version != '3.1'
|
|
30
|
+
|
|
31
|
+
- run: bundle exec rake rubocop flay spec
|
|
32
|
+
if: matrix.ruby-version == '3.1'
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
name: Gitleaks
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
branches: ["main"]
|
|
6
|
+
push:
|
|
7
|
+
branches: ["main"]
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
scan:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v6
|
|
17
|
+
with:
|
|
18
|
+
fetch-depth: 0
|
|
19
|
+
|
|
20
|
+
- uses: gitleaks/gitleaks-action@v2
|
|
21
|
+
env:
|
|
22
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
publish:
|
|
13
|
+
name: Build and publish
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v6
|
|
17
|
+
|
|
18
|
+
- uses: ruby/setup-ruby@v1
|
|
19
|
+
with:
|
|
20
|
+
ruby-version: "3.4"
|
|
21
|
+
bundler-cache: true
|
|
22
|
+
|
|
23
|
+
- run: bundle exec rake
|
|
24
|
+
|
|
25
|
+
- name: Build gem
|
|
26
|
+
run: gem build ruby_llm-providers-apfel.gemspec
|
|
27
|
+
|
|
28
|
+
- name: Publish to RubyGems
|
|
29
|
+
run: |
|
|
30
|
+
mkdir -p "$HOME/.gem"
|
|
31
|
+
touch "$HOME/.gem/credentials"
|
|
32
|
+
chmod 0600 "$HOME/.gem/credentials"
|
|
33
|
+
printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > "$HOME/.gem/credentials"
|
|
34
|
+
gem push ruby_llm-providers-apfel-*.gem
|
|
35
|
+
env:
|
|
36
|
+
GEM_HOST_API_KEY: ${{ secrets.RUBYGEMS_AUTH_TOKEN }}
|
data/.overcommit.yml
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
PreCommit:
|
|
2
|
+
RuboCop:
|
|
3
|
+
enabled: true
|
|
4
|
+
auto_correct: true
|
|
5
|
+
on_warn: fail
|
|
6
|
+
|
|
7
|
+
Flay:
|
|
8
|
+
enabled: true
|
|
9
|
+
include:
|
|
10
|
+
- lib/**/*.rb
|
|
11
|
+
- spec/**/*.rb
|
|
12
|
+
mass_threshold: 70
|
|
13
|
+
|
|
14
|
+
ArchSpec:
|
|
15
|
+
enabled: true
|
|
16
|
+
command: ['bundle', 'exec', 'archspec', 'check']
|
|
17
|
+
on_warn: fail
|
|
18
|
+
|
|
19
|
+
RSpec:
|
|
20
|
+
enabled: true
|
|
21
|
+
command: ['bundle', 'exec', 'rspec']
|
|
22
|
+
on_warn: fail
|
|
23
|
+
|
|
24
|
+
TrailingWhitespace:
|
|
25
|
+
enabled: true
|
|
26
|
+
auto_correct: true
|
|
27
|
+
|
|
28
|
+
Gitleaks:
|
|
29
|
+
enabled: true
|
|
30
|
+
required_executable: gitleaks
|
|
31
|
+
command: ['gitleaks', 'protect', '--staged', '--redact', '--verbose']
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
plugins:
|
|
2
|
+
- rubocop-performance
|
|
3
|
+
- rubocop-rake
|
|
4
|
+
- rubocop-rspec
|
|
5
|
+
|
|
6
|
+
AllCops:
|
|
7
|
+
NewCops: enable
|
|
8
|
+
TargetRubyVersion: 3.1
|
|
9
|
+
Exclude:
|
|
10
|
+
- Archspec.rb
|
|
11
|
+
- vendor/**/*
|
|
12
|
+
- gemfiles/**/*
|
|
13
|
+
SuggestExtensions: false
|
|
14
|
+
|
|
15
|
+
Metrics/AbcSize:
|
|
16
|
+
Max: 80
|
|
17
|
+
Metrics/MethodLength:
|
|
18
|
+
Max: 40
|
|
19
|
+
CountAsOne:
|
|
20
|
+
- array
|
|
21
|
+
- hash
|
|
22
|
+
- heredoc
|
|
23
|
+
Metrics/BlockLength:
|
|
24
|
+
Exclude:
|
|
25
|
+
- spec/**/*.rb
|
|
26
|
+
RSpec/ExampleLength:
|
|
27
|
+
Enabled: false
|
|
28
|
+
RSpec/MultipleExpectations:
|
|
29
|
+
Enabled: false
|
data/Archspec.rb
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
source 'lib/**/*.rb'
|
|
4
|
+
|
|
5
|
+
component :provider,
|
|
6
|
+
in: %w[
|
|
7
|
+
lib/ruby_llm/providers/apfel.rb
|
|
8
|
+
lib/ruby_llm/providers/apfel/**/*.rb
|
|
9
|
+
],
|
|
10
|
+
namespace: 'RubyLLM::Providers::Apfel'
|
|
11
|
+
|
|
12
|
+
provider.cannot_reference_constants 'RSpec', 'WebMock', 'VCR'
|
|
13
|
+
|
|
14
|
+
preset :ruby_conventions
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 your-github-org
|
|
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.
|
data/README.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# ruby_llm-providers-apfel
|
|
2
|
+
|
|
3
|
+
RubyLLM provider gem for [Apfel](https://apfel.franzai.com/), a CLI tool that exposes Apple's
|
|
4
|
+
on-device Foundation Models framework through an OpenAI-compatible HTTP server. With Apfel and
|
|
5
|
+
this gem, RubyLLM can talk to Apple's on-device model (AFM) as a fully local, offline LLM
|
|
6
|
+
provider — no API key, no cloud calls, no per-token billing.
|
|
7
|
+
|
|
8
|
+
## Requirements
|
|
9
|
+
|
|
10
|
+
- Apple Silicon Mac (M-series)
|
|
11
|
+
- macOS 26 (Tahoe) or later
|
|
12
|
+
- Apple Intelligence enabled
|
|
13
|
+
|
|
14
|
+
## Installing Apfel
|
|
15
|
+
|
|
16
|
+
Apfel itself is a separate Homebrew package, not a Ruby gem. Install it once per machine:
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
brew install apfel
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Start it as a local OpenAI-compatible server:
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
apfel --serve
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
By default the server listens at `http://127.0.0.1:11434/v1`. Authentication is optional — pass
|
|
29
|
+
`--token <value>` to `apfel --serve` if you want to require a Bearer token; otherwise no API key
|
|
30
|
+
is needed at all.
|
|
31
|
+
|
|
32
|
+
Apfel serves Apple's on-device Foundation Model (AFM, ~3B parameters) running entirely on the
|
|
33
|
+
Neural Engine. It has a combined input/output context window of 4,096 tokens, so keep prompts and
|
|
34
|
+
expected responses within that budget.
|
|
35
|
+
|
|
36
|
+
## Installing the gem
|
|
37
|
+
|
|
38
|
+
Add this line to your application's Gemfile:
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
gem 'ruby_llm-providers-apfel', require: 'ruby_llm/providers/apfel'
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Then configure the provider to point at your local Apfel server:
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
require 'ruby_llm/providers/apfel'
|
|
48
|
+
|
|
49
|
+
RubyLLM.configure do |config|
|
|
50
|
+
config.apfel_api_base = ENV.fetch('APFEL_API_BASE', 'http://127.0.0.1:11434/v1')
|
|
51
|
+
# Only needed if you started `apfel --serve --token <value>`
|
|
52
|
+
config.apfel_api_key = ENV['APFEL_API_KEY']
|
|
53
|
+
end
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Since Apfel runs locally with no API key required, `apfel_api_key` can be left unset unless you
|
|
57
|
+
started the server with `--token`.
|
|
58
|
+
|
|
59
|
+
## Usage
|
|
60
|
+
|
|
61
|
+
```ruby
|
|
62
|
+
model = RubyLLM.models.by_provider(:apfel).chat_models.first
|
|
63
|
+
chat = RubyLLM.chat(model: model.id)
|
|
64
|
+
response = chat.ask('Hello')
|
|
65
|
+
puts response.content
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Development
|
|
69
|
+
|
|
70
|
+
The generator installs the bundle and creates an ignored `.env`. Edit the generated `op read` reference so it points to your 1Password credential. If you do not use the 1Password CLI, replace the expression with the provider key.
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
bundle exec rake models
|
|
74
|
+
bundle exec rake
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`rake models` calls only Apfel's model-listing endpoint and writes `models.json` at the gem root. RubyLLM loads that catalog as a fallback when this provider is registered, so applications do not need to combine registry files. The main RubyLLM registry always wins when both catalogs carry the same model. If the API uses a different path, change `models_url` in `lib/ruby_llm/providers/apfel.rb`.
|
|
78
|
+
|
|
79
|
+
Run `rake models` from this provider gem when you want to update its packaged catalog. `RubyLLM.models.refresh` updates the application's main registry and does not refresh provider gem catalogs.
|
|
80
|
+
|
|
81
|
+
If the provider has no model-listing endpoint, uncomment `assume_models_exist?` in the provider and do not run `rake models`.
|
|
82
|
+
|
|
83
|
+
The suite always runs the provider integration specs. The first local run calls the API and records VCR cassettes; CI only replays committed cassettes. A failing example deletes its cassette so the next local run tests the live API again.
|
|
84
|
+
|
|
85
|
+
After refreshing the catalog, add real model IDs to `spec/support/models.rb`. Keep only the operation matrices the provider supports. The portable contract specs are adapted from [RubyLLM's live specs](https://github.com/crmne/ruby_llm/tree/main/spec/ruby_llm); use those as the reference when your provider needs coverage for another feature or dialect.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'delegate'
|
|
4
|
+
|
|
5
|
+
module RubyLLM
|
|
6
|
+
module Providers
|
|
7
|
+
class Apfel < Provider
|
|
8
|
+
# Wraps the transport connection so a request against a server that
|
|
9
|
+
# is not running surfaces as a RubyLLM::Error explaining how to start
|
|
10
|
+
# Apfel, instead of a raw Faraday::ConnectionFailed.
|
|
11
|
+
class ConnectionGuard < SimpleDelegator
|
|
12
|
+
%i[get post patch delete].each do |verb|
|
|
13
|
+
define_method(verb) do |*args, **kwargs, &block|
|
|
14
|
+
__getobj__.public_send(verb, *args, **kwargs, &block)
|
|
15
|
+
rescue Faraday::ConnectionFailed
|
|
16
|
+
raise Error, unreachable_message
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def unreachable_message
|
|
21
|
+
"Could not connect to the Apfel server at #{provider.api_base}. " \
|
|
22
|
+
'Install it with `brew install apfel` if needed, then start it with `apfel --serve`, ' \
|
|
23
|
+
'and try again.'
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Providers
|
|
5
|
+
# Version of the ruby_llm-providers-apfel gem. Kept in its own file so the
|
|
6
|
+
# gemspec can read the literal without loading the provider.
|
|
7
|
+
class Apfel < Provider
|
|
8
|
+
VERSION = '0.1.0'
|
|
9
|
+
|
|
10
|
+
def self.version
|
|
11
|
+
VERSION
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'ruby_llm'
|
|
4
|
+
require 'ruby_llm/providers/apfel/version'
|
|
5
|
+
require 'ruby_llm/providers/apfel/connection_guard'
|
|
6
|
+
|
|
7
|
+
module RubyLLM
|
|
8
|
+
module Providers
|
|
9
|
+
# Apfel API integration.
|
|
10
|
+
class Apfel < Provider
|
|
11
|
+
# Apfel's ChatCompletions protocol.
|
|
12
|
+
class ChatCompletions < Protocols::ChatCompletions
|
|
13
|
+
def models_url
|
|
14
|
+
'models'
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
protocol :chat_completions, ChatCompletions
|
|
19
|
+
|
|
20
|
+
def initialize(config)
|
|
21
|
+
super
|
|
22
|
+
@connection = ConnectionGuard.new(@connection)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def api_base
|
|
26
|
+
@config.apfel_api_base || 'https://api.example.com/v1'
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def headers
|
|
30
|
+
{ 'Authorization' => "Bearer #{@config.apfel_api_key}" }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
class << self
|
|
34
|
+
def configuration_options
|
|
35
|
+
%i[apfel_api_key apfel_api_base]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def configuration_requirements
|
|
39
|
+
%i[apfel_api_key]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Use this only when the provider has no model-listing endpoint.
|
|
43
|
+
# def assume_models_exist?
|
|
44
|
+
# true
|
|
45
|
+
# end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
RubyLLM::Provider.register :apfel, RubyLLM::Providers::Apfel,
|
|
52
|
+
models: File.expand_path('../../../models.json', __dir__)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe RubyLLM::Chat, :live do
|
|
6
|
+
include_context 'with configured RubyLLM'
|
|
7
|
+
|
|
8
|
+
let(:person_schema) do
|
|
9
|
+
{
|
|
10
|
+
type: 'object',
|
|
11
|
+
properties: {
|
|
12
|
+
name: { type: 'string' },
|
|
13
|
+
age: { type: 'integer' }
|
|
14
|
+
},
|
|
15
|
+
required: %w[name age],
|
|
16
|
+
additionalProperties: false
|
|
17
|
+
}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
each_model(STRUCTURED_OUTPUT_MODELS) do |provider, model|
|
|
21
|
+
it "#{provider}/#{model} returns structured output" do
|
|
22
|
+
response = RubyLLM.chat(model: model, provider: provider, assume_model_exists: true)
|
|
23
|
+
.with_schema(person_schema)
|
|
24
|
+
.ask('Generate a person named John who is 30 years old')
|
|
25
|
+
|
|
26
|
+
expect(response.content).to be_a(String)
|
|
27
|
+
expect(response.parsed).to include('name' => 'John', 'age' => 30)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe RubyLLM::Chat, :live do
|
|
6
|
+
include_context 'with configured RubyLLM'
|
|
7
|
+
|
|
8
|
+
each_model(CHAT_MODELS) do |provider, model|
|
|
9
|
+
it "#{provider}/#{model} can have a basic conversation" do
|
|
10
|
+
response = RubyLLM.chat(model: model, provider: provider, assume_model_exists: true).ask("What's 2 + 2?")
|
|
11
|
+
|
|
12
|
+
expect(response.content).to include('4')
|
|
13
|
+
expect(response.role).to eq(:assistant)
|
|
14
|
+
expect(response.tokens.input.to_i).to be_positive
|
|
15
|
+
expect(response.tokens.output.to_i).to be_positive
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it "#{provider}/#{model} returns the raw response" do
|
|
19
|
+
response = RubyLLM.chat(model: model, provider: provider, assume_model_exists: true)
|
|
20
|
+
.ask('What is the capital of France?')
|
|
21
|
+
|
|
22
|
+
expect(response.raw.status).to eq(200)
|
|
23
|
+
expect(response.raw.headers).not_to be_empty
|
|
24
|
+
expect(response.raw.body).not_to be_empty
|
|
25
|
+
expect(response.raw.env.request_body).not_to be_empty
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it "#{provider}/#{model} can handle a multi-turn conversation" do
|
|
29
|
+
chat = RubyLLM.chat(model: model, provider: provider, assume_model_exists: true)
|
|
30
|
+
|
|
31
|
+
expect(chat.ask('Who created the programming language Ruby?').content).to match(/Matz|Matsumoto/i)
|
|
32
|
+
expect(chat.ask('What year was Ruby first released?').content).to include('199')
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe RubyLLM::Chat, :live do
|
|
6
|
+
include_context 'with configured RubyLLM'
|
|
7
|
+
|
|
8
|
+
each_model(CHAT_MODELS) do |provider, model|
|
|
9
|
+
it "#{provider}/#{model} supports streaming responses" do
|
|
10
|
+
chunks = []
|
|
11
|
+
chat = RubyLLM.chat(model: model, provider: provider, assume_model_exists: true)
|
|
12
|
+
|
|
13
|
+
response = chat.ask('Count from 1 to 3') { |chunk| chunks << chunk }
|
|
14
|
+
|
|
15
|
+
expect(chunks).not_to be_empty
|
|
16
|
+
expect(chunks.first).to be_a(RubyLLM::Chunk)
|
|
17
|
+
expect(response.raw.status).to eq(200)
|
|
18
|
+
expect(response.raw.headers).not_to be_empty
|
|
19
|
+
expect(response.raw.env.request_body).not_to be_empty
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe RubyLLM::Chat, :live do
|
|
6
|
+
include_context 'with configured RubyLLM'
|
|
7
|
+
|
|
8
|
+
let(:weather_tool) do
|
|
9
|
+
Class.new(RubyLLM::Tool) do
|
|
10
|
+
description 'Gets current weather for a location'
|
|
11
|
+
parameter :latitude, description: 'Latitude'
|
|
12
|
+
parameter :longitude, description: 'Longitude'
|
|
13
|
+
|
|
14
|
+
def execute(latitude:, longitude:)
|
|
15
|
+
"Current weather at #{latitude}, #{longitude}: 15°C, Wind: 10 km/h"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
each_model(TOOL_MODELS) do |provider, model|
|
|
21
|
+
it "#{provider}/#{model} can use tools" do
|
|
22
|
+
chat = RubyLLM.chat(model: model, provider: provider, assume_model_exists: true).with_tools(weather_tool)
|
|
23
|
+
response = chat.ask("What's the weather in Berlin? Use 52.5200, 13.4050.")
|
|
24
|
+
|
|
25
|
+
expect(response.content).to include('15')
|
|
26
|
+
expect(response.content).to include('10')
|
|
27
|
+
expect(chat.messages.any?(&:tool_call?)).to be(true)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe RubyLLM::Embedding, :live do
|
|
6
|
+
include_context 'with configured RubyLLM'
|
|
7
|
+
|
|
8
|
+
each_model(EMBEDDING_MODELS) do |provider, model, model_info|
|
|
9
|
+
it "#{provider}/#{model} embeds one text" do
|
|
10
|
+
embedding = RubyLLM.embed(
|
|
11
|
+
"Ruby is a programmer's best friend",
|
|
12
|
+
model: model,
|
|
13
|
+
provider: provider,
|
|
14
|
+
assume_model_exists: true
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
expect(embedding.vectors).to be_an(Array)
|
|
18
|
+
expect(embedding.vectors.first).to be_a(Numeric)
|
|
19
|
+
expect(embedding.model).to eq(model)
|
|
20
|
+
expect(embedding.tokens.input.to_i).to be >= 0
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "#{provider}/#{model} embeds several texts" do
|
|
24
|
+
embeddings = RubyLLM.embed(
|
|
25
|
+
%w[Ruby Python JavaScript],
|
|
26
|
+
model: model,
|
|
27
|
+
provider: provider,
|
|
28
|
+
assume_model_exists: true
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
expect(embeddings.vectors.size).to eq(3)
|
|
32
|
+
expect(embeddings.vectors).to all(be_an(Array))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
next unless model_info[:dimensions]
|
|
36
|
+
|
|
37
|
+
it "#{provider}/#{model} supports custom dimensions" do
|
|
38
|
+
embedding = RubyLLM.embed(
|
|
39
|
+
'Ruby',
|
|
40
|
+
model: model,
|
|
41
|
+
provider: provider,
|
|
42
|
+
assume_model_exists: true,
|
|
43
|
+
dimensions: model_info[:dimensions]
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
expect(embedding.vectors.length).to eq(model_info[:dimensions])
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe RubyLLM::Image, :live do
|
|
6
|
+
include_context 'with configured RubyLLM'
|
|
7
|
+
|
|
8
|
+
each_model(IMAGE_GENERATION_MODELS) do |provider, model, model_info|
|
|
9
|
+
it "#{provider}/#{model} paints an image" do
|
|
10
|
+
image = RubyLLM.paint(
|
|
11
|
+
'a siamese cat',
|
|
12
|
+
model: model,
|
|
13
|
+
provider: provider,
|
|
14
|
+
assume_model_exists: true,
|
|
15
|
+
provider_options: model_info.fetch(:provider_options, {})
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
expect(image.mime_type).to include('image')
|
|
19
|
+
expect(image.model).to eq(model)
|
|
20
|
+
expect(image.to_blob.bytesize).to be > 1000
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe RubyLLM::Models do
|
|
6
|
+
include_context 'with configured RubyLLM'
|
|
7
|
+
|
|
8
|
+
it 'expects generated providers to add registry metadata before use' do
|
|
9
|
+
expect(RubyLLM::Providers::Apfel.assume_models_exist?).to be(false)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe RubyLLM::Moderation, :live do
|
|
6
|
+
include_context 'with configured RubyLLM'
|
|
7
|
+
|
|
8
|
+
each_model(MODERATION_MODELS) do |provider, model|
|
|
9
|
+
it "#{provider}/#{model} moderates content" do
|
|
10
|
+
moderation = RubyLLM.moderate(
|
|
11
|
+
'This is a safe message',
|
|
12
|
+
model: model,
|
|
13
|
+
provider: provider,
|
|
14
|
+
assume_model_exists: true
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
expect(moderation).to be_a(described_class)
|
|
18
|
+
expect(moderation.results).to all(be_a(RubyLLM::Moderation::Result))
|
|
19
|
+
expect(moderation.flagged?).to be_in([true, false])
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe RubyLLM::Providers::Apfel do
|
|
6
|
+
subject(:provider) { described_class.new(config) }
|
|
7
|
+
|
|
8
|
+
let(:config) do
|
|
9
|
+
RubyLLM::Configuration.new.tap do |provider_config|
|
|
10
|
+
provider_config.apfel_api_key = 'test-key'
|
|
11
|
+
provider_config.apfel_api_base = 'https://example.test/v1'
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it 'is registered with RubyLLM' do
|
|
16
|
+
expect(RubyLLM::Provider.resolve(:apfel)).to eq(described_class)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it 'registers a default protocol' do
|
|
20
|
+
expect(described_class.protocols).to include(chat_completions: described_class::ChatCompletions)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'declares provider configuration' do
|
|
24
|
+
expect(described_class.configuration_options).to eq(%i[apfel_api_key apfel_api_base])
|
|
25
|
+
expect(described_class.configuration_requirements).to eq(%i[apfel_api_key])
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'uses configured API base and bearer token' do
|
|
29
|
+
expect(provider.api_base).to eq('https://example.test/v1')
|
|
30
|
+
expect(provider.headers).to eq('Authorization' => 'Bearer test-key')
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe RubyLLM::Rerank, :live do
|
|
6
|
+
include_context 'with configured RubyLLM'
|
|
7
|
+
|
|
8
|
+
each_model(RERANK_MODELS) do |provider, model|
|
|
9
|
+
it "#{provider}/#{model} orders documents by relevance" do
|
|
10
|
+
rerank = RubyLLM.rerank(
|
|
11
|
+
'What is the capital of the United States?',
|
|
12
|
+
['Carson City is the capital of Nevada.', 'Washington, D.C. is the capital of the United States.'],
|
|
13
|
+
model: model,
|
|
14
|
+
provider: provider,
|
|
15
|
+
assume_model_exists: true
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
expect(rerank.results.first.document).to include('Washington')
|
|
19
|
+
expect(rerank.results.first.score).to be > rerank.results.last.score
|
|
20
|
+
expect(rerank.results.map(&:index)).to contain_exactly(0, 1)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe RubyLLM::Speech, :live do
|
|
6
|
+
include_context 'with configured RubyLLM'
|
|
7
|
+
|
|
8
|
+
each_model(SPEECH_MODELS) do |provider, model, model_info|
|
|
9
|
+
it "#{provider}/#{model} speaks" do
|
|
10
|
+
speech = RubyLLM.speak(
|
|
11
|
+
'Ruby is a programming language designed for developer happiness.',
|
|
12
|
+
model: model,
|
|
13
|
+
provider: provider,
|
|
14
|
+
assume_model_exists: true,
|
|
15
|
+
voice: model_info[:voice],
|
|
16
|
+
format: model_info[:format]
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
expect(speech.data).to be_a(String)
|
|
20
|
+
expect(speech.data.bytesize).to be > 1000
|
|
21
|
+
expect(speech.model).to eq(model)
|
|
22
|
+
expect(speech.mime_type).to start_with('audio/')
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe RubyLLM::Video, :live do
|
|
6
|
+
include_context 'with configured RubyLLM'
|
|
7
|
+
|
|
8
|
+
before do
|
|
9
|
+
RubyLLM.config.video_generation_poll_interval = VCR.current_cassette&.recording? ? 5 : 0
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
each_model(VIDEO_GENERATION_MODELS) do |provider, model, model_info|
|
|
13
|
+
it "#{provider}/#{model} animates a video" do
|
|
14
|
+
video = RubyLLM.animate(
|
|
15
|
+
'a calm ocean wave at sunset',
|
|
16
|
+
model: model,
|
|
17
|
+
provider: provider,
|
|
18
|
+
assume_model_exists: true,
|
|
19
|
+
provider_options: model_info.fetch(:provider_options, {})
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
expect(video.mime_type).to include('video')
|
|
23
|
+
expect(video.url || video.data).not_to be_nil
|
|
24
|
+
expect(video.to_blob.bytesize).to be > 10_000
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
data/spec/spec_helper.rb
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bundler/setup'
|
|
4
|
+
require 'dotenv/load'
|
|
5
|
+
require 'fileutils'
|
|
6
|
+
|
|
7
|
+
require 'vcr'
|
|
8
|
+
require 'ruby_llm/providers/apfel'
|
|
9
|
+
require 'webmock/rspec'
|
|
10
|
+
|
|
11
|
+
Dir[File.expand_path('support/**/*.rb', __dir__)].each { |file| require file }
|
|
12
|
+
|
|
13
|
+
RSpec.configure do |config|
|
|
14
|
+
config.disable_monkey_patching!
|
|
15
|
+
config.expect_with(:rspec) { |expectations| expectations.syntax = :expect }
|
|
16
|
+
config.order = :random
|
|
17
|
+
Kernel.srand config.seed
|
|
18
|
+
|
|
19
|
+
config.around(:each, :live) do |example|
|
|
20
|
+
cassette_name = example.full_description.downcase.gsub(/[^a-z0-9]+/, '_').delete_prefix('_').delete_suffix('_')
|
|
21
|
+
cassette_path = File.join(VCR.configuration.cassette_library_dir, "#{cassette_name}.yml")
|
|
22
|
+
|
|
23
|
+
VCR.use_cassette(cassette_name) { example.run }
|
|
24
|
+
FileUtils.rm_f(cassette_path) if example.exception
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Remove chat-derived matrices the model does not support and add real ids for the empty operation matrices.
|
|
4
|
+
# See RubyLLM's full live matrix and specs: https://github.com/crmne/ruby_llm/tree/main/spec
|
|
5
|
+
PROVIDER = :apfel
|
|
6
|
+
CHAT_MODELS = [].freeze
|
|
7
|
+
TOOL_MODELS = CHAT_MODELS
|
|
8
|
+
STRUCTURED_OUTPUT_MODELS = CHAT_MODELS
|
|
9
|
+
|
|
10
|
+
EMBEDDING_MODELS = [].freeze
|
|
11
|
+
IMAGE_GENERATION_MODELS = [].freeze
|
|
12
|
+
SPEECH_MODELS = [].freeze
|
|
13
|
+
VIDEO_GENERATION_MODELS = [].freeze
|
|
14
|
+
MODERATION_MODELS = [].freeze
|
|
15
|
+
RERANK_MODELS = [].freeze
|
|
16
|
+
|
|
17
|
+
def each_model(models)
|
|
18
|
+
models.each { |model_info| yield model_info[:provider], model_info[:model], model_info }
|
|
19
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.shared_context 'with configured RubyLLM' do
|
|
4
|
+
before do
|
|
5
|
+
RubyLLM.configure do |config|
|
|
6
|
+
config.apfel_api_key = ENV.fetch('APFEL_API_KEY', 'test')
|
|
7
|
+
config.apfel_api_base = ENV.fetch('APFEL_API_BASE', 'https://api.example.com/v1')
|
|
8
|
+
config.max_retries = 0
|
|
9
|
+
config.retry_backoff_factor = 0
|
|
10
|
+
config.retry_interval = 0
|
|
11
|
+
config.retry_interval_randomness = 0
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
VCR.configure do |config|
|
|
4
|
+
config.cassette_library_dir = 'spec/fixtures/vcr_cassettes'
|
|
5
|
+
config.hook_into :webmock
|
|
6
|
+
config.default_cassette_options = { record: ENV['CI'] ? :none : :once }
|
|
7
|
+
config.allow_http_connections_when_no_cassette = true
|
|
8
|
+
config.filter_sensitive_data('<APFEL_API_KEY>') { ENV.fetch('APFEL_API_KEY', nil) }
|
|
9
|
+
config.filter_sensitive_data('<APFEL_API_BASE>') { ENV.fetch('APFEL_API_BASE', nil) }
|
|
10
|
+
|
|
11
|
+
config.before_record do |interaction|
|
|
12
|
+
next unless interaction.request.headers['Authorization']
|
|
13
|
+
|
|
14
|
+
interaction.request.headers['Authorization'] = ['Bearer <AUTH_TOKEN>']
|
|
15
|
+
end
|
|
16
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: ruby_llm-providers-apfel
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- madbomber
|
|
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: ruby_llm
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 2.0.0.rc1
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: 2.0.0.rc1
|
|
26
|
+
description: Adds Apfel provider support to RubyLLM. Apfel is a macOS-only local server
|
|
27
|
+
that exposes Apple's on-device Apple Intelligence model over an OpenAI-compatible
|
|
28
|
+
API, letting RubyLLM talk to it like any other provider.
|
|
29
|
+
email:
|
|
30
|
+
- dvanhoozer@gmail.com
|
|
31
|
+
executables: []
|
|
32
|
+
extensions: []
|
|
33
|
+
extra_rdoc_files: []
|
|
34
|
+
files:
|
|
35
|
+
- ".flayignore"
|
|
36
|
+
- ".github/workflows/ci.yml"
|
|
37
|
+
- ".github/workflows/gitleaks.yml"
|
|
38
|
+
- ".github/workflows/release.yml"
|
|
39
|
+
- ".overcommit.yml"
|
|
40
|
+
- ".rspec"
|
|
41
|
+
- ".rubocop.yml"
|
|
42
|
+
- Archspec.rb
|
|
43
|
+
- LICENSE
|
|
44
|
+
- README.md
|
|
45
|
+
- lib/ruby_llm/providers/apfel.rb
|
|
46
|
+
- lib/ruby_llm/providers/apfel/connection_guard.rb
|
|
47
|
+
- lib/ruby_llm/providers/apfel/version.rb
|
|
48
|
+
- spec/ruby_llm/chat_schema_spec.rb
|
|
49
|
+
- spec/ruby_llm/chat_spec.rb
|
|
50
|
+
- spec/ruby_llm/chat_streaming_spec.rb
|
|
51
|
+
- spec/ruby_llm/chat_tools_spec.rb
|
|
52
|
+
- spec/ruby_llm/embedding_spec.rb
|
|
53
|
+
- spec/ruby_llm/image_spec.rb
|
|
54
|
+
- spec/ruby_llm/models_spec.rb
|
|
55
|
+
- spec/ruby_llm/moderation_spec.rb
|
|
56
|
+
- spec/ruby_llm/providers/apfel_spec.rb
|
|
57
|
+
- spec/ruby_llm/rerank_spec.rb
|
|
58
|
+
- spec/ruby_llm/speech_spec.rb
|
|
59
|
+
- spec/ruby_llm/video_spec.rb
|
|
60
|
+
- spec/spec_helper.rb
|
|
61
|
+
- spec/support/models.rb
|
|
62
|
+
- spec/support/rubyllm_configuration.rb
|
|
63
|
+
- spec/support/vcr_configuration.rb
|
|
64
|
+
homepage: https://github.com/madbomber/ruby_llm-providers-apfel
|
|
65
|
+
licenses:
|
|
66
|
+
- MIT
|
|
67
|
+
metadata:
|
|
68
|
+
homepage_uri: https://github.com/madbomber/ruby_llm-providers-apfel
|
|
69
|
+
source_code_uri: https://github.com/madbomber/ruby_llm-providers-apfel
|
|
70
|
+
changelog_uri: https://github.com/madbomber/ruby_llm-providers-apfel/releases
|
|
71
|
+
bug_tracker_uri: https://github.com/madbomber/ruby_llm-providers-apfel/issues
|
|
72
|
+
rubygems_mfa_required: 'true'
|
|
73
|
+
rdoc_options: []
|
|
74
|
+
require_paths:
|
|
75
|
+
- lib
|
|
76
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
77
|
+
requirements:
|
|
78
|
+
- - ">="
|
|
79
|
+
- !ruby/object:Gem::Version
|
|
80
|
+
version: '3.1'
|
|
81
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
82
|
+
requirements:
|
|
83
|
+
- - ">="
|
|
84
|
+
- !ruby/object:Gem::Version
|
|
85
|
+
version: '0'
|
|
86
|
+
requirements: []
|
|
87
|
+
rubygems_version: 4.0.20
|
|
88
|
+
specification_version: 4
|
|
89
|
+
summary: RubyLLM provider for Apfel.
|
|
90
|
+
test_files: []
|