runapi-core 0.2.5 → 0.2.7
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 +39 -0
- data/lib/runapi/core/account.rb +44 -0
- data/lib/runapi/core/client.rb +30 -0
- data/lib/runapi/core/files.rb +65 -0
- data/lib/runapi/core/http_client.rb +33 -2
- data/lib/runapi/core/multipart_body.rb +24 -0
- data/lib/runapi/core/resource_helpers.rb +145 -0
- data/lib/runapi/core/version.rb +1 -1
- data/lib/runapi/core.rb +4 -0
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f165d79fca32b409a50b34fc13a629eb666e063a1fc2a72e51bd63f1dcf91534
|
|
4
|
+
data.tar.gz: 338918ccaeff5eb66a9366f2e56e868f55e8b728580064989453d6942c5babe1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 59a92208fc80fbaeede0a56af09e4a8d41c91a40bdbbd8242635bf762985c1177b340253de20dad30dc44b3a4fd4d76587102c00ceb80b84194c4594a5892b72
|
|
7
|
+
data.tar.gz: 96b6055a8ff2a405b500c76418fc91c9b7ebdae676b6832e59052b0700b2500b39813faadcb13204ace7ecea6b754765245c04193b7d31dcf92c95dd0c30a2ad
|
data/README.md
CHANGED
|
@@ -12,6 +12,45 @@ 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 model SDKs 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
|
+
## Request Identifiers
|
|
16
|
+
|
|
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
|
+
|
|
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.
|
|
20
|
+
|
|
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({
|
|
32
|
+
prompt: "A chill lo-fi beat",
|
|
33
|
+
model: "suno-v4.5-plus",
|
|
34
|
+
vocal_mode: "instrumental"
|
|
35
|
+
})
|
|
36
|
+
|
|
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)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## File Upload
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
client = RunApi::NanoBanana::Client.new(api_key: ENV["RUNAPI_API_KEY"])
|
|
46
|
+
|
|
47
|
+
upload = client.files.create(source: {type: "url", url: "https://cdn.runapi.ai/public/samples/image.jpg"})
|
|
48
|
+
puts upload.url
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
> [!IMPORTANT]
|
|
52
|
+
> Uploaded file URLs expire 1 hour after creation. Pass them to a model promptly rather than storing them for later use.
|
|
53
|
+
|
|
15
54
|
## License
|
|
16
55
|
|
|
17
56
|
Licensed under the Apache License, Version 2.0.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RunApi
|
|
4
|
+
module Core
|
|
5
|
+
class Account
|
|
6
|
+
include RunApi::Core::ResourceHelpers
|
|
7
|
+
|
|
8
|
+
INFO_ENDPOINT = "/api/v1/me"
|
|
9
|
+
BALANCE_ENDPOINT = "/api/v1/me/balance"
|
|
10
|
+
|
|
11
|
+
class AccountRecord < RunApi::Core::BaseModel
|
|
12
|
+
required :id, Integer
|
|
13
|
+
required :name, String
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class InfoResponse < RunApi::Core::BaseModel
|
|
17
|
+
required :id, Integer
|
|
18
|
+
required :name, String
|
|
19
|
+
required :email, String
|
|
20
|
+
required :account, AccountRecord
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class BalanceResponse < RunApi::Core::BaseModel
|
|
24
|
+
required :balance_cents, Integer
|
|
25
|
+
required :paid_balance_cents, Integer
|
|
26
|
+
required :bonus_balance_cents, Integer
|
|
27
|
+
required :spent_cents_today, Integer
|
|
28
|
+
required :spent_cents_total, Integer
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def initialize(http)
|
|
32
|
+
@http = http
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def info(options: nil)
|
|
36
|
+
request(:get, INFO_ENDPOINT, options:, response_class: InfoResponse)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def balance(options: nil)
|
|
40
|
+
request(:get, BALANCE_ENDPOINT, options:, response_class: BalanceResponse)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RunApi
|
|
4
|
+
module Core
|
|
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
|
|
7
|
+
# are available on any client regardless of which model gem was required.
|
|
8
|
+
#
|
|
9
|
+
# Provider clients inherit from this and build their model resources from the
|
|
10
|
+
# protected +http+ reader.
|
|
11
|
+
class Client
|
|
12
|
+
# @return [Files] Temporary file upload operations.
|
|
13
|
+
attr_reader :files
|
|
14
|
+
# @return [Account] Account info and balance operations.
|
|
15
|
+
attr_reader :account
|
|
16
|
+
|
|
17
|
+
def initialize(api_key: nil, **options)
|
|
18
|
+
@api_key = Core::Auth.resolve_api_key(api_key)
|
|
19
|
+
client_options = Core::ClientOptions.new(api_key: @api_key, **options)
|
|
20
|
+
@http = client_options.http_client || Core::HttpClient.new(client_options)
|
|
21
|
+
@files = Files.new(@http)
|
|
22
|
+
@account = Account.new(@http)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
protected
|
|
26
|
+
|
|
27
|
+
attr_reader :http
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RunApi
|
|
4
|
+
module Core
|
|
5
|
+
class Files
|
|
6
|
+
include RunApi::Core::ResourceHelpers
|
|
7
|
+
|
|
8
|
+
ENDPOINT = "/api/v1/files"
|
|
9
|
+
|
|
10
|
+
class UploadResponse < RunApi::Core::BaseModel
|
|
11
|
+
required :file_name, String
|
|
12
|
+
required :url, String
|
|
13
|
+
required :size_bytes, Integer
|
|
14
|
+
required :mime_type, String
|
|
15
|
+
required :created_at, String
|
|
16
|
+
required :expires_at, String
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
RESPONSE_CLASS = UploadResponse
|
|
20
|
+
|
|
21
|
+
def initialize(http)
|
|
22
|
+
@http = http
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def create(file: nil, source: nil, file_name: nil, options: nil)
|
|
26
|
+
validate_source!(file:, source:)
|
|
27
|
+
|
|
28
|
+
body = if file
|
|
29
|
+
multipart_body(file, file_name:)
|
|
30
|
+
else
|
|
31
|
+
compact_params(source:, file_name:)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
request(:post, ENDPOINT, body:, options:)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def validate_source!(file:, source:)
|
|
40
|
+
source_count = [file, source].count { |value| !value.nil? }
|
|
41
|
+
return if source_count == 1
|
|
42
|
+
|
|
43
|
+
raise ArgumentError, "Exactly one source is required: file or source"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def multipart_body(file, file_name:)
|
|
47
|
+
path = file_path(file)
|
|
48
|
+
filename = file_name || File.basename(path)
|
|
49
|
+
Core::MultipartBody.new(
|
|
50
|
+
fields: compact_params(file_name: file_name),
|
|
51
|
+
files: {
|
|
52
|
+
file: Core::MultipartFile.new(path:, filename:)
|
|
53
|
+
}
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def file_path(file)
|
|
58
|
+
return file if file.is_a?(String)
|
|
59
|
+
return file.path if file.respond_to?(:path)
|
|
60
|
+
|
|
61
|
+
raise ArgumentError, "file must be a file path or respond to :path"
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -50,6 +50,8 @@ module RunApi
|
|
|
50
50
|
|
|
51
51
|
raise error
|
|
52
52
|
end
|
|
53
|
+
ensure
|
|
54
|
+
close_multipart_files(req)
|
|
53
55
|
end
|
|
54
56
|
|
|
55
57
|
private
|
|
@@ -69,16 +71,45 @@ module RunApi
|
|
|
69
71
|
req = klass.new(uri.request_uri)
|
|
70
72
|
|
|
71
73
|
req["Authorization"] = "Bearer #{@options.api_key}"
|
|
72
|
-
req["Content-Type"] = "application/json"
|
|
73
74
|
req["Accept"] = "application/json"
|
|
74
75
|
req["User-Agent"] = Constants::SDK_USER_AGENT
|
|
75
76
|
|
|
76
77
|
options&.headers&.each { |k, v| req[k.to_s] = v }
|
|
77
78
|
|
|
78
|
-
|
|
79
|
+
if body.is_a?(MultipartBody)
|
|
80
|
+
req.set_form(multipart_parts(body), "multipart/form-data")
|
|
81
|
+
elsif body
|
|
82
|
+
req["Content-Type"] = "application/json"
|
|
83
|
+
req.body = JSON.generate(body)
|
|
84
|
+
end
|
|
79
85
|
req
|
|
80
86
|
end
|
|
81
87
|
|
|
88
|
+
def multipart_parts(body)
|
|
89
|
+
opened_files = []
|
|
90
|
+
field_parts = body.fields.map { |key, value| [key, value.to_s] }
|
|
91
|
+
file_parts = body.files.map do |key, file|
|
|
92
|
+
options = {filename: file.filename}
|
|
93
|
+
options[:content_type] = file.content_type if file.content_type
|
|
94
|
+
opened_files << File.open(file.path, "rb")
|
|
95
|
+
[key, opened_files.last, options]
|
|
96
|
+
end
|
|
97
|
+
field_parts + file_parts
|
|
98
|
+
rescue
|
|
99
|
+
opened_files.each { |file| file.close unless file.closed? }
|
|
100
|
+
raise
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def close_multipart_files(request)
|
|
104
|
+
body_data = request&.instance_variable_get(:@body_data)
|
|
105
|
+
return unless body_data
|
|
106
|
+
|
|
107
|
+
body_data.each do |part|
|
|
108
|
+
file = part[1]
|
|
109
|
+
file.close if file.is_a?(File) && !file.closed?
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
82
113
|
def retryable?(method, status)
|
|
83
114
|
Constants::IDEMPOTENT_METHODS.include?(method.to_s.upcase) &&
|
|
84
115
|
Constants::RETRYABLE_STATUS_CODES.include?(status)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RunApi
|
|
4
|
+
module Core
|
|
5
|
+
MultipartFile = Struct.new(:path, :filename, :content_type, keyword_init: true)
|
|
6
|
+
|
|
7
|
+
class MultipartBody
|
|
8
|
+
attr_reader :fields, :files
|
|
9
|
+
|
|
10
|
+
def initialize(fields: {}, files: {})
|
|
11
|
+
@fields = stringify_keys(fields)
|
|
12
|
+
@files = stringify_keys(files)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def stringify_keys(hash)
|
|
18
|
+
hash.each_with_object({}) do |(key, value), result|
|
|
19
|
+
result[key.to_s] = value
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -41,6 +41,151 @@ module RunApi
|
|
|
41
41
|
end
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
+
# ---- Contract validation ------------------------------------------
|
|
45
|
+
# Validates request params against the generated contract: model
|
|
46
|
+
# membership, then per-field required/enum/integer/min/max/length, then
|
|
47
|
+
# declared cross-field rules. `schema` is one action entry from the generated
|
|
48
|
+
# per-package CONTRACT (CONTRACT["<action>"]).
|
|
49
|
+
|
|
50
|
+
def validate_contract!(schema, params)
|
|
51
|
+
model = param_value(params, "model")
|
|
52
|
+
models = schema["models"] || []
|
|
53
|
+
unless models.include?(model)
|
|
54
|
+
raise Core::ValidationError, "model must be one of: #{models.sort.join(", ")}"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
fields = schema.dig("fields_by_model", model) || {}
|
|
58
|
+
fields.each do |field, rules|
|
|
59
|
+
validate_schema_field!(params, field, rules)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
Array(schema["rules"]).each { |rule| enforce_contract_rule!(params, rule) }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def validate_schema_field!(params, field, rules)
|
|
66
|
+
present = field_present?(params, field)
|
|
67
|
+
raise Core::ValidationError, "#{field} is required" if rules["required"] && !present
|
|
68
|
+
return unless present
|
|
69
|
+
|
|
70
|
+
value = param_value(params, field)
|
|
71
|
+
if (enum = rules["enum"]) && !enum_value_allowed?(enum, value)
|
|
72
|
+
raise Core::ValidationError, "#{field} must be one of: #{enum.join(", ")}"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
validate_schema_integer!(field, value, rules) if rules["type"] == "integer"
|
|
76
|
+
validate_schema_range!(field, value, rules) if rules.key?("min") || rules.key?("max")
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Mirrors GatewayEntry#validate_schema_integer!: a type: integer field
|
|
80
|
+
# rejects non-integer numbers (e.g. 11.5), which min/max alone admit.
|
|
81
|
+
def validate_schema_integer!(field, value, rules)
|
|
82
|
+
return if value.is_a?(Integer)
|
|
83
|
+
|
|
84
|
+
min = rules["min"]
|
|
85
|
+
max = rules["max"]
|
|
86
|
+
detail = (min && max) ? " between #{min} and #{max}" : ""
|
|
87
|
+
raise Core::ValidationError, "#{field} must be an integer#{detail}"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def validate_schema_range!(field, value, rules)
|
|
91
|
+
if rules["length"]
|
|
92
|
+
measured = value.to_s.length
|
|
93
|
+
unit = "characters"
|
|
94
|
+
else
|
|
95
|
+
raise Core::ValidationError, "#{field} must be a number" unless value.is_a?(Numeric)
|
|
96
|
+
|
|
97
|
+
measured = value
|
|
98
|
+
unit = nil
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
min = rules["min"]
|
|
102
|
+
max = rules["max"]
|
|
103
|
+
return if (min.nil? || measured >= min) && (max.nil? || measured <= max)
|
|
104
|
+
|
|
105
|
+
raise Core::ValidationError, schema_range_message(field, min, max, unit)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def schema_range_message(field, min, max, unit)
|
|
109
|
+
suffix = unit ? " #{unit}" : ""
|
|
110
|
+
if min && max
|
|
111
|
+
"#{field} must be between #{min} and #{max}#{suffix}"
|
|
112
|
+
elsif min
|
|
113
|
+
"#{field} must be at least #{min}#{suffix}"
|
|
114
|
+
else
|
|
115
|
+
"#{field} must be at most #{max}#{suffix}"
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def enum_value_allowed?(enum, value)
|
|
120
|
+
enum.any? do |allowed|
|
|
121
|
+
if allowed.is_a?(Numeric)
|
|
122
|
+
value.is_a?(Numeric) && value == allowed
|
|
123
|
+
elsif value.is_a?(Numeric)
|
|
124
|
+
allowed == value
|
|
125
|
+
else
|
|
126
|
+
allowed.to_s == value.to_s
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def enforce_contract_rule!(params, rule)
|
|
132
|
+
conditions = rule["when"] || {}
|
|
133
|
+
return unless conditions.all? { |field, value| rule_condition_met?(params, field, value) }
|
|
134
|
+
|
|
135
|
+
context = conditions.map { |field, value| "#{field} is #{value}" }.join(" and ")
|
|
136
|
+
Array(rule["required"]).each do |field|
|
|
137
|
+
next if field_present?(params, field)
|
|
138
|
+
|
|
139
|
+
raise Core::ValidationError, "#{field} is required when #{context}"
|
|
140
|
+
end
|
|
141
|
+
Array(rule["forbidden"]).each do |field|
|
|
142
|
+
next unless field_present?(params, field)
|
|
143
|
+
|
|
144
|
+
raise Core::ValidationError, "#{field} is not allowed when #{context}"
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def rule_condition_met?(params, field, value)
|
|
149
|
+
return false unless param_key?(params, field)
|
|
150
|
+
|
|
151
|
+
param_value(params, field).to_s == value.to_s
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def field_present?(params, field)
|
|
155
|
+
return false unless param_key?(params, field)
|
|
156
|
+
|
|
157
|
+
value = param_value(params, field)
|
|
158
|
+
case value
|
|
159
|
+
when false then true
|
|
160
|
+
when Array then value.any? { |item| present_value?(item) }
|
|
161
|
+
else present_value?(value)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def param_key?(params, field)
|
|
166
|
+
params.key?(field.to_sym) || params.key?(field.to_s)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Indifferent value lookup for a string-or-symbol field across params that
|
|
170
|
+
# may be keyed either way.
|
|
171
|
+
def param_value(params, field)
|
|
172
|
+
if params.key?(field.to_sym)
|
|
173
|
+
params[field.to_sym]
|
|
174
|
+
elsif params.key?(field.to_s)
|
|
175
|
+
params[field.to_s]
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def present_value?(value)
|
|
180
|
+
case value
|
|
181
|
+
when nil, false then false
|
|
182
|
+
when true then true
|
|
183
|
+
when String then !value.strip.empty?
|
|
184
|
+
when Array, Hash then !value.empty?
|
|
185
|
+
else true
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
44
189
|
def default_response_class
|
|
45
190
|
if self.class.const_defined?(:RESPONSE_CLASS, false)
|
|
46
191
|
self.class::RESPONSE_CLASS
|
data/lib/runapi/core/version.rb
CHANGED
data/lib/runapi/core.rb
CHANGED
|
@@ -14,6 +14,10 @@ require_relative "core/base_model"
|
|
|
14
14
|
require_relative "core/types"
|
|
15
15
|
require_relative "core/errors"
|
|
16
16
|
require_relative "core/auth"
|
|
17
|
+
require_relative "core/multipart_body"
|
|
17
18
|
require_relative "core/http_client"
|
|
18
19
|
require_relative "core/polling"
|
|
19
20
|
require_relative "core/resource_helpers"
|
|
21
|
+
require_relative "core/files"
|
|
22
|
+
require_relative "core/account"
|
|
23
|
+
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.
|
|
4
|
+
version: 0.2.7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- RunAPI
|
|
@@ -38,12 +38,16 @@ files:
|
|
|
38
38
|
- README.md
|
|
39
39
|
- lib/runapi-core.rb
|
|
40
40
|
- lib/runapi/core.rb
|
|
41
|
+
- lib/runapi/core/account.rb
|
|
41
42
|
- lib/runapi/core/auth.rb
|
|
42
43
|
- lib/runapi/core/base_model.rb
|
|
44
|
+
- lib/runapi/core/client.rb
|
|
43
45
|
- lib/runapi/core/configuration.rb
|
|
44
46
|
- lib/runapi/core/constants.rb
|
|
45
47
|
- lib/runapi/core/errors.rb
|
|
48
|
+
- lib/runapi/core/files.rb
|
|
46
49
|
- lib/runapi/core/http_client.rb
|
|
50
|
+
- lib/runapi/core/multipart_body.rb
|
|
47
51
|
- lib/runapi/core/polling.rb
|
|
48
52
|
- lib/runapi/core/resource_helpers.rb
|
|
49
53
|
- lib/runapi/core/types.rb
|
|
@@ -52,9 +56,11 @@ homepage: https://runapi.ai/models
|
|
|
52
56
|
licenses:
|
|
53
57
|
- Apache-2.0
|
|
54
58
|
metadata:
|
|
59
|
+
runapi_slug: core
|
|
55
60
|
homepage_uri: https://runapi.ai/models
|
|
56
61
|
documentation_uri: https://github.com/runapi-ai/core-sdk/blob/main/ruby/README.md
|
|
57
62
|
source_code_uri: https://github.com/runapi-ai/core-sdk
|
|
63
|
+
bug_tracker_uri: https://github.com/runapi-ai/core-sdk/issues
|
|
58
64
|
changelog_uri: https://github.com/runapi-ai/core-sdk/blob/main/CHANGELOG.md
|
|
59
65
|
rdoc_options: []
|
|
60
66
|
require_paths:
|