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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 66cc8586f64564c496bb7c4008f5cbaf0a30d1b1528cea036db368a3f9bdc307
4
- data.tar.gz: 43cb58a1644f09209e1da2e14c6128cf1a334fa245d0d285499b5e4e892a411f
3
+ metadata.gz: 952bc04d7631dd26161b8f055e0711a4574bf017f9ebf7c5a871c67af1a5c7dd
4
+ data.tar.gz: 44c1263fbe06f435878d2b2e7da5595921e1d4c3de5d2da789d4552c9b37884e
5
5
  SHA512:
6
- metadata.gz: 957bcd8962bcde64fd16281a2b21876ba93b505546277d5e61a0bd898cb9b1ec6369b34ffcbeb0b79df42e5a56fbe1ff1dc8531872f8cd29b49375c5c46a37a7
7
- data.tar.gz: 43261e4cdcaef883977a9efc32ed752b9000b05602671cabc7471b000a2f01065666746ae09c4be7eeceae622cb7162f84562170eb22214b11b4b1c483e43b0d
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 model SDKs share. Public SDK docs live at https://runapi.ai/docs#runapi-sdks and the model catalog lives at https://runapi.ai/models.
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 model SDK methods currently return parsed response bodies. When an integration needs to send a client request id or read `X-RunAPI-Task-Id`, make the call through direct HTTP or a custom transport so response headers stay available.
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 "json"
23
- require "net/http"
24
- require "uri"
25
-
26
- uri = URI("https://runapi.ai/api/v1/suno/text_to_music")
27
- request = Net::HTTP::Post.new(uri)
28
- request["Authorization"] = "Bearer #{ENV.fetch("RUNAPI_API_KEY")}"
29
- request["Content-Type"] = "application/json"
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
- response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(request) }
38
- runapi_task_id = response["X-RunAPI-Task-Id"]
39
- body = JSON.parse(response.body)
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
@@ -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
- return parse_body(response.body) if response.is_a?(Net::HTTPSuccess)
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
@@ -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(message, details: details_for(response))
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, "Task polling timed out after #{options.max_wait}s"
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("Unknown task status: #{status}", details: details_for(response))
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::BaseModel.coerce(response, as: response_class)
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
@@ -62,7 +62,9 @@ module RunApi
62
62
  #
63
63
  # @example
64
64
  # client.text_to_image.run(
65
- # { prompt: "A sunset" },
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
  #
@@ -2,7 +2,7 @@
2
2
 
3
3
  module RunApi
4
4
  module Core
5
- VERSION = "0.2.8"
5
+ VERSION = "0.2.9"
6
6
  end
7
7
 
8
8
  VERSION = Core::VERSION
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.8
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