Configuration

Reference for every config file shipped with oxide-dispatch.

Reference for every config file shipped with oxide-dispatch. All config files live under shared/ and are listed in escrow_ignore so they remain editable.

shared/config.lua

Global runtime settings — broadcast policy, popup behavior, auto-expiry, and the heatmap cache.

KeyTypeDefaultDescription
Config.AutoExpire.enabledbooleantrueToggles the background expiry sweeper
Config.AutoExpire.defaultMinutesnumber15TTL applied to new alerts (in minutes)
Config.AutoExpire.sweepIntervalSecnumber30How often the sweeper runs (in seconds)
Config.Broadcast.onDutyOnlybooleantrueOnly deliver alerts to on-duty responders. Defers to oxide-police for LEO duty when installed
Config.Broadcast.maxJobsPerAlertnumber12Hard cap on the number of resolved jobs per alert (extras are trimmed)
Config.Broadcast.rateLimitMsnumber5000Minimum gap between client-originated alerts per player
Config.DispatchHub.RadialEnabledbooleantrueMaster switch for the Dispatch Hub entry inside the Dispatch radial menu. Disable on servers where another resource (e.g. oxide-police MDT) already provides a dispatch view
Config.Popup.durationMsnumber20000How long each popup stays on screen (ms)
Config.Popup.maxStackednumber3Maximum number of stacked popups visible at once
Config.Popup.mapZoomnumber3Zoom level used by the popup mini-map
Config.Popup.soundEffectstring'Lose_1st'Default sound effect played on alert receive
Config.Popup.soundSetstring'GTAO_FM_Events_Soundset'Sound set the effect is loaded from
Config.Heatmap.enabledbooleantrueEnables the heatmap data feed in the Dispatch Hub
Config.Heatmap.defaultWindowstring'30d'Default time window. One of '24h', '7d', '30d', 'all'
Config.Heatmap.cacheSecondsnumber60TTL of the in-memory heatmap cache (per window)
Config.Heatmap.maxPointsnumber5000Max points returned per query (downsampled if exceeded)
Config.AutoExpire = {
    enabled = true,
    defaultMinutes = 15,
    sweepIntervalSec = 30,
}

Config.Broadcast = {
    onDutyOnly = true,
    maxJobsPerAlert = 12,
    rateLimitMs = 5000,
}

Config.DispatchHub = {
    RadialEnabled = true,
}

Config.Popup = {
    durationMs = 20000,
    maxStacked = 3,
    mapZoom = 3,
    soundEffect = 'Lose_1st',
    soundSet = 'GTAO_FM_Events_Soundset',
}

Config.Heatmap = {
    enabled       = true,
    defaultWindow = '30d',
    cacheSeconds  = 60,
    maxPoints     = 5000,
}

shared/config/alerts.lua

The dispatch code catalog. Producers supply a code field on every alert; this catalog supplies the defaults (title, priority, icon, blip) for any field the producer does not explicitly populate. Per-alert sound overrides are also supported via data.soundEffect / data.soundSet.

Config.Codes

A mapping of code{ title, priority, icon, blip }. Each entry's blip is { sprite, color, scale }. Priority 1 is highest.

