runapi-core 0.2.15 → 0.3.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: 5f91d61d251ad17e65d85bdc82c70abe8c6b9aa607b668667ccf63369434ab1f
4
- data.tar.gz: '08055ced6c024b4e543a95907dd57c196fa05ade62772e090492ea8f019ceefa'
3
+ metadata.gz: 9a0bc423a77bb7770c6db1e3a6fa10ae1679ef58f7762231b02c28e86f85b4e3
4
+ data.tar.gz: fce5aa1e8e03dfc0c3c873a6f499bd5c51b173b87f91b75a74955413f59b19e2
5
5
  SHA512:
6
- metadata.gz: 777f30e49b6cc9d82084ad707577008002e983ef129d643da5e26849bb816af7c33bb35cd9087897d475f389d1a33c35bc5be694e4990f08366f50bcc36163f2
7
- data.tar.gz: 5c7355e6c06a8867addb06233e0eecb644bd740c90d5f552ad422e77c6cb4a9088b1d7b8c0eea1a9e6bc401b4eb96f2234ca805ab5a6bcf166867491e6bb699e
6
+ metadata.gz: fc56fca2936974941318607a19905d77542bf56f529fdf68577a7da9b18bc186cdbe4a3ea45f367b1d64d8f48622f384cfc8ef089503a1fdf87df399ed6dc84f
7
+ data.tar.gz: 548078f4a0b851f5a15f3f76fbadfd5164a122100c0842921bf9e6efa572049b3b715fbfdec7f82a8b7cdfe7773d0565a46ae648772cc75304c1f9a06c348838
data/README.md CHANGED
@@ -12,10 +12,36 @@ gem install runapi-core
12
12
 
13
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
+ ## Live Pricing
16
+
17
+ Every Provider Client exposes `pricing`. Schedule lookup and quotes use the live API response and do not retain a local price cache.
18
+
19
+ ```ruby
20
+ schedules = client.pricing.list(service: "suno")
21
+ quote = client.pricing.quote(
22
+ service: "suno",
23
+ action: "convert_audio",
24
+ params: {}
25
+ )
26
+ ```
27
+
28
+ Public schedule lookup and quotes that use only public parameters work without an API key. Pass a standard API key when a quote references an account-owned source Task.
29
+
30
+ Use `PricingClient` when pricing is the only capability needed:
31
+
32
+ ```ruby
33
+ require "runapi/core"
34
+
35
+ pricing = RunApi::Core::PricingClient.new
36
+ schedules = pricing.list(service: "suno")
37
+ ```
38
+
15
39
  ## Request Identifiers
16
40
 
17
41
  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
42
 
43
+ Task-creation calls also accept an optional opaque `Idempotency-Key` up to 512 characters. Generate one value per logical task and reuse it only with identical input after an unknown result. Reusing the value with different input returns `409 Conflict`; do not derive it from `X-Client-Request-Id`.
44
+
19
45
  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
46
 
