runapi-core 0.2.16 → 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: f199f45d065827bb1931ab5faff8dd945fd62b3cb0a5260b268fdfee41875a45
4
- data.tar.gz: 6d2c40ffded8406818798c40bfddca448e1a7149981fd0b012dc5c0f4d17e02e
3
+ metadata.gz: 9a0bc423a77bb7770c6db1e3a6fa10ae1679ef58f7762231b02c28e86f85b4e3
4
+ data.tar.gz: fce5aa1e8e03dfc0c3c873a6f499bd5c51b173b87f91b75a74955413f59b19e2
5
5
  SHA512:
6
- metadata.gz: 17e79439f00b8b10fbbbbcf00b3d074cfd9e3cf9d70c4b20b91743aee12cba2e122c9da364a66c0c21844d5e2153083102acacfb639871e1a24377899b3bda41
7
- data.tar.gz: 707cadac8b31ca82c31fa191cb017e58192eb7c6b4da7bd15aba8fe40209f783537ee2452bc4846538c123ddc0843c31c7b24a86158a766f7740ec309e269d38
6
+ metadata.gz: fc56fca2936974941318607a19905d77542bf56f529fdf68577a7da9b18bc186cdbe4a3ea45f367b1d64d8f48622f384cfc8ef089503a1fdf87df399ed6dc84f
7
+ data.tar.gz: 548078f4a0b851f5a15f3f76fbadfd5164a122100c0842921bf9e6efa572049b3b715fbfdec7f82a8b7cdfe7773d0565a46ae648772cc75304c1f9a06c348838
data/README.md CHANGED
@@ -12,6 +12,30 @@ 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.
@@ -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,6 +13,8 @@ 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)
@@ -21,6 +23,7 @@ module RunApi
21
23
  @http = client_options.http_client || Core::HttpClient.new(client_options)
22
24
  @files = Files.new(@http)
23
25
  @account = Account.new(@http)
26
+ @pricing = Pricing.new(@http)
24
27
  end
25
28
 
26
29
  # Closes the SDK-created HTTP client. Injected clients remain caller-owned.
@@ -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?
@@ -106,7 +110,7 @@ module RunApi
106
110
  klass = Net::HTTP.const_get(method.to_s.capitalize)
107
111
  req = klass.new(uri.request_uri)
108
112
 
109
- req["Authorization"] = "Bearer #{@options.api_key}"
113
+ req["Authorization"] = "Bearer #{@options.api_key}" if @options.api_key
110
114
  req["Accept"] = "application/json"
111
115
  req["User-Agent"] = Constants::SDK_USER_AGENT
112
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.16"
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.16
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - RunAPI
@@ -55,6 +55,7 @@ files:
55
55
  - lib/runapi/core/http_client.rb
56
56
  - lib/runapi/core/multipart_body.rb
57
57
  - lib/runapi/core/polling.rb
58
+ - lib/runapi/core/pricing.rb
58
59
  - lib/runapi/core/resource_helpers.rb
59
60
  - lib/runapi/core/response.rb
60
61
  - lib/runapi/core/types.rb