hermes-client 0.0.0 → 0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 43e89034ffc51c50bf49f73ad2bb1b26d74c924c543ce26a95f25523310375b8
4
- data.tar.gz: 618f19001d44ccfac6b0f97a1f7b0757b4743a09c6eab9feb4cc082f5654c4a6
3
+ metadata.gz: 0c8205d1a6aed4260e80ca3d351772791025195265b92a883b379a1f697c098a
4
+ data.tar.gz: 66bc29e492989c686d9866633e119b5cd2bdc2a4c8b1cc8fc34a0d9dc8b9d615
5
5
  SHA512:
6
- metadata.gz: d3368a4b8a47c703e8e1ae31b7306b7d9da99b92d3663bc58a04692709e28af7b83020f0b2554a52d1b4ed6cc44907f079342a34db8115897e7665a744ba35a8
7
- data.tar.gz: 431bbd67359fc7ce185b276e6030dd2ee30d923ed18a34ef9513ea0aca93b026786f72a7c40320b8521f51470d802bfd54d4281eefe12cd4a68a4e2ab61c9b4b
6
+ metadata.gz: 741e6c38b1937b260ddd206449ba5bf6095eb3f3ad037ba569a6eed6cc924c5b734805fc9ede3a043bc21359dc95a32f4abb1f2a55d89118ec1267d16c029da1
7
+ data.tar.gz: b006e53068f75e41434c913105956dac7335b1a49427daa82e7b29dd537152ec9decc50ef506d739378c3f5df20f9429cd9ce09b120f03c6f853c8a9fea3f829
data/.yardopts ADDED
@@ -0,0 +1,11 @@
1
+ --no-private
2
+ --title=HermesAgent::Client
3
+ --markup=markdown
4
+ --markup-provider redcarpet
5
+ --main=README.md
6
+ ./lib/hermes_agent/**/*.rb
7
+ ./lib/hermes-client.rb
8
+ -
9
+ README.md
10
+ LICENSE.md
11
+ CHANGELOG.md
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # Release History
2
+
3
+ ### v0.1.0 / 2026-05-26
4
+
5
+ Initial release (alpha quality)
data/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ # License
2
+
3
+ Copyright 2026 Daniel Azuma
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
21
+ IN THE SOFTWARE.
data/README.md CHANGED
@@ -1,9 +1,107 @@
1
- # Placeholder for hermes-client
1
+ # Hermes-Client
2
2
 
3
- This is a placeholder gem, which was generated on 2026-05-22 to
4
- reserve the gem "hermes-client".
5
- The actual gem is planned for release in the near future.
3
+ `hermes-client` is a Ruby client library for the Hermes Agent API Server.
6
4
 
