Skip to content

Course Builder settings

The admin link Courses → Build with AI (/courses/builder) opens the studio in the web app; the studio itself is described in the author guide.

The builder calls Claude through the official Anthropic SDK. Set in the API environment (api/.env, or the tenant’s .env.<host>):

Variable Default Meaning
AI_DRIVER anthropic anthropic, fake (no network: recorded or synthetic answers, for demos and tests) or disabled
ANTHROPIC_API_KEY – API key. The legacy spelling ANTROPHIC_API_KEY is accepted second. Without a key the driver is disabled
AI_MODEL_DEFAULT / AI_MODEL_LIGHT / AI_MODEL_PREMIUM claude-sonnet-5-5 / claude-haiku-5-5 / claude-opus-5-5 Models of the three profiles. The premium profile is only used when a task is switched to it
AI_TASK_<TASK>_PROFILE see below Switch one task to another profile, e.g. AI_TASK_OUTLINE_PROFILE=premium
AI_CACHE_TTL 1h Prompt cache TTL for the source (5m or 1h)
AI_TIMEOUT / AI_MAX_RETRIES 600 / 2 Seconds per model request and retries of transient HTTP errors
ANTHROPIC_BASE_URL – Point the SDK at a proxy or gateway
AI_FAKE_MODE synthetic With AI_DRIVER=fake: cassette (replay only) or synthetic (replay, else a built-in deterministic answer)

When AI is disabled, every builder endpoint answers 503 with a message and the studio says so; the rest of the LMS works as before.

Tasks and their default profiles: interview (light), outline (default), lesson (default), quiz (default), grounding (light), metadata (light), price (light), patch (default) and, for Living Course, update (default).

The web app needs nothing new. The admin link derives the studio address from the admin host (coffee.admin.example → coffee.app.example); set REACT_APP_STUDIO_URL in the admin’s runtime environment when your hosts differ.

The permission course_builder_use is given to the admin and tutor roles. On an existing tenant, run the permissions seeder once after deploying:

Terminal window
php artisan db:seed --class=PermissionsSeeder --force --domain=<tenant host>

Sessions are visible to their author and to admins; only the author can act on a session.

Variable Default
COURSE_BUILDER_SOURCE_MB 20 Upload size
COURSE_BUILDER_PDF_PAGES 300 PDF pages
COURSE_BUILDER_SOURCE_TOKENS 400000 Source tokens per session
COURSE_BUILDER_SESSION_TOKENS 3000000 Tokens per session (cache reads count 10 %)
COURSE_BUILDER_SESSION_COST_USD 5 AI cost per session
COURSE_BUILDER_CONCURRENT_RUNS 2 AI runs at once per author
COURSE_BUILDER_SESSIONS_PER_DAY 10 New sessions per author per day
COURSE_BUILDER_LESSON_CONCURRENCY 4 Lessons generated at once per run
AI_LIMIT_TENANT_MONTHLY_USD 50 AI spend per tenant per month (all tasks); 0 turns the cap off
COURSE_BUILDER_EVAL_MONTHLY_USD 20 Spend cap of the eval command
COURSE_BUILDER_DOCX_UNCOMPRESSED_MB 100 Largest unpacked size of a DOCX (zip-bomb guard)

LiaScript, H5P and interactive lessons are offered in the outline only when the installation can create them. Each can be turned off for the whole installation:

Variable Default
COURSE_BUILDER_LIASCRIPT true LiaScript lessons with self-checks
COURSE_BUILDER_H5P true Lessons with an H5P activity (needs the H5P service and the libraries Blanks, Drag the Words and Dialog Cards)
COURSE_BUILDER_INTERACTIVE true Lessons with an interactive from the library (needs at least one package, and the interactive topic type switched on)
COURSE_BUILDER_AUTO_FORMATS false The outline gives every lesson with two or more objectives the LiaScript format; authors can still change it

The libraries the builder may use are listed in config/course_builder.php (h5p_libraries). Install them in the H5P service (H5P libraries). The extra generation tasks (selfcheck, interaction_h5p, interaction_interactive) use the same models as the other tasks and can be given another profile with AI_TASK_SELFCHECK_PROFILE, AI_TASK_INTERACTION_H5P_PROFILE and AI_TASK_INTERACTION_INTERACTIVE_PROFILE.

