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 menus
  • baseUrl — Root endpoint for the provider's inference API
  • auth — Optional authentication configuration with type (api-key, oauth, none) and secretName referencing 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:

  1. Validates the providerId exists in the organization's plugin store
  2. Retrieves the associated baseUrl and auth configuration
  3. Fetches the named secret from the runtime secret manager
  4. Constructs the proxied request with injected authentication headers
  5. 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:

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 baseUrl reachability
  • Edit auth configurations 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 openworkProviderRefSchema in packages/types/src/openwork-provider.ts to ensure type safety across the stack
  • Registration occurs via POST to /api/v1/org/<orgId>/provider in the Den API, with Zod validation before persistence
  • Client selection sends providerId/modelId pairs through headless threads RPC, constructed in packages/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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →