openrouter_client 0.1.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 +7 -0
- data/.env.example +1 -0
- data/.rubocop.yml +68 -0
- data/.ruby-version +2 -0
- data/Gemfile +14 -0
- data/Gemfile.lock +124 -0
- data/LICENSE +22 -0
- data/README.md +378 -0
- data/Rakefile +16 -0
- data/lib/openrouter/api_key.rb +136 -0
- data/lib/openrouter/client.rb +157 -0
- data/lib/openrouter/completion.rb +293 -0
- data/lib/openrouter/credit.rb +71 -0
- data/lib/openrouter/generation.rb +107 -0
- data/lib/openrouter/model.rb +164 -0
- data/lib/openrouter/stream.rb +105 -0
- data/lib/openrouter/version.rb +5 -0
- data/lib/openrouter.rb +119 -0
- data/openrouter_client.gemspec +33 -0
- data/rbi/openrouter/api_key.rbi +85 -0
- data/rbi/openrouter/client.rbi +56 -0
- data/rbi/openrouter/completion.rbi +152 -0
- data/rbi/openrouter/credit.rbi +42 -0
- data/rbi/openrouter/generation.rbi +69 -0
- data/rbi/openrouter/model.rbi +92 -0
- data/rbi/openrouter/openrouter.rbi +77 -0
- data/rbi/openrouter/stream.rbi +39 -0
- data/rbi/openrouter/version.rbi +6 -0
- data/sorbet/config +2 -0
- data/sorbet/rbi/.gitignore +2 -0
- metadata +89 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenRouter
|
|
4
|
+
# Represents an API key and provides management operations.
|
|
5
|
+
# Provides helpers to list, create, update, and delete API keys.
|
|
6
|
+
class ApiKey
|
|
7
|
+
KEYS_PATH = "/keys"
|
|
8
|
+
AUTH_KEY_PATH = "/auth/key"
|
|
9
|
+
|
|
10
|
+
# @return [String] The key identifier
|
|
11
|
+
attr_reader :id
|
|
12
|
+
# @return [String, nil] The key name/label
|
|
13
|
+
attr_reader :name
|
|
14
|
+
# @return [String, nil] The key value (only available on creation)
|
|
15
|
+
attr_reader :key
|
|
16
|
+
# @return [Float, nil] Credit limit for this key
|
|
17
|
+
attr_reader :limit
|
|
18
|
+
# @return [Float, nil] Usage amount for this key
|
|
19
|
+
attr_reader :usage
|
|
20
|
+
# @return [Boolean, nil] Whether the key is disabled
|
|
21
|
+
attr_reader :disabled
|
|
22
|
+
# @return [String, nil] Created timestamp
|
|
23
|
+
attr_reader :created_at
|
|
24
|
+
# @return [String, nil] Updated timestamp
|
|
25
|
+
attr_reader :updated_at
|
|
26
|
+
|
|
27
|
+
# @param attributes [Hash] Raw attributes from OpenRouter API
|
|
28
|
+
# @param client [OpenRouter::Client] HTTP client
|
|
29
|
+
def initialize(attributes, client: OpenRouter.client)
|
|
30
|
+
@client = client
|
|
31
|
+
reset_attributes(attributes)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
class << self
|
|
35
|
+
# Get information about the current API key being used.
|
|
36
|
+
# Corresponds to GET /api/v1/auth/key
|
|
37
|
+
# @param client [OpenRouter::Client] HTTP client
|
|
38
|
+
# @return [OpenRouter::ApiKey]
|
|
39
|
+
def current(client: OpenRouter.client)
|
|
40
|
+
response = client.get(AUTH_KEY_PATH)
|
|
41
|
+
new(response["data"] || response, client: client)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# List all API keys.
|
|
45
|
+
# @param client [OpenRouter::Client] HTTP client
|
|
46
|
+
# @return [Array<OpenRouter::ApiKey>]
|
|
47
|
+
def all(client: OpenRouter.client)
|
|
48
|
+
response = client.get(KEYS_PATH)
|
|
49
|
+
keys = Array(response && response["data"])
|
|
50
|
+
keys.map { |attributes| new(attributes, client: client) }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Find an API key by ID.
|
|
54
|
+
# @param id [String] The key identifier
|
|
55
|
+
# @param client [OpenRouter::Client] HTTP client
|
|
56
|
+
# @return [OpenRouter::ApiKey, nil]
|
|
57
|
+
def find_by(id:, client: OpenRouter.client)
|
|
58
|
+
response = client.get("#{KEYS_PATH}/#{id}")
|
|
59
|
+
return nil unless response
|
|
60
|
+
|
|
61
|
+
new(response["data"] || response, client: client)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Create a new API key.
|
|
65
|
+
# @param name [String] A name/label for the key
|
|
66
|
+
# @param limit [Float, nil] Credit limit for this key
|
|
67
|
+
# @param client [OpenRouter::Client] HTTP client
|
|
68
|
+
# @return [OpenRouter::ApiKey]
|
|
69
|
+
def create!(name:, limit: nil, client: OpenRouter.client)
|
|
70
|
+
payload = { name: name, limit: limit }.compact
|
|
71
|
+
response = client.post(KEYS_PATH, payload)
|
|
72
|
+
new(response["data"] || response, client: client)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Update this API key.
|
|
77
|
+
# @param name [String, nil] New name for the key
|
|
78
|
+
# @param limit [Float, nil] New credit limit
|
|
79
|
+
# @param disabled [Boolean, nil] Whether to disable the key
|
|
80
|
+
# @return [OpenRouter::ApiKey]
|
|
81
|
+
def update!(name: nil, limit: nil, disabled: nil)
|
|
82
|
+
payload = { name: name, limit: limit, disabled: disabled }.compact
|
|
83
|
+
response = @client.patch("#{KEYS_PATH}/#{@id}", payload)
|
|
84
|
+
reset_attributes(response["data"] || response)
|
|
85
|
+
self
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Delete this API key.
|
|
89
|
+
# @return [Boolean] true if deletion was successful
|
|
90
|
+
def destroy!
|
|
91
|
+
@client.delete("#{KEYS_PATH}/#{@id}")
|
|
92
|
+
true
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Check if this key is active (not disabled).
|
|
96
|
+
# @return [Boolean]
|
|
97
|
+
def active?
|
|
98
|
+
!@disabled
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Check if this key has a limit set.
|
|
102
|
+
# @return [Boolean]
|
|
103
|
+
def limited?
|
|
104
|
+
!@limit.nil? && @limit.positive?
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Check if this key has exceeded its limit.
|
|
108
|
+
# @return [Boolean]
|
|
109
|
+
def exceeded_limit?
|
|
110
|
+
return false unless limited?
|
|
111
|
+
|
|
112
|
+
(@usage || 0) >= @limit
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Get remaining credit for this key.
|
|
116
|
+
# @return [Float, nil]
|
|
117
|
+
def remaining
|
|
118
|
+
return nil unless limited?
|
|
119
|
+
|
|
120
|
+
@limit - (@usage || 0)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
private
|
|
124
|
+
|
|
125
|
+
def reset_attributes(attributes)
|
|
126
|
+
@id = attributes["id"] || attributes["hash"]
|
|
127
|
+
@name = attributes["name"] || attributes["label"]
|
|
128
|
+
@key = attributes["key"]
|
|
129
|
+
@limit = attributes["limit"]&.to_f
|
|
130
|
+
@usage = attributes["usage"]&.to_f
|
|
131
|
+
@disabled = attributes["disabled"]
|
|
132
|
+
@created_at = attributes["created_at"] || attributes["createdAt"]
|
|
133
|
+
@updated_at = attributes["updated_at"] || attributes["updatedAt"]
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenRouter
|
|
4
|
+
class Client
|
|
5
|
+
attr_accessor :configuration
|
|
6
|
+
|
|
7
|
+
def initialize(configuration = OpenRouter.configuration)
|
|
8
|
+
@configuration = configuration || OpenRouter::Configuration.new
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Perform a POST request to the OpenRouter API.
|
|
12
|
+
# @param path [String]
|
|
13
|
+
# @param payload [Hash]
|
|
14
|
+
# @param headers [Hash]
|
|
15
|
+
# @return [Hash, nil]
|
|
16
|
+
def post(path, payload = {}, headers: {})
|
|
17
|
+
response = connection.post(build_url(path)) do |request|
|
|
18
|
+
configure_request_headers(request, headers)
|
|
19
|
+
request.body = payload.compact.to_json
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
handle_error(response) unless response.success?
|
|
23
|
+
|
|
24
|
+
parse_json(response.body)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Perform a POST request with SSE streaming support.
|
|
28
|
+
# The provided on_data Proc will be used to receive chunked data.
|
|
29
|
+
# @param path [String]
|
|
30
|
+
# @param payload [Hash]
|
|
31
|
+
# @param on_data [Proc] called with chunks as they arrive
|
|
32
|
+
# @return [void]
|
|
33
|
+
def post_stream(path, payload = {}, on_data:)
|
|
34
|
+
url = build_url(path)
|
|
35
|
+
connection.post(url) do |request|
|
|
36
|
+
configure_request_headers(request, {})
|
|
37
|
+
request.headers["Accept"] = "text/event-stream"
|
|
38
|
+
request.headers["Cache-Control"] = "no-store"
|
|
39
|
+
request.body = payload.compact.to_json
|
|
40
|
+
request.options.on_data = on_data
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Perform a GET request to the OpenRouter API.
|
|
45
|
+
# @param path [String]
|
|
46
|
+
# @param query [Hash, nil]
|
|
47
|
+
# @param headers [Hash]
|
|
48
|
+
# @return [Hash, nil]
|
|
49
|
+
def get(path, query: nil, headers: {})
|
|
50
|
+
url = build_url(path)
|
|
51
|
+
url = "#{url}?#{URI.encode_www_form(query)}" if query && !query.empty?
|
|
52
|
+
|
|
53
|
+
response = connection.get(url) do |request|
|
|
54
|
+
configure_request_headers(request, headers)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
handle_error(response) unless response.success?
|
|
58
|
+
|
|
59
|
+
parse_json(response.body)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Perform a DELETE request to the OpenRouter API.
|
|
63
|
+
# @param path [String]
|
|
64
|
+
# @param headers [Hash]
|
|
65
|
+
# @return [Hash, nil]
|
|
66
|
+
def delete(path, headers: {})
|
|
67
|
+
response = connection.delete(build_url(path)) do |request|
|
|
68
|
+
configure_request_headers(request, headers)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
handle_error(response) unless response.success?
|
|
72
|
+
|
|
73
|
+
parse_json(response.body)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Perform a PATCH request to the OpenRouter API.
|
|
77
|
+
# @param path [String]
|
|
78
|
+
# @param payload [Hash]
|
|
79
|
+
# @param headers [Hash]
|
|
80
|
+
# @return [Hash, nil]
|
|
81
|
+
def patch(path, payload = {}, headers: {})
|
|
82
|
+
response = connection.patch(build_url(path)) do |request|
|
|
83
|
+
configure_request_headers(request, headers)
|
|
84
|
+
request.body = payload.compact.to_json
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
handle_error(response) unless response.success?
|
|
88
|
+
|
|
89
|
+
parse_json(response.body)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Handle HTTP error responses by raising appropriate exceptions.
|
|
93
|
+
# @param response [Faraday::Response]
|
|
94
|
+
# @raise [OpenRouter::Error]
|
|
95
|
+
def handle_error(response)
|
|
96
|
+
error_message = extract_error_message(response)
|
|
97
|
+
|
|
98
|
+
case response.status
|
|
99
|
+
when 400
|
|
100
|
+
raise BadRequestError, error_message
|
|
101
|
+
when 401
|
|
102
|
+
raise UnauthorizedError, error_message
|
|
103
|
+
when 402
|
|
104
|
+
raise PaymentRequiredError, error_message
|
|
105
|
+
when 403
|
|
106
|
+
raise ForbiddenError, error_message
|
|
107
|
+
when 404
|
|
108
|
+
raise NotFoundError, error_message
|
|
109
|
+
when 429
|
|
110
|
+
raise RateLimitError, error_message
|
|
111
|
+
else
|
|
112
|
+
raise ServerError, error_message
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
private
|
|
117
|
+
|
|
118
|
+
def configure_request_headers(request, custom_headers)
|
|
119
|
+
request.headers["Authorization"] = "Bearer #{@configuration.api_key}" if @configuration.api_key
|
|
120
|
+
request.headers["Content-Type"] = "application/json"
|
|
121
|
+
request.headers["Accept"] = "application/json"
|
|
122
|
+
request.headers["HTTP-Referer"] = @configuration.site_url if @configuration.site_url
|
|
123
|
+
request.headers["X-Title"] = @configuration.site_name if @configuration.site_name
|
|
124
|
+
request.headers.merge!(custom_headers)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def extract_error_message(response)
|
|
128
|
+
body = parse_json(response.body)
|
|
129
|
+
if body.is_a?(Hash) && body["error"]
|
|
130
|
+
error_data = body["error"]
|
|
131
|
+
error_data.is_a?(Hash) ? error_data["message"] || response.body : error_data
|
|
132
|
+
else
|
|
133
|
+
response.body
|
|
134
|
+
end
|
|
135
|
+
rescue JSON::ParserError
|
|
136
|
+
response.body
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def parse_json(body)
|
|
140
|
+
return nil if body.nil? || body.strip.empty?
|
|
141
|
+
|
|
142
|
+
JSON.parse(body)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def build_url(path)
|
|
146
|
+
"#{@configuration.api_base}#{path}"
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def connection
|
|
150
|
+
Faraday.new do |faraday|
|
|
151
|
+
faraday.request :url_encoded
|
|
152
|
+
faraday.options.timeout = @configuration.request_timeout
|
|
153
|
+
faraday.options.open_timeout = @configuration.request_timeout
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenRouter
|
|
4
|
+
# Represents a chat completion request and response from the OpenRouter API.
|
|
5
|
+
# Provides helpers to create completions, handle streaming, and access response data.
|
|
6
|
+
class Completion
|
|
7
|
+
COMPLETIONS_PATH = "/chat/completions"
|
|
8
|
+
|
|
9
|
+
# Role constants for messages.
|
|
10
|
+
module Role
|
|
11
|
+
SYSTEM = "system"
|
|
12
|
+
USER = "user"
|
|
13
|
+
ASSISTANT = "assistant"
|
|
14
|
+
TOOL = "tool"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Finish reason constants.
|
|
18
|
+
module FinishReason
|
|
19
|
+
STOP = "stop"
|
|
20
|
+
LENGTH = "length"
|
|
21
|
+
TOOL_CALLS = "tool_calls"
|
|
22
|
+
CONTENT_FILTER = "content_filter"
|
|
23
|
+
ERROR = "error"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @return [String] The completion identifier
|
|
27
|
+
attr_reader :id
|
|
28
|
+
# @return [String] The model used for this completion
|
|
29
|
+
attr_reader :model
|
|
30
|
+
# @return [Integer] Unix timestamp of when the completion was created
|
|
31
|
+
attr_reader :created
|
|
32
|
+
# @return [String] The object type (chat.completion or chat.completion.chunk)
|
|
33
|
+
attr_reader :object
|
|
34
|
+
# @return [Array<Hash>] The completion choices
|
|
35
|
+
attr_reader :choices
|
|
36
|
+
# @return [Hash, nil] Token usage information
|
|
37
|
+
attr_reader :usage
|
|
38
|
+
# @return [String, nil] System fingerprint from the provider
|
|
39
|
+
attr_reader :system_fingerprint
|
|
40
|
+
|
|
41
|
+
# @param attributes [Hash] Raw attributes from OpenRouter API
|
|
42
|
+
# @param client [OpenRouter::Client] HTTP client to use for subsequent calls
|
|
43
|
+
def initialize(attributes, client: OpenRouter.client)
|
|
44
|
+
@client = client
|
|
45
|
+
reset_attributes(attributes)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
class << self
|
|
49
|
+
# Create a new chat completion.
|
|
50
|
+
# Corresponds to POST /api/v1/chat/completions
|
|
51
|
+
# @param messages [Array<Hash>] The messages to send
|
|
52
|
+
# @param model [String, nil] The model to use (optional, uses user's default if omitted)
|
|
53
|
+
# @param temperature [Float, nil] Sampling temperature (0-2)
|
|
54
|
+
# @param max_tokens [Integer, nil] Maximum tokens to generate
|
|
55
|
+
# @param top_p [Float, nil] Nucleus sampling parameter (0-1)
|
|
56
|
+
# @param top_k [Integer, nil] Top-k sampling parameter
|
|
57
|
+
# @param frequency_penalty [Float, nil] Frequency penalty (-2 to 2)
|
|
58
|
+
# @param presence_penalty [Float, nil] Presence penalty (-2 to 2)
|
|
59
|
+
# @param repetition_penalty [Float, nil] Repetition penalty (0-2)
|
|
60
|
+
# @param stop [String, Array<String>, nil] Stop sequences
|
|
61
|
+
# @param seed [Integer, nil] Random seed for reproducibility
|
|
62
|
+
# @param tools [Array<Hash>, nil] Tools available to the model
|
|
63
|
+
# @param tool_choice [String, Hash, nil] Tool choice preference
|
|
64
|
+
# @param response_format [Hash, nil] Response format specification
|
|
65
|
+
# @param transforms [Array<String>, nil] Message transforms to apply
|
|
66
|
+
# @param models [Array<String>, nil] Model fallback list
|
|
67
|
+
# @param route [String, nil] Routing strategy
|
|
68
|
+
# @param provider [Hash, nil] Provider preferences
|
|
69
|
+
# @param client [OpenRouter::Client] HTTP client
|
|
70
|
+
# @param options [Hash] Additional options (modalities, image_config, plugins, reasoning, etc.)
|
|
71
|
+
# @return [OpenRouter::Completion]
|
|
72
|
+
def create!(messages:, model: nil, temperature: nil, max_tokens: nil, top_p: nil,
|
|
73
|
+
top_k: nil, frequency_penalty: nil, presence_penalty: nil, repetition_penalty: nil,
|
|
74
|
+
stop: nil, seed: nil, tools: nil, tool_choice: nil, response_format: nil,
|
|
75
|
+
transforms: nil, models: nil, route: nil, provider: nil, client: OpenRouter.client, **)
|
|
76
|
+
payload = build_payload(
|
|
77
|
+
messages: messages,
|
|
78
|
+
model: model,
|
|
79
|
+
temperature: temperature,
|
|
80
|
+
max_tokens: max_tokens,
|
|
81
|
+
top_p: top_p,
|
|
82
|
+
top_k: top_k,
|
|
83
|
+
frequency_penalty: frequency_penalty,
|
|
84
|
+
presence_penalty: presence_penalty,
|
|
85
|
+
repetition_penalty: repetition_penalty,
|
|
86
|
+
stop: stop,
|
|
87
|
+
seed: seed,
|
|
88
|
+
tools: tools,
|
|
89
|
+
tool_choice: tool_choice,
|
|
90
|
+
response_format: response_format,
|
|
91
|
+
transforms: transforms,
|
|
92
|
+
models: models,
|
|
93
|
+
route: route,
|
|
94
|
+
provider: provider,
|
|
95
|
+
stream: false,
|
|
96
|
+
**
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
attributes = client.post(COMPLETIONS_PATH, payload)
|
|
100
|
+
new(attributes, client: client)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Stream a chat completion using SSE and yield response chunks as they arrive.
|
|
104
|
+
# Returns a Completion initialized with the final response data.
|
|
105
|
+
# @param messages [Array<Hash>] The messages to send
|
|
106
|
+
# @param model [String, nil] The model to use
|
|
107
|
+
# @param client [OpenRouter::Client] HTTP client
|
|
108
|
+
# @param options [Hash] Additional options (modalities, image_config, plugins, reasoning, etc.)
|
|
109
|
+
# @yield [chunk] yields each parsed chunk Hash from the stream
|
|
110
|
+
# @yieldparam chunk [Hash]
|
|
111
|
+
# @return [OpenRouter::Completion]
|
|
112
|
+
def stream!(messages:, model: nil, temperature: nil, max_tokens: nil, top_p: nil,
|
|
113
|
+
top_k: nil, frequency_penalty: nil, presence_penalty: nil, repetition_penalty: nil,
|
|
114
|
+
stop: nil, seed: nil, tools: nil, tool_choice: nil, response_format: nil,
|
|
115
|
+
transforms: nil, models: nil, route: nil, provider: nil, client: OpenRouter.client, **, &block)
|
|
116
|
+
payload = build_payload(
|
|
117
|
+
messages: messages,
|
|
118
|
+
model: model,
|
|
119
|
+
temperature: temperature,
|
|
120
|
+
max_tokens: max_tokens,
|
|
121
|
+
top_p: top_p,
|
|
122
|
+
top_k: top_k,
|
|
123
|
+
frequency_penalty: frequency_penalty,
|
|
124
|
+
presence_penalty: presence_penalty,
|
|
125
|
+
repetition_penalty: repetition_penalty,
|
|
126
|
+
stop: stop,
|
|
127
|
+
seed: seed,
|
|
128
|
+
tools: tools,
|
|
129
|
+
tool_choice: tool_choice,
|
|
130
|
+
response_format: response_format,
|
|
131
|
+
transforms: transforms,
|
|
132
|
+
models: models,
|
|
133
|
+
route: route,
|
|
134
|
+
provider: provider,
|
|
135
|
+
stream: true,
|
|
136
|
+
**
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
last_data = nil
|
|
140
|
+
collected_content = ""
|
|
141
|
+
final_attributes = {}
|
|
142
|
+
|
|
143
|
+
Stream.new(path: COMPLETIONS_PATH, input: payload, client: client).each do |event|
|
|
144
|
+
data = event["data"]
|
|
145
|
+
next unless data
|
|
146
|
+
|
|
147
|
+
last_data = data
|
|
148
|
+
block&.call(data)
|
|
149
|
+
|
|
150
|
+
# Collect content from delta
|
|
151
|
+
collected_content += data["choices"].first["delta"]["content"] if data["choices"]&.first&.dig("delta", "content")
|
|
152
|
+
|
|
153
|
+
# Capture final attributes
|
|
154
|
+
final_attributes["id"] ||= data["id"]
|
|
155
|
+
final_attributes["model"] ||= data["model"]
|
|
156
|
+
final_attributes["created"] ||= data["created"]
|
|
157
|
+
final_attributes["object"] = "chat.completion"
|
|
158
|
+
|
|
159
|
+
# Capture usage if present (usually in the final chunk)
|
|
160
|
+
final_attributes["usage"] = data["usage"] if data["usage"]
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Build final response structure
|
|
164
|
+
final_attributes["choices"] = [{
|
|
165
|
+
"message" => {
|
|
166
|
+
"role" => "assistant",
|
|
167
|
+
"content" => collected_content
|
|
168
|
+
},
|
|
169
|
+
"finish_reason" => last_data&.dig("choices", 0, "finish_reason") || "stop"
|
|
170
|
+
}]
|
|
171
|
+
|
|
172
|
+
new(final_attributes, client: client)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
private
|
|
176
|
+
|
|
177
|
+
def build_payload(messages:, model:, temperature:, max_tokens:, top_p:, top_k:,
|
|
178
|
+
frequency_penalty:, presence_penalty:, repetition_penalty:,
|
|
179
|
+
stop:, seed:, tools:, tool_choice:, response_format:,
|
|
180
|
+
transforms:, models:, route:, provider:, stream:, **options)
|
|
181
|
+
{
|
|
182
|
+
messages: messages,
|
|
183
|
+
model: model,
|
|
184
|
+
temperature: temperature,
|
|
185
|
+
max_tokens: max_tokens,
|
|
186
|
+
top_p: top_p,
|
|
187
|
+
top_k: top_k,
|
|
188
|
+
frequency_penalty: frequency_penalty,
|
|
189
|
+
presence_penalty: presence_penalty,
|
|
190
|
+
repetition_penalty: repetition_penalty,
|
|
191
|
+
stop: stop,
|
|
192
|
+
seed: seed,
|
|
193
|
+
tools: tools,
|
|
194
|
+
tool_choice: tool_choice,
|
|
195
|
+
response_format: response_format,
|
|
196
|
+
transforms: transforms,
|
|
197
|
+
models: models,
|
|
198
|
+
route: route,
|
|
199
|
+
provider: provider,
|
|
200
|
+
stream: stream,
|
|
201
|
+
**options
|
|
202
|
+
}.compact
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Get the content from the first choice's message.
|
|
207
|
+
# @return [String, nil]
|
|
208
|
+
def content
|
|
209
|
+
@choices&.first&.dig("message", "content")
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Get the role from the first choice's message.
|
|
213
|
+
# @return [String, nil]
|
|
214
|
+
def role
|
|
215
|
+
@choices&.first&.dig("message", "role")
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Get the finish reason from the first choice.
|
|
219
|
+
# @return [String, nil]
|
|
220
|
+
def finish_reason
|
|
221
|
+
@choices&.first&.dig("finish_reason")
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Get tool calls from the first choice's message.
|
|
225
|
+
# @return [Array<Hash>, nil]
|
|
226
|
+
def tool_calls
|
|
227
|
+
@choices&.first&.dig("message", "tool_calls")
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Get images from the first choice's message (for image generation).
|
|
231
|
+
# @return [Array<Hash>, nil]
|
|
232
|
+
def images
|
|
233
|
+
@choices&.first&.dig("message", "images")
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Check if the completion includes images.
|
|
237
|
+
# @return [Boolean]
|
|
238
|
+
def images?
|
|
239
|
+
!images.nil? && !images.empty?
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Get the prompt tokens used.
|
|
243
|
+
# @return [Integer, nil]
|
|
244
|
+
def prompt_tokens
|
|
245
|
+
@usage&.dig("prompt_tokens")
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Get the completion tokens used.
|
|
249
|
+
# @return [Integer, nil]
|
|
250
|
+
def completion_tokens
|
|
251
|
+
@usage&.dig("completion_tokens")
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Get the total tokens used.
|
|
255
|
+
# @return [Integer, nil]
|
|
256
|
+
def total_tokens
|
|
257
|
+
@usage&.dig("total_tokens")
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Check if the completion finished due to stop sequence.
|
|
261
|
+
# @return [Boolean]
|
|
262
|
+
def stopped?
|
|
263
|
+
finish_reason == FinishReason::STOP
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Check if the completion was truncated due to length.
|
|
267
|
+
# @return [Boolean]
|
|
268
|
+
def truncated?
|
|
269
|
+
finish_reason == FinishReason::LENGTH
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Check if the completion includes tool calls.
|
|
273
|
+
# @return [Boolean]
|
|
274
|
+
def tool_calls?
|
|
275
|
+
finish_reason == FinishReason::TOOL_CALLS || !tool_calls.nil?
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# Alias for backwards compatibility.
|
|
279
|
+
alias has_tool_calls? tool_calls?
|
|
280
|
+
|
|
281
|
+
private
|
|
282
|
+
|
|
283
|
+
def reset_attributes(attributes)
|
|
284
|
+
@id = attributes["id"]
|
|
285
|
+
@model = attributes["model"]
|
|
286
|
+
@created = attributes["created"]
|
|
287
|
+
@object = attributes["object"]
|
|
288
|
+
@choices = attributes["choices"]
|
|
289
|
+
@usage = attributes["usage"]
|
|
290
|
+
@system_fingerprint = attributes["system_fingerprint"]
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenRouter
|
|
4
|
+
# Represents credit balance and usage information from the OpenRouter API.
|
|
5
|
+
# Provides helpers to check remaining credits.
|
|
6
|
+
class Credit
|
|
7
|
+
CREDITS_PATH = "/credits"
|
|
8
|
+
|
|
9
|
+
# @return [Float] Total credits available
|
|
10
|
+
attr_reader :total_credits
|
|
11
|
+
# @return [Float] Total credits used
|
|
12
|
+
attr_reader :total_usage
|
|
13
|
+
# @return [Float] Remaining credits
|
|
14
|
+
attr_reader :remaining
|
|
15
|
+
|
|
16
|
+
# @param attributes [Hash] Raw attributes from OpenRouter API
|
|
17
|
+
# @param client [OpenRouter::Client] HTTP client
|
|
18
|
+
def initialize(attributes, client: OpenRouter.client)
|
|
19
|
+
@client = client
|
|
20
|
+
reset_attributes(attributes)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class << self
|
|
24
|
+
# Fetch the current credit balance.
|
|
25
|
+
# Corresponds to GET /api/v1/credits
|
|
26
|
+
# @param client [OpenRouter::Client] HTTP client
|
|
27
|
+
# @return [OpenRouter::Credit]
|
|
28
|
+
def fetch(client: OpenRouter.client)
|
|
29
|
+
response = client.get(CREDITS_PATH)
|
|
30
|
+
new(response["data"] || response, client: client)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Get remaining credits as a simple value.
|
|
34
|
+
# @param client [OpenRouter::Client] HTTP client
|
|
35
|
+
# @return [Float]
|
|
36
|
+
def remaining(client: OpenRouter.client)
|
|
37
|
+
fetch(client: client).remaining
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Check if credits are low (less than 10% remaining).
|
|
42
|
+
# @return [Boolean]
|
|
43
|
+
def low?
|
|
44
|
+
return false if @total_credits.nil? || @total_credits.zero?
|
|
45
|
+
|
|
46
|
+
(@remaining / @total_credits) < 0.1
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Check if credits are exhausted.
|
|
50
|
+
# @return [Boolean]
|
|
51
|
+
def exhausted?
|
|
52
|
+
@remaining.nil? || @remaining <= 0
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Get the usage percentage.
|
|
56
|
+
# @return [Float, nil]
|
|
57
|
+
def usage_percentage
|
|
58
|
+
return nil if @total_credits.nil? || @total_credits.zero?
|
|
59
|
+
|
|
60
|
+
(@total_usage / @total_credits) * 100
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def reset_attributes(attributes)
|
|
66
|
+
@total_credits = attributes["total_credits"]&.to_f
|
|
67
|
+
@total_usage = attributes["total_usage"].to_f
|
|
68
|
+
@remaining = attributes["remaining"]&.to_f || (@total_credits.to_f - @total_usage.to_f)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|