Descles documentation

BYOK model gateway + control plane

Descles sits between your agent and its model provider. You keep your provider endpoint and key (or store the key with us); your traffic gets authenticated, attributed to a person, recorded, budgeted, and gated by policy — without changing your client code.

Overview

You getA stable gateway URL per provider, a control plane that attributes every call to the agent that made it, and policy that can deny or pause dangerous tool calls.
You keepYour provider account and API key (two modes, see Provider keys).
You changeOnly the base_url your client points at.
No SDK, no sidecar, no code changes. Any OpenAI-compatible or Anthropic client works.

Quickstart

1 · Claim the workspace and agent key

Create a Clerk account, then submit the test launch form. You receive a data key bound to your primary agent (1,000 free middleware requests, 30 days). Open the console with the same Clerk session; the key is shown there for the rest of the current browser tab.

2 · Choose how provider keys flow

Hosted BYOK: save a provider endpoint and key once under Console → Providers. Point the client at https://<provider>.gw.descles.com/v1 and authenticate only with the Descles agent key. This is the recommended team setup.

export DESCLES_BASE_URL=https://deepseek.gw.descles.com/v1
export DESCLES_API_KEY=<your-descles-key>

curl "$DESCLES_BASE_URL/chat/completions" \
  -H "Authorization: Bearer $DESCLES_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"deepseek-chat","messages":[{"role":"user","content":"Hello"}]}'

Per-request BYOK: send the provider endpoint and key on each request. Descles forwards the credential for that call without saving it:

curl "https://api.gw.descles.com/v1/chat/completions" \
  -H "Authorization: Bearer <your-descles-key>" \
  -H "X-Descles-Provider-Base-URL: https://api.deepseek.com/v1" \
  -H "X-Descles-Provider-Key: <your-provider-key>" \
  -H "Content-Type: application/json" \
  -d '{"model":"deepseek-chat","messages":[{"role":"user","content":"Hello"}]}'

OpenAI SDK (TypeScript):

import OpenAI from "openai";
const client = new OpenAI({
  baseURL: "https://api.gw.descles.com/v1",
  apiKey: process.env.DESCLES_KEY,          // your Descles token
  defaultHeaders: {
    "X-Descles-Provider-Base-URL": "https://api.deepseek.com/v1",
    "X-Descles-Provider-Key": process.env.PROVIDER_KEY,
  },
});

Hermes Agent with the hosted DeepSeek credential:

# ~/.hermes/.env
DEEPSEEK_BASE_URL=https://deepseek.gw.descles.com/v1
DEEPSEEK_API_KEY=<your-descles-agent-key>

# then select deepseek-chat in: hermes model
Hermes probes /v1/models before accepting a custom endpoint. Descles routes that probe through the same organization-scoped BYOK credential as chat requests.

Claude Code uses hosted BYOK with the Anthropic-native endpoint:

export ANTHROPIC_BASE_URL=https://anthropic.gw.descles.com
export ANTHROPIC_AUTH_TOKEN=<your-descles-agent-key>
# store your Anthropic key once in the console Providers page (hosted BYOK);
# no provider key stays on this machine.

3 · Load a starter policy

Open Console → Policy, choose Load starter policy, review the tool names, and save. Tool names must match what your runtime declares; the starter includes common Claude Code and terminal-style names.

defaults:
  tools:
    Bash: require_approval
    Edit: require_approval
    Write: deny
    terminal: require_approval
    execute_code: deny
    shell.rm: deny
  budget:
    daily_usd: 10

Budgets are enforced per UTC day and can set a USD cap (daily_usd), a token cap (daily_tokens, counting input + output + cached), or both. Token caps are price-independent — use them for router aggregators or any endpoint whose pricing you know better than a generic estimate.

Both provider modes use the same gateway enforcement and create the same attributed traces and audit evidence.

Endpoints & protocols

One wildcard domain serves every provider you route through. The subdomain names the provider; the tenant is identified by your token.

https://<provider>.gw.descles.com/v1/chat/completions   OpenAI-compatible
https://<provider>.gw.descles.com/v1/responses         Responses API
https://<provider>.gw.descles.com/anthropic/v1/messages Anthropic Messages
https://<provider>.gw.descles.com/v1/models            Model list
You call todaySwitch base_url to
api.deepseek.comdeepseek.gw.descles.com
api.openai.comopenai.gw.descles.com
api.anthropic.comanthropic.gw.descles.com
openrouter.ai/api/v1openrouter.gw.descles.com
your own vLLManything.gw.descles.com (name it what you like)

