nitro_intelligence 4.1.0 → 4.1.1

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: c43b6d21a0be3ea130217218e82e017b7f01d5be0a8dcafeb55578741f6097fd
4
- data.tar.gz: c872f0b16af367955b74116a54f62de096688f461e365f04ee2184351c7ca020
3
+ metadata.gz: 0e7cc3cbb0388e9001c106d00fddb60d6e1d7f2c3b9b54aa50d842e9ed719b8f
4
+ data.tar.gz: c1fce6397a2b3bba37ac56531c27608f9e8503e11603c0d9d2634a77dc17937f
5
5
  SHA512:
6
- metadata.gz: 472a5f7d3b53b76f2967cdc38456b0ee5926f29900036bd1ff44ac0d03615e53226d8a47191a75d07727c70f8566a7855b5e3dbf3721dae3554814d8a13d93a4
7
- data.tar.gz: 4a2c14cc4556cb88a658ae4994af79d2c8a001921accdfb9b731afeb210ea706e4307f9ea45a104abb061254263fa55d23f114007cdef9d35ffb70114c9116c0
6
+ metadata.gz: 5c40c5f964e4e9c5441460f7c5113ea4e55d83e73ed9be8cc82c8021a0b8d69772ea8f29a3de6b9a3706557fb8260377bebac2231542cfcbd940ef3973156a6c
7
+ data.tar.gz: ee972990ddda7041e3a756384022e46c68bc3e8af6e5188c545ad559e1c46618b7a1c0a5733940dd093c7a3d73aeb71a2012266de082776a3ec945b32c925067
data/docs/CHANGELOG.md CHANGED
@@ -5,7 +5,12 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [Unreleased]
8
+ ## [4.1.1](https://github.com/powerhome/nitro-intelligence.rb/compare/v4.1.0...v4.1.1) (2026-11-05)
9
+
10
+
11
+ ### Fixed
12
+
13
+ * reject a model of the wrong type before sending a request ([#181](https://github.com/powerhome/nitro-intelligence.rb/issues/181)) ([c84955d](https://github.com/powerhome/nitro-intelligence.rb/commit/c84955d9b3751f1c456ff1e03df48fa962204b7e))
9
14
 
10
15
  ## [4.1.0] - 2026-11-04
11
16
 
@@ -223,7 +228,6 @@ Each warns through `NitroIntelligence.deprecator`, and is removed in 5.0. See [U
223
228
  - Require Ruby 3.3 or later (#10)
224
229
  - Upgrade langfuse-rb to 0.7.0. (#12)
225
230
 
226
- [Unreleased]: https://github.com/powerhome/nitro-intelligence.rb/compare/v4.1.0-nitro_intelligence...HEAD
227
231
  [4.1.0]: https://github.com/powerhome/nitro-intelligence.rb/compare/v4.0.0-nitro_intelligence...v4.1.0-nitro_intelligence
228
232
  [4.0.0]: https://github.com/powerhome/nitro-intelligence.rb/compare/v3.1.0-nitro_intelligence...v4.0.0-nitro_intelligence
229
233
  [3.1.0]: https://github.com/powerhome/nitro-intelligence.rb/compare/v3.0.2-nitro_intelligence...v3.1.0-nitro_intelligence
@@ -26,8 +26,8 @@ module NitroIntelligence
26
26
  end
27
27
 
28
28
  # @api private
29
- def self.validate_model(model)
30
- raise ArgumentError, "Unsupported model: '#{model}'" unless NitroIntelligence.model_catalog.exists?(model)
29
+ def self.validate_model(model, type: Model)
30
+ NitroIntelligence.model_catalog.fetch(model, type:)
31
31
  end
32
32
  end
33
33
  end
@@ -14,9 +14,7 @@ module NitroIntelligence
14
14
  end
15
15
 
16
16
  def perform_request(audio_file:, message: "", parameters: {}, correlation_trace_id: nil, mask_content: false)
17
- add_request_headers(parameters, MODALITY_HEADER => "audio", REQUESTED_MODEL_HEADER => parameters[:model])
18
- add_correlation_headers(parameters, trace_id: correlation_trace_id)
19
- add_message_redaction_header(parameters, mask_content:)
17
+ add_gateway_headers(parameters, correlation_trace_id:, mask_content:, modality: "audio")
20
18
  @client.audio.transcriptions.create(
21
19
  prompt: message,
22
20
  file: audio_file,
@@ -31,7 +29,7 @@ module NitroIntelligence
31
29
  }
32
30
 
33
31
  parameters.replace(default_parameters.merge(parameters))
34
- Client.validate_model(parameters[:model])
32
+ Client.validate_model(parameters[:model], type: TextModel)
35
33
  end
36
34
  end
37
35
  end
@@ -119,6 +119,15 @@ module NitroIntelligence
119
119
  )
120
120
  end
121
121
 
122
+ # Every request to the gateway names the model asked for, and the modality where the
123
+ # route alone does not, carries the correlation headers and, for a project that masks
124
+ # content, asks the gateway to redact it from its logs.
125
+ def add_gateway_headers(parameters, correlation_trace_id:, mask_content:, modality: nil)
126
+ add_request_headers(parameters, MODALITY_HEADER => modality, REQUESTED_MODEL_HEADER => parameters[:model])
127
+ add_correlation_headers(parameters, trace_id: correlation_trace_id)
128
+ add_message_redaction_header(parameters, mask_content:)
129
+ end
130
+
122
131
  # A project that masks content in Cerebro keeps it out of the gateway's logs
123
132
  # too, so one setting decides where a request's content is recorded.
124
133
  def add_message_redaction_header(parameters, mask_content:)
@@ -14,9 +14,7 @@ module NitroIntelligence
14
14
  end
15
15
 
16
16
  def perform_request(parameters: {}, correlation_trace_id: nil, mask_content: false)
17
- add_request_headers(parameters, REQUESTED_MODEL_HEADER => parameters[:model])
18
- add_correlation_headers(parameters, trace_id: correlation_trace_id)
19
- add_message_redaction_header(parameters, mask_content:)
17
+ add_gateway_headers(parameters, correlation_trace_id:, mask_content:)
20
18
  @client.chat.completions.create(**parameters.slice(*ALLOWED_EXTRA_PARAMETERS))
21
19
  end
22
20
 
@@ -33,7 +31,7 @@ module NitroIntelligence
33
31
  }
34
32
 
35
33
  parameters.replace(default_parameters.merge(parameters))
36
- Client.validate_model(parameters[:model])
34
+ Client.validate_model(parameters[:model], type: TextModel)
37
35
  end
38
36
  end
39
37
  end
@@ -34,9 +34,7 @@ module NitroIntelligence
34
34
  end
35
35
 
36
36
  def perform_request(state:, questions:, parameters: {}, correlation_trace_id: nil, mask_content: false)
37
- add_request_headers(parameters, REQUESTED_MODEL_HEADER => parameters[:model])
38
- add_correlation_headers(parameters, trace_id: correlation_trace_id)
39
- add_message_redaction_header(parameters, mask_content:)
37
+ add_gateway_headers(parameters, correlation_trace_id:, mask_content:)
40
38
  response = @client.request(
41
39
  method: :post,
42
40
  path:,
@@ -54,12 +52,7 @@ module NitroIntelligence
54
52
  }
55
53
 
56
54
  parameters.replace(default_parameters.merge(parameters))
57
- Client.validate_model(parameters[:model])
58
- return if NitroIntelligence.model_catalog.lookup_by_name(parameters[:model]).is_a?(DecisionModel)
59
-
60
- # A decision request has nothing in common with a chat one, so a model of another type
61
- # would fail at the gateway with an error that does not say why.
62
- raise ArgumentError, "Model '#{parameters[:model]}' is not a decision model"
55
+ Client.validate_model(parameters[:model], type: DecisionModel)
63
56
  end
64
57
 
65
58
  private
@@ -21,12 +21,12 @@ module NitroIntelligence
21
21
  end
22
22
 
23
23
  def perform_request(parameters: {}, correlation_trace_id: nil, mask_content: false)
24
- add_request_headers(parameters, MODALITY_HEADER => "image", REQUESTED_MODEL_HEADER => parameters[:model])
25
- add_correlation_headers(parameters, trace_id: correlation_trace_id)
26
- add_message_redaction_header(parameters, mask_content:)
24
+ add_gateway_headers(parameters, correlation_trace_id:, mask_content:, modality: "image")
27
25
  @client.chat.completions.create(**parameters.slice(*ALLOWED_EXTRA_PARAMETERS))
28
26
  end
29
27
 
28
+ # The model is not checked here: building the ImageGeneration already checked it, along
29
+ # with the aspect ratio and resolution it supports.
30
30
  def validate_and_resolve!(parameters, image_generation)
31
31
  default_parameters = {
32
32
  image_generation:,
@@ -45,7 +45,6 @@ module NitroIntelligence
45
45
  # Deep, so that request options a caller passes (headers, max_retries,
46
46
  # timeout) sit alongside the image config rather than replacing it.
47
47
  parameters.replace(default_parameters.deep_merge(parameters))
48
- Client.validate_model(parameters[:model])
49
48
  end
50
49
 
51
50
  private
@@ -1,3 +1,4 @@
1
+ require "nitro_intelligence/client/handlers/observed/base_handler"
1
2
  require "base64"
2
3
  require "nitro_intelligence/media/audio"
3
4
  require "nitro_intelligence/observability/prompt_resolver"
@@ -7,18 +8,13 @@ module NitroIntelligence
7
8
  module Handlers
8
9
  module Observed
9
10
  # @api private
10
- class AudioTranscriptionHandler
11
+ class AudioTranscriptionHandler < BaseHandler
11
12
  # Raised by {Client::Base#transcribe_audio} on an observed client when the prompt it names
12
13
  # is not a text prompt.
13
14
  #
14
15
  # @api public
15
16
  class ObservedAudioTranscriptionPromptError < StandardError; end
16
17
 
17
- def initialize(base_handler:, observer:)
18
- @base_handler = base_handler
19
- @observer = observer
20
- end
21
-
22
18
  def create(audio_file:, message: +"", parameters: {})
23
19
  @base_handler.validate_and_resolve!(parameters)
24
20
 
@@ -39,10 +35,6 @@ module NitroIntelligence
39
35
 
40
36
  private
41
37
 
42
- def project
43
- @observer.project_client.project
44
- end
45
-
46
38
  def handle_prompt(message:, parameters:)
47
39
  prompt = NitroIntelligence::Observability::PromptResolver.for(
48
40
  store: project.prompt_store,
@@ -0,0 +1,23 @@
1
+ module NitroIntelligence
2
+ module Client
3
+ module Handlers
4
+ module Observed
5
+ # Wraps a handler's request in an observation recorded to the observer's Cerebro project.
6
+ #
7
+ # @api private
8
+ class BaseHandler
9
+ def initialize(base_handler:, observer:)
10
+ @base_handler = base_handler
11
+ @observer = observer
12
+ end
13
+
14
+ private
15
+
16
+ def project
17
+ @observer.project_client.project
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -1,3 +1,4 @@
1
+ require "nitro_intelligence/client/handlers/observed/base_handler"
1
2
  require "nitro_intelligence/observability/prompt_resolver"
2
3
 
3
4
  module NitroIntelligence
@@ -6,7 +7,7 @@ module NitroIntelligence
6
7
  # @api private
7
8
  module Observed
8
9
  # @api private
9
- class ChatHandler
10
+ class ChatHandler < BaseHandler
10
11
  # Raised by {Client::Base#chat} on an observed client when the request carries no user
11
12
  # message for the model to answer: the prompt supplies only a system message, and the
12
13
  # caller passed no `message`.
@@ -14,11 +15,6 @@ module NitroIntelligence
14
15
  # @api public
15
16
  class ObservedChatPromptError < StandardError; end
16
17
 
17
- def initialize(base_handler:, observer:)
18
- @base_handler = base_handler
19
- @observer = observer
20
- end
21
-
22
18
  def create(message: "", parameters: {})
23
19
  @base_handler.validate_and_resolve!(parameters, message)
24
20
 
@@ -41,10 +37,6 @@ module NitroIntelligence
41
37
 
42
38
  private
43
39
 
44
- def project
45
- @observer.project_client.project
46
- end
47
-
48
40
  # A chat completion needs a turn for the model to answer, and it is the model's own
49
41
  # chat template that insists on one: Qwen's raises "No user query found in
50
42
  # messages." Left to the gateway that costs a round trip and comes back as a 400
@@ -1,3 +1,5 @@
1
+ require "nitro_intelligence/client/handlers/observed/base_handler"
2
+
1
3
  module NitroIntelligence
2
4
  module Client
3
5
  module Handlers
@@ -7,12 +9,7 @@ module NitroIntelligence
7
9
  # powerhome/cerebro#128.
8
10
  #
9
11
  # @api private
10
- class DecisionHandler
11
- def initialize(base_handler:, observer:)
12
- @base_handler = base_handler
13
- @observer = observer
14
- end
15
-
12
+ class DecisionHandler < BaseHandler
16
13
  def create(state:, questions:, parameters: {})
17
14
  @base_handler.validate_and_resolve!(parameters)
18
15
 
@@ -33,10 +30,6 @@ module NitroIntelligence
33
30
 
34
31
  private
35
32
 
36
- def project
37
- @observer.project_client.project
38
- end
39
-
40
33
  def workflow(generation:, state:, questions:, parameters:)
41
34
  decision = @base_handler.perform_request(
42
35
  state:, questions:, parameters:, correlation_trace_id: generation.trace_id,
@@ -1,3 +1,4 @@
1
+ require "nitro_intelligence/client/handlers/observed/base_handler"
1
2
  require "nitro_intelligence/media/image_generation"
2
3
  require "nitro_intelligence/observability/prompt_resolver"
3
4
 
@@ -6,12 +7,7 @@ module NitroIntelligence
6
7
  module Handlers
7
8
  module Observed
8
9
  # @api private
9
- class ImageHandler
10
- def initialize(base_handler:, observer:)
11
- @base_handler = base_handler
12
- @observer = observer
13
- end
14
-
10
+ class ImageHandler < BaseHandler
15
11
  def create(message: "", target_image: nil, reference_images: [], parameters: {})
16
12
  image_generation = build_image_generation(message:, target_image:, reference_images:, parameters:)
17
13
 
@@ -36,10 +32,6 @@ module NitroIntelligence
36
32
 
37
33
  private
38
34
 
39
- def project
40
- @observer.project_client.project
41
- end
42
-
43
35
  def build_image_generation(message:, target_image:, reference_images:, parameters:)
44
36
  NitroIntelligence::ImageGeneration.new(message:, target_image:, reference_images:) do |config|
45
37
  config.aspect_ratio = parameters[:aspect_ratio] if parameters.key?(:aspect_ratio)
@@ -1,3 +1,4 @@
1
+ require "nitro_intelligence/client/handlers/observed/base_handler"
1
2
  require "nitro_intelligence/media/audio"
2
3
  require "nitro_intelligence/observability/prompt_resolver"
3
4
 
@@ -6,18 +7,13 @@ module NitroIntelligence
6
7
  module Handlers
7
8
  module Observed
8
9
  # @api private
9
- class TextToSpeechHandler
10
+ class TextToSpeechHandler < BaseHandler
10
11
  # Raised by {Client::Base#text_to_speech} on an observed client when the prompt it names
11
12
  # is not a text prompt.
12
13
  #
13
14
  # @api public
14
15
  class ObservedTextToSpeechPromptError < StandardError; end
15
16
 
16
- def initialize(base_handler:, observer:)
17
- @base_handler = base_handler
18
- @observer = observer
19
- end
20
-
21
17
  def create(message: "", parameters: {})
22
18
  prompt = handle_prompt(parameters:)
23
19
 
@@ -39,10 +35,6 @@ module NitroIntelligence
39
35
 
40
36
  private
41
37
 
42
- def project
43
- @observer.project_client.project
44
- end
45
-
46
38
  def handle_prompt(parameters:)
47
39
  prompt = NitroIntelligence::Observability::PromptResolver.for(
48
40
  store: project.prompt_store,
@@ -14,9 +14,7 @@ module NitroIntelligence
14
14
  end
15
15
 
16
16
  def perform_request(message: "", parameters: {}, correlation_trace_id: nil, mask_content: false)
17
- add_request_headers(parameters, MODALITY_HEADER => "audio", REQUESTED_MODEL_HEADER => parameters[:model])
18
- add_correlation_headers(parameters, trace_id: correlation_trace_id)
19
- add_message_redaction_header(parameters, mask_content:)
17
+ add_gateway_headers(parameters, correlation_trace_id:, mask_content:, modality: "audio")
20
18
  @client.audio.speech.create(
21
19
  input: message,
22
20
  **parameters.slice(*ALLOWED_EXTRA_PARAMETERS)
@@ -25,8 +23,7 @@ module NitroIntelligence
25
23
 
26
24
  def validate_and_resolve!(parameters)
27
25
  model_name = parameters[:model] || NitroIntelligence.model_catalog.default_text_to_speech_model&.name
28
- model = NitroIntelligence.model_catalog.lookup_by_name(model_name)
29
- raise ArgumentError, "Unsupported model: '#{model_name}'" unless model
26
+ model = Client.validate_model(model_name, type: TextToSpeechModel)
30
27
 
31
28
  default_parameters = {
32
29
  metadata: {},
@@ -123,8 +123,7 @@ module NitroIntelligence
123
123
 
124
124
  def validate_config!
125
125
  # Check model supported
126
- @model = NitroIntelligence.model_catalog.lookup_by_name(@config.model)
127
- raise ArgumentError, "Unsupported model: '#{@config.model}'" unless @model
126
+ @model = NitroIntelligence.model_catalog.fetch(@config.model, type: ImageModel)
128
127
 
129
128
  # Check aspect_ratio supported
130
129
  unless @model.aspect_ratios.include?(@config.aspect_ratio)
@@ -1,6 +1,11 @@
1
1
  module NitroIntelligence
2
2
  # @api private
3
3
  class Model
4
+ # What the model is for, as an error names it: "image" for ImageModel, "text-to-speech" for
5
+ # TextToSpeechModel. Taken from the class name, so a new kind of model has one without
6
+ # declaring it.
7
+ def self.kind = name.demodulize.delete_suffix("Model").underscore.dasherize
8
+
4
9
  attr_reader :name, :omit_output_fields
5
10
 
6
11
  def initialize(name:, omit_output_fields: [], **)
@@ -24,6 +24,21 @@ module NitroIntelligence
24
24
  @models.find { |model| model.name == name }
25
25
  end
26
26
 
27
+ # The named model, checked to be of the type a request needs. A model of another type
28
+ # would fail at the gateway, or deep in a handler's own validation, with an error that
29
+ # does not say why.
30
+ def fetch(name, type: Model)
31
+ model = lookup_by_name(name)
32
+ raise ArgumentError, "Unsupported model: '#{name}'" unless model
33
+
34
+ return model if model.is_a?(type)
35
+
36
+ article = type.kind.start_with?(/[aeiou]/) ? "an" : "a"
37
+ raise ArgumentError, "Model '#{name}' is not #{article} #{type.kind} model"
38
+ end
39
+
40
+ # Unused within the gem, which validates through #fetch, but kept for applications that check
41
+ # whether a model is configured before offering it.
27
42
  def exists?(name)
28
43
  lookup_by_name(name).present?
29
44
  end
@@ -1,4 +1,4 @@
1
1
  module NitroIntelligence
2
2
  # The gem's version.
3
- VERSION = "4.1.0".freeze
3
+ VERSION = "4.1.1".freeze
4
4
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nitro_intelligence
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.1.0
4
+ version: 4.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Igor Artemenko
@@ -90,6 +90,7 @@ files:
90
90
  - lib/nitro_intelligence/client/handlers/decision_handler.rb
91
91
  - lib/nitro_intelligence/client/handlers/image_handler.rb
92
92
  - lib/nitro_intelligence/client/handlers/observed/audio_transcription_handler.rb
93
+ - lib/nitro_intelligence/client/handlers/observed/base_handler.rb
93
94
  - lib/nitro_intelligence/client/handlers/observed/chat_handler.rb
94
95
  - lib/nitro_intelligence/client/handlers/observed/decision_handler.rb
95
96
  - lib/nitro_intelligence/client/handlers/observed/image_handler.rb