aihub2api 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/LICENSE.txt +21 -0
- data/README.md +78 -0
- data/exe/aihub2api +54 -0
- data/lib/aihub2api/client.rb +127 -0
- data/lib/aihub2api/errors.rb +17 -0
- data/lib/aihub2api/version.rb +3 -0
- data/lib/aihub2api.rb +6 -0
- metadata +58 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 5c217a92bc821e354fc53232edfc94dce1648768f4c4b74df6deebde8d8d457a
|
|
4
|
+
data.tar.gz: 2453613e660708d21d575b732ba942272e5ef90d94f175fd3e2c95923734e738
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: cd0a82f063d233e77f77cf3538700e0b49b152d17dccada3cac650fb750d17c5fc3b6e1e1cc1124afe87c8f75c7a13b0ac5e2b01a468c7ee5a56f737de96230f
|
|
7
|
+
data.tar.gz: d86e7798444be0d2231346e55dd8160398a07cd78a597df0ccccc25ce628dfc11a32f4fd2886f7b475c443ac2ac965a7f962f7eeb7abee753d510d8a464427ca
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AIHub
|
|
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,78 @@
|
|
|
1
|
+
# AIHub2API for Ruby
|
|
2
|
+
|
|
3
|
+
`aihub2api` is a small, dependency-free Ruby client and command-line preflight
|
|
4
|
+
checker for the [AIHub OpenAI-compatible API](https://aihub2api.cloud/?utm_source=rubygems&utm_medium=readme&utm_campaign=westdev).
|
|
5
|
+
It can check the public health endpoint, list models with your own API key, and
|
|
6
|
+
send Chat Completions or Responses requests. API keys are read from the
|
|
7
|
+
environment and are never printed by the client.
|
|
8
|
+
|
|
9
|
+
AIHub is an independent third-party gateway. It is not affiliated with or
|
|
10
|
+
endorsed by OpenAI. The service is intended only for users outside mainland
|
|
11
|
+
China.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
gem install aihub2api
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Preflight check
|
|
20
|
+
|
|
21
|
+
The health check does not need a key:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
aihub2api check
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
To query the authenticated model list:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
export AIHUB_API_KEY='your-key'
|
|
31
|
+
aihub2api models
|
|
32
|
+
aihub2api check --model gpt-5.6-sol
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The AIHub operator reports availability of `gpt-5.6-sol`. Treat the output of
|
|
36
|
+
the authenticated `/v1/models` endpoint as authoritative for your account at
|
|
37
|
+
the time of use.
|
|
38
|
+
|
|
39
|
+
## Ruby usage
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
require "aihub2api"
|
|
43
|
+
|
|
44
|
+
client = AIHub2API::Client.new(api_key: ENV.fetch("AIHUB_API_KEY"))
|
|
45
|
+
|
|
46
|
+
puts client.model_ids
|
|
47
|
+
|
|
48
|
+
result = client.chat_completions(
|
|
49
|
+
model: "gpt-5.6-sol",
|
|
50
|
+
messages: [{ role: "user", content: "Reply with exactly: connection ok" }],
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
puts result.dig("choices", 0, "message", "content")
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
You can test another OpenAI-compatible endpoint without changing code:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
AIHUB_BASE_URL='https://your-gateway.example' aihub2api check
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Billing terminology
|
|
63
|
+
|
|
64
|
+
On AIHub, `0.2x` is an internal balance-conversion convention: CNY 0.20 covers
|
|
65
|
+
USD 1.00 of official-list-price-equivalent usage, so CNY 2 covers USD 10 of
|
|
66
|
+
that usage. It is not a foreign-exchange rate and not an OpenAI discount.
|
|
67
|
+
Actual charges and model availability must be checked in the AIHub dashboard.
|
|
68
|
+
|
|
69
|
+
## Security notes
|
|
70
|
+
|
|
71
|
+
- Use environment variables or a secret manager for API keys.
|
|
72
|
+
- Do not commit keys to a repository or pass them as command-line arguments.
|
|
73
|
+
- The client requires HTTPS, except for explicit loopback testing.
|
|
74
|
+
- Pin the gem version in production and review changes before upgrading.
|
|
75
|
+
|
|
76
|
+
The initial implementation was drafted with AI assistance and validated with
|
|
77
|
+
the included automated tests. Bug reports should include Ruby version and a
|
|
78
|
+
redacted response status, never an API key.
|
data/exe/aihub2api
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
require "aihub2api"
|
|
5
|
+
|
|
6
|
+
command = ARGV.shift || "help"
|
|
7
|
+
options = { base_url: ENV.fetch("AIHUB_BASE_URL", AIHub2API::Client::DEFAULT_BASE_URL) }
|
|
8
|
+
|
|
9
|
+
parser = OptionParser.new do |opts|
|
|
10
|
+
opts.banner = "Usage: aihub2api COMMAND [options]"
|
|
11
|
+
opts.on("--base-url URL", "OpenAI-compatible base URL") { |value| options[:base_url] = value }
|
|
12
|
+
opts.on("--model MODEL", "Model ID to verify or use") { |value| options[:model] = value }
|
|
13
|
+
opts.on("--prompt TEXT", "Prompt for the chat command") { |value| options[:prompt] = value }
|
|
14
|
+
opts.on("-h", "--help", "Show help") do
|
|
15
|
+
puts opts
|
|
16
|
+
puts "Commands: check, models, chat"
|
|
17
|
+
exit 0
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
parser.parse!(ARGV)
|
|
22
|
+
|
|
23
|
+
begin
|
|
24
|
+
client = AIHub2API::Client.new(base_url: options[:base_url])
|
|
25
|
+
case command
|
|
26
|
+
when "check"
|
|
27
|
+
health = client.health
|
|
28
|
+
puts "health: #{health["status"] || (health["ok"] ? "ok" : "reachable")}"
|
|
29
|
+
if ENV["AIHUB_API_KEY"].to_s.empty?
|
|
30
|
+
puts "models: skipped (set AIHUB_API_KEY to run the authenticated check)"
|
|
31
|
+
else
|
|
32
|
+
ids = client.model_ids
|
|
33
|
+
puts "models: #{ids.length} returned"
|
|
34
|
+
puts "requested_model: #{ids.include?(options[:model]) ? "available" : "not returned"}" if options[:model]
|
|
35
|
+
end
|
|
36
|
+
when "models"
|
|
37
|
+
puts client.model_ids
|
|
38
|
+
when "chat"
|
|
39
|
+
abort "--model is required" unless options[:model]
|
|
40
|
+
abort "--prompt is required" unless options[:prompt]
|
|
41
|
+
result = client.chat_completions(
|
|
42
|
+
model: options[:model],
|
|
43
|
+
messages: [{ role: "user", content: options[:prompt] }],
|
|
44
|
+
)
|
|
45
|
+
puts result.dig("choices", 0, "message", "content") || JSON.pretty_generate(result)
|
|
46
|
+
else
|
|
47
|
+
puts parser
|
|
48
|
+
puts "Commands: check, models, chat"
|
|
49
|
+
exit(command == "help" ? 0 : 1)
|
|
50
|
+
end
|
|
51
|
+
rescue AIHub2API::Error => e
|
|
52
|
+
warn "error: #{e.message}"
|
|
53
|
+
exit 1
|
|
54
|
+
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "net/http"
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module AIHub2API
|
|
6
|
+
class Client
|
|
7
|
+
DEFAULT_BASE_URL = "https://aihub2api.cloud".freeze
|
|
8
|
+
USER_AGENT = "aihub2api-ruby/#{AIHub2API::VERSION}".freeze
|
|
9
|
+
|
|
10
|
+
attr_reader :api_key, :base_url
|
|
11
|
+
|
|
12
|
+
def initialize(api_key: ENV["AIHUB_API_KEY"], base_url: ENV.fetch("AIHUB_BASE_URL", DEFAULT_BASE_URL),
|
|
13
|
+
open_timeout: 10, read_timeout: 60, http_factory: nil)
|
|
14
|
+
@api_key = api_key.to_s.strip
|
|
15
|
+
@base_url = base_url.to_s.sub(%r{/+\z}, "")
|
|
16
|
+
@open_timeout = open_timeout
|
|
17
|
+
@read_timeout = read_timeout
|
|
18
|
+
@http_factory = http_factory
|
|
19
|
+
validate_base_url!
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def health
|
|
23
|
+
request(:get, "/health", authenticated: false)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def models
|
|
27
|
+
request(:get, "/v1/models")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def model_ids
|
|
31
|
+
Array(models["data"]).filter_map { |model| model["id"] if model.is_a?(Hash) }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def chat_completions(model:, messages:, **options)
|
|
35
|
+
payload = options.merge(model: model, messages: messages)
|
|
36
|
+
request(:post, "/v1/chat/completions", body: payload)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def responses(model:, input:, **options)
|
|
40
|
+
payload = options.merge(model: model, input: input)
|
|
41
|
+
request(:post, "/v1/responses", body: payload)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def request(method, path, body: nil, authenticated: true)
|
|
45
|
+
ensure_api_key! if authenticated
|
|
46
|
+
uri = endpoint_uri(path)
|
|
47
|
+
request = build_request(method, uri, body, authenticated)
|
|
48
|
+
response = perform_request(uri, request)
|
|
49
|
+
parsed_body = parse_body(response.body)
|
|
50
|
+
|
|
51
|
+
if response.code.to_i.between?(200, 299)
|
|
52
|
+
parsed_body
|
|
53
|
+
else
|
|
54
|
+
raise APIError.new(
|
|
55
|
+
"AIHub API returned HTTP #{response.code}",
|
|
56
|
+
status: response.code.to_i,
|
|
57
|
+
response_body: parsed_body,
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
rescue Timeout::Error, SocketError, SystemCallError, IOError => e
|
|
61
|
+
raise TransportError, "AIHub API request failed: #{e.class}"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def validate_base_url!
|
|
67
|
+
uri = URI.parse(base_url)
|
|
68
|
+
valid_https = uri.is_a?(URI::HTTPS) && uri.host
|
|
69
|
+
valid_loopback = uri.is_a?(URI::HTTP) && ["127.0.0.1", "localhost", "::1"].include?(uri.host)
|
|
70
|
+
return if valid_https || valid_loopback
|
|
71
|
+
|
|
72
|
+
raise ConfigurationError, "base_url must use HTTPS (plain HTTP is allowed only for loopback tests)"
|
|
73
|
+
rescue URI::InvalidURIError
|
|
74
|
+
raise ConfigurationError, "base_url is not a valid URL"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def ensure_api_key!
|
|
78
|
+
return unless api_key.empty?
|
|
79
|
+
|
|
80
|
+
raise ConfigurationError, "AIHUB_API_KEY is required for authenticated endpoints"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def endpoint_uri(path)
|
|
84
|
+
base = URI.parse(base_url)
|
|
85
|
+
target = base.dup
|
|
86
|
+
suffix = path.to_s.start_with?("/") ? path.to_s : "/#{path}"
|
|
87
|
+
prefix = base.path.to_s.sub(%r{/+\z}, "")
|
|
88
|
+
target.path = "#{prefix}#{suffix}"
|
|
89
|
+
target.query = nil
|
|
90
|
+
target.fragment = nil
|
|
91
|
+
target
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def build_request(method, uri, body, authenticated)
|
|
95
|
+
klass = case method.to_s.downcase
|
|
96
|
+
when "get" then Net::HTTP::Get
|
|
97
|
+
when "post" then Net::HTTP::Post
|
|
98
|
+
else raise ConfigurationError, "unsupported HTTP method: #{method}"
|
|
99
|
+
end
|
|
100
|
+
request = klass.new(uri.request_uri)
|
|
101
|
+
request["Accept"] = "application/json"
|
|
102
|
+
request["User-Agent"] = USER_AGENT
|
|
103
|
+
request["Authorization"] = "Bearer #{api_key}" if authenticated
|
|
104
|
+
if body
|
|
105
|
+
request["Content-Type"] = "application/json"
|
|
106
|
+
request.body = JSON.generate(body)
|
|
107
|
+
end
|
|
108
|
+
request
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def perform_request(uri, request)
|
|
112
|
+
http = @http_factory ? @http_factory.call(uri) : Net::HTTP.new(uri.host, uri.port)
|
|
113
|
+
http.use_ssl = uri.scheme == "https" if http.respond_to?(:use_ssl=)
|
|
114
|
+
http.open_timeout = @open_timeout if http.respond_to?(:open_timeout=)
|
|
115
|
+
http.read_timeout = @read_timeout if http.respond_to?(:read_timeout=)
|
|
116
|
+
http.request(request)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def parse_body(body)
|
|
120
|
+
return {} if body.to_s.empty?
|
|
121
|
+
|
|
122
|
+
JSON.parse(body)
|
|
123
|
+
rescue JSON::ParserError
|
|
124
|
+
{ "raw" => body.to_s }
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module AIHub2API
|
|
2
|
+
class Error < StandardError; end
|
|
3
|
+
|
|
4
|
+
class ConfigurationError < Error; end
|
|
5
|
+
|
|
6
|
+
class APIError < Error
|
|
7
|
+
attr_reader :status, :response_body
|
|
8
|
+
|
|
9
|
+
def initialize(message, status:, response_body: nil)
|
|
10
|
+
super(message)
|
|
11
|
+
@status = status
|
|
12
|
+
@response_body = response_body
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class TransportError < Error; end
|
|
17
|
+
end
|
data/lib/aihub2api.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: aihub2api
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- AIHub
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-08-17 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: |-
|
|
14
|
+
A small Ruby client and command-line preflight checker for AIHub's
|
|
15
|
+
OpenAI-compatible API. It checks health, lists authenticated models,
|
|
16
|
+
and sends Chat Completions or Responses requests without logging API keys.
|
|
17
|
+
email:
|
|
18
|
+
- aihub2api@mail.ljmai.online
|
|
19
|
+
executables:
|
|
20
|
+
- aihub2api
|
|
21
|
+
extensions: []
|
|
22
|
+
extra_rdoc_files: []
|
|
23
|
+
files:
|
|
24
|
+
- LICENSE.txt
|
|
25
|
+
- README.md
|
|
26
|
+
- exe/aihub2api
|
|
27
|
+
- lib/aihub2api.rb
|
|
28
|
+
- lib/aihub2api/client.rb
|
|
29
|
+
- lib/aihub2api/errors.rb
|
|
30
|
+
- lib/aihub2api/version.rb
|
|
31
|
+
homepage: https://aihub2api.cloud/?utm_source=rubygems&utm_medium=package&utm_campaign=westdev
|
|
32
|
+
licenses:
|
|
33
|
+
- MIT
|
|
34
|
+
metadata:
|
|
35
|
+
homepage_uri: https://aihub2api.cloud/?utm_source=rubygems&utm_medium=package&utm_campaign=westdev
|
|
36
|
+
documentation_uri: https://aihub2api.cloud/?utm_source=rubygems&utm_medium=documentation&utm_campaign=westdev
|
|
37
|
+
rubygems_mfa_required: 'false'
|
|
38
|
+
post_install_message:
|
|
39
|
+
rdoc_options: []
|
|
40
|
+
require_paths:
|
|
41
|
+
- lib
|
|
42
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '2.7'
|
|
47
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
48
|
+
requirements:
|
|
49
|
+
- - ">="
|
|
50
|
+
- !ruby/object:Gem::Version
|
|
51
|
+
version: '0'
|
|
52
|
+
requirements: []
|
|
53
|
+
rubygems_version: 3.1.2
|
|
54
|
+
signing_key:
|
|
55
|
+
specification_version: 4
|
|
56
|
+
summary: Dependency-free Ruby client and preflight checker for the AIHub OpenAI-compatible
|
|
57
|
+
API
|
|
58
|
+
test_files: []
|