OpenMAIC

Configuration

Models and providers (openmaic.yml and the model settings), access control, feature flags and other server options.

OpenMAIC has three configuration sources, and each has one job:

SourceWritten byHoldsTakes effect
openmaic.ymlThe operatorProviders, the server's default assignments, what is locked, and whether users may add keysAt startup (restart to apply)
Model settings in the web appUsers of a workspaceTheir own providers, and assignments for every slot openmaic.yml does not lock, stored in the databaseImmediately
Environment variables (.env.local, a container secret)The operatorSecrets that openmaic.yml references, and every other server optionAt startup

DATABASE_URL is required; other environment variables are optional (see Deployment). Providers that use server credentials for model selection also need authorized model lists, as described below. See .env.example for environment variables, openmaic.example.yml for a sample model configuration, and Supported models for preset and model IDs.

Earlier releases configured models only through provider environment variables, server-providers.yml, DEFAULT_MODEL and MODEL_ROUTES. Those still work for now, with the exception of MODEL_ROUTES, and are deprecated; see Migrating from the legacy configuration.

Models and providers

Concepts

  • Provider: an account the server can call, such as an OpenAI key, a MiniMax token plan or a self-hosted OpenAI-compatible server. Each provider has an id you choose and a preset that says which vendor it is and which capabilities it covers.
  • Slot: a use of AI in the product, such as the course outline, slide content, text to speech or web search. Slots form one tree per capability; a slot without an assignment follows its parent.
  • Assignment: which provider and model a slot uses (provider:model), optionally with call parameters and a fallback model, or null to turn the slot off.

A capability is available exactly when its slot resolves to an assignment. There are no separate on/off switches.

openmaic.yml

The server reads openmaic.yml from the directory it starts in (the repository root; /app in the Docker image). Set OPENMAIC_CONFIG to use another path; a relative path is resolved against the same directory, and the file must then exist. Without the file, the server uses the web settings and, while they are still set, the legacy variables.

The file is read once at startup: restart the server after changing it. It is validated completely before the server starts. An unset ${VAR}, an unknown preset or slot, a reference to a provider that is not declared, or a provider that does not offer a slot's capability stops the server, and each problem is listed with its location:

[boot] Invalid server configuration; the server will not start: Invalid model configuration in /app/openmaic.yml:
  - providers.openai.apiKey: environment variable OPENAI_API_KEY is not set
  - slots.course.outlines: unknown slot

Keep secrets out of the file itself: reference them as ${VAR} and set the variable in the environment. Values that came from a variable are never printed in these messages.

A complete example

openmaic.yml
providers:
  # <provider id>: lowercase letters, digits and dashes
  openai:
    preset: openai
    apiKey: ${OPENAI_API_KEY}      # read from the environment at startup
  anthropic:
    preset: anthropic
    apiKey: ${ANTHROPIC_API_KEY}
  openai-image:                    # the same key, used for images
    preset: openai-image
    apiKey: ${OPENAI_API_KEY}
  minimax:                         # a token plan: chat, image, video, TTS, search
    preset: minimax
    apiKey: ${MINIMAX_API_KEY}
  tavily:
    preset: tavily
    apiKey: ${TAVILY_API_KEY}
  local:                           # any OpenAI-compatible server
    preset: openai-compatible
    baseUrl: http://ollama:11434/v1
    models: [qwen3:8b]

slots:
  llm: openai:gpt-5.5              # default for every chat slot below
  course.outline:
    model: anthropic:claude-sonnet-4-6
    fallback: openai:gpt-5.4-mini  # tried once on a retryable failure
  course.content.slide:
    model: minimax:MiniMax-M3
    thinking: { enabled: false }
    fallback: minimax:MiniMax-M2.7
  classroom: local:qwen3:8b        # the model id may contain colons
  agent:
    model: openai:gpt-5.5          # needs tool calling
    api: openai-completions
  tts: minimax:speech-2.8-turbo
  image: openai-image:gpt-image-2
  webSearch: tavily                # provider only: its default model
  video: null                      # off by default; users may turn it on

lock: [tts]                        # fixed for everyone, with its subtree
allowUserKeys: false               # users choose among these providers and add none

With this file, the slots written under slots are the server's defaults: users may change each of them in the model settings, except tts, which lock fixes for everyone. A chat slot it leaves out, such as course.agents, follows its parent (llm); a capability root it leaves out, such as asr or document, stays unconfigured until a user assigns it. With allowUserKeys: false, users choose only among the providers declared here.

Providers

Each entry under providers declares one provider. The key is its id, which slots use in references: 1 to 63 lowercase letters, digits and dashes, starting with a letter or digit.

FieldRequiredMeaning
presetYesWhich built-in preset this provider uses. See Supported models → Presets.
apiKeyUsuallyThe API key. Keyless services (local servers, Brave, SearXNG, unpdf) omit it.
baseUrlSome presetsEndpoint to call instead of the preset's own. Required for openai-compatible, azure, searxng and mineru.
modelsFor server-managed credentialsAuthorized model ids. Deployment slot defaults/fallbacks for the capability are also authorized; catalogues and workspace assignments do not grant additional models.
proxyNoHTTP proxy URL for this provider's requests.
credentialsSome presetsKey pairs for services that do not use one key. AliDocMind takes accessKeyId and accessKeySecret.
optionsNoNon-secret, provider-specific settings, such as backend for voxcpm-tts. Names are letters, digits, _ and -; values are strings, numbers or booleans (no nested maps or lists), and ${VAR} works. The model settings show options, so never put a key in them. Deployment only: the model settings cannot set them.

Most presets cover one capability, following the built-in registries: the openai preset is chat only, and OpenAI images and speech are the openai-image, openai-tts and openai-whisper presets. Declare one provider per capability, and reference the same ${VAR} when one key covers several. Token plan presets (minimax, tokendance, volcengine-ark, kimi-coding-plan) cover several capabilities with one key, each at the plan's own endpoint. A baseUrl on a provider replaces the endpoint for every capability that provider covers, so give a token plan a baseUrl only when all of its capabilities move with it.

Amazon Bedrock (preset: bedrock) signs requests with the AWS credential chain of the server (environment, profile or role) and reads the region from BEDROCK_REGION, AWS_REGION or AWS_DEFAULT_REGION; apiKey is an optional bearer token.

Slots