CodeTitlePriorityIconCategory
10-11Traffic Stop3fas fa-carPatrol / routine
10-14Prowler3fas fa-user-secretPatrol / routine
10-66Suspicious Activity3fas fa-magnifying-glassPatrol / routine
10-30Grand Theft Auto2fas fa-keyCrime in progress
10-31Crime in Progress2fas fa-handcuffsCrime in progress
10-34Assault in Progress2fas fa-hand-fistCrime in progress
10-91Store Robbery2fas fa-sack-dollarCrime in progress
10-99Narcotics Activity2fas fa-cannabisCrime in progress
10-50Vehicle Accident2fas fa-car-burstVehicle
10-55Intoxicated Driver2fas fa-wine-bottleVehicle
10-57Hit and Run2fas fa-car-sideVehicle
10-32Person With a Gun1fas fa-gunHigh priority
10-71Shots Fired1fas fa-gunHigh priority
10-72Drive-By Shooting1fas fa-gunHigh priority
10-77Explosion1fas fa-bombHigh priority
10-80Pursuit1fas fa-car-sideHigh priority
10-90Silent Alarm1fas fa-building-columnsHigh priority
10-52Medical Emergency2fas fa-truck-medicalEMS
10-53Person Down1fas fa-person-fallingEMS
10-54Cardiac Arrest1fas fa-heart-pulseEMS
10-56Trauma / Injury2fas fa-user-injuredEMS
10-58Overdose1fas fa-syringeEMS
10-70Structure Fire1fas fa-fireFire / hazmat
10-73Vehicle Fire1fas fa-car-burstFire / hazmat
10-74Brush / Wildfire1fas fa-fire-flame-curvedFire / hazmat
10-75Hazmat Incident1fas fa-radiationFire / hazmat
10-76Rescue / Extrication1fas fa-hands-holdingFire / hazmat
10-13Officer Needs Backup1fas fa-shield-halvedOfficer safety
panicPanic Button1fas fa-bellOfficer safety
911911 Call2fas fa-phone-volumeCitizen-initiated
311311 Tip4fas fa-comment-dotsCitizen-initiated

Config.GetCodeDefaults(code)

Helper used by the alert pipeline. Returns the catalog entry for the code, or a generic fallback (title = code, priority = 3, icon = 'fas fa-triangle-exclamation', generic blip) when the code is not in the catalog.

local defaults = Config.GetCodeDefaults('10-71')
-- { title = 'Shots Fired', priority = 1, icon = 'fas fa-gun', blip = {...} }

To add a new code, drop a new entry into Config.Codes:

Config.Codes['10-100'] = {
    title    = 'Burglary in Progress',
    priority = 2,
    icon     = 'fas fa-house-lock',
    blip     = { sprite = 161, color = 1, scale = 1.1 },
    -- optional per-code sound override:
    -- soundEffect = '...', soundSet = '...',
}

shared/config/jobs.lua

Maps responder group keys (police, ems, fire) to framework job names plus the UI metadata used by the Dispatch radial menu and panic title resolver.

Config.ResponderGroups

Each entry defines:

FieldTypeDescription
jobsstring[]Framework job names that count as members of this group
labelstringHuman-readable label used in dispatch UI and panic titles
iconstringFont Awesome icon name (no fas fa- prefix)
Config.ResponderGroups = {
    police = {
        jobs  = { 'police', 'lspd', 'bcso', 'sasp', 'sheriff', 'statetrooper',
                  'LSPD', 'BCSO', 'SASP' },
        label = 'Officer',
        icon  = 'shield-halved',
    },
    ems = {
        jobs  = { 'ambulance', 'ems', 'EMS' },
        label = 'Paramedic',
        icon  = 'truck-medical',
    },
    fire = {
        jobs  = { 'fire', 'FIRE' },
        label = 'Firefighter',
        icon  = 'fire',
    },
}

A radialId field from older configs is ignored — all groups now share the single Dispatch radial menu. You can safely delete the field.

Job-name matching is case-sensitive. Include every casing your framework actually uses.

Config.ResolveJobsDefault

Fallback used when a producer omits jobs. nil (the default) means an alert with no jobs reaches no recipients and a one-time warning is logged. Set to a group key (e.g. 'police') to opt back into the legacy silent-fallback behavior.

Config.ResolveJobsDefault = nil    -- or 'police' to silently default

Config.ResolveJobs(input)

Normalizes producer inputs into a flat job-name array.

InputReturns
nilConfig.ResolveJobsDefault (or empty + warning)
'police' (group key)The group's jobs array
'sheriff' (raw job){ 'sheriff' }
{ 'police', 'sasp' }Deduped expansion of all values

Config.GetGroupForJob(jobName)

Reverse lookup. Returns the group key ('police', 'ems', 'fire') that owns a given raw job name, or nil if it isn't grouped. Used by the broadcast layer, panic subsystem, and per-group status broadcasts.

Adding a new responder group

To add a new responder group (for example coastguard):

  1. Add the group key + jobs to Config.ResponderGroups here.
  2. Add a status list under the same key in Config.ResponderStatuses (or rely on _default).
  3. (Optional) Add a titles / messages entry for the group in Config.Panic so panic alerts read naturally.

Full recipes (add a job, rename a job, build a new group) live in the Jobs & Responder Groups guide.

shared/config/statuses.lua

Per-session responder status states. Each responder group has its own status list so police-coded labels (Code-4, 10-7) don't leak into EMS/Fire UIs. Status changes broadcast to colleagues in the SAME group.

Config.ResponderStatuses

Each entry is a list of { id, label, color, icon } rows.

GroupStatuses (id → label)
policeavailable → Available, busy → Busy / On Call, code4 → Code-4, 10-7 → 10-7 (Out of Service)
emsavailable, oncall, enroute, onscene, transport, 10-7
fireavailable, enroute, onscene, returning, 10-7
_defaultavailable, busy, 10-7 (used when a custom group has no specific list)

Config.ResponderStatusDefault

KeyTypeDefaultDescription
Config.ResponderStatusDefaultstring'available'Status id new responders are initialized with. Must exist in every group's status list (and in _default)

Helpers

FunctionReturnsDescription
Config.GetResponderStatusList(group)table[]The status list for a group (falls back to _default)
Config.GetResponderStatus(group, id)table|nilResolves a status entry by group + id, with fallback to _default

Config.ResponderCallsign

KeyTypeDefaultDescription
Config.ResponderCallsign.maxLengthnumber16Maximum callsign length (chars). Enforced server-side and on the input dialog
Config.ResponderCallsign.patternstring'^[%w%-]+$'Lua string pattern (anchored) callsigns must match. Extend the character class to allow dots, slashes, etc.
Config.ResponderCallsign = {
    maxLength = 16,
    pattern   = '^[%w%-]+$',     -- alphanumerics + hyphen
    -- pattern = '^[%w%-%.]+$',  -- alphanumerics + hyphen + dot ('A-12.5')
    -- pattern = '^[%w%-/%.]+$', -- alphanumerics + hyphen + slash + dot ('EMS/01')
}

shared/config/detection.lua

Toggles the automatic crime detection module. Each detector funnels into the standard alert pipeline (rate limiting + duty filter + persistence apply normally).

Config.Detection

Every detector accepts a jobs field for recipient routing. It takes the same input as Config.ResolveJobs: a responder-group key ('police'), a raw job name ('lspd'), or an array of either.

KeyTypeDefaultDescription
Config.Detection.rateLimitPerPlayerMsnumber8000Fallback per-player cooldown (ms), used only for detectors that have no entry in cooldownsMs
Config.Detection.cooldownsMs.shootingnumber12000Per-player cooldown (ms) between shots-fired alerts
Config.Detection.cooldownsMs.drivebynumber15000Per-player cooldown (ms) between drive-by alerts
Config.Detection.cooldownsMs.meleenumber60000Per-player cooldown (ms) between melee alerts
Config.Detection.cooldownsMs.explosionnumber15000Per-player cooldown (ms) between explosion alerts
Config.Detection.cooldownsMs.vehicletheftnumber30000Per-player cooldown (ms) between vehicle-theft alerts
Config.Detection.ServerDedup.enabledbooleantrueServer-side duplicate suppression — drops repeated auto-detect alerts of the same code from the same offender, even if a modified client skips its own cooldown
Config.Detection.ServerDedup.windowMsnumber60000How long (ms) repeats of the same alert from the same offender are suppressed
Config.Detection.Shooting.enabledbooleantrueDetect shots fired on foot
Config.Detection.Shooting.codestring'10-71'Code used for the alert
Config.Detection.Shooting.jobsstring|table'police'Recipient group(s)
Config.Detection.DriveBy.enabledbooleantrueDetect shots fired from a vehicle
Config.Detection.DriveBy.codestring'10-72'Code used for the alert
Config.Detection.DriveBy.jobsstring|table'police'Recipient group(s)
Config.Detection.Melee.enabledbooleantrueDetect melee combat (witness-based)
Config.Detection.Melee.codestring'10-34'Code used for the alert
Config.Detection.Melee.jobsstring|table'police'Recipient group(s)
Config.Detection.Explosion.enabledbooleantrueDetect explosions
Config.Detection.Explosion.codestring'10-77'Code used for the alert
Config.Detection.Explosion.jobsstring|table{'police','ems','fire'}Recipient group(s)
Config.Detection.Death.enabledbooleantrueAuto-dispatch on player death
Config.Detection.Death.codestring'10-52'Code used for the alert
Config.Detection.Death.jobsstring|table'ems'Recipient group(s)
Config.Detection.VehicleTheft.enabledbooleantrueDetect car-alarm trip + carjacking + oxide-vehicles lockpick/hotwire
Config.Detection.VehicleTheft.codestring'10-30'Code used for the alert
Config.Detection.VehicleTheft.jobsstring|table'police'Recipient group(s)

