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 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
@@ -0,0 +1,3 @@
1
+ module AIHub2API
2
+ VERSION = "0.1.0"
3
+ end
data/lib/aihub2api.rb ADDED
@@ -0,0 +1,6 @@
1
+ require_relative "aihub2api/version"
2
+ require_relative "aihub2api/errors"
3
+ require_relative "aihub2api/client"
4
+
5
+ module AIHub2API
6
+ end
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: []