Connections / Model tools

# Connect model tools to Muniment

Use `https://api.muniment.ai` as the model endpoint to keep model access, budgets, and usage receipts governed by your organization. Ask an organization administrator for your Muniment virtual key and an entitled model name. The key is an identity credential: keep it out of source control, command history, support messages, and application logs.

## Wire protocols

The endpoint accepts three wire protocols through the same virtual-key policy:

-   Anthropic Messages at `/v1/messages`
-   OpenAI Responses at `/v1/responses`
-   OpenAI-compatible chat completions at `/v1/chat/completions`

Changing protocol does not change access. A model outside your entitlements is refused before a provider call, and the same key budget applies across tools. Successful calls produce usage receipts attributed to the organization and user represented by the virtual key.

## Claude Code

Claude Code uses the Anthropic Messages protocol. Set these variables in the shell that launches `claude`:

shell

```
export ANTHROPIC_BASE_URL="https://api.muniment.ai"
export ANTHROPIC_AUTH_TOKEN="<muniment-virtual-key>"
unset ANTHROPIC_API_KEY
claude --model "<entitled-model>"
```

Claude Code supplies the required `anthropic-version` header. Feature-specific `anthropic-beta` headers are passed through unchanged.

For fleet-managed installations, deploy the following `managed-settings.json` through MDM or the platform's system-level managed-settings location:

managed-settings.json

```
{
  "env": {
    "ANTHROPIC_BASE_URL": "https://api.muniment.ai"
  }
}
```

Inject `ANTHROPIC_AUTH_TOKEN` separately for each identity; do not put a shared virtual key in managed settings.

## Codex CLI

Export the key, then add a provider to `~/.codex/config.toml`:

shell

```
export MUNIMENT_API_KEY="<muniment-virtual-key>"
```

~/.codex/config.toml

```
model_provider = "muniment"
model = "<entitled-model>"

[model_providers.muniment]
name = "Muniment"
base_url = "https://api.muniment.ai/v1"
env_key = "MUNIMENT_API_KEY"
wire_api = "responses"
```

Codex sends OpenAI Responses requests. Keep `wire_api = "responses"`; chat completions is not a Codex CLI wire mode.

## aider

aider uses OpenAI-compatible chat completions for this connection:

shell

```
export OPENAI_API_BASE="https://api.muniment.ai/v1"
export OPENAI_API_KEY="<muniment-virtual-key>"
aider --model "openai/<entitled-model>"
```

If the model is not in aider's built-in metadata, its model warning does not grant or deny access; Muniment's server-side entitlement remains authoritative.

## OpenCode

Export the key:

shell

```
export MUNIMENT_API_KEY="<muniment-virtual-key>"
```

Add a custom OpenAI-compatible provider to `opencode.json`:

opencode.json

```
{
  "$schema": "https://opencode.ai/config.json",
  "provider": {
    "muniment": {
      "npm": "@ai-sdk/openai-compatible",
      "name": "Muniment",
      "options": {
        "baseURL": "https://api.muniment.ai/v1",
        "apiKey": "{env:MUNIMENT_API_KEY}"
      },
      "models": {
        "<entitled-model>": {
          "name": "<entitled-model>"
        }
      }
    }
  }
}
```

Select `muniment/<entitled-model>` in `/models`. This provider uses chat completions. For an OpenCode model that specifically requires Responses, use `"npm": "@ai-sdk/openai"` with the same base URL and key.

## OpenAI Agents SDK for Python

Install `openai-agents`, export an identity's virtual key and entitled model, then run this complete example:

shell

```
export MUNIMENT_API_KEY="<muniment-virtual-key>"
export MUNIMENT_MODEL="<entitled-model>"
```

Python

```
import os

from agents import Agent, OpenAIChatCompletionsModel, Runner, set_tracing_disabled
from openai import AsyncOpenAI

key = os.environ["MUNIMENT_API_KEY"]
model_name = os.environ["MUNIMENT_MODEL"]

# Muniment governs model calls, but does not accept OpenAI trace exports.
set_tracing_disabled(True)

client = AsyncOpenAI(
    api_key=key,
    base_url="https://api.muniment.ai/v1",
)
agent = Agent(
    name="Muniment assistant",
    instructions="Answer concisely.",
    model=OpenAIChatCompletionsModel(
        model=model_name,
        openai_client=client,
    ),
)

result = Runner.run_sync(agent, "Name one benefit of least-privilege access.")
print(result.final_output)
```

## OpenAI Agents SDK for TypeScript

Create a project and install the SDKs and TypeScript runner:

shell

```
npm init -y
npm install @openai/agents openai tsx
```

Export the same two variables shown in the Python example and save this complete example as `muniment-agent.ts`:

muniment-agent.ts

```
import {
  Agent,
  run,
  setDefaultOpenAIClient,
  setOpenAIAPI,
  setTracingDisabled,
} from "@openai/agents";
import OpenAI from "openai";

async function main() {
  const apiKey = process.env.MUNIMENT_API_KEY;
  const model = process.env.MUNIMENT_MODEL;
  if (!apiKey || !model) {
    throw new Error("Set MUNIMENT_API_KEY and MUNIMENT_MODEL");
  }

  setDefaultOpenAIClient(new OpenAI({
    apiKey,
    baseURL: "https://api.muniment.ai/v1",
  }));
  setOpenAIAPI("chat_completions");
  setTracingDisabled(true);

  const agent = new Agent({
    name: "Muniment assistant",
    instructions: "Answer concisely.",
    model,
  });

  const result = await run(
    agent,
    "Name one benefit of least-privilege access.",
  );
  console.log(result.finalOutput);
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});
```

Run it with the installed TypeScript runner:

shell

```
npx tsx muniment-agent.ts
```

