> ## Documentation Index
> Fetch the complete documentation index at: https://docs.modulex.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Connect LLM providers

> Connect language-model providers to ModuleX as managed (modulexai, billed in credits) or BYOK (your own key, uncosted), and select models for chat, the Assistant, and workflow nodes.

export const MediaEmbed = ({id, type = 'screenshot', caption = '', ext, ratio = '16 / 9'}) => {
  const isVideo = type === 'video' || type === 'app_video';
  const resolvedExt = ext || (isVideo ? 'mp4' : type === 'screenshot' ? 'webp' : 'svg');
  const src = 'https://media.modulex.dev/' + id + '.' + resolvedExt;
  const [status, setStatus] = useState('loading');
  const [isDev, setIsDev] = useState(false);
  const [inView, setInView] = useState(false);
  const boxRef = useRef(null);
  useEffect(() => {
    if (typeof window === 'undefined') return;
    const h = window.location.hostname;
    setIsDev(h === 'localhost' || h === '127.0.0.1' || h.endsWith('.mintlify.app'));
  }, []);
  useEffect(() => {
    if (inView) return;
    if (typeof IntersectionObserver === 'undefined') {
      setInView(true);
      return;
    }
    const el = boxRef.current;
    if (!el) return;
    const io = new IntersectionObserver(entries => {
      if (entries.some(e => e.isIntersecting)) {
        setInView(true);
        io.disconnect();
      }
    }, {
      rootMargin: '300px'
    });
    io.observe(el);
    return () => io.disconnect();
  }, [inView]);
  if (status === 'missing') {
    if (!isDev) return null;
    return <div style={{
      display: 'flex',
      flexDirection: 'column',
      justifyContent: 'center',
      gap: '0.4rem',
      padding: '1rem 1.25rem',
      margin: '1.25rem 0',
      width: '100%',
      aspectRatio: ratio,
      boxSizing: 'border-box',
      border: '1px dashed rgba(128,128,128,0.45)',
      borderRadius: '0.75rem',
      background: 'rgba(128,128,128,0.06)',
      color: 'currentColor',
      fontSize: '0.85rem',
      lineHeight: 1.45
    }}>
        <div style={{
      display: 'flex',
      alignItems: 'center',
      gap: '0.5rem',
      opacity: 0.75
    }}>
          <span aria-hidden="true">🎬</span>
          <code style={{
      fontSize: '0.75rem'
    }}>{id}</code>
          <span style={{
      fontSize: '0.65rem',
      textTransform: 'uppercase',
      letterSpacing: '0.04em',
      padding: '0.1rem 0.4rem',
      borderRadius: '0.4rem',
      background: 'rgba(128,128,128,0.18)'
    }}>
            {type}
          </span>
        </div>
        <div style={{
      opacity: 0.9
    }}>{caption || 'Media not uploaded yet.'}</div>
        <div style={{
      fontSize: '0.7rem',
      opacity: 0.5
    }}>
          Upload to R2 as <code>{id}.{resolvedExt}</code> — preview only, hidden in production.
        </div>
      </div>;
  }
  const mediaStyle = {
    display: status === 'loaded' ? 'block' : 'none',
    width: '100%',
    height: 'auto',
    borderRadius: '0.75rem'
  };
  const media = isVideo ? <video src={inView ? src : undefined} autoPlay loop muted playsInline preload="metadata" onLoadedData={() => setStatus('loaded')} onError={() => setStatus('missing')} style={mediaStyle} /> : <img src={inView ? src : undefined} alt={caption} onLoad={() => setStatus('loaded')} onError={() => setStatus('missing')} style={mediaStyle} />;
  return <figure style={{
    margin: '1.25rem 0'
  }}>
      <div ref={boxRef} style={status === 'loaded' ? {
    width: '100%'
  } : {
    width: '100%',
    aspectRatio: ratio,
    borderRadius: '0.75rem',
    background: 'rgba(128,128,128,0.06)'
  }}>
        {media}
      </div>
      {status === 'loaded' && caption ? <figcaption style={{
    marginTop: '0.5rem',
    textAlign: 'center',
    fontSize: '0.85rem',
    opacity: 0.7
  }}>
          {caption}
        </figcaption> : null}
    </figure>;
};

ModuleX runs every language-model call through an **LLM provider**: a connector that exposes a set of **models** and one or more **auth schemas**. You connect a provider once, then any surface that calls a model — the [Chat model selector](/platform/chat/model-selection), the [Assistant](/assistant/models-and-settings), the [LLM node](/workflow-builder/nodes/llm), the [Agent node](/workflow-builder/nodes/agent), and the [AI Composer](/concepts/ai-composer) — can use its models.

There are two ways to connect a provider, and the difference is who pays and how:

<CardGroup cols={2}>
  <Card title="ModuleX-managed (modulexai)" icon="server" href="/integrations/llm-providers/modulexai">
    ModuleX provisions the upstream provider keys for you. Usage is metered in **credits** against your plan allowance and wallet. No provider account or key required.
  </Card>

  <Card title="Bring your own key (BYOK)" icon="key">
    You supply your own provider API key. Usage is **billed directly by the provider** with no ModuleX markup, and is **not** charged in ModuleX credits (analytics-only).
  </Card>
</CardGroup>

<MediaEmbed id="MX-MEDIA-4050" type="image" caption={"Diagram of the two connection paths for an LLM provider."} />

## Supported providers

The catalog ships these LLM providers. The **wire name** is the value you send as `integration_name` when you create a credential or pick a model.

| Provider        | Wire name   | Connect with                  | Billing         | Page                                                            |
| --------------- | ----------- | ----------------------------- | --------------- | --------------------------------------------------------------- |
| ModuleX-managed | `modulexai` | `modulex_key` (no key needed) | Credits         | [ModuleX-managed models](/integrations/llm-providers/modulexai) |
| OpenAI          | `openai`    | `api_key` (BYOK)              | Provider-direct | [OpenAI](/integrations/llm-providers/openai)                    |
| Anthropic       | `anthropic` | `api_key` (BYOK)              | Provider-direct | [Anthropic](/integrations/llm-providers/anthropic)              |
| Google Gemini   | `gemini`    | `api_key` (BYOK)              | Provider-direct | [Google Gemini](/integrations/llm-providers/gemini)             |
| xAI Grok        | `xai`       | `api_key` (BYOK)              | Provider-direct | [xAI Grok](/integrations/llm-providers/xai)                     |

