nitro_intelligence 3.1.0 → 4.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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +9 -0
  3. data/Rakefile +21 -1
  4. data/docs/ASSISTANTS.md +594 -0
  5. data/docs/CHANGELOG.md +243 -0
  6. data/docs/README.md +190 -12
  7. data/lib/nitro_intelligence/assistant.rb +82 -3
  8. data/lib/nitro_intelligence/assistant_registry.rb +18 -0
  9. data/lib/nitro_intelligence/assistants.rb +340 -102
  10. data/lib/nitro_intelligence/client/base.rb +88 -8
  11. data/lib/nitro_intelligence/client/client.rb +22 -2
  12. data/lib/nitro_intelligence/client/factory.rb +12 -3
  13. data/lib/nitro_intelligence/client/handlers/audio_transcription_handler.rb +3 -1
  14. data/lib/nitro_intelligence/client/handlers/base_handler.rb +17 -2
  15. data/lib/nitro_intelligence/client/handlers/chat_handler.rb +3 -1
  16. data/lib/nitro_intelligence/client/handlers/decision_handler.rb +75 -0
  17. data/lib/nitro_intelligence/client/handlers/image_handler.rb +6 -2
  18. data/lib/nitro_intelligence/client/handlers/observed/audio_transcription_handler.rb +30 -12
  19. data/lib/nitro_intelligence/client/handlers/observed/chat_handler.rb +16 -3
  20. data/lib/nitro_intelligence/client/handlers/observed/decision_handler.rb +72 -0
  21. data/lib/nitro_intelligence/client/handlers/observed/image_handler.rb +20 -5
  22. data/lib/nitro_intelligence/client/handlers/observed/text_to_speech_handler.rb +27 -9
  23. data/lib/nitro_intelligence/client/handlers/text_to_speech_handler.rb +3 -1
  24. data/lib/nitro_intelligence/client/observed.rb +8 -0
  25. data/lib/nitro_intelligence/client/observers/langfuse_observer.rb +35 -7
  26. data/lib/nitro_intelligence/configuration.rb +48 -0
  27. data/lib/nitro_intelligence/decision.rb +105 -0
  28. data/lib/nitro_intelligence/deprecator.rb +10 -0
  29. data/lib/nitro_intelligence/langfuse_extension.rb +1 -0
  30. data/lib/nitro_intelligence/langfuse_tracer_provider.rb +7 -6
  31. data/lib/nitro_intelligence/media/audio.rb +6 -0
  32. data/lib/nitro_intelligence/media/image.rb +14 -1
  33. data/lib/nitro_intelligence/media/image_generation.rb +14 -2
  34. data/lib/nitro_intelligence/media/media.rb +4 -0
  35. data/lib/nitro_intelligence/models/model.rb +7 -0
  36. data/lib/nitro_intelligence/models/model_catalog.rb +3 -0
  37. data/lib/nitro_intelligence/models/model_factory.rb +2 -0
  38. data/lib/nitro_intelligence/null_cache.rb +2 -0
  39. data/lib/nitro_intelligence/observability/project.rb +41 -2
  40. data/lib/nitro_intelligence/observability/project_client.rb +10 -1
  41. data/lib/nitro_intelligence/observability/project_client_registry.rb +18 -0
  42. data/lib/nitro_intelligence/observability/prompt.rb +4 -3
  43. data/lib/nitro_intelligence/observability/prompt_resolver.rb +2 -0
  44. data/lib/nitro_intelligence/observability/prompt_store.rb +9 -0
  45. data/lib/nitro_intelligence/observability/upload_handler.rb +3 -1
  46. data/lib/nitro_intelligence/pending_review.rb +257 -0
  47. data/lib/nitro_intelligence/railtie.rb +10 -0
  48. data/lib/nitro_intelligence/reporter.rb +29 -0
  49. data/lib/nitro_intelligence/review_decision.rb +36 -0
  50. data/lib/nitro_intelligence/run_result.rb +160 -0
  51. data/lib/nitro_intelligence/tool_call_review_interrupt.rb +9 -13
  52. data/lib/nitro_intelligence/tool_call_review_validator.rb +6 -8
  53. data/lib/nitro_intelligence/trace.rb +8 -0
  54. data/lib/nitro_intelligence/version.rb +2 -1
  55. data/lib/nitro_intelligence.rb +40 -0
  56. metadata +12 -1
