ruby_llm-providers-lms 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.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.flayignore +1 -0
  3. data/.github/workflows/ci.yml +32 -0
  4. data/.github/workflows/gitleaks.yml +22 -0
  5. data/.github/workflows/release.yml +36 -0
  6. data/.overcommit.yml +31 -0
  7. data/.rspec +2 -0
  8. data/.rubocop.yml +29 -0
  9. data/Archspec.rb +14 -0
  10. data/LICENSE +21 -0
  11. data/README.md +87 -0
  12. data/lib/ruby_llm/providers/lms/models.rb +96 -0
  13. data/lib/ruby_llm/providers/lms.rb +55 -0
  14. data/models.json +603 -0
  15. data/spec/fixtures/vcr_cassettes/rubyllm_chat_lms_openai_gpt_oss_20b_can_handle_a_multi_turn_conversation.yml +92 -0
  16. data/spec/fixtures/vcr_cassettes/rubyllm_chat_lms_openai_gpt_oss_20b_can_have_a_basic_conversation.yml +45 -0
  17. data/spec/fixtures/vcr_cassettes/rubyllm_chat_lms_openai_gpt_oss_20b_can_use_tools.yml +129 -0
  18. data/spec/fixtures/vcr_cassettes/rubyllm_chat_lms_openai_gpt_oss_20b_returns_the_raw_response.yml +73 -0
  19. data/spec/fixtures/vcr_cassettes/rubyllm_chat_lms_openai_gpt_oss_20b_supports_streaming_responses.yml +81 -0
  20. data/spec/fixtures/vcr_cassettes/rubyllm_chat_lms_qwen3_0_6b_bible_assistant_can_have_a_basic_conversation.yml +73 -0
  21. data/spec/fixtures/vcr_cassettes/rubyllm_chat_lms_qwen3_0_6b_bible_assistant_returns_structured_output.yml +73 -0
  22. data/spec/fixtures/vcr_cassettes/rubyllm_chat_lms_qwen3_0_6b_bible_assistant_returns_the_raw_response.yml +73 -0
  23. data/spec/fixtures/vcr_cassettes/rubyllm_embedding_lms_text_embedding_nomic_embed_text_v1_5_embeds_one_text.yml +828 -0
  24. data/spec/fixtures/vcr_cassettes/rubyllm_embedding_lms_text_embedding_nomic_embed_text_v1_5_embeds_several_texts.yml +2375 -0
  25. data/spec/ruby_llm/chat_schema_spec.rb +30 -0
  26. data/spec/ruby_llm/chat_spec.rb +35 -0
  27. data/spec/ruby_llm/chat_streaming_spec.rb +22 -0
  28. data/spec/ruby_llm/chat_tools_spec.rb +30 -0
  29. data/spec/ruby_llm/embedding_spec.rb +49 -0
  30. data/spec/ruby_llm/image_spec.rb +23 -0
  31. data/spec/ruby_llm/models_spec.rb +11 -0
  32. data/spec/ruby_llm/moderation_spec.rb +22 -0
  33. data/spec/ruby_llm/providers/lms_spec.rb +149 -0
  34. data/spec/ruby_llm/rerank_spec.rb +23 -0
  35. data/spec/ruby_llm/speech_spec.rb +25 -0
  36. data/spec/ruby_llm/video_spec.rb +27 -0
  37. data/spec/spec_helper.rb +26 -0
  38. data/spec/support/models.rb +29 -0
  39. data/spec/support/rubyllm_configuration.rb +14 -0
  40. data/spec/support/vcr_configuration.rb +16 -0
  41. metadata +99 -0