These examples deliberately select chat completions, the OpenAI-compatible agent-framework path verified by the gateway integration suite. Features that require OpenAI-hosted services, including OpenAI trace export, are not routed through Muniment.

## CrewAI 1.15.2

Install the exact version exercised by the gateway integration suite:

shell

```
python -m pip install "crewai==1.15.2"
export MUNIMENT_AGENT_BEARER="<short-lived-muniment-agent-bearer>"
export MUNIMENT_MODEL="<entitled-model>"
```

Use CrewAI's public `LLM` configuration with the Muniment URL and agent bearer. The explicit `provider="openai"` selects CrewAI's OpenAI-compatible chat client without rewriting or restricting Muniment's public model name.

Python

```
import os

from crewai import LLM

llm = LLM(
    model=os.environ["MUNIMENT_MODEL"],
    provider="openai",
    api_key=os.environ["MUNIMENT_AGENT_BEARER"],
    base_url="https://api.muniment.ai/v1",
    max_tokens=64,
)

print(llm.call("Name one benefit of least-privilege access."))
```

Do not substitute an OpenAI or other provider key. Version 1.15.2 is verified to honor the configured Muniment base URL without OpenAI-host key validation.

## PydanticAI 2.9.1

Install the exact verified version and inject the same kind of short-lived agent bearer:

shell

```
python -m pip install "pydantic-ai==2.9.1"
export MUNIMENT_AGENT_BEARER="<short-lived-muniment-agent-bearer>"
export MUNIMENT_MODEL="<entitled-model>"
```

Python

```
import os

from pydantic_ai import Agent
from pydantic_ai.models.openai import OpenAIChatModel
from pydantic_ai.providers.openai import OpenAIProvider

model = OpenAIChatModel(
    os.environ["MUNIMENT_MODEL"],
    provider=OpenAIProvider(
        base_url="https://api.muniment.ai/v1",
        api_key=os.environ["MUNIMENT_AGENT_BEARER"],
    ),
)
agent = Agent(model)

print(agent.run_sync("Name one benefit of least-privilege access.").output)
```

These two configurations are connection proofs: the suite verifies one non-streaming chat-completions result per framework, tenant-scoped receipt provenance, provider-boundary redaction, and rejection of a model entitled only to another organization. Framework tools, capabilities, streaming, and framework-managed tracing are outside this verification scope.

## Other OpenAI-compatible tools

Configure the tool's OpenAI base URL as `https://api.muniment.ai/v1`, its API key as your Muniment virtual key, and its model as an entitled model name. The tool must use either `/v1/chat/completions` or `/v1/responses`; changing the URL to a provider endpoint bypasses Muniment and does not produce a Muniment usage receipt.

## Troubleshooting

-   `401`: the virtual key is missing, invalid, expired, revoked, or blocked.
-   `403`: the requested model is not entitled for this identity. Changing the client or wire protocol will not change the decision.
-   `429`: the virtual-key budget or a gateway rate limit has been reached.
-   `502`: the gateway or configured model provider is temporarily unavailable.

Never replace the virtual key with a provider API key. Provider credentials remain server-side.

## Verified client scope

The gateway integration suite exercises Anthropic Messages, OpenAI Responses, and OpenAI-compatible chat completions with real virtual-key model scope, budget enforcement, provider routing, and usage receipts. It also verifies forwarding of Claude Code's `anthropic-version` and `anthropic-beta` headers. The Python and TypeScript agent examples above use the agent SDKs' documented custom OpenAI client and chat-completions selection against that verified protocol. The TypeScript example's documented `tsx` execution path was also verified with the current packages. CrewAI 1.15.2 and PydanticAI 2.9.1 are additionally exercised as real pinned clients with short-lived agent bearers, including a cross-tenant model denial before provider dispatch.

This page makes support claims only for the clients and configurations shown above. Claude Desktop, Claude's VS Code extension, Codex desktop, and subscription-auth interactions have not been verified against the shipped gateway and are intentionally not described as supported configurations.

## Operator appendix: Configure the beta homelab provider vault

The beta homelab may store organization provider credentials with a deployment-secret-store supplied local key-encryption key. Configure the same values on the API and provider resolver services:

shell

```
PROVIDER_VAULT_KMS_PROVIDER=local
PROVIDER_VAULT_LOCAL_KEK_ID=local:homelab-primary
PROVIDER_VAULT_LOCAL_KEK_ACTIVE_VERSION=1
PROVIDER_VAULT_LOCAL_KEK_KEYRING='{"1":"<padded base64 encoding of exactly 32 random bytes>"}'
```

Generate the key outside the repository and inject the keyring through the deployment secret store. Keep `PROVIDER_VAULT_LOCAL_KEK_ID` stable across rotations. Keep key-store backups separate from database backups. Never put the keyring in a repository file, image, command-line argument, log, or diagnostic response.

For a local rotation, add the new version to the keyring, select it as the active version, restart (the startup check verifies every retained key), then create and run the durable rewrap operation:

shell

```
npm run provider-vault:rewrap -- create local:homelab-primary 1 operator@example.com
npm run provider-vault:rewrap -- resume <operation-id>
```

The first command prints the operation ID. `resume` is safe to repeat after an interruption. Do not remove the old keyring entry until the operation reports completion and a subsequent restart passes readiness.

For local-to-AWS migration, retain all three local settings while changing `PROVIDER_VAULT_KMS_PROVIDER` to `aws-kms` and setting `PROVIDER_VAULT_KMS_KEY_ID`. Restart to verify both readers, create the operation using the old local ID/version as above, and resume it to completion. Only then remove the local settings and restart. The operation rewrites wrapped DEKs and their KEK references; provider ciphertext is unchanged.