Model routing: the request body is forwarded as-is; your provider decides which models it serves. No registry, no model mapping to maintain.

Router companies & custom endpoints. Descles never needs to know your provider — any endpoint that speaks the OpenAI protocol works: aggregators (OpenRouter, OneAPI-style routers), LiteLLM proxies, and self-hosted vLLM / SGLang / TGI. In Console → Providers choose custom / router / self-hosted and give the endpoint a name; the name becomes your subdomain, so https://<your-name>.gw.descles.com/v1 routes to that base URL with the stored key. For a hostname the name must be lowercase letters, digits and hyphens only; if your label cannot be a hostname (dots, underscores, long vendor strings) — or you keep no stored credential — use the per-request X-Descles-Provider-Base-URL + X-Descles-Provider-Key headers against api.gw.descles.com instead. Under an explicit-allowlist egress firewall, add the endpoint host once and it is reachable like any provider.

Cost on unknown models. Your model string is forwarded untouched, so Descles may not have a price-table entry for it — the cost_usd estimate then falls back to a default price and can be off. For custom or aggregator endpoints prefer token budgets (daily_tokens), which are price-independent.

Provider keys — two modes

Mode A · Hosted BYOK (recommended for teams)

Your org stores the provider credential once, encrypted at rest (AES-GCM, never returned by any list endpoint). Employees authenticate with plain Descles tokens — no provider key on their machines, instant revocation when you rotate the org key.

# console → Providers → add { provider, base_url, api_key }
# then your client needs only:
baseURL: "https://deepseek.gw.descles.com/v1"
apiKey:  "<descles token>"      // no provider headers

Mode B · Per-request BYOK

Your key never touches Descles storage; you send it on each request (see Quickstart). Use this while evaluating or when your security policy forbids storing provider keys with a third party.

Both modes record the same spans, attribution, budget checks and policy gates. The only difference is where the credential lives.

Console — manage the workspace

Open the Descles console and sign in with Clerk. Your account resolves its workspace automatically; there is no separate admin token to paste.

  • Agents — the single identity view: person groups (members) with their primary agent, plus service agents per group. Every key hangs on an agent; issuing another key rotates the previous one.
  • Providers — store your upstream endpoint + key (Mode A).
  • Budgets — daily USD limits per agent or group. Over budget = the next request is blocked, not just charted.
  • Model traces — every call: employee, agent, model, tokens, cost, latency, policy decision.
  • Approvals — dangerous tool calls pause here until an operator approves or denies.

Policy — writing rules

Policy decides what an agent may do with the tools it proposes. It is evaluated on every model call at the gateway, before anything executes:

  • deny — the tool call is stripped from the model response; the runtime never sees it.
  • require_approval — the call is replaced with a pending-approval placeholder and an approval is opened for an operator.
  • allow (or no rule) — the call passes.

Most-restrictive wins: deny > require_approval > allow. Tool names are matched with . and _ normalized (shell.rm matches what providers may emit as shell_rm), and trailing * wildcards are supported.

# policy.yaml — YAML or JSON. Serve it with DESCLES_POLICY_FILE on startup
# or edit it live in the console (Policy page). JSON inline via DESCLES_POLICY.
defaults:
  tools:
    shell.rm: deny                # nobody deletes files through an agent
    service.delete: deny
  require_approval:
    - github.create_issue         # always ask a human first
  budget:
    daily_usd: 10                 # default daily spend cap per agent

groups:                           # group-wide rules (group = your Agents page
  backend:                        # groups: team or person containers)
    tools:
      shell.kubectl.delete: require_approval
      shell.kubectl.rollout: require_approval
    budget:
      daily_usd: 25

agents:                           # per-agent overrides — highest precedence
  backend-deploy-bot:             # (the agent id from your console)
    tools:
      deploy_to_prod: allow       # explicit allow overrides the group rule
    budget:
      daily_usd: 50

Precedence: agent rules > the agent’s group rules > defaults. An explicit allow at a higher layer overrides a deny/approval below it; within one layer, deny beats approval. Unlisted tools are allowed.