Every detector also accepts chance (number, default 1.0) — the 0.0–1.0 probability that a qualifying event is reported (0.0 disables it, like enabled = false). For the cooldown-gated detectors the roll happens once per cooldown window; Death rolls once per death.

Config.Detection.WeaponBlacklist

Weapons that should never trigger an automatic alert (less-lethal, training weapons, fists). Each key is a weapon, each value is true. Write the weapon name inside backticks (e.g. `WEAPON_PISTOL`) — the game converts the backticked name into the weapon hash it uses internally.

Config.Detection.WeaponBlacklist = {
    [`WEAPON_UNARMED`] = true,
    [`WEAPON_STUNGUN`] = true,
    [`WEAPON_FLAREGUN`] = true,
}

Config.Detection.ZoneBlacklist

Geographic exclusion zones — shooting/explosion in these areas won't alert. Each entry: { coords = vector3, radius = number, label = string }.

Config.Detection.ZoneBlacklist = {
    -- { coords = vector3(450.0, -980.0, 30.0), radius = 60.0, label = 'Mission Row PD' },
    -- { coords = vector3(842.0, -1290.0, 28.0), radius = 80.0, label = 'PD Range' },
}

Config.Detection.JobBlacklist

Jobs whose holders should never trip an auto-detect alert. Keys are framework job names and must exactly match the job name as your framework spells it (case-sensitive); values true. Use this to stop on-duty responders from dispatching themselves whenever they fire their service weapon, commandeer a vehicle in pursuit, or take a melee hit on the job. Manual paths (911, panic, MDT-issued alerts) ignore this list.

Config.Detection.JobBlacklist = {
    -- ['police']       = true,
    -- ['lspd']         = true,
    -- ['bcso']         = true,
    -- ['sasp']         = true,
    -- ['sheriff']      = true,
    -- ['statetrooper'] = true,
    -- ['ambulance']    = true,
    -- ['ems']          = true,
    -- ['fire']         = true,
}

shared/config/panic.lua

Two distinct subsystems live in this file: the panic / responder-needs-backup alert (separate from the panic dispatch code) and the civilian 911/311 category catalog.

Config.Panic

KeyTypeDefaultDescription
Config.Panic.cooldownSecnumber60Per-responder cooldown (seconds) on the panic keybind / /panic
Config.Panic.broadcastOffDutybooleantrueWhen true, panic alerts ignore Config.Broadcast.onDutyOnly
Config.Panic.autoWaypointbooleantrueAuto-set the recipient's GPS waypoint to the panicker
Config.Panic.soundEffectstring|nilnilDistinct alert sound for panic. Falls back to Config.Popup.soundEffect when nil
Config.Panic.soundSetstring|nilnilSound set for the panic alert. Falls back to Config.Popup.soundSet when nil
Config.Panic.titles[group]stringper groupPanic alert title per responder group. _default is the fallback
Config.Panic.messages[group]stringper groupPanic alert message per responder group. _default is the fallback
Config.Panic = {
    cooldownSec = 60,
    broadcastOffDuty = true,
    autoWaypoint = true,
    soundEffect = nil,
    soundSet = nil,
    titles = {
        police   = 'OFFICER NEEDS BACKUP',
        ems      = 'MEDIC NEEDS BACKUP',
        fire     = 'FIREFIGHTER NEEDS BACKUP',
        _default = 'RESPONDER NEEDS BACKUP',
    },
    messages = {
        police   = 'Officer panic alert',
        ems      = 'Medic panic alert',
        fire     = 'Firefighter panic alert',
        _default = 'Responder panic alert',
    },
}