data/docs/CHANGELOG.md ADDED
@@ -0,0 +1,243 @@
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
+ ## [4.1.0] - 2026-11-04
11
+
12
+ ### Added
13
+
14
+ - A Cerebro project can now opt in to content masking with `"mask_content" => true` in `observability_projects`. For a masked project the input and output of every observed request are recorded as `[masked]`, a failure records its error class without its message, and no media is uploaded; model, usage, cost, prompt link, metadata and the gateway call ID are still recorded. Masking is applied by the gem on every write rather than through langfuse-rb's `mask`, which is read from process-global configuration and so cannot differ between projects. A masked project's requests also send `x-litellm-enable-message-redaction`, so the inference gateway redacts their messages and responses from its own logs. Projects that do not set the flag are unchanged. (#169)
15
+ - `Client#decide` asks a decision model `noul`, `choice` and `score` questions over the gateway's System One route (`POST /v1/systemone`) and returns a `NitroIntelligence::Decision` of typed answers keyed by question name. With observability, each call is recorded in Cerebro as a generation with its cost. Decision models are configured in `model_config` with the type `decision` and a `default_decision_model`. (#178)
16
+ - A stated public API: what the API reference generated by YARD documents is public and follows Semantic Versioning. It includes, and the guides now explain, what applications already relied on without it being written down: `Reporter#create_dataset_item`, the `current_revision` setting, `NitroIntelligence.project_client_registry` and a project's `id` and `auth_token`, the errors a request raises for a prompt it cannot use, `Image#mime_type`, `#width` and `#height`, and the hooks a subclass of `Assistants` may call. Everything else is marked `@api private`, left out of the reference, and may change in any release. (#180)
17
+
18
+ ### Fixed
19
+
20
+ - A `parameters` or `request_options` hash, or a message, passed to several requests is left unchanged, so what one request adds to it is no longer sent with the next: its default messages, gateway headers and spend-logs metadata, a managed prompt's instructions and name, the values a chat prompt was compiled with, or the media references that replace images in its recording. (#178)
21
+ - Text-to-speech uploads are labeled as output instead of input. (#173)
22
+ - Image and audio uploads now include the generation's observation ID so Cerebro can display them in the correct observation preview. `Observability::UploadHandler#upload` now requires `observation_id:`. (#167)
23
+
24
+ ## [4.0.0] - 2026-10-28
25
+
26
+ See [Upgrading to 4.0](ASSISTANTS.md#upgrading-to-40).
27
+
28
+ ### Changed
29
+
30
+ - **Breaking:** `Assistants#await_run` (and `Assistant#await_run`) returns `nil` when the run paused for review, and when it ended without an AI reply, where it returned the last message's content: a draft awaiting review, a tool-calling turn's text, or `""`. A caller that sent whatever it returned would send a draft nobody had reviewed. A reply given as content blocks is returned as its joined text, not the raw Array. The method keeps its name and arguments, and is now `run(...).reply`. Upgrade before any assistant turns reply review on. With a delivery tool configured, the platform sends the reply itself: the reply `#run`, `#await_run` and `#review` return is for display and records, and must not be sent again.
31
+ - **Breaking:** resumes are keyed by the interrupt they answer, `command.resume = {"<interrupt id>" => {"decisions" => [...]}}`, so a resume never decides a pause that appeared after it was read. `#review_tool_calls` raises `Assistants::ReviewSupersededError`, a `ThreadResumptionError`, when the resumed run shows its decisions were not applied; a resume Assistants refuses is still raised with the response body as its message.
32
+ - `#review_tool_calls` and `#tool_calls_under_review` read the pause through `PendingReview`. On a thread paused on its reply, `#tool_calls_under_review` returns `[]` and `#review_tool_calls` raises its usual "has no tool calls awaiting review", where both raised that no tool call matched `__reply__`. `#review_tool_calls` also takes a list of reviews that each carry their tool call's `id`, and optional `reviewer_id:` and `reviewed_at:`, sent in the resume run's `config.metadata`.
33
+ - The client no longer retries failed requests by default. openai-ruby retried each `429`, `408`, `409` and `5xx` twice within about two seconds, which turned every rate-limited request into three rejected calls on top of any retries the caller made. Retries and the request timeout can now be set for every client (`inference_max_retries`, `inference_timeout`), for one client (`NitroIntelligence::Client.new(inference_max_retries:, inference_timeout:)`) or for one request (`request_options: { max_retries:, timeout: }`); set `inference_max_retries` to restore retries for every client. `generate_image` now keeps its image configuration when the caller passes `request_options`; before, the caller's options replaced it, dropping the requested aspect ratio and resolution. (#157)
34
+
35
+ ### Added
36
+
37
+ - Reply review, for assistants that hold their final reply for review (`__reply__`), and the delivery status of replies the platform delivers through the assistant's delivery tool. See [ASSISTANTS.md](ASSISTANTS.md).
38
+ - `Assistants#run` (and `Assistant#run`) returns a `NitroIntelligence::RunResult`: `reply` (the run's final AI message with text and no tool calls; `nil` while paused or after a rejected reply), `interrupted?`, `review`, `delivery`, `messages` and `raw`.
39
+ - `RunResult#delivery` reads the reply's `response_metadata.delivery` stamp: `status` (`"delivered"`, `"failed"`, or `"pending"` when delivery was cut off before it recorded an outcome), `error`, `attempts`, `result`, `tool`, `delivered_at` and `failed_at`; `nil` when the reply carries no stamp, as without a delivery tool, so read it with `&.`. A draft still stamped `review: {"type": "pending"}` is never returned as `reply`.
40
+ - `NitroIntelligence::PendingReview`, what a paused run waits on: `kind`, `interrupt_id`, and either `tool_calls` or `reply` (`content`, `allowed_decisions` from `approve`, `edit` and `reject`, `description`).
41
+ - `#under_review` reads the pending review from the thread state.
42
+ - `#review` resumes a pause with exactly one of `reply:` or `tool_calls:` and a required `reviewer_id`. A reply review also requires the `interrupt_id` the reviewer was shown, so an approval never decides a newer draft that replaced the one they read; for tool calls it is optional. It validates the decisions before sending, puts `reviewer_id` and `reviewed_at` in the resume run's `config.metadata`, keys the resume by the interrupt it read, and returns the resumed run's `RunResult`.
43
+ - `NitroIntelligence.deprecator`, an `ActiveSupport::Deprecation` that a Railtie registers with the Rails application's deprecators, so the host's deprecation settings apply to this gem's warnings.
44
+ - `Assistants::ReviewSupersededError`, a `ThreadResumptionError`, raised by `#review` when the thread has moved past the review: it is no longer paused on the given `interrupt_id`, another review resumed it first, or the resumed run shows the decision was not applied.
45
+
46
+ ### Deprecated
47
+
48
+ Each warns through `NitroIntelligence.deprecator`, and is removed in 5.0. See [Upgrading to 4.0](ASSISTANTS.md#upgrading-to-40).
49
+
50
+ - `#await_run`: use `#run(...).reply`.
51
+ - `#review_tool_calls`: use `#review(tool_calls:, reviewer_id:, interrupt_id:)`.
52
+ - `#tool_calls_under_review`: use `#under_review(thread_id:)&.tool_calls.to_a`.
53
+ - `#tool_calls_pending_review`: use `#under_review(thread_id:)&.tool_calls.to_a`, which reports only the calls a reviewer has to decide. nitro-web's `VirtualConfirmationAgent::Client` still needs it until the VCA moves onto the platform.
54
+
55
+ ## [3.1.0] - 2026-10-06
56
+
57
+ ### Changed
58
+
59
+ - Cerebro OpenTelemetry exports now send `x-langfuse-ingestion-version: 4` by default, selecting Langfuse's v4 ingestion path for the migration to `events_only`. Consumers must use v4-compatible observation attributes and roots. (#137)
60
+ - Seeded observations now follow the configured OpenTelemetry root sampler instead of being forced sampled by an unexported placeholder parent. `OTEL_TRACES_SAMPLER` and `OTEL_TRACES_SAMPLER_ARG` can therefore reduce or disable seeded-trace export; the default always-on policy continues to export them. (#137)
61
+
62
+ ### Fixed
63
+
64
+ - Seeded observations now export as actual roots under their deterministic trace ID, so Cerebro retains their trace input and output instead of treating each generation as a child of an unexported placeholder. Calls without a seed retain their ambient parent context. (#137)
65
+
66
+ ### Added
67
+
68
+ - `NitroIntelligence::Reporter#score` now accepts a `data_type` keyword argument, allowing for `:numeric` (the default), `:boolean`, and `:categorical` scores to be submitted on traces. (#119)
69
+ - `LangfuseObserver` records two things a handler could not report before: `model_parameters`, the settings a generation ran under, which matter because a prompt config can change them without the caller ever naming them; and a `level` and `status_message` for a response the endpoint answered but did not finish, which defaults to `WARNING` so that a generation cut off at its token ceiling is findable without sitting among the errors. Both are written only when a handler reports them, so handlers adopt them one at a time and every existing observation is unchanged (#123)
70
+
71
+ ## [3.0.2] - 2026-09-15
72
+
73
+ ### Fixed
74
+
75
+ - Audio transcriptions now handle models that report usage in seconds.
76
+
77
+ ## [3.0.1] - 2026-09-15
78
+
79
+ ### Fixed
80
+
81
+ - `#chat` accepted a request carrying no user message and sent it to be inferred, where it could only fail. A chat completion needs a turn for the model to answer -- the model's own chat template is what insists on one, and Qwen's raises `No user query found in messages.` -- so the request cost a round trip and came back as a `400` whose body the client could not read, leaving the caller with `status=400` and nothing pointing at the cause. Such a request is now refused before any inference happens, raising `Observed::ChatHandler::ObservedChatPromptError` with the cause it found: a text prompt contributes only a system message and needs a `message:` from the caller, a chat prompt can carry its own user message and should be given one in Cerebro, and a request with neither is told what to pass. A user message with blank content is refused too, which is marginally stricter than templates that accept one, on the grounds that a blank turn is nearly always a caller bug; content arriving as an array of parts is accepted, since a turn carrying only an image is legitimate. Callers wanting a generation driven by an instruction alone should use `#complete`, which applies no chat template (#108)
82
+
83
+ ## [3.0.0] - 2026-09-13
84
+
85
+ ### Added
86
+
87
+ - `Assistants#tool_calls_under_review`: the tool calls the thread's interrupt is holding, in the order the platform wants decisions for them, each carrying the `allowed_decisions` a reviewer may take on it. A tool the assistant is not configured to interrupt on runs without review, so one AI message can mix calls under review with calls that are only waiting to be executed; `#tool_calls_pending_review` reports both, and only the calls this reports may be reviewed. A review interface reading it no longer has to fetch the thread state and interpret the interrupt itself to know which decisions to offer (#91)
88
+ - `Assistants#review_tool_calls` accepts the `reject` and `respond` actions alongside `approve` and `edit`, and takes an optional `context`, sent with the resumed run as `#await_run` sends its own. `reject` skips the call and tells the model why; `respond` skips it and returns the reviewer's `message` to the model as the tool's result. `edit` arguments are merged over the ones the model asked for, so a reviewer correcting one of them cannot drop the rest by omitting them (#91)
89
+
90
+ ### Changed
91
+
92
+ - The review collaborators follow the protocol under Fixed: `ToolCallReviewValidator#validate!` takes `tool_calls_under_review` in place of `thread_state` and `pending_tool_calls`, reading the permitted actions off the interrupt rather than off the thread state, so the interrupt is interpreted in one place -- the new `ToolCallReviewInterrupt`, which recovers the tool calls an interrupt is holding and builds the decisions that answer them. `Assistant#review_tool_calls` drops `reviewer_id` to match the client it delegates to, and `Assistant` delegates `tool_calls_under_review` alongside the other thread-scoped calls (#91)
93
+
94
+ ### Removed
95
+
96
+ - `Assistants#review_tool_calls`'s `reviewer_id` and `reviewed_at` arguments, outright rather than through a deprecation: the platform records neither, and the resume payload it accepts has nowhere to carry them. Nothing can be relying on them, since no review could complete at all before this, so there has never been a working call to pass them to; the one consumer that reaches this area, nitro-web's `ContactCenter::VirtualConfirmationAgent::Client`, overrides `#review_tool_calls` entirely. A call still passing either now raises `ArgumentError` (#91)
97
+ - `NitroIntelligence::AgentServer`, `NitroIntelligence.agent_server` and the `agent_server_config` setting, deprecated in 2.4.0 for removal in 3.0. A host still on the old names must move to `NitroIntelligence::Assistants`, `NitroIntelligence.assistants` and `assistants_config` before upgrading: the constant now raises `NameError`, the method `NoMethodError`, and `agent_server_config` is neither readable nor writable. A host that set `agent_server_config` and never set `assistants_config` is left with no configuration at all: `assistants_config` defaults to `{}`, and `Assistants.new` takes `api_key` as a required keyword, so the first `NitroIntelligence.assistants` call raises `ArgumentError: missing keyword: :api_key` before a client is built or a request sent. The failure is immediate rather than deferred, but it names the missing keyword rather than the setting that was removed, so migrate before upgrading rather than after the first exception. `NitroIntelligence.deprecator` goes with them: it existed to carry these three names and nothing else, and its horizon was 3.0, so it has no remaining subject. A future deprecation introduces its own deprecator against its own horizon (#102)
98
+
99
+ ### Fixed
100
+
101
+ - `Assistants#review_tool_calls` speaks the review protocol Assistants actually implements, so an interrupt can be resumed at all. It validated the reviewer's action against `interrupts[0].value.review_actions` and resumed with `{reviewer_id, reviewed_at, tool_calls}`, neither of which exists on the platform: every assistant runs LangChain's `HumanInTheLoopMiddleware`, which publishes `action_requests` and `review_configs` and resumes with `decisions`. The old key made the permitted actions an empty array, so every review failed validation before a request was sent, and the payload would have been rejected by the server had it got that far. Actions are now validated against the interrupt's `review_configs[].allowed_decisions`, and the resume sends one decision per action request, in the order the middleware matches them. Action requests carry no tool call id, so each is matched back onto the tool calls of the thread's last AI message to recover the id reviews are keyed by. The resumed run carries the caller's `context` for the same reason: it sent `interrupts[0].value.context`, another key the platform never publishes, so a resume always sent `{}` -- wrong for an assistant whose prompt has to be rendered again with its `prompt_variables` once the tool has run (#91)
102
+ - `Assistants#review_tool_calls` reads the thread state once. It fetched `/threads/{thread_id}/state` directly and then again inside the `#tool_calls_pending_review` call it passed to the validator, so every review cost two reads of the same state -- and the two could disagree, leaving a review validated against one state and resumed against another. The interrupt is now parsed once, from a single read (#91)
103
+
104
+ ## [2.8.0] - 2026-09-13
105
+
106
+ ### Added
107
+
108
+ - Observed text-to-speech generations carry the inference gateway's cost as `cost_details`, alongside the chat, image and audio-transcription handlers that already did. Speech was the one modality left out: its endpoint returns a bare `StringIO` rather than a typed model, and the OpenAI SDK attached response metadata only to typed models, so the header the gateway reports cost in never reached us. Fixed upstream in openai/openai-ruby#561 and released in 0.86. Usage details are still absent for speech - token counts come from a response body that a binary endpoint does not have - so these generations carry a cost without a usage breakdown (#99)
109
+
110
+ ### Changed
111
+
112
+ - The minimum `openai` dependency is now 0.86, raised from 0.79. 0.86 is the first release that exposes `last_response` on the binary responses text-to-speech returns, and on anything older the speech cost is silently never recorded rather than failing loudly (#99)
113
+
114
+ ### Fixed
115
+
116
+ - `Assistants#await_run` raises `Assistants::RunError` when a run fails inside an HTTP 200 response, instead of returning `nil` as if the agent had nothing to say. The wait endpoint streams, so its status is committed before the run finishes and the failure is reported in the body as `__error__`; the message from the run is carried into the exception. A run that never finished raises the same way. `#review_tool_calls` raises `ThreadResumptionError` for the same condition on the run it resumes, which it previously discarded entirely. A run that pauses for human review without producing text still returns `nil`
117
+
118
+ ## [2.7.0] - 2026-09-10
119
+
120
+ ### Changed
121
+
122
+ - The base URLs of the three services this gem talks to default to their shared deployments: `inference_base_url` to `https://inference.powerhome.ai`, `observability_base_url` to `https://cerebro.powerhome.ai`, and an `assistants_config` entry's `base_url` to `https://assistants.powerhome.ai`. Every consumer set all three identically at boot, and one that forgot got a client built against an empty base URL - a request to a relative path, failing wherever the underlying SDK happened to notice - rather than a clear failure or the deployment it meant. A host reaching a different deployment still says so and is unaffected. The observability default is ungated: a host's development and staging environments report to Cerebro production too, since there is no one-to-one mapping between an application's environment and a Cerebro instance (#98)
123
+ - `base_url` is no longer required anywhere in `assistants_config`. `Assistant::ConfigurationError` no longer names it among an entry's missing fields, and `Assistants.new` no longer raises `"base_url is required"`, so both the registry shape and the single-client shape that predates it reach the shared deployment when the configuration names none. That is the point of the change, but it does mean a `base_url` that is absent or misspelled surfaces when a request is made rather than when the client is built (#98)
124
+
125
+ ## [2.6.0] - 2026-09-04
126
+
127
+ ### Added
128
+
129
+ - Assistants are addressable by name. When `assistants_config` carries a `definitions` hash, `NitroIntelligence.assistants` returns an `AssistantRegistry`, and `NitroIntelligence.assistants["candidate-concierge"]` resolves one `Assistant`: a client built for that assistant's deployment, plus the `assistant_id` that `await_run` and `review_tool_calls` supply on the caller's behalf. The thread-scoped calls (`thread_state`, `thread_messages`, `tool_calls_pending_review`) delegate untouched, and `assistant.client` reaches the underlying client. A host talking to several assistants previously had to build and hold a client per assistant itself and thread an assistant id through every call. Connection settings shared by every assistant sit at the top level of the configuration and each entry overrides them where it needs to, so a single assistant can be pointed at a review deployment without restating the rest. The key an entry is filed under is what it is looked up by, distinct from any `name` the entry carries for display. An entry may carry keys this gem has no use for - a graph id, an observability project - and they are ignored, so a deployment and the application reading it can share one structure. Credentials are plain attributes on the entry: this gem reads no environment and assumes no naming convention, because how a deployment mounts the Secret `nip-operator` writes is each consumer's choice and resolving it here would encode one consumer's wiring for all of them. A name that resolves without `base_url`, `api_key` or `assistant_id` raises `Assistant::ConfigurationError` naming every missing field at once; an unconfigured name raises `AssistantRegistry::UnknownAssistantError` listing the names that are configured (#94)
130
+
131
+ ### Changed
132
+
133
+ - `NitroIntelligence.assistants` returns an `AssistantRegistry` only when `assistants_config` carries `definitions`. Configuration without it is read exactly as before - as keyword arguments for a single `Assistants` client, which is what `NitroIntelligence.assistants` still returns - so a host that has not reshaped its configuration is unaffected and needs no coordinated release (#94)
134
+
135
+ ## [2.5.0] - 2026-08-29
136
+
137
+ ### Added
138
+
139
+ - Record the inference gateway's `x-litellm-response-cost` on observed generations as `cost_details`, so the observability platform reports the cost the gateway calculated instead of inferring one from its own per-project model pricing. The gateway is the only component that knows which deployment actually served a request - the same model group can be served by internal capacity or by any of several third-party providers at materially different rates - so a cost inferred downstream duplicates the price table doing the billing and drifts from it silently. Applies to the observed chat, image and audio-transcription handlers. The input and output components are recorded when the gateway sends them; routes whose cost comes from the upstream provider rather than the gateway's own calculation report only a total. A deployment the gateway has no price for sends no cost header at all, and those generations are left without a cost rather than recorded as free, so an unpriced model is never mistaken for a free one (#84)
140
+
141
+ ### Changed
142
+
143
+ - The minimum `openai` dependency is now 0.79, raised from 0.58. The gateway reports its cost in an HTTP response header, and `last_response` - the only route to a response's headers from a typed model - was added to the OpenAI SDK in 0.79. On an older SDK the cost is silently never recorded rather than failing loudly, so an application holding an older lock would upgrade `nitro_intelligence` and see no cost at all (#84)
144
+
145
+ ### Fixed
146
+
147
+ - `Reporter#create_dataset_item` raises `Reporter::DatasetItemError` when the write is rejected, instead of returning the failed response as if it had worked. A rejected write - bad credentials, a malformed item, a dataset that does not exist - was indistinguishable from a successful one, so a caller building a dataset could run an experiment against items that were never stored (#78)
148
+
149
+ ## [2.4.0] - 2026-08-28
150
+
151
+ ### Added
152
+
153
+ - `Assistants#thread_state` and `Assistants#thread_messages`: read a thread's state, or just its messages, as Assistants reports them. Consumers displaying an existing conversation no longer have to repeat the request, authentication, parsing and error handling the SDK already does. Both raise the new `Assistants::ThreadStateError` when the state cannot be fetched; the error raised by the existing review flows is unchanged (#36)
154
+
155
+ ### Changed
156
+
157
+ - The agent server is now Nitro Intelligence Assistants. `NitroIntelligence::AgentServer` is `NitroIntelligence::Assistants`, `NitroIntelligence.agent_server` is `NitroIntelligence.assistants`, and the `agent_server_config` setting is `assistants_config`. Documentation refers to the service by that name and defers to the [Nitro Intelligence Assistants documentation](https://portal.powerapp.cloud/docs/default/system/nip-assistants) rather than to the underlying Agent Protocol server (#73)
158
+
159
+ ### Deprecated
160
+
161
+ - `NitroIntelligence::AgentServer`, `NitroIntelligence.agent_server` and the `agent_server_config` setting. Each still works and warns through `NitroIntelligence.deprecator`; all three are removed in 3.0. `NitroIntelligence::AgentServer` resolves to the `Assistants` class itself rather than to a stand-in for it, so `is_a?`, `===`, `rescue` and the nested error constants all keep working against the old name. A host that sets both `assistants_config` and `agent_server_config` gets `assistants_config`, so a stale legacy setting cannot override the one that replaced it (#73)
162
+
163
+ ## [2.3.0] - 2026-08-26
164
+
165
+ ### Added
166
+
167
+ - Record an observation's `input` before the request runs, so a request that raises still shows what was sent. Image generation is excluded: its input carries base64 payloads that are only replaced with media references on success (#66)
168
+ - Mark a failed observation with `level: "ERROR"` and a `status_message` carrying the exception class and message, and record the inference gateway's `x-litellm-call-id` from the error response as observation metadata. Previously a request that raised left an observation carrying only its name, requested model and metadata, with no indication anything had gone wrong. The exception is still re-raised, so caller behaviour is unchanged (#66)
169
+ - Send the observation's trace ID to the inference gateway as `x-litellm-trace-id`, so a trace can be matched to a gateway request even when the request fails and never produces a response body. Sent only on the observed path: the trace ID comes from the observation being recorded, never from whatever tracing context happens to be active, so a client built without an `observability_project_slug` sends none even inside an instrumented host (#66)
170
+ - Send `metadata` to the inference gateway as `x-litellm-spend-logs-metadata`, so gateway spend can be attributed to the work that caused it. Sent whenever metadata is set, observed or not, and omitted above 4KB to stay within proxy header limits (#66)
171
+ - `session_id` and `tags` parameters, forwarded to the observability platform when set (#66)
172
+
173
+ ## [2.2.0] - 2026-08-13
174
+
175
+ ### Added
176
+
177
+ - `prompt_fallback_name`, `prompt_fallback_label` and `prompt_fallback_version` parameters: name a fallback prompt to use when the requested `prompt_name` is missing or its lookup fails, so a feature can ship a prompt variant without one existing for every caller. The fallback is looked up at its own label and version, inheriting neither from the requested prompt (#61)
178
+
179
+ ### Changed
180
+
181
+ - The observed chat, audio-transcription, image and text-to-speech handlers resolve prompts through a shared `Observability::PromptResolver` rather than each fetching from the prompt store (#61)
182
+
183
+ ## [2.1.0] - 2026-07-31
184
+
185
+ ### Added
186
+
187
+ - Tag inference requests with custom NIP headers (#54)
188
+ - Resolve an agent thread's graph from its assistant, which the agent server requires before a new thread's state can be seeded. Adds one assistant lookup per client, and raises `ThreadInitializationError` when the assistant cannot be fetched or has no graph (#58)
189
+
190
+ ### Changed
191
+
192
+ - Remove malformed wait header (#54)
193
+
194
+ ### Fixed
195
+
196
+ - Seed new agent threads through the thread state endpoint, so the agent's context includes the messages sent before the run (#58)
197
+ - Discard a newly created agent thread when seeding its state fails, so a retry seeds it again instead of silently running without the earlier messages (#58)
198
+
199
+ ## [2.0.0] - 2026-05-20
200
+
201
+ ### Added
202
+
203
+ - Support text-to-speech (#45)
204
+
205
+ ### Changed
206
+
207
+ - Rely on stricter 'type' key in model_config to infer model type
208
+
209
+ ## [1.0.1] - 2026-05-08
210
+
211
+ ### Changed
212
+ - Correct Portal data (#20)
213
+ - Make required HTTParty version less strict (#24)
214
+
215
+ ## [1.0.0] - 2026-04-15
216
+
217
+ ### Added
218
+ - Support audio transcription (#6)
219
+ - Enable agent server to manage human-in-the-loop threads (#6)
220
+
221
+ ### Changed
222
+ - Refactor client into multiple handlers, each specific to a type of inference (#6)
223
+ - Require Ruby 3.3 or later (#10)
224
+ - Upgrade langfuse-rb to 0.7.0. (#12)
225
+
226
+ [Unreleased]: https://github.com/powerhome/nitro-intelligence.rb/compare/v4.1.0-nitro_intelligence...HEAD
227
+ [4.1.0]: https://github.com/powerhome/nitro-intelligence.rb/compare/v4.0.0-nitro_intelligence...v4.1.0-nitro_intelligence
228
+ [4.0.0]: https://github.com/powerhome/nitro-intelligence.rb/compare/v3.1.0-nitro_intelligence...v4.0.0-nitro_intelligence
229
+ [3.1.0]: https://github.com/powerhome/nitro-intelligence.rb/compare/v3.0.2-nitro_intelligence...v3.1.0-nitro_intelligence
230
+ [3.0.2]: https://github.com/powerhome/nitro-intelligence.rb/compare/v3.0.0-nitro_intelligence...v3.0.2-nitro_intelligence
231
+ [3.0.1]: https://github.com/powerhome/nitro-intelligence.rb/compare/v3.0.0-nitro_intelligence...v3.0.1-nitro_intelligence
232
+ [3.0.0]: https://github.com/powerhome/nitro-intelligence.rb/compare/v2.8.0-nitro_intelligence...v3.0.0-nitro_intelligence
233
+ [2.8.0]: https://github.com/powerhome/nitro-intelligence.rb/compare/v2.7.0-nitro_intelligence...v2.8.0-nitro_intelligence
234
+ [2.7.0]: https://github.com/powerhome/nitro-intelligence.rb/compare/v2.6.0-nitro_intelligence...v2.7.0-nitro_intelligence
235
+ [2.6.0]: https://github.com/powerhome/nitro-intelligence.rb/compare/v2.5.0-nitro_intelligence...v2.6.0-nitro_intelligence
236
+ [2.5.0]: https://github.com/powerhome/nitro-intelligence.rb/compare/v2.4.0-nitro_intelligence...v2.5.0-nitro_intelligence
237
+ [2.4.0]: https://github.com/powerhome/nitro-intelligence.rb/compare/v2.3.0-nitro_intelligence...v2.4.0-nitro_intelligence
238
+ [2.3.0]: https://github.com/powerhome/nitro-intelligence.rb/compare/v2.2.0-nitro_intelligence...v2.3.0-nitro_intelligence
239
+ [2.2.0]: https://github.com/powerhome/nitro-intelligence.rb/compare/v2.1.0-nitro_intelligence...v2.2.0-nitro_intelligence
240
+ [2.1.0]: https://github.com/powerhome/nitro-intelligence.rb/compare/v2.0.0-nitro_intelligence...v2.1.0-nitro_intelligence
241
+ [2.0.0]: https://github.com/powerhome/nitro-intelligence.rb/compare/v1.0.1-nitro_intelligence...v2.0.0-nitro_intelligence
242
+ [1.0.1]: https://github.com/powerhome/nitro-intelligence.rb/compare/v1.0.0-nitro_intelligence...v1.0.1-nitro_intelligence
243
+ [1.0.0]: https://github.com/powerhome/nitro-intelligence.rb/compare/v0.0.1-nitro_intelligence...v1.0.0-nitro_intelligence
data/docs/README.md CHANGED
@@ -4,6 +4,10 @@ The entrypoint to everything AI.
4
4
 
5
5
  This component aims to consolidate and standardize AI features implemented in the host application.
6
6
 
7
+ ## Public API
8
+
9
+ The gem's public API is what its API reference documents, and follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html): it changes incompatibly only in a major release. The reference is generated from the source with [YARD](https://yardoc.org): run `bundle exec yard doc` and open `doc/api/index.html`, or browse it for any released version on [RubyDoc.info](https://www.rubydoc.info/gems/nitro_intelligence). Anything marked `@api private` in the source is left out of it, may change in any release, and is not for callers. This README and [ASSISTANTS.md](https://github.com/powerhome/nitro-intelligence.rb/blob/main/docs/ASSISTANTS.md) are guides to using the gem, not a list of everything public.
10
+
7
11
  ## Configuration
8
12
 
9
13
  `NitroIntelligence` is configured via an initializer in the host application. All external dependencies
@@ -19,6 +23,8 @@ NitroIntelligence.configure do |config|
19
23
  # Inference (LLM) settings
20
24
  config.inference_api_key = "..." # API key for the inference service
21
25
  config.inference_base_url = "https://..." # Base URL for the inference service (optional; defaults to the inference gateway)
26
+ config.inference_max_retries = 0 # Retries the SDK makes itself (optional; defaults to 0 - see "Retries and Timeouts")
27
+ config.inference_timeout = 600 # Request timeout in seconds (optional; defaults to openai-ruby's)
22
28
 
23
29
  # Observability (Langfuse) settings
24
30
  config.observability_base_url = "https://..." # Base URL for the observability service (optional; defaults to Cerebro)
@@ -28,6 +34,7 @@ NitroIntelligence.configure do |config|
28
34
  "id" => "project-id",
29
35
  "public_key" => "pk-...",
30
36
  "secret_key" => "sk-...",
37
+ # "mask_content" => true, # Optional: keep prompts and responses out of Cerebro and the gateway logs, see "Masking Content"
31
38
  },
32
39
  ]
33
40
 
@@ -46,6 +53,7 @@ NitroIntelligence.configure do |config|
46
53
  "default_text_model" => "Qwen/Qwen3.6-35B-A3B",
47
54
  "default_image_model" => "google/gemini-3.1-flash-image",
48
55
  "default_text_to_speech_model" => "openai/gpt-4o-mini-tts",
56
+ "default_decision_model" => "typesafe/jev-1.14",
49
57
  "models" => [
50
58
  {
51
59
  "name" => "Qwen/Qwen3.6-35B-A3B",
@@ -69,6 +77,10 @@ NitroIntelligence.configure do |config|
69
77
  "default_response_format" => "mp3",
70
78
  "voices" => ["echo", "nova", "marin", "cedar"],
71
79
  "response_formats" => ["mp3", "wav"]
80
+ },
81
+ {
82
+ "name" => "typesafe/jev-1.14",
83
+ "type" => "decision"
72
84
  }
73
85
  ]
74
86
  }
@@ -81,13 +93,17 @@ end
81
93
  | ------------------------ | ------------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
82
94
  | `logger` | `Logger` | `Logger.new($stdout)` | Logger used for diagnostic output |
83
95
  | `environment` | `String` | `"test"` | Runtime environment name |
96
+ | `current_revision` | `String` | `""` | The host application's revision, such as its commit, recorded on each trace in Cerebro as its `release` |
84
97
  | `cache_provider` | cache store | `NullCache` | ActiveSupport-compatible cache store |
85
98
  | `inference_api_key` | `String` | `""` | API key for the LLM inference service |
86
99
  | `inference_base_url` | `String` | `"https://inference.powerhome.ai"` | Base URL for the LLM inference service. Defaults to the shared inference gateway, so only a host talking to a different one needs to set it |
100
+ | `inference_max_retries` | `Integer` | `0` | Retries the SDK makes itself after a failed request. Off by default — see [Retries and Timeouts](#retries-and-timeouts) |
101
+ | `inference_timeout` | `Numeric` | `nil` | Request timeout in seconds. `nil` keeps openai-ruby's default of 600 |
87
102
  | `observability_base_url` | `String` | `"https://cerebro.powerhome.ai"` | Base URL for the Langfuse observability service. Defaults to Cerebro, so only a host talking to a different one needs to set it |
88
- | `observability_projects` | `Array<Hash>` | `[]` | Langfuse project credentials (slug, id, public_key, secret_key) |
103
+ | `observability_projects` | `Array<Hash>` | `[]` | Langfuse project credentials (slug, id, public_key, secret_key). Add `"mask_content" => true` to keep a project's prompts and responses out of Cerebro and the gateway logs, see [Masking Content](#masking-content) |
104
+ | `observability_user_id` | `String` | `""` | User ID recorded on observed traces when a request passes no `user_id` parameter — see [Users](#users) |
89
105
  | `assistants_config` | `Hash` | `{}` | Assistants to make addressable by key. `base_url` (String, default: `"https://assistants.powerhome.ai"`) and `user_id` (String, default: `"default-user"`) are shared by every entry; `definitions` (Hash) holds one entry per assistant, keyed by what it is looked up with, each able to override a shared value. Without `definitions` the hash is read as credentials for a single `Assistants.new`, which takes the same two defaults — see [Assistants](#assistants) |
90
- | `model_config` | `Hash` | `{}` | Model defaults and per-model settings. Top-level keys: `default_text_model`, `default_audio_transcription_model`, `default_image_model`, `default_text_to_speech_model`, and `models` (array of per-model hashes keyed by `name` and `type`, with type-specific options like `aspect_ratios`/`resolutions` for images or `voices`/`response_formats` for TTS) |
106
+ | `model_config` | `Hash` | `{}` | Model defaults and per-model settings. Top-level keys: `default_text_model`, `default_audio_transcription_model`, `default_image_model`, `default_text_to_speech_model`, `default_decision_model`, and `models` (array of per-model hashes keyed by `name` and `type`, with type-specific options like `aspect_ratios`/`resolutions` for images or `voices`/`response_formats` for TTS) |
91
107
 
92
108
  ## Basic Usage
93
109
 
@@ -121,10 +137,27 @@ client.chat(parameters: { model: "Qwen/Qwen3.8-27B", max_tokens: 1000, temperatu
121
137
 
122
138
  For a full list of supported parameters, see the [API reference here](https://developers.openai.com/api/reference/resources/completions/methods/create).
123
139
 
140
+ #### Retries and Timeouts
141
+
142
+ The client does not retry failed requests by default. openai-ruby on its own retries a `429`, `408`, `409` or `5xx` twice within about two seconds, but none of those is worth repeating that quickly from here: the inference gateway has already failed over between providers before it answers, and a rate limit or exhausted budget it passes on will not clear within a second. Each retry only adds load, and for billable work such as image generation a retry after a timeout can pay twice. A caller that wants retries decides when, typically in a job with backoff, and can tell a rate limit from an exhausted budget first (see the gateway's [error guidance](https://github.com/powerhome/nitro-intelligence/blob/main/docs/inference-gateway.md)).
143
+
144
+ To retry anyway, or to change the timeout, set them for every client in the configuration (`inference_max_retries`, `inference_timeout`), for one client with the same names, or for one request with openai-ruby's `request_options`. The narrower setting wins.
145
+
146
+ ```ruby
147
+ # For one client
148
+ client = NitroIntelligence::Client.new(inference_max_retries: 2, inference_timeout: 30)
149
+
150
+ # For one request, with any method
151
+ client.chat(message: "Why is the sky blue?", parameters: { request_options: { max_retries: 2, timeout: 30 } })
152
+ client.generate_image(message: "A bear installing a window", parameters: { request_options: { timeout: 120 } })
153
+ ```
154
+
124
155
  ### Audio Transcription
125
156
 
126
157
  Nitro Intelligence can be used to transcribe audio from a file into text.
127
158
 
159
+ With observability enabled, the source audio is attached to the generation observation as input.
160
+
128
161
  Basic examples of usage:
129
162
 
130
163
  ```ruby
@@ -155,6 +188,8 @@ puts result
155
188
 
156
189
  Nitro Intelligence can be used to create spoken-word audio files from text.
157
190
 
191
+ With observability enabled, generated audio is labeled as output and attached to the generation observation.
192
+
158
193
  Basic example of usage:
159
194
 
160
195
  ```ruby
@@ -185,6 +220,48 @@ File.binwrite('tts.mp3', tts.string)
185
220
 
186
221
  For a full list of supported parameters, see the [API reference here](https://developers.openai.com/api/reference/resources/audio/subresources/speech/methods/create ). Note that `voice` and `response_format` are further constrained to the `voices` and `response_formats` listed for the chosen model in `config.model_config`.
187
222
 
223
+ ### Decisions
224
+
225
+ A decision model answers questions about some content with a probability for each answer you allow, rather than generated text. `decide` sends the content as `state` (a string, hash or array) and a hash of named questions to the gateway's System One route, `POST /v1/systemone`. Each question is `noul` (yes or no), `choice` (one of the options in `criteria`) or `score` (one of the ordered levels in `criteria`, the first being 0). See the [Inference Gateway's decision model docs](https://github.com/powerhome/nitro-intelligence/blob/main/docs/inference-gateway.md#decision-models) for when to use one and how far to trust its probabilities.
226
+
227
+ ```ruby
228
+ client = NitroIntelligence::Client.new
229
+ decision = client.decide(
230
+ state: { subject: "Duplicate charge", message: "I was charged twice. Please help." },
231
+ questions: {
232
+ billing: { type: "noul", instructions: "Is this message about billing?" },
233
+ team: {
234
+ type: "choice",
235
+ instructions: "Which team should handle this message?",
236
+ criteria: {
237
+ payments: "Charges, refunds and invoices",
238
+ scheduling: "Appointments and visits",
239
+ other: "Anything else, or not enough information to tell",
240
+ },
241
+ },
242
+ urgency: {
243
+ type: "score",
244
+ instructions: "How urgent is this message?",
245
+ criteria: ["Can wait", "Needs attention this week", "Needs attention today"],
246
+ },
247
+ }
248
+ )
249
+
250
+ decision.answers[:billing].noul # => 0.98, the probability of yes
251
+ decision.answers[:team].choice # => "payments"
252
+ decision.answers[:team].confidence # => 0.91
253
+ decision.answers[:team].probabilities # => {"payments" => 0.95, "scheduling" => 0.01, "other" => 0.04}
254
+ decision.answers[:urgency].score # => 1.7, the expected level, which may fall between levels
255
+ decision.answers[:urgency].legend # => {0 => "Can wait", 1 => "Needs attention this week", 2 => "Needs attention today"}
256
+ decision.answers[:urgency].probabilities # => {0 => 0.1, 1 => 0.1, 2 => 0.8}
257
+ ```
258
+
259
+ `answers` is keyed by question name and accepts strings or symbols. Each answer is a `NitroIntelligence::Decision::NoulAnswer`, `ChoiceAnswer` or `ScoreAnswer`; an answer of any other type is returned as the gateway sent it. `decision.usage` holds the token counts, also by string or symbol, and `decision.last_response` the HTTP response, including the gateway's headers.
260
+
261
+ The model defaults to `default_decision_model` and must be listed in `config.model_config` with the type `decision`. `parameters` takes `model`, `assets` (images that `state` and the questions refer to, see the [System One docs](https://docs.typesafe.ai/api/system-one.md); the gateway refuses assets for a model it reaches through OpenRouter, which includes `typesafe/jev-1.14`), `metadata`, `request_options` and, with observability, `trace_name`, `trace_seed`, `user_id`, `session_id` and `tags`, described under [Observability](#observability). Decisions take no managed prompt: `prompt_name` and `prompt_variables` are ignored, so put the instructions in `questions`. With observability enabled, each call is recorded in Cerebro as a generation with the state and questions as input, the answers as output, and the gateway's cost.
262
+
263
+ The client does not limit the request. To keep a feature portable to models NIP hosts itself, stay within the gateway's [portable subset](https://github.com/powerhome/nitro-intelligence/blob/main/docs/inference-gateway.md#stay-within-what-every-backend-supports): up to 64 questions, 26 options per `choice` question and 10 levels per `score` question, with text `state`.
264
+
188
265
  ### Image Editing and Generation
189
266
 
190
267
  Nitro Intelligence can be used for image editing and generation
@@ -200,6 +277,10 @@ result = client.generate_image(message: "Create an image of a bear installing a
200
277
 
201
278
  `result` is a `NitroIntelligence::ImageGeneration` object, and the generated image can be accessed via `result.generated_image` which returns a `NitroIntelligence::Image`.
202
279
 
280
+ With observability enabled, `result.trace_id` and `result.observation_id` identify the trace and generation observation. Both are `nil` without observability.
281
+
282
+ A `NitroIntelligence::Image` carries the file in `byte_string`, its `file_extension` (such as `"png"`) and `mime_type` (such as `"image/png"`), and its `width` and `height` in pixels.
283
+
203
284
  You may write the file to disk:
204
285
 
205
286
  ```ruby
@@ -228,6 +309,8 @@ siding = File.binread("./siding.png")
228
309
  result = client.generate_image(target_image: house, reference_images: [siding], parameters: {prompt_name: "Sample Prompt Name"})
229
310
  ```
230
311
 
312
+ With observability enabled, the input and generated images are attached to the generation observation. Cerebro's Formatted observation preview displays them together, including in the tracing list's preview panel.
313
+
231
314
  #### Image Configuration
232
315
 
233
316
  You can specify parameters such as model to use, aspect ratio and resolution via the `parameters` key:
@@ -336,22 +419,38 @@ client.chat(
336
419
  )
337
420
  ```
338
421
 
339
- ### Grouping Traces: Sessions and Tags
422
+ ### Grouping Traces: Users, Sessions and Tags
340
423
 
341
- `session_id` groups related traces in the observability platform, and `tags` label
342
- them for filtering. Both are optional and are only sent when you set them.
424
+ `user_id` attributes traces to a user of your application, `session_id` groups related traces, and `tags` label them for filtering. All three are optional. `session_id` and `tags` are sent only when you set them.
343
425
 
344
426
  ```ruby
345
427
  client = NitroIntelligence::Client.new(observability_project_slug: "fake-feature-project")
346
428
  client.chat(
347
429
  message: "why did this deploy fail?",
348
430
  parameters: {
431
+ user_id: current_user.id.to_s,
349
432
  session_id: "deploy-9f2c1ab",
350
433
  tags: ["deployment-failure-analyzer"],
351
434
  }
352
435
  )
353
436
  ```
354
437
 
438
+ #### Users
439
+
440
+ A trace that carries a `user_id` shows up under that user in Cerebro's [Users](https://langfuse.com/docs/observability/features/users) view, which lists each user with their trace count, token usage and cost, and links through to their traces. The trace list can also be filtered by user.
441
+
442
+ When a request passes no `user_id`, the `observability_user_id` configuration key is used instead, so a host can attribute all of its traces to one identity (the application itself, say) and override it per request for the traces that belong to a person:
443
+
444
+ ```ruby
445
+ NitroIntelligence.configure do |config|
446
+ config.observability_user_id = "nitro-web"
447
+ end
448
+ ```
449
+
450
+ Use an identifier that is stable and not itself personal data — a database ID rather than an email address or name — since it is stored on every trace and shown in Cerebro. It must be a String of at most 200 characters; langfuse-rb drops any other value with a warning, so convert numeric IDs with `to_s`.
451
+
452
+ `user_id` is recorded only in Cerebro. It is not sent to the inference gateway, so to attribute gateway spend to a user as well, also put the identifier in `metadata` (see below).
453
+
355
454
  ### Correlating With Application Logs and the Inference Gateway
356
455
 
357
456
  Every observed request is correlated across three systems automatically:
@@ -369,6 +468,8 @@ Every observed request is correlated across three systems automatically:
369
468
  * **Inference gateway → observability platform.** When a request fails, the
370
469
  gateway's own request identifier is read from the error response and recorded on
371
470
  the observation as `litellm_call_id` metadata.
471
+ * **Application users → observability platform.** Pass `user_id` to see a
472
+ user's traces, usage and cost together in Cerebro — see [Users](#users).
372
473
  * **Application logs → observability platform.** Put whatever your logs are keyed
373
474
  by into `metadata` — it lands on the trace *and* in the gateway's spend logs.
374
475
 
@@ -402,6 +503,24 @@ handle failures. Image generation is the one exception to input recording: its
402
503
  input carries base64 payloads that are replaced with media references only on
403
504
  success, so it records no input up front.
404
505
 
506
+ ### Masking Content
507
+
508
+ A project can opt in to recording what happened without recording what was said. Set `"mask_content" => true` on its entry in `observability_projects`; projects that do not set it are unchanged.
509
+
510
+ For a masked project, every observed request (chat, image generation, audio transcription, text-to-speech and decisions) is still traced, with its model, usage, cost, prompt link, tags, user, session, metadata and gateway call ID, but:
511
+
512
+ * The input and output of the generation and of the trace are recorded as `[masked]`. The whole value is replaced, so reasoning, tool calls and anything else in a response are masked however the provider shaped it.
513
+ * A failed request records the error class (for example `OpenAI::Errors::BadRequestError`) as its status message, without the error's own message, which can quote the request or the response.
514
+ * No media is uploaded. Generated images, generated speech and transcribed audio never reach Cerebro. Callers still receive them as usual.
515
+ * The request carries `x-litellm-enable-message-redaction`, so the inference gateway records `redacted-by-litellm` in place of the messages and the response in its own logs. It still records the model, usage, cost and spend-logs metadata, so gateway spend stays attributable.
516
+
517
+ Masking is not applied to what you choose to put on a trace yourself. **Do not put prompt or response content in `metadata`, `tags`, `session_id`, `user_id` or trace names**; metadata is the place for the identifiers that join a trace to your application's logs, and none of it is masked.
518
+
519
+ Two things this does not cover:
520
+
521
+ * **The gateway's redaction is by field, and misses some.** It replaces the fields it knows about, such as `content` and `reasoning_content`, rather than the whole response, so a field a provider adds beyond those is still logged. Reasoning that a provider returns under `provider_specific_fields` is one such field today; see [powerhome/nitro-intelligence#704](https://github.com/powerhome/nitro-intelligence/issues/704).
522
+ * **Prompts you fetch from Cerebro are unaffected.** Masking applies to traces, not to prompt management.
523
+
405
524
  ### Scoring
406
525
 
407
526
  You can use `NitroIntelligence::Reporter` to evaluate existing traces. Calling `NitroIntelligence::Reporter#score` lets you attach metrics to a trace in the observability platform.
@@ -434,6 +553,47 @@ reporter.score(name: "precision", value: "Partially Correct", trace_id:, data_ty
434
553
 
435
554
  Numeric data is helpful for scores that happen on a sliding scale, like success rate or percent similarity. Boolean data is helpful to convey something that either passes or fails. Categorical data is helpful for scores that have pre-defined categories, like "Correct", "Partially Correct", and "Incorrect".
436
555
 
556
+ ### Dataset Items
557
+
558
+ `NitroIntelligence::Reporter#create_dataset_item` adds an item to one of the project's datasets, to evaluate prompts against later. It takes the item as Langfuse's [`POST /api/public/dataset-items`](https://api.reference.langfuse.com) does, and raises `NitroIntelligence::Reporter::DatasetItemError`, carrying the response's status and body, when Cerebro does not store it.
559
+
560
+ ```ruby
561
+ reporter = NitroIntelligence::Reporter.new(observability_project_slug: "fake-feature-project")
562
+ reporter.create_dataset_item({
563
+ datasetName: "call-categorization",
564
+ input: { transcript: },
565
+ expectedOutput: "Rescheduled",
566
+ sourceTraceId: trace_id,
567
+ })
568
+ ```
569
+
570
+ ### Reaching Cerebro Directly
571
+
572
+ For what this gem does not wrap, such as a Langfuse API it has no method for, or a link to a trace, a project's details are available from its configuration:
573
+
574
+ ```ruby
575
+ project = NitroIntelligence.project_client_registry.fetch("fake-feature-project").project
576
+ project.id # the project's id in Cerebro, as in its URLs
577
+ project.auth_token # the Base64 credential for Cerebro's public API, without the "Basic " scheme
578
+
579
+ trace_url = "#{NitroIntelligence.config.observability_base_url}/project/#{project.id}/traces/#{trace_id}"
580
+ request["Authorization"] = "Basic #{project.auth_token}"
581
+ ```
582
+
583
+ `NitroIntelligence.project_client_registry.fetch(slug)` builds each project's client once per process, and raises `NitroIntelligence::Observability::Project::NotFoundError` for a slug `observability_projects` does not configure. `NitroIntelligence::Observability::Project.find_by_slug(slug:)` reads the same configuration without building a client, and returns `nil` instead. The gem never raises `NitroIntelligence::Observability::ProjectClient::NotFoundError`; it is public only because applications raise it themselves, and is to be retired ([#182](https://github.com/powerhome/nitro-intelligence.rb/issues/182)). Rescue `Project::NotFoundError` for a project that is not configured.
584
+
585
+ Traces and scores are sent to Cerebro in the background. Fetch each project in an initializer to build its client at boot, and shut them all down as the process exits so that the last ones are sent:
586
+
587
+ ```ruby
588
+ NitroIntelligence.config.observability_projects.each do |project|
589
+ NitroIntelligence.project_client_registry.fetch(project["slug"])
590
+ end
591
+
592
+ at_exit { NitroIntelligence.project_client_registry.shutdown_all }
593
+ ```
594
+
595
+ A server that loads the application and then forks its workers, such as Puma with `preload_app!` or Unicorn, runs initializers before the fork. Each worker then inherits the project clients but not the background thread that sends their scores, so a worker's scores wait until it shuts down, and are lost if it is killed. In such a server, fetch the projects in each worker after it forks instead, from Puma's `on_worker_boot` or Unicorn's `after_fork`, or leave each to be fetched on first use.
596
+
437
597
  ### Prompts
438
598
 
439
599
  #### Message Shape
@@ -560,11 +720,25 @@ client.chat(
560
720
  )
561
721
  ```
562
722
 
723
+ #### Prompt Errors
724
+
725
+ A request naming a prompt raises when it cannot use one:
726
+
727
+ | Error | Raised when |
728
+ | --- | --- |
729
+ | `NitroIntelligence::Observability::PromptStore::ObservabilityPromptNotFoundError` | The prompt does not exist, nor its fallback when `prompt_fallback_name` names one |
730
+ | `NitroIntelligence::Observability::PromptStore::ObservabilityPromptError` | Cerebro fails to return the prompt; the message is its response body |
731
+ | `NitroIntelligence::Client::Handlers::Observed::ChatHandler::ObservedChatPromptError` | `#chat` has no user message for the model to answer (see [Message Shape](#message-shape)) |
732
+ | `NitroIntelligence::Client::Handlers::Observed::AudioTranscriptionHandler::ObservedAudioTranscriptionPromptError` | `#transcribe_audio` names a prompt that is not a text prompt |
733
+ | `NitroIntelligence::Client::Handlers::Observed::TextToSpeechHandler::ObservedTextToSpeechPromptError` | `#text_to_speech` names a prompt that is not a text prompt |
734
+
735
+ On an observed client whose project does not mask content, pass an open `File` as `audio_file` to `#transcribe_audio`. If `audio_file` has no path, as with a `StringIO`, the upload to Cerebro raises `NitroIntelligence::Audio::AudioFileFormatError`. By then, the gateway has already transcribed the audio and billed the request, but your app never receives the transcript. A masked project skips the upload, so the same input works there. Every method raises `ArgumentError` for a model, or a model setting such as a voice or aspect ratio, that `model_config` does not list.
736
+
563
737
  ## Nitro Intelligence Assistants
564
738
 
565
- `NitroIntelligence::Assistants` is Nitro Intelligence's lightweight SDK for working with hosted agent threads, runs, and human review flows. It is mainly used to initialize conversation threads, trigger agent runs, inspect agent tool calls pending human approval, and resume interrupted threads after human reviews.
739
+ `NitroIntelligence::Assistants` is Nitro Intelligence's lightweight SDK for working with hosted agent threads, runs, and human review flows. It is mainly used to initialize conversation threads, trigger agent runs, inspect what a paused run is waiting on (tool calls or a reply held for review), and resume paused threads after human reviews.
566
740
 
567
- For the full guide, see [ASSISTANTS.md](ASSISTANTS.md). For the service this SDK talks to, see the [Nitro Intelligence Assistants documentation](https://portal.powerapp.cloud/docs/default/system/nip-assistants).
741
+ For the full guide, see [ASSISTANTS.md](https://github.com/powerhome/nitro-intelligence.rb/blob/main/docs/ASSISTANTS.md). For the service this SDK talks to, see the [Nitro Intelligence Assistants documentation](https://portal.powerapp.cloud/docs/default/system/nip-assistants).
568
742
 
569
743
  ## Assistants
570
744
 
@@ -573,14 +747,18 @@ An assistant is addressed by the key it is configured under:
573
747
  ```ruby
574
748
  assistant = NitroIntelligence.assistants["candidate-concierge"]
575
749
 
576
- assistant.await_run(thread_id: thread.id, messages: messages)
750
+ result = assistant.run(thread_id: thread.id, messages: messages)
751
+ result.reply # => nil while the reply is held for review
752
+ assistant.review(thread_id: thread.id, reviewer_id: user.email,
753
+ interrupt_id: result.review.interrupt_id, reply: { "action" => "approve" })
577
754
  assistant.thread_state(thread_id: thread.id)
578
755
  ```
579
756
 
580
- `await_run` and `review_tool_calls` supply the assistant's own id, so a caller never passes
581
- it -- passing `assistant_id:` to either raises `ArgumentError` rather than quietly sending the
582
- call to a different assistant, which is what a migrated caller carrying its old argument would
583
- otherwise do. Everything else is thread-scoped and delegates to the client untouched. Reach
757
+ `run`, `review`, and the deprecated `await_run` and `review_tool_calls` (see
758
+ [Upgrading to 4.0](https://github.com/powerhome/nitro-intelligence.rb/blob/main/docs/ASSISTANTS.md#upgrading-to-40)), supply the assistant's own id, so a
759
+ caller never passes it -- passing `assistant_id:` to any of them raises `ArgumentError` rather
760
+ than quietly sending the call to a different assistant, which is what a migrated caller
761
+ carrying its old argument would otherwise do. Everything else is thread-scoped and delegates to the client untouched. Reach
584
762
  the client directly with `assistant.client` if you need it.
585
763
 
586
764
  ### Configuration