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 +4 -4
- data/CHANGELOG.md +8 -0
- data/Gemfile.lock +1 -1
- data/Rakefile +25 -0
- data/lib/raix/chat_completion.rb +1 -1
- data/lib/raix/configuration.rb +9 -2
- data/lib/raix/function_tool_adapter.rb +12 -4
- data/lib/raix/multimodal_content_adapter.rb +63 -0
- data/lib/raix/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3f9cda9662a819d937b2be909563e7bbd145111b2f70006a596b6fc30cf25fe6
|
|
4
|
+
data.tar.gz: 280b9bee2cc6b3f47ded7b0ae88b406842184c48597efc4c9aa085458bff4cab
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
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
|
data/lib/raix/chat_completion.rb
CHANGED
|
@@ -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"])
|
data/lib/raix/configuration.rb
CHANGED
|
@@ -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
|
-
#
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
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
|
+
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-
|
|
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
|