Config.Civilian

Configures the citizen 911 / 311 menu (radial + chat commands).

KeyTypeDefaultDescription
Config.Civilian.anonymousDefaultbooleantrueWhether 911/311 calls strip caller identity from the broadcast by default
Config.Civilian.maxMessageLengthnumber240Hard cap on civilian call message + responder reply length (chars). Truncated server-side
Config.Civilian.Categories911table[]see fileCategory list for /911 and the radial Call 911 submenu
Config.Civilian.Categories311table[]see fileCategory list for /311 and the radial Call 311 submenu

Each category entry is { id, label, code, jobs, icon? }. jobs accepts the same input Config.ResolveJobs does (group key, raw job, or array).

Config.Civilian = {
    anonymousDefault = true,
    maxMessageLength = 240,

    Categories911 = {
        { id = 'crime',    label = 'Crime in Progress',   code = '10-31', jobs = 'police' },
        { id = 'medical',  label = 'Medical Emergency',   code = '10-52', jobs = 'ems' },
        { id = 'injury',   label = 'Person Injured',      code = '10-56', jobs = 'ems' },
        { id = 'overdose', label = 'Overdose',            code = '10-58', jobs = 'ems' },
        { id = 'fire',     label = 'Structure Fire',      code = '10-70', jobs = 'fire' },
        { id = 'vfire',    label = 'Vehicle Fire',        code = '10-73', jobs = 'fire' },
        { id = 'hazmat',   label = 'Hazmat / Spill',      code = '10-75', jobs = 'fire' },
        { id = 'shooting', label = 'Shots Fired',         code = '10-71', jobs = 'police' },
        { id = 'accident', label = 'Vehicle Accident',    code = '10-50', jobs = { 'police', 'ems' } },
        { id = 'other',    label = 'Other Emergency',     code = '911',   jobs = { 'police', 'ems', 'fire' } },
    },

    Categories311 = {
        { id = 'tip',         label = 'Anonymous Tip',       code = '311',   jobs = 'police' },
        { id = 'noise',       label = 'Noise Complaint',     code = '311',   jobs = 'police' },
        { id = 'suspicious',  label = 'Suspicious Person',   code = '10-66', jobs = 'police' },
        { id = 'medical_q',   label = 'Medical Question',    code = '311',   jobs = 'ems' },
        { id = 'fire_safety', label = 'Fire / Safety Issue', code = '311',   jobs = 'fire' },
        { id = 'other',       label = 'Other Non-Emergency', code = '311',   jobs = 'police' },
    },
}

shared/config/compat.lua

Per-framework toggles for the legacy police-alert shims. The shims forward old QB/ESX events into the modern Alerts.Create pipeline so existing scripts keep working without edits.

Top-level toggles

KeyTypeDefaultDescription
Config.Compatibility.enabledbooleantrueMaster toggle for all shims

Config.Compatibility['qb-core']

KeyTypeDefaultDescription
enabledbooleantrueEnable the QBCore shim
serverPoliceAlertbooleantrueListen for police:server:policeAlert from server scripts
clientPoliceAlertbooleanfalseListen for the QB client-side police-alert relay
ambulanceAlertbooleantrueListen for hospital:server:ambulanceAlert (qb-ambulancejob / qbx_ambulancejob downed alerts, /911e, third-party)
emergencyAlertbooleantrueListen for hospital:server:emergencyAlert (QBX radial menu fast path)
clientAmbulanceAlertbooleanfalseListen for the QB client-side ambulance-alert relay
emsJobsstring|table'ems'Group / job(s) the converted ambulance alert is addressed to
emsCodestring'10-52'Code used for converted ambulance alerts
jobsstring|table'police'Group / job(s) the converted police alert is addressed to
defaultCodestring'10-31'Code used when no keyword matches the police alert message
rateLimitMsnumber5000Per-source rate limit for the shim (shared across police + EMS paths)
mirrorQbPhonebooleanfalseWhen true and qb-phone is started, mirror the police alert into qb-phone:client:addPoliceAlert
keywordCodestablesee fileOrdered keyword → code rules for the police shim (case-insensitive substring match)

