Hosted API

Hosted API

The hosted service runs the statistics engine of cci-sdk 0.1.1, unmodified, behind an authenticated HTTP API. Around it, it adds what a shared service needs: your own provider credentials stored as encrypted connections, a calibration lifecycle with background jobs, monitors and audits, idempotent retries, rate limits, and a trace of every request. This page is the operational contract; request and response shapes are on API reference.

Base URL and authentication

https://cci.gitdate.ink/api/v1

This is the SDK’s default base_url. Every route except GET /v1/healthz and GET /v1/readyz needs an API key:

Authorization: Bearer cci_live_...
export CLI_API_KEY=cci_live_...
export CLI_BASE_URL=https://cci.gitdate.ink/api/v1     # optional: this is the default
from cli_sdk import CLIClient
 
client = CLIClient()       # reads CLI_API_KEY and CLI_BASE_URL

For a self-hosted deployment, point CLI_BASE_URL at it. The SDK accepts a missing key only when the URL’s host is localhost, 127.0.0.1, 0.0.0.0 or ::1.

401 codeMeaning
missing_api_keyNo Authorization: Bearer header
invalid_api_keyThe key is unknown
revoked_api_keyThe key was revoked in the dashboard; create a new one
key_owner_removedThe person who created the key is no longer a member of its organization

API keys and scopes

Create keys under Dashboard > API keys. The full key is shown once; the service stores only a SHA-256 hash and a short display prefix. A key acts in the organization it was created in, and carries one or more scopes:

ScopeGrantsRoutes
evaluateevaluate, profiles:read, backends:readPOST /evaluate, GET /jobs, GET /jobs/{id}, every GET on profiles, monitors, alerts, audits and label jobs, GET /backends
profiles:writeeverything evaluate grants, plus profiles:writeCreating, changing and deleting profiles; examples, outcomes, calibrate, audit, label-with-judge, monitors; POST /jobs/{id}/cancel
telemetry:readtelemetry:read, profiles:read, backends:readThe events read API: GET /requests, GET /requests/{id}, GET /usage, GET /events, GET /calibration-profiles/{name}/events, plus profile reads
events:writeevents:writePOST /events (guard events) and POST /traces (OTLP ingest)
  • New keys default to evaluate profiles:write. Keys created before scopes existed behave as evaluate profiles:write.
  • A key never has more than its creator: its effective scopes are its stored scopes intersected with what the creator’s current role allows. A viewer can only create telemetry:read keys. Role changes apply on the key’s next request.
  • A key whose creator leaves the organization stops working (401 key_owner_removed).
  • A missing scope is 403 insufficient_scope, with WWW-Authenticate: Bearer error="insufficient_scope", scope="..." naming the scope to add.

Use separate keys per service, so a leaked or revoked key affects one workload, and so Usage shows cost per service.

Bring your own provider key

The hosted service holds no shared provider credential. Every model call uses a provider connection that your organization stored: an encrypted OpenAI, Anthropic, Gemini, Bedrock or other key, or the URL of your own vLLM or SGLang server. Without one, evaluate returns 422 connection_required with field: "backend.connection".

A backend uses the organization’s default connection for its provider, or the one it names:

from cli_sdk import OpenAIBackend
 
OpenAIBackend(model="gpt-4.1-2025-04-14", connection="openai-prod")
# equivalent dict: {"provider": "openai", "model": "gpt-4.1-2025-04-14", "connection": "openai-prod"}

With a CustomBackend the SDK computes evidence in your process and the server never calls a model, so no connection is needed.

Providers and access levels

providerConnectionLevel with access_hint="auto"Highest level
openaiopenaiL1 on gpt-4.1 and on GPT-5.x and GPT-6 Sol and Luna at reasoning_effort="none" (or unset); L0 otherwiseL1
azure-openaiazure-openaiAs OpenAI, read from model (the model the deployment serves)L1
anthropicanthropicL0L0
geminigemini (API key, or Vertex mode)L0L0
vertexgemini in Vertex modeL0L0
bedrockbedrockL0 (Converse API)L0
bedrock with custom_model_import=TruebedrockL1L2 with access_hint="prompt-scoring"
openrouteropenrouterL1 for an OpenAI model that reaches L1 on its own (for example openai/gpt-4.1) on a pinned openai, together or fireworks upstream; L0 otherwiseL1
vllmvllmL1L3 (L2 prompt-scoring, L3 exact)
sglangsglangL1L4 (hidden-state)
openai-compatibleopenai-compatibleL0, or L1 with "use_logprobs": trueL1
mistral, groq, xaione per presetL0L0
together, fireworks, deepseek, cerebrasone per presetL1L1
client_evidencenoneAs your CustomBackend declares