7
- If this is a problem, or if the actual gem has not been released
8
- in a timely manner, you can contact the owner at
9
- `dazuma@gmail.com`.
5
+ ## Getting started
6
+
7
+ Install the gem using
8
+
9
+ ```sh
10
+ gem install hermes-client
11
+ ```
12
+
13
+ Or add it to your bundle:
14
+
15
+ ```ruby
16
+ # In your Gemfile
17
+ gem "hermes-client"
18
+ ```
19
+
20
+ Create and use a client object:
21
+
22
+ ```ruby
23
+ require "hermes-client"
24
+
25
+ # Create a new client and point it at a Hermes Gateway server
26
+ hermes_client = HermesAgent::Client.new(
27
+ base_url: "http://localhost:8642",
28
+ api_key: "my-key-12345678"
29
+ )
30
+
31
+ # Send chat messages to the gateway
32
+ response = hermes_client.responses.create(input: "Tell me a joke.")
33
+ puts response.output_text
34
+
35
+ # Manage jobs
36
+ briefing_job = hermes_client.jobs.create(
37
+ name: "daily-briefing",
38
+ schedule: "every morning at 8am",
39
+ prompt: "Collect the day's news and email me a summary."
40
+ )
41
+ puts "Daily-briefing will next run at #{briefing_job.next_run_at}"
42
+ ```
43
+
44
+ A client is not thread-safe (it holds a persistent connection); create one
45
+ client per thread.
46
+
47
+ For more information, see the
48
+ [Hermes Gateway API documentation](https://hermes-agent.nousresearch.com/docs/user-guide/features/api-server).
49
+
50
+ Full API documentation is available at https://dazuma.github.io/hermes-client
51
+ for released gems.
52
+
53
+ ## Requirements and status
54
+
55
+ `hermes-client` requires Ruby 3.4 or later.
56
+
57
+ The gem can be considered alpha quality. Initial development is complete, but
58
+ the library has seen minimal real-world testing, and it may experience
59
+ significant changes, including breaking interface changes. It is available now
60
+ on an experimental basis, but not currently recommended for production use.
61
+
62
+ ## Contributing
63
+
64
+ Development is done in GitHub at https://github.com/dazuma/hermes-client.
65
+
66
+ * To file issues: https://github.com/dazuma/hermes-client/issues.
67
+ * For questions and discussion, please do not file an issue. Instead, use the
68
+ discussions feature: https://github.com/dazuma/hermes-client/discussions.
69
+ * Before opening any non-trivial pull request, please report a bug or feature
70
+ request using an issue.
71
+
72
+ The library uses [toys](https://dazuma.github.io/toys) for testing and CI. To
73
+ run the test suite, `gem install toys` and then run `toys ci`. You can also run
74
+ unit tests, rubocop, and build tests independently.
75
+
76
+ As of late May, 2026, the documentation provided by Hermes is fairly thin, and
77
+ the developer had to cobble together an understanding of the API interfaces and
78
+ protocols from various sources, including the Hermes source and empirical
79
+ probing of a live gateway. Documents related to our findings are available in
80
+ the `devdocs` directory, and some Toys-based probing tools are also included in
81
+ this repository. (These are not included in the gem distribution.)
82
+
83
+ Much of the heavy lifting in the original implementation and documentation, as
84
+ well as the research behind it into the actual behavior of the gateway API, was
85
+ done in close collaboration with Claude Code (Opus 4.7).
86
+
87
+ ## License
88
+
89
+ Copyright 2026 Daniel Azuma
90
+
91
+ Permission is hereby granted, free of charge, to any person obtaining a copy
92
+ of this software and associated documentation files (the "Software"), to deal
93
+ in the Software without restriction, including without limitation the rights
94
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
95
+ copies of the Software, and to permit persons to whom the Software is
96
+ furnished to do so, subject to the following conditions:
97
+
98
+ The above copyright notice and this permission notice shall be included in
99
+ all copies or substantial portions of the Software.
100
+
101
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
102
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
103
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
104
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
105
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
106
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
107
+ IN THE SOFTWARE.
data/lib/hermes-client.rb CHANGED
@@ -1,8 +1,3 @@
1
- #
2
- # This is a placeholder Ruby file for gem "hermes-client".
3
- # It was generated on 2026-05-22 to reserve the gem name.
4
- # The actual gem is planned for release in the near future.
5
- # If this is a problem, or if the actual gem has not been
6
- # released in a timely manner, you can contact the owner at
7
- # dazuma@gmail.com
8
- #
1
+ # frozen_string_literal: true
2
+
3
+ require "hermes_agent/client"
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HermesAgent
4
+ class Client
5
+ ##
6
+ # Connection settings for a {HermesAgent::Client}.
7
+ #
8
+ # Holds the server location and credentials shared by all requests. An
9
+ # instance is created when a client is constructed and may be customized
10
+ # either via keyword arguments or by yielding the configuration to a block.
11
+ #
12
+ class Configuration
13
+ ##
14
+ # The default server root URL.
15
+ # @return [String]
16
+ #
17
+ DEFAULT_BASE_URL = "http://127.0.0.1:8642"
18
+
19
+ ##
20
+ # The default keep-alive timeout, in seconds.
21
+ # @return [Numeric]
22
+ #
23
+ DEFAULT_KEEP_ALIVE_TIMEOUT = 5
24
+
25
+ ##
26
+ # Create a configuration.
27
+ #
28
+ # @param base_url [String] The server root URL, _without_ a path prefix
29
+ # such as `/v1`. Defaults to {DEFAULT_BASE_URL}.
30
+ # @param api_key [String, nil] The bearer token sent on every request, or
31
+ # `nil` to send no `Authorization` header. Defaults to the
32
+ # `HERMES_API_KEY` environment variable.
33
+ # @param read_timeout [Numeric, nil] The read timeout in seconds, or `nil`
34
+ # for no client-side limit.
35
+ # @param open_timeout [Numeric, nil] The connection-open timeout in
36
+ # seconds, or `nil` for no client-side limit.
37
+ # @param write_timeout [Numeric, nil] The timeout in seconds for writing
38
+ # a request, or `nil` for no client-side limit.
39
+ # @param keep_alive_timeout [Numeric] How long, in seconds, an idle
40
+ # persistent connection may be reused before it is considered stale
41
+ # and reopened on the next request. Defaults to
42
+ # {DEFAULT_KEEP_ALIVE_TIMEOUT}.
43
+ #
44
+ def initialize(base_url: DEFAULT_BASE_URL,
45
+ api_key: ::ENV.fetch("HERMES_API_KEY", nil),
46
+ read_timeout: nil,
47
+ open_timeout: nil,
48
+ write_timeout: nil,
49
+ keep_alive_timeout: DEFAULT_KEEP_ALIVE_TIMEOUT)
50
+ @base_url = base_url
51
+ @api_key = api_key
52
+ @read_timeout = read_timeout
53
+ @open_timeout = open_timeout
54
+ @write_timeout = write_timeout
55
+ @keep_alive_timeout = keep_alive_timeout
56
+ end
57
+
58
+ ##
59
+ # The server root URL, without a path prefix.
60
+ # @return [String]
61
+ #
62
+ attr_accessor :base_url
63
+
64
+ ##
65
+ # The bearer token sent on every request, or `nil` for none.
66
+ # @return [String, nil]
67
+ #
68
+ attr_accessor :api_key
69
+
70
+ ##
71
+ # The read timeout in seconds, or `nil` for no client-side limit.
72
+ # @return [Numeric, nil]
73
+ #
74
+ attr_accessor :read_timeout
75
+
76
+ ##
77
+ # The connection-open timeout in seconds, or `nil` for no client-side
78
+ # limit.
79
+ # @return [Numeric, nil]
80
+ #
81
+ attr_accessor :open_timeout
82
+
83
+ ##
84
+ # The request-write timeout in seconds, or `nil` for no client-side
85
+ # limit.
86
+ # @return [Numeric, nil]
87
+ #
88
+ attr_accessor :write_timeout
89
+
90
+ ##
91
+ # How long, in seconds, an idle persistent connection may be reused before
92
+ # it is considered stale and reopened on the next request.
93
+ # @return [Numeric]
94
+ #
95
+ attr_accessor :keep_alive_timeout
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HermesAgent
4
+ class Client
5
+ ##
6
+ # A stateful, multi-turn conversation over the Responses API that chains its
7
+ # turns automatically, so each call takes only the turn's `input:`.
8
+ #
9
+ # Construct one via {Resources::Responses#conversation} rather than directly:
10
+ #
11
+ # convo = client.responses.conversation
12
+ # convo.create(input: "Hello").output_text
13
+ # convo.create(input: "And what about X?").output_text # auto-chains
14
+ #
15
+ # There are two chaining mechanisms, selected at construction:
16
+ #
17
+ # - **id-tracking mode** (the default): the conversation remembers each
18
+ # turn's response id client-side and threads it into the next turn as
19
+ # `previous_response_id`. Pass `previous_response_id:` to resume such a
20
+ # thread from a known id (e.g. across process restarts).
21
+ # - **named mode** (`name:`): every turn sends a stable `conversation` name
22
+ # and the server keeps the thread; no client-side id is threaded.
23
+ #
24
+ # The verb methods mirror {Resources::Responses} ({#create} /
25
+ # {#stream_create}) and return the same entities and stream, so the helper
26
+ # is a drop-in. {#last_response_id} is recorded in both modes for
27
+ # inspection or persistence.
28
+ #
29
+ # A conversation models a single sequential thread and is not thread-safe:
30
+ # issue and (for streaming) consume one turn before starting the next.
31
+ #
32
+ class Conversation
33
+ ##
34
+ # Create a conversation. Prefer {Resources::Responses#conversation}.
35
+ #
36
+ # @param responses [Resources::Responses] The responses resource to issue
37
+ # turns through.
38
+ # @param name [String, nil] A conversation name for server-side chaining.
39
+ # Mutually exclusive with `previous_response_id`.
40
+ # @param previous_response_id [String, nil] A prior response id to seed
41
+ # client-side chaining from. Mutually exclusive with `name`.
42
+ # @raise [ArgumentError] If both `name` and `previous_response_id` are
43
+ # given (they select different chaining mechanisms).
44
+ #
45
+ # @private
46
+ def initialize(responses, name: nil, previous_response_id: nil)
47
+ raise ::ArgumentError, "name and previous_response_id are mutually exclusive" if name && previous_response_id
48
+
49
+ @responses = responses
50
+ @name = name
51
+ @last_response_id = previous_response_id
52
+ end
53
+
54
+ ##
55
+ # The conversation name, in named mode; `nil` in id-tracking mode.
56
+ # @return [String, nil]
57
+ #
58
+ attr_reader :name
59
+
60
+ ##
61
+ # The id of the most recent turn's response (also the seed id before any
62
+ # turn, in id-tracking mode). In named mode it is recorded for inspection
63
+ # but not used for chaining.
64
+ # @return [String, nil]
65
+ #
66
+ attr_reader :last_response_id
67
+
68
+ ##
69
+ # Create the next turn in the conversation.
70
+ #
71
+ # @param input [String, Array<Hash>] The turn's input (see
72
+ # {Resources::Responses#create}).
73
+ # @param extra [Hash] Additional request-body fields merged into the body.
74
+ # @return [Entities::Response] The response. Its id becomes
75
+ # {#last_response_id}.
76
+ # @raise [APIError] If the server returns a non-2xx response.
77
+ #
78
+ def create(input:, **extra)
79
+ response = @responses.create(input: input, **chaining, **extra)
80
+ capture(response)
81
+ response
82
+ end
83
+
84
+ ##
85
+ # Create the next turn, streaming its events. Follows the same
86
+ # block-or-enumerator contract as {Resources::Responses#stream_create}:
87
+ # with a block, each event is yielded and the assembled
88
+ # {Entities::Response} is returned; without one, a {Stream} is returned.
89
+ # In either case the turn's response id is captured into
90
+ # {#last_response_id} when the stream's result is built (during
91
+ # consumption), so a subsequent turn chains onto it.
92
+ #
93
+ # @param input [String, Array<Hash>] The turn's input.
94
+ # @param extra [Hash] Additional request-body fields merged into the body.
95
+ # @yieldparam event [Entities::ResponseStreamEvent] Each streamed event.
96
+ # @return [Entities::Response, Stream] The assembled response when a block
97
+ # is given, otherwise the {Stream}.
98
+ # @raise [APIError] If the server returns a non-2xx response.
99
+ #
100
+ def stream_create(input:, **extra, &)
101
+ @responses.stream_response(
102
+ on_result: method(:capture), input: input, **chaining, **extra, &
103
+ )
104
+ end
105
+
106
+ private
107
+
108
+ ##
109
+ # The chaining fields for the next turn: the conversation name in named
110
+ # mode, the tracked previous response id in id-tracking mode, or none.
111
+ #
112
+ # @return [Hash]
113
+ #
114
+ def chaining
115
+ return {conversation: @name} if @name
116
+ return {previous_response_id: @last_response_id} if @last_response_id
117
+
118
+ {}
119
+ end
120
+
121
+ ##
122
+ # Record a turn's response id as {#last_response_id}, ignoring a missing
123
+ # id (which would otherwise drop a previously tracked one).
124
+ #
125
+ # @param response [Entities::Response, nil] The turn's response.
126
+ # @return [void]
127
+ #
128
+ def capture(response)
129
+ id = response&.id
130
+ @last_response_id = id if id
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,289 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "hermes_agent/client/entity"
4
+
5
+ module HermesAgent
6
+ class Client
7
+ module Entities
8
+ ##
9
+ # The authentication scheme advertised by the server
10
+ # ({Capabilities#auth}).
11
+ #
12
+ class Auth < Entity
13
+ ##
14
+ # The authentication type, e.g. `"bearer"`.
15
+ # @return [String, nil]
16
+ #
17
+ def type
18
+ self["type"]
19
+ end
20
+
21
+ ##
22
+ # Whether authentication is required.
23
+ # @return [boolean, nil]
24
+ #
25
+ def required?
26
+ self["required"]
27
+ end
28
+ end
29
+
30
+ ##
31
+ # The server's execution model ({Capabilities#runtime}).
32
+ #
33
+ class Runtime < Entity
34
+ ##
35
+ # The runtime mode, e.g. `"server_agent"`.
36
+ # @return [String, nil]
37
+ #
38
+ def mode
39
+ self["mode"]
40
+ end
41
+
42
+ ##
43
+ # Where tools execute, e.g. `"server"`.
44
+ # @return [String, nil]
45
+ #
46
+ def tool_execution
47
+ self["tool_execution"]
48
+ end
49
+
50
+ ##
51
+ # Whether the runtime is split between client and server. A runtime
52
+ # that does not advertise this is treated as not split (`false`).
53
+ # @return [boolean]
54
+ #
55
+ def split_runtime?
56
+ !!self["split_runtime"]
57
+ end
58
+
59
+ ##
60
+ # A human-readable description of the runtime.
61
+ # @return [String, nil]
62
+ #
63
+ def description
64
+ self["description"]
65
+ end
66
+ end
67
+
68
+ ##
69
+ # The server's feature matrix ({Capabilities#features}).
70
+ #
71
+ # Each reader returns whether the server advertises that feature: `true`
72
+ # when the flag is set, `false` when it is unset or absent (an
73
+ # unadvertised feature is treated as unsupported). Readers are
74
+ # best-effort; use {#[]} / {#to_h} for any feature not yet modeled here.
75
+ #
76
+ class Features < Entity
77
+ ##
78
+ # Whether the chat-completions endpoint is supported.
79
+ # @return [boolean]
80
+ #
81
+ def chat_completions?
82
+ !!self["chat_completions"]
83
+ end
84
+
85
+ ##
86
+ # Whether chat-completions streaming is supported.
87
+ # @return [boolean]
88
+ #
89
+ def chat_completions_streaming?
90
+ !!self["chat_completions_streaming"]
91
+ end
92
+
93
+ ##
94
+ # Whether the Responses API is supported.
95
+ # @return [boolean]
96
+ #
97
+ def responses_api?
98
+ !!self["responses_api"]
99
+ end
100
+
101
+ ##
102
+ # Whether Responses API streaming is supported.
103
+ # @return [boolean]
104
+ #
105
+ def responses_streaming?
106
+ !!self["responses_streaming"]
107
+ end
108
+
109
+ ##
110
+ # Whether run submission is supported.
111
+ # @return [boolean]
112
+ #
113
+ def run_submission?
114
+ !!self["run_submission"]
115
+ end
116
+
117
+ ##
118
+ # Whether run status polling is supported.
119
+ # @return [boolean]
120
+ #
121
+ def run_status?
122
+ !!self["run_status"]
123
+ end
124
+
125
+ ##
126
+ # Whether the run events SSE stream is supported.
127
+ # @return [boolean]
128
+ #
129
+ def run_events_sse?
130
+ !!self["run_events_sse"]
131
+ end
132
+
133
+ ##
134
+ # Whether stopping a run is supported.
135
+ # @return [boolean]
136
+ #
137
+ def run_stop?
138
+ !!self["run_stop"]
139
+ end
140
+
141
+ ##
142
+ # Whether responding to a run approval request is supported.
143
+ # @return [boolean]
144
+ #
145
+ def run_approval_response?
146
+ !!self["run_approval_response"]
147
+ end
148
+
149
+ ##
150
+ # Whether the server emits custom tool-progress events.
151
+ # @return [boolean]
152
+ #
153
+ def tool_progress_events?
154
+ !!self["tool_progress_events"]
155
+ end
156
+
157
+ ##
158
+ # Whether the server emits approval events.
159
+ # @return [boolean]
160
+ #
161
+ def approval_events?
162
+ !!self["approval_events"]
163
+ end
164
+
165
+ ##
166
+ # Whether CORS is enabled.
167
+ # @return [boolean]
168
+ #
169
+ def cors?
170
+ !!self["cors"]
171
+ end
172
+
173
+ ##
174
+ # The request header carrying the session-continuity id, e.g.
175
+ # `"X-Hermes-Session-Id"`.
176
+ # @return [String, nil]
177
+ #
178
+ def session_continuity_header
179
+ self["session_continuity_header"]
180
+ end
181
+
182
+ ##
183
+ # The request header carrying the session key, e.g.
184
+ # `"X-Hermes-Session-Key"`.
185
+ # @return [String, nil]
186
+ #
187
+ def session_key_header
188
+ self["session_key_header"]
189
+ end
190
+ end
191
+
192
+ ##
193
+ # A single advertised route (one entry of {Capabilities#endpoints}).
194
+ #
195
+ class Endpoint < Entity
196
+ ##
197
+ # The HTTP method, e.g. `"GET"`. (Named `http_method` rather than
198
+ # `method` to avoid shadowing `Object#method`.)
199
+ # @return [String, nil]
200
+ #
201
+ def http_method
202
+ self["method"]
203
+ end
204
+
205
+ ##
206
+ # The request path, e.g. `"/v1/models"`. May contain `{...}`
207
+ # placeholders such as `/v1/runs/{run_id}`.
208
+ # @return [String, nil]
209
+ #
210
+ def path
211
+ self["path"]
212
+ end
213
+ end
214
+
215
+ ##
216
+ # The server's advertised capabilities (`GET /v1/capabilities`).
217
+ # Field readers are best-effort; {#to_h} remains the source of truth.
218
+ #
219
+ class Capabilities < Entity
220
+ ##
221
+ # The object type, `"hermes.api_server.capabilities"`.
222
+ # @return [String, nil]
223
+ #
224
+ def object
225
+ self["object"]
226
+ end
227
+
228
+ ##
229
+ # The platform identifier, e.g. `"hermes-agent"`.
230
+ # @return [String, nil]
231
+ #
232
+ def platform
233
+ self["platform"]
234
+ end
235
+
236
+ ##
237
+ # The configured server-side model id, e.g. `"hermes-test"`.
238
+ # @return [String, nil]
239
+ #
240
+ def model
241
+ self["model"]
242
+ end
243
+
244
+ ##
245
+ # The authentication scheme, wrapped in an {Auth} entity. Returns
246
+ # `nil` when the field is absent.
247
+ # @return [Auth, nil]
248
+ #
249
+ def auth
250
+ raw = self["auth"]
251
+ raw.is_a?(::Hash) ? Auth.new(raw) : nil
252
+ end
253
+
254
+ ##
255
+ # The execution model, wrapped in a {Runtime} entity. Returns `nil`
256
+ # when the field is absent.
257
+ # @return [Runtime, nil]
258
+ #
259
+ def runtime
260
+ raw = self["runtime"]
261
+ raw.is_a?(::Hash) ? Runtime.new(raw) : nil
262
+ end
263
+
264
+ ##
265
+ # The feature matrix, wrapped in a {Features} entity. Returns `nil`
266
+ # when the field is absent.
267
+ # @return [Features, nil]
268
+ #
269
+ def features
270
+ raw = self["features"]
271
+ raw.is_a?(::Hash) ? Features.new(raw) : nil
272
+ end
273
+
274
+ ##
275
+ # The advertised routes, keyed by logical name (e.g. `"models"`), each
276
+ # value wrapped in an {Endpoint}. Returns `nil` when the field is
277
+ # absent.
278
+ # @return [Hash{String => Endpoint}, nil]
279
+ #
280
+ def endpoints
281
+ raw = self["endpoints"]
282
+ return nil unless raw.is_a?(::Hash)
283
+
284
+ raw.transform_values { |value| Endpoint.new(value) }
285
+ end
286
+ end
287
+ end
288
+ end
289
+ end