When Config.Detection.Death.enabled AND ambulanceAlert are both on, the shared rateLimitMs absorbs the duplicate alert that fires when a player dies. If you set rateLimitMs = 0, disable one of the two to avoid duplicates.

Config.Compatibility['qb-core'] = {
    enabled = true,
    serverPoliceAlert = true,
    clientPoliceAlert = false,
    ambulanceAlert = true,
    emergencyAlert = true,
    clientAmbulanceAlert = false,
    emsJobs = 'ems',
    emsCode = '10-52',
    jobs = 'police',
    defaultCode = '10-31',
    rateLimitMs = 5000,
    mirrorQbPhone = false,
    keywordCodes = {
        { pattern = 'drug',       code = '10-99' },
        { pattern = 'narcotic',   code = '10-99' },
        { pattern = 'store',      code = '10-91' },
        { pattern = 'bank',       code = '10-90' },
        { pattern = 'jewel',      code = '10-90' },
        { pattern = 'suspicious', code = '10-66' },
        { pattern = 'suspicous',  code = '10-66' },  -- common misspelling
        { pattern = 'vehicle',    code = '10-80' },
        { pattern = 'robbery',    code = '10-31' },
    },
}

Config.Compatibility['es_extended']

KeyTypeDefaultDescription
enabledbooleantrueEnable the ESX shim
serverPoliceAlertbooleantrueListen for esx_policejob:alert / esx_policejob:policeAlert
clientPoliceAlertbooleanfalseListen for the ESX client-side police-alert relay
esxPhoneSendbooleantrueConvert police-contact esx_phone:send calls to alerts
outlawAlertbooleantrueListen for esx_outlawalert:policeNotify / outlawNotify
distressAlertbooleantrueListen for esx_ambulancejob:onPlayerDistress (downed-player keybind)
esxPhoneEmsbooleantrueConvert ambulance-contact esx_phone:send calls to alerts
emsJobsstring|table'ems'Group / job(s) the converted ambulance alert is addressed to
emsCodestring'10-52'Code used for converted ambulance alerts
jobsstring|table'police'Group / job(s) the converted police alert is addressed to
defaultCodestring'10-31'Code used when no keyword matches the police alert message
rateLimitMsnumber5000Per-source rate limit for the shim (shared across police + EMS paths)
keywordCodestablesee fileOrdered keyword → code rules for the police shim
Config.Compatibility['es_extended'] = {
    enabled = true,
    serverPoliceAlert = true,
    clientPoliceAlert = false,
    esxPhoneSend = true,
    outlawAlert = true,
    distressAlert = true,
    esxPhoneEms = true,
    emsJobs = 'ems',
    emsCode = '10-52',
    jobs = 'police',
    defaultCode = '10-31',
    rateLimitMs = 5000,
    keywordCodes = {
        { pattern = 'drug',       code = '10-99' },
        { pattern = 'narcotic',   code = '10-99' },
        { pattern = 'store',      code = '10-91' },
        { pattern = 'bank',       code = '10-90' },
        { pattern = 'jewel',      code = '10-90' },
        { pattern = 'suspicious', code = '10-66' },
        { pattern = 'suspicous',  code = '10-66' },
        { pattern = 'vehicle',    code = '10-80' },
        { pattern = 'robbery',    code = '10-31' },
        { pattern = 'shot',       code = '10-71' },
        { pattern = 'gun',        code = '10-32' },
    },
}

QBX