21
47
  ```ruby
@@ -23,7 +49,10 @@ require "runapi/suno"
23
49
 
24
50
  client = RunApi::Suno::Client.new(api_key: ENV.fetch("RUNAPI_API_KEY"))
25
51
  options = RunApi::Core::RequestOptions.new(
26
- headers: {"X-Client-Request-Id" => "order-123"}
52
+ headers: {
53
+ "X-Client-Request-Id" => "order-123",
54
+ "Idempotency-Key" => "opaque-logical-task-123"
55
+ }
27
56
  )
28
57
 
29
58
  response = client.text_to_music.create(
@@ -19,15 +19,20 @@ module RunApi
19
19
  # @param explicit [String, nil] API key passed directly to a client constructor.
20
20
  # @return [String] the resolved API key
21
21
  def self.resolve_api_key(explicit = nil)
22
- resolved = normalize(explicit) ||
23
- normalize(RunApi.respond_to?(:api_key) ? RunApi.api_key : nil) ||
24
- normalize(ENV[ENV_VAR_NAME])
22
+ resolved = resolve_optional_api_key(explicit)
25
23
 
26
24
  raise AuthenticationError, MISSING_KEY_MESSAGE unless resolved
27
25
 
28
26
  resolved
29
27
  end
30
28
 
29
+ # Resolve an API key when present without requiring one for public resources.
30
+ def self.resolve_optional_api_key(explicit = nil)
31
+ normalize(explicit) ||
32
+ normalize(RunApi.respond_to?(:api_key) ? RunApi.api_key : nil) ||
33
+ normalize(ENV[ENV_VAR_NAME])
34
+ end
35
+
31
36
  def self.normalize(value)
32
37
  return nil unless value.is_a?(String)
33
38
 
@@ -3,7 +3,7 @@
3
3
  module RunApi
4
4
  module Core
5
5
  # Base class for every RunAPI client. Resolves the API key, builds the shared
6
- # HTTP client, and exposes the Universal Resources (file upload, account) that
6
+ # HTTP client, and exposes the Universal Resources (file upload, account, pricing) that
7
7
  # are available on any client regardless of which model gem was required.
8
8
  #
9
9
  # Provider clients inherit from this and build their model resources from the
@@ -13,13 +13,22 @@ module RunApi
13
13
  attr_reader :files
14
14
  # @return [Account] Account info and balance operations.
15
15
  attr_reader :account
16
+ # @return [Pricing] Live Price Schedule lookup and Price Quote operations.
17
+ attr_reader :pricing
16
18
 
17
19
  def initialize(api_key: nil, **options)
18
20
  @api_key = Core::Auth.resolve_api_key(api_key)
19
21
  client_options = Core::ClientOptions.new(api_key: @api_key, **options)
22
+ @owns_http_client = client_options.http_client.nil?
20
23
  @http = client_options.http_client || Core::HttpClient.new(client_options)
21
24
  @files = Files.new(@http)
22
25
  @account = Account.new(@http)
26
+ @pricing = Pricing.new(@http)
27
+ end
28
+
29
+ # Closes the SDK-created HTTP client. Injected clients remain caller-owned.
30
+ def close
31
+ @http.close if @owns_http_client
23
32
  end
24
33
 
25
34
  protected
@@ -37,6 +37,10 @@ module RunApi
37
37
  raise NetworkError, e.message
38
38
  end
39
39
 
40
+ if response.is_a?(Net::HTTPNotModified) && options&.allow_not_modified
41
+ return Response.new(body: {"not_modified" => true}, headers: response_headers(response))
42
+ end
43
+
40
44
  if response.is_a?(Net::HTTPSuccess)
41
45
  body = parse_body(response.body)
42
46
  return nil if body.nil?
@@ -60,6 +64,12 @@ module RunApi
60
64
  close_multipart_files(req)
61
65
  end
62
66
 
67
+ def close
68
+ @pool.shutdown do |http|
69
+ http.finish if http.started?
70
+ end
71
+ end
72
+
63
73
  # PUT bytes straight to a pre-authorized upload URL with the exact headers
64
74
  # issued for it. Skips the base URL, auth, and retries: the URL is single-use
65
75
  # and the body is not safe to replay.
@@ -100,7 +110,7 @@ module RunApi
100
110
  klass = Net::HTTP.const_get(method.to_s.capitalize)
101
111
  req = klass.new(uri.request_uri)
102
112
 
103
- req["Authorization"] = "Bearer #{@options.api_key}"
113
+ req["Authorization"] = "Bearer #{@options.api_key}" if @options.api_key
104
114
  req["Accept"] = "application/json"
105
115
  req["User-Agent"] = Constants::SDK_USER_AGENT
106
116
 
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RunApi
4
+ module Core
5
+ class Pricing
6
+ include RunApi::Core::ResourceHelpers
7
+
8
+ SCHEDULES_ENDPOINT = "/api/v1/price_schedules"
9
+ QUOTES_ENDPOINT = "/api/v1/price_quotes"
10
+
11
+ class Schedule < RunApi::Core::BaseModel
12
+ required :service, String
13
+ required :action, String
14
+ optional :model, String
15
+ required :pricing_status, String
16
+ required :catalog_status, String
17
+ required :currency, String
18
+ required :billing_unit, String
19
+ required :billing_strategy, String
20
+ optional :unit_price_cents, Numeric
21
+ optional :input_price_per_1m_cents, Numeric
22
+ optional :output_price_per_1m_cents, Numeric
23
+ optional :cache_read_price_per_1m_cents, Numeric
24
+ optional :cache_write_5m_price_per_1m_cents, Numeric
25
+ optional :cache_write_1h_price_per_1m_cents, Numeric
26
+ required :billing_config, Hash
27
+ end
28
+
29
+ class ScheduleListResponse < RunApi::Core::BaseModel
30
+ required :as_of, String
31
+ required :price_schedules, [Schedule]
32
+ end
33
+
34
+ class ScheduleNotModifiedResponse < RunApi::Core::BaseModel
35
+ required :not_modified, TrueClass
36
+ end
37
+
38
+ class QuoteResponse < RunApi::Core::BaseModel
39
+ required :service, String
40
+ required :action, String
41
+ optional :model, String
42
+ required :pricing_status, String
43
+ required :currency, String
44
+ required :reservation_amount_cents, Numeric
45
+ required :estimate_basis, String
46
+ required :as_of, String
47
+ end
48
+
49
+ class QuoteEnvelope < RunApi::Core::BaseModel
50
+ required :price_quote, QuoteResponse
51
+ end
52
+
53
+ def initialize(http)
54
+ @http = http
55
+ end
56
+
57
+ def list(service: nil, action: nil, model: nil, options: nil)
58
+ query = URI.encode_www_form(compact_params(service:, action:, model:))
59
+ path = query.empty? ? SCHEDULES_ENDPOINT : "#{SCHEDULES_ENDPOINT}?#{query}"
60
+ request_options = (options || RequestOptions.new).dup
61
+ request_options.allow_not_modified = true
62
+ response = @http.request(:get, path, options: request_options)
63
+ payload = response.is_a?(Core::Response) ? response.body : response
64
+ response_class = payload["not_modified"] ? ScheduleNotModifiedResponse : ScheduleListResponse
65
+ result = RunApi::Core::BaseModel.coerce(payload, as: response_class)
66
+ result.with_response_headers(response.response_headers) if response.is_a?(Core::Response)
67
+ result
68
+ end
69
+
70
+ def quote(service:, action:, model: nil, params: {}, options: nil)
71
+ response = request(
72
+ :post,
73
+ QUOTES_ENDPOINT,
74
+ body: compact_params(service:, action:, model:, params:),
75
+ options:,
76
+ response_class: QuoteEnvelope
77
+ )
78
+ response.price_quote
79
+ end
80
+ end
81
+
82
+ # Standalone live Pricing client with optional API authentication.
83
+ class PricingClient < Pricing
84
+ def initialize(api_key: nil, **options)
85
+ client_options = Core::ClientOptions.new(
86
+ api_key: Core::Auth.resolve_optional_api_key(api_key),
87
+ **options
88
+ )
89
+ @owns_http_client = client_options.http_client.nil?
90
+ super(client_options.http_client || Core::HttpClient.new(client_options))
91
+ end
92
+
93
+ # Closes the SDK-created HTTP client. Injected clients remain caller-owned.
94
+ def close
95
+ @http.close if @owns_http_client
96
+ end
97
+ end
98
+ end
99
+ end
@@ -22,7 +22,7 @@ module RunApi
22
22
  # )
23
23
  #
24
24
  # @!attribute [rw] api_key
25
- # @return [String] API key for authentication. Required.
25
+ # @return [String, nil] API key for authenticated resources and protected Price Quotes.
26
26
  # @!attribute [rw] base_url
27
27
  # @return [String] Base URL for API requests. Defaults to RunApi.base_url.
28
28
  # @!attribute [rw] timeout
@@ -78,6 +78,7 @@ module RunApi
78
78
  :headers,
79
79
  :timeout,
80
80
  :max_retries,
81
+ :allow_not_modified,
81
82
  keyword_init: true
82
83
  )
83
84
 
@@ -100,6 +101,25 @@ module RunApi
100
101
  end
101
102
  end
102
103
 
104
+ class TaskReservation < BaseModel
105
+ required :amount_cents, Numeric
106
+ end
107
+
108
+ class TaskSettlement < BaseModel
109
+ required :charged_amount_cents, Numeric
110
+ required :amount_micro_cents, Numeric
111
+ end
112
+
113
+ class TaskRefund < BaseModel
114
+ required :refunded_at, String
115
+ end
116
+
117
+ class TaskBillingFacts < BaseModel
118
+ optional :reservation, TaskReservation
119
+ optional :settlement, TaskSettlement
120
+ optional :refund, TaskRefund
121
+ end
122
+
103
123
  # Typed response structure for async task operations.
104
124
  # Additional API-specific fields are preserved and exposed via dot notation.
105
125
  #
@@ -122,6 +142,7 @@ module RunApi
122
142
  optional :id, String
123
143
  optional :status, String
124
144
  optional :error, String
145
+ optional :billing, TaskBillingFacts
125
146
  end
126
147
  end
127
148
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module RunApi
4
4
  module Core
5
- VERSION = "0.2.15"
5
+ VERSION = "0.3.0"
6
6
  end
7
7
 
8
8
  VERSION = Core::VERSION
data/lib/runapi/core.rb CHANGED
@@ -21,4 +21,5 @@ require_relative "core/polling"
21
21
  require_relative "core/resource_helpers"
22
22
  require_relative "core/files"
23
23
  require_relative "core/account"
24
+ require_relative "core/pricing"
24
25
  require_relative "core/client"
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.15
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - RunAPI
@@ -13,16 +13,22 @@ dependencies:
13
13
  name: connection_pool
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
- - - "~>"
16
+ - - ">="
17
17
  - !ruby/object:Gem::Version
18
18
  version: '2.4'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '4'
19
22
  type: :runtime
20
23
  prerelease: false
21
24
  version_requirements: !ruby/object:Gem::Requirement
22
25
  requirements:
23
- - - "~>"
26
+ - - ">="
24
27
  - !ruby/object:Gem::Version
25
28
  version: '2.4'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '4'
26
32
  description: The RunAPI Core Ruby SDK provides shared authentication, HTTP, retry,
27
33
  error, and polling primitives for RunAPI model gems. Install `runapi-core` only
28
34
  when you are building SDK infrastructure or shared Ruby tooling; application code
@@ -49,6 +55,7 @@ files:
49
55
  - lib/runapi/core/http_client.rb
50
56
  - lib/runapi/core/multipart_body.rb
51
57
  - lib/runapi/core/polling.rb
58
+ - lib/runapi/core/pricing.rb
52
59
  - lib/runapi/core/resource_helpers.rb
53
60
  - lib/runapi/core/response.rb
54
61
  - lib/runapi/core/types.rb