raix 2.0.4 → 2.0.5

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ec3b7449e51440e1d669e323dea0c371a3d18d7d4083090d61599ff1dbce3bbe
4
- data.tar.gz: 6f2029218c2099ab1172d1d458be9ccb33836078d5abc9b04865cae0d497bc07
3
+ metadata.gz: 3f9cda9662a819d937b2be909563e7bbd145111b2f70006a596b6fc30cf25fe6
4
+ data.tar.gz: 280b9bee2cc6b3f47ded7b0ae88b406842184c48597efc4c9aa085458bff4cab
5
5
  SHA512:
6
- metadata.gz: 76e1d77e80023dbc634e4b2f72eaa269f05d539ea97470df1355e69e73d75a6cf4bd2bd7e627ab2cf01fcb381fd85c4b153d28005f32190ff1b8ae69c3bea275
7
- data.tar.gz: 6fedf4bae5c406b72b83a1aacbc985849eb7c461237e7cc1e12d9ca73ea05a861912f13739bc92ba8ad992bf88f5a31fa530dc9468c5bdb4763f53caf3833843
6
+ metadata.gz: dc7ee985af7a5b832803e7aa9300986f83fd520315e4d3668e68fa1d9084be08782517054dac91e9a187e190de2876215e0dfa71bb2f09ffb462d9f275b22e71
7
+ data.tar.gz: 49380b2201f8563dd7e69c30a76866b16b89d2085c6bc3e953afd6f233343d4f9c93ba891c02122228d2579341b9c64c423f16751d588d97e5105c6443d2da07
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [2.0.5] - 2026-06-04
4
+
5
+ ### Fixed
6
+ - `Raix::Configuration` no longer defaults `temperature` to `0.0`. The default was being injected into every request payload, which OpenRouter rejects with `404 No endpoints found that can handle the requested parameters` when routed to providers whose `supported_parameters` list omits `temperature` (notably Anthropic's Claude 4.7 family) and `provider.require_parameters: true` is set — which Raix sets automatically whenever `json: true` is passed to `chat_completion`. Callers that want a specific temperature should set one explicitly (`self.temperature = 0.0` on the including class, or `Raix.configure { |c| c.temperature = 0.0 }` globally); when unset, Raix now omits the parameter and the provider's own server-side default applies. `max_tokens`, `max_completion_tokens`, and `model` defaults are unchanged.
7
+ - `Raix::FunctionToolAdapter` now forwards the full JSON-Schema dict for each function parameter to RubyLLM instead of rebuilding it from `type` + `description` only. Rich schema fields like `additionalProperties`, `items`, `enum`, and nested `properties` were silently dropped, leaving providers (notably Gemini's structured output via OpenRouter) to invent degenerate shapes for `type: object` arguments — e.g. emitting `{"prefix" => false}` instead of `{"prefix:title" => "..."}`. Function declarations with rich object schemas now reach the provider intact.
8
+ - The outer tool-args schema continues to inject `additionalProperties: false` and `strict: true` by default for OpenAI strict-mode compatibility, but consumers can override either by setting them explicitly in the function declaration.
9
+ - Multimodal `transcript` content is no longer silently dropped on the RubyLLM backend. OpenAI-style content arrays (a `text` part plus one or more `{ type: "image_url", image_url: { url: ... } }` parts) were passed to RubyLLM verbatim, which treats the array as plain text — so a vision model received text only and confabulated an answer. Raix now translates `image_url` parts into RubyLLM attachments via `Raix::MultimodalContentAdapter`, decoding base64 `data:` URIs into binary IO (RubyLLM's `Attachment` does not natively recognize `data:` URIs) and passing http(s) URLs through. Text-only completions are unaffected (#51).
10
+
3
11
  ## [2.0.4] - 2026-05-19
4
12
 
5
13
  ### Fixed
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- raix (2.0.4)
4
+ raix (2.0.5)
5
5
  activesupport (>= 6.0)
6
6
  faraday-retry (~> 2.0)
7
7
  ostruct
data/Rakefile CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  require "bundler/gem_tasks"
4
4
  require "rspec/core/rake_task"
5
+ require "shellwords"
6
+ require "tmpdir"
5
7
 
6
8
  RSpec::Core::RakeTask.new(:spec)
7
9
 
@@ -16,3 +18,26 @@ RuboCop::RakeTask.new(:rubocop) do |task|
16
18
  end
17
19
 
18
20
  task default: %i[spec rubocop]
21
+
22
+ namespace :release do
23
+ desc "Create a GitHub release for the current version (runs automatically after `rake release`)"
24
+ task :github do
25
+ version = Bundler::GemHelper.gemspec.version.to_s
26
+ tag = "v#{version}"
27
+ section = File.read("CHANGELOG.md").match(/^## \[#{Regexp.escape(version)}\][^\n]*\n(.*?)(?=^## \[|\z)/m)
28
+
29
+ if section.nil?
30
+ warn "release:github — no CHANGELOG entry for #{version}; skipping GitHub release."
31
+ next
32
+ end
33
+
34
+ notes_file = File.join(Dir.tmpdir, "raix-release-notes-#{version}.md")
35
+ File.write(notes_file, "#{section[1].strip}\n")
36
+
37
+ sh "gh release create #{tag.shellescape} --title #{"Release #{version}".shellescape} --notes-file #{notes_file.shellescape} --latest=true"
38
+ end
39
+ end
40
+
41
+ Rake::Task["release"].enhance do
42
+ Rake::Task["release:github"].invoke
43
+ end
@@ -355,7 +355,7 @@ module Raix
355
355
  chat.with_instructions(content)
356
356
  when "user"
357
357
  has_user_message = true
358
- chat.add_message(role: :user, content:)
358
+ chat.add_message(role: :user, content: MultimodalContentAdapter.translate(content))
359
359
  when "assistant"
360
360
  if msg[:tool_calls] || msg["tool_calls"]
361
361
  chat.add_message(role: :assistant, content:, tool_calls: msg[:tool_calls] || msg["tool_calls"])
@@ -51,12 +51,19 @@ module Raix
51
51
  DEFAULT_MAX_TOKENS = 1000
52
52
  DEFAULT_MAX_COMPLETION_TOKENS = 16_384
53
53
  DEFAULT_MODEL = "meta-llama/llama-3.3-8b-instruct:free"
54
- DEFAULT_TEMPERATURE = 0.0
55
54
  DEFAULT_MAX_TOOL_CALLS = 25
56
55
 
57
56
  # Initializes a new instance of the Configuration class with default values.
57
+ #
58
+ # Note: temperature is intentionally not defaulted. Setting a non-nil
59
+ # temperature here would force it into every request payload, and some
60
+ # providers (e.g. Anthropic's Claude 4.7 family on OpenRouter) do not list
61
+ # `temperature` in their supported parameters. Combined with
62
+ # `provider.require_parameters: true` (which Raix sets when `json: true`),
63
+ # an injected default of 0.0 causes OpenRouter to reject the request with
64
+ # "No endpoints found that can handle the requested parameters." Callers
65
+ # who want a specific temperature should set one explicitly.
58
66
  def initialize(fallback: nil)
59
- self.temperature = DEFAULT_TEMPERATURE
60
67
  self.max_completion_tokens = DEFAULT_MAX_COMPLETION_TOKENS
61
68
  self.max_tokens = DEFAULT_MAX_TOKENS
62
69
  self.model = DEFAULT_MODEL
@@ -7,10 +7,18 @@ module Raix
7
7
  tool_class = Class.new(RubyLLM::Tool) do
8
8
  description function_def[:description] if function_def[:description]
9
9
 
10
- # Define parameters based on function definition
11
- function_def[:parameters][:properties]&.each do |param_name, param_def|
12
- required = function_def[:parameters][:required]&.include?(param_name)
13
- param param_name.to_sym, type: param_def[:type], desc: param_def[:description], required:
10
+ # Forward the full JSON-schema parameter dict to RubyLLM rather than
11
+ # rebuilding it field-by-field via `param(...)`. The per-field path
12
+ # only carried `type` and `description`, which silently dropped richer
13
+ # schema like `additionalProperties`, `items`, `enum`, or nested
14
+ # `properties` — leaving providers (notably Gemini's structured output)
15
+ # to invent degenerate shapes for `type: object` arguments.
16
+ if function_def[:parameters].is_a?(Hash) && function_def[:parameters][:properties].present?
17
+ # RubyLLM's `params(schema)` path forwards the schema verbatim and, unlike the
18
+ # per-field `param(...)` path, does not inject the OpenAI strict-mode guards. Default
19
+ # them on so existing tools keep strict behavior, while letting a function declaration
20
+ # override either by setting it explicitly.
21
+ params({ additionalProperties: false, strict: true }.merge(function_def[:parameters]))
14
22
  end
15
23
 
16
24
  # Store reference to the instance and function name
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/hash/indifferent_access"
4
+ require "base64"
5
+ require "stringio"
6
+
7
+ module Raix
8
+ # Translates OpenAI-style multimodal content arrays (a `text` part plus one or
9
+ # more `image_url` parts) into a RubyLLM::Content so images survive the trip to
10
+ # the provider.
11
+ #
12
+ # RubyLLM's `add_message`/`ask` treat a raw array of OpenAI content hashes as
13
+ # plain text, so an `{ type: "image_url", image_url: { url: ... } }` part is
14
+ # silently dropped and a vision model receives text only. See
15
+ # https://github.com/OlympiaAI/raix/issues/51
16
+ #
17
+ # Anything that is not an array of hashes containing at least one `image_url`
18
+ # part is returned untouched, so existing text completions are unaffected.
19
+ class MultimodalContentAdapter
20
+ def self.translate(content)
21
+ new(content).translate
22
+ end
23
+
24
+ def initialize(content)
25
+ @content = content
26
+ end
27
+
28
+ def translate
29
+ return @content unless translatable?
30
+
31
+ parts = @content.map(&:with_indifferent_access)
32
+ attachments = parts.select { |part| part[:type].to_s == "image_url" }
33
+ .filter_map { |part| attachment_source(part.dig(:image_url, :url)) }
34
+ return @content if attachments.empty?
35
+
36
+ text = parts.select { |part| part[:type].to_s == "text" }.filter_map { |part| part[:text] }.join("\n")
37
+ RubyLLM::Content.new(text.empty? ? nil : text, attachments)
38
+ end
39
+
40
+ private
41
+
42
+ def translatable?
43
+ @content.is_a?(Array) &&
44
+ @content.all? { |part| part.is_a?(Hash) } &&
45
+ @content.any? { |part| (part[:type] || part["type"]).to_s == "image_url" }
46
+ end
47
+
48
+ # RubyLLM::Attachment recognizes http(s) URLs, file paths, and IO objects, but
49
+ # not base64 `data:` URIs (it would treat one as a filesystem path). Decode
50
+ # those into a binary StringIO, which Attachment handles as an IO source.
51
+ def attachment_source(url)
52
+ return if url.nil? || url.empty?
53
+ return url unless url.start_with?("data:")
54
+
55
+ match = url.match(/\Adata:[^;,]*;base64,(.+)\z/m)
56
+ return url unless match
57
+
58
+ io = StringIO.new(Base64.decode64(match[1]))
59
+ io.set_encoding(Encoding::BINARY) if io.respond_to?(:set_encoding)
60
+ io
61
+ end
62
+ end
63
+ end
data/lib/raix/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Raix
4
- VERSION = "2.0.4"
4
+ VERSION = "2.0.5"
5
5
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: raix
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.4
4
+ version: 2.0.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Obie Fernandez
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-05-19 00:00:00.000000000 Z
10
+ date: 2026-06-04 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: activesupport
@@ -109,6 +109,7 @@ files:
109
109
  - lib/raix/mcp/stdio_client.rb
110
110
  - lib/raix/mcp/tool.rb
111
111
  - lib/raix/message_adapters/base.rb
112
+ - lib/raix/multimodal_content_adapter.rb
112
113
  - lib/raix/predicate.rb
113
114
  - lib/raix/prompt_declarations.rb
114
115
  - lib/raix/response_format.rb