The levels above auto come from the connection’s capability probe: the service records what a self-hosted server or an imported Bedrock model supports when you run Test on the connection. Details and examples per provider are on Backends and Provider connections.

  • A Set with more than 20 options cannot be scored from first-token log-probabilities, so it runs at L0 on providers whose auto level is L1.
  • An access_hint (or a Set’s backend_access_hint) pins a level: sampling (L0), logprobs (L1), prompt-scoring (L2), exact (L3), hidden-state (L4). A level the backend cannot reach fails with 502 access_level_unavailable before any model call is made. hidden-state serves Set, Belief, Gate and Interval queries.
  • At L0 each score is built from sample_count samples: 8 by default, at most 32, set per backend (AnthropicBackend(sample_count=...), or "sample_count" in a backend dict). The sample count is part of the profile’s fingerprint.

Request ids and correlation

Every response carries x-request-id: req_<16 hex digits>, and every JSON body includes the same value as "request_id". The SDK exposes it as result.request_id. Log it next to any decision you act on: it opens the request’s trace in the dashboard and in GET /v1/requests/{id}.

To tie a request to your own logs you can send:

  • x-request-id in the same req_<16 hex> format. It is adopted unless the service has seen it in the last day, in which case a new id is minted. Do not put it in default_headers, where every request would share it.
  • A W3C traceparent header. Its trace id becomes the request’s trace id.
  • The X-CCI-* correlation headers (session, thread, run name, environment, agent, tool call, tags, metadata), listed on Observability.

Invalid correlation headers never fail a request: they are dropped, and the request’s trace records a warning naming the header (an evaluate response also lists it in warnings).

Idempotency

Every POST, PUT, PATCH and DELETE may carry an Idempotency-Key header (1 to 255 printable ASCII characters). The SDK sends a fresh UUID for each logical POST and reuses it across that call’s retries, so a retried write is applied once.

SituationResponse
The key was used before for the same request, within 24 hoursThe original status and body, replayed, with Idempotent-Replayed: true
The original request with this key is still running409 idempotency_in_flight with Retry-After: retry shortly
The key was used for a different route or body422 idempotency_key_reused: use a new key for a different call
The key is not 1 to 255 printable ASCII characters422 invalid_idempotency_key

Keys are scoped to your organization and the API key that sent them. Responses with status 2xx, 400, 404, 413, 415, 422 and 424 are stored for replay. Responses a retry could change (401, 402, 403, 408, 409, 429, 5xx, 529) are not stored, so a retry runs again.

Rate limits and concurrency

LimitValueWhen exceeded
Request rate per API keyToken bucket: bursts of 60, refilled at 10 per second429 rate_limited
Concurrent writes (POST, PUT, PATCH, DELETE) per organization16429 concurrency_limited
Concurrent writes per API key8429 concurrency_limited
Requests in flight across the servicebounded529 service_overloaded

Every 429 and 529 carries Retry-After in seconds. Rate-limited routes also return RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset. Limits your provider enforces on your own key come back as 429 provider_rate_limited (with the provider’s Retry-After), 529 provider_overloaded, or 402 provider_quota_exhausted.

The SDK retries 429, 502 and 529 with full-jitter exponential backoff, honoring a numeric Retry-After of up to 60 seconds. It does not retry 402: an exhausted quota needs attention on the provider account. See Errors and retries.

Timeouts and background jobs

TimeoutValueResult
The hosted proxy waiting for a response300 seconds504 gateway_timeout
One provider call (connect, read)5 s, 60 s502 provider_timeout (retried by the SDK)
The SDK’s HTTP timeoutCLIClient(timeout=60.0) by defaultAPIConnectionError after retries