QBX uses the same police:server:policeAlert event names QBCore does (verified across qbx_bankrobbery, qbx_truckrobbery, qbx_houserobbery, qbx_jewelery, qbx_storerobbery, qbx_vehiclekeys, and the legacy qbx_police). The qb-core shim above handles QBX traffic — there is no separate qbx_core block. See the QBX Setup page for QBX-specific notes.

shared/config/ai_dispatch.lua

Configuration for the optional AI Radio Dispatcher feature — provider selection, radio channels (one per department job), idle timeout, identifier whitelist, integration toggles, model and voice choices.

KeyTypeDefaultDescription
Config.AIDispatch.EnabledbooleantrueMaster switch for the AI feature
Config.AIDispatch.Providerstring'chained'Global default provider ('realtime' or 'chained'); each channel can override
Config.AIDispatch.Channelstablesee belowMap of radio channel number → channel entry. If left empty, a warning is logged at startup and the AI dispatch feature turns itself off — the server still starts normally
Config.AIDispatch.IdleTimeoutMinnumber10Auto-close idle realtime sessions after N minutes (chained mode skips)
Config.AIDispatch.RateLimit.alertsnumber3Per-officer alert cap per window
Config.AIDispatch.RateLimit.windowSecnumber60Rate-limit window in seconds
Config.AIDispatch.Access.IdentifierWhiteliststring[]{}Global FiveM identifier allowlist fallback. Per-channel whitelist takes precedence
Config.AIDispatch.Integrations.Mdttable[]{ {name='oxide-police',...}, {name='ps-mdt',...}, {name='wasabi_mdt',...}, {name='lb-tablet',...} }Priority list of MDT backends powering the 8 MDT-flavored AI tools (plate/person/profile/BOLO/custody/on-duty). First entry with enabled ~= false whose named resource is started wins. Reorder to change priority. Write your own bridge per the MDT Bridge Development guide.
Config.AIDispatch.Realtime.Modelstring'gpt-realtime'OpenAI Realtime model id
Config.AIDispatch.Realtime.Voicestring'alloy'Default Realtime voice if a channel doesn't set its own
Config.AIDispatch.Realtime.Instructionsstring''Populated automatically from ai_dispatch_prompt.lua — leave empty
Config.AIDispatch.Chained.STT.Provider / .Modelstring'openai' / 'whisper-1'Speech-to-text adapter and model
Config.AIDispatch.Chained.LLM.Provider / .Modelstring'openai' / 'gpt-4o-mini'Language-model adapter and model
Config.AIDispatch.Chained.LLM.MaxHistoryTurnsnumber20History window per session
Config.AIDispatch.Chained.LLM.MaxToolCascadenumber4Cap on chained tool-call loops per transmission
Config.AIDispatch.Chained.TTS.Provider / .Modelstring'openai' / 'tts-1'Text-to-speech adapter and model
Config.AIDispatch.Chained.TTS.Voicestring'onyx'Default TTS voice if a channel doesn't set its own

Channel entry fields (Config.AIDispatch.Channels[N])

FieldTypeRequiredDescription
jobstringyesFramework job name bound to this channel (e.g. 'lspd', 'bcso', 'sasp', 'ambulance', 'fire'). Police-MDT tools appear on this channel when the job is listed in Config.ResponderGroups.police.jobs
labelstringnoUI label for the channel
voicestringnoOverride the default voice for this channel
providerstringnoOverride the global Provider for this channel ('realtime' or 'chained')
whiteliststring[]noPer-channel identifier allowlist; takes precedence over Access.IdentifierWhitelist when non-empty

Three channels ship enabled by default: 911 → job 'police' (voice ash), 921'ambulance' (voice shimmer), and 931'fire' (voice sage). Channels 912 (BCSO) and 913 (SASP) are included as commented-out examples.

Important: each channel's job must match a job name that actually exists on your server, spelled exactly as your framework has it (usually lowercase). If your police job isn't called police (e.g. lspd), change channel 911's job value — otherwise no player will pass the channel's job check.

Config.AIDispatch.RadioFX

The radio-static filter applied to the AI dispatcher's voice — band-limiting, light distortion, and a faint background hiss make it sound like a real comms radio instead of a clean synthesized stream.