SlotCapabilityParentWhat it drives
llmchat—The default chat model: every chat call no slot below covers
course.researchchatllmRewriting the web search query while researching a course
course.outlinechatllmScene outlines
course.agentschatllmTeacher and classmate agent profiles
course.contentchatllmScene content, for scene types without a slot of their own
course.content.slidechatcourse.contentSlide scenes
course.content.quizchatcourse.contentQuiz scenes
course.content.interactivechatcourse.contentInteractive scenes
course.content.pblchatcourse.contentProject-based learning scenes
course.actionschatllmScene actions (what the agents say and do on each scene)
classroomchatllmClassroom chat, quiz grading and the project-based learning runtime
agentchatllmThe Pro workbench agent. Requires tool calling. Also accepts api and contextWindow
agent.titlechatagentPro session titles. Configurable in openmaic.yml only; the model settings do not show it
ttstts—Text to speech
asrasr—Speech recognition (voice input, audio and video transcripts)
imageimage—Image generation
videovideo—Video generation
webFetchwebFetch—Web page extraction (default: builtin)
webSearchwebSearch—Web search
documentdocument—The document parsing service (MinerU, MinerU Cloud, AliDocMind). The built-in PDF text extraction needs no service

How a slot resolves:

  • Resolution starts at the slot of the call and walks up to its capability root. The first assignment found wins, so a parent's assignment is the default for its whole subtree. Changing a parent changes every child without its own assignment.
  • Inside a locked subtree (the slot or an ancestor is named by lock, or lock: all), only openmaic.yml counts: the walk goes up to the locked slot and the first assignment the file writes wins. Everywhere else the user's choice wins: the walk first looks for the workspace's own assignment from the slot up to the root, and only when there is none, for the defaults openmaic.yml writes, again from the slot up. So a slot whose server default the workspace has not changed follows the workspace's choice on a parent; server values are only defaults.
  • null stops the walk: the slot and every slot below it that has no assignment of its own are off.
  • Reaching the root without an assignment leaves the capability unconfigured. Calls fail with an error that names the slot; there is no fallback to any vendor.
  • Requirements belong to the slot that declares them and are checked against the model it resolves to, assigned or inherited. agent requires tool calling; if the built-in catalogue says its model lacks it, the agent refuses to run instead of making do. A model the catalogue does not know is allowed. agent.title has no requirement, so it can use a smaller model.

Assignments

An assignment has one of four forms:

slots:
  llm: openai:gpt-5.5            # provider:model
  webSearch: tavily              # provider only: the provider's default model
  course.outline:                # an object with parameters
    model: openai:gpt-5.5
    fallback: anthropic:claude-sonnet-4-6
    thinking: { effort: low }
  video: null                    # off

The object form accepts:

FieldMeaning
modelRequired. provider:model, or provider alone outside chat slots.
fallbackA model to retry on once, in the same form. Language-model slots only; see Fallbacks.
thinkingReasoning parameters for chat models: enabled (true/false), mode (default, disabled, enabled, auto), effort (none, minimal, low, medium, high, xhigh, max), level (minimal, low, medium, high), budgetTokens, excludeReasoningOutput. Each provider adapter applies what its API supports.
apiagent only: the transport dialect, openai-completions (the default) or openai-responses.
contextWindowagent only: the context window to assume for compaction, when it should be lower than the catalogue's.

The agent slot must not set thinking.effort: its tool-calling transport cannot combine reasoning effort with function tools.

Provider-only references

A reference without :model means the provider's default model: the model a token plan recommends for that capability, else the default of the built-in registry. Search and document services mostly have no model to pick, so webSearch: tavily or document: mineru-cloud is the usual form. Chat slots always name a model; llm: openai is refused at startup.

Fallbacks

Fallbacks apply to language-model slots: llm and every slot below it. A fallback on a tts, asr, image, video, webSearch, webFetch or document slot is refused, at startup for openmaic.yml and on save in the model settings.

A slot's fallback is tried once when a call on the primary model fails after its own retries with a retryable failure: a transient SDK error, a timeout, a network error, a rate limit (429), capacity (503), or empty output. An explicit quota refusal from the provider (an OpenAI-compatible code such as insufficient_quota, whatever its HTTP status) is not retried on the primary model: the call goes to the fallback at once. Content-safety refusals and other 4xx errors never fall back. The fallback applies to both regular and streaming calls and uses the slot's thinking parameters.

The fallback belongs to the assignment and is inherited with it: course.content.slide without an assignment of its own uses the model and the fallback of course.content. A slot with a requirement does not retry on a fallback that fails it.

Turning a capability off

Write null on its slot. video: null turns video generation off by default, and users may still turn it on in the model settings; add lock: [video] to keep it off for everyone. course.content.pbl: null turns off project-based learning scene generation while the rest of course.content keeps working. A call on a slot that is off fails immediately, and the media routes answer 403 instead of choosing another vendor.

Defaults, locks and the model settings

What openmaic.yml writes under slots is the server's default: users may change it in the model settings, where the card says Server default, and Reset to server default removes their change. What it names under lock is fixed by the administrator for everyone, together with its whole subtree:

lock: [llm, video]   # these slots and every slot below them
# lock: all          # every slot
  • Every slot lock names must be written under slots (null counts: video: null with lock: [video] keeps video off for everyone). An unknown slot, or a locked slot slots does not write, stops the server at startup with its position (lock.1: ...).
  • A locked slot fixes its children too: with lock: [llm], no user can set course.outline or any other language model slot, and all of them resolve from openmaic.yml alone.
  • lock: all fixes every slot, written or not; a capability root slots leaves out is then unconfigured for everyone. For webFetch, an omitted assignment uses builtin; set webFetch: null to disable it.
  • null, a server default and a lock combine as you would expect: video: null alone is off until a user turns it on; with lock: [video] it stays off.

The settings take one of three shapes, derived from the configuration:

ShapeWhenWhat is shown
Set it up yourselfallowUserKeys is on (the default)Token Plan, Model Services and Course Model Config
Choose a modelallowUserKeys: falseOnly Course Model Config, choosing among the providers openmaic.yml declares
Configured by the administratorEvery slot is locked (lock: all)The course model diagram, every card read-only (each shows its model, or that it is off or not set)

