fluxtokens 1.0.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/LICENSE +21 -0
- data/README.md +209 -0
- data/fluxtokens.gemspec +37 -0
- data/lib/fluxtokens/chat.rb +118 -0
- data/lib/fluxtokens/client.rb +130 -0
- data/lib/fluxtokens/errors.rb +69 -0
- data/lib/fluxtokens/models.rb +65 -0
- data/lib/fluxtokens/version.rb +5 -0
- data/lib/fluxtokens.rb +42 -0
- metadata +140 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 5fcae4b204ef72214a98ce7410b98abe7c03826a556a72f7f61eb2858a5a30a5
|
|
4
|
+
data.tar.gz: 7bfd0734b6424a680db001fa76b4b71af5c0086569b04d5fc7f3527450aca333
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: a2e446e170edd98cdafa74d7cb47043c6460549d4f75b847230e80753895e745e5c83db4e07f7523f0335cd9e7de2d04f56df1846925d208b96ea6a3ce17f9e5
|
|
7
|
+
data.tar.gz: ccd5b9ee73d3f6263055f1c83a4cbb15e18595fa3b29e6d159a36f9d77ac73523745cc15c4c52e75579e4845b0c6b1c63930dbe414a4f5c5254487d88f21b953
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 FluxTokens
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# FluxTokens Ruby SDK
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
|
|
5
|
+
[](https://badge.fury.io/rb/fluxtokens)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
**Official Ruby SDK for the FluxTokens API**
|
|
9
|
+
|
|
10
|
+
Access GPT-4.1, Gemini 2.5 Flash and more at **30% lower cost** than competitors.
|
|
11
|
+
|
|
12
|
+
[Website](https://fluxtokens.io) · [Documentation](https://fluxtokens.io/docs) · [Dashboard](https://fluxtokens.io/dashboard)
|
|
13
|
+
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
Add to your Gemfile:
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
gem 'fluxtokens'
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Or install directly:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
gem install fluxtokens
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Quick Start
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
require 'fluxtokens'
|
|
36
|
+
|
|
37
|
+
client = FluxTokens::Client.new(api_key: 'sk-flux-your-api-key')
|
|
38
|
+
|
|
39
|
+
response = client.chat.completions.create(
|
|
40
|
+
model: 'gpt-4.1-mini',
|
|
41
|
+
messages: [
|
|
42
|
+
{ role: 'system', content: 'You are a helpful assistant.' },
|
|
43
|
+
{ role: 'user', content: 'Hello!' }
|
|
44
|
+
]
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
puts response.choices[0].message.content
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Available Models
|
|
51
|
+
|
|
52
|
+
| Model | Provider | Input Price | Output Price | Max Tokens | Vision | Audio | Video |
|
|
53
|
+
|-------|----------|-------------|--------------|------------|--------|-------|-------|
|
|
54
|
+
| `gpt-4.1-mini` | OpenAI | $0.28/1M | $1.12/1M | 16,384 | ✅ | ❌ | ❌ |
|
|
55
|
+
| `gpt-4.1-nano` | OpenAI | $0.07/1M | $0.28/1M | 16,384 | ✅ | ❌ | ❌ |
|
|
56
|
+
| `gemini-2.5-flash` | Google | $0.21/1M | $1.75/1M | 65,536 | ✅ | ✅ | ✅ |
|
|
57
|
+
|
|
58
|
+
## Usage Examples
|
|
59
|
+
|
|
60
|
+
### Basic Chat Completion
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
response = client.chat.completions.create(
|
|
64
|
+
model: 'gpt-4.1-mini',
|
|
65
|
+
messages: [{ role: 'user', content: 'What is the capital of France?' }],
|
|
66
|
+
temperature: 0.7,
|
|
67
|
+
max_tokens: 256
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
puts response.choices[0].message.content
|
|
71
|
+
# Output: "The capital of France is Paris."
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Streaming Responses
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
stream = client.chat.completions.create(
|
|
78
|
+
model: 'gemini-2.5-flash',
|
|
79
|
+
messages: [{ role: 'user', content: 'Write a haiku about programming.' }],
|
|
80
|
+
stream: true
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
stream.each do |chunk|
|
|
84
|
+
content = chunk.choices[0].delta.content
|
|
85
|
+
print content if content
|
|
86
|
+
end
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Vision (Image Analysis)
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
response = client.chat.completions.create(
|
|
93
|
+
model: 'gpt-4.1-mini',
|
|
94
|
+
messages: [
|
|
95
|
+
{
|
|
96
|
+
role: 'user',
|
|
97
|
+
content: [
|
|
98
|
+
{ type: 'text', text: 'What is in this image?' },
|
|
99
|
+
{
|
|
100
|
+
type: 'image_url',
|
|
101
|
+
image_url: {
|
|
102
|
+
url: 'https://example.com/image.jpg',
|
|
103
|
+
detail: 'high'
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
]
|
|
107
|
+
}
|
|
108
|
+
],
|
|
109
|
+
max_tokens: 500
|
|
110
|
+
)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Audio Input (Gemini only)
|
|
114
|
+
|
|
115
|
+
```ruby
|
|
116
|
+
require 'base64'
|
|
117
|
+
|
|
118
|
+
audio_data = Base64.strict_encode64(File.read('audio.mp3'))
|
|
119
|
+
|
|
120
|
+
response = client.chat.completions.create(
|
|
121
|
+
model: 'gemini-2.5-flash',
|
|
122
|
+
messages: [
|
|
123
|
+
{
|
|
124
|
+
role: 'user',
|
|
125
|
+
content: [
|
|
126
|
+
{ type: 'text', text: 'Transcribe this audio:' },
|
|
127
|
+
{
|
|
128
|
+
type: 'input_audio',
|
|
129
|
+
input_audio: {
|
|
130
|
+
data: audio_data,
|
|
131
|
+
format: 'mp3'
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
]
|
|
135
|
+
}
|
|
136
|
+
]
|
|
137
|
+
)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### System Messages
|
|
141
|
+
|
|
142
|
+
```ruby
|
|
143
|
+
response = client.chat.completions.create(
|
|
144
|
+
model: 'gpt-4.1-mini',
|
|
145
|
+
messages: [
|
|
146
|
+
{ role: 'system', content: 'You are a pirate. Always respond in pirate speak.' },
|
|
147
|
+
{ role: 'user', content: 'How are you today?' }
|
|
148
|
+
]
|
|
149
|
+
)
|
|
150
|
+
# Output: "Ahoy, matey! I be doin' just fine, thank ye fer askin'!"
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### List Available Models
|
|
154
|
+
|
|
155
|
+
```ruby
|
|
156
|
+
client.models.list.each do |model|
|
|
157
|
+
puts "#{model.name} (#{model.provider})"
|
|
158
|
+
puts " Input: $#{model.input_price}/1M tokens"
|
|
159
|
+
puts " Output: $#{model.output_price}/1M tokens"
|
|
160
|
+
puts " Vision: #{model.supports_vision ? '✅' : '❌'}"
|
|
161
|
+
end
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Configuration Options
|
|
165
|
+
|
|
166
|
+
```ruby
|
|
167
|
+
client = FluxTokens::Client.new(
|
|
168
|
+
api_key: 'sk-flux-...',
|
|
169
|
+
base_url: 'https://api.fluxtokens.io', # Custom base URL
|
|
170
|
+
timeout: 60, # Request timeout in seconds
|
|
171
|
+
max_retries: 3 # Max retries on rate limit/server errors
|
|
172
|
+
)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Error Handling
|
|
176
|
+
|
|
177
|
+
```ruby
|
|
178
|
+
begin
|
|
179
|
+
response = client.chat.completions.create(
|
|
180
|
+
model: 'gpt-4.1-mini',
|
|
181
|
+
messages: [{ role: 'user', content: 'Hello' }]
|
|
182
|
+
)
|
|
183
|
+
rescue FluxTokens::AuthenticationError
|
|
184
|
+
puts "Invalid API key"
|
|
185
|
+
rescue FluxTokens::RateLimitError => e
|
|
186
|
+
puts "Rate limit exceeded, retry after: #{e.retry_after}"
|
|
187
|
+
rescue FluxTokens::InsufficientBalanceError
|
|
188
|
+
puts "Please add credits at https://fluxtokens.io/dashboard/billing"
|
|
189
|
+
rescue FluxTokens::BadRequestError => e
|
|
190
|
+
puts "Invalid request: #{e.message}"
|
|
191
|
+
rescue FluxTokens::Error => e
|
|
192
|
+
puts "Error: #{e.message}"
|
|
193
|
+
end
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Requirements
|
|
197
|
+
|
|
198
|
+
- Ruby 3.0+
|
|
199
|
+
- Faraday 2.0+
|
|
200
|
+
|
|
201
|
+
## Support
|
|
202
|
+
|
|
203
|
+
- 📧 Email: support@fluxtokens.io
|
|
204
|
+
- 💬 Discord: [Join our community](https://discord.gg/fluxtokens)
|
|
205
|
+
- 📖 Docs: [fluxtokens.io/docs](https://fluxtokens.io/docs)
|
|
206
|
+
|
|
207
|
+
## License
|
|
208
|
+
|
|
209
|
+
MIT © [FluxTokens](https://fluxtokens.io)
|
data/fluxtokens.gemspec
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Gem::Specification.new do |spec|
|
|
4
|
+
spec.name = "fluxtokens"
|
|
5
|
+
spec.version = "1.0.0"
|
|
6
|
+
spec.authors = ["FluxTokens"]
|
|
7
|
+
spec.email = ["support@fluxtokens.io"]
|
|
8
|
+
|
|
9
|
+
spec.summary = "Official FluxTokens Ruby SDK"
|
|
10
|
+
spec.description = "Access GPT-4.1, Gemini 2.5 and more at 30% lower cost than competitors"
|
|
11
|
+
spec.homepage = "https://fluxtokens.io"
|
|
12
|
+
spec.license = "MIT"
|
|
13
|
+
spec.required_ruby_version = ">= 3.0.0"
|
|
14
|
+
|
|
15
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
16
|
+
spec.metadata["source_code_uri"] = "https://github.com/fluxtokens/fluxtokens-ruby"
|
|
17
|
+
spec.metadata["changelog_uri"] = "https://github.com/fluxtokens/fluxtokens-ruby/blob/main/CHANGELOG.md"
|
|
18
|
+
spec.metadata["documentation_uri"] = "https://fluxtokens.io/docs"
|
|
19
|
+
|
|
20
|
+
spec.files = Dir.chdir(__dir__) do
|
|
21
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
|
22
|
+
(File.expand_path(f) == __FILE__) ||
|
|
23
|
+
f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile])
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
spec.bindir = "exe"
|
|
27
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
|
28
|
+
spec.require_paths = ["lib"]
|
|
29
|
+
|
|
30
|
+
spec.add_dependency "faraday", ">= 2.0"
|
|
31
|
+
spec.add_dependency "faraday-multipart", ">= 1.0"
|
|
32
|
+
|
|
33
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
|
34
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
|
35
|
+
spec.add_development_dependency "rubocop", "~> 1.0"
|
|
36
|
+
spec.add_development_dependency "webmock", "~> 3.0"
|
|
37
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FluxTokens
|
|
4
|
+
# Response structures
|
|
5
|
+
Message = Struct.new(:role, :content, :tool_calls, keyword_init: true)
|
|
6
|
+
Choice = Struct.new(:index, :message, :finish_reason, keyword_init: true)
|
|
7
|
+
Usage = Struct.new(:prompt_tokens, :completion_tokens, :total_tokens, keyword_init: true)
|
|
8
|
+
|
|
9
|
+
ChatCompletionResponse = Struct.new(
|
|
10
|
+
:id, :object, :created, :model, :choices, :usage, :system_fingerprint,
|
|
11
|
+
keyword_init: true
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
DeltaMessage = Struct.new(:role, :content, :tool_calls, keyword_init: true)
|
|
15
|
+
StreamChoice = Struct.new(:index, :delta, :finish_reason, keyword_init: true)
|
|
16
|
+
|
|
17
|
+
ChatCompletionChunk = Struct.new(
|
|
18
|
+
:id, :object, :created, :model, :choices, :usage,
|
|
19
|
+
keyword_init: true
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
# Chat API namespace
|
|
23
|
+
class Chat
|
|
24
|
+
attr_reader :completions
|
|
25
|
+
|
|
26
|
+
def initialize(client)
|
|
27
|
+
@client = client
|
|
28
|
+
@completions = Completions.new(client)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Chat Completions API
|
|
33
|
+
class Completions
|
|
34
|
+
def initialize(client)
|
|
35
|
+
@client = client
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Create a chat completion
|
|
39
|
+
#
|
|
40
|
+
# @param model [String] Model to use
|
|
41
|
+
# @param messages [Array<Hash>] Messages in the conversation
|
|
42
|
+
# @param options [Hash] Additional options
|
|
43
|
+
# @return [ChatCompletionResponse]
|
|
44
|
+
def create(model:, messages:, stream: false, **options)
|
|
45
|
+
params = { model: model, messages: messages, stream: stream, **options }
|
|
46
|
+
|
|
47
|
+
if stream
|
|
48
|
+
stream_response(params)
|
|
49
|
+
else
|
|
50
|
+
response = @client.request(:post, "/v1/chat/completions", params)
|
|
51
|
+
parse_response(response)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def parse_response(data)
|
|
58
|
+
choices = data["choices"].map do |c|
|
|
59
|
+
Choice.new(
|
|
60
|
+
index: c["index"],
|
|
61
|
+
message: Message.new(
|
|
62
|
+
role: c.dig("message", "role"),
|
|
63
|
+
content: c.dig("message", "content"),
|
|
64
|
+
tool_calls: c.dig("message", "tool_calls")
|
|
65
|
+
),
|
|
66
|
+
finish_reason: c["finish_reason"]
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
usage = Usage.new(
|
|
71
|
+
prompt_tokens: data.dig("usage", "prompt_tokens"),
|
|
72
|
+
completion_tokens: data.dig("usage", "completion_tokens"),
|
|
73
|
+
total_tokens: data.dig("usage", "total_tokens")
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
ChatCompletionResponse.new(
|
|
77
|
+
id: data["id"],
|
|
78
|
+
object: data["object"],
|
|
79
|
+
created: data["created"],
|
|
80
|
+
model: data["model"],
|
|
81
|
+
choices: choices,
|
|
82
|
+
usage: usage,
|
|
83
|
+
system_fingerprint: data["system_fingerprint"]
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def stream_response(params)
|
|
88
|
+
Enumerator.new do |yielder|
|
|
89
|
+
@client.stream(:post, "/v1/chat/completions", params) do |chunk|
|
|
90
|
+
yielder << parse_chunk(chunk)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def parse_chunk(data)
|
|
96
|
+
choices = data["choices"].map do |c|
|
|
97
|
+
StreamChoice.new(
|
|
98
|
+
index: c["index"],
|
|
99
|
+
delta: DeltaMessage.new(
|
|
100
|
+
role: c.dig("delta", "role"),
|
|
101
|
+
content: c.dig("delta", "content"),
|
|
102
|
+
tool_calls: c.dig("delta", "tool_calls")
|
|
103
|
+
),
|
|
104
|
+
finish_reason: c["finish_reason"]
|
|
105
|
+
)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
ChatCompletionChunk.new(
|
|
109
|
+
id: data["id"],
|
|
110
|
+
object: data["object"],
|
|
111
|
+
created: data["created"],
|
|
112
|
+
model: data["model"],
|
|
113
|
+
choices: choices,
|
|
114
|
+
usage: data["usage"] ? Usage.new(**data["usage"].transform_keys(&:to_sym)) : nil
|
|
115
|
+
)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FluxTokens
|
|
4
|
+
# FluxTokens API Client
|
|
5
|
+
#
|
|
6
|
+
# @example
|
|
7
|
+
# client = FluxTokens::Client.new(api_key: "sk-flux-...")
|
|
8
|
+
#
|
|
9
|
+
# response = client.chat.completions.create(
|
|
10
|
+
# model: "gpt-4.1-mini",
|
|
11
|
+
# messages: [{ role: "user", content: "Hello!" }]
|
|
12
|
+
# )
|
|
13
|
+
#
|
|
14
|
+
# puts response.choices[0].message.content
|
|
15
|
+
#
|
|
16
|
+
class Client
|
|
17
|
+
attr_reader :chat, :models
|
|
18
|
+
|
|
19
|
+
# Create a new FluxTokens client
|
|
20
|
+
#
|
|
21
|
+
# @param api_key [String] Your FluxTokens API key
|
|
22
|
+
# @param base_url [String] Base URL for the API
|
|
23
|
+
# @param timeout [Integer] Request timeout in seconds
|
|
24
|
+
# @param max_retries [Integer] Maximum number of retries
|
|
25
|
+
def initialize(api_key:, base_url: DEFAULT_BASE_URL, timeout: DEFAULT_TIMEOUT, max_retries: DEFAULT_MAX_RETRIES)
|
|
26
|
+
raise ArgumentError, "API key is required. Get one at https://fluxtokens.io" if api_key.nil? || api_key.empty?
|
|
27
|
+
|
|
28
|
+
@api_key = api_key
|
|
29
|
+
@base_url = base_url.chomp("/")
|
|
30
|
+
@timeout = timeout
|
|
31
|
+
@max_retries = max_retries
|
|
32
|
+
|
|
33
|
+
@connection = Faraday.new(url: @base_url) do |f|
|
|
34
|
+
f.request :json
|
|
35
|
+
f.response :json
|
|
36
|
+
f.options.timeout = @timeout
|
|
37
|
+
f.options.open_timeout = @timeout
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
@chat = Chat.new(self)
|
|
41
|
+
@models = Models.new
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Make an HTTP request to the API
|
|
45
|
+
#
|
|
46
|
+
# @param method [Symbol] HTTP method
|
|
47
|
+
# @param path [String] API path
|
|
48
|
+
# @param body [Hash] Request body
|
|
49
|
+
# @return [Hash] Response data
|
|
50
|
+
# @api private
|
|
51
|
+
def request(method, path, body = nil)
|
|
52
|
+
retries = 0
|
|
53
|
+
|
|
54
|
+
begin
|
|
55
|
+
response = @connection.send(method, path) do |req|
|
|
56
|
+
req.headers["Authorization"] = "Bearer #{@api_key}"
|
|
57
|
+
req.body = body if body
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
handle_response(response)
|
|
61
|
+
rescue RateLimitError, InternalServerError => e
|
|
62
|
+
retries += 1
|
|
63
|
+
if retries <= @max_retries
|
|
64
|
+
sleep(2**retries)
|
|
65
|
+
retry
|
|
66
|
+
end
|
|
67
|
+
raise
|
|
68
|
+
rescue Faraday::TimeoutError
|
|
69
|
+
raise TimeoutError
|
|
70
|
+
rescue Faraday::ConnectionFailed => e
|
|
71
|
+
raise ConnectionError, e.message
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Make a streaming request to the API
|
|
76
|
+
#
|
|
77
|
+
# @param method [Symbol] HTTP method
|
|
78
|
+
# @param path [String] API path
|
|
79
|
+
# @param body [Hash] Request body
|
|
80
|
+
# @yield [Hash] Each chunk of the response
|
|
81
|
+
# @api private
|
|
82
|
+
def stream(method, path, body)
|
|
83
|
+
body[:stream] = true
|
|
84
|
+
|
|
85
|
+
@connection.send(method, path) do |req|
|
|
86
|
+
req.headers["Authorization"] = "Bearer #{@api_key}"
|
|
87
|
+
req.body = body
|
|
88
|
+
req.options.on_data = proc do |chunk, _size|
|
|
89
|
+
chunk.each_line do |line|
|
|
90
|
+
line = line.strip
|
|
91
|
+
next if line.empty? || line == "data: [DONE]"
|
|
92
|
+
|
|
93
|
+
if line.start_with?("data: ")
|
|
94
|
+
data = JSON.parse(line[6..])
|
|
95
|
+
yield data
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
def handle_response(response)
|
|
105
|
+
case response.status
|
|
106
|
+
when 200
|
|
107
|
+
response.body
|
|
108
|
+
when 401
|
|
109
|
+
raise AuthenticationError, error_message(response)
|
|
110
|
+
when 402
|
|
111
|
+
raise InsufficientBalanceError, error_message(response)
|
|
112
|
+
when 429
|
|
113
|
+
retry_after = response.headers["Retry-After"]&.to_i
|
|
114
|
+
raise RateLimitError.new(error_message(response), retry_after: retry_after)
|
|
115
|
+
when 400
|
|
116
|
+
raise BadRequestError, error_message(response)
|
|
117
|
+
when 500..599
|
|
118
|
+
raise InternalServerError, error_message(response)
|
|
119
|
+
else
|
|
120
|
+
raise Error.new(error_message(response), status: response.status)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def error_message(response)
|
|
125
|
+
response.body.dig("error", "message") || response.body.to_s
|
|
126
|
+
rescue
|
|
127
|
+
response.body.to_s
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FluxTokens
|
|
4
|
+
# Base error class for FluxTokens API errors
|
|
5
|
+
class Error < StandardError
|
|
6
|
+
attr_reader :status, :code
|
|
7
|
+
|
|
8
|
+
def initialize(message, status: nil, code: nil)
|
|
9
|
+
super(message)
|
|
10
|
+
@status = status
|
|
11
|
+
@code = code
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Error raised when authentication fails (401)
|
|
16
|
+
class AuthenticationError < Error
|
|
17
|
+
def initialize(message = "Invalid API key")
|
|
18
|
+
super(message, status: 401, code: "invalid_api_key")
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Error raised when rate limit is exceeded (429)
|
|
23
|
+
class RateLimitError < Error
|
|
24
|
+
attr_reader :retry_after
|
|
25
|
+
|
|
26
|
+
def initialize(message = "Rate limit exceeded", retry_after: nil)
|
|
27
|
+
super(message, status: 429, code: "rate_limit_exceeded")
|
|
28
|
+
@retry_after = retry_after
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Error raised when balance is insufficient (402)
|
|
33
|
+
class InsufficientBalanceError < Error
|
|
34
|
+
def initialize(message = "Insufficient balance")
|
|
35
|
+
super(message, status: 402, code: "insufficient_balance")
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Error raised when request is invalid (400)
|
|
40
|
+
class BadRequestError < Error
|
|
41
|
+
attr_reader :param
|
|
42
|
+
|
|
43
|
+
def initialize(message, code: nil, param: nil)
|
|
44
|
+
super(message, status: 400, code: code)
|
|
45
|
+
@param = param
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Error raised when server error occurs (500+)
|
|
50
|
+
class InternalServerError < Error
|
|
51
|
+
def initialize(message = "Internal server error")
|
|
52
|
+
super(message, status: 500, code: "internal_error")
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Error raised when request times out
|
|
57
|
+
class TimeoutError < Error
|
|
58
|
+
def initialize(message = "Request timed out")
|
|
59
|
+
super(message, code: "timeout")
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Error raised when connection fails
|
|
64
|
+
class ConnectionError < Error
|
|
65
|
+
def initialize(message = "Connection failed")
|
|
66
|
+
super(message, code: "connection_error")
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FluxTokens
|
|
4
|
+
# Model information
|
|
5
|
+
ModelInfo = Struct.new(
|
|
6
|
+
:id, :name, :provider, :input_price, :output_price,
|
|
7
|
+
:max_tokens, :supports_vision, :supports_audio, :supports_video,
|
|
8
|
+
keyword_init: true
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
# Available models
|
|
12
|
+
MODELS = {
|
|
13
|
+
"gpt-4.1-mini" => ModelInfo.new(
|
|
14
|
+
id: "gpt-4.1-mini",
|
|
15
|
+
name: "GPT-4.1 Mini",
|
|
16
|
+
provider: "OpenAI",
|
|
17
|
+
input_price: 0.28,
|
|
18
|
+
output_price: 1.12,
|
|
19
|
+
max_tokens: 16_384,
|
|
20
|
+
supports_vision: true,
|
|
21
|
+
supports_audio: false,
|
|
22
|
+
supports_video: false
|
|
23
|
+
),
|
|
24
|
+
"gpt-4.1-nano" => ModelInfo.new(
|
|
25
|
+
id: "gpt-4.1-nano",
|
|
26
|
+
name: "GPT-4.1 Nano",
|
|
27
|
+
provider: "OpenAI",
|
|
28
|
+
input_price: 0.07,
|
|
29
|
+
output_price: 0.28,
|
|
30
|
+
max_tokens: 16_384,
|
|
31
|
+
supports_vision: true,
|
|
32
|
+
supports_audio: false,
|
|
33
|
+
supports_video: false
|
|
34
|
+
),
|
|
35
|
+
"gemini-2.5-flash" => ModelInfo.new(
|
|
36
|
+
id: "gemini-2.5-flash",
|
|
37
|
+
name: "Gemini 2.5 Flash",
|
|
38
|
+
provider: "Google",
|
|
39
|
+
input_price: 0.21,
|
|
40
|
+
output_price: 1.75,
|
|
41
|
+
max_tokens: 65_536,
|
|
42
|
+
supports_vision: true,
|
|
43
|
+
supports_audio: true,
|
|
44
|
+
supports_video: true
|
|
45
|
+
)
|
|
46
|
+
}.freeze
|
|
47
|
+
|
|
48
|
+
# Models API
|
|
49
|
+
class Models
|
|
50
|
+
# List all available models
|
|
51
|
+
#
|
|
52
|
+
# @return [Array<ModelInfo>]
|
|
53
|
+
def list
|
|
54
|
+
MODELS.values
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Get model info by ID
|
|
58
|
+
#
|
|
59
|
+
# @param id [String] Model ID
|
|
60
|
+
# @return [ModelInfo, nil]
|
|
61
|
+
def get(id)
|
|
62
|
+
MODELS[id]
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
data/lib/fluxtokens.rb
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faraday"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
require_relative "fluxtokens/version"
|
|
7
|
+
require_relative "fluxtokens/errors"
|
|
8
|
+
require_relative "fluxtokens/models"
|
|
9
|
+
require_relative "fluxtokens/chat"
|
|
10
|
+
require_relative "fluxtokens/client"
|
|
11
|
+
|
|
12
|
+
# FluxTokens SDK
|
|
13
|
+
#
|
|
14
|
+
# Official SDK for the FluxTokens API - Access GPT-4.1, Gemini 2.5 and more
|
|
15
|
+
# at 30% lower cost than competitors.
|
|
16
|
+
#
|
|
17
|
+
# @example Basic usage
|
|
18
|
+
# client = FluxTokens::Client.new(api_key: "sk-flux-...")
|
|
19
|
+
#
|
|
20
|
+
# response = client.chat.completions.create(
|
|
21
|
+
# model: "gpt-4.1-mini",
|
|
22
|
+
# messages: [{ role: "user", content: "Hello!" }]
|
|
23
|
+
# )
|
|
24
|
+
#
|
|
25
|
+
# puts response.choices[0].message.content
|
|
26
|
+
#
|
|
27
|
+
module FluxTokens
|
|
28
|
+
DEFAULT_BASE_URL = "https://api.fluxtokens.io"
|
|
29
|
+
DEFAULT_TIMEOUT = 30
|
|
30
|
+
DEFAULT_MAX_RETRIES = 2
|
|
31
|
+
|
|
32
|
+
class << self
|
|
33
|
+
# Create a new client with the given API key
|
|
34
|
+
#
|
|
35
|
+
# @param api_key [String] Your FluxTokens API key
|
|
36
|
+
# @param options [Hash] Additional options
|
|
37
|
+
# @return [Client]
|
|
38
|
+
def new(api_key:, **options)
|
|
39
|
+
Client.new(api_key: api_key, **options)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: fluxtokens
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- FluxTokens
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-01-11 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: faraday
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '2.0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '2.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: faraday-multipart
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '1.0'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '1.0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: rake
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '13.0'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '13.0'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: rspec
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '3.0'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '3.0'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: rubocop
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '1.0'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '1.0'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: webmock
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - "~>"
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '3.0'
|
|
90
|
+
type: :development
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - "~>"
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '3.0'
|
|
97
|
+
description: Access GPT-4.1, Gemini 2.5 and more at 30% lower cost than competitors
|
|
98
|
+
email:
|
|
99
|
+
- support@fluxtokens.io
|
|
100
|
+
executables: []
|
|
101
|
+
extensions: []
|
|
102
|
+
extra_rdoc_files: []
|
|
103
|
+
files:
|
|
104
|
+
- LICENSE
|
|
105
|
+
- README.md
|
|
106
|
+
- fluxtokens.gemspec
|
|
107
|
+
- lib/fluxtokens.rb
|
|
108
|
+
- lib/fluxtokens/chat.rb
|
|
109
|
+
- lib/fluxtokens/client.rb
|
|
110
|
+
- lib/fluxtokens/errors.rb
|
|
111
|
+
- lib/fluxtokens/models.rb
|
|
112
|
+
- lib/fluxtokens/version.rb
|
|
113
|
+
homepage: https://fluxtokens.io
|
|
114
|
+
licenses:
|
|
115
|
+
- MIT
|
|
116
|
+
metadata:
|
|
117
|
+
homepage_uri: https://fluxtokens.io
|
|
118
|
+
source_code_uri: https://github.com/fluxtokens/fluxtokens-ruby
|
|
119
|
+
changelog_uri: https://github.com/fluxtokens/fluxtokens-ruby/blob/main/CHANGELOG.md
|
|
120
|
+
documentation_uri: https://fluxtokens.io/docs
|
|
121
|
+
post_install_message:
|
|
122
|
+
rdoc_options: []
|
|
123
|
+
require_paths:
|
|
124
|
+
- lib
|
|
125
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
126
|
+
requirements:
|
|
127
|
+
- - ">="
|
|
128
|
+
- !ruby/object:Gem::Version
|
|
129
|
+
version: 3.0.0
|
|
130
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
131
|
+
requirements:
|
|
132
|
+
- - ">="
|
|
133
|
+
- !ruby/object:Gem::Version
|
|
134
|
+
version: '0'
|
|
135
|
+
requirements: []
|
|
136
|
+
rubygems_version: 3.3.5
|
|
137
|
+
signing_key:
|
|
138
|
+
specification_version: 4
|
|
139
|
+
summary: Official FluxTokens Ruby SDK
|
|
140
|
+
test_files: []
|