Argument-scoped rules (arg_tools)

Rules inside arg_tools match a tool call’s arguments, not just its name — the finest grain wins. They are evaluated before the plain tools rules of the same layer, and they inherit the same layer precedence (agent > group > defaults) and allow-override semantics. Each entry lists a tool, glob patterns per argument key (path, command, query, url, … — any key the runtime sends), and a decision:

defaults:
  tools:
    read_file: allow             # plain rule: reading is fine…
  arg_tools:
    - tool: read_file            # …but these paths are never readable
      args:
        path: ["**/.env", "**/secrets/**"]
      decision: deny
    - tool: terminal
      args:
        command: ["rm -rf*"]     # destructive shells always ask a human
      decision: require_approval

agents:
  deploy-bot:
    arg_tools:
      - tool: terminal           # a finer allow at the top layer wins
        args:
          command: ["git push*"]
        decision: allow

Matching: * matches any run of characters; a leading **/ matches any directory depth (**/.env also matches a bare .env). A rule matches when every listed argument key matches at least one of its globs; if the argument is missing or structured, its compact JSON form is matched. Among several matching argument rules, deny > require_approval > allow.

Caveat: argument matching on shell command strings is best-effort — rmdir, cd x && rm *, aliases and escapes all dodge a string glob, so treat a command deny as a speed bump, not a wall. Denies on structured keys like path / url are reliable; for a hard execution boundary use a sandbox or an action gateway.

Per-tool status labels

The console’s tool inventory (Policy page) labels each declared or used tool with the state of its effective defaults-layer rules:

  • allowed — no rule (or an explicit allow) applies; calls pass unless a lower/higher layer says otherwise.
  • approval — a plain require_approval rule holds every call for a human.
  • denied — a plain deny rule blocks every call; the runtime never sees it.
  • partial — argument-scoped (arg_tools) rules refine the tool: the plain disposition may be allow/approval but specific argument globs (e.g. read_file allow + **/.env deny) decide per call.

Status is computed from the defaults layer of the effective policy; per-agent overrides in agents can still change the outcome for a specific key. Skills declared by your runtime are just tools with skill names — they get the same status treatment as any other tool.

Tool names are runtime-specific

Rules match the tool names your agent actually declares — Descles does not invent names. If your agent’s tools are terminal, execute_code, web_search (e.g. a Hermes-style runtime), a rule for shell.rm governs nothing. Starter presets for common runtimes:

# Claude Code / Anthropic-style tools
defaults:
  tools:
    Bash: require_approval        # name is exactly what the tool declares
    Read: allow
    Edit: require_approval
    Write: deny
    WebFetch: allow
  require_approval:
    - Bash
    - Edit

# Hermes-style / Codex-style tools
defaults:
  tools:
    terminal: require_approval
    execute_code: deny
    web_search: allow
    read_file: allow
    patch: require_approval

Matching is exact after normalizing . and _ as the same separator (a rule for shell.rm also matches a tool named shell_rm). There is no fuzzy matching — an unknown spelling simply does not match, so a typo’d rule is silently inert. Check the Now in effect card after applying to confirm the rules you intended are live.

Budget enforcement is live at the gateway: an employee or agent over its cap has the next request blocked, not merely charted. Organization policies edited in the hosted console are persisted with that workspace. Self-hosted deployments can also load a baseline from DESCLES_POLICY_FILE.

Security model

  • Identity — every data-plane key is bound to an agent (a member or a service); attribution comes from the key, never from client headers.
  • Tenant isolation — your org sees only its own agents, groups, traces, budgets and approvals; platform views (signups, audit chain) stay with Descles.
  • Tool policy — policy can strip a denied tool call from the model response or replace it with a pending-approval placeholder before the runtime ever executes it.
  • Audit chain — governance events append to a signed hash chain; tampering breaks verification.

Limits & roadmap

Available todayChat Completions, Responses, Anthropic Messages; streaming tool-call enforcement (deny / human approval / resume) across all three SSE protocols; quota; budgets; hosted + per-request BYOK; hash-chained audit.
PlannedSlack approvals, broader native provider endpoints, hosted MCP / action-execution gateways (today the test launch delivers the Model Gateway: streaming tool intent governance, BYOK, budgets, traces), and content guardrails.

Complaints, suggestions, or a roadmap vote? Write to outreach@descles.com.