One rule decides every control: it is shown only if using it can change something. A Model Services tab, and adding a service there, appears only while its capability has a slot a user may set; Token Plan appears only while a plan can still fill a slot that is not locked, and connecting a plan never touches a locked slot. A locked card in Course Model Config is one read-only line marked Fixed by the administrator. The home page model picker appears only when the default model can be changed (llm is not locked and a provider offers a language model).

The model settings in the web app read and edit the same tree for the current workspace:

  • Token Plan: connecting a plan adds its provider with the key and fills the slots the plan recommends that are still unassigned. Disconnecting removes the provider; the slots that used it follow their parents again.
  • Model Services: one tab per capability (language models, images, video, text to speech, speech recognition, document parsing, web search) lists its services. Providers from openmaic.yml are shown read-only and without anything about their keys. A service the workspace sets up becomes a workspace provider: its key is write-only and shown only as a mask such as …abcd, a custom endpoint is possible for language models only (Azure Speech takes its official regional endpoint, such as https://<region>.tts.speech.microsoft.com), and self-hosted services and key pairs are configured in openmaic.yml. The test buttons check the saved provider on the server.
  • Course Model Config: the main model is the llm slot and each pipeline stage is its own slot (following the main model means the stage has no assignment of its own); the image, video, speech, search and transcription switches set their slot to null. A locked slot is one read-only line, and a slot on a server default offers Reset to server default once a user has changed it.

Provider settings that a browser stored in earlier versions (providers with their keys and base URLs, the chosen model, token plan enrollment and each capability's selection) are imported into the workspace once. The import never replaces an existing workspace setting or a slot in a subtree openmaic.yml locks, and it skips items that allowUserKeys: false or validation refuses. After a 2xx answer, only what the workspace now holds leaves the browser: every item the server skipped (a custom endpoint for a media service, a preset a workspace may not use, a provider id the deployment declares, ...) is kept in this browser with its key, never sent again, announced once, and listed in Settings → Model Services with the reason until it is set up again or discarded. An Azure Speech provider with its regional endpoint is imported. A 400 refuses the whole proposal: it is not sent again, and its items are kept in this browser with their keys and listed like the skipped ones. An unreadable proposal is dropped. The proposal stays in the browser for a later attempt only on 401, 404, 409, a 5xx or a network error. Speech input that was explicitly switched off becomes asr: null, and narration, images or video switched off while a usable provider for them was set up become tts: null, image: null or video: null (a slot openmaic.yml locks is left as it is). These have to be set up again by hand: per-stage models (in Course Model Config, or slots in openmaic.yml), thinking settings (thinking on a slot), custom speech and transcription providers and AliDocMind's key pair (in openmaic.yml; they are kept in the browser and listed with the skipped items), the VoxCPM backend (options.backend), and Baidu search sub-sources (the server's defaults apply). Research switched off is not carried over (it only stopped course research; chat and the agent kept searching), nor is a switch that was off only by default: each capability now runs whenever its slot resolves.

A workspace is an owner in the sense of the persistence documentation: with the default Docker Compose setup (single-user mode) there is one workspace, and in a multi-user deployment each signed-in account has its own.

allowUserKeys

allowUserKeys: false

allowUserKeys (default true) decides whether users may add providers, keys and token plans of their own. With false, users only choose among the providers openmaic.yml declares, for the slots it does not lock: Token Plan and adding services are not shown, and providers a workspace added earlier are no longer used. Requests that still name their own model, key or endpoint (the deprecated x-model, x-api-key and x-base-url headers, and the provider fields of the media routes) are ignored then, and the header form of the provider test buttons is refused. A multi-user deployment that pays for all usage typically sets it and locks the slots it wants fixed.

Secrets and OPENMAIC_SECRET_KEY

  • Keys in openmaic.yml come from the environment through ${VAR}. They are never stored in the database and never sent to the browser.
  • Keys a workspace enters in the model settings are stored in the database, encrypted with AES-256-GCM under the instance secret OPENMAIC_SECRET_KEY.
  • When OPENMAIC_SECRET_KEY is unset, the server creates a random secret on first use in data/instance-secret.key (inside the openmaic-data volume with Docker Compose) and uses it from then on.

Keep the secret with the database backups: keys saved under a lost or changed secret cannot be read and have to be entered again (the model settings mark them). Set OPENMAIC_SECRET_KEY explicitly when more than one instance shares the database, or when the server has no persistent data directory (a container without the data volume); any string works, for example the output of openssl rand -base64 32.

Without it, a data directory that does not survive a restart (a container without a volume, an ephemeral disk) means a new secret on every start, and several replicas each generate their own; on a read-only working directory no secret can be created and saving a key in the model settings fails. At startup the server warns when OPENMAIC_SECRET_KEY is unset and the data directory is not writable, and when the database holds keys sealed under a different secret than the current one (for example because data/instance-secret.key was lost); it starts anyway.

OPENMAIC_SECRET_KEY=...        # instance secret for keys saved in the model settings
OPENMAIC_CONFIG=/etc/openmaic/openmaic.yml   # optional: another path for openmaic.yml

What workspaces may add

Providers from openmaic.yml can be anything the schema accepts. Providers a workspace adds in the model settings are more restricted, because their endpoint and key are typed by a user of the web app:

  • Amazon Bedrock is not offered: it would sign with the server's own AWS credentials.
  • No proxy, and no key pairs (credentials): presets that authenticate with a key pair, such as AliDocMind, are deployment-only.
  • A custom baseUrl is allowed only for presets that offer chat, and a provider with one serves chat slots only. The URL must not contain credentials, a query string or a fragment, and must resolve to a public address unless the server sets ALLOW_LOCAL_NETWORKS=true. Requests to it do not follow redirects.
  • Local model servers (Ollama, Lemonade) must name their own endpoint rather than use the preset's localhost default.
  • Self-hosted media, search and document services (VoxCPM2, FunASR, Lemonade speech and images, ComfyUI, SearXNG, self-hosted MinerU) run on the server's own network and are deployment-only.
  • Providers the operator switched off with a legacy <CAP>_<VENDOR>_ENABLED=false variable are not offered.
  • A workspace cannot declare a provider id that openmaic.yml already declares.

A deployment that wants users to reach internal services declares those providers in openmaic.yml.

API clients and deprecated request fields

Generation requests no longer need to carry keys or model choices: browserless API calls such as /api/generate-classroom resolve through the same slots as the web app, for the workspace of the caller.

The older request headers x-model, x-api-key, x-base-url, x-provider-type, x-model-routes, x-image-provider, x-image-model, x-video-provider and x-video-model, and the equivalent body fields of the speech, search and document routes, are deprecated. They are honoured only while the slot they would affect has nothing assigned or, on a server still configured through the legacy variables, only a default those translate to (as DEFAULT_MODEL always ranked below the model a request named); a default in openmaic.yml, the workspace's own choice and a locked slot win, and a slot set to null refuses the request whatever it names. With allowUserKeys: false they are ignored altogether. The server logs a warning the first time a request uses them. They will be removed two minor versions after the model settings described above ship.

Migrating from the legacy configuration

When openmaic.yml is absent, the server translates the legacy configuration at startup and logs a deprecation notice, plus a notice for each entry that could not carry over:

Legacy settingBecomes
Provider variables (OPENAI_API_KEY, TTS_MINIMAX_API_KEY, …) and server-providers.ymlDeployment providers (read-only in the model settings). The provider id is the preset id, which is the legacy provider id except for web search MiniMax (minimax-search), web search Doubao (doubao-search) and Lemonade images (lemonade-image)
<PREFIX>_MODELSThe provider's models
DEFAULT_MODELThe llm assignment, as a default a workspace can override (not locked). The agent slot stays off, as before
MODEL_FALLBACKThe fallback of DEFAULT_MODEL. Without DEFAULT_MODEL it is dropped with a notice
The first configured TTS, ASR, image, video and document provider, and web search in the old priority orderThe default assignment of that capability (DEFAULT_IMAGE_PROVIDER picks the image provider)
MODEL_ROUTESNot translated. The server refuses to start (see below)
<CAP>_<VENDOR>_ENABLED=falseStill honoured: the provider stays off whatever assigns it

When openmaic.yml exists, the provider variables, server-providers.yml, DEFAULT_MODEL, MODEL_FALLBACK and MODEL_ROUTES are ignored for model resolution, with a notice. <CAP>_<VENDOR>_ENABLED=false still applies; to turn a capability off, set its slot to null instead.

Environment variables. Before:

OPENAI_API_KEY=sk-...
ANTHROPIC_API_KEY=sk-ant-...
DEFAULT_MODEL=openai:gpt-5.5
MODEL_FALLBACK=anthropic:claude-sonnet-4-6
TTS_MINIMAX_API_KEY=...
TAVILY_API_KEY=tvly-...

After (the keys stay in the environment):

openmaic.yml
providers:
  openai:
    preset: openai
    apiKey: ${OPENAI_API_KEY}
  anthropic:
    preset: anthropic
    apiKey: ${ANTHROPIC_API_KEY}
  minimax-tts:
    preset: minimax-tts
    apiKey: ${TTS_MINIMAX_API_KEY}
  tavily:
    preset: tavily
    apiKey: ${TAVILY_API_KEY}

slots:
  llm:
    model: openai:gpt-5.5
    fallback: anthropic:claude-sonnet-4-6
  tts: minimax-tts
  webSearch: tavily

Remove DEFAULT_MODEL and MODEL_FALLBACK afterwards. The provider variables can stay as the source of the ${VAR} references. A <PREFIX>_BASE_URL becomes the provider's baseUrl (OPENAI_BASE_URL → baseUrl under openai; write the URL, or ${OPENAI_BASE_URL} while the variable stays set), and <PREFIX>_MODELS becomes its models list. As with the translated DEFAULT_MODEL, the llm written here is a server default workspaces may change; add lock: [llm] to fix it for everyone. Unlike the translation, agent now follows llm: write agent: null to keep the Pro agent off.

server-providers.yml. Before:

server-providers.yml
providers:
  azure:
    apiKey: ...
    baseUrl: https://YOUR-RESOURCE.openai.azure.com/openai
    models: [my-deployment]
pdf:
  alidocmind:
    accessKeyId: ...
    accessKeySecret: ...
web-search:
  searxng:
    baseUrl: http://searxng:8080

After:

openmaic.yml
providers:
  azure:
    preset: azure
    apiKey: ${AZURE_OPENAI_API_KEY}
    baseUrl: https://YOUR-RESOURCE.openai.azure.com/openai
    models: [my-deployment]
  alidocmind:
    preset: alidocmind
    credentials:
      accessKeyId: ${ALIDOCMIND_ACCESS_KEY_ID}
      accessKeySecret: ${ALIDOCMIND_ACCESS_KEY_SECRET}
  searxng:
    preset: searxng
    baseUrl: http://searxng:8080

slots:
  llm: azure:my-deployment
  document: alidocmind
  webSearch: searxng

Each section of server-providers.yml becomes providers with the matching preset (providers → chat presets, tts, asr, pdf → document, image, video, web-search), and accessKeyId/accessKeySecret move under credentials.

MODEL_ROUTES. A server that sets MODEL_ROUTES without openmaic.yml does not start:

MODEL_ROUTES does not carry over to the model configuration: write the per-stage models as slots in openmaic.yml (or the file named by OPENMAIC_CONFIG) and remove MODEL_ROUTES

Per-stage routes do not map one to one onto slots, so they are not translated. Rewrite each route as the slot its stage belongs to:

MODEL_ROUTES stageSlot
web-search-query-rewritecourse.research
scene-outlines-streamcourse.outline
agent-profilescourse.agents
scene-contentcourse.content
scene-content:slide, :quiz, :interactive, :pblcourse.content.slide, .quiz, .interactive, .pbl
scene-actionscourse.actions
chat-adapter, quiz-grade, pbl-v2-runtime and its :instructor, :open-task, :evaluate, :simulator variantsclassroom
generate-classroomllm
maic-agent-driveragent
conversation-titleagent.title
pbl-chat, maic-agentNone: these stages were removed. Drop them

Several stages share a slot: give the slot the one model they should use. A route's thinking moves to the slot's thinking, and a maic-agent-driver route's api and contextWindow move to the agent slot. Before:

DEFAULT_MODEL=openai:gpt-5.5
MODEL_ROUTES='{"scene-outlines-stream":"anthropic:claude-sonnet-4-6","scene-content:slide":{"model":"openai:gpt-5.4","thinking":{"effort":"low"}},"maic-agent-driver":{"model":"openai:gpt-5.5","api":"openai-completions"}}'

After:

openmaic.yml
providers:
  openai:
    preset: openai
    apiKey: ${OPENAI_API_KEY}
  anthropic:
    preset: anthropic
    apiKey: ${ANTHROPIC_API_KEY}

slots:
  llm: openai:gpt-5.5
  course.outline: anthropic:claude-sonnet-4-6
  course.content.slide:
    model: openai:gpt-5.4
    thinking: { effort: low }
  agent:
    model: openai:gpt-5.5
    api: openai-completions

Once openmaic.yml exists, the provider variables configure nothing on their own: a TTS, image, document or web search provider that had only its variables (TTS_MINIMAX_API_KEY, IMAGE_SEEDREAM_API_KEY, PDF_MINERU_BASE_URL, TAVILY_API_KEY, …) is no longer used, and other capabilities get no default provider. Declare each provider you use under providers and assign tts, image, document, webSearch (and asr, video) in slots, as in the environment-variable example above; a slot for another capability left unassigned stays unconfigured unless a workspace assigns it in the model settings.

Then remove MODEL_ROUTES, DEFAULT_MODEL and MODEL_FALLBACK.

Docker. Mount the file next to the app and keep the keys in .env.local:

services:
  openmaic:
    volumes:
      - ./openmaic.yml:/app/openmaic.yml:ro

ACCESS_CODE — site-wide password

For shared deployments such as internal demos or classrooms, set an access code so visitors must enter a password before they can see the app:

ACCESS_CODE=your-secret-code

Use a long random value (at least 16 characters) — this code is the only secret guarding the deployment. The code is remembered in a signed token stored in an HTTP-only cookie for 7 days; the lifetime is enforced server-side, so visitors re-verify after it expires. Verification is rate limited only when TRUST_PROXY_HEADERS=true is set behind a trusted proxy that overwrites x-forwarded-for / x-real-ip: each client gets 10 attempts per 60 seconds, and a successful verification clears that client's counter. Without a trusted proxy the app cannot attribute a request to a client, so there is no throttle; rely on the length and randomness of the code. Leave it empty to disable access control.

Feature flags

Feature flags accept true or 1; unless noted otherwise, all other values are treated as disabled. NEXT_PUBLIC_* flags are injected into the client at build time, so changing them requires a rebuild:

# MAIC Editor (Pro mode)
NEXT_PUBLIC_MAIC_EDITOR_ENABLED=true

# PPT and Interactive courseware references in Pi playback (build-time; editor references are separate)
NEXT_PUBLIC_COURSEWARE_REFERENCE_ENABLED=true

# Vocational education task engine (server-side flag)
OPENMAIC_ENABLE_VOCATIONAL=true

# Show the vocational education experimental switch
NEXT_PUBLIC_SHOW_VOCATIONAL_TEST_UI=true

# Show the video export entry point
NEXT_PUBLIC_ENABLE_VIDEO_EXPORT=true

# Show the experimental PPTX import entry point
NEXT_PUBLIC_ENABLE_PPTX_IMPORT=true

Classroom chat always runs on Pi, which requires a model with tool (function) calling: select one for Classroom in Course Model Config, or set the classroom slot (or the llm slot it inherits from) in openmaic.yml. For Gemini, use the native google preset rather than Google's OpenAI-compatible endpoint. It refuses a model that the built-in catalogue lists without tool calling before inference, with 400 MODEL_CONFIG_INVALID (reason: tool_calling_required), and Course Model Config notes the requirement on the classroom slot.

The old director-graph classroom chat has been removed, and NEXT_PUBLIC_PI_CHAT_ENABLED no longer selects a chat path. Remove it from your configuration: a production build accepts only unset, an empty string, true or 1 (exact match) and fails with migration guidance on any other value. With Docker Compose, the service env_file (.env.local) supplies runtime values, not build args.

PPTX import is still experimental, and its parsed output is not yet fully integrated into the classroom data flow. All NEXT_PUBLIC_* variables above are build-time flags. Docker deployments must also pass them as build args; setting them only as container runtime environment variables is not sufficient.

Other server options

The following server options can also be configured with environment variables:

# Parallel scene-content generation; 0 or unset means sequential
PARALLEL_SCENE_CONCURRENCY=3

# Allow localhost and private-network addresses; self-hosted/private deployments only
ALLOW_LOCAL_NETWORKS=true

# Optional MP4 rendering service
RENDER_SERVICE_URL=http://render-service:9000

# Logging and reasoning
LOG_LEVEL=info
LOG_FORMAT=pretty
LLM_THINKING_DISABLED=false

Legacy provider environment variables

These variables and server-providers.yml are the configuration of earlier releases. They still work when there is no openmaic.yml (see Migrating from the legacy configuration) and are deprecated: they will be removed two minor versions after the model settings ship. With openmaic.yml, use them only as the source of ${VAR} references. DEFAULT_MODEL and MODEL_FALLBACK are covered in the migration section; MODEL_ROUTES is no longer read.

LLM providers

Cloud providers generally use three environment variables: an API key, a base URL, and a model list. The API key is usually required; the base URL and model list are optional. Azure OpenAI requires a resource endpoint, while Ollama and Lemonade do not require an API key.

OPENAI_API_KEY=sk-...
OPENAI_BASE_URL=                # optional base URL override
OPENAI_MODELS=                  # optional model allowlist (comma-separated)

Supported provider prefixes:

PrefixProvider
OPENAI_OpenAI
AZURE_OPENAI_Azure OpenAI
ANTHROPIC_Anthropic
GOOGLE_Google Gemini
DEEPSEEK_DeepSeek
QWEN_Alibaba Qwen
KIMI_Moonshot Kimi
MINIMAX_MiniMax (Anthropic-compatible endpoint by default)
GLM_Zhipu GLM
SILICONFLOW_SiliconFlow
DOUBAO_Doubao (ByteDance)
OPENROUTER_OpenRouter
GROK_xAI Grok
TENCENT_Tencent Hunyuan
TENCENT_HUNYUAN_Tencent Hunyuan (alias)
XIAOMI_Xiaomi MiMo
MIMO_Xiaomi MiMo (alias)
OLLAMA_Ollama (local)
LEMONADE_Lemonade (local)

Azure OpenAI uses the deployment name as the model ID:

AZURE_OPENAI_API_KEY=...
AZURE_OPENAI_BASE_URL=https://YOUR-RESOURCE.openai.azure.com/openai
AZURE_OPENAI_MODELS=your-deployment-name

To connect another OpenAI-compatible LLM service, declare an openai-compatible provider with its baseUrl and models in openmaic.yml, or add one with Add provider under Settings → Model Services → LLM.

For Gemini classroom interaction, use the native google preset: add Google in Settings → Model Services and select it for Classroom in Course Model Config. Pi rejects Google’s official OpenAI-compatible endpoint (https://generativelanguage.googleapis.com/v1beta/openai) with migration guidance; compatible tool-calling support is tracked in #1556. This check recognizes only that official endpoint and cannot identify the upstream behind arbitrary third-party gateways.

Local models (Ollama and Lemonade)

Ollama and Lemonade do not require an API key. Put the local service base URL in the server configuration so it passes SSRF validation:

OLLAMA_BASE_URL=http://localhost:11434/v1
# LEMONADE_BASE_URL=http://localhost:13305/v1

To limit available models, use OLLAMA_MODELS or LEMONADE_MODELS as a model allowlist.

TTS providers

Server-side TTS providers use TTS_<PROVIDER>_API_KEY and optionally TTS_<PROVIDER>_BASE_URL.

# Doubao TTS (Volcengine Seed-TTS, native MP3)
TTS_DOUBAO_API_KEY=appId:accessKey
TTS_DOUBAO_BASE_URL=       # optional override

# Qwen TTS
TTS_QWEN_API_KEY=
TTS_QWEN_BASE_URL=         # optional override

# Any OpenAI-compatible TTS endpoint
TTS_OPENAI_API_KEY=
TTS_OPENAI_BASE_URL=

# VoxCPM2 (self-hosted TTS with voice cloning; see the dedicated VoxCPM2 page)
TTS_VOXCPM_BASE_URL=http://localhost:8000

Supported TTS prefixes include TTS_OPENAI_, TTS_AZURE_, TTS_GLM_, TTS_QWEN_, TTS_MINIMAX_, TTS_DOUBAO_, TTS_ELEVENLABS_, TTS_GOOGLE_, TTS_VOXCPM_, and TTS_LEMONADE_. Local Lemonade TTS and VoxCPM2 do not require an API key. Browser-native TTS requires no server configuration.

Administrators can force-disable a TTS provider on the server with TTS_<PROVIDER>_ENABLED=false. See the dedicated VoxCPM2 section for self-hosted TTS and voice cloning.

To use another OpenAI-compatible TTS endpoint, declare an openai-tts provider with that baseUrl in openmaic.yml and assign the tts slot to it. Workspaces cannot add custom TTS endpoints in the model settings.

ASR (speech to text)

# OpenAI Whisper
ASR_OPENAI_API_KEY=
ASR_OPENAI_BASE_URL=       # optional override

# Qwen ASR
ASR_QWEN_API_KEY=
ASR_QWEN_BASE_URL=         # optional override

# Azure ASR
ASR_AZURE_API_KEY=
ASR_AZURE_BASE_URL=https://{region}.api.cognitive.microsoft.com

# FunASR (local, no key required)
ASR_FUNASR_BASE_URL=http://localhost:8000/v1

# Lemonade ASR (local, no key required)
ASR_LEMONADE_BASE_URL=http://localhost:13305/v1

Start the OpenAI-compatible FunASR service with funasr-server --device cuda. It accepts WAV uploads at /v1/audio/transcriptions and provides SenseVoiceSmall, Paraformer, and Fun-ASR-Nano through the model selector.

Browser-native ASR requires no server configuration.

To use another OpenAI-compatible ASR endpoint, declare it in openmaic.yml with the openai-whisper preset and its baseUrl, and assign the asr slot to it. Workspaces cannot add custom ASR endpoints in the model settings:

openmaic.yml
providers:
  my-asr:
    preset: openai-whisper
    apiKey: ${MY_ASR_API_KEY}
    baseUrl: https://asr.example.com/v1

slots:
  asr: my-asr:whisper-1

Image generation providers

Image generation providers use IMAGE_<PROVIDER>_API_KEY and optionally IMAGE_<PROVIDER>_BASE_URL. Supported prefixes include:

IMAGE_OPENAI_, IMAGE_SEEDREAM_, IMAGE_QWEN_IMAGE_, IMAGE_NANO_BANANA_, IMAGE_MINIMAX_, IMAGE_GROK_, IMAGE_GLM_, and IMAGE_LEMONADE_.

GLM Image (Zhipu CogView) generates images with CogView models (cogview-3-flash, cogview-4):

openmaic.yml
providers:
  glm-image-service:
    preset: glm-image
    apiKey: ${IMAGE_GLM_API_KEY}
    # Optional:
    baseUrl: https://open.bigmodel.cn/api/paas/v4

slots:
  image: glm-image-service:cogview-3-flash

Or configure via environment variables:

IMAGE_GLM_API_KEY=
# Optional:
IMAGE_GLM_BASE_URL=https://open.bigmodel.cn/api/paas/v4

Lemonade is a local service and does not require a key:

IMAGE_LEMONADE_BASE_URL=http://localhost:13305/v1

ComfyUI Image does not require an API key. It runs on your own network, so it is configured by the deployment: declare it in openmaic.yml with the address of your ComfyUI server and assign the image slot to it.

openmaic.yml
providers:
  comfyui:
    preset: comfyui-image
    baseUrl: http://host.docker.internal:8188

slots:
  image: comfyui

Place workflow JSON exported in API format in OpenMAIC's public/ directory. Use filenames matching comfyui-*.json or containing workflow; OpenMAIC discovers these files automatically and offers them as workflows. With Docker, add the workflow before building the image or mount an individual workflow file into /app/public/ in the container. Because the provider is declared by the deployment, a ComfyUI server on the host or a private network does not need ALLOW_LOCAL_NETWORKS.

Video generation providers

Video generation providers use VIDEO_<PROVIDER>_API_KEY and optionally VIDEO_<PROVIDER>_BASE_URL. Supported prefixes include:

VIDEO_SEEDANCE_, VIDEO_KLING_, VIDEO_VEO_, VIDEO_SORA_, VIDEO_MINIMAX_, VIDEO_GROK_, and VIDEO_HAPPYHORSE_.

Document and media parsing

The exact course-material formats depend on the selected parser. OpenMAIC currently supports text, PDF, Office documents, images, and some audio/video formats; supported formats and capabilities vary by provider.

# MinerU self-hosted
PDF_MINERU_BASE_URL=http://localhost:8888

# Optional backend for self-hosted MinerU
PDF_MINERU_BACKEND=pipeline

# MinerU Cloud
PDF_MINERU_CLOUD_API_KEY=
PDF_MINERU_CLOUD_BASE_URL=https://mineru.net/api/v4

# AliDocMind (uses Alibaba Cloud AccessKey, not a separate API key)
ALIDOCMIND_ACCESS_KEY_ID=
ALIDOCMIND_ACCESS_KEY_SECRET=
ALIDOCMIND_BASE_URL=       # optional override

unpdf is built into OpenMAIC and supports basic PDF parsing. For Office documents, images, and material requiring OCR, table, formula, or layout analysis, select a compatible MinerU or AliDocMind provider.

AliDocMind document parsing supports PDF, DOCX, PPTX, XLSX, and PNG, JPG/JPEG, BMP, and GIF. Audio and video material is currently supported only through AliDocMind: MP4, MOV, AVI, MKV, and WMV for video; MP3, WAV, and AAC for audio. M4A is not supported.

Configure Tavily, Exa, Firecrawl, Bocha, Brave, Baidu, SearXNG, or MiniMax:

TAVILY_API_KEY=
TAVILY_BASE_URL=           # optional override

EXA_API_KEY=
EXA_BASE_URL=https://api.exa.ai  # optional override

FIRECRAWL_API_KEY=
FIRECRAWL_BASE_URL=https://api.firecrawl.dev  # optional override

BOCHA_API_KEY=
BOCHA_BASE_URL=            # optional override

BAIDU_API_KEY=
BAIDU_BASE_URL=https://qianfan.baidubce.com  # optional override

# Self-hosted SearXNG, no API key required
SEARXNG_BASE_URL=

WEB_SEARCH_MINIMAX_API_KEY=
WEB_SEARCH_MINIMAX_BASE_URL=https://api.minimaxi.com  # optional override

Brave and SearXNG do not require an API key. Web search has no per-generation switch: it runs whenever the workspace's webSearch slot resolves, and turning it off (webSearch: null in openmaic.yml, or with the search switch in Settings → Course Model Config) affects later generations of that workspace. Grok web search is provided through the Grok LLM search tool, not a standalone web-search provider.

server-providers.yml

Deprecated in favour of openmaic.yml. When both exist, only openmaic.yml configures models.

In addition to environment variables, place configuration for providers registered in the code in server-providers.yml at the project root. The server loads it at startup, and its structure follows the environment variable categories:

server-providers.yml
providers:
  openai:
    apiKey: sk-...
    baseUrl: https://api.openai.com/v1
    models:
      - gpt-5.5
  anthropic:
    apiKey: sk-ant-...

tts:
  doubao-tts:
    apiKey: appId:accessKey

asr:
  openai-whisper:
    apiKey: sk-...

pdf:
  mineru:
    baseUrl: http://localhost:8888
  alidocmind:
    accessKeyId: your-access-key-id
    accessKeySecret: your-access-key-secret

image:
  seedream:
    apiKey: ...

video:
  seedance:
    apiKey: ...

web-search:
  tavily:
    apiKey: tvly-...
  exa:
    apiKey: ...
  firecrawl:
    apiKey: fc-...
  searxng:
    baseUrl: http://localhost:8080

Environment variables override matching YAML provider fields one by one. Keys must use provider IDs registered in the code, such as openai, doubao-tts, openai-whisper, mineru, alidocmind, seedream, seedance, exa, tavily, or searxng. Unknown IDs cannot register custom LLM providers here.

Model authorization for server-managed providers

For providers that use deployment credentials to send a selected model upstream, set an explicit models list in openmaic.yml (providers.<id>.models), or the corresponding *_MODELS environment variable / server-providers.yml section's models list. This applies to chat, image, video, text to speech, speech recognition and model-based web search. Model ids match exactly, including case, after surrounding whitespace is removed at configuration load. Deterministic adapter aliases use the declared model id for authorization. Requests for unlisted models return HTTP 403 with PROVIDER_DISABLED and the message “This model is not authorized for server-managed credentials”.

Breaking change: without an explicit list, only deployment default/fallback models and the primary/fallback models referenced by slots for that capability are usable. DEFAULT_MODEL and MODEL_FALLBACK grant chat models on legacy deployments. A provider-only media slot grants its single adapter default model; it does not grant the preset's catalogue. Workspace assignments cannot expand the deployment's authorized set. Voice registration and synthesis must also authorize any model selected for voice compatibility.

To migrate, list every model users should be able to select, restart the server, and update any workspace selections that are no longer authorized. Startup logs name the setting to add for each provider that has credentials but no list. Users' own provider credentials and keyless local providers such as Ollama and Lemonade are unaffected. A provider configured without credentials is not restricted, so do not point one at a gateway that adds its own upstream credential (for example an internal OpenAI-compatible proxy): give that gateway a key and configure it as a credentialed provider with a models list, or restrict models in the gateway itself. Document parsers and search services without model selection (such as Tavily) need no model list.

providers:
  openai:
    preset: openai
    apiKey: ${OPENAI_API_KEY}
    models: [gpt-5.4-mini]
slots:
  llm: openai:gpt-5.4-mini

Legacy equivalent: OPENAI_API_KEY=... and OPENAI_MODELS=gpt-5.4-mini. For media, use the capability prefix, for example IMAGE_SEEDREAM_MODELS, VIDEO_SEEDANCE_MODELS, TTS_OPENAI_MODELS, ASR_OPENAI_MODELS and WEB_SEARCH_CLAUDE_MODELS.

DeepSeek web search and fetch

The deepseek preset shares DEEPSEEK_API_KEY and the API base across chat, web search and hosted Markdown extraction. Search uses POST /v1/tools/search, returns up to 50 results, truncates queries to 128 Unicode code points, and uses summary with snippet as a fallback. Fetch uses POST /v1/tools/fetch and reads content and an optional nonempty title.

In legacy configuration, opt in with WEB_SEARCH_DEEPSEEK_ENABLED=true or an explicit web-search.deepseek entry in server-providers.yml. Chat credentials alone never enable search or replace another search default. WEB_SEARCH_DEEPSEEK_ENABLED=false force-disables search. When openmaic.yml exists, the legacy variable cannot assign a slot: explicitly declare the provider and assign webSearch and/or webFetch.

The default API base is https://api.deepseek.com/v1. On api.deepseek.com, every base path (including /beta) is normalized to /v1 before appending /tools/search or /tools/fetch. Custom gateway bases preserve their configured path prefix; a bare root gets /v1 first. Custom endpoints belong in deployment configuration, while client and workspace overrides remain restricted to official endpoints. The default fetch reader remains builtin. See DeepSeek official pricing for current request charges. Failed requests and searches with no results are not billed.

providers:
  deepseek:
    preset: deepseek
    apiKey: ${DEEPSEEK_API_KEY}
    baseUrl: https://api.deepseek.com/v1
slots:
  webSearch: deepseek
  webFetch: deepseek

Declare one Kimi account and assign both slots to it:

providers:
  kimi:
    preset: kimi
    apiKey: ${KIMI_API_KEY}
    # Optional: https://api.moonshot.ai/v1 for the international Open Platform.
    # baseUrl: https://api.moonshot.ai/v1
slots:
  llm: kimi:kimi-k3
  webSearch: kimi

For a Coding Plan key, use preset: kimi-coding-plan and llm: kimi:kimi-for-coding; its default base is https://api.kimi.com/coding/v1. The Open Platform and Coding Plan keys must be paired with their matching base URL. In legacy configuration, KIMI_API_KEY and optional KIMI_BASE_URL supply search credentials only after explicit opt-in: set WEB_SEARCH_KIMI_ENABLED=true or add web-search.kimi in server-providers.yml (for example, with enabled: true). Chat credentials alone do not enable search or change the default search provider. WEB_SEARCH_KIMI_ENABLED=false disables Kimi search even when a slot assigns it. WEB_SEARCH_KIMI_ENABLED is the only Kimi search switch. KIMI_MODELS applies only to chat. Conflicting legacy chat and search accounts produce a warning and the search entry is skipped; declare separate provider ids in openmaic.yml when separate accounts are needed. When openmaic.yml exists, declare the provider and assign the slots explicitly. Settings and the first-run wizard offer both capabilities through the same account.

Endpoint selection follows the base path: after removing trailing slashes, a path ending in /coding/v1 uses /search; otherwise it uses /tools/search_pro. Custom proxies configured by the deployment must preserve that path layout. The wizard recommends search only for official account bases, so a custom workspace chat proxy does not automatically acquire a search assignment.

When openmaic.yml is present, declare providers.<id>: { preset: kimi } and assign slots.webSearch: <id>; the legacy WEB_SEARCH_KIMI_ENABLED variable cannot assign that slot.

Web fetch provider slot

webFetch selects the reader used by the agent's fetch_url tool. It defaults to builtin in legacy, empty and YAML configurations, including an omitted root under lock: all. Explicit webFetch: null disables fetching and removes the tool and its prompt from subsequent runs. Each request uses one provider, with no fallback or escalation. An invalid provider reference or a workspace endpoint rejected by policy fails the entire agent run during capability resolution, before the agent starts, as with webSearch. This is a configuration error, not a failed fetch_url result; it does not switch to the builtin reader.

ProviderCredentialsExtraction
builtinNoneLocal Readability/Turndown reader; PDFs use the document slot
kimiExisting Kimi provider key and base URLHosted Markdown extraction, including PDF URLs
deepseekExisting DeepSeek chat key and API baseHosted Markdown extraction
firecrawlFirecrawl API key (preset: firecrawl)Hosted Markdown extraction of the page's main content, including PDF URLs
providers:
  reader:
    preset: kimi
    apiKey: ${KIMI_API_KEY}
    baseUrl: https://api.moonshot.cn/v1
slots:
  webFetch: reader

Both kimi and kimi-coding-plan presets offer webFetch. For Coding Plan credentials, use preset: kimi-coding-plan; its default base is https://api.kimi.com/coding/v1. Any explicitly configured base URL is preserved. Moonshot Open Platform bases (https://api.moonshot.cn/v1 and https://api.moonshot.ai/v1) use /tools/fetch; Coding Plan uses /fetch. Both send POST with { "url": ... } and Bearer authentication. The adapter accepts markdown or content. A maintainer live-tested POST https://api.kimi.com/coding/v1/fetch with a real key and confirmed { markdown, title, url }; international endpoint availability remains unverified. Requests respect the provider's proxy setting and standard proxy environment variables.

DeepSeek also offers hosted Markdown extraction through the deepseek preset, reusing the chat key and API base. Assign webFetch explicitly as shown above. It uses /v1/tools/fetch; an explicit deployment gateway base is preserved. It retains the same transport deadlines and single transport-error retry as Kimi, without retrying HTTP errors or invalid responses.

In Settings → Model Services → Web Fetch, use the existing provider credentials. The key is shared with that provider's chat service: editing or removing it also affects chat. Choose the reader in Course Model Config → Web Fetch. Administrator locks make the card read-only. First-run setup keeps the builtin default until a reader is explicitly selected. Workspace credentials can use official API bases; arbitrary fetch endpoints and proxies must be declared by the administrator in openmaic.yml.

Unlike kimi, firecrawl has its own key: declare a provider with preset: firecrawl and apiKey (for example ${FIRECRAWL_API_KEY}), or enter the key in Settings → Model Services → Web Fetch. It sends POST /v2/scrape with Bearer authentication and asks for the page's main content as Markdown. The default base is https://api.firecrawl.dev; a self-hosted Firecrawl base must be declared by the administrator in openmaic.yml.

The same firecrawl provider also serves web search (POST /v2/search): one key and base URL cover both, so a single provider can be assigned to slots.webSearch and slots.webFetch. The legacy FIRECRAWL_API_KEY and FIRECRAWL_BASE_URL variables configure that provider; web fetch stays on builtin until webFetch is assigned to it.

The session URL gate, strict URL normalization and all local DNS address checks run before forwarding a URL to a hosted reader. The same checks run against a reported final URL before accepting its content. Hosted readers control their own DNS and intermediate redirects; local checks cannot pin their networks. Their text remains untrusted, with challenge-page detection, byte limits, a truncation notice and the existing web material persistence and {trusted, untrusted} result.

On this page