crystil 0.5.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 (46) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +77 -0
  3. data/LICENSE +21 -0
  4. data/README.md +112 -0
  5. data/lib/crystil/api/base.rb +104 -0
  6. data/lib/crystil/api/invocation.rb +59 -0
  7. data/lib/crystil/api/sentinel.rb +11 -0
  8. data/lib/crystil/api/workflow.rb +34 -0
  9. data/lib/crystil/api/workflows.rb +39 -0
  10. data/lib/crystil/attribution.rb +66 -0
  11. data/lib/crystil/client.rb +95 -0
  12. data/lib/crystil/collector.rb +88 -0
  13. data/lib/crystil/config.rb +79 -0
  14. data/lib/crystil/errors.rb +35 -0
  15. data/lib/crystil/sentinel.rb +100 -0
  16. data/lib/crystil/version.rb +5 -0
  17. data/lib/crystil/wrappers/anthropic.rb +112 -0
  18. data/lib/crystil/wrappers/base.rb +259 -0
  19. data/lib/crystil/wrappers/constants.rb +12 -0
  20. data/lib/crystil/wrappers/geminiai.rb +173 -0
  21. data/lib/crystil/wrappers/google.rb +133 -0
  22. data/lib/crystil/wrappers/groq.rb +295 -0
  23. data/lib/crystil/wrappers/openai.rb +249 -0
  24. data/lib/crystil/wrappers/ruby_llm.rb +170 -0
  25. data/lib/crystil.rb +52 -0
  26. data/sig/crystil/api/base.rbs +24 -0
  27. data/sig/crystil/api/invocation.rbs +16 -0
  28. data/sig/crystil/api/sentinel.rbs +9 -0
  29. data/sig/crystil/api/workflow.rbs +15 -0
  30. data/sig/crystil/api/workflows.rbs +16 -0
  31. data/sig/crystil/attribution.rbs +17 -0
  32. data/sig/crystil/client.rbs +29 -0
  33. data/sig/crystil/collector.rbs +20 -0
  34. data/sig/crystil/config.rbs +33 -0
  35. data/sig/crystil/errors.rbs +28 -0
  36. data/sig/crystil/sentinel.rbs +17 -0
  37. data/sig/crystil/version.rbs +5 -0
  38. data/sig/crystil/wrappers/anthropic.rbs +18 -0
  39. data/sig/crystil/wrappers/base.rbs +21 -0
  40. data/sig/crystil/wrappers/constants.rbs +10 -0
  41. data/sig/crystil/wrappers/geminiai.rbs +19 -0
  42. data/sig/crystil/wrappers/google.rbs +19 -0
  43. data/sig/crystil/wrappers/groq.rbs +22 -0
  44. data/sig/crystil/wrappers/openai.rbs +19 -0
  45. data/sig/crystil/wrappers/ruby_llm.rbs +21 -0
  46. metadata +100 -0