<Note>
  Two further providers exist on the wire but are routing layers, not separately documented destinations. `openrouter` is a BYOK aggregator used internally to route many of the managed models, and `bedrock` carries only deprecated model ids that route to their replacements. Connect `modulexai` for managed access or a first-party provider above for BYOK; you do not need to connect `openrouter` or `bedrock` directly. See [Selecting a model](#selecting-a-model) for how deprecated ids are handled, and the [open questions](#open-questions-tbd) below.
</Note>

Each provider's detail record is read-only metadata served by the catalog API (`GET /integrations/llm-providers/{provider_name}`). It carries the provider's `models[]` and its `auth_schemas[]`; it never returns or stores your secret key.

## Managed vs BYOK

The two modes differ in authentication, billing, and what you have to supply.

<Tabs>
  <Tab title="ModuleX-managed (modulexai)">
    * **Wire name:** `modulexai`.
    * **Auth schema:** `modulex_key`. ModuleX provides the upstream key automatically — `setup_environment_variables` is empty, so there is nothing for you to enter.
    * **Billing:** **credited.** Every managed turn is metered (see [Credit cost](#credit-cost-for-managed-usage)) and flows through the billing admission gate, which can return a [`DenialEnvelope`](/api-reference/errors) on `402` / `403` / `429` when your allowance and wallet are exhausted.
    * **Use it when** you want one provider with no key management, curated models, and unified usage tracking. It is the default and recommended provider.
    * Models are curated and may be routed across upstreams (for example, several `modulexai` models route through OpenRouter with a Bedrock-then-Anthropic fallback order). You select by ModuleX `model_id`; the routing is handled for you.
  </Tab>

  <Tab title="Bring your own key (BYOK)">
    * **Wire names:** `openai`, `anthropic`, `gemini`, `xai`.
    * **Auth schema:** `api_key`. You supply the provider's key (and optionally a custom `base_url` for compatible endpoints).
    * **Billing:** **uncosted in ModuleX credits.** The upstream provider bills your account directly, with no ModuleX markup. ModuleX records BYOK calls for analytics only; the credit gate does not charge them.
    * **Use it when** you have provider accounts, want vendor-direct pricing, or need a specific model not offered under `modulexai`.
    * There is no `feature.byok` plan entitlement — BYOK is not plan-gated. You still need the [owner or admin role](/security/roles-permissions) to manage the provider credential.
  </Tab>
</Tabs>

<Warning>
  Managed (`modulexai`) usage **consumes credits and is subject to the billing gate**; BYOK usage is **not** credited. Switching a chat, Assistant, or node from a `modulexai` model to a BYOK model moves that cost off your ModuleX credits and onto your provider bill, and vice versa. Confirm which model is selected before you rely on either billing path.
</Warning>

## Connecting a provider

Managed access needs no setup — `modulexai` is available out of the box and requires no credential. For BYOK, you create a **credential** that stores your provider key (encrypted at rest), in the app or over the API.

### In the app

<Steps>
  <Step title="Open the integration's connect screen">
    Open the integration catalog, find the provider (for example, OpenAI), and choose to connect it. You must be an **owner or admin** of the organization.
  </Step>

  <Step title="Enter your provider key">
    Paste the provider API key into the field defined by the provider's `api_key` auth schema. Some providers also accept an optional **Base URL** for compatible endpoints.
  </Step>

  <Step title="Validate and save">
    ModuleX validates the key against the provider's `test_endpoint` before saving (a minimal, low-cost request). On success the credential is stored and becomes selectable wherever models are chosen.
  </Step>
</Steps>

<MediaEmbed id="MX-MEDIA-4051" type="screenshot" caption={"The connect-credential dialog for a BYOK LLM provider."} />

### Over the API

Create the credential with `POST /credentials`. The request body is read raw and the **auth type is auto-detected from the body** — sending `auth_data.api_key` creates an `api_key` credential. Authenticate every request per [Authentication](/api-reference/authentication): `Authorization: Bearer mx_live_…` plus `X-Organization-ID`.

<ParamField body="integration_name" type="string" required>
  The provider wire name — `openai`, `anthropic`, `gemini`, or `xai`. Use `modulexai` only for managed access, which needs no key.
</ParamField>

<ParamField body="auth_data" type="object" required>
  The secret payload. For a BYOK provider, include `api_key`. The presence of `auth_data.api_key` is what selects the `api_key` credential type.

  <Expandable title="auth_data fields">
    <ParamField body="auth_data.api_key" type="string" required>
      Your provider API key (for example, a key starting `sk-ant-` for Anthropic or `sk-` for OpenAI). Stored encrypted; never returned in responses.
    </ParamField>

    <ParamField body="auth_data.base_url" type="string">
      Optional custom base URL for a provider-compatible endpoint. Defaults to the provider's standard API host when omitted.
    </ParamField>
  </Expandable>
</ParamField>

<ParamField body="display_name" type="string">
  A human-readable label for the credential, shown in the app and credential list.
</ParamField>

<ParamField body="make_default" type="boolean" default="false">
  When `true`, this credential becomes the default for the integration; any prior default for the same integration is unset.
</ParamField>

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.modulex.dev/credentials \
    -H "Authorization: Bearer mx_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
    -H "X-Organization-ID: 8f1c2b3a-4d5e-6f70-8192-a3b4c5d6e7f8" \
    -H "Content-Type: application/json" \
    -d '{
      "integration_name": "openai",
      "display_name": "Production OpenAI key",
      "make_default": true,
      "auth_data": { "api_key": "sk-proj-xxxxxxxxxxxxxxxxxxxx" }
    }'
  ```

  ```python Python theme={null}
  import os
  from modulex import Modulex

  client = Modulex(
      api_key=os.environ["MODULEX_API_KEY"],          # mx_live_...
      organization_id=os.environ["MODULEX_ORG_ID"],
  )

  credential = await client.credentials.create(
      integration_name="openai",
      display_name="Production OpenAI key",
      make_default=True,
      auth_data={"api_key": os.environ["OPENAI_API_KEY"]},
  )
  print(credential.id)
  ```

  ```javascript JavaScript theme={null}
  import { Modulex } from "modulex-js";

  const client = new Modulex({
    apiKey: process.env.MODULEX_API_KEY,              // mx_live_...
    organizationId: process.env.MODULEX_ORG_ID,
  });

  const credential = await client.credentials.create({
    integration_name: "openai",
    display_name: "Production OpenAI key",
    make_default: true,
    auth_data: { api_key: process.env.OPENAI_API_KEY },
  });
  console.log(credential.id);
  ```
</CodeGroup>

<Note>
  The SDK method names and shapes above follow the documented credential surface. Confirm the exact SDK method path against the [SDK ⇄ API parity matrix](/sdks/parity); where an SDK lacks a method, call `POST /credentials` directly with cURL.
</Note>

A successful create returns `201` with the credential record. To check a key before saving it, use `POST /credentials/test-temporary` with `integration_name`, `auth_type: "api_key"`, and `auth_data`; it runs the provider's validation endpoint and returns `is_valid`. For rotation, scoping, and listing, see [Managing credentials](/integrations/managing-credentials).

#### Errors

| Status | Shape      | When                                                                                            |
| ------ | ---------- | ----------------------------------------------------------------------------------------------- |
| `400`  | `{detail}` | `auth_data`/`auth_type` could not be resolved to a known credential type, or validation failed. |
| `401`  | `{detail}` | Missing or invalid `Authorization`, or missing `X-Organization-ID`.                             |
| `403`  | `{detail}` | You are not an owner or admin of the organization.                                              |
| `500`  | `{detail}` | Unexpected server error while creating the credential.                                          |

The credential routes return the FastAPI `{detail}` `HTTPException` envelope (a string message). The flat `DenialEnvelope` billing shape (`{code, layer, key, current, limit, reason}`) appears only on **managed-usage** surfaces — the run, Composer, Assistant, and managed-knowledge calls that actually meter credits — not on credential CRUD. See [Errors & status codes](/api-reference/errors) for all three envelope shapes.

## Selecting a model

A provider exposes its models in a `models[]` array. You select a model by its ModuleX `model_id` (for example, `claude-sonnet-4.6`), and ModuleX resolves that to the upstream wire model and request shape for you.

### The model record

Each entry in `models[]` carries the fields below. Values come from the provider's catalog record; pricing fields apply to BYOK provider-direct billing and inform managed credit metering.

<ResponseField name="id" type="string">
  The ModuleX `model_id` you select (for example, `claude-opus-4.7`, `gpt-5.5`, `gemini-2.5-pro`).
</ResponseField>

<ResponseField name="display_name" type="string">
  Human-readable model name shown in selectors.
</ResponseField>

<ResponseField name="provider" type="string">
  Display vendor, for example `Anthropic` or `OpenAI`.
</ResponseField>

<ResponseField name="provider_id" type="string">
  Routing provider slug — `anthropic`, `openai`, `gemini`, `xai`, or `openrouter` for models routed through the aggregator under `modulexai`.
</ResponseField>

<ResponseField name="max_input_tokens" type="integer">
  Maximum context window in tokens.
</ResponseField>

<ResponseField name="max_output_tokens" type="integer">
  Maximum tokens the model can generate. `0` for embedding models.
</ResponseField>

<ResponseField name="data_freshness" type="string">
  Knowledge-cutoff date (for example, `2026-01-01`). This field, not a `knowledge_cutoff` key, carries the cutoff.
</ResponseField>

<ResponseField name="intelligence" type="integer">
  Quality score, 1–5.
</ResponseField>

<ResponseField name="speed" type="integer">
  Latency score, 1–5 (higher is faster).
</ResponseField>

<ResponseField name="supports_vision" type="boolean">
  Whether the model accepts image input.
</ResponseField>

<ResponseField name="input_usd_per_1m_tokens" type="number">
  Input price per 1M tokens, as a flat number (not a nested `pricing` object).
</ResponseField>

<ResponseField name="output_usd_per_1m_tokens" type="number">
  Output price per 1M tokens.
</ResponseField>

<ResponseField name="supports_tools" type="boolean">
  Present on some models — whether the model supports tool/function calling.
</ResponseField>

<ResponseField name="supports_structured_outputs" type="boolean">
  Present on some models — whether JSON-schema structured output is supported.
</ResponseField>

<ResponseField name="is_embedding" type="boolean">
  `true` for embedding models. Embedding models report `embedding_dimension` and a zero output price.
</ResponseField>

<ResponseField name="embedding_dimension" type="integer">
  Vector dimension for embedding models (for example, `3072` for `text-embedding-3-large`).
</ResponseField>

<ResponseField name="status" type="string">
  Lifecycle state. Present and set to `deprecated` on retired model ids.
</ResponseField>

<ResponseField name="replacement_id" type="string">
  On a deprecated model, the successor `model_id` that ModuleX routes the call to instead.
</ResponseField>

<Expandable title="Example model record (Anthropic claude-opus-4.7)">
  ```json theme={null}
  {
    "id": "claude-opus-4.7",
    "display_name": "Claude Opus 4.7",
    "provider": "Anthropic",
    "provider_id": "anthropic",
    "max_input_tokens": 1000000,
    "max_output_tokens": 128000,
    "data_freshness": "2026-01-01",
    "intelligence": 5,
    "speed": 1,
    "supports_vision": true,
    "input_usd_per_1m_tokens": 5.0,
    "output_usd_per_1m_tokens": 25.0
  }
  ```
</Expandable>

### Where you select a model

* **Chat** — pick the model in the chat composer. See [Model selection](/platform/chat/model-selection).
* **Assistant** — set the model in Assistant settings. See [Models & settings](/assistant/models-and-settings).
* **LLM node** — set `model_id` on the node. See [LLM node](/workflow-builder/nodes/llm).
* **Agent node** — set `model_id` on the node. See [Agent node](/workflow-builder/nodes/agent).

### Deprecated models route automatically

When you select a model whose `status` is `deprecated` (or `maintenance`), ModuleX resolves it to its `replacement_id` before the call — for example, a `bedrock-claude-sonnet-4.6` id routes to `claude-sonnet-4.6`. Routing is cycle- and depth-guarded, so a chain of replacements terminates at a live model. You do not need to update saved selections immediately, but prefer the live id for clarity.

## Credit cost for managed usage

BYOK calls are **not** credited. Managed (`modulexai`) calls are metered two ways and charged against your [credits](/billing/credits):

* **Per-turn run charge** — each logical run or chat turn is charged a flat `RUN_CREDIT` of **1** credit.
* **Token metering** — the model's tokens are converted to credits:

```text Token-to-credit formula theme={null}
cost = (prompt_tokens · input_rate / 1e6 + completion_tokens · output_rate / 1e6) · MARGIN · SCALE
```

where `input_rate`/`output_rate` are the model's `input_usd_per_1m_tokens` / `output_usd_per_1m_tokens`, `MARGIN` is `1.05` (a 5% system margin), and `SCALE` is `100` (so \*\*100 credits = $1.00**, and 1 credit = $0.01). Embedding calls force `completion_tokens = 0`. An unknown model meters to `0`, never a silent fallback rate.

When managed usage exhausts your plan allowance and wallet, the call is denied with a [`DenialEnvelope`](/api-reference/errors) on `402` / `403` / `429`. See [Credits & metering](/billing/credits) and [Usage gating & limits](/billing/usage-gating) for the full model.

## Permissions and access

Connecting, listing, and managing LLM-provider credentials requires the **owner** or **admin** role in the organization (`member` is retired). Every request also needs the org context header `X-Organization-ID`; a missing header is rejected. See [Roles & permissions](/security/roles-permissions) and [Org context & X-Organization-ID](/security/org-context).

## Per-provider pages

<CardGroup cols={2}>
  <Card title="ModuleX-managed models" icon="server" href="/integrations/llm-providers/modulexai">
    The default managed provider (`modulexai`), billed in credits — no key required.
  </Card>

  <Card title="OpenAI" icon="circle" href="/integrations/llm-providers/openai">
    Connect your OpenAI account (BYOK) and use GPT models.
  </Card>

  <Card title="Anthropic" icon="circle" href="/integrations/llm-providers/anthropic">
    Connect your Anthropic account (BYOK) and use Claude models.
  </Card>

  <Card title="Google Gemini" icon="circle" href="/integrations/llm-providers/gemini">
    Connect your Google account (BYOK) and use Gemini models.
  </Card>

  <Card title="xAI Grok" icon="circle" href="/integrations/llm-providers/xai">
    Connect your xAI account (BYOK) and use Grok models.
  </Card>
</CardGroup>

## Related

* [Authentication & credentials](/integrations/authentication) — the auth-schema variants and OAuth2 flow for integrations.
* [Managing credentials](/integrations/managing-credentials) — create, rotate, scope, and test credentials.
* [Knowledge providers](/integrations/knowledge-providers/overview) — the vector-store side of managed vs BYOK.
* [Credits & metering](/billing/credits) — what consumes credits and how it is measured.

## Open questions (TBD)

* **OpenRouter and Bedrock pages.** Both names exist on the wire (`openrouter` as the BYOK aggregator behind several managed models; `bedrock` carrying only deprecated, replacement-routed ids), but neither has a canonical documentation page in the current information architecture. Whether to document them as standalone connectable providers is **TBD** pending a product decision.
* **Public catalog base URL.** The provider catalog and credential examples use `https://api.modulex.dev` per the repo-wide convention; the exact public host for these endpoints is not pinned from source and should be confirmed against the deployed environment.