@@ -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::LMS.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,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe RubyLLM::Providers::LMS do
6
+ subject(:provider) { described_class.new(config) }
7
+
8
+ let(:config) do
9
+ RubyLLM::Configuration.new.tap do |provider_config|
10
+ provider_config.lms_api_base = 'http://example.test:1234/v1'
11
+ end
12
+ end
13
+
14
+ it 'is registered with RubyLLM' do
15
+ expect(RubyLLM::Provider.resolve(:lms)).to eq(described_class)
16
+ end
17
+
18
+ it 'registers its protocols' do
19
+ expect(described_class.protocols).to include(
20
+ chat_completions: described_class::ChatCompletions,
21
+ responses: RubyLLM::Protocols::Responses
22
+ )
23
+ end
24
+
25
+ it 'declares provider configuration' do
26
+ expect(described_class.configuration_options).to eq(%i[lms_api_base lms_api_key])
27
+ expect(described_class.configuration_requirements).to eq([])
28
+ end
29
+
30
+ it 'is a local provider with a human-readable name' do
31
+ expect(described_class.local?).to be(true)
32
+ expect(described_class.display_name).to eq('LM Studio')
33
+ end
34
+
35
+ it 'uses the configured API base' do
36
+ expect(provider.api_base).to eq('http://example.test:1234/v1')
37
+ end
38
+
39
+ it 'defaults to the LM Studio server address' do
40
+ provider = described_class.new(RubyLLM::Configuration.new)
41
+ expect(provider.api_base).to eq('http://localhost:1234/v1')
42
+ end
43
+
44
+ it 'sends no Authorization header without an API key' do
45
+ expect(provider.headers).to eq({})
46
+ end
47
+
48
+ it 'sends a bearer token when an API key is configured' do
49
+ config.lms_api_key = 'test-key'
50
+ expect(provider.headers).to eq('Authorization' => 'Bearer test-key')
51
+ end
52
+
53
+ describe 'model listing' do
54
+ subject(:protocol) do
55
+ described_class::ChatCompletions.new(described_class.new(config))
56
+ end
57
+
58
+ let(:llm_detail) do
59
+ {
60
+ 'type' => 'llm',
61
+ 'arch' => 'qwen2',
62
+ 'publisher' => 'lmstudio-community',
63
+ 'compatibility_type' => 'gguf',
64
+ 'quantization' => 'Q4_K_M',
65
+ 'state' => 'loaded',
66
+ 'max_context_length' => 32_768,
67
+ 'capabilities' => ['tool_use']
68
+ }
69
+ end
70
+ let(:vlm_detail) { { 'type' => 'vlm', 'arch' => 'qwen2_vl' } }
71
+ let(:embedding_detail) { { 'type' => 'embeddings', 'arch' => 'nomic-bert' } }
72
+
73
+ describe '#build_modalities' do
74
+ it 'maps chat models to text in and out' do
75
+ expect(protocol.build_modalities(llm_detail)).to eq(input: %w[text], output: %w[text])
76
+ end
77
+
78
+ it 'adds image input for vision models' do
79
+ expect(protocol.build_modalities(vlm_detail)).to eq(input: %w[text image], output: %w[text])
80
+ end
81
+
82
+ it 'maps embedding models to embedding output' do
83
+ expect(protocol.build_modalities(embedding_detail)).to eq(input: %w[text], output: %w[embeddings])
84
+ end
85
+ end
86
+
87
+ describe '#build_capabilities' do
88
+ it 'derives function calling from reported tool_use' do
89
+ expect(protocol.build_capabilities(llm_detail))
90
+ .to eq(%w[streaming structured_output function_calling])
91
+ end
92
+
93
+ it 'derives vision from the model type' do
94
+ expect(protocol.build_capabilities(vlm_detail)).to include('vision')
95
+ end
96
+
97
+ it 'reports no chat capabilities for embedding models' do
98
+ expect(protocol.build_capabilities(embedding_detail)).to eq([])
99
+ end
100
+
101
+ it 'falls back to base capabilities without native details' do
102
+ expect(protocol.build_capabilities({})).to eq(%w[streaming structured_output])
103
+ end
104
+ end
105
+
106
+ describe '#build_metadata' do
107
+ it 'keeps native details and drops missing fields' do
108
+ metadata = protocol.build_metadata({ 'owned_by' => 'organization_owner' }, llm_detail)
109
+ expect(metadata).to eq(
110
+ owned_by: 'organization_owner',
111
+ publisher: 'lmstudio-community',
112
+ arch: 'qwen2',
113
+ compatibility_type: 'gguf',
114
+ quantization: 'Q4_K_M',
115
+ state: 'loaded'
116
+ )
117
+ end
118
+
119
+ it 'compacts unknown details away' do
120
+ expect(protocol.build_metadata({ 'owned_by' => 'organization_owner' }, {}))
121
+ .to eq(owned_by: 'organization_owner')
122
+ end
123
+ end
124
+
125
+ describe '#parse_list_models_response' do
126
+ it 'builds enriched models from the listing and native details' do
127
+ response = instance_double(Faraday::Response,
128
+ body: { 'data' => [{ 'id' => 'qwen2.5-7b-instruct',
129
+ 'owned_by' => 'organization_owner' }] })
130
+ models = protocol.parse_list_models_response(response, 'lms',
131
+ details: { 'qwen2.5-7b-instruct' => llm_detail })
132
+ expect(models.length).to eq(1)
133
+ model = models.first
134
+ expect(model.id).to eq('qwen2.5-7b-instruct')
135
+ expect(model.provider).to eq('lms')
136
+ expect(model.family).to eq('qwen2')
137
+ expect(model.context_window).to eq(32_768)
138
+ expect(model.capabilities).to include('function_calling')
139
+ end
140
+
141
+ it 'builds plain models when native details are unavailable' do
142
+ response = instance_double(Faraday::Response, body: { 'data' => [{ 'id' => 'some-model' }] })
143
+ model = protocol.parse_list_models_response(response, 'lms').first
144
+ expect(model.family).to eq('lms')
145
+ expect(model.capabilities).to eq(%w[streaming structured_output])
146
+ end
147
+ end
148
+ end
149
+ 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
@@ -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/lms'
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,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Model matrices for the live contract specs. LM Studio serves whatever
4
+ # models are downloaded on this machine, so these ids are machine-specific;
5
+ # swap in ids from `lms ls` (or models.json) when recording cassettes.
6
+ # See RubyLLM's full live matrix and specs: https://github.com/crmne/ruby_llm/tree/main/spec
7
+ PROVIDER = :lms
8
+ CHAT_MODELS = [
9
+ { provider: PROVIDER, model: 'openai/gpt-oss-20b' }
10
+ ].freeze
11
+ TOOL_MODELS = CHAT_MODELS
12
+ # gpt-oss models mangle json_schema values on LM Studio (harmony format);
13
+ # qwen3 models honor it.
14
+ STRUCTURED_OUTPUT_MODELS = [
15
+ { provider: PROVIDER, model: 'qwen3-0.6b-bible-assistant' }
16
+ ].freeze
17
+
18
+ EMBEDDING_MODELS = [
19
+ { provider: PROVIDER, model: 'text-embedding-nomic-embed-text-v1.5' }
20
+ ].freeze
21
+ IMAGE_GENERATION_MODELS = [].freeze
22
+ SPEECH_MODELS = [].freeze
23
+ VIDEO_GENERATION_MODELS = [].freeze
24
+ MODERATION_MODELS = [].freeze
25
+ RERANK_MODELS = [].freeze
26
+
27
+ def each_model(models)
28
+ models.each { |model_info| yield model_info[:provider], model_info[:model], model_info }
29
+ 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.lms_api_key = ENV.fetch('LMS_API_KEY', 'test')
7
+ config.lms_api_base = ENV.fetch('LMS_API_BASE', 'http://localhost:1234/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('<LMS_API_KEY>') { ENV.fetch('LMS_API_KEY', nil) }
9
+ config.filter_sensitive_data('<LMS_API_BASE>') { ENV.fetch('LMS_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,99 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruby_llm-providers-lms
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 LM Studio provider support to RubyLLM via LM Studio's OpenAI-compatible
27
+ local server, with model listings enriched from LM Studio's native REST API.
28
+ email:
29
+ - dvanhoozer@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - ".flayignore"
35
+ - ".github/workflows/ci.yml"
36
+ - ".github/workflows/gitleaks.yml"
37
+ - ".github/workflows/release.yml"
38
+ - ".overcommit.yml"
39
+ - ".rspec"
40
+ - ".rubocop.yml"
41
+ - Archspec.rb
42
+ - LICENSE
43
+ - README.md
44
+ - lib/ruby_llm/providers/lms.rb
45
+ - lib/ruby_llm/providers/lms/models.rb
46
+ - models.json
47
+ - spec/fixtures/vcr_cassettes/rubyllm_chat_lms_openai_gpt_oss_20b_can_handle_a_multi_turn_conversation.yml
48
+ - spec/fixtures/vcr_cassettes/rubyllm_chat_lms_openai_gpt_oss_20b_can_have_a_basic_conversation.yml
49
+ - spec/fixtures/vcr_cassettes/rubyllm_chat_lms_openai_gpt_oss_20b_can_use_tools.yml
50
+ - spec/fixtures/vcr_cassettes/rubyllm_chat_lms_openai_gpt_oss_20b_returns_the_raw_response.yml
51
+ - spec/fixtures/vcr_cassettes/rubyllm_chat_lms_openai_gpt_oss_20b_supports_streaming_responses.yml
52
+ - spec/fixtures/vcr_cassettes/rubyllm_chat_lms_qwen3_0_6b_bible_assistant_can_have_a_basic_conversation.yml
53
+ - spec/fixtures/vcr_cassettes/rubyllm_chat_lms_qwen3_0_6b_bible_assistant_returns_structured_output.yml
54
+ - spec/fixtures/vcr_cassettes/rubyllm_chat_lms_qwen3_0_6b_bible_assistant_returns_the_raw_response.yml
55
+ - spec/fixtures/vcr_cassettes/rubyllm_embedding_lms_text_embedding_nomic_embed_text_v1_5_embeds_one_text.yml
56
+ - spec/fixtures/vcr_cassettes/rubyllm_embedding_lms_text_embedding_nomic_embed_text_v1_5_embeds_several_texts.yml
57
+ - spec/ruby_llm/chat_schema_spec.rb
58
+ - spec/ruby_llm/chat_spec.rb
59
+ - spec/ruby_llm/chat_streaming_spec.rb
60
+ - spec/ruby_llm/chat_tools_spec.rb
61
+ - spec/ruby_llm/embedding_spec.rb
62
+ - spec/ruby_llm/image_spec.rb
63
+ - spec/ruby_llm/models_spec.rb
64
+ - spec/ruby_llm/moderation_spec.rb
65
+ - spec/ruby_llm/providers/lms_spec.rb
66
+ - spec/ruby_llm/rerank_spec.rb
67
+ - spec/ruby_llm/speech_spec.rb
68
+ - spec/ruby_llm/video_spec.rb
69
+ - spec/spec_helper.rb
70
+ - spec/support/models.rb
71
+ - spec/support/rubyllm_configuration.rb
72
+ - spec/support/vcr_configuration.rb
73
+ homepage: https://github.com/madbomber/ruby_llm-providers-lms
74
+ licenses:
75
+ - MIT
76
+ metadata:
77
+ homepage_uri: https://github.com/madbomber/ruby_llm-providers-lms
78
+ source_code_uri: https://github.com/madbomber/ruby_llm-providers-lms
79
+ changelog_uri: https://github.com/madbomber/ruby_llm-providers-lms/releases
80
+ bug_tracker_uri: https://github.com/madbomber/ruby_llm-providers-lms/issues
81
+ rubygems_mfa_required: 'true'
82
+ rdoc_options: []
83
+ require_paths:
84
+ - lib
85
+ required_ruby_version: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '3.1'
90
+ required_rubygems_version: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ requirements: []
96
+ rubygems_version: 4.0.20
97
+ specification_version: 4
98
+ summary: RubyLLM provider for LM Studio.
99
+ test_files: []