KeyTypeDefaultDescription
EnabledbooleantrueMaster toggle for the radio effect
BandpassLownumber400Lowest voice frequency let through (Hz). Higher = thinner sound
BandpassHighnumber3400Highest voice frequency let through (Hz)
Distortionnumber0.04Distortion amount, 0–1 (0 = clean, 0.3 = crunchy)
NoiseFloornumber0.008Volume of the constant background hiss, 0–1 (0 = off, 0.04 = very loud)
NoiseLownumber1000Lowest frequency of the hiss (Hz)
NoiseHighnumber4500Highest frequency of the hiss (Hz)
ChainedGainnumber1.85Output volume of the voice in chained mode (1.0 = unchanged)
RealtimeGainnumber1.85Output volume of the voice in realtime mode (1.0 = unchanged)

API key

The OpenAI API key is not stored in this file. Set it as a server convar (a server-side variable, set in server.cfg or the live console):

set oxide:ai-dispatch:openai_key sk-...your-key-here...

Use set, not setrsetr replicates the value to every connected player and would leak your key. Other providers read their key from oxide:ai-dispatch:<provider>_key (e.g. set oxide:ai-dispatch:groq_key ...).

Full feature documentation, cost notes, voice options, identifier-whitelist usage, and privacy disclosure templates live in the AI Radio Dispatcher guide.

shared/config/ai_dispatch_acks.lua

Spoken acknowledgements for the AI dispatcher. When the dispatcher decides to look something up (a plate, a person's records, a BOLO), the chained pipeline can take a few seconds to answer. To avoid dead silence on the radio while that happens, a short pre-rendered acknowledgement plays the moment the lookup starts — "Running that plate.", "Pulling those records.", and so on.

Config.AIDispatch.Acks

KeyTypeDefaultDescription
EnabledbooleantrueMaster toggle. When false, no clips are rendered at startup and the pipeline behaves as before (silence during lookups)
Phrasestablesee fileOne list of phrases per tool name. A phrase from the matching list is picked at random each time so it doesn't feel scripted — add as many variants as you like
Defaultstring[]{ "Copy, stand by.", "Stand by." }Fallback phrases used when a tool has no Phrases entry (e.g. a new tool added later, or a custom one)

The audio clips are generated automatically on first server startup using each channel's text-to-speech voice and cached on disk under <resource>/cache/acks/<voice>/. You can add or edit phrases freely — changed phrases are re-rendered on the next boot while unchanged ones are reused from the cache.

Shipped phrase lists cover the core dispatch tools (create_dispatch_alert, cancel_dispatch_alert, attach_responder, detach_responder, set_responder_status, set_officer_status, trigger_panic, list_active_alerts, update_dispatch_alert) and the police MDT tools (lookup_plate, lookup_person, get_person_profile, create_bolo, list_active_bolos, clear_bolo, list_on_duty_officers, check_custody).

shared/config/ai_dispatch_prompt.lua

The dispatcher's system prompt — the behavior contract the AI follows. Built per session by Config.BuildAIDispatchPrompt(job, hasPoliceTools) and used by both the realtime and chained providers.

Structure:

  • JOB_BLOCKS — per-job lookup of { name, flavor }. name is how the dispatcher introduces itself ("LSPD", "BCSO", "EMS", etc.); flavor selects which routing-rules block applies ('police', 'ems', 'fire', or nil for generic). Add a key for any custom job you want to tune.
  • FLAVOR_ROUTING — three blocks of natural-language routing guidance keyed by flavor.
  • BASE_PROMPT, CORE_TOOL_RULES, POLICE_TOOL_RULES, RADIO_AWARENESS — structural sections spliced together by BuildAIDispatchPrompt. Police MDT guidance is only included when the channel's job qualifies for those tools.

Jobs that aren't in JOB_BLOCKS get a generic uppercase fallback (e.g. 'amr_paramedic' → "AMR_PARAMEDIC dispatch"). If you customize the prompt, keep the structural sections intact — the model relies on them to use the tools correctly.

Next Steps