phaseo_sdk 2.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 +7 -0
- data/README.md +99 -0
- data/examples/models.rb +16 -0
- data/lib/gen/client.rb +86 -0
- data/lib/gen/models.rb +2124 -0
- data/lib/gen/operations.rb +512 -0
- data/lib/index.rb +974 -0
- data/lib/phaseo_sdk/model_ids.rb +1840 -0
- data/lib/phaseo_sdk/version.rb +3 -0
- data/lib/phaseo_sdk.rb +2 -0
- data/phaseo_sdk.gemspec +22 -0
- data/tests/api_key_mutations_test.rb +39 -0
- data/tests/api_key_test.rb +30 -0
- data/tests/api_keys_test.rb +30 -0
- data/tests/async_jobs_test.rb +13 -0
- data/tests/batches_test.rb +124 -0
- data/tests/chat_test.rb +75 -0
- data/tests/current_key_test.rb +29 -0
- data/tests/devtools_test.rb +364 -0
- data/tests/endpoints_test.rb +27 -0
- data/tests/files_test.rb +67 -0
- data/tests/generation_test.rb +29 -0
- data/tests/health_test.rb +26 -0
- data/tests/lifecycle_test.rb +55 -0
- data/tests/models_test.rb +39 -0
- data/tests/organisations_test.rb +36 -0
- data/tests/pricing_calculate_test.rb +36 -0
- data/tests/pricing_models_test.rb +42 -0
- data/tests/provider_ops_test.rb +44 -0
- data/tests/smoke_chat.rb +22 -0
- data/tests/smoke_responses.rb +25 -0
- data/tests/smoke_responses_sdk.rb +30 -0
- data/tests/video_test.rb +152 -0
- data/tests/workspace_mutations_test.rb +39 -0
- data/tests/workspaces_test.rb +45 -0
- metadata +81 -0
data/lib/index.rb
ADDED
|
@@ -0,0 +1,974 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "time"
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
require "cgi"
|
|
6
|
+
require "uri"
|
|
7
|
+
require_relative "phaseo_sdk/model_ids"
|
|
8
|
+
require_relative "gen/client"
|
|
9
|
+
require_relative "gen/models"
|
|
10
|
+
require_relative "gen/operations"
|
|
11
|
+
|
|
12
|
+
module PhaseoSdk
|
|
13
|
+
class AsyncJobsResource
|
|
14
|
+
def initialize(parent)
|
|
15
|
+
@parent = parent
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def websocket_url(kind, job_id, interval_ms: nil, close_on_terminal: nil)
|
|
19
|
+
@parent.get_async_job_websocket_url(
|
|
20
|
+
kind,
|
|
21
|
+
job_id,
|
|
22
|
+
interval_ms: interval_ms,
|
|
23
|
+
close_on_terminal: close_on_terminal
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Thin wrapper around the in-house generated Ruby SDK.
|
|
29
|
+
# Regenerate with: `pnpm openapi:gen:ruby`
|
|
30
|
+
class Phaseo
|
|
31
|
+
ACTIVE_MODEL_SOURCE_STATUSES = %w[active available].freeze
|
|
32
|
+
INACTIVE_MODEL_SOURCE_STATUSES = %w[
|
|
33
|
+
deprecated
|
|
34
|
+
retired
|
|
35
|
+
withheld
|
|
36
|
+
announced
|
|
37
|
+
rumoured
|
|
38
|
+
rumored
|
|
39
|
+
unavailable
|
|
40
|
+
disabled
|
|
41
|
+
internal
|
|
42
|
+
private
|
|
43
|
+
removed
|
|
44
|
+
sunset
|
|
45
|
+
eol
|
|
46
|
+
end_of_life
|
|
47
|
+
end-of-life
|
|
48
|
+
].freeze
|
|
49
|
+
|
|
50
|
+
attr_reader :raw_client, :async_jobs
|
|
51
|
+
|
|
52
|
+
def initialize(
|
|
53
|
+
api_key: nil,
|
|
54
|
+
base_path: "https://api.phaseo.app/v1",
|
|
55
|
+
enable_deprecation_warnings: true,
|
|
56
|
+
warnings_as_errors: false,
|
|
57
|
+
logger: nil,
|
|
58
|
+
lifecycle_resolver: nil,
|
|
59
|
+
devtools: nil
|
|
60
|
+
)
|
|
61
|
+
api_key ||= ENV["PHASEO_API_KEY"]
|
|
62
|
+
raise ArgumentError, "Missing API key. Pass api_key or set PHASEO_API_KEY." if api_key.to_s.empty?
|
|
63
|
+
|
|
64
|
+
@raw_client = ::Phaseo::Gen::Client.new(
|
|
65
|
+
base_url: base_path,
|
|
66
|
+
headers: { "Authorization" => "Bearer #{api_key}" }
|
|
67
|
+
)
|
|
68
|
+
@base_path = base_path.sub(%r{/+\z}, "")
|
|
69
|
+
@enable_deprecation_warnings = enable_deprecation_warnings
|
|
70
|
+
@warnings_as_errors = warnings_as_errors
|
|
71
|
+
@logger = logger
|
|
72
|
+
@lifecycle_resolver = lifecycle_resolver
|
|
73
|
+
@warned_models = {}
|
|
74
|
+
@model_lifecycle_cache = {}
|
|
75
|
+
@telemetry_recorder = TelemetryRecorder.new(devtools, "2.1.0")
|
|
76
|
+
@async_jobs = AsyncJobsResource.new(self)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def get_model_deprecation_info(model_id)
|
|
80
|
+
normalized = as_trimmed_string(model_id)
|
|
81
|
+
return nil unless normalized
|
|
82
|
+
return @model_lifecycle_cache[normalized] if @model_lifecycle_cache.key?(normalized)
|
|
83
|
+
|
|
84
|
+
info = resolve_model_lifecycle(normalized)
|
|
85
|
+
@model_lifecycle_cache[normalized] = info
|
|
86
|
+
info
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def validate_model(model_id)
|
|
90
|
+
info = get_model_deprecation_info(model_id)
|
|
91
|
+
return { ok: true, info: nil } unless info
|
|
92
|
+
unless is_model_requestable_for_inference?(info)
|
|
93
|
+
return { ok: false, info: info, reason: build_inactive_model_request_message(info) }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
{ ok: true, info: info }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def generate_text(payload)
|
|
100
|
+
with_lifecycle_and_telemetry(endpoint: "chat.completions", payload: payload, check_lifecycle: true) do
|
|
101
|
+
::Phaseo::Gen::Operations.createChatCompletion(@raw_client, body: payload)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def create_chat_completion(payload)
|
|
106
|
+
generate_text(payload)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def generate_response(payload)
|
|
110
|
+
with_lifecycle_and_telemetry(endpoint: "responses", payload: payload, check_lifecycle: true) do
|
|
111
|
+
::Phaseo::Gen::Operations.createResponse(@raw_client, body: payload)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def create_response(payload)
|
|
116
|
+
generate_response(payload)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def stream_response(payload)
|
|
120
|
+
@raw_client.request_stream(method:"post",path:"/responses",body:payload.merge(stream:true))
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def create_anthropic_message(payload)
|
|
124
|
+
with_lifecycle_and_telemetry(endpoint: "messages", payload: payload, check_lifecycle: true) do
|
|
125
|
+
::Phaseo::Gen::Operations.createAnthropicMessage(@raw_client, body: payload)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def generate_image(payload)
|
|
130
|
+
with_lifecycle_and_telemetry(endpoint: "images.generations", payload: payload, check_lifecycle: true) do
|
|
131
|
+
::Phaseo::Gen::Operations.createImage(@raw_client, body: payload)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def create_image(payload)
|
|
136
|
+
generate_image(payload)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def generate_video(payload)
|
|
140
|
+
with_lifecycle_and_telemetry(endpoint: "video.generations", payload: payload, check_lifecycle: true) do
|
|
141
|
+
::Phaseo::Gen::Operations.createVideo(@raw_client, body: payload)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def create_video(payload)
|
|
146
|
+
generate_video(payload)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def get_video(video_id)
|
|
150
|
+
with_lifecycle_and_telemetry(endpoint: "video.retrieve", payload: { "video_id" => video_id }, check_lifecycle: false) do
|
|
151
|
+
::Phaseo::Gen::Operations.getVideo(@raw_client, path: { "video_id" => video_id })
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def cancel_video(video_id)
|
|
156
|
+
with_lifecycle_and_telemetry(endpoint: "video.cancel", payload: { "video_id" => video_id }, check_lifecycle: false) do
|
|
157
|
+
::Phaseo::Gen::Operations.cancelVideo(@raw_client, path: { "video_id" => video_id })
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def delete_video(video_id)
|
|
162
|
+
with_lifecycle_and_telemetry(endpoint: "video.delete", payload: { "video_id" => video_id }, check_lifecycle: false) do
|
|
163
|
+
::Phaseo::Gen::Operations.deleteVideo(@raw_client, path: { "video_id" => video_id })
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def list_video_models
|
|
168
|
+
with_lifecycle_and_telemetry(endpoint: "video.models", payload: nil, check_lifecycle: false) do
|
|
169
|
+
::Phaseo::Gen::Operations.listVideoModels(@raw_client)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def list_videos(options = {})
|
|
174
|
+
with_lifecycle_and_telemetry(endpoint: "video.list", payload: options, check_lifecycle: false) do
|
|
175
|
+
::Phaseo::Gen::Operations.listVideos(@raw_client, query: options)
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def generate_image_edit(payload)
|
|
180
|
+
with_lifecycle_and_telemetry(endpoint: "images.edits", payload: payload, check_lifecycle: true) do
|
|
181
|
+
::Phaseo::Gen::Operations.createImageEdit(@raw_client, body: payload)
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def create_image_edit(payload)
|
|
186
|
+
generate_image_edit(payload)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def generate_embedding(payload)
|
|
190
|
+
with_lifecycle_and_telemetry(endpoint: "embeddings", payload: payload, check_lifecycle: true) do
|
|
191
|
+
::Phaseo::Gen::Operations.createEmbedding(@raw_client, body: payload)
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def create_embedding(payload)
|
|
196
|
+
generate_embedding(payload)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def generate_moderation(payload)
|
|
200
|
+
with_lifecycle_and_telemetry(endpoint: "moderations", payload: payload, check_lifecycle: true) do
|
|
201
|
+
::Phaseo::Gen::Operations.createModeration(@raw_client, body: payload)
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def create_moderation(payload)
|
|
206
|
+
generate_moderation(payload)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def generate_speech(payload)
|
|
210
|
+
with_lifecycle_and_telemetry(endpoint: "audio.speech", payload: payload, check_lifecycle: true) do
|
|
211
|
+
::Phaseo::Gen::Operations.createSpeech(@raw_client, body: payload)
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def create_speech(payload)
|
|
216
|
+
generate_speech(payload)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def generate_transcription(payload)
|
|
220
|
+
with_lifecycle_and_telemetry(endpoint: "audio.transcriptions", payload: payload, check_lifecycle: true) do
|
|
221
|
+
::Phaseo::Gen::Operations.createTranscription(@raw_client, body: payload)
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def create_transcription(payload)
|
|
226
|
+
generate_transcription(payload)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def generate_translation(payload)
|
|
230
|
+
with_lifecycle_and_telemetry(endpoint: "audio.translations", payload: payload, check_lifecycle: true) do
|
|
231
|
+
::Phaseo::Gen::Operations.createTranslation(@raw_client, body: payload)
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def create_translation(payload)
|
|
236
|
+
generate_translation(payload)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def create_batch(payload)
|
|
240
|
+
with_lifecycle_and_telemetry(endpoint: "batches.create", payload: payload, check_lifecycle: true) do
|
|
241
|
+
::Phaseo::Gen::Operations.createBatch(@raw_client, body: payload)
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def retrieve_batch(batch_id)
|
|
246
|
+
with_lifecycle_and_telemetry(endpoint: "batches.retrieve", payload: { "batch_id" => batch_id }, check_lifecycle: false) do
|
|
247
|
+
::Phaseo::Gen::Operations.retrieveBatch(@raw_client, path: { "batch_id" => batch_id })
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def cancel_batch(batch_id)
|
|
252
|
+
with_lifecycle_and_telemetry(endpoint: "batches.cancel", payload: { "batch_id" => batch_id }, check_lifecycle: false) do
|
|
253
|
+
::Phaseo::Gen::Operations.cancelBatch(@raw_client, path: { "batch_id" => batch_id })
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def get_async_job_websocket_url(kind, job_id, interval_ms: nil, close_on_terminal: nil)
|
|
258
|
+
normalized_kind = kind.to_s.strip
|
|
259
|
+
normalized_job_id = job_id.to_s.strip
|
|
260
|
+
raise ArgumentError, "kind is required" if normalized_kind.empty?
|
|
261
|
+
raise ArgumentError, "job_id is required" if normalized_job_id.empty?
|
|
262
|
+
|
|
263
|
+
uri = URI.parse(@base_path)
|
|
264
|
+
uri.scheme = uri.scheme == "https" ? "wss" : "ws"
|
|
265
|
+
uri.path = "#{uri.path.sub(%r{/+\z}, "")}/async/#{escape_path_segment(normalized_kind)}/#{escape_path_segment(normalized_job_id)}/ws"
|
|
266
|
+
query = {}
|
|
267
|
+
query["interval_ms"] = interval_ms.to_s unless interval_ms.nil?
|
|
268
|
+
query["close_on_terminal"] = close_on_terminal ? "true" : "false" unless close_on_terminal.nil?
|
|
269
|
+
uri.query = query.empty? ? nil : URI.encode_www_form(query)
|
|
270
|
+
uri.to_s
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def batch_websocket_url(batch_id, interval_ms: nil, close_on_terminal: nil)
|
|
274
|
+
get_async_job_websocket_url("batch", batch_id, interval_ms: interval_ms, close_on_terminal: close_on_terminal)
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def video_websocket_url(video_id, interval_ms: nil, close_on_terminal: nil)
|
|
278
|
+
get_async_job_websocket_url("video", video_id, interval_ms: interval_ms, close_on_terminal: close_on_terminal)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def escape_path_segment(value)
|
|
282
|
+
CGI.escape(value).gsub("+", "%20")
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
private :escape_path_segment
|
|
286
|
+
|
|
287
|
+
def list_files(options = {})
|
|
288
|
+
with_lifecycle_and_telemetry(endpoint: "files.list", payload: options, check_lifecycle: false) do
|
|
289
|
+
::Phaseo::Gen::Operations.listFiles(@raw_client, query: options)
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def retrieve_file(file_id)
|
|
294
|
+
with_lifecycle_and_telemetry(endpoint: "files.retrieve", payload: { "file_id" => file_id }, check_lifecycle: false) do
|
|
295
|
+
::Phaseo::Gen::Operations.retrieveFile(@raw_client, path: { "file_id" => file_id })
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def retrieve_file_content(file_id)
|
|
300
|
+
with_lifecycle_and_telemetry(endpoint: "files.content", payload: { "file_id" => file_id }, check_lifecycle: false) do
|
|
301
|
+
@raw_client.request_bytes(method: "get", path: "/files/#{file_id}/content")
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def retrieve_video_content(video_id)
|
|
306
|
+
with_lifecycle_and_telemetry(endpoint: "video.content", payload: { "video_id" => video_id }, check_lifecycle: false) do
|
|
307
|
+
@raw_client.request_bytes(method: "get", path: "/videos/#{video_id}/content")
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def get_video_download_url(video_id, params = {})
|
|
312
|
+
with_lifecycle_and_telemetry(
|
|
313
|
+
endpoint: "video.download_url",
|
|
314
|
+
payload: { "video_id" => video_id, "body" => params },
|
|
315
|
+
check_lifecycle: false
|
|
316
|
+
) do
|
|
317
|
+
@raw_client.request(method: "POST", path: "/videos/#{video_id}/download_url", body: params)
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def upload_file(payload)
|
|
322
|
+
with_lifecycle_and_telemetry(endpoint: "files.upload", payload: payload, check_lifecycle: false) do
|
|
323
|
+
::Phaseo::Gen::Operations.uploadFile(@raw_client, body: payload)
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def list_models(options = {})
|
|
328
|
+
with_lifecycle_and_telemetry(endpoint: "models.list", payload: options, check_lifecycle: false) do
|
|
329
|
+
::Phaseo::Gen::Operations.listModels(@raw_client, query: options)
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def list_providers(options = {})
|
|
334
|
+
with_lifecycle_and_telemetry(endpoint: "providers", payload: options, check_lifecycle: false) do
|
|
335
|
+
::Phaseo::Gen::Operations.listProviders(@raw_client, query: options)
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def get_analytics(options = {})
|
|
340
|
+
with_lifecycle_and_telemetry(endpoint: "analytics", payload: options, check_lifecycle: false) do
|
|
341
|
+
::Phaseo::Gen::Operations.getActivityAlias(@raw_client, query: options)
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def get_credits(options = {})
|
|
346
|
+
with_lifecycle_and_telemetry(endpoint: "credits", payload: options, check_lifecycle: false) do
|
|
347
|
+
::Phaseo::Gen::Operations.getCredits(@raw_client, query: options)
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def get_activity(options = {})
|
|
352
|
+
with_lifecycle_and_telemetry(endpoint: "activity", payload: options, check_lifecycle: false) do
|
|
353
|
+
::Phaseo::Gen::Operations.getActivity(@raw_client, query: options)
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def get_generation(generation_id)
|
|
358
|
+
with_lifecycle_and_telemetry(endpoint: "generations.retrieve", payload: { "id" => generation_id }, check_lifecycle: false) do
|
|
359
|
+
::Phaseo::Gen::Operations.getGeneration(@raw_client, query: { "id" => generation_id })
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def list_endpoints
|
|
364
|
+
with_lifecycle_and_telemetry(endpoint: "endpoints.list", payload: {}, check_lifecycle: false) do
|
|
365
|
+
::Phaseo::Gen::Operations.listEndpoints(@raw_client)
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def list_organisations(options = {})
|
|
370
|
+
with_lifecycle_and_telemetry(endpoint: "organisations.list", payload: options, check_lifecycle: false) do
|
|
371
|
+
::Phaseo::Gen::Operations.listOrganisations(@raw_client, query: options)
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def list_pricing_models(options = {})
|
|
376
|
+
with_lifecycle_and_telemetry(endpoint: "pricing.models", payload: options, check_lifecycle: false) do
|
|
377
|
+
::Phaseo::Gen::Operations.listPricingModels(@raw_client, query: options)
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def calculate_pricing(payload)
|
|
382
|
+
with_lifecycle_and_telemetry(endpoint: "pricing.calculate", payload: payload, check_lifecycle: false) do
|
|
383
|
+
::Phaseo::Gen::Operations.calculatePricing(@raw_client, body: payload)
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def list_api_keys(options = {})
|
|
388
|
+
with_lifecycle_and_telemetry(endpoint: "provisioning.keys.list", payload: options, check_lifecycle: false) do
|
|
389
|
+
::Phaseo::Gen::Operations.listApiKeys(@raw_client, query: options)
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
def create_api_key(payload)
|
|
394
|
+
with_lifecycle_and_telemetry(endpoint: "provisioning.keys.create", payload: payload, check_lifecycle: false) do
|
|
395
|
+
::Phaseo::Gen::Operations.createApiKey(@raw_client, body: payload)
|
|
396
|
+
end
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
def get_api_key(id)
|
|
400
|
+
with_lifecycle_and_telemetry(endpoint: "provisioning.keys.get", payload: { "id" => id }, check_lifecycle: false) do
|
|
401
|
+
::Phaseo::Gen::Operations.getApiKey(@raw_client, path: { "id" => id })
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
def update_api_key(id, payload)
|
|
406
|
+
with_lifecycle_and_telemetry(endpoint: "provisioning.keys.update", payload: { "id" => id, "body" => payload }, check_lifecycle: false) do
|
|
407
|
+
::Phaseo::Gen::Operations.updateApiKey(@raw_client, path: { "id" => id }, body: payload)
|
|
408
|
+
end
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
def delete_api_key(id)
|
|
412
|
+
with_lifecycle_and_telemetry(endpoint: "provisioning.keys.delete", payload: { "id" => id }, check_lifecycle: false) do
|
|
413
|
+
::Phaseo::Gen::Operations.deleteApiKey(@raw_client, path: { "id" => id })
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def list_workspaces(options = {})
|
|
418
|
+
with_lifecycle_and_telemetry(endpoint: "provisioning.workspaces.list", payload: options, check_lifecycle: false) do
|
|
419
|
+
::Phaseo::Gen::Operations.listWorkspaces(@raw_client, query: options)
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def get_workspace(id)
|
|
424
|
+
with_lifecycle_and_telemetry(endpoint: "provisioning.workspaces.get", payload: { "id" => id }, check_lifecycle: false) do
|
|
425
|
+
::Phaseo::Gen::Operations.getWorkspace(@raw_client, path: { "id" => id })
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
def create_workspace(payload)
|
|
430
|
+
with_lifecycle_and_telemetry(endpoint: "provisioning.workspaces.create", payload: payload, check_lifecycle: false) do
|
|
431
|
+
::Phaseo::Gen::Operations.createWorkspace(@raw_client, body: payload)
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
def update_workspace(id, payload)
|
|
436
|
+
with_lifecycle_and_telemetry(
|
|
437
|
+
endpoint: "provisioning.workspaces.update",
|
|
438
|
+
payload: { "id" => id, **payload },
|
|
439
|
+
check_lifecycle: false
|
|
440
|
+
) do
|
|
441
|
+
::Phaseo::Gen::Operations.updateWorkspace(@raw_client, path: { "id" => id }, body: payload)
|
|
442
|
+
end
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
def delete_workspace(id)
|
|
446
|
+
with_lifecycle_and_telemetry(endpoint: "provisioning.workspaces.delete", payload: { "id" => id }, check_lifecycle: false) do
|
|
447
|
+
::Phaseo::Gen::Operations.deleteWorkspace(@raw_client, path: { "id" => id })
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
def get_current_api_key
|
|
452
|
+
with_lifecycle_and_telemetry(endpoint: "key.current", payload: {}, check_lifecycle: false) do
|
|
453
|
+
::Phaseo::Gen::Operations.getCurrentApiKey(@raw_client)
|
|
454
|
+
end
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
def health
|
|
458
|
+
with_lifecycle_and_telemetry(endpoint: "health", payload: nil, check_lifecycle: false) do
|
|
459
|
+
@raw_client.request(method: "GET", path: "/health")
|
|
460
|
+
end
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
def healthz
|
|
464
|
+
health
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
private
|
|
468
|
+
|
|
469
|
+
def maybe_warn_for_payload(payload)
|
|
470
|
+
model_id = extract_model_id(payload)
|
|
471
|
+
return unless model_id
|
|
472
|
+
ensure_model_requestable(model_id)
|
|
473
|
+
maybe_warn_for_model(model_id)
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
def ensure_model_requestable(model_id)
|
|
477
|
+
normalized = as_trimmed_string(model_id)
|
|
478
|
+
return unless normalized
|
|
479
|
+
|
|
480
|
+
lifecycle = get_model_deprecation_info(normalized)
|
|
481
|
+
return unless lifecycle
|
|
482
|
+
return if is_model_requestable_for_inference?(lifecycle)
|
|
483
|
+
|
|
484
|
+
raise RuntimeError, build_inactive_model_request_message(lifecycle)
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
def with_lifecycle_and_telemetry(endpoint:, payload:, check_lifecycle:)
|
|
488
|
+
started_at = (Process.clock_gettime(Process::CLOCK_MONOTONIC) * 1000).to_i
|
|
489
|
+
begin
|
|
490
|
+
maybe_warn_for_payload(payload) if check_lifecycle
|
|
491
|
+
result = yield
|
|
492
|
+
duration = (Process.clock_gettime(Process::CLOCK_MONOTONIC) * 1000).to_i - started_at
|
|
493
|
+
@telemetry_recorder.capture_success(endpoint: endpoint, request: payload, response: result, duration_ms: duration)
|
|
494
|
+
result
|
|
495
|
+
rescue StandardError => e
|
|
496
|
+
duration = (Process.clock_gettime(Process::CLOCK_MONOTONIC) * 1000).to_i - started_at
|
|
497
|
+
@telemetry_recorder.capture_error(endpoint: endpoint, request: payload, error: e, duration_ms: duration)
|
|
498
|
+
raise
|
|
499
|
+
end
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
def maybe_warn_for_model(model_id)
|
|
503
|
+
return unless @enable_deprecation_warnings
|
|
504
|
+
normalized = as_trimmed_string(model_id)
|
|
505
|
+
return unless normalized
|
|
506
|
+
|
|
507
|
+
lifecycle = get_model_deprecation_info(normalized)
|
|
508
|
+
return unless lifecycle
|
|
509
|
+
return if lifecycle[:status] == "active"
|
|
510
|
+
|
|
511
|
+
message = lifecycle[:message] || build_lifecycle_message(
|
|
512
|
+
lifecycle[:status],
|
|
513
|
+
lifecycle[:model_id],
|
|
514
|
+
lifecycle[:deprecation_date],
|
|
515
|
+
lifecycle[:retirement_date],
|
|
516
|
+
lifecycle[:replacement_model_id]
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
raise RuntimeError, message if @warnings_as_errors
|
|
520
|
+
return if @warned_models[normalized]
|
|
521
|
+
@warned_models[normalized] = true
|
|
522
|
+
|
|
523
|
+
meta = {
|
|
524
|
+
model_id: lifecycle[:model_id],
|
|
525
|
+
status: lifecycle[:status],
|
|
526
|
+
deprecation_date: lifecycle[:deprecation_date],
|
|
527
|
+
retirement_date: lifecycle[:retirement_date],
|
|
528
|
+
replacement_model_id: lifecycle[:replacement_model_id]
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if @logger.respond_to?(:call)
|
|
532
|
+
@logger.call("warn", message, meta)
|
|
533
|
+
else
|
|
534
|
+
warn(message)
|
|
535
|
+
end
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
def resolve_model_lifecycle(model_id)
|
|
539
|
+
if @lifecycle_resolver.respond_to?(:call)
|
|
540
|
+
resolved = @lifecycle_resolver.call(model_id)
|
|
541
|
+
return normalize_hash(resolved)
|
|
542
|
+
end
|
|
543
|
+
fetch_model_lifecycle(model_id)
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
def fetch_model_lifecycle(model_id)
|
|
547
|
+
response = ::Phaseo::Gen::Operations.listModels(
|
|
548
|
+
@raw_client,
|
|
549
|
+
query: { "model_id" => model_id, "limit" => "1" }
|
|
550
|
+
)
|
|
551
|
+
decoded = normalize_hash(response)
|
|
552
|
+
return nil unless decoded
|
|
553
|
+
models = decoded[:models]
|
|
554
|
+
return nil unless models.is_a?(Array)
|
|
555
|
+
|
|
556
|
+
models.each do |entry|
|
|
557
|
+
model = normalize_hash(entry)
|
|
558
|
+
next unless model
|
|
559
|
+
next unless as_trimmed_string(model[:model_id]) == model_id
|
|
560
|
+
|
|
561
|
+
return to_model_lifecycle_info(model, model_id)
|
|
562
|
+
end
|
|
563
|
+
nil
|
|
564
|
+
rescue StandardError
|
|
565
|
+
nil
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
def to_model_lifecycle_info(model, fallback_model_id)
|
|
569
|
+
lifecycle = normalize_hash(model[:lifecycle]) || {}
|
|
570
|
+
model_id = first_non_empty(as_trimmed_string(model[:model_id]), fallback_model_id) || fallback_model_id
|
|
571
|
+
source_status = first_non_empty(as_trimmed_string(model[:status]), as_trimmed_string(lifecycle[:status]))
|
|
572
|
+
deprecation_date = first_non_empty(as_trimmed_string(lifecycle[:deprecation_date]), as_trimmed_string(model[:deprecation_date]))
|
|
573
|
+
retirement_date = first_non_empty(as_trimmed_string(lifecycle[:retirement_date]), as_trimmed_string(model[:retirement_date]))
|
|
574
|
+
status = normalize_lifecycle_status(
|
|
575
|
+
first_non_empty(as_trimmed_string(lifecycle[:status]), as_trimmed_string(model[:status])),
|
|
576
|
+
deprecation_date,
|
|
577
|
+
retirement_date
|
|
578
|
+
)
|
|
579
|
+
replacement_model_id = first_non_empty(as_trimmed_string(lifecycle[:replacement_model_id]))
|
|
580
|
+
message = first_non_empty(
|
|
581
|
+
as_trimmed_string(lifecycle[:message]),
|
|
582
|
+
build_lifecycle_message(status, model_id, deprecation_date, retirement_date, replacement_model_id)
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
{
|
|
586
|
+
model_id: model_id,
|
|
587
|
+
status: status,
|
|
588
|
+
source_status: source_status,
|
|
589
|
+
deprecation_date: deprecation_date,
|
|
590
|
+
retirement_date: retirement_date,
|
|
591
|
+
replacement_model_id: replacement_model_id,
|
|
592
|
+
message: message
|
|
593
|
+
}
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
def normalize_lifecycle_status(status, deprecation_date, retirement_date)
|
|
597
|
+
normalized = as_trimmed_string(status)&.downcase
|
|
598
|
+
return normalized if %w[active deprecated retired].include?(normalized)
|
|
599
|
+
|
|
600
|
+
now = Time.now.utc
|
|
601
|
+
retirement_time = parse_iso_time(retirement_date)
|
|
602
|
+
return "retired" if retirement_time && retirement_time <= now
|
|
603
|
+
|
|
604
|
+
deprecation_time = parse_iso_time(deprecation_date)
|
|
605
|
+
return "deprecated" if deprecation_time && deprecation_time <= now
|
|
606
|
+
|
|
607
|
+
"active"
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
def parse_iso_time(value)
|
|
611
|
+
trimmed = as_trimmed_string(value)
|
|
612
|
+
return nil unless trimmed
|
|
613
|
+
Time.iso8601(trimmed)
|
|
614
|
+
rescue ArgumentError
|
|
615
|
+
nil
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
def build_lifecycle_message(status, model_id, deprecation_date, retirement_date, replacement_model_id)
|
|
619
|
+
replacement = replacement_model_id ? %( Use "#{replacement_model_id}" instead.) : ""
|
|
620
|
+
if status == "retired"
|
|
621
|
+
return %[ [phaseo] Model "#{model_id}" is retired as of #{retirement_date}.#{replacement} ].strip if retirement_date
|
|
622
|
+
return %[ [phaseo] Model "#{model_id}" is retired.#{replacement} ].strip
|
|
623
|
+
end
|
|
624
|
+
if status == "deprecated"
|
|
625
|
+
return %[ [phaseo] Model "#{model_id}" is deprecated and scheduled for retirement on #{retirement_date}.#{replacement} ].strip if retirement_date
|
|
626
|
+
return %[ [phaseo] Model "#{model_id}" has been deprecated since #{deprecation_date}.#{replacement} ].strip if deprecation_date
|
|
627
|
+
return %[ [phaseo] Model "#{model_id}" is deprecated.#{replacement} ].strip
|
|
628
|
+
end
|
|
629
|
+
""
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
def normalize_source_status(value)
|
|
633
|
+
normalized = as_trimmed_string(value)
|
|
634
|
+
normalized&.downcase
|
|
635
|
+
end
|
|
636
|
+
|
|
637
|
+
def is_model_requestable_for_inference?(info)
|
|
638
|
+
return false unless info[:status] == "active"
|
|
639
|
+
|
|
640
|
+
source_status = normalize_source_status(info[:source_status])
|
|
641
|
+
return true unless source_status
|
|
642
|
+
return true if ACTIVE_MODEL_SOURCE_STATUSES.include?(source_status)
|
|
643
|
+
return false if INACTIVE_MODEL_SOURCE_STATUSES.include?(source_status)
|
|
644
|
+
|
|
645
|
+
false
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
def build_inactive_model_request_message(info)
|
|
649
|
+
if info[:status] != "active"
|
|
650
|
+
fallback = build_lifecycle_message(
|
|
651
|
+
info[:status] || "retired",
|
|
652
|
+
info[:model_id] || "unknown-model",
|
|
653
|
+
info[:deprecation_date],
|
|
654
|
+
info[:retirement_date],
|
|
655
|
+
info[:replacement_model_id]
|
|
656
|
+
)
|
|
657
|
+
return info[:message] if as_trimmed_string(info[:message])
|
|
658
|
+
return fallback if as_trimmed_string(fallback)
|
|
659
|
+
|
|
660
|
+
return %[ [phaseo] Model "#{info[:model_id]}" is not active for inference. ].strip
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
source_status = normalize_source_status(info[:source_status]) || "unknown"
|
|
664
|
+
replacement = info[:replacement_model_id] ? %( Use "#{info[:replacement_model_id]}" instead.) : ""
|
|
665
|
+
%[ [phaseo] Model "#{info[:model_id]}" is not active for inference (status: #{source_status}).#{replacement} ].strip
|
|
666
|
+
end
|
|
667
|
+
|
|
668
|
+
def extract_model_id(payload)
|
|
669
|
+
decoded = normalize_hash(payload)
|
|
670
|
+
return nil unless decoded
|
|
671
|
+
as_trimmed_string(decoded[:model])
|
|
672
|
+
end
|
|
673
|
+
|
|
674
|
+
def normalize_hash(value)
|
|
675
|
+
return symbolize_keys(value) if value.is_a?(Hash)
|
|
676
|
+
if value.is_a?(String)
|
|
677
|
+
parsed = JSON.parse(value)
|
|
678
|
+
return symbolize_keys(parsed) if parsed.is_a?(Hash)
|
|
679
|
+
end
|
|
680
|
+
if value.respond_to?(:to_h)
|
|
681
|
+
parsed = value.to_h
|
|
682
|
+
return symbolize_keys(parsed) if parsed.is_a?(Hash)
|
|
683
|
+
end
|
|
684
|
+
nil
|
|
685
|
+
rescue JSON::ParserError, TypeError
|
|
686
|
+
nil
|
|
687
|
+
end
|
|
688
|
+
|
|
689
|
+
def symbolize_keys(hash)
|
|
690
|
+
hash.each_with_object({}) do |(k, v), out|
|
|
691
|
+
key = k.is_a?(Symbol) ? k : k.to_s.to_sym
|
|
692
|
+
out[key] =
|
|
693
|
+
case v
|
|
694
|
+
when Hash
|
|
695
|
+
symbolize_keys(v)
|
|
696
|
+
when Array
|
|
697
|
+
v.map { |item| item.is_a?(Hash) ? symbolize_keys(item) : item }
|
|
698
|
+
else
|
|
699
|
+
v
|
|
700
|
+
end
|
|
701
|
+
end
|
|
702
|
+
end
|
|
703
|
+
|
|
704
|
+
def as_trimmed_string(value)
|
|
705
|
+
return nil if value.nil?
|
|
706
|
+
trimmed = value.to_s.strip
|
|
707
|
+
trimmed.empty? ? nil : trimmed
|
|
708
|
+
end
|
|
709
|
+
|
|
710
|
+
def first_non_empty(*values)
|
|
711
|
+
values.each do |value|
|
|
712
|
+
trimmed = as_trimmed_string(value)
|
|
713
|
+
return trimmed if trimmed
|
|
714
|
+
end
|
|
715
|
+
nil
|
|
716
|
+
end
|
|
717
|
+
end
|
|
718
|
+
|
|
719
|
+
module Devtools
|
|
720
|
+
module_function
|
|
721
|
+
|
|
722
|
+
def create(enabled: true, directory: nil, capture_headers: false, save_assets: true)
|
|
723
|
+
{
|
|
724
|
+
enabled: enabled,
|
|
725
|
+
directory: directory,
|
|
726
|
+
capture_headers: capture_headers,
|
|
727
|
+
save_assets: save_assets
|
|
728
|
+
}
|
|
729
|
+
end
|
|
730
|
+
end
|
|
731
|
+
|
|
732
|
+
class TelemetryRecorder
|
|
733
|
+
def initialize(config = nil, sdk_version = "2.1.0")
|
|
734
|
+
config ||= {}
|
|
735
|
+
enabled = config.fetch(:enabled, false)
|
|
736
|
+
directory = config.fetch(:directory, ".phaseo-devtools")
|
|
737
|
+
directory = ".phaseo-devtools" if directory.to_s.strip.empty?
|
|
738
|
+
|
|
739
|
+
env_enabled = ENV["PHASEO_DEVTOOLS"] || ENV["PHASEO_DEVTOOLS"]
|
|
740
|
+
unless env_enabled.to_s.strip.empty?
|
|
741
|
+
enabled = %w[1 true yes on].include?(env_enabled.to_s.strip.downcase)
|
|
742
|
+
end
|
|
743
|
+
|
|
744
|
+
env_directory = ENV["PHASEO_DEVTOOLS_DIR"] || ENV["PHASEO_DEVTOOLS_DIR"]
|
|
745
|
+
directory = env_directory.to_s.strip unless env_directory.to_s.strip.empty?
|
|
746
|
+
|
|
747
|
+
@enabled = enabled
|
|
748
|
+
@directory = directory
|
|
749
|
+
@capture_headers = config.fetch(:capture_headers, false)
|
|
750
|
+
@save_assets = config.fetch(:save_assets, true)
|
|
751
|
+
@sdk_version = sdk_version
|
|
752
|
+
|
|
753
|
+
return unless @enabled
|
|
754
|
+
ensure_layout
|
|
755
|
+
write_metadata_if_missing
|
|
756
|
+
end
|
|
757
|
+
|
|
758
|
+
def capture_success(endpoint:, request:, response:, duration_ms:)
|
|
759
|
+
return unless @enabled
|
|
760
|
+
|
|
761
|
+
metadata = {
|
|
762
|
+
sdk: "ruby",
|
|
763
|
+
sdk_version: @sdk_version,
|
|
764
|
+
stream: false
|
|
765
|
+
}
|
|
766
|
+
usage = extract_usage(response)
|
|
767
|
+
metadata[:usage] = usage if usage
|
|
768
|
+
|
|
769
|
+
model, provider = extract_model_provider(response, request)
|
|
770
|
+
metadata[:model] = model if model
|
|
771
|
+
metadata[:provider] = provider if provider
|
|
772
|
+
enrich_metadata_from_response!(metadata, normalize_hash(response))
|
|
773
|
+
metadata.delete(:headers) unless @capture_headers
|
|
774
|
+
|
|
775
|
+
entry = {
|
|
776
|
+
id: new_entry_id,
|
|
777
|
+
type: endpoint,
|
|
778
|
+
timestamp: (Time.now.to_f * 1000).to_i,
|
|
779
|
+
duration_ms: duration_ms,
|
|
780
|
+
request: normalize_json_value(request),
|
|
781
|
+
response: normalize_json_value(response),
|
|
782
|
+
error: nil,
|
|
783
|
+
metadata: metadata
|
|
784
|
+
}
|
|
785
|
+
append_entry(entry)
|
|
786
|
+
end
|
|
787
|
+
|
|
788
|
+
def capture_error(endpoint:, request:, error:, duration_ms:)
|
|
789
|
+
return unless @enabled
|
|
790
|
+
|
|
791
|
+
error_response = extract_error_response(error)
|
|
792
|
+
model, provider = extract_model_provider(nil, request)
|
|
793
|
+
metadata = {
|
|
794
|
+
sdk: "ruby",
|
|
795
|
+
sdk_version: @sdk_version,
|
|
796
|
+
stream: false
|
|
797
|
+
}
|
|
798
|
+
metadata[:model] = model if model
|
|
799
|
+
metadata[:provider] = provider if provider
|
|
800
|
+
enrich_metadata_from_response!(metadata, error_response)
|
|
801
|
+
|
|
802
|
+
entry = {
|
|
803
|
+
id: new_entry_id,
|
|
804
|
+
type: endpoint,
|
|
805
|
+
timestamp: (Time.now.to_f * 1000).to_i,
|
|
806
|
+
duration_ms: duration_ms,
|
|
807
|
+
request: normalize_json_value(request),
|
|
808
|
+
response: normalize_json_value(error_response),
|
|
809
|
+
error: {
|
|
810
|
+
message: error.message,
|
|
811
|
+
status_code: extract_error_status_code(error)
|
|
812
|
+
}.compact,
|
|
813
|
+
metadata: metadata
|
|
814
|
+
}
|
|
815
|
+
append_entry(entry)
|
|
816
|
+
end
|
|
817
|
+
|
|
818
|
+
private
|
|
819
|
+
|
|
820
|
+
def append_entry(entry)
|
|
821
|
+
ensure_layout
|
|
822
|
+
File.open(File.join(@directory, "generations.jsonl"), "a:utf-8") do |file|
|
|
823
|
+
file.puts(JSON.generate(entry))
|
|
824
|
+
end
|
|
825
|
+
rescue StandardError
|
|
826
|
+
nil
|
|
827
|
+
end
|
|
828
|
+
|
|
829
|
+
def ensure_layout
|
|
830
|
+
FileUtils.mkdir_p(@directory)
|
|
831
|
+
return unless @save_assets
|
|
832
|
+
FileUtils.mkdir_p(File.join(@directory, "assets", "images"))
|
|
833
|
+
FileUtils.mkdir_p(File.join(@directory, "assets", "audio"))
|
|
834
|
+
FileUtils.mkdir_p(File.join(@directory, "assets", "video"))
|
|
835
|
+
end
|
|
836
|
+
|
|
837
|
+
def write_metadata_if_missing
|
|
838
|
+
path = File.join(@directory, "metadata.json")
|
|
839
|
+
return if File.exist?(path)
|
|
840
|
+
|
|
841
|
+
payload = {
|
|
842
|
+
session_id: new_entry_id,
|
|
843
|
+
started_at: (Time.now.to_f * 1000).to_i,
|
|
844
|
+
sdk: "ruby",
|
|
845
|
+
sdk_version: @sdk_version,
|
|
846
|
+
platform: RUBY_PLATFORM,
|
|
847
|
+
ruby_version: RUBY_VERSION
|
|
848
|
+
}
|
|
849
|
+
File.write(path, JSON.pretty_generate(payload))
|
|
850
|
+
rescue StandardError
|
|
851
|
+
nil
|
|
852
|
+
end
|
|
853
|
+
|
|
854
|
+
def extract_usage(response)
|
|
855
|
+
payload = normalize_hash(response)
|
|
856
|
+
return nil unless payload
|
|
857
|
+
usage = payload[:usage]
|
|
858
|
+
return nil unless usage.is_a?(Hash)
|
|
859
|
+
|
|
860
|
+
prompt = usage[:prompt_tokens] || usage[:input_tokens]
|
|
861
|
+
completion = usage[:completion_tokens] || usage[:output_tokens]
|
|
862
|
+
total = usage[:total_tokens]
|
|
863
|
+
|
|
864
|
+
out = {}
|
|
865
|
+
out[:prompt_tokens] = prompt unless prompt.nil?
|
|
866
|
+
out[:completion_tokens] = completion unless completion.nil?
|
|
867
|
+
out[:total_tokens] = total unless total.nil?
|
|
868
|
+
out.empty? ? nil : out
|
|
869
|
+
end
|
|
870
|
+
|
|
871
|
+
def extract_model_provider(response, request)
|
|
872
|
+
response_payload = normalize_hash(response) || {}
|
|
873
|
+
request_payload = normalize_hash(request) || {}
|
|
874
|
+
|
|
875
|
+
model = as_trimmed_string(response_payload[:model]) || as_trimmed_string(request_payload[:model])
|
|
876
|
+
provider = as_trimmed_string(response_payload[:provider])
|
|
877
|
+
[model, provider]
|
|
878
|
+
end
|
|
879
|
+
|
|
880
|
+
def extract_error_response(error)
|
|
881
|
+
if error.is_a?(::Phaseo::Gen::RequestError)
|
|
882
|
+
parsed = normalize_hash(error.response_body)
|
|
883
|
+
return parsed if parsed
|
|
884
|
+
return {
|
|
885
|
+
status_code: error.status_code,
|
|
886
|
+
error: as_trimmed_string(error.response_body)
|
|
887
|
+
}.compact
|
|
888
|
+
end
|
|
889
|
+
|
|
890
|
+
response = error.respond_to?(:response) ? error.response : nil
|
|
891
|
+
return symbolize_keys(response) if response.is_a?(Hash)
|
|
892
|
+
|
|
893
|
+
body = error.respond_to?(:body) ? error.body : nil
|
|
894
|
+
normalize_hash(body)
|
|
895
|
+
end
|
|
896
|
+
|
|
897
|
+
def extract_error_status_code(error)
|
|
898
|
+
return error.status_code if error.is_a?(::Phaseo::Gen::RequestError)
|
|
899
|
+
return error.status_code if error.respond_to?(:status_code)
|
|
900
|
+
nil
|
|
901
|
+
end
|
|
902
|
+
|
|
903
|
+
def enrich_metadata_from_response!(metadata, payload)
|
|
904
|
+
return unless payload.is_a?(Hash)
|
|
905
|
+
|
|
906
|
+
%i[
|
|
907
|
+
request_id
|
|
908
|
+
session_id
|
|
909
|
+
upstream_request_id
|
|
910
|
+
native_response_id
|
|
911
|
+
status_code
|
|
912
|
+
latency_ms
|
|
913
|
+
generation_ms
|
|
914
|
+
throughput
|
|
915
|
+
provider_attempts
|
|
916
|
+
pricing_lines
|
|
917
|
+
request_counts
|
|
918
|
+
billing
|
|
919
|
+
].each do |key|
|
|
920
|
+
metadata[key] = payload[key] if payload.key?(key) && !payload[key].nil?
|
|
921
|
+
end
|
|
922
|
+
|
|
923
|
+
finish_reason = payload[:finish_reason] || payload[:stop_reason]
|
|
924
|
+
metadata[:finish_reason] = finish_reason if finish_reason
|
|
925
|
+
end
|
|
926
|
+
|
|
927
|
+
def normalize_json_value(value)
|
|
928
|
+
JSON.parse(JSON.generate(value), symbolize_names: false)
|
|
929
|
+
rescue StandardError
|
|
930
|
+
value.to_s
|
|
931
|
+
end
|
|
932
|
+
|
|
933
|
+
def normalize_hash(value)
|
|
934
|
+
return symbolize_keys(value) if value.is_a?(Hash)
|
|
935
|
+
if value.is_a?(String)
|
|
936
|
+
parsed = JSON.parse(value)
|
|
937
|
+
return symbolize_keys(parsed) if parsed.is_a?(Hash)
|
|
938
|
+
end
|
|
939
|
+
if value.respond_to?(:to_h)
|
|
940
|
+
parsed = value.to_h
|
|
941
|
+
return symbolize_keys(parsed) if parsed.is_a?(Hash)
|
|
942
|
+
end
|
|
943
|
+
nil
|
|
944
|
+
rescue JSON::ParserError, TypeError
|
|
945
|
+
nil
|
|
946
|
+
end
|
|
947
|
+
|
|
948
|
+
def symbolize_keys(hash)
|
|
949
|
+
hash.each_with_object({}) do |(k, v), out|
|
|
950
|
+
key = k.is_a?(Symbol) ? k : k.to_s.to_sym
|
|
951
|
+
out[key] =
|
|
952
|
+
case v
|
|
953
|
+
when Hash
|
|
954
|
+
symbolize_keys(v)
|
|
955
|
+
when Array
|
|
956
|
+
v.map { |item| item.is_a?(Hash) ? symbolize_keys(item) : item }
|
|
957
|
+
else
|
|
958
|
+
v
|
|
959
|
+
end
|
|
960
|
+
end
|
|
961
|
+
end
|
|
962
|
+
|
|
963
|
+
def as_trimmed_string(value)
|
|
964
|
+
return nil if value.nil?
|
|
965
|
+
trimmed = value.to_s.strip
|
|
966
|
+
trimmed.empty? ? nil : trimmed
|
|
967
|
+
end
|
|
968
|
+
|
|
969
|
+
def new_entry_id
|
|
970
|
+
"#{(Time.now.to_f * 1000).to_i}-#{SecureRandom.hex(4)}"
|
|
971
|
+
end
|
|
972
|
+
end
|
|
973
|
+
|
|
974
|
+
end
|