Skip to content

AI Providers

Ed Mozley edited this page Jul 21, 2026 · 3 revisions

AI Providers

FreeITSM's AI features share a single, provider-agnostic building block. Any module can offer a provider / model / key settings panel and call out to an LLM without re-implementing the plumbing. Three providers are supported β€” Anthropic (Claude), OpenAI (GPT) and OpenRouter β€” and each AI feature is configured independently with its own key.

This page documents the shared layer added in June 2026. For the per-module behaviour see the individual module pages (Knowledge, CMDB, Workflows, Forms, Tickets, Contracts).

The building block

Five pieces, of which a module only ever touches the first:

File Responsibility
includes/ai_settings_panel.php renderAiSettingsPanel('<ns>') β€” renders the provider/model/key panel. One line per settings page.
assets/js/ai-settings.js Drives every [data-ai-panel] on the page β€” provider switching, the model dropdown, save/test.
includes/ai_settings.php Namespace registry + load/save/forUi. Reads config from system_settings, decrypts keys.
includes/ai_provider.php Provider-agnostic aiProviderChat() β€” the only code that knows each vendor's wire format. Plus the OpenRouter model catalogue.
api/system/ai/{get_settings,save_settings,test_connection,openrouter_models}.php Shared endpoints behind the panel. No per-module logic.

The split matters: ai_provider.php is storage-agnostic (it takes a plain config array and never reads settings itself), so it can be reused anywhere, while ai_settings.php is the part that knows about system_settings.

Providers

aiProviderChat(array $cfg, array $opts) normalises all three into one return shape (content, tokens_in, tokens_out, provider, model, duration_ms).

Provider Endpoint Wire format
anthropic https://api.anthropic.com/v1/messages Anthropic Messages API (x-api-key, anthropic-version: 2023-06-01)
openai https://api.openai.com/v1/chat/completions OpenAI chat-completions (Authorization: Bearer)
openrouter https://openrouter.ai/api/v1/chat/completions OpenAI-compatible β€” same code path as openai

OpenAI and OpenRouter share one implementation (aiProviderCallOpenAICompatible()) because OpenRouter speaks the OpenAI wire format. All outbound POSTs go through aiProviderHttpPost(), which retries on 429 / 5xx / network errors with exponential backoff (3 attempts, 2s base).

OpenRouter specifics

  • One key, hundreds of models. OpenRouter is an aggregator β€” a single key reaches Claude, GPT, Gemini, Llama, Mistral, DeepSeek and more. Models are addressed by namespaced id, e.g. anthropic/claude-3.5-sonnet, google/gemini-2.0-flash.
  • Live model catalogue. aiProviderListOpenRouterModels() fetches https://openrouter.ai/api/v1/models (no key required β€” public) and caches it in system_settings (openrouter_models_cache, openrouter_models_cached_at) for 24h. On a failed refresh it falls back to the stale cache rather than returning an empty list. The panel renders this as a searchable dropdown showing each model's id and per-million-token pricing (in / out).
  • Attribution headers. OpenRouter calls send HTTP-Referer: https://freeitsm.co.uk and X-Title: FreeITSM so usage is attributed on the OpenRouter dashboard.
  • Per-key spend limits. OpenRouter supports multiple keys with per-key spend limits, so per-module granular billing is preserved under a single OpenRouter account.

Namespace registry

Each AI feature registers a namespace in aiSettingsRegistry() (includes/ai_settings.php) with a default provider and model:

Namespace Feature Settings location Default model
knowledge_ai Knowledge β€” Ask AI Knowledge β†’ Settings β†’ AI claude-haiku-4-5-20251001
cmdb_ai CMDB β€” summaries + Suggest Properties CMDB β†’ Settings β†’ AI Integration claude-haiku-4-5-20251001
workflow_ai Workflows β€” AI co-author Workflows β†’ Settings β†’ AI claude-sonnet-4-6
forms_ai Forms β€” AI Assist Forms β†’ Settings β†’ AI claude-sonnet-4-6
tickets_reply_cleanup Tickets β€” Reply Cleanup Tickets β†’ Settings β†’ Reply Cleanup claude-haiku-4-5-20251001

RFP Builder (Contracts) is deliberately deferred from the shared panel β€” its long streaming generations need a dedicated OpenRouter SSE path first, so it keeps its existing Anthropic streaming for now.

The registry is also a security boundary β€” see below.

Storage & security

Config for a namespace <ns> lives in four system_settings rows:

Key Contents
<ns>_provider anthropic | openai | openrouter
<ns>_model Model id
<ns>_api_key Encrypted at rest (AES-256-GCM). Must be listed in ENCRYPTED_SETTING_KEYS + MASKED_SETTING_KEYS in includes/encryption.php.
<ns>_verify_ssl '1' | '0' (absent β†’ falls back to the global SSL_VERIFY_PEER)

Security properties:

  • The registry is an allowlist. The generic api/system/ai/* endpoints only ever read or write keys derived from a registered namespace, so they can't be abused to read or overwrite arbitrary system_settings (e.g. other secrets). An unregistered namespace is rejected.
  • Keys never leave the server. aiSettingsForUi() returns only a mask (****<last4>) and a has_key flag β€” the plaintext key is never sent to the browser. It exists in plaintext only for the moment a request is made to the provider.
  • Masked / no-change saves. aiSettingsSave() treats a masked or empty key submission as "leave unchanged", so re-saving provider + model never wipes the stored key. You only overwrite it by typing a new one.
  • SSL verify dial. The effective verify flag is the global SSL_VERIFY_PEER constant ANDed with the per-namespace toggle. On a dev box where the global is off (no CA bundle), outbound HTTPS skips verification regardless; in production set the global on and use the toggle to opt out per feature.

No schema change was required β€” everything lives in system_settings.

Streaming

Two features stream their output live (claude.ai-style SSE). The rollout deliberately left the proven streaming engine (includes/rfp_ai.php) untouched:

  • Provider = anthropic β†’ existing native streaming path (live tokens), unchanged.
  • Provider = openai / openrouter β†’ one-shot aiProviderChat() emitted as a single SSE event (no token-by-token streaming for these providers yet).

This applies to Forms AI Assist and Tickets Reply Cleanup.

Adding a new AI feature

  1. Add a namespace to aiSettingsRegistry() in includes/ai_settings.php (label + default provider + default model).
  2. Add <ns>_api_key to ENCRYPTED_SETTING_KEYS and MASKED_SETTING_KEYS in includes/encryption.php.
  3. Drop <?php renderAiSettingsPanel('<ns>'); ?> on the module's settings page (ensure ai-settings.js, the common i18n namespace, and showToast() are available).
  4. In the backend, load config with aiSettingsLoad($conn, '<ns>') and pass it to aiProviderChat().

That's the whole adoption cost β€” no per-module provider plumbing.

Related pages

FreeITSM

Getting Started

Modules

Multi-tenancy (planned)

Blue sky thinking

Bugs resolved

Links

Clone this wiki locally