Codumentor logo Codumentor

Model Profiles Plugin

The Model Profiles plugin lets authenticated users define their own named LLM profiles (model id, base URL, API key, thinking options) and assign them to specific agent roles — overriding which model each role uses for that user's turns. Deployers control whether users may bring their own API keys and which endpoints are allowed.

Using it

On the web

When Thinking Modes is also loaded, open Settings → Models (the root
pane Thinking Modes declares). Its Connections section is this plugin's
element:

The Role map in the same panel, with Custom selected, is where you
assign a connection to a role.

This plugin's settings_widget is conditional: it claims an Extensions
row of its own only when Thinking Modes is not among loaded, enabled
siblings — with it, Settings → Models is the one surface for both. The UI bundle (<x-model-profiles-connections>) is always
registered so Thinking Modes can mount it. The HTTP API never depends on who
is showing the UI.

Without Thinking Modes, Settings → Extensions → Model Profiles is the
standalone mount of the same element: title and description of its own, then
the same CRUD. There is no mode selector; the table lists your assignments
only — an unassigned role reads not set, because working out what it would
inherit is that plugin's job.

Assignments made in the Custom role map are the same per-user assignments this
plugin has always stored — they are what the
Thinking Modes plugin surfaces as the implicit Custom
mode.

Roles you can assign: main_agent, a catch-all default, subagent,
agentic_developer, agentic_memory_store, context_summarization,
explore, goal, web_retrieve, web_search, auto_title, translation
and voice_mediator.

In the terminal UI

Settings → Model Profiles shows both halves in one pane: the role map
first, then the profile list with its Add / Edit / Test / Del buttons. The map
is the web's, drawn for a terminal — a Mode dropdown at the top chooses
which mode it describes, every role gets its name in words, one line saying
what it does and the resolved connection → model-id with the same badge, and
the rows turn into dropdowns on Custom. Without the
Thinking Modes plugin there is no mode dropdown and the
map lists your own assignments, unassigned roles reading not set.

Everywhere

Deployers can define read-only admin profiles in the main models.profiles
block. Users may adopt those by supplying their own key (the POST /adopt/{name}
route and the terminal pane; the web's Connections section lists them among
the organization's connections instead).

Overrides apply only to authenticated users, and only while their turn is running.

Configuration

plugins:
  - module: codumentor.plugins.model_profiles
    class: ModelProfilesPlugin
    priority: 5
    args:
      enabled: true
      allow_custom_api_keys: true
      max_profiles_per_user: 20
      # allowed_base_urls: ["https://openrouter.ai/api/v1"]

Not enabled in the default shipped config; uncomment or add the snippet above to turn it on. Managing profiles — and typing in an endpoint on the web — requires the model_profiles:manage permission (power_user and admin have it by default) — without it, the role map is still shown, read-only, and the web's Connections section still lists every connection, lets the user hide models from their own picker and edit the ChatGPT plan they signed in with (models:connect_personal).

ParameterTypeDefaultDescription
enabledbooltrueWhether the plugin is active.
allow_custom_api_keysbooltrueAllow users to store their own API keys in profiles. When false, users can only use deployer-defined profiles.
allowed_base_urlslistunsetAllow-list of permitted endpoint base URLs. Omit or leave empty to allow any.
max_profiles_per_userint20Maximum profiles per user.
data_dirstringagent storage dir, else ./dataWhere profile data is stored.
previous_jwt_secretslistunsetOlder JWT secrets, so profiles encrypted under a rotated key can still be decrypted.

Connections

A user's models come from connections (thinking-modes-ux-rethink §4.2):
their own endpoints (an API key or a local server) and the subscription a
sign-in implies — the ChatGPT connection derived from an
openai_codex account, found by a duck-typed sibling probe
(derived_connections), one per account, never stored twice. Each model is
referenced as <connection>/<model> and listed under Yours in the model
picker, after the organization's connections, unless the user hid it ("show
in picker").

Routes, under /ui/plugins/model_profiles. Who may call them: any
signed-in user lists and trims; a subscription's preferences need
models:connect_personal (the user role has it) or model_profiles:manage;
creating, editing, testing or removing an endpoint needs
model_profiles:manage (403 Permission denied: model_profiles:manage). The
profile, assignment and adopt routes keep model_profiles:manage.

RouteNeedsWhat
GET /connectionssigned inorganization: the organization's connections as read-only cards (models with key and the caller's hidden, the light model, privacy, scope, provider_label); connections: the user's own as cards (models with hidden, light model, privacy, needs_rename, listing source, editable, a subscription's sign_in account); doors: each way to add one, open (then with allowed_base_urls, the form's hint) or with its reason; providers the API key / Local server doors offer (inside allowed_base_urls)
PATCH /organization-connections/{id}signed inhidden_models: the model keys the caller hides from their own picker (a legacy profile group's keys are profile names); 404 for an id with no card
POST /connectionsmodel_profiles:manageadd an endpoint: id, base_url, models (ids without whitespace, quotes, backticks, angle brackets or control characters), optional api_key, title, light_model, privacy (community), settings (model settings only, see Keys), hidden_models
PATCH /connections/{id}by kindpreferences: hidden_models, light_model, privacy, new_id; an endpoint's title, base_url, api_key, models
DELETE /connections/{id}model_profiles:manageremove an endpoint (a sign-in's connection goes with the account: disconnect it through the OAuth plugin)
POST /connections/{id}/testmodel_profiles:manageone request against an endpoint
POST /connections/{id}/refreshby kindre-read a subscription's model listing

The organization's cards and the "show in picker" choice on them are not
personal connections: they stay listed and editable whatever the policy below
says. The choice is applied in the picker only, never in resolution.

Personal connections off. When model_selection.personal_connections is
false, or the caller's access profile says personal_connections: false,
the user's connections are withheld from every surface this plugin feeds: the
override dict a turn installs carries none of their models (only their titles,
so the resolver can say what it fell back from), the picker has no "Yours"
group, GET /connections answers connections: [], providers: [], every
door closed with the reason and
personal_connections: {"allowed": false, "reason": …}, and every other
route on one of the user's own connections — and POST/PUT /profiles/{name}, POST /adopt/{name},
POST /profiles/{name}/test — answers 403 with the reason. It is decided
on every call, per turn for a turn, so a profile that tightens is in force at
the next turn. Nothing is deleted. See
Access profiles.

Notes