Before every model call the budget is checked against the session tokens, the session cost and the tenant’s month. The first that would be exceeded stops the call: the run fails with RUN_ERROR (code: "budget") and a message such as “Budget reached: this course has used $5.00 of its $5.00 AI budget. Ask an admin to raise the limit to continue.”, the studio shows the message, and the session is marked budgetReached. Nothing already generated is lost; the author retries after you raise the limit. AI_LIMIT_SUBJECT_TOKENS and AI_LIMIT_SUBJECT_COST_USD (3,000,000 and 5) are the defaults for AI calls that carry no budget of their own; builder sessions always carry one. A run that would start another AI run beyond COURSE_BUILDER_CONCURRENT_RUNS is refused with 429, a new session beyond COURSE_BUILDER_SESSIONS_PER_DAY too. The API page has the exact messages.

Every model call is logged in the ai_calls table of the tenant: task, prompt version, profile, model requested and served, input, output and cache tokens, cost in micro-USD, latency, status and the session. Totals per day and task:

Terminal window
php artisan ai:usage --days=30 --domain=<tenant host>

The studio shows the running cost per course; GET /api/admin/course-builder/sessions/{id}/usage returns it per task and model.

Every generation, patch or apply is a run. GET /api/admin/course-builder/runs/{run} returns its state without opening the event stream, which is what the ulams CLI polls for --wait:

{ "id": "01j9z3k8m2x4q7r5t6v8w0y1ab", "sessionId": "01j9z3…", "kind": "generate",
"status": "running", "needsAttention": false,
"steps": [{ "id": "01j9z4…", "name": "lesson:el_8f2c", "status": "succeeded", "error": null }],
"startedAt": "2026-10-09T10:02:11+00:00", "finishedAt": null, "error": null }

status is one of queued, running, succeeded, failed, cancelled; step status is one of queued, running, succeeded, failed. needsAttention is true while a step has failed and the run waits for a retry (ulams builder runs retry-step), so a poller stops and asks. Only the session’s author and tenant admins can read a run (403 otherwise, 404 for an unknown id).

Generation runs in queued jobs on a dedicated connection (COURSE_BUILDER_QUEUE_CONNECTION, COURSE_BUILDER_QUEUE). With QUEUE_CONNECTION=database or redis the default is database-builder or redis-builder, queue builder, defined in api/config/queue.php with retry_after 2400 s (BUILDER_QUEUE_RETRY_AFTER), above the 1800 s job timeout. Run a worker for it on each tenant: Horizon (supervisor-builder) or api/workers.sh queue, which starts queue:work <driver>-builder --queue=builder --timeout=1800 per tenant. Do not point these jobs at a connection with a shorter retry_after: a second worker would pick up a step that is still running and the model would be called, and billed, twice. You can run several workers (queue:work processes) against the same queue, including the database connection: stage changes of a run are serialised on the run row, so a run finishes the same way with one worker or four. The long-job queue (LONG_JOB_QUEUE_CONNECTION, LONG_JOB_QUEUE; <driver>-long-job, queue queue-long-job, retry_after 19000 s) is separate and serves course clone and import. Living Course checks and analysis steps use the builder queue by default (LIVING_COURSE_QUEUE_CONNECTION, LIVING_COURSE_QUEUE). A run job has one try (tries = 1): the SDK already retries transient errors, and a failed step is retried by the author, so a model call is never repeated silently. A step can take minutes; a test (QueueRetryAfterConfigTest) keeps retry_after above every job timeout. The event stream (/api/admin/course-builder/sessions/{id}/events) holds a PHP worker for up to 25 s per open browser tab (COURSE_BUILDER_SSE_SECONDS); in production route it to a small separate PHP-FPM pool. Old events are deleted by php artisan course-builder:prune-events (30 days, COURSE_BUILDER_EVENTS_RETENTION_DAYS); schedule it daily.

Terminal window
php artisan course-builder:eval --fixtures=all --author=<user id> # fake driver, free
php artisan course-builder:eval --fixtures=coffee,injection --live --author=<id> # real model, costs money
php artisan course-builder:eval --fixtures=coffee --live --record --author=<id> # also write cassettes

Options: --fixtures (all or a list of coffee, injection, git, pdf, docx), --live, --record, --author (default: the first admin), --patch=1 (one element-chat patch) and --total-minutes=30. --live needs ANTHROPIC_API_KEY and stops at COURSE_BUILDER_EVAL_MONTHLY_USD for the month. Details: LLM layer. For the endpoints and the event stream see the Course Builder API.

Uploaded sources are stored on a private disk (COURSE_BUILDER_DISK, default storage/app/private), never in the public bucket. Removing a builder session does not remove a course it created.