Last reviewed: October 31, 2026 (Asia/Shanghai).
Direct answer
A small Go integration can call CometAPI with the standard library. Keep one reusable http.Client, build a POST request for the documented /v1/chat/completions path, marshal a narrow request struct, attach a server-side bearer key, and pass the caller’s context.Context through the request. Check the HTTP status and media type before parsing, bound the response body before reading it, and accept only the response shape your application understands.
The current CometAPI chat reference
documents the API host, bearer authentication, model and messages fields, non-streaming chat.completion responses, and finish reasons. Go’s net/http package
documents reusable clients, request contexts, response-body ownership, and timeouts. The encoding/json package
documents marshaling and unmarshaling, while context
documents cancellation and deadlines. Those contracts are enough for a dependency-free adapter.
This example is intentionally text-only and non-streaming. It does not claim that every model supports every optional field, and it does not turn a tool call, refusal, or truncated response into ordinary assistant text.
Who this is for
Use this pattern when a Go service, worker, command-line tool, or test harness needs a transparent HTTP boundary without a provider SDK. It is useful for teams that want request construction, cancellation, response limits, and error categories visible in code review.
The example assumes the model ID and key come from trusted process configuration. Do not accept either from a browser request, put the key in source control, or copy raw prompts and provider bodies into routine logs. The local limits below are application settings; they are not CometAPI account limits or promises about model behavior.
Key takeaways
- Reuse an
http.Client; the Go documentation says clients and transports are safe for concurrent use and should be created once and reused. - Put
context.Contextfirst in the public method and create derived timeouts withdefer cancel()when the caller does not already own the deadline. - Use
http.NewRequestWithContextso the context covers connection setup, request transmission, response headers, and body reads. - Check status and
Content-Typebefore decoding JSON, and return fixed error categories instead of upstream bodies. - Read through
io.LimitReaderwith a one-byte-over-limit sentinel so an oversized response is rejected before parsing. - Validate
object, choice count, assistant role, finish reason, refusal, and non-empty content as one application contract. - Keep streaming, tool calls, retries, and model discovery as separate contracts with their own tests.
For a comparable standard-library implementation in Ruby, see Build a CometAPI Ruby Client with Net::HTTP . For a managed .NET transport, see Connect ASP.NET Core to CometAPI with a Typed HttpClient .
Build the client
Start a small module with an installed Go SDK. The example uses only standard-library APIs:
mkdir cometdemo
cd cometdemo
go mod init example.com/cometdemo
mkdir -p cometclient cmd/smoke
Create cometclient/client.go:
package cometclient
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
"time"
"unicode/utf8"
)
type Client struct {
httpClient *http.Client
endpoint string
apiKey string
model string
maxBytes int64
}
func New(apiKey, model string) (*Client, error) {
apiKey, model = strings.TrimSpace(apiKey), strings.TrimSpace(model)
if apiKey == "" || model == "" || strings.ContainsAny(apiKey, "\r\n") {
return nil, errors.New("missing_cometapi_configuration")
}
return &Client{
httpClient: &http.Client{
Timeout: 30 * time.Second,
CheckRedirect: func(*http.Request, []*http.Request) error {
return http.ErrUseLastResponse
},
},
endpoint: "https://api.cometapi.com/v1/chat/completions",
apiKey: apiKey,
model: model,
maxBytes: 1 << 20,
}, nil
}
type chatRequest struct {
Model string `json:"model"`
Messages []message `json:"messages"`
Stream bool `json:"stream"`
}
type message struct {
Role string `json:"role"`
Content string `json:"content"`
}
type chatResponse struct {
Object string `json:"object"`
Choices []choice `json:"choices"`
}
type choice struct {
FinishReason string `json:"finish_reason"`
Message *replyMessage `json:"message"`
}
type replyMessage struct {
Role string `json:"role"`
Content *string `json:"content"`
Refusal *string `json:"refusal"`
ToolCalls []json.RawMessage `json:"tool_calls"`
}
type HTTPError struct{ Status int }
func (e *HTTPError) Error() string {
return fmt.Sprintf("cometapi_http_%d", e.Status)
}
func (c *Client) Complete(ctx context.Context, prompt string) (string, error) {
if ctx == nil {
return "", errors.New("nil_context")
}
if err := ctx.Err(); err != nil {
return "", err
}
if strings.TrimSpace(prompt) == "" || len(prompt) > 8000 || !utf8.ValidString(prompt) {
return "", errors.New("invalid_prompt")
}
payload, err := json.Marshal(chatRequest{
Model: c.model,
Messages: []message{{Role: "user", Content: prompt}},
Stream: false,
})
if err != nil {
return "", errors.New("request_encode_failed")
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.endpoint, bytes.NewReader(payload))
if err != nil {
return "", errors.New("request_build_failed")
}
req.Header.Set("Authorization", "Bearer "+c.apiKey)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
if ctx.Err() != nil {
return "", ctx.Err()
}
return "", errors.New("transport_failed")
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", &HTTPError{Status: resp.StatusCode}
}
mediaType := strings.ToLower(strings.TrimSpace(strings.SplitN(resp.Header.Get("Content-Type"), ";", 2)[0]))
if mediaType != "application/json" {
return "", errors.New("unexpected_response_media_type")
}
limited := io.LimitReader(resp.Body, c.maxBytes+1)
body, err := io.ReadAll(limited)
if err != nil {
if ctx.Err() != nil {
return "", ctx.Err()
}
return "", errors.New("response_read_failed")
}
if int64(len(body)) > c.maxBytes {
return "", errors.New("response_too_large")
}
var data chatResponse
if err := json.Unmarshal(body, &data); err != nil {
return "", errors.New("response_json_invalid")
}
if data.Object != "chat.completion" || len(data.Choices) != 1 {
return "", errors.New("unexpected_chat_completion_shape")
}
ch := data.Choices[0]
msg := ch.Message
if msg == nil || msg.Role != "assistant" || ch.FinishReason != "stop" ||
msg.Refusal != nil || len(msg.ToolCalls) != 0 || msg.Content == nil ||
strings.TrimSpace(*msg.Content) == "" {
return "", errors.New("unaccepted_chat_completion")
}
return *msg.Content, nil
}
The response structs follow the documented wire shape: refusal, nullable content, and tool_calls belong inside the assistant message. The pointer fields distinguish absent or null values from a returned text value. The application accepts exactly one completed text choice. Supporting several choices, refusals, truncation, or tools requires a result type and product behavior for those outcomes.
http.Client.Do does not treat a non-2xx status as a Go error. This adapter therefore checks for the documented HTTP 200 itself. It also disables redirect following with http.ErrUseLastResponse; a redirect is returned as an HTTP failure and the authenticated POST is not sent to another destination. The client retains Go’s default transport behavior, including proxy environment configuration, so review that configuration in the deployment environment.
Every response body is closed. On the accepted path, the bounded read reaches EOF for an in-limit body, allowing connection reuse. Early status, media-type, and oversize failures close without draining arbitrary data; connection reuse may be lost on those paths. Avoid an unbounded drain merely to retain a connection.
The 8,000-byte prompt ceiling, 30-second HTTP timeout, and 1 MiB response ceiling are illustrative local settings. Prompt bytes are not model tokens. The io reference documents that LimitReader stops after its byte limit
; the extra byte here distinguishes an exactly-at-limit document from an oversized one before JSON parsing.
http.NewRequestWithContext binds the context to the full outgoing request and response lifetime. A canceled caller remains distinguishable through ctx.Err(). Other transport and read failures use fixed messages, keeping raw network diagnostics out of ordinary errors. The client’s timeout remains active during body reading; when a caller has an earlier deadline, that earlier cancellation applies.
json.Unmarshal parses the whole bounded document. Unknown response fields are ignored by default, so adding provider metadata does not break the adapter; required application fields still receive explicit checks. This is a narrow response projection, not strict schema validation, and it does not make generated text safe for HTML rendering or tool execution.
Call it with a request deadline
Save this as cmd/smoke/main.go. Configure COMETAPI_KEY through your local secret mechanism and set COMETAPI_MODEL to an exact, currently supported chat model ID before running it:
package main
import (
"context"
"fmt"
"os"
"time"
"example.com/cometdemo/cometclient"
)
func run() error {
client, err := cometclient.New(
os.Getenv("COMETAPI_KEY"), os.Getenv("COMETAPI_MODEL"),
)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
reply, err := client.Complete(ctx, "Explain idempotency in one sentence.")
if err != nil {
return err
}
fmt.Println(reply)
return nil
}
func main() {
if err := run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
Run go run ./cmd/smoke only when you intend to make a live model request. The shorter 20-second context is a caller budget; the 30-second client timeout remains a defensive ceiling. Go’s context reference says to call the cancel function when the operation completes, releasing associated resources. Keeping the operation in run ensures deferred cleanup executes before main exits.
In a service, construct the client during startup and pass r.Context() from each HTTP handler, deriving a shorter context only when needed. Avoid substituting context.Background() in a handler: doing so disconnects the upstream call from that request’s cancellation. Local cancellation does not establish that remote generation stopped or that no usage was billed.
Contract details to verify
Keep the client method narrow. A caller passes a context and prompt, then receives accepted text or an error. HTTPError carries the status without exposing the upstream body; other fixed messages can be mapped to your application’s error codes. Select the model from current account and model documentation before setting configuration.
| Boundary | Check | Evidence to retain |
|---|---|---|
| Request | One POST targets /v1/chat/completions with model, one user message, stream: false, and bearer auth. | A redacted httptest request assertion. |
| Cancellation | A canceled context stops a delayed handler and no second request starts. | A handler counter and context.Canceled classification. |
| Success | HTTP 200 JSON has object: chat.completion, one assistant choice, finish_reason: stop, non-null content, and no refusal or tool calls. | A fixture with the exact fields. |
| Failure | Non-200, wrong media type, and malformed JSON return fixed errors; refusal, tool call, length finish reason, and empty content share the unaccepted-outcome branch. | One synthetic response per category. |
| Size | A body at the cap is accepted only if valid; a body one byte over the cap is rejected before decode. | Boundary-size fixtures. |
| Secrets | Logs contain a request ID and category, never the key, prompt, raw body, or authorization header. | Redaction review or log assertion. |
Use a custom RoundTripper in a test file with package cometclient. That test package can replace client.httpClient.Transport with a fake before starting calls, capture the fixed outbound URL, and return synthetic responses without a live key. If you use a loopback httptest server instead, set the private endpoint to its URL inside that test package. Keep those overrides out of production configuration and never mutate the client or transport while requests are running.
After adding the fixtures, run go test ./... and go vet ./..., then make one authorized staging smoke test with a harmless prompt. Save only the status class, endpoint family, model ID, response-shape verdict, and redaction result. Synthetic tests establish local request and parser behavior; account access, model availability, and the live network path need the staging request.
Failure modes
The adapter is constructed for every call. Construct it during service setup and reuse it across goroutines. This sample uses the shared default transport; replacing that with a newly created transport per call would also lose the intended pool reuse.
The context is stored in Client. Go’s context documentation says to pass contexts explicitly, usually as the first parameter. Keep request cancellation per call; do not retain a request context in a long-lived client.
The response body is decoded without a cap. json.Decoder or io.ReadAll alone does not impose an application size limit. Read through io.LimitReader and reject the sentinel byte before parsing.
A 401 or 429 body is returned to the user. Inspect HTTPError.Status with errors.As in the application layer and map it to a stable response. The adapter discards the upstream body. Keep the client object, headers, prompts, and raw provider content out of automatic object dumps and routine logs.
A stream is passed to the non-streaming parser. The CometAPI reference says streaming returns server-sent-event chunks and ends with [DONE]. A final usage chunk is included when stream_options.include_usage is enabled. Add a separate SSE reader with its own deadline and size limits.
Tool calls are shown as assistant text. The documented finish reason is tool_calls, and message content may be null. Return a typed tool request and require application authorization before executing anything.
A timeout is retried automatically. A timeout can leave the remote outcome unknown. Add retries only outside this client, with an attempt limit, total deadline, and replay policy appropriate to the operation.
Verification before rollout
Run a sanitized matrix before using the adapter in a user-facing route:
| Case | Expected result |
|---|---|
| Valid short prompt | One POST and an accepted text result. |
| Blank prompt | Local rejection and zero outbound calls. |
| Canceled context | Transport stops and the caller receives cancellation. |
| 200 HTML response | Media-type error; no JSON parse attempt. |
| 429 JSON response | Fixed HTTP category; upstream body is not logged. |
| Malformed JSON | Parse category; no partial text returned. |
Refusal or content_filter | Unaccepted outcome requiring a product decision. |
| Oversized body | Size category before unbounded buffering. |
Record the Go version, dependency-free source revision, fixture outcome, endpoint family, and model identifier. Keep the key in the environment or secret manager. If the live response differs from the current CometAPI reference, stop and update the adapter contract before changing the parser.
The preparation environment did not have a working Go toolchain, so these examples were reviewed against the live references but were not compiled or executed here. The matrix above states acceptance criteria, not observed test results. No authenticated CometAPI request or deployment was performed.
Sources checked
These five primary public references were fetched on October 30, 2026 UTC and support the current evergreen integration task:
- CometAPI: Create a chat completion
— documents the API host,
/v1/chat/completions, bearer authentication, request fields, streaming shape, finish reasons, and response fields. - Go
net/httppackage — documents reusable clients, request contexts, redirect and timeout behavior, and the requirement to close response bodies. - Go
encoding/jsonpackage — documentsMarshal,Unmarshal, struct-field matching, and syntax errors. - Go
contextpackage — documents request-scoped cancellation, deadlines, explicit context parameters, and prompt cancellation cleanup. - Go
iopackage — documentsLimitReader,ReadAll, EOF behavior, and bounded stream reads.
These sources support the wire contract and standard-library mechanics. The one-megabyte body cap, 30-second default timeout, accepted finish reason, retry policy, and logging categories are application recommendations; tune and test them for the service rather than treating them as provider defaults.
FAQ
Does this require a Go SDK?
No. The example uses net/http, encoding/json, context, and io. An SDK can be appropriate when it matches the endpoint and response modes you need, but the same status, cancellation, size, and response checks still belong at your application boundary.
Should the browser choose the model or API key?
No. Keep the key server-side and choose the model from trusted configuration after checking the current model catalog. A caller-controlled model can change cost, capability, and data-handling assumptions without review.
Can this client parse streaming responses?
No. Streaming uses server-sent events and chunk objects, not one JSON completion. Add a separate method that reads events incrementally, handles [DONE] and the usage chunk when requested, and enforces a total deadline and byte budget.
Why reject finish_reason: length?
This example promises complete assistant text. The CometAPI reference defines length as reaching a token limit. A product that can display partial text should return a typed result with that state instead of silently labeling it complete.
Is a passing httptest proof of production readiness?
No. It proves your request construction and response classification. A separate staging request checks credentials, account permissions, model access, and the live network path. It does not establish uptime or billing behavior.
Reader next step
Create a small Go module, run the synthetic matrix, and inspect the captured request for method, path, headers, model, and stream: false. Then make one authorized staging request with a harmless prompt and record only the sanitized evidence fields. If the result is not the documented response family, pause the rollout and re-check the live chat page and model catalog.
When the local boundary is ready and you need account access, Start with CometAPI . Keep the endpoint, model ID, response contract, and source access date beside the code so the next maintainer can tell which assumptions need re-verification.