Raise the SDK’s timeout for synchronous audits and first calibrations, for example CLIClient(timeout=300). A 504 is not retried by the SDK; it raises CLIError("HTTP 504: ...").

Work that would take long runs as a persisted background job:

  • Calibration. When the first evaluate against a profile needs more than 200 estimated model calls to score its examples, calibration runs as a job. Until it finishes, evaluate answers with 424 and heuristic-labelled answers, plus a warning naming the job (the SDK returns them; with strict_guarantees=True it raises). Smaller calibrations run inline, within the request.
  • Appended examples. Examples added to a calibrated profile are scored inline when cheap, otherwise by a job; the profile card then shows pending_n and job.
  • Explicit work. POST .../calibrate, an audit sent with "async": true, and a large label-with-judge pool return 202 Accepted with the job.

Poll a job until it leaves queued and running:

import os
import time
 
import httpx
 
API = os.environ.get("CLI_BASE_URL", "https://cci.gitdate.ink/api/v1").rstrip("/")
HEADERS = {"Authorization": f"Bearer {os.environ['CLI_API_KEY']}"}
 
 
def wait_for_job(job_id: str, every: float = 5.0) -> dict:
    while True:
        job = httpx.get(f"{API}/jobs/{job_id}", headers=HEADERS, timeout=30).json()
        if job["status"] not in ("queued", "running"):
            return job        # "succeeded", "failed" or "cancelled"
        time.sleep(every)

A job has id, kind, status, profile, progress ({done, total}), estimated_calls, actual_calls, result, error, created_at, started_at, finished_at and request_id (the request that queued it). GET /v1/jobs?profile=&status=&limit= lists jobs, and POST /v1/jobs/{id}/cancel cancels a queued or running one. Jobs survive a restart of the service: running jobs go back to the queue.

Limits

LimitValueOver the limit
Request body5 MB (20 MB for POST /v1/traces)413 payload_too_large
Queries per evaluate16422, field queries
Options per Set256422
Stages per cascade6422
Examples per add_examples call10,000422 payload_too_large, field examples
Examples per profile100,000422 profile_full
Profiles per organization1,000422 profile_limit
Monitors per profile20422 monitor_limit
Outcomes per call1,000422, field outcomes
Examples per audit2,000422, field examples
Synchronous auditabout 1,500 estimated model calls422 audit_too_large: send "async": true
Unlabelled pool for label-with-judge20,000422
Inline calibration200 estimated model callsa background job
Samples per score at L08 by default, at most 32422
Guard events per batch500422
Idempotent replay window24 hoursthe key can be reused
Profile versions kept10older versions are pruned

Status codes

StatusMeaningSDK
200OK
201Created: a new profile (with Location) or monitor
202Accepted: a job was queued, guard events were accepted, or a label-with-judge result
401Missing, invalid or revoked API keyAuthenticationError
402provider_quota_exhausted on your provider accountCLIError
403insufficient_scope (API key) or forbidden_role (dashboard role)CLIError
404Unknown profile, monitor, job, request or routeCLIError
405Method not allowed on this routeCLIError
409profile_exists, idempotency_in_flight, profile_busyCLIError
413Body too largeCLIError
415Unsupported content type (OTLP ingest)CLIError
422Validation failed; field names the offending fieldValidationError (.field)
424evaluate: some answers are heuristic (the body holds every answer); other routes: the profile is not calibrated yetHeuristic answers, or InsufficientCalibrationError
429Rate or concurrency limit, here or at your providerRateLimitError after retries
500Internal errorCLIError
502The model backend failed or timed out, or the access level is unavailableBackendError after retries
503The service is not ready (for example its account store is unavailable)CLIError
504The proxy timed outCLIError
529The service or your provider is overloadedBackendError after retries

Every error body is {"message", "code", "field"?, "details"?, "request_id"}. The full code catalog is on Errors and retries.

GET /v1/healthz answers {"status": "ok"} while the process is up. GET /v1/readyz also checks the stores and the telemetry writer and answers 503 with {"status": "not_ready", "checks": {...}} when one is unavailable. Neither needs a key.