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.Context first in the public method and create derived timeouts with defer cancel() when the caller does not already own the deadline.
  • Use http.NewRequestWithContext so the context covers connection setup, request transmission, response headers, and body reads.
  • Check status and Content-Type before decoding JSON, and return fixed error categories instead of upstream bodies.
  • Read through io.LimitReader with 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.

BoundaryCheckEvidence to retain
RequestOne POST targets /v1/chat/completions with model, one user message, stream: false, and bearer auth.A redacted httptest request assertion.
CancellationA canceled context stops a delayed handler and no second request starts.A handler counter and context.Canceled classification.
SuccessHTTP 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.
FailureNon-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.
SizeA body at the cap is accepted only if valid; a body one byte over the cap is rejected before decode.Boundary-size fixtures.
SecretsLogs 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:

CaseExpected result
Valid short promptOne POST and an accepted text result.
Blank promptLocal rejection and zero outbound calls.
Canceled contextTransport stops and the caller receives cancellation.
200 HTML responseMedia-type error; no JSON parse attempt.
429 JSON responseFixed HTTP category; upstream body is not logged.
Malformed JSONParse category; no partial text returned.
Refusal or content_filterUnaccepted outcome requiring a product decision.
Oversized bodySize 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/http package — documents reusable clients, request contexts, redirect and timeout behavior, and the requirement to close response bodies.
  • Go encoding/json package — documents Marshal, Unmarshal, struct-field matching, and syntax errors.
  • Go context package — documents request-scoped cancellation, deadlines, explicit context parameters, and prompt cancellation cleanup.
  • Go io package — documents LimitReader, 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.