ruby_llm-voyage 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8ff013c0d7cc147977b07d6bd229fe1417390abf857b3252cb9721969609c1bf
4
+ data.tar.gz: 9e6075faf6732a6dcb9f68a03f25a7302435f4fa12b89a253999d09ec8e98549
5
+ SHA512:
6
+ metadata.gz: ce3de774db04508201dcf029283c28d3c3c9dc1de6286cbf7dab209743f46692af300516439c23ba4bb49a9e654ee123479eeef29ff6ec7dfa35edaa57899f40
7
+ data.tar.gz: e0ba556b4ffa536441a2760c2190dec86ccc80894772702a5ca187f2db753241192e26a180ac9005530f59d1f0465e5b78f2596c98454cec991f944144673f6f
data/CHANGELOG.md ADDED
@@ -0,0 +1,45 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2026-07-11
11
+
12
+ ### Added
13
+
14
+ - Voyage text embedding provider for RubyLLM 1.16, covering single and batch
15
+ inputs, query and document input types, truncation control, flexible
16
+ dimensions, quantized output types, and Base64 decoding.
17
+ - `RubyLLM::Voyage.embed_query` and `RubyLLM::Voyage.embed_documents` for the
18
+ two sides of retrieval, with `input_type` fixed per method.
19
+ - `RubyLLM::Voyage.embed` facade for request-level Voyage options, falling
20
+ back to `config.default_embedding_model` when `model:` is omitted.
21
+ - Reranking via `RubyLLM::Voyage.rerank`, with `Reranking#reorder` to map
22
+ results back onto the original candidate collection.
23
+ - Contextualized chunk embeddings via
24
+ `RubyLLM::Voyage.contextualized_embed`, including server-side auto
25
+ chunking.
26
+ - Files API support: upload, retrieve, list, download, and bulk delete, with
27
+ `find_file` and `download_file` aliases matching RubyLLM's provider file
28
+ naming. Downloads follow signed-URL redirects without forwarding the API
29
+ key and reject non-HTTPS redirects.
30
+ - Public `VectorDecoder` for decoding Base64 embeddings out of batch result
31
+ files.
32
+ - Configurable request defaults: `voyage_default_input_type`,
33
+ `voyage_default_truncation`, `voyage_default_output_dimension`,
34
+ `voyage_default_output_dtype`, and `voyage_default_encoding_format`.
35
+ - Fail-fast `ArgumentError` for nil or empty inputs before any network
36
+ request.
37
+ - Instrumentation events for embeddings, reranking, and contextualized
38
+ embeddings with result, response model, and token usage.
39
+ - Offline unit and HTTP contract tests, opt-in live API tests, and CI
40
+ coverage for Ruby 3.1 through 4.0.
41
+ - Trusted-publishing release workflow, SHA-pinned GitHub Actions, and
42
+ contributor, security, and release documentation.
43
+
44
+ [Unreleased]: https://github.com/dinsley/ruby_llm-voyage/compare/v0.1.0...HEAD
45
+ [0.1.0]: https://github.com/dinsley/ruby_llm-voyage/releases/tag/v0.1.0
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,72 @@
1
+ # Contributing
2
+
3
+ Thanks for helping improve `ruby_llm-voyage`.
4
+
5
+ ## Development setup
6
+
7
+ The project supports Ruby 3.1.3 through Ruby 4.0 and RubyLLM 1.x beginning
8
+ with 1.16.
9
+
10
+ ```sh
11
+ git clone https://github.com/dinsley/ruby_llm-voyage.git
12
+ cd ruby_llm-voyage
13
+ bin/setup
14
+ bundle exec rake test
15
+ bundle exec rubocop
16
+ bundle exec rake yard
17
+ bundle exec rake rdoc
18
+ ```
19
+
20
+ `bin/console` starts an IRB session with the gem loaded.
21
+
22
+ The default suite is network-isolated with WebMock. New behavior should have
23
+ offline coverage and must not require a Voyage API key.
24
+
25
+ ## Live API tests
26
+
27
+ Live tests are manual, billable, and never run in CI:
28
+
29
+ ```sh
30
+ VOYAGE_API_KEY=your-key bundle exec rake test:live
31
+ ```
32
+
33
+ The live suite covers single and batch embedding, context-based defaults,
34
+ input types, retrieval ranking, every flexible dimension and output dtype,
35
+ Base64 decoding, `provider_options` precedence, reranking, and contextualized
36
+ embeddings. WebMock allows connections only to `api.voyageai.com`, so an
37
+ accidental request to any other host still fails.
38
+
39
+ The suite defaults to `voyage-4-lite`, which supports the complete
40
+ dimension/dtype matrix. Environment variables adjust what runs:
41
+
42
+ ```sh
43
+ # Use a different primary model (must support flexible dimensions and dtypes)
44
+ VOYAGE_LIVE_MODEL=voyage-4 bundle exec rake test:live
45
+
46
+ # Smoke-test additional model families with their defaults
47
+ VOYAGE_LIVE_MODELS=voyage-4-large,voyage-code-3 bundle exec rake test:live
48
+
49
+ # Include the mutating Files API lifecycle test (uploads and deletes a file)
50
+ VOYAGE_LIVE_FILES=1 bundle exec rake test:live
51
+ ```
52
+
53
+ Use short inputs and the default `voyage-4-lite` model unless a test
54
+ specifically requires another model. Never commit credentials, captured
55
+ authorization headers, or API responses containing private input.
56
+
57
+ ## Pull requests
58
+
59
+ - Add or update tests for behavior changes.
60
+ - Update `README.md` for user-facing changes.
61
+ - Add an entry under `Unreleased` in `CHANGELOG.md`.
62
+ - Run `bundle exec rake` (tests and RuboCop) and
63
+ `gem build ruby_llm-voyage.gemspec`.
64
+ - Add YARD/RDoc comments for public API changes and verify both documentation
65
+ tasks complete without warnings.
66
+ - Keep live tests separate from the default suite.
67
+
68
+ ## Compatibility changes
69
+
70
+ Changes to the minimum Ruby or RubyLLM version are user-facing compatibility
71
+ changes. Update the gemspec, CI matrix, the requirements note at the top of
72
+ the README, and the changelog together.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Daniel Insley
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,389 @@
1
+ # ruby_llm-voyage
2
+
3
+ [![CI](https://github.com/dinsley/ruby_llm-voyage/actions/workflows/ci.yml/badge.svg)](https://github.com/dinsley/ruby_llm-voyage/actions/workflows/ci.yml)
4
+ [![Gem Version](https://badge.fury.io/rb/ruby_llm-voyage.svg)](https://rubygems.org/gems/ruby_llm-voyage)
5
+
6
+ [Voyage AI](https://docs.voyageai.com/) builds embedding and reranking models
7
+ for search and retrieval. This gem adds them to
8
+ [RubyLLM](https://rubyllm.com/): document and query embeddings, reranking,
9
+ contextualized chunk embeddings, and the Files API.
10
+
11
+ Requires Ruby 3.1.3+ and RubyLLM 1.16+.
12
+
13
+ ## Installation
14
+
15
+ Add the gem to your Gemfile:
16
+
17
+ ```ruby
18
+ gem 'ruby_llm-voyage'
19
+ ```
20
+
21
+ Then configure your API key (created in the [Voyage dashboard](https://dashboard.voyageai.com/)):
22
+
23
+ ```ruby
24
+ RubyLLM.configure do |config|
25
+ config.voyage_api_key = ENV.fetch('VOYAGE_API_KEY')
26
+ end
27
+ ```
28
+
29
+ ## Your first embedding
30
+
31
+ Voyage works through the standard RubyLLM embedding interface:
32
+
33
+ ```ruby
34
+ embedding = RubyLLM.embed(
35
+ 'Ruby is optimized for programmer happiness.',
36
+ model: 'voyage-4',
37
+ provider: :voyage
38
+ )
39
+
40
+ embedding.vectors # => [0.018, -0.032, ...]
41
+ embedding.input_tokens # => 9
42
+ ```
43
+
44
+ Pass an array to embed a batch in one request, and `dimensions:` to control vector size:
45
+
46
+ ```ruby
47
+ RubyLLM.embed(%w[Ruby Rails], model: 'voyage-4-lite', provider: :voyage, dimensions: 512)
48
+ ```
49
+
50
+ Voyage tunes vectors differently for the text you store (`document`) and the text people search with (`query`), and using
51
+ both sides improves retrieval quality. The gem gives each side its own method:
52
+
53
+ ```ruby
54
+ RubyLLM::Voyage.embed_documents(['Reset your password from account settings.'])
55
+ RubyLLM::Voyage.embed_query('how do I reset my password')
56
+ ```
57
+
58
+ Both return the same `RubyLLM::Embedding` object and use `config.default_embedding_model` when `model:` is omitted.
59
+
60
+ For the remaining options — truncation, quantized output, an explicit `nil` input type — use `RubyLLM::Voyage.embed`:
61
+
62
+ ```ruby
63
+ embedding = RubyLLM::Voyage.embed(
64
+ 'How do I reset my password?',
65
+ model: 'voyage-4-lite',
66
+ input_type: 'query',
67
+ truncation: false,
68
+ dimensions: 512
69
+ )
70
+ ```
71
+
72
+ ## Semantic search in Rails
73
+
74
+ Here's a complete search feature for a help center, using [Neighbor](https://github.com/ankane/neighbor) and PostgreSQL's pgvector
75
+ extension.
76
+
77
+ Set up the gems and configure the embedding defaults once:
78
+
79
+ ```ruby
80
+ # Gemfile
81
+ gem 'ruby_llm-voyage'
82
+ gem 'neighbor'
83
+ ```
84
+
85
+ ```ruby
86
+ # config/initializers/ruby_llm.rb
87
+ RubyLLM.configure do |config|
88
+ config.voyage_api_key = Rails.credentials.voyage_api_key
89
+ config.default_embedding_model = 'voyage-4-lite'
90
+ config.voyage_default_output_dimension = 512
91
+ end
92
+ ```
93
+
94
+ Then the vector column:
95
+
96
+ ```sh
97
+ bin/rails generate neighbor:vector
98
+ bin/rails db:migrate
99
+ ```
100
+
101
+ ```ruby
102
+ class AddEmbeddingToArticles < ActiveRecord::Migration[8.0]
103
+ def change
104
+ add_column :articles, :embedding, :vector, limit: 512
105
+
106
+ add_column :articles, :embedding_model, :string # Identifies rows that need re-embedding when you upgrade models
107
+ add_column :articles, :embedding_digest, :string # Digest of the embedded data to track changes to the field
108
+ add_column :articles, :embedded_at, :datetime # Identifies when the data last had its embedding created
109
+
110
+ add_index :articles, :embedding, using: :hnsw, opclass: :vector_cosine_ops
111
+ add_index :articles, :embedding_model
112
+ end
113
+ end
114
+ ```
115
+
116
+ Embed in the background as articles are created or updated:
117
+
118
+ ```ruby
119
+ class Article < ApplicationRecord
120
+ has_neighbors :embedding
121
+
122
+ after_save_commit -> { EmbedArticleJob.perform_later(self) }, if: :saved_change_to_content?
123
+
124
+ def refresh_embedding!
125
+ digest = Digest::SHA256.hexdigest(content)
126
+ return if embedding_digest == digest
127
+
128
+ result = RubyLLM::Voyage.embed_documents(content)
129
+
130
+ update_columns(
131
+ embedding: result.vectors,
132
+ embedding_model: result.model,
133
+ embedding_digest: digest,
134
+ embedded_at: Time.zone.current
135
+ )
136
+ end
137
+ end
138
+
139
+ class EmbedArticleJob < ApplicationJob
140
+ def perform(article) = article.refresh_embedding!
141
+ end
142
+ ```
143
+
144
+ Then search with a query embedding:
145
+
146
+ ```ruby
147
+ class ArticlesController < ApplicationController
148
+ def search
149
+ query_vector = RubyLLM::Voyage.embed_query(params[:q]).vectors
150
+
151
+ @articles = Article.nearest_neighbors(
152
+ :embedding, query_vector, distance: 'cosine'
153
+ ).first(10)
154
+ end
155
+ end
156
+ ```
157
+
158
+ ### Embedding an entire table
159
+
160
+ The per-record job above handles ongoing writes, but backfilling existing data one row at a time wastes
161
+ API calls. Embed whole tables in batches instead — each batch is one request, and Voyage accepts up to
162
+ 1,000 texts per request ([current limits](https://docs.voyageai.com/reference/embeddings-api)):
163
+
164
+ ```ruby
165
+ class Article < ApplicationRecord
166
+ def self.embed_missing!(batch_size: 100)
167
+ where(embedding: nil).find_in_batches(batch_size: batch_size) do |batch|
168
+ result = RubyLLM::Voyage.embed_documents(batch.map(&:content))
169
+
170
+ batch.zip(result.vectors) do |(article, vector)|
171
+ article.update_columns(
172
+ embedding: vector,
173
+ embedding_model: result.model,
174
+ embedding_digest: Digest::SHA256.hexdigest(article.content),
175
+ embedded_at: Time.current
176
+ )
177
+ end
178
+ end
179
+ end
180
+ end
181
+
182
+ Article.embed_missing!
183
+ ```
184
+
185
+ The provenance columns make model upgrades a repeat of the same procedure:
186
+ clear the vectors built with the old model, backfill, and switch the query
187
+ side once nothing is left to embed.
188
+
189
+ ```ruby
190
+ Article.where.not(embedding_model: 'voyage-4').update_all(embedding: nil)
191
+ Article.embed_missing!
192
+ ```
193
+
194
+ ## Better results with reranking
195
+
196
+ Vector search is fast but approximate. When result order matters, fetch a generous candidate
197
+ set with vector search, then let a [Voyage reranker](https://docs.voyageai.com/docs/reranker) put
198
+ the best matches first:
199
+
200
+ ```ruby
201
+ question = 'How do I configure SAML SSO?'
202
+ query_vector = RubyLLM::Voyage.embed_query(question).vectors
203
+
204
+ candidates = Article.nearest_neighbors(
205
+ :embedding, query_vector, distance: 'cosine'
206
+ ).first(25)
207
+
208
+ reranking = RubyLLM::Voyage.rerank(
209
+ query: question,
210
+ documents: candidates.map(&:content),
211
+ model: 'rerank-2.5-lite',
212
+ top_k: 5
213
+ )
214
+
215
+ best_articles = reranking.reorder(candidates) # the 5 most relevant records
216
+ ```
217
+
218
+ `reorder` maps the reranked positions back onto the collection you passed as
219
+ `documents:`. Each result also carries the original `index`, a
220
+ `relevance_score`, and (with `return_documents: true`) the document text.
221
+
222
+ Rerankers score every candidate against the query, so rerank a small candidate set rather than the whole corpus.
223
+
224
+ ## Embedding long documents with context
225
+
226
+ Chunking a long document strips each chunk of its surroundings: a handbook chunk that reads "this does not apply during the probation period" no longer
227
+ says which policy it belongs to. Voyage's [contextualized models](https://docs.voyageai.com/docs/contextualized-chunk-embeddings) embed each chunk with
228
+ information from the rest of its document.
229
+
230
+ Pass pre-chunked documents as nested arrays:
231
+
232
+ ```ruby
233
+ result = RubyLLM::Voyage.contextualized_embed(
234
+ [
235
+ ['Vacation policy overview', 'Accrual rates by tenure', 'Carryover rules'],
236
+ ['Remote work policy', 'Equipment stipend', 'Home office requirements']
237
+ ],
238
+ model: 'voyage-context-4',
239
+ input_type: :document,
240
+ dimensions: 512
241
+ )
242
+
243
+ result.results.each do |document|
244
+ document.chunks.each do |chunk|
245
+ # document.index, chunk.index, chunk.embedding
246
+ end
247
+ end
248
+ ```
249
+
250
+ Or let Voyage do the chunking server-side:
251
+
252
+ ```ruby
253
+ result = RubyLLM::Voyage.contextualized_embed(
254
+ [handbook.full_text],
255
+ model: 'voyage-context-4',
256
+ input_type: :document,
257
+ dimensions: 512,
258
+ auto_chunk: true,
259
+ chunk_size: 512,
260
+ chunk_overlap: 0
261
+ )
262
+
263
+ result.results.first.chunks.each do |chunk|
264
+ HandbookChunk.create!(
265
+ position: chunk.index,
266
+ content: chunk.text, # the exact text this vector represents
267
+ embedding: chunk.embedding
268
+ )
269
+ end
270
+ ```
271
+
272
+ With `auto_chunk`, store `chunk.text` alongside each vector, and query the
273
+ index with the same contextualized model.
274
+
275
+ ## Giving a chatbot search
276
+
277
+ A RubyLLM tool can embed the model's search query with Voyage and pull
278
+ matching articles out of your database:
279
+
280
+ ```ruby
281
+ class SearchKnowledgeBase < RubyLLM::Tool
282
+ desc 'Search the help center for articles relevant to a question'
283
+
284
+ param :query, desc: 'A concise search query'
285
+
286
+ def execute(query:)
287
+ query_vector = RubyLLM::Voyage.embed_query(query).vectors
288
+
289
+ Article.nearest_neighbors(:embedding, query_vector, distance: 'cosine')
290
+ .first(5)
291
+ .map { |article| { id: article.id, content: article.content } }
292
+ .to_json
293
+ end
294
+ end
295
+
296
+ chat = RubyLLM.chat(model: 'claude-sonnet-4-5')
297
+ chat.with_instructions('Answer using the knowledge base. Cite article IDs.')
298
+ chat.with_tool(SearchKnowledgeBase)
299
+
300
+ chat.ask('How do I set up single sign-on?')
301
+ ```
302
+
303
+ If articles are scoped per account, filter the `nearest_neighbors` query by the current user's permissions before returning results to the model.
304
+
305
+ ## Batch workflows with the Files API
306
+
307
+ Voyage's asynchronous [batch workflows](https://docs.voyageai.com/docs/batch-inference) exchange JSONL files. This gem covers the full file lifecycle:
308
+
309
+ ```ruby
310
+ uploaded = RubyLLM::Voyage.upload_file('requests.jsonl')
311
+
312
+ RubyLLM::Voyage.retrieve_file(uploaded.id) # metadata
313
+ RubyLLM::Voyage.list_files(purpose: 'batch') # cursor-paginated listing
314
+ RubyLLM::Voyage.file_content(uploaded.id) # raw JSONL back out
315
+
316
+ RubyLLM::Voyage.delete_files([uploaded.id])
317
+ ```
318
+
319
+ `find_file` and `download_file` are aliases of `retrieve_file` and `file_content`, matching RubyLLM's own file-method naming.
320
+
321
+ Downloads follow Voyage's redirect to a signed storage URL without forwarding your API key, and refuse non-HTTPS redirects.
322
+
323
+ ## Configuration reference
324
+
325
+ Every Voyage option can be set globally, per `RubyLLM.context`, or per
326
+ request:
327
+
328
+ ```ruby
329
+ RubyLLM.configure do |config|
330
+ config.voyage_api_key = ENV.fetch('VOYAGE_API_KEY')
331
+ config.default_embedding_model = 'voyage-4-lite' # when model: is omitted
332
+ config.voyage_default_output_dimension = 512 # when dimensions: is omitted
333
+ config.voyage_default_input_type = 'document'
334
+ config.voyage_default_truncation = false # error instead of truncating
335
+ config.voyage_default_output_dtype = 'float'
336
+ config.voyage_default_encoding_format = 'base64'
337
+ config.voyage_api_base = 'https://api.voyageai.com/v1'
338
+ end
339
+ ```
340
+
341
+ Request-level options on `RubyLLM::Voyage.embed` (and on `embed_query` /
342
+ `embed_documents`, which fix `input_type` for you):
343
+
344
+ | Option | Voyage field | Values |
345
+ | --- | --- | --- |
346
+ | `model:` | `model` | Any Voyage embedding model ID |
347
+ | `input_type:` | `input_type` | `query`, `document`, or `nil` |
348
+ | `truncation:` | `truncation` | `true` or `false` |
349
+ | `dimensions:` | `output_dimension` | Model-dependent |
350
+ | `output_dtype:` | `output_dtype` | `float`, `int8`, `uint8`, `binary`, `ubinary` |
351
+ | `encoding_format:` | `encoding_format` | `base64` or `nil` |
352
+ | `provider_options:` | merged into request | Fields the gem doesn't have a keyword for yet |
353
+
354
+ ## Not supported
355
+
356
+ - **Multimodal embeddings** (`voyage-multimodal-3.5`) — text inputs only.
357
+ - **Batch jobs** — the Files API is covered, but creating and managing
358
+ asynchronous batch jobs is not.
359
+ - **Token counting** — Voyage's official SDKs tokenize locally; this gem
360
+ reports `input_tokens` from API responses instead.
361
+ - **Request splitting** — one call sends one API request. Collections over
362
+ Voyage's 1,000-text limit must be batched by the caller, as in the
363
+ backfill example above.
364
+
365
+ New Voyage request fields usually work before the gem names them: pass them through `provider_options:`. For anything else, open an [issue](https://github.com/dinsley/ruby_llm-voyage/issues).
366
+
367
+ ## Development
368
+
369
+ ```sh
370
+ bin/setup
371
+ bundle exec rake test # offline suite, no API key needed
372
+ bundle exec rubocop
373
+ ```
374
+
375
+ An opt-in live suite runs against the real Voyage API:
376
+
377
+ ```sh
378
+ VOYAGE_API_KEY=your-key bundle exec rake test:live
379
+ ```
380
+
381
+ See [CONTRIBUTING.md](https://github.com/dinsley/ruby_llm-voyage/blob/main/CONTRIBUTING.md)
382
+ for development guidelines and live test options, and
383
+ [docs/RELEASING.md](https://github.com/dinsley/ruby_llm-voyage/blob/main/docs/RELEASING.md)
384
+ for the release process.
385
+
386
+ ## License
387
+
388
+ The gem is available as open source under the terms of the
389
+ [MIT License](https://github.com/dinsley/ruby_llm-voyage/blob/main/LICENSE.txt).
data/SECURITY.md ADDED
@@ -0,0 +1,31 @@
1
+ # Security Policy
2
+
3
+ ## Supported versions
4
+
5
+ Security fixes are provided for the latest released version of
6
+ `ruby_llm-voyage`.
7
+
8
+ ## Reporting a vulnerability
9
+
10
+ Please do not open a public issue for a suspected vulnerability. Use
11
+ [GitHub private vulnerability reporting](https://github.com/dinsley/ruby_llm-voyage/security/advisories/new)
12
+ to report it privately.
13
+
14
+ Include:
15
+
16
+ - The affected version
17
+ - Reproduction steps or a minimal proof of concept
18
+ - The potential impact
19
+ - Any suggested mitigation
20
+
21
+ Do not include real Voyage API keys, customer embedding inputs, or production
22
+ responses. You should receive an acknowledgement within seven days.
23
+
24
+ ## Credential and data handling
25
+
26
+ - Store `VOYAGE_API_KEY` in a secret manager or environment variable.
27
+ - Never expose the API key to browsers or other untrusted clients.
28
+ - Only configure `voyage_api_base` from trusted application configuration.
29
+ - Treat embedding inputs and vectors as potentially sensitive data.
30
+ - Review RubyLLM instrumentation exporters before sending event payloads to a
31
+ third party; events can contain embedding text or reranking queries.
data/docs/RELEASING.md ADDED
@@ -0,0 +1,91 @@
1
+ # Releasing
2
+
3
+ Releases are published by the `Release` GitHub Actions workflow using
4
+ [RubyGems trusted publishing](https://guides.rubygems.org/trusted-publishing/).
5
+ No RubyGems API key exists anywhere; the workflow authenticates with an OIDC
6
+ token scoped to this repository, the `release.yml` workflow file, and the
7
+ `release` environment.
8
+
9
+ ## One-time setup
10
+
11
+ Already done for this repository, listed for reference:
12
+
13
+ - A trusted publisher on rubygems.org for `ruby_llm-voyage` pointing at
14
+ `dinsley/ruby_llm-voyage`, workflow `release.yml`, environment `release`.
15
+ (The first release used a pending publisher, which expires after about
16
+ 12 hours if unused — re-register if it lapses before the first tag.)
17
+ - Private vulnerability reporting and Dependabot security updates enabled.
18
+ - Default branch protected, requiring the `CI` workflow to pass.
19
+
20
+ Optional hardening: add required reviewers to the `release` GitHub
21
+ environment so tag pushes pause for approval before publishing.
22
+
23
+ ## Prepare the release
24
+
25
+ 1. Confirm CI passes on every supported Ruby version.
26
+ 2. Run the offline release checks:
27
+
28
+ ```sh
29
+ bundle exec rake
30
+ bundle exec rake yard
31
+ bundle exec rake rdoc
32
+ gem build ruby_llm-voyage.gemspec
33
+ gem specification ruby_llm-voyage-*.gem
34
+ ```
35
+
36
+ 3. Optionally run the billable live suite:
37
+
38
+ ```sh
39
+ VOYAGE_API_KEY=your-key bundle exec rake test:live
40
+ ```
41
+
42
+ 4. Update `RubyLLM::Voyage::VERSION` in
43
+ `lib/ruby_llm/voyage/version.rb`.
44
+ 5. Move the `CHANGELOG.md` entries under `Unreleased` into a dated version
45
+ section and update its comparison links.
46
+ 6. Commit the release changes and open a pull request.
47
+
48
+ ## Publish
49
+
50
+ After the release commit is merged:
51
+
52
+ ```sh
53
+ git switch main
54
+ git pull --ff-only
55
+ git tag -s vVERSION -m "ruby_llm-voyage VERSION"
56
+ git push origin vVERSION
57
+ ```
58
+
59
+ The tag push triggers the `Release` workflow, which runs the test suite and
60
+ publishes the gem to RubyGems with provenance attestations. Watch it at:
61
+
62
+ ```sh
63
+ gh run watch
64
+ ```
65
+
66
+ Then create a GitHub release from the tag using the matching changelog
67
+ section as the release notes:
68
+
69
+ ```sh
70
+ gh release create vVERSION --title "ruby_llm-voyage VERSION" --notes-file <(changelog section)
71
+ ```
72
+
73
+ ## Verify
74
+
75
+ ```sh
76
+ gem install ruby_llm-voyage -v VERSION
77
+ ruby -e 'require "ruby_llm-voyage"; puts RubyLLM::Voyage::VERSION'
78
+ ```
79
+
80
+ Confirm that the RubyGems page links to the expected source, changelog,
81
+ documentation, and issue tracker. Do not reuse or move a published version
82
+ tag; release a new patch version for corrections.
83
+
84
+ ## Manual fallback
85
+
86
+ If the workflow is unavailable, a gem owner with MFA can publish manually:
87
+
88
+ ```sh
89
+ bundle exec rake build
90
+ gem push pkg/ruby_llm-voyage-VERSION.gem
91
+ ```