runapi-core 0.2.8 → 0.2.9
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 +4 -4
- data/README.md +16 -17
- data/lib/runapi/core/base_model.rb +17 -1
- data/lib/runapi/core/errors.rb +23 -2
- data/lib/runapi/core/http_client.rb +13 -1
- data/lib/runapi/core/polling.rb +20 -3
- data/lib/runapi/core/resource_helpers.rb +16 -2
- data/lib/runapi/core/response.rb +87 -0
- data/lib/runapi/core/types.rb +3 -1
- data/lib/runapi/core/version.rb +1 -1
- data/lib/runapi/core.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 952bc04d7631dd26161b8f055e0711a4574bf017f9ebf7c5a871c67af1a5c7dd
|
|
4
|
+
data.tar.gz: 44c1263fbe06f435878d2b2e7da5595921e1d4c3de5d2da789d4552c9b37884e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5f89ff25ea6c94689ce1fde2741a774ae7d0f5db82bd083284846136922518d5029bee0d918fa451cdf231a1d6b0523a1b123ef363cdefb90a3ab71156ec6c27
|
|
7
|
+
data.tar.gz: e72c1b9336e37e6abba3d701fd6bd9a954a773158d091c428415750bb4521031d1138be18eed217b9d109adcede94e5c4a7e36e2946f566491654700f9d1fedf
|
data/README.md
CHANGED
|
@@ -10,33 +10,32 @@ gem install runapi-core
|
|
|
10
10
|
|
|
11
11
|
## Notes
|
|
12
12
|
|
|
13
|
-
Use the core gem for common client options, error classes, request helpers, and task polling behavior that
|
|
13
|
+
Use the core gem for common client options, error classes, request helpers, and task polling behavior that Provider Clients share. Public SDK docs live at https://runapi.ai/docs#runapi-sdks and the model catalog lives at https://runapi.ai/models.
|
|
14
14
|
|
|
15
15
|
## Request Identifiers
|
|
16
16
|
|
|
17
17
|
RunAPI accepts an optional `X-Client-Request-Id` header on public API calls. Use printable ASCII values up to 512 characters. Accepted values are echoed in the response and stored with the RunAPI task for support and reconciliation.
|
|
18
18
|
|
|
19
|
-
High-level Ruby
|
|
19
|
+
High-level Ruby Provider Client resource methods accept per-request options and keep response headers on the returned model object. This example uses the Suno Provider Client; install `runapi-suno` to run it.
|
|
20
20
|
|
|
21
21
|
```ruby
|
|
22
|
-
require "
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
request["X-Client-Request-Id"] = "order-123"
|
|
31
|
-
request.body = JSON.generate({
|
|
22
|
+
require "runapi/suno"
|
|
23
|
+
|
|
24
|
+
client = RunApi::Suno::Client.new(api_key: ENV.fetch("RUNAPI_API_KEY"))
|
|
25
|
+
options = RunApi::Core::RequestOptions.new(
|
|
26
|
+
headers: {"X-Client-Request-Id" => "order-123"}
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
response = client.text_to_music.create(
|
|
32
30
|
prompt: "A chill lo-fi beat",
|
|
33
31
|
model: "suno-v4.5-plus",
|
|
34
|
-
vocal_mode: "instrumental"
|
|
35
|
-
|
|
32
|
+
vocal_mode: "instrumental",
|
|
33
|
+
options: options
|
|
34
|
+
)
|
|
36
35
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
runapi_task_id = response.runapi_task_id
|
|
37
|
+
# Equivalent case-insensitive lookup:
|
|
38
|
+
runapi_task_id = response.response_headers["X-RunAPI-Task-Id"]
|
|
40
39
|
```
|
|
41
40
|
|
|
42
41
|
## File Upload
|
|
@@ -40,7 +40,7 @@ module RunApi
|
|
|
40
40
|
nil
|
|
41
41
|
when BaseModel
|
|
42
42
|
if target_model && target_model <= BaseModel && !value.is_a?(target_model)
|
|
43
|
-
target_model.from_hash(value.to_h)
|
|
43
|
+
target_model.from_hash(value.to_h).with_response_headers(value.response_headers)
|
|
44
44
|
else
|
|
45
45
|
value
|
|
46
46
|
end
|
|
@@ -87,11 +87,27 @@ module RunApi
|
|
|
87
87
|
def initialize(attributes = {})
|
|
88
88
|
source = normalize_input(attributes)
|
|
89
89
|
@attributes = {}
|
|
90
|
+
@response_headers = ResponseHeaders.new
|
|
90
91
|
|
|
91
92
|
assign_declared_fields!(source)
|
|
92
93
|
assign_extra_fields!(source)
|
|
93
94
|
end
|
|
94
95
|
|
|
96
|
+
attr_reader :response_headers
|
|
97
|
+
|
|
98
|
+
def response_header(name)
|
|
99
|
+
response_headers[name]
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def runapi_task_id
|
|
103
|
+
response_header("X-RunAPI-Task-Id")
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def with_response_headers(headers)
|
|
107
|
+
@response_headers = headers.is_a?(ResponseHeaders) ? headers : ResponseHeaders.new(headers)
|
|
108
|
+
self
|
|
109
|
+
end
|
|
110
|
+
|
|
95
111
|
def [](key)
|
|
96
112
|
@attributes[key.to_s]
|
|
97
113
|
end
|
data/lib/runapi/core/errors.rb
CHANGED
|
@@ -13,12 +13,23 @@ module RunApi
|
|
|
13
13
|
attr_reader :request_id
|
|
14
14
|
# @return [Hash, String, nil] Parsed response body or error details.
|
|
15
15
|
attr_reader :details
|
|
16
|
+
# @return [ResponseHeaders] HTTP response headers when available.
|
|
17
|
+
attr_reader :response_headers
|
|
16
18
|
|
|
17
|
-
def initialize(message = nil, status: nil, request_id: nil, details: nil)
|
|
19
|
+
def initialize(message = nil, status: nil, request_id: nil, details: nil, response_headers: nil)
|
|
18
20
|
super(message)
|
|
19
21
|
@status = status
|
|
20
22
|
@request_id = request_id
|
|
21
23
|
@details = details
|
|
24
|
+
@response_headers = response_headers.is_a?(ResponseHeaders) ? response_headers : ResponseHeaders.new(response_headers)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def response_header(name)
|
|
28
|
+
response_headers[name]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def runapi_task_id
|
|
32
|
+
response_header("X-RunAPI-Task-Id")
|
|
22
33
|
end
|
|
23
34
|
|
|
24
35
|
def to_h
|
|
@@ -71,6 +82,7 @@ module RunApi
|
|
|
71
82
|
def from_response(response, body = nil)
|
|
72
83
|
status = response.code.to_i
|
|
73
84
|
request_id = response["x-request-id"]
|
|
85
|
+
headers = response_headers(response)
|
|
74
86
|
|
|
75
87
|
parsed_body = parse_body(body)
|
|
76
88
|
message = extract_message(parsed_body) ||
|
|
@@ -89,7 +101,8 @@ module RunApi
|
|
|
89
101
|
kwargs = {
|
|
90
102
|
status: status,
|
|
91
103
|
request_id: request_id,
|
|
92
|
-
details: parsed_body
|
|
104
|
+
details: parsed_body,
|
|
105
|
+
response_headers: headers
|
|
93
106
|
}
|
|
94
107
|
kwargs[:retry_after] = retry_after if error_class == RateLimitError
|
|
95
108
|
|
|
@@ -147,6 +160,14 @@ module RunApi
|
|
|
147
160
|
nil
|
|
148
161
|
end
|
|
149
162
|
end
|
|
163
|
+
|
|
164
|
+
def response_headers(response)
|
|
165
|
+
return {} unless response.respond_to?(:each_header)
|
|
166
|
+
|
|
167
|
+
headers = {}
|
|
168
|
+
response.each_header { |key, value| headers[key] = value }
|
|
169
|
+
headers
|
|
170
|
+
end
|
|
150
171
|
end
|
|
151
172
|
end
|
|
152
173
|
|
|
@@ -37,7 +37,13 @@ module RunApi
|
|
|
37
37
|
raise NetworkError, e.message
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
if response.is_a?(Net::HTTPSuccess)
|
|
41
|
+
body = parse_body(response.body)
|
|
42
|
+
return nil if body.nil?
|
|
43
|
+
return body unless body.is_a?(Hash) || body.is_a?(Array)
|
|
44
|
+
|
|
45
|
+
return Response.new(body:, headers: response_headers(response))
|
|
46
|
+
end
|
|
41
47
|
|
|
42
48
|
error = Error.from_response(response, response.body)
|
|
43
49
|
|
|
@@ -156,6 +162,12 @@ module RunApi
|
|
|
156
162
|
rescue JSON::ParserError
|
|
157
163
|
body
|
|
158
164
|
end
|
|
165
|
+
|
|
166
|
+
def response_headers(response)
|
|
167
|
+
headers = {}
|
|
168
|
+
response.each_header { |key, value| headers[key] = value }
|
|
169
|
+
headers
|
|
170
|
+
end
|
|
159
171
|
end
|
|
160
172
|
end
|
|
161
173
|
end
|
data/lib/runapi/core/polling.rb
CHANGED
|
@@ -16,15 +16,27 @@ module RunApi
|
|
|
16
16
|
|
|
17
17
|
if status == "failed"
|
|
18
18
|
message = value_for(response, "error") || "Task failed"
|
|
19
|
-
raise TaskFailedError.new(
|
|
19
|
+
raise TaskFailedError.new(
|
|
20
|
+
message,
|
|
21
|
+
details: details_for(response),
|
|
22
|
+
response_headers: response_headers_for(response)
|
|
23
|
+
)
|
|
20
24
|
end
|
|
21
25
|
|
|
22
26
|
if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
23
|
-
raise TaskTimeoutError
|
|
27
|
+
raise TaskTimeoutError.new(
|
|
28
|
+
"Task polling timed out after #{options.max_wait}s",
|
|
29
|
+
details: details_for(response),
|
|
30
|
+
response_headers: response_headers_for(response)
|
|
31
|
+
)
|
|
24
32
|
end
|
|
25
33
|
|
|
26
34
|
unless ACTIVE_STATUSES.include?(status)
|
|
27
|
-
raise TaskFailedError.new(
|
|
35
|
+
raise TaskFailedError.new(
|
|
36
|
+
"Unknown task status: #{status}",
|
|
37
|
+
details: details_for(response),
|
|
38
|
+
response_headers: response_headers_for(response)
|
|
39
|
+
)
|
|
28
40
|
end
|
|
29
41
|
|
|
30
42
|
sleep(options.poll_interval)
|
|
@@ -45,6 +57,11 @@ module RunApi
|
|
|
45
57
|
response.is_a?(Core::BaseModel) ? response.to_h : response
|
|
46
58
|
end
|
|
47
59
|
private_class_method :details_for
|
|
60
|
+
|
|
61
|
+
def self.response_headers_for(response)
|
|
62
|
+
response.response_headers if response.respond_to?(:response_headers)
|
|
63
|
+
end
|
|
64
|
+
private_class_method :response_headers_for
|
|
48
65
|
end
|
|
49
66
|
end
|
|
50
67
|
end
|
|
@@ -20,7 +20,10 @@ module RunApi
|
|
|
20
20
|
@http.request(method, path, **kwargs)
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
Core::
|
|
23
|
+
payload = response.is_a?(Core::Response) ? response.body : response
|
|
24
|
+
result = Core::BaseModel.coerce(payload, as: response_class)
|
|
25
|
+
attach_response_headers(result, response.response_headers) if response.is_a?(Core::Response)
|
|
26
|
+
result
|
|
24
27
|
end
|
|
25
28
|
|
|
26
29
|
def compact_params(params)
|
|
@@ -205,7 +208,18 @@ module RunApi
|
|
|
205
208
|
return response if response.is_a?(completed_class)
|
|
206
209
|
|
|
207
210
|
payload = response.is_a?(Core::BaseModel) ? response.to_h : response
|
|
208
|
-
completed_class.from_hash(payload)
|
|
211
|
+
completed = completed_class.from_hash(payload)
|
|
212
|
+
completed.with_response_headers(response.response_headers) if response.is_a?(Core::BaseModel)
|
|
213
|
+
completed
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def attach_response_headers(result, headers)
|
|
217
|
+
case result
|
|
218
|
+
when Core::BaseModel
|
|
219
|
+
result.with_response_headers(headers)
|
|
220
|
+
when Array
|
|
221
|
+
result.each { |item| attach_response_headers(item, headers) }
|
|
222
|
+
end
|
|
209
223
|
end
|
|
210
224
|
end
|
|
211
225
|
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RunApi
|
|
4
|
+
module Core
|
|
5
|
+
class ResponseHeaders
|
|
6
|
+
include Enumerable
|
|
7
|
+
|
|
8
|
+
def initialize(headers = {})
|
|
9
|
+
@headers = {}
|
|
10
|
+
headers&.each { |key, value| @headers[normalize(key)] = value }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def [](key)
|
|
14
|
+
@headers[normalize(key)]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def fetch(key, *fallback, &block)
|
|
18
|
+
@headers.fetch(normalize(key), *fallback, &block)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def key?(key)
|
|
22
|
+
@headers.key?(normalize(key))
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def each(&block)
|
|
26
|
+
@headers.each(&block)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def empty?
|
|
30
|
+
@headers.empty?
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def to_h
|
|
34
|
+
@headers.dup
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def normalize(key)
|
|
40
|
+
key.to_s.downcase
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
class Response < Hash
|
|
45
|
+
attr_reader :response_headers
|
|
46
|
+
|
|
47
|
+
def initialize(body:, headers: nil)
|
|
48
|
+
super()
|
|
49
|
+
@body = body unless body.is_a?(Hash)
|
|
50
|
+
@response_headers = headers.is_a?(ResponseHeaders) ? headers : ResponseHeaders.new(headers)
|
|
51
|
+
update(body) if body.is_a?(Hash)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
alias_method :headers, :response_headers
|
|
55
|
+
|
|
56
|
+
def body
|
|
57
|
+
@body.nil? ? self : @body
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def [](key)
|
|
61
|
+
return @body[key] if @body.respond_to?(:[])
|
|
62
|
+
|
|
63
|
+
super
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def dig(*keys)
|
|
67
|
+
return @body.dig(*keys) if @body.respond_to?(:dig)
|
|
68
|
+
|
|
69
|
+
super
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def to_h
|
|
73
|
+
return @body.to_h if !@body.nil? && @body.respond_to?(:to_h)
|
|
74
|
+
|
|
75
|
+
super
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def ==(other)
|
|
79
|
+
if @body
|
|
80
|
+
@body == (other.is_a?(Response) ? other.body : other)
|
|
81
|
+
else
|
|
82
|
+
super(other.is_a?(Response) ? other.to_h : other)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
data/lib/runapi/core/types.rb
CHANGED
|
@@ -62,7 +62,9 @@ module RunApi
|
|
|
62
62
|
#
|
|
63
63
|
# @example
|
|
64
64
|
# client.text_to_image.run(
|
|
65
|
-
#
|
|
65
|
+
# prompt: "A sunset",
|
|
66
|
+
# model: "z-image",
|
|
67
|
+
# aspect_ratio: "1:1",
|
|
66
68
|
# options: RequestOptions.new(timeout: 30, headers: { "X-Custom" => "value" })
|
|
67
69
|
# )
|
|
68
70
|
#
|
data/lib/runapi/core/version.rb
CHANGED
data/lib/runapi/core.rb
CHANGED
|
@@ -10,6 +10,7 @@ require "connection_pool"
|
|
|
10
10
|
require_relative "core/version"
|
|
11
11
|
require_relative "core/constants"
|
|
12
12
|
require_relative "core/configuration"
|
|
13
|
+
require_relative "core/response"
|
|
13
14
|
require_relative "core/base_model"
|
|
14
15
|
require_relative "core/types"
|
|
15
16
|
require_relative "core/errors"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: runapi-core
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.9
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- RunAPI
|
|
@@ -50,6 +50,7 @@ files:
|
|
|
50
50
|
- lib/runapi/core/multipart_body.rb
|
|
51
51
|
- lib/runapi/core/polling.rb
|
|
52
52
|
- lib/runapi/core/resource_helpers.rb
|
|
53
|
+
- lib/runapi/core/response.rb
|
|
53
54
|
- lib/runapi/core/types.rb
|
|
54
55
|
- lib/runapi/core/version.rb
|
|
55
56
|
homepage: https://runapi.ai/models
|