Connect Third-Party LLMs
This tutorial shows how to connect SOAT to hosted LLM providers such as xAI, OpenAI, Anthropic, and Amazon Bedrock. You will:
- Log in as admin.
- Create a project.
- Store provider credentials as secrets.
- Create provider records for third-party LLMs.
- Create an agent backed by one of those providers.
- Start a conversation and inspect the result.
By the end you will understand how Secrets, AI Providers, Agents, and Sessions work together for externally hosted models.
Prerequisites
- SOAT running locally. Follow Quick Start if needed.
- CLI installed and configured, or SDK set up. See CLI or SDK.
- Server is at
http://localhost:5047. - Valid credentials for at least one third-party provider.
- CLI
- SDK
- curl
export SOAT_BASE_URL=http://localhost:5047
import { SoatClient } from '@soat/sdk';
export SOAT_BASE_URL=http://localhost:5047
Step 1 — Log in as admin
Admin is the built-in superuser role. It bypasses policy evaluation entirely. See Users for full authentication and user management details.
- CLI
- SDK
- curl
ADMIN_TOKEN=$(soat login-user --username admin --password Admin1234! | jq -r '.token')
export SOAT_TOKEN=$ADMIN_TOKEN
const soat = new SoatClient({ baseUrl: 'http://localhost:5047' });
const { data: session, error } = await soat.users.loginUser({
body: { username: 'admin', password: 'Admin1234!' },
});
if (error) throw new Error(JSON.stringify(error));
const adminSoat = new SoatClient({
baseUrl: 'http://localhost:5047',
token: session.token,
});
ADMIN_TOKEN=$(curl -s -X POST "$SOAT_BASE_URL/api/v1/users/login" \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"Admin1234!"}' | jq -r '.token')
Step 2 — Create a project
Every resource in SOAT lives inside a project. Create one to hold the provider and agent.
- CLI
- SDK
- curl
PROJECT_ID=$(soat create-project --name "Hosted LLM Demo" | jq -r '.id')
echo "PROJECT_ID: $PROJECT_ID"
const { data: project, error } = await adminSoat.projects.createProject({
body: { name: 'Hosted LLM Demo' },
});
if (error) throw new Error(JSON.stringify(error));
const PROJECT_ID = project.id;
PROJECT_ID=$(curl -s -X POST "$SOAT_BASE_URL/api/v1/projects" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"Hosted LLM Demo"}' | jq -r '.id')
Step 3 — Store provider credentials as secrets
Secrets store sensitive values encrypted; providers reference them by ID. Create one secret per provider credential set.
- CLI
- SDK
- curl
OPENAI_SECRET_ID=$(soat create-secret \
--project-id "$PROJECT_ID" \
--name "openai-api-key" \
--value "sk-<your-openai-key>" | jq -r '.id')
ANTHROPIC_SECRET_ID=$(soat create-secret \
--project-id "$PROJECT_ID" \
--name "anthropic-api-key" \
--value "sk-ant-<your-anthropic-key>" | jq -r '.id')
XAI_SECRET_ID=$(soat create-secret \
--project-id "$PROJECT_ID" \
--name "xai-api-key" \
--value "xai-<your-xai-key>" | jq -r '.id')
BEDROCK_SECRET_ID=$(soat create-secret \
--project-id "$PROJECT_ID" \
--name "bedrock-credentials" \
--value '{"accessKeyId":"<aws-access-key-id>","secretAccessKey":"<aws-secret-access-key>","sessionToken":"<optional-session-token>"}' | jq -r '.id')
const { data: openAiSecret } = await adminSoat.secrets.createSecret({
body: {
project_id: PROJECT_ID,
name: 'openai-api-key',
value: 'sk-<your-openai-key>',
},
});
const { data: anthropicSecret } = await adminSoat.secrets.createSecret({
body: {
project_id: PROJECT_ID,
name: 'anthropic-api-key',
value: 'sk-ant-<your-anthropic-key>',
},
});
const { data: xaiSecret } = await adminSoat.secrets.createSecret({
body: {
project_id: PROJECT_ID,
name: 'xai-api-key',
value: 'xai-<your-xai-key>',
},
});
const { data: bedrockSecret } = await adminSoat.secrets.createSecret({
body: {
project_id: PROJECT_ID,
name: 'bedrock-credentials',
value:
'{"accessKeyId":"<aws-access-key-id>","secretAccessKey":"<aws-secret-access-key>","sessionToken":"<optional-session-token>"}',
},
});
OPENAI_SECRET_ID=$(curl -s -X POST "$SOAT_BASE_URL/api/v1/secrets" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"project_id\":\"$PROJECT_ID\",\"name\":\"openai-api-key\",\"value\":\"sk-<your-openai-key>\"}" \
| jq -r '.id')
XAI_SECRET_ID=$(curl -s -X POST "$SOAT_BASE_URL/api/v1/secrets" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"project_id\":\"$PROJECT_ID\",\"name\":\"xai-api-key\",\"value\":\"xai-<your-xai-key>\"}" \
| jq -r '.id')
Step 4 — Create provider records
Each provider points to a hosted model endpoint. See AI Providers for the full list of supported providers and configuration options. Choose the provider that matches your hosted model:
- CLI
- SDK
- curl
# OpenAI
OPENAI_PROVIDER_ID=$(soat create-ai-provider \
--project-id "$PROJECT_ID" \
--name "OpenAI" \
--provider "openai" \
--default-model "gpt-4.1-mini" \
--secret-id "$OPENAI_SECRET_ID" | jq -r '.id')
# Anthropic
ANTHROPIC_PROVIDER_ID=$(soat create-ai-provider \
--project-id "$PROJECT_ID" \
--name "Anthropic" \
--provider "anthropic" \
--default-model "claude-3-5-sonnet-latest" \
--secret-id "$ANTHROPIC_SECRET_ID" | jq -r '.id')
# xAI
XAI_PROVIDER_ID=$(soat create-ai-provider \
--project-id "$PROJECT_ID" \
--name "xAI" \
--provider "xai" \
--default-model "grok-3-mini" \
--secret-id "$XAI_SECRET_ID" | jq -r '.id')
# Bedrock (secret value can be JSON credentials, region can live in config)
BEDROCK_PROVIDER_ID=$(soat create-ai-provider \
--project-id "$PROJECT_ID" \
--name "Bedrock" \
--provider "bedrock" \
--default-model "anthropic.claude-3-5-sonnet-20240620-v1:0" \
--secret-id "$BEDROCK_SECRET_ID" \
--config '{"region":"us-east-1"}' | jq -r '.id')
const { data: openAiProvider } = await adminSoat.aiProviders.createAiProvider({
body: {
project_id: PROJECT_ID,
name: 'OpenAI',
provider: 'openai',
default_model: 'gpt-4.1-mini',
secret_id: openAiSecret.id,
},
});
const { data: xaiProvider } = await adminSoat.aiProviders.createAiProvider({
body: {
project_id: PROJECT_ID,
name: 'xAI',
provider: 'xai',
default_model: 'grok-3-mini',
secret_id: xaiSecret.id,
},
});
const { data: bedrockProvider } = await adminSoat.aiProviders.createAiProvider({
body: {
project_id: PROJECT_ID,
name: 'Bedrock',
provider: 'bedrock',
default_model: 'anthropic.claude-3-5-sonnet-20240620-v1:0',
secret_id: bedrockSecret.id,
config: { region: 'us-east-1' },
},
});
OPENAI_PROVIDER_ID=$(curl -s -X POST "$SOAT_BASE_URL/api/v1/ai-providers" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"project_id\":\"$PROJECT_ID\",\"name\":\"OpenAI\",\"provider\":\"openai\",\"default_model\":\"gpt-4.1-mini\",\"secret_id\":\"$OPENAI_SECRET_ID\"}" \
| jq -r '.id')
Step 5 — Create an agent
Once the provider exists, create an agent that points at it.
- CLI
- SDK
- curl
AGENT_ID=$(soat create-agent \
--project-id "$PROJECT_ID" \
--ai-provider-id "$OPENAI_PROVIDER_ID" \
--name "Hosted Assistant" \
--instructions "You are a helpful assistant using a hosted LLM." \
| jq -r '.id')
const { data: agent } = await adminSoat.agents.createAgent({
body: {
project_id: PROJECT_ID,
ai_provider_id: openAiProvider.id,
name: 'Hosted Assistant',
instructions: 'You are a helpful assistant using a hosted LLM.',
},
});
AGENT_ID=$(curl -s -X POST "$SOAT_BASE_URL/api/v1/agents" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"project_id\":\"$PROJECT_ID\",\"ai_provider_id\":\"$OPENAI_PROVIDER_ID\",\"name\":\"Hosted Assistant\",\"instructions\":\"You are a helpful assistant using a hosted LLM.\"}" \
| jq -r '.id')
Step 6 — Start a conversation
Create a session and send a message through the provider-backed agent.
- CLI
- SDK
- curl
SESSION_ID=$(soat create-agent-session --agent-id "$AGENT_ID" | jq -r '.id')
soat add-session-message \
--agent-id "$AGENT_ID" \
--session-id "$SESSION_ID" \
--message "Summarize why model routing matters in one paragraph."
const { data: session2 } = await adminSoat.sessions.createAgentSession({
path: { agent_id: agent.id },
body: {},
});
await adminSoat.sessions.addSessionMessage({
path: { agent_id: agent.id, session_id: session2.id },
body: { message: 'Summarize why model routing matters in one paragraph.' },
});
SESSION_ID=$(curl -s -X POST "$SOAT_BASE_URL/api/v1/agents/$AGENT_ID/sessions" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{}' | jq -r '.id')
curl -s -X POST "$SOAT_BASE_URL/api/v1/agents/$AGENT_ID/sessions/$SESSION_ID/messages" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"message":"Summarize why model routing matters in one paragraph."}'
What's next
- Provider rotation: Create multiple provider records in the same project and switch agents between them.
- Custom gateways: Use the
gatewayorcustomprovider types when you have an OpenAI-compatible upstream. - Production secrets: Rotate provider secrets by creating a new secret and updating the provider's
secret_id.