Last reviewed: October 30, 2026 (Asia/Shanghai).
Direct answer
A small Ruby client can call CometAPI without an SDK: create a POST request for the CometAPI chat-completions endpoint
, send a JSON body with a verified model ID and messages, add the bearer token from process configuration, and parse the response only after checking its HTTP status and media type. Keep the transport code separate from application code so timeouts, malformed JSON, provider errors, and an unexpected success shape have explicit outcomes.
The current CometAPI chat reference documents the OpenAI-compatible endpoint, the model and messages request fields, bearer authentication, and the chat completion response shape. Ruby’s standard Net::HTTP documentation
covers HTTPS sessions, request objects, headers, bodies, and response handling. Ruby’s JSON documentation
covers generating a JSON string and parsing a response. Those three contracts are enough for a dependency-free client.
This example deliberately keeps the public method narrow:
reply = CometClient.new(
ENV.fetch("COMETAPI_KEY"),
model: ENV.fetch("COMETAPI_MODEL")
).complete("Explain idempotency in one sentence.")
puts reply
The model remains configuration, rather than a value supplied by an end user. Check the current model catalog before deploying a chosen ID. Do not put the key in source control, command arguments, fixtures, or exception messages.
Who this is for
This tutorial is for Ruby developers who need a small server-side integration, a reproducible smoke test, or a reference implementation that makes the HTTP boundary visible. It uses only Ruby’s standard library, so it fits a worker, a Rails service object, a Sinatra application, or a command-line utility.
It is not a promise that every CometAPI model accepts every optional parameter. The client sends the minimal documented chat shape. Add options only after checking the current chat page and the selected model’s capabilities. If the application needs streaming, structured output, image input, or tools, treat each as a separate contract and test it separately.
Key takeaways
- Build the request from trusted configuration and a validated string, rather than forwarding a caller’s hash.
- Use
URI,Net::HTTP::Post, andJSON.generateso URL, headers, and encoding are visible in code review. - Set both an open timeout and a read timeout; make them configuration with bounded defaults.
- Check
Content-Type, HTTP status, top-levelobject,choices,finish_reason, and message content before returning text. - Map transport, HTTP, JSON, and response-contract failures to stable application errors.
- Test the client with a fake transport so no key or live request is needed for unit coverage.
Build the client
Create comet_client.rb:
# frozen_string_literal: true
require "json"
require "net/http"
require "uri"
class CometClient
class Error < StandardError; end
class ConfigurationError < Error; end
class TransportError < Error; end
class HttpError < Error
attr_reader :status
def initialize(status)
@status = status
super("cometapi_http_#{status}")
end
end
class ResponseError < Error; end
API_URI = URI::HTTPS.build(host: "api.cometapi.com", path: "/v1/chat/completions")
MAX_RESPONSE_BYTES = 1_048_576
def initialize(auth_value, model:, http_factory: Net::HTTP,
open_timeout: 5, read_timeout: 30)
@auth_value = auth_value.to_s
@model = model.to_s
@http_factory = http_factory
@open_timeout = Integer(open_timeout)
@read_timeout = Integer(read_timeout)
if @auth_value.strip.empty? || @model.strip.empty?
raise ConfigurationError, "missing_cometapi_configuration"
end
if @open_timeout < 1 || @read_timeout < 1
raise ConfigurationError, "invalid_cometapi_timeout"
end
rescue ArgumentError, TypeError
raise ConfigurationError, "invalid_cometapi_timeout"
end
def complete(prompt)
text = prompt.to_s
raise ArgumentError, "prompt_must_be_a_nonempty_string" if text.strip.empty?
request = Net::HTTP::Post.new(API_URI)
request.add_field("Authorization", "Bearer #{@auth_value}")
request["Content-Type"] = "application/json"
request["Accept"] = "application/json"
request.body = JSON.generate(
model: @model,
messages: [{ role: "user", content: text }]
)
response = perform(request)
parse_reply(response)
rescue JSON::GeneratorError, EncodingError
raise ResponseError, "request_json_encoding_failed"
rescue Net::OpenTimeout, Net::ReadTimeout, Timeout::Error,
SocketError, EOFError, IOError => error
raise TransportError, "cometapi_transport_failed:#{error.class}"
end
private
def perform(request)
http = @http_factory.new(API_URI.host, API_URI.port)
http.use_ssl = (API_URI.scheme == "https")
http.open_timeout = @open_timeout
http.read_timeout = @read_timeout
response = http.request(request)
status = response.code.to_i
raise HttpError.new(status) unless status.between?(200, 299)
media_type = response["content-type"].to_s.split(";", 2).first.to_s.downcase
raise ResponseError, "unexpected_response_media_type" unless media_type == "application/json"
body = response.body.to_s
raise ResponseError, "response_too_large" if body.bytesize > MAX_RESPONSE_BYTES
body
end
def parse_reply(body)
data = JSON.parse(body)
unless data["object"] == "chat.completion" &&
data["choices"].is_a?(Array) &&
!data["choices"].empty?
raise ResponseError, "unexpected_chat_completion_shape"
end
choice = data["choices"].first
unless choice["finish_reason"] == "stop" &&
choice["message"].is_a?(Hash) &&
choice["message"]["refusal"].nil? &&
choice["message"]["content"].is_a?(String)
raise ResponseError, "unaccepted_chat_completion"
end
content = choice["message"]["content"]
raise ResponseError, "empty_chat_completion" if content.strip.empty?
content
rescue JSON::ParserError
raise ResponseError, "response_json_invalid"
end
end
Net::HTTP::Post.new(API_URI) gives the request its method and target. The Authorization header carries the runtime key, while Content-Type describes the JSON body and Accept states what the client expects back. JSON.generate handles quoting and escaping; string concatenation would make prompt content unsafe and fragile.
perform creates a new HTTPS session for each call. open_timeout bounds connection setup and read_timeout bounds waiting for response data. These are local application limits, not CometAPI account limits. Tune them against the user-facing deadline and upstream behavior, and keep retries outside this class until the application defines which failures are safe to repeat.
The status check happens before parsing. A provider error body is not treated as a successful completion just because it happens to contain JSON. The media-type check then prevents an HTML gateway page or an unexpected representation from entering the JSON parser. The one-megabyte ceiling is a defensive local limit; choose a value appropriate for the route and test it at the boundary.
The parser accepts one ordinary, completed text choice. The chat reference shows that streamed responses are chunks, and tool calls use a different finish reason and message shape. A client that needs those modes should return a typed result or add a separate method. It should not silently turn a tool call or refusal into ordinary text.
The exception messages are fixed categories. The transport wrapper preserves only the Ruby exception class, not the URL, key, prompt, or raw response. Log a request ID and category in the application layer if operators need correlation. Avoid logging request.body, response.body, or the authorization header.
Add a small application wrapper
Keep configuration and user-facing error mapping outside the transport class:
# app_example.rb
require_relative "comet_client"
client = CometClient.new(
ENV.fetch("COMETAPI_KEY"),
model: ENV.fetch("COMETAPI_MODEL"),
open_timeout: Integer(ENV.fetch("COMETAPI_OPEN_TIMEOUT", "5")),
read_timeout: Integer(ENV.fetch("COMETAPI_READ_TIMEOUT", "30"))
)
begin
puts client.complete("Explain idempotency in one sentence.")
rescue KeyError, ArgumentError, CometClient::ConfigurationError
warn "configuration_error"
exit 2
rescue CometClient::TransportError
warn "upstream_unreachable"
exit 3
rescue CometClient::HttpError => error
warn "upstream_http_#{error.status}"
exit 4
rescue CometClient::ResponseError
warn "upstream_response_invalid"
exit 5
end
A web endpoint should validate its incoming content before invoking complete, enforce its own request and response limits, and avoid returning provider diagnostics to the browser. The nearby Fastify input-validation guide
shows the same boundary in another runtime: a narrow public contract, server-owned model selection, and a zero-dispatch assertion for invalid input.
Test the boundary without a live key
Inject a fake Net::HTTP factory. The fake can capture the request and return a response object with the methods the client reads:
# test_comet_client.rb
require "json"
require "minitest/autorun"
require_relative "comet_client"
FakeResponse = Struct.new(:code, :headers, :body) do
def [](name)
headers[name.downcase] || headers[name]
end
end
class FakeHttp
attr_reader :request
def initialize(_host, _port, response)
@response = response
end
def use_ssl=(_value); end
def open_timeout=(_value); end
def read_timeout=(_value); end
def request(request)
@request = request
@response
end
end
class FakeFactory
attr_reader :instance
def initialize(response)
@response = response
end
def new(host, port)
@instance = FakeHttp.new(host, port, @response)
end
end
class CometClientTest < Minitest::Test
def json_response(payload, content_type: "application/json", code: "200")
FakeResponse.new(code, { "content-type" => content_type }, JSON.generate(payload))
end
def client_for(response)
CometClient.new(
"[REDACTED]",
model: "test-model",
http_factory: FakeFactory.new(response)
)
end
def test_builds_authenticated_json_request
response = json_response(
"object" => "chat.completion",
"choices" => [{
"finish_reason" => "stop",
"message" => { "refusal" => nil, "content" => "A repeat has the same effect as one execution." }
}]
)
factory = FakeFactory.new(response)
client = CometClient.new("[REDACTED]", model: "test-model", http_factory: factory)
assert_equal "A repeat has the same effect as one execution.", client.complete("Explain idempotency.")
request = factory.instance.request
assert_equal "POST", request.method
assert request["Authorization"].start_with?("Bearer ")
assert_equal "application/json", request["Content-Type"]
payload = JSON.parse(request.body)
assert_equal "test-model", payload["model"]
assert_equal [{ "role" => "user", "content" => "Explain idempotency." }], payload["messages"]
end
def test_rejects_blank_prompt_before_transport
client = client_for(json_response("object" => "chat.completion", "choices" => []))
assert_raises(ArgumentError) { client.complete(" ") }
end
def test_classifies_http_media_and_json_failures
assert_raises(CometClient::HttpError) do
client_for(FakeResponse.new("429", { "content-type" => "application/json" }, "{}" )).complete("Hi")
end
assert_raises(CometClient::ResponseError) do
client_for(FakeResponse.new("200", { "content-type" => "text/html" }, "<h1>bad</h1>")).complete("Hi")
end
assert_raises(CometClient::ResponseError) do
client_for(FakeResponse.new("200", { "content-type" => "application/json" }, "not-json")).complete("Hi")
end
end
end
The first test is more valuable than checking only the returned sentence. It proves that the code used POST, the expected endpoint family, a bearer header, a JSON body, the configured model, and a user message. The failure test proves that HTTP status, media type, and JSON syntax are classified before application code sees a reply. Keep the fake response deterministic and do not place a real key or copied production text in it.
Also add fixtures for an empty choices array, a refusal, a tool-call finish reason, an over-limit body, and a timeout exception. These tests prove classification and secrecy; they do not prove model quality, account access, billing, or production latency.
Contract details to verify
| Boundary | Local contract | Evidence to keep |
|---|---|---|
| Endpoint | POST to the documented chat-completions endpoint | The current CometAPI chat reference URL and access date. |
| Authentication | Authorization: Bearer <runtime key> | A request assertion that checks the header without logging its value. |
| Request body | JSON with configured model and messages | A parsed fake request body and a model-catalog check. |
| Success shape | chat.completion, nonempty choices, completed text message | A fixture that checks object, finish reason, refusal, and content. |
| Failure shape | Non-2xx, non-JSON, invalid JSON, or oversized body is an error | One fixture per category and a stable application error. |
| Time limits | Open and read timeouts are bounded local settings | Configuration review and timeout exception tests. |
The CometAPI model catalog
documents a public GET /api/models catalog with IDs, providers, capabilities, endpoint hints, and optional context metadata. Use that page or the catalog itself to verify a model before putting its ID in COMETAPI_MODEL. Catalog metadata can change, so do not turn a one-time lookup into a permanent promise about availability.
Failure modes
The request receives HTML and JSON parsing fails. Check the status and media type first. A reverse proxy or gateway can return an HTML error page even when the request path is wrong.
A 429 is retried forever. This class does not retry. Decide in the caller whether a retry is safe, set a maximum attempt count and total deadline, and preserve an idempotency strategy if the operation has side effects outside the model call.
A streaming response is treated as one JSON completion. Set stream only in a method designed to parse server-sent events, or keep it false for this client. Do not reuse parse_reply for chunks.
A tool call is displayed as text. The documented tool-call path has a different finish reason and message fields. Return a typed tool result and require application authorization before executing anything.
A prompt or key appears in logs. Inspect exception construction and request logging. Keep fixed categories in errors and redact headers, bodies, and user content at the logger boundary.
A model ID works in one environment but not another. Compare the current catalog, account permissions, and endpoint family. Record the model ID and catalog access date in a local test note, rather than silently switching models.
Sources checked
These official primary references were fetched on October 29, 2026 UTC. They are stable technical references for an evergreen integration task; the article’s currentness comes from re-fetching the live pages and checking the CometAPI chat page’s current modification metadata.
- CometAPI: Create a chat completion
— documents
POST /v1/chat/completions, the documented CometAPI API base URL, bearer authentication, model and message fields, streaming, structured output, tool-call finish reasons, and response examples. - CometAPI: List available models — documents the public model catalog, model IDs, provider and capability metadata, endpoint hints, and optional context/completion metadata.
- Ruby 3.3 Net::HTTP — documents HTTPS sessions, request classes, headers, bodies, timeouts, and response handling in the standard library.
- Ruby 3.3 JSON — documents JSON generation and parsing, including parser errors and options.
Reader next step
Run the fake-transport tests first, then set COMETAPI_KEY and COMETAPI_MODEL in a local secret mechanism and make one small request from a non-production environment. Save only the HTTP status class, endpoint family, model identifier, response-shape verdict, and a redaction check. If the live result differs from the documented shape, stop and inspect the current CometAPI page and catalog before changing the parser.
For a JavaScript comparison, read Cap CometAPI Response Bytes Before JSON Parsing in Node.js . It demonstrates the same ordering principle: bound the response before parsing and keep provider failures out of application output.
FAQ
Do I need a Ruby HTTP gem?
No. This example uses Net::HTTP and JSON from Ruby’s standard library. A gem can still be appropriate if your application already standardizes on one HTTP stack, but keep the same explicit headers, limits, timeouts, and response checks.
Should I accept a model from the browser?
No. Select the model in trusted server configuration after checking the current catalog. Allowing callers to select arbitrary models can change capability, cost, latency, or data-handling assumptions without a review.
Can I return the raw CometAPI error body?
Avoid returning it to end users or placing it in ordinary logs. Preserve a stable category and a correlation ID; store a redacted diagnostic only in a controlled operator channel if your retention policy allows it.
How should I add streaming?
Create a separate method that requests and parses server-sent events incrementally, enforces a byte and time budget, and handles the final usage chunk. Keep the non-streaming parser here for ordinary JSON responses.
Is a successful fake test proof that the integration works?
No. It proves request construction and response classification. A separate, authorized smoke test verifies credentials, model access, current provider behavior, and the live network path. Record that evidence without treating one response as proof of uptime or billing behavior.
What if the response contains a refusal?
Treat it as an application outcome, not ordinary assistant text. The example raises ResponseError; a product may instead return a typed refusal result after deciding how it should be shown and logged.