@@ -0,0 +1,249 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "constants"
4
+
5
+ module Crystil
6
+ module Wrappers
7
+ # Wrapper for OpenAI Ruby client (ruby-openai gem)
8
+ class OpenAI
9
+ def initialize(config, collector, sentinel = nil)
10
+ @config = config
11
+ @collector = collector
12
+ @sentinel = sentinel
13
+ end
14
+
15
+ def register(client)
16
+ validate_client!(client)
17
+
18
+ # Prevent double registration
19
+ return client if client.instance_variable_defined?(:@crystil_registered)
20
+
21
+ # Store references in client instance
22
+ client.instance_variable_set(:@crystil_config, @config)
23
+ client.instance_variable_set(:@crystil_collector, @collector)
24
+ client.instance_variable_set(:@crystil_sentinel, @sentinel)
25
+ client.instance_variable_set(:@crystil_registered, true)
26
+
27
+ # Wrap the chat method
28
+ wrap_chat_method(client)
29
+
30
+ # Wrap responses.create if the client supports the Responses API (ruby-openai >= 8.0)
31
+ wrap_responses_method(client) if client.respond_to?(:responses)
32
+
33
+ client
34
+ end
35
+
36
+ private
37
+
38
+ def wrap_responses_method(client)
39
+ responses_obj = client.responses
40
+
41
+ # Copy Crystil references onto the responses sub-object so Base helpers can access them
42
+ responses_obj.instance_variable_set(:@crystil_config, client.instance_variable_get(:@crystil_config))
43
+ responses_obj.instance_variable_set(:@crystil_collector, client.instance_variable_get(:@crystil_collector))
44
+ responses_obj.instance_variable_set(:@crystil_sentinel, client.instance_variable_get(:@crystil_sentinel))
45
+
46
+ responses_obj.singleton_class.class_eval do
47
+ include Base
48
+
49
+ alias_method :original_create, :create
50
+
51
+ define_method(:create) do |parameters: {}|
52
+ start_time = Time.now
53
+ version = defined?(::OpenAI::VERSION) ? ::OpenAI::VERSION : nil
54
+
55
+ sentinel = instance_variable_get(:@crystil_sentinel)
56
+ sentinel&.raise_if_irrelevant!(
57
+ title: OPENAI_CLIENT_TITLE,
58
+ request: parameters,
59
+ version: version
60
+ )
61
+
62
+ # Dup before mutating so we don't replace the caller's :stream key with our
63
+ # internal wrapping proc — the caller's hash should be unchanged after the call.
64
+ parameters = parameters.dup
65
+
66
+ streaming = parameters[:stream].respond_to?(:call)
67
+
68
+ if streaming
69
+ accumulated_events = []
70
+ user_callback = parameters[:stream]
71
+ # Determine how many arguments to forward to the user's callback.
72
+ # .arity returns negative values for methods with optional/splat params
73
+ # (e.g. def call(*args) => -1, def call(a, b=nil) => -2), so .abs gives
74
+ # us the minimum required argument count. For fixed-arity procs/lambdas
75
+ # this is exact. Note: a variadic callable (def call(*args)) has arity -1,
76
+ # so .abs yields 1 — the event_type argument will be silently dropped for
77
+ # that signature. Use def call(event, event_type = nil) to receive both.
78
+ user_callback_arity =
79
+ case user_callback
80
+ when Proc
81
+ user_callback.arity.abs
82
+ else
83
+ user_callback.method(:call).arity.abs
84
+ end
85
+
86
+ parameters[:stream] = proc do |chunk, event_type|
87
+ accumulated_events << chunk if chunk.is_a?(Hash)
88
+ user_callback.call(*[chunk, event_type].first(user_callback_arity))
89
+ end
90
+ end
91
+
92
+ response = original_create(parameters: parameters)
93
+
94
+ # Default to the returned response; for streaming, replace it with the
95
+ # full response from the terminal response.completed event.
96
+ final_response = response
97
+
98
+ if streaming
99
+ error_event = accumulated_events.find { |e| e["type"] == "error" }
100
+ if error_event
101
+ final_response = {
102
+ "status" => "failed",
103
+ "error" => error_event["error"]
104
+ }
105
+ end
106
+
107
+ # Extract the full response from the terminal response.completed event.
108
+ # A missing terminal event means the stream was interrupted.
109
+ completed = accumulated_events.find { |e| e["type"] == "response.completed" }
110
+ unless completed || error_event
111
+ raise StreamError,
112
+ "Responses API stream ended without response.completed event"
113
+ end
114
+
115
+ final_response = completed["response"] if completed && !error_event
116
+ end
117
+
118
+ # Treat only an explicit failed response status as failed analytics.
119
+ # Other non-exceptional statuses are still recorded as succeeded.
120
+ response_status = final_response.is_a?(Hash) ? final_response["status"] : nil
121
+ analytics_status = response_status == "failed" ? "failed" : "succeeded"
122
+ analytics_exception =
123
+ if analytics_status == "failed"
124
+ final_response.dig("error", "message") ||
125
+ "OpenAI response status: #{response_status || "unknown"}"
126
+ end
127
+
128
+ crystil_submit_analytics(
129
+ method: :create,
130
+ args: [],
131
+ kwargs: parameters,
132
+ response: final_response,
133
+ start_time: start_time,
134
+ end_time: Time.now,
135
+ title: OPENAI_CLIENT_TITLE,
136
+ version: version,
137
+ status: analytics_status,
138
+ exception: analytics_exception
139
+ )
140
+
141
+ response
142
+ rescue CrystilRequestInterceptedError => e
143
+ raise e
144
+ rescue StandardError => e
145
+ crystil_submit_error_analytics(
146
+ method: :create,
147
+ args: [],
148
+ kwargs: parameters,
149
+ error: e,
150
+ start_time: start_time,
151
+ end_time: Time.now,
152
+ title: OPENAI_CLIENT_TITLE,
153
+ version: version
154
+ )
155
+ raise e
156
+ end
157
+ end
158
+ end
159
+
160
+ def validate_client!(client)
161
+ # When register an OpenAI client, it currently only checks if there
162
+ # is the classic "chat" interface. OpenAI now supports the newer
163
+ # "responses" interface. So, if an OpenAI client later only has
164
+ # the newer "responses" interface, this validate_client will fail.
165
+ return if client.respond_to?(:chat)
166
+
167
+ raise RegistrationError, "Client does not appear to be a valid OpenAI client (missing chat method)"
168
+ end
169
+
170
+ def wrap_chat_method(client)
171
+ client.singleton_class.class_eval do
172
+ include Base
173
+
174
+ alias_method :original_chat, :chat
175
+
176
+ define_method(:chat) do |parameters: {}|
177
+ start_time = Time.now
178
+ version = defined?(::OpenAI::VERSION) ? ::OpenAI::VERSION : nil
179
+
180
+ sentinel = instance_variable_get(:@crystil_sentinel)
181
+ sentinel&.raise_if_irrelevant!(
182
+ title: OPENAI_CLIENT_TITLE,
183
+ request: parameters,
184
+ version: version
185
+ )
186
+
187
+ # Detect streaming and wrap callback to accumulate chunks
188
+ streaming = parameters[:stream].is_a?(Proc)
189
+ accumulated_response = {} if streaming
190
+
191
+ if streaming
192
+ # Configure streaming to include usage (matches Python SDK behavior).
193
+ # A caller-set value (true or false) wins — we only force `true`
194
+ # when the key is absent.
195
+ parameters[:stream_options] ||= {}
196
+ parameters[:stream_options][:include_usage] = true unless parameters[:stream_options].key?(:include_usage)
197
+
198
+ user_callback = parameters[:stream]
199
+ parameters[:stream] = proc do |chunk, bytesize|
200
+ # Normalize chunk to match Python SDK format (add missing delta keys with nil)
201
+ normalized_chunk = crystil_normalize_openai_chunk(chunk)
202
+ # Accumulate chunk (merge into accumulated_response)
203
+ crystil_merge_streaming_chunk(accumulated_response, normalized_chunk)
204
+ # Call user's original callback with original chunk (don't modify user's data)
205
+ user_callback.call(chunk, bytesize)
206
+ end
207
+ end
208
+
209
+ # Call the original method
210
+ response = original_chat(parameters: parameters)
211
+
212
+ # Use accumulated response for streaming, otherwise use returned response
213
+ final_response = streaming ? accumulated_response : response
214
+
215
+ # Submit analytics
216
+ crystil_submit_analytics(
217
+ method: :chat,
218
+ args: [],
219
+ kwargs: parameters,
220
+ response: final_response,
221
+ start_time: start_time,
222
+ end_time: Time.now,
223
+ title: OPENAI_CLIENT_TITLE,
224
+ version: version
225
+ )
226
+
227
+ response
228
+ rescue CrystilRequestInterceptedError => e
229
+ # We don't want to send intercepts to collector
230
+ raise e
231
+ rescue StandardError => e
232
+ crystil_submit_error_analytics(
233
+ method: :chat,
234
+ args: [],
235
+ kwargs: parameters,
236
+ error: e,
237
+ start_time: start_time,
238
+ end_time: Time.now,
239
+ title: OPENAI_CLIENT_TITLE,
240
+ version: version
241
+ )
242
+
243
+ raise e
244
+ end
245
+ end
246
+ end
247
+ end
248
+ end
249
+ end
@@ -0,0 +1,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "constants"
4
+
5
+ module Crystil
6
+ module Wrappers
7
+ # Wrapper for RubyLLM (ruby_llm gem)
8
+ # Supports any provider available via RubyLLM (Anthropic, OpenAI, Google, etc.)
9
+ # Returns raw response and raw formatted query to backend so that the existing
10
+ # extractors can be used.
11
+ class RubyLLM
12
+ def initialize(config, collector, sentinel = nil)
13
+ @config = config
14
+ @collector = collector
15
+ @sentinel = sentinel
16
+ end
17
+
18
+ def register(client)
19
+ validate_client!(client)
20
+
21
+ # Prevent double registration
22
+ return client if client.instance_variable_defined?(:@crystil_registered)
23
+
24
+ # Store references in the client instance
25
+ client.instance_variable_set(:@crystil_config, @config)
26
+ client.instance_variable_set(:@crystil_collector, @collector)
27
+ client.instance_variable_set(:@crystil_sentinel, @sentinel)
28
+ client.instance_variable_set(:@crystil_registered, true)
29
+
30
+ # Ask method handles both streaming and non-streaming cases unlike some others.
31
+ wrap_ask_method(client)
32
+
33
+ client
34
+ end
35
+
36
+ private
37
+
38
+ def validate_client!(client)
39
+ return if client.respond_to?(:ask)
40
+
41
+ raise RegistrationError,
42
+ "Client does not appear to be a valid RubyLLM client (missing ask method)"
43
+ end
44
+
45
+ def wrap_ask_method(client)
46
+ # Capture the wrapper instance in a local variable so it's accessible as a closure
47
+ # inside the block, where self will no longer refer to it.
48
+ wrapper = self
49
+
50
+ client.singleton_class.class_eval do
51
+ include Base
52
+
53
+ alias_method :original_ask, :ask
54
+
55
+ define_method(:ask) do |message, with: nil, &block|
56
+ start_time = Time.now
57
+ query = {} # Fallback for error analytics if an exception occurs before build_query completes
58
+ # Version reported is RubyLLM's, not the underlying provider's — RubyLLM is the
59
+ # meta-wrapper Crystil sees; the native provider gem may not even be loaded.
60
+ version = defined?(::RubyLLM::VERSION) ? ::RubyLLM::VERSION : nil
61
+
62
+ sentinel = instance_variable_get(:@crystil_sentinel)
63
+ provider = model.provider
64
+ # Telemetry invariant: `conversation.client.title` is never nil.
65
+ # `model.provider` should always return a non-empty string for
66
+ # well-formed RubyLLM models, but fall back to the meta-wrapper
67
+ # name (matches JS's pattern of "fallback to the SDK family
68
+ # name") if it ever isn't.
69
+ title = case provider
70
+ when "gemini" then GOOGLE_CLIENT_TITLE
71
+ when nil, "" then RUBY_LLM_PROVIDER
72
+ else provider
73
+ end
74
+
75
+ # Capture existing messages (e.g. system instruction) before the call
76
+ # adds the new user message to history.
77
+ existing_msgs = messages.map { |m| { role: m.role.to_s, content: m.content.to_s } }
78
+ all_msgs = existing_msgs + [{ role: "user", content: message.to_s }]
79
+
80
+ # Build a query in the provider's native format so the backend extractor can parse it.
81
+ query = wrapper.send(:build_query, provider, model.id, all_msgs, tools)
82
+
83
+ # Pass the actual provider title (e.g. "anthropic", "openai", "google") so the
84
+ # sentinel backend can route to the correct classifier for this request format.
85
+ sentinel&.raise_if_irrelevant!(title: title, request: query, version: version)
86
+
87
+ response = original_ask(message, with: with, &block)
88
+
89
+ crystil_submit_analytics(
90
+ method: :ask,
91
+ args: [],
92
+ kwargs: query,
93
+ response: response.raw&.body,
94
+ start_time: start_time,
95
+ end_time: Time.now,
96
+ title: title,
97
+ provider: RUBY_LLM_PROVIDER,
98
+ version: version
99
+ )
100
+
101
+ response
102
+ rescue CrystilRequestInterceptedError => e
103
+ raise e
104
+ rescue StandardError => e
105
+ crystil_submit_error_analytics(
106
+ method: :ask,
107
+ args: [],
108
+ kwargs: query,
109
+ error: e,
110
+ start_time: start_time,
111
+ end_time: Time.now,
112
+ title: title,
113
+ provider: RUBY_LLM_PROVIDER,
114
+ version: version
115
+ )
116
+ raise e
117
+ end
118
+ end
119
+ end
120
+
121
+ # Dispatches to the appropriate provider-native query builder.
122
+ def build_query(provider, model_id, messages, tools)
123
+ if provider == "gemini"
124
+ build_google_query(model_id, messages, tools)
125
+ else
126
+ # Anthropic and OpenAI have the same query format
127
+ build_messages_query(model_id, messages, tools)
128
+ end
129
+ end
130
+
131
+ # Builds an OpenAI-compatible query (used for Anthropic and OpenAI providers).
132
+ def build_messages_query(model_id, messages, tools)
133
+ query = { model: model_id, messages: messages }
134
+ unless tools.empty?
135
+ query[:tools] = tools.values.map do |t|
136
+ { name: t.name, description: t.description, parameters: t.params_schema }
137
+ end
138
+ end
139
+ query
140
+ end
141
+
142
+ # Builds a Google-native query with contents/systemInstruction structure.
143
+ def build_google_query(model_id, messages, tools)
144
+ system_msgs = messages.select { |m| m[:role] == "system" }
145
+ content_msgs = messages.reject { |m| m[:role] == "system" }
146
+
147
+ query = {
148
+ model: model_id,
149
+ contents: content_msgs.map do |m|
150
+ google_role = m[:role] == "assistant" ? "model" : m[:role]
151
+ { role: google_role, parts: [{ text: m[:content] }] }
152
+ end
153
+ }
154
+
155
+ if system_msgs.any?
156
+ system_text = system_msgs.map { |m| m[:content] }.join("\n")
157
+ query[:systemInstruction] = { parts: [{ text: system_text }] }
158
+ end
159
+
160
+ unless tools.empty?
161
+ query[:tools] = tools.values.map do |t|
162
+ { name: t.name, description: t.description, parameters: t.params_schema }
163
+ end
164
+ end
165
+
166
+ query
167
+ end
168
+ end
169
+ end
170
+ end
data/lib/crystil.rb ADDED
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "crystil/version"
4
+ require_relative "crystil/errors"
5
+ require_relative "crystil/config"
6
+ require_relative "crystil/sentinel"
7
+ require_relative "crystil/attribution"
8
+ require_relative "crystil/collector"
9
+ require_relative "crystil/wrappers/base"
10
+ require_relative "crystil/wrappers/openai"
11
+ require_relative "crystil/wrappers/anthropic"
12
+ require_relative "crystil/wrappers/google"
13
+ require_relative "crystil/wrappers/geminiai"
14
+ require_relative "crystil/wrappers/ruby_llm"
15
+ require_relative "crystil/wrappers/groq"
16
+ require_relative "crystil/api/base"
17
+ require_relative "crystil/api/sentinel"
18
+ require_relative "crystil/api/workflows"
19
+ require_relative "crystil/api/invocation"
20
+ require_relative "crystil/api/workflow"
21
+ require_relative "crystil/client"
22
+
23
+ # Crystil - Cost visibility for AI agents
24
+ #
25
+ # Crystil is a lightweight infrastructure layer that gives AI teams
26
+ # real-time visibility into the true costs of deploying agents.
27
+ #
28
+ # @example Basic usage with OpenAI
29
+ # require 'openai'
30
+ # require 'crystil'
31
+ #
32
+ # openai = OpenAI::Client.new(access_token: ENV['OPENAI_API_KEY'])
33
+ # crystil = Crystil::Client.new(api_key: ENV['CRYSTIL_API_KEY'])
34
+ # crystil.openai.register(openai)
35
+ #
36
+ # # Use OpenAI normally - analytics tracked automatically
37
+ # response = openai.chat(
38
+ # parameters: {
39
+ # model: "gpt-4",
40
+ # messages: [{ role: "user", content: "Hello!" }]
41
+ # }
42
+ # )
43
+ #
44
+ # @example With attribution
45
+ # crystil.attribution(
46
+ # parent_id: "user-123",
47
+ # parent_name: "John Doe"
48
+ # )
49
+ #
50
+ # @see https://developers.crystil.com
51
+ module Crystil
52
+ end
@@ -0,0 +1,24 @@
1
+ # Starter signatures generated by typeprof + hand-refined; see CLAUDE.md
2
+ # "Code style: type signatures (RBS)" for the refinement convention.
3
+ module Crystil
4
+ module API
5
+ class Base
6
+ @original_api_url: String
7
+ @api_url: String
8
+ @api_key: String
9
+ @timeout: Numeric
10
+
11
+ def initialize: (String api_url, String api_key, Numeric timeout) -> void
12
+
13
+ private
14
+ def get: (String path) -> untyped
15
+ def post: (String path, ?Hash[Symbol, untyped]? body) -> untyped
16
+ def put: (String path, ?Hash[Symbol, untyped]? body) -> untyped
17
+ def delete: (String path) -> untyped
18
+ def request: (Symbol method, String path, ?Hash[Symbol, untyped]? body) -> untyped
19
+ def build_request: (Symbol method, URI::Generic uri, Hash[Symbol, untyped]? body) -> (Net::HTTP::Delete | Net::HTTP::Get | Net::HTTP::Post | Net::HTTP::Put)
20
+ def handle_response: (Net::HTTPResponse response) -> untyped
21
+ def parse_json_response: (String? body) -> untyped
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,16 @@
1
+ # Starter signatures generated by typeprof + hand-refined; see CLAUDE.md
2
+ # "Code style: type signatures (RBS)" for the refinement convention.
3
+ module Crystil
4
+ module API
5
+ class Invocation < Base
6
+ @attribution: Hash[Symbol, untyped]?
7
+
8
+ def initialize: (String api_url, String api_key, Numeric timeout) -> void
9
+ def attribution: (parent_id: String, ?parent_name: String?, ?subsidiary_id: String?, ?subsidiary_name: String?) -> self
10
+ def summary: (String workflow_uuid, date_start: (Date | Time | String), ?date_end: (Date | Time | String)?) -> untyped
11
+
12
+ private
13
+ def format_date: ((Date | Time | String) date) -> String
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,9 @@
1
+ # Starter signatures generated by typeprof + hand-refined; see CLAUDE.md
2
+ # "Code style: type signatures (RBS)" for the refinement convention.
3
+ module Crystil
4
+ module API
5
+ class Sentinel < Base
6
+ def relevance_intercept: (Hash[Symbol, untyped] payload) -> untyped
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,15 @@
1
+ # Starter signatures generated by typeprof + hand-refined; see CLAUDE.md
2
+ # "Code style: type signatures (RBS)" for the refinement convention.
3
+ module Crystil
4
+ module API
5
+ class Workflow < Base
6
+ @uuid: String
7
+
8
+ def initialize: (String uuid, String api_url, String api_key, Numeric timeout) -> void
9
+ def details: -> untyped
10
+ def update: (label: String) -> untyped
11
+ def destroy: -> untyped
12
+ def invocation: -> Crystil::API::Invocation
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,16 @@
1
+ # Starter signatures generated by typeprof + hand-refined; see CLAUDE.md
2
+ # "Code style: type signatures (RBS)" for the refinement convention.
3
+ module Crystil
4
+ module API
5
+ class Workflows < Base
6
+ @invocation: Crystil::API::Invocation
7
+
8
+ def list: -> untyped
9
+ def create: (name: String, ?description: String?) -> untyped
10
+ def details: (String uuid) -> untyped
11
+ def update: (String uuid, label: String) -> untyped
12
+ def destroy: (String uuid) -> untyped
13
+ def invocation: -> Crystil::API::Invocation
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,17 @@
1
+ # Starter signatures generated by typeprof + hand-refined; see CLAUDE.md
2
+ # "Code style: type signatures (RBS)" for the refinement convention.
3
+ module Crystil
4
+ class Attribution
5
+ attr_reader parent_id: String
6
+ attr_reader parent_name: String?
7
+ attr_reader subsidiary_id: String?
8
+ attr_reader subsidiary_name: String?
9
+ def initialize: (parent_id: String, ?parent_name: String?, ?subsidiary_id: String?, ?subsidiary_name: String?) -> void
10
+ def to_h: -> Hash[Symbol, untyped]
11
+
12
+ private
13
+ def validate_parent_id!: (untyped value) -> String
14
+ def validate_string_length!: (untyped value, String field_name) -> String?
15
+ def validate_subsidiary_requirements!: -> void
16
+ end
17
+ end
@@ -0,0 +1,29 @@
1
+ # Starter signatures generated by typeprof + hand-refined; see CLAUDE.md
2
+ # "Code style: type signatures (RBS)" for the refinement convention.
3
+ module Crystil
4
+ class Client
5
+ @openai: Crystil::Wrappers::OpenAI
6
+ @anthropic: Crystil::Wrappers::Anthropic
7
+ @google: Crystil::Wrappers::Google
8
+ @geminiai: Crystil::Wrappers::GeminiAI
9
+ @ruby_llm: Crystil::Wrappers::RubyLLM
10
+ @groq: Crystil::Wrappers::Groq
11
+ @workflows: Crystil::API::Workflows
12
+
13
+ attr_reader config: Crystil::Config
14
+ attr_reader collector: Crystil::Collector
15
+ attr_reader sentinel: Crystil::Sentinel
16
+
17
+ def initialize: (?api_key: String?, ?collector_url: String?, ?api_url: String?, ?timeout: Numeric?) -> void
18
+ def openai: -> Crystil::Wrappers::OpenAI
19
+ def anthropic: -> Crystil::Wrappers::Anthropic
20
+ def google: -> Crystil::Wrappers::Google
21
+ def geminiai: -> Crystil::Wrappers::GeminiAI
22
+ def ruby_llm: -> Crystil::Wrappers::RubyLLM
23
+ def groq: -> Crystil::Wrappers::Groq
24
+ def attribution: (parent_id: String, ?parent_name: String?, ?subsidiary_id: String?, ?subsidiary_name: String?) -> self
25
+ def new_transaction: -> self
26
+ def workflows: -> Crystil::API::Workflows
27
+ def shutdown: -> void
28
+ end
29
+ end
@@ -0,0 +1,20 @@
1
+ # Starter signatures generated by typeprof + hand-refined; see CLAUDE.md
2
+ # "Code style: type signatures (RBS)" for the refinement convention.
3
+ module Crystil
4
+ class Collector
5
+ DEFAULT_MAX_RETRIES: Integer
6
+ RETRY_DELAY: Integer
7
+ @config: Crystil::Config
8
+ # Concurrent::ThreadPoolExecutor — no upstream signatures available.
9
+ @executor: untyped
10
+
11
+ def initialize: (Crystil::Config config) -> void
12
+ def submit_async: (Hash[Symbol, untyped] payload) -> void
13
+ def submit: (Hash[Symbol, untyped] payload) -> untyped
14
+ def shutdown: -> void
15
+
16
+ private
17
+ def submit_with_retry: (Hash[Symbol, untyped] payload, ?Integer attempt) -> untyped
18
+ def post_to_collector: (Hash[Symbol, untyped] payload) -> untyped
19
+ end
20
+ end
@@ -0,0 +1,33 @@
1
+ # Starter signatures generated by typeprof + hand-refined; see CLAUDE.md
2
+ # "Code style: type signatures (RBS)" for the refinement convention.
3
+ module Crystil
4
+ class Config
5
+ # Concurrent::AtomicReference[T] — no `concurrent-ruby` signatures for
6
+ # that class are shipped in the public rbs_collection, so falls back to
7
+ # `untyped`. Refine once we add a hand-rolled stub or upstream sig.
8
+ @attribution: untyped
9
+ @tx_uuid: untyped
10
+ @raise_if_irrelevant: untyped
11
+ @secs_irrelevant_request_timeout: untyped
12
+
13
+ attr_reader api_key: String
14
+ attr_reader collector_url: String
15
+ attr_reader api_url: String
16
+ attr_reader timeout: Numeric
17
+ attr_reader version: String
18
+
19
+ def initialize: (?api_key: String?, ?collector_url: String?, ?api_url: String?, ?timeout: Numeric?) -> void
20
+ def attribution: -> Crystil::Attribution?
21
+ def attribution=: (Crystil::Attribution? value) -> void
22
+ def tx_uuid: -> String
23
+ def tx_uuid=: (String value) -> void
24
+ def raise_if_irrelevant: -> bool
25
+ def raise_if_irrelevant=: (bool value) -> void
26
+ def secs_irrelevant_request_timeout: -> Numeric
27
+ def secs_irrelevant_request_timeout=: (Numeric value) -> void
28
+ def new_transaction: -> String
29
+
30
+ private
31
+ def nonempty_env: (String name) -> String?
32
+ end
33
+ end