How to Configure OpenWork Server with Custom Model Providers and Inference Routing
You configure the OpenWork server by defining model providers through a JSON schema in the plugin store, then referencing them via providerId/modelId pairs in client requests that the inference router resolves at runtime.
OpenWork's server architecture centers on a pluggable model-provider system that decouples inference endpoints from the core platform. This design lets organizations integrate proprietary APIs, self-hosted models, or third-party services without modifying the codebase. The configuration flow spans type definitions, API routes, client-side selectors, and runtime secret management—each implemented across the different-ai/openwork monorepo.
Provider Schema and Type Definitions
Every model provider in OpenWork conforms to a strict schema defined in packages/types/src/openwork-provider.ts. This shared package ensures consistency between server validation, client UI, and headless thread execution.
The openworkProviderRefSchema uses Zod to enforce:
id— URL-safe identifier used in routing (e.g.,anthropic-claude,azure-openai,local-ollama)displayName— Human-readable label for dropdown menusbaseUrl— Root endpoint for the provider's inference APIauth— Optional authentication configuration withtype(api-key,oauth,none) andsecretNamereferencing server-side secrets
// packages/types/src/openwork-provider.ts
export const openworkProviderRefSchema = z.object({
id: z.string(),
displayName: z.string(),
baseUrl: z.string().url(),
auth: z.object({
type: z.enum(["api-key", "oauth", "none"]),
secretName: z.string().optional(),
}).optional(),
});
export type OpenworkProviderRef = z.infer<typeof openworkProviderRefSchema>;
Import this type when building custom provider integrations to ensure compile-time safety across the stack.
Registering Custom Model Providers
Providers persist in the organization's plugin store through the Den API, OpenWork's enterprise backend service. The registration endpoint in ee/apps/den-api/src/routes/org/plugin-system/store.ts validates payloads against the schema above before writing to the database.
API Endpoint for Provider Registration
curl -X POST https://your-openwork-server/api/v1/org/<orgId>/provider \
-H "Authorization: Bearer <org-admin-token>" \
-H "Content-Type: application/json" \
-d '{
"id": "custom-bedrock",
"displayName": "AWS Bedrock",
"baseUrl": "https://bedrock-runtime.us-east-1.amazonaws.com",
"auth": { "type": "api-key", "secretName": "BEDROCK_ACCESS_KEY" }
}'
The server returns 201 Created on success or 400 Bad Request with Zod validation errors if fields are malformed. Duplicate id values within an organization trigger 409 Conflict.
Provider Storage Architecture
Registered providers live in the plugin store as versioned records. The Den API wraps CRUD operations with organization-scoped permissions—only admins can mutate provider definitions, while members can read them for model selection.
Inference Routing: From Client Request to Provider Execution
OpenWork's inference router bridges client model selectors to actual HTTP calls against configured providers. The routing flow involves three layers:
1. Client Model Selection
Headless threads and UI components use the ModelSelector type from packages/headless-threads/src/types.ts:
// packages/headless-threads/src/types.ts
export type ModelSelector = {
providerId: string; // Matches provider.id in the store
modelId: string; // Provider-specific model identifier
variant?: string; // Optional routing hint (e.g., "thinking" for tool use)
};
The variant field enables specialized behavior—some providers expose separate endpoints or parameters for reasoning-heavy tasks.
2. RPC Payload Construction
The headless threads client automatically normalizes model selection into RPC parameters. In packages/headless-threads/src/client.ts:
// packages/headless-threads/src/client.ts
await sendRpc({
method: "runTask",
params: {
prompt: userInput,
threadId: activeThread,
...(model === undefined
? {}
: { providerId: model.providerId, modelId: model.modelId }),
},
});
Omitting the model selector triggers fallback to the organization's default provider.
3. Server-Side Resolution and Forwarding
On receiving the RPC, the Den API:
- Validates the
providerIdexists in the organization's plugin store - Retrieves the associated
baseUrlandauthconfiguration - Fetches the named secret from the runtime secret manager
- Constructs the proxied request with injected authentication headers
- Streams the provider's response back to the client
Error handling in ee/apps/den-api/src/automations/authority.ts maps provider failures to standardized codes:
| Error Code | Trigger Condition |
|---|---|
provider_unavailable |
Provider record missing or baseUrl unreachable |
provider_authentication_denied |
Secret missing or rejected by provider |
provider_rate_limited |
HTTP 429 from provider with retry-after header |
model_not_found |
Valid provider, but modelId unrecognized |
Complete Configuration Example: Self-Hosted Ollama
Below is an end-to-end setup for routing requests to a local Ollama instance.
Step 1: Register the Provider
curl -X POST https://openwork.internal/api/v1/org/acme-corp/provider \
-H "Authorization: Bearer $ACME_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"id": "ollama-local",
"displayName": "Local Ollama (GPU Server)",
"baseUrl": "http://ollama-gpu.internal:11434/v1",
"auth": { "type": "none" }
}'
Step 2: Set Organization Default (Optional)
Configure via admin API or UI to make Ollama the fallback when no model is specified.
Step 3: Client-Side Invocation
import { OpenWorkClient } from "@openwork/client";
const client = new OpenWorkClient({
serverUrl: "https://openwork.internal"
});
const response = await client.runTask({
prompt: "Explain quantum error correction",
model: {
providerId: "ollama-local",
modelId: "codellama:34b",
variant: "thinking" // Uses Ollama's extended context mode
},
});
The router transparently handles request/response translation between OpenWork's internal protocol and Ollama's OpenAI-compatible endpoint.
Runtime Secret Management
Providers requiring authentication reference secrets by name—the actual values never enter the plugin store. At server startup, OpenWork loads secrets from:
- Production: Infisical or HashiCorp Vault integration
- Development:
.envfiles withOPENWORK_SECRET_*prefixes - Testing: Mock secret provider in
packages/enterprise-mcp-mock-server/src/runtime/mock-server.ts
The mock server implementation demonstrates secret resolution patterns used in production:
// packages/enterprise-mcp-mock-server/src/runtime/mock-server.ts
async function resolveProviderSecret(secretName: string): Promise<string> {
const value = process.env[`OPENWORK_SECRET_${secretName}`];
if (!value) {
throw new ProviderSecretError(`Missing secret: ${secretName}`);
}
return value;
}
Use environment-specific secret loading to rotate credentials without redeploying provider definitions.
UI-Based Provider Administration
The desktop application exposes provider management through components in apps/app/src/components/provider-settings.tsx. Administrators can:
- Browse existing providers with live connectivity indicators
- Add new providers via guided forms that validate
baseUrlreachability - Edit
authconfigurations without exposing secret values - Delete or disable providers with automatic client migration prompts
The UI shares validation logic with the server by importing openworkProviderRefSchema from the types package, ensuring consistent error messages across interfaces.
Advanced: Multi-Provider Routing Strategies
For high-availability deployments, configure provider fallthrough chains via organization policy:
{
"inferencePolicy": {
"defaultProvider": "azure-openai",
"fallbacks": [
{ "providerId": "anthropic-claude", "if": "rate_limited" },
{ "providerId": "ollama-local", "if": "all_unavailable" }
]
}
}
This policy, stored in ee/apps/den-api/src/routes/org/policies/settings.ts, triggers automatic rerouting when the primary provider returns specific error conditions.
Summary
- Provider definitions use
openworkProviderRefSchemainpackages/types/src/openwork-provider.tsto ensure type safety across the stack - Registration occurs via POST to
/api/v1/org/<orgId>/providerin the Den API, with Zod validation before persistence - Client selection sends
providerId/modelIdpairs through headless threads RPC, constructed inpackages/headless-threads/src/client.ts - Runtime routing resolves providers from the plugin store, injects secrets, and proxies requests with standardized error handling in
authority.ts - Secrets remain external to provider configs, loaded at runtime from environment or vault systems
Frequently Asked Questions
How do I add a provider that requires OAuth2 instead of API keys?
Set auth.type to "oauth" and include secretName referencing a JSON blob with clientId, clientSecret, and tokenUrl. The Den API exchanges credentials for access tokens before proxying requests, caching tokens with automatic refresh. See ee/apps/den-api/src/routes/org/plugin-system/oauth.ts for the token exchange implementation.
Can I restrict which models appear for each provider?
Yes—implement a model manifest endpoint on your provider's baseUrl (e.g., GET /models). The OpenWork server periodically fetches this list to populate UI dropdowns. Filter the returned array to control visibility without changing server configuration.
What happens if my custom provider's endpoint is temporarily down?
The inference router returns provider_unavailable after a configurable timeout (default 30s). Clients receive this error with the provider's displayName for user-friendly messaging. If fallbacks are configured in organization policy, the server automatically retries with the next provider before surfacing failure to the client.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →