Skip to content

Configuration

llm_expose.config.models

Configuration models for llm-expose using Pydantic.

DiscordClientConfig

Bases: BaseModel

Configuration for the Discord client adapter.

Source code in llm_expose/config/models.py
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
class DiscordClientConfig(BaseModel):
    """Configuration for the Discord client adapter."""

    client_type: Literal["discord"] = "discord"
    bot_token: str = Field(description="Discord bot token from the Developer Portal")
    mcp_servers: list[str] = Field(
        default_factory=list,
        description="List of MCP server names attached to this channel",
    )
    system_prompt_path: str | None = Field(
        default=None,
        description="Path to a file containing the custom system prompt for this channel. Falls back to default when omitted.",
    )
    model_name: str | None = Field(
        default=None,
        description="Default LLM model name for this channel (used with --llm-completion)",
    )

    @field_validator("bot_token")
    @classmethod
    def token_must_not_be_empty(cls, v: str) -> str:
        """Ensure bot token is not blank."""
        if not v or not v.strip():
            raise ValueError("bot_token must not be empty or whitespace")
        return v.strip()

    @field_validator("mcp_servers")
    @classmethod
    def normalize_mcp_servers(cls, values: list[str]) -> list[str]:
        """Normalize attached MCP server names preserving order and uniqueness."""
        normalized: list[str] = []
        seen: set[str] = set()
        for value in values:
            name = value.strip()
            if not name or name in seen:
                continue
            seen.add(name)
            normalized.append(name)
        return normalized

    @field_validator("system_prompt_path")
    @classmethod
    def validate_system_prompt_path(cls, value: str | None) -> str | None:
        """Validate system prompt path by trimming outer whitespace."""
        if value is None:
            return None
        normalized = value.strip()
        return normalized or None

    @field_validator("model_name")
    @classmethod
    def normalize_model_name(cls, value: str | None) -> str | None:
        """Normalize optional model name by trimming outer whitespace."""
        if value is None:
            return None
        normalized = value.strip()
        return normalized or None

normalize_mcp_servers(values) classmethod

Normalize attached MCP server names preserving order and uniqueness.

Source code in llm_expose/config/models.py
250
251
252
253
254
255
256
257
258
259
260
261
262
@field_validator("mcp_servers")
@classmethod
def normalize_mcp_servers(cls, values: list[str]) -> list[str]:
    """Normalize attached MCP server names preserving order and uniqueness."""
    normalized: list[str] = []
    seen: set[str] = set()
    for value in values:
        name = value.strip()
        if not name or name in seen:
            continue
        seen.add(name)
        normalized.append(name)
    return normalized

normalize_model_name(value) classmethod

Normalize optional model name by trimming outer whitespace.

Source code in llm_expose/config/models.py
273
274
275
276
277
278
279
280
@field_validator("model_name")
@classmethod
def normalize_model_name(cls, value: str | None) -> str | None:
    """Normalize optional model name by trimming outer whitespace."""
    if value is None:
        return None
    normalized = value.strip()
    return normalized or None

token_must_not_be_empty(v) classmethod

Ensure bot token is not blank.

Source code in llm_expose/config/models.py
242
243
244
245
246
247
248
@field_validator("bot_token")
@classmethod
def token_must_not_be_empty(cls, v: str) -> str:
    """Ensure bot token is not blank."""
    if not v or not v.strip():
        raise ValueError("bot_token must not be empty or whitespace")
    return v.strip()

validate_system_prompt_path(value) classmethod

Validate system prompt path by trimming outer whitespace.

Source code in llm_expose/config/models.py
264
265
266
267
268
269
270
271
@field_validator("system_prompt_path")
@classmethod
def validate_system_prompt_path(cls, value: str | None) -> str | None:
    """Validate system prompt path by trimming outer whitespace."""
    if value is None:
        return None
    normalized = value.strip()
    return normalized or None

ExposureConfig

Bases: BaseModel

Top-level configuration for a single LLM exposure.

Source code in llm_expose/config/models.py
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
class ExposureConfig(BaseModel):
    """Top-level configuration for a single LLM exposure."""

    name: str = Field(description="Unique name for this exposure configuration")
    channel_name: str | None = Field(
        default=None,
        description="Selected saved channel config name used for pair scoping.",
    )
    provider: ProviderConfig = Field(description="LLM provider settings")
    client: ClientConfig = Field(description="Messaging client settings")

    @field_validator("name")
    @classmethod
    def name_must_not_be_empty(cls, v: str) -> str:
        """Ensure name is a non-empty, cross-platform filesystem-safe identifier.

        The forbidden set covers characters that are invalid in file names on
        Windows (``/ \\ : * ? " < > |``) so configs remain portable across
        operating systems.
        """
        if not v or not v.strip():
            raise ValueError("Exposure name must not be empty or whitespace")
        stripped = v.strip()
        forbidden = set('/\\:*?"<>|')
        if any(c in forbidden for c in stripped):
            raise ValueError(
                f"Exposure name contains forbidden characters: {forbidden}"
            )
        return stripped

    @field_validator("channel_name")
    @classmethod
    def normalize_channel_name(cls, value: str | None) -> str | None:
        """Normalize optional channel namespace by trimming whitespace."""
        if value is None:
            return None
        normalized = value.strip()
        return normalized or None

name_must_not_be_empty(v) classmethod

Ensure name is a non-empty, cross-platform filesystem-safe identifier.

The forbidden set covers characters that are invalid in file names on Windows (/ \ : * ? " < > |) so configs remain portable across operating systems.

Source code in llm_expose/config/models.py
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
@field_validator("name")
@classmethod
def name_must_not_be_empty(cls, v: str) -> str:
    """Ensure name is a non-empty, cross-platform filesystem-safe identifier.

    The forbidden set covers characters that are invalid in file names on
    Windows (``/ \\ : * ? " < > |``) so configs remain portable across
    operating systems.
    """
    if not v or not v.strip():
        raise ValueError("Exposure name must not be empty or whitespace")
    stripped = v.strip()
    forbidden = set('/\\:*?"<>|')
    if any(c in forbidden for c in stripped):
        raise ValueError(
            f"Exposure name contains forbidden characters: {forbidden}"
        )
    return stripped

normalize_channel_name(value) classmethod

Normalize optional channel namespace by trimming whitespace.

Source code in llm_expose/config/models.py
321
322
323
324
325
326
327
328
@field_validator("channel_name")
@classmethod
def normalize_channel_name(cls, value: str | None) -> str | None:
    """Normalize optional channel namespace by trimming whitespace."""
    if value is None:
        return None
    normalized = value.strip()
    return normalized or None

MCPConfig

Bases: BaseModel

Top-level MCP configuration persisted on disk.

Source code in llm_expose/config/models.py
183
184
185
186
187
class MCPConfig(BaseModel):
    """Top-level MCP configuration persisted on disk."""

    settings: MCPSettingsConfig = Field(default_factory=MCPSettingsConfig)
    servers: list[MCPServerConfig] = Field(default_factory=list)

MCPServerConfig

Bases: BaseModel

Configuration for a single MCP server integration.

Source code in llm_expose/config/models.py
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
class MCPServerConfig(BaseModel):
    """Configuration for a single MCP server integration."""

    name: str = Field(description="Unique MCP server name")
    transport: Literal["stdio", "sse", "http", "builtin"] = Field(
        default="stdio", description="Transport type used to connect to the MCP server"
    )
    command: str | None = Field(
        default=None,
        description="Command to run for stdio transport (e.g. 'npx', 'uvx')",
    )
    args: list[str] = Field(
        default_factory=list,
        description="Arguments passed to the command for stdio transport",
    )
    url: str | None = Field(
        default=None,
        description="Server URL for SSE or HTTP transport",
    )
    env: dict[str, str] = Field(
        default_factory=dict,
        description="Environment variables passed to the MCP server process",
    )
    allowed_tools: list[str] = Field(
        default_factory=list,
        description="Optional allow-list of MCP tool names available to the model",
    )
    enabled: bool = Field(default=True, description="Whether this server is enabled")
    tool_confirmation: Literal["required", "never", "default"] = Field(
        default="default",
        description="Tool confirmation mode: 'required' forces approval, 'never' auto-executes, 'default' uses global setting",
    )

    @field_validator("name")
    @classmethod
    def server_name_must_not_be_empty(cls, v: str) -> str:
        """Ensure server name is not blank."""
        if not v or not v.strip():
            raise ValueError("MCP server name must not be empty or whitespace")
        return v.strip()

    @field_validator("command")
    @classmethod
    def command_must_not_be_empty_when_present(cls, v: str | None) -> str | None:
        """Normalize optional command by trimming whitespace."""
        if v is None:
            return None
        stripped = v.strip()
        return stripped or None

    @field_validator("url")
    @classmethod
    def url_must_not_be_empty_when_present(cls, v: str | None) -> str | None:
        """Normalize optional URL by trimming whitespace."""
        if v is None:
            return None
        stripped = v.strip()
        return stripped or None

command_must_not_be_empty_when_present(v) classmethod

Normalize optional command by trimming whitespace.

Source code in llm_expose/config/models.py
142
143
144
145
146
147
148
149
@field_validator("command")
@classmethod
def command_must_not_be_empty_when_present(cls, v: str | None) -> str | None:
    """Normalize optional command by trimming whitespace."""
    if v is None:
        return None
    stripped = v.strip()
    return stripped or None

server_name_must_not_be_empty(v) classmethod

Ensure server name is not blank.

Source code in llm_expose/config/models.py
134
135
136
137
138
139
140
@field_validator("name")
@classmethod
def server_name_must_not_be_empty(cls, v: str) -> str:
    """Ensure server name is not blank."""
    if not v or not v.strip():
        raise ValueError("MCP server name must not be empty or whitespace")
    return v.strip()

url_must_not_be_empty_when_present(v) classmethod

Normalize optional URL by trimming whitespace.

Source code in llm_expose/config/models.py
151
152
153
154
155
156
157
158
@field_validator("url")
@classmethod
def url_must_not_be_empty_when_present(cls, v: str | None) -> str | None:
    """Normalize optional URL by trimming whitespace."""
    if v is None:
        return None
    stripped = v.strip()
    return stripped or None

MCPSettingsConfig

Bases: BaseModel

Global MCP runtime settings shared by all exposures.

Source code in llm_expose/config/models.py
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
class MCPSettingsConfig(BaseModel):
    """Global MCP runtime settings shared by all exposures."""

    confirmation_mode: Literal["required", "optional"] = Field(
        default="optional",
        description="Whether tool calls require explicit user confirmation",
    )
    tool_timeout_seconds: int = Field(
        default=30,
        ge=1,
        le=300,
        description="Max tool execution time in seconds",
    )
    expose_attachment_paths: bool = Field(
        default=False,
        description=(
            "Expose local absolute attachment paths in tool execution context. "
            "When disabled, only non-sensitive metadata is exposed."
        ),
    )

PairingsConfig

Bases: BaseModel

Top-level pairing configuration persisted on disk.

Pair IDs are stored by channel config name so each configured channel can have an independent allowlist of sender/channel identifiers.

Source code in llm_expose/config/models.py
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
class PairingsConfig(BaseModel):
    """Top-level pairing configuration persisted on disk.

    Pair IDs are stored by channel config name so each configured channel can
    have an independent allowlist of sender/channel identifiers.
    """

    pairs_by_channel: dict[str, list[str]] = Field(default_factory=dict)

    @field_validator("pairs_by_channel")
    @classmethod
    def normalize_pairs_by_channel(
        cls, values: dict[str, list[str]]
    ) -> dict[str, list[str]]:
        """Trim and deduplicate channel names and pair IDs."""
        normalized: dict[str, list[str]] = {}
        for raw_channel_name, raw_pair_ids in values.items():
            channel_name = raw_channel_name.strip()
            if not channel_name:
                continue

            cleaned_ids: list[str] = []
            seen_ids: set[str] = set()
            for raw_pair_id in raw_pair_ids:
                pair_id = raw_pair_id.strip()
                if not pair_id or pair_id in seen_ids:
                    continue
                seen_ids.add(pair_id)
                cleaned_ids.append(pair_id)

            normalized[channel_name] = cleaned_ids
        return normalized

normalize_pairs_by_channel(values) classmethod

Trim and deduplicate channel names and pair IDs.

Source code in llm_expose/config/models.py
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
@field_validator("pairs_by_channel")
@classmethod
def normalize_pairs_by_channel(
    cls, values: dict[str, list[str]]
) -> dict[str, list[str]]:
    """Trim and deduplicate channel names and pair IDs."""
    normalized: dict[str, list[str]] = {}
    for raw_channel_name, raw_pair_ids in values.items():
        channel_name = raw_channel_name.strip()
        if not channel_name:
            continue

        cleaned_ids: list[str] = []
        seen_ids: set[str] = set()
        for raw_pair_id in raw_pair_ids:
            pair_id = raw_pair_id.strip()
            if not pair_id or pair_id in seen_ids:
                continue
            seen_ids.add(pair_id)
            cleaned_ids.append(pair_id)

        normalized[channel_name] = cleaned_ids
    return normalized

ProviderConfig

Bases: BaseModel

Configuration for an LLM provider.

Source code in llm_expose/config/models.py
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class ProviderConfig(BaseModel):
    """Configuration for an LLM provider."""

    provider_name: str = Field(
        description="Provider name, e.g. 'openai', 'local', 'anthropic'"
    )
    model: str = Field(description="Model identifier, e.g. 'gpt-4o', 'llama3'")
    api_key: str | None = Field(
        default=None,
        description="API key for the provider (not required for local models)",
    )
    base_url: str | None = Field(
        default=None,
        description="Base URL for local or self-hosted models (e.g. LM Studio, Ollama proxy)",
    )
    supports_vision: bool | None = Field(
        default=None,
        description=(
            "Override model vision capability detection. "
            "When unset, provider attempts auto-detection."
        ),
    )

    @field_validator("provider_name", "model")
    @classmethod
    def must_not_be_empty(cls, v: str) -> str:
        """Ensure required string fields are not blank."""
        if not v or not v.strip():
            raise ValueError("Field must not be empty or whitespace")
        return v.strip()

must_not_be_empty(v) classmethod

Ensure required string fields are not blank.

Source code in llm_expose/config/models.py
33
34
35
36
37
38
39
@field_validator("provider_name", "model")
@classmethod
def must_not_be_empty(cls, v: str) -> str:
    """Ensure required string fields are not blank."""
    if not v or not v.strip():
        raise ValueError("Field must not be empty or whitespace")
    return v.strip()

TelegramClientConfig

Bases: BaseModel

Configuration for the Telegram client adapter.

Source code in llm_expose/config/models.py
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
class TelegramClientConfig(BaseModel):
    """Configuration for the Telegram client adapter."""

    client_type: Literal["telegram"] = "telegram"
    bot_token: str = Field(description="Telegram bot token from @BotFather")
    mcp_servers: list[str] = Field(
        default_factory=list,
        description="List of MCP server names attached to this channel",
    )
    system_prompt_path: str | None = Field(
        default=None,
        description="Path to a file containing the custom system prompt for this channel. Falls back to default when omitted.",
    )
    model_name: str | None = Field(
        default=None,
        description="Default LLM model name for this channel (used with --llm-completion)",
    )

    @field_validator("bot_token")
    @classmethod
    def token_must_not_be_empty(cls, v: str) -> str:
        """Ensure bot token is not blank."""
        if not v or not v.strip():
            raise ValueError("bot_token must not be empty or whitespace")
        return v.strip()

    @field_validator("mcp_servers")
    @classmethod
    def normalize_mcp_servers(cls, values: list[str]) -> list[str]:
        """Normalize attached MCP server names preserving order and uniqueness."""
        normalized: list[str] = []
        seen: set[str] = set()
        for value in values:
            name = value.strip()
            if not name or name in seen:
                continue
            seen.add(name)
            normalized.append(name)
        return normalized

    @field_validator("system_prompt_path")
    @classmethod
    def validate_system_prompt_path(cls, value: str | None) -> str | None:
        """Validate system prompt path by trimming outer whitespace."""
        if value is None:
            return None
        normalized = value.strip()
        return normalized or None

    @field_validator("model_name")
    @classmethod
    def normalize_model_name(cls, value: str | None) -> str | None:
        """Normalize optional model name by trimming outer whitespace."""
        if value is None:
            return None
        normalized = value.strip()
        return normalized or None

normalize_mcp_servers(values) classmethod

Normalize attached MCP server names preserving order and uniqueness.

Source code in llm_expose/config/models.py
68
69
70
71
72
73
74
75
76
77
78
79
80
@field_validator("mcp_servers")
@classmethod
def normalize_mcp_servers(cls, values: list[str]) -> list[str]:
    """Normalize attached MCP server names preserving order and uniqueness."""
    normalized: list[str] = []
    seen: set[str] = set()
    for value in values:
        name = value.strip()
        if not name or name in seen:
            continue
        seen.add(name)
        normalized.append(name)
    return normalized

normalize_model_name(value) classmethod

Normalize optional model name by trimming outer whitespace.

Source code in llm_expose/config/models.py
91
92
93
94
95
96
97
98
@field_validator("model_name")
@classmethod
def normalize_model_name(cls, value: str | None) -> str | None:
    """Normalize optional model name by trimming outer whitespace."""
    if value is None:
        return None
    normalized = value.strip()
    return normalized or None

token_must_not_be_empty(v) classmethod

Ensure bot token is not blank.

Source code in llm_expose/config/models.py
60
61
62
63
64
65
66
@field_validator("bot_token")
@classmethod
def token_must_not_be_empty(cls, v: str) -> str:
    """Ensure bot token is not blank."""
    if not v or not v.strip():
        raise ValueError("bot_token must not be empty or whitespace")
    return v.strip()

validate_system_prompt_path(value) classmethod

Validate system prompt path by trimming outer whitespace.

Source code in llm_expose/config/models.py
82
83
84
85
86
87
88
89
@field_validator("system_prompt_path")
@classmethod
def validate_system_prompt_path(cls, value: str | None) -> str | None:
    """Validate system prompt path by trimming outer whitespace."""
    if value is None:
        return None
    normalized = value.strip()
    return normalized or None

llm_expose.config.loader

Configuration loading and saving utilities for llm-expose.

add_pair(channel_name, pair_id)

Add a pair ID to a channel's pairing allowlist.

Source code in llm_expose/config/loader.py
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
def add_pair(channel_name: str, pair_id: str) -> Path:
    """Add a pair ID to a channel's pairing allowlist."""
    normalized_channel_name = channel_name.strip()
    normalized_pair_id = pair_id.strip()
    if not normalized_channel_name:
        raise ValueError("channel_name must not be empty or whitespace")
    if not normalized_pair_id:
        raise ValueError("pair_id must not be empty or whitespace")

    config = load_pairings_config()
    existing = config.pairs_by_channel.get(normalized_channel_name, [])
    if normalized_pair_id not in existing:
        config.pairs_by_channel[normalized_channel_name] = [
            *existing,
            normalized_pair_id,
        ]
    return save_pairings_config(config)

delete_channel(name)

Delete a saved channel config by name.

Parameters:

Name Type Description Default
name str

The name of the channel config to delete.

required

Raises:

Type Description
FileNotFoundError

If no channel with the given name exists.

Source code in llm_expose/config/loader.py
230
231
232
233
234
235
236
237
238
239
240
241
242
def delete_channel(name: str) -> None:
    """Delete a saved channel config by name.

    Args:
        name: The name of the channel config to delete.

    Raises:
        FileNotFoundError: If no channel with the given name exists.
    """
    path = get_channels_dir() / f"{name}.yaml"
    if not path.exists():
        raise FileNotFoundError(f"No channel configuration named '{name}' found")
    path.unlink()

delete_mcp_server(name)

Delete a configured MCP server by name.

Raises:

Type Description
FileNotFoundError

If no MCP server with the given name exists.

Source code in llm_expose/config/loader.py
307
308
309
310
311
312
313
314
315
316
317
318
def delete_mcp_server(name: str) -> None:
    """Delete a configured MCP server by name.

    Raises:
        FileNotFoundError: If no MCP server with the given name exists.
    """
    config = _load_persisted_mcp_config()
    original_count = len(config.servers)
    config.servers = [server for server in config.servers if server.name != name]
    if len(config.servers) == original_count:
        raise FileNotFoundError(f"No MCP server named '{name}' found")
    save_mcp_config(config)

delete_model(name)

Delete a saved model config by name.

Parameters:

Name Type Description Default
name str

The name of the model config to delete.

required

Raises:

Type Description
FileNotFoundError

If no model with the given name exists.

Source code in llm_expose/config/loader.py
152
153
154
155
156
157
158
159
160
161
162
163
164
def delete_model(name: str) -> None:
    """Delete a saved model config by name.

    Args:
        name: The name of the model config to delete.

    Raises:
        FileNotFoundError: If no model with the given name exists.
    """
    path = get_models_dir() / f"{name}.yaml"
    if not path.exists():
        raise FileNotFoundError(f"No model configuration named '{name}' found")
    path.unlink()

delete_pair(channel_name, pair_id)

Delete a pair ID from a channel's pairing allowlist.

Raises:

Type Description
FileNotFoundError

If the channel/pair entry does not exist.

Source code in llm_expose/config/loader.py
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
def delete_pair(channel_name: str, pair_id: str) -> Path:
    """Delete a pair ID from a channel's pairing allowlist.

    Raises:
        FileNotFoundError: If the channel/pair entry does not exist.
    """
    normalized_channel_name = channel_name.strip()
    normalized_pair_id = pair_id.strip()
    if not normalized_channel_name:
        raise ValueError("channel_name must not be empty or whitespace")
    if not normalized_pair_id:
        raise ValueError("pair_id must not be empty or whitespace")

    config = load_pairings_config()
    existing = config.pairs_by_channel.get(normalized_channel_name, [])
    if normalized_pair_id not in existing:
        raise FileNotFoundError(
            f"No pair '{normalized_pair_id}' found for channel '{normalized_channel_name}'"
        )

    updated = [value for value in existing if value != normalized_pair_id]
    if updated:
        config.pairs_by_channel[normalized_channel_name] = updated
    else:
        config.pairs_by_channel.pop(normalized_channel_name, None)

    return save_pairings_config(config)

get_base_dir()

Return the base directory used to store all configs.

The directory is resolved from the LLM_EXPOSE_CONFIG_DIR environment variable when set, otherwise defaults to ~/.llm-expose.

Source code in llm_expose/config/loader.py
61
62
63
64
65
66
67
68
69
70
def get_base_dir() -> Path:
    """Return the base directory used to store all configs.

    The directory is resolved from the ``LLM_EXPOSE_CONFIG_DIR`` environment
    variable when set, otherwise defaults to ``~/.llm-expose``.
    """
    env_dir = os.environ.get("LLM_EXPOSE_CONFIG_DIR")
    if env_dir:
        return Path(env_dir)
    return _DEFAULT_BASE_DIR

get_builtin_mcp_servers()

Return builtin MCP server definitions available to all installs.

Source code in llm_expose/config/loader.py
25
26
27
28
29
30
31
32
def get_builtin_mcp_servers() -> list[MCPServerConfig]:
    """Return builtin MCP server definitions available to all installs."""
    return [
        MCPServerConfig(
            name="builtin-core",
            transport="builtin",
        )
    ]

get_channels_dir()

Return the directory used to store channel configs.

Source code in llm_expose/config/loader.py
78
79
80
def get_channels_dir() -> Path:
    """Return the directory used to store channel configs."""
    return get_base_dir() / "channels"

get_mcp_config_path()

Return the file path used to store MCP server settings/config.

Source code in llm_expose/config/loader.py
83
84
85
def get_mcp_config_path() -> Path:
    """Return the file path used to store MCP server settings/config."""
    return get_base_dir() / "mcp_servers.yaml"

get_mcp_server(name)

Return a single MCP server config by name.

Raises:

Type Description
FileNotFoundError

If no MCP server with the given name exists.

Source code in llm_expose/config/loader.py
280
281
282
283
284
285
286
287
288
289
290
def get_mcp_server(name: str) -> MCPServerConfig:
    """Return a single MCP server config by name.

    Raises:
        FileNotFoundError: If no MCP server with the given name exists.
    """
    config = load_mcp_config()
    for server in config.servers:
        if server.name == name:
            return server
    raise FileNotFoundError(f"No MCP server named '{name}' found")

get_models_dir()

Return the directory used to store model configs.

Source code in llm_expose/config/loader.py
73
74
75
def get_models_dir() -> Path:
    """Return the directory used to store model configs."""
    return get_base_dir() / "models"

get_pairs_config_path()

Return the file path used to store channel pairing settings.

Source code in llm_expose/config/loader.py
88
89
90
def get_pairs_config_path() -> Path:
    """Return the file path used to store channel pairing settings."""
    return get_base_dir() / "pairs.yaml"

get_pairs_for_channel(channel_name)

Return the pair IDs configured for a specific channel config name.

Source code in llm_expose/config/loader.py
378
379
380
381
382
383
384
385
def get_pairs_for_channel(channel_name: str) -> list[str]:
    """Return the pair IDs configured for a specific channel config name."""
    normalized_channel_name = channel_name.strip()
    if not normalized_channel_name:
        raise ValueError("channel_name must not be empty or whitespace")

    config = load_pairings_config()
    return config.pairs_by_channel.get(normalized_channel_name, []).copy()

list_channels()

Return names of all saved channel configs.

Returns:

Type Description
list[str]

A sorted list of channel config names.

Source code in llm_expose/config/loader.py
218
219
220
221
222
223
224
225
226
227
def list_channels() -> list[str]:
    """Return names of all saved channel configs.

    Returns:
        A sorted list of channel config names.
    """
    directory = get_channels_dir()
    if not directory.exists():
        return []
    return sorted(p.stem for p in directory.glob("*.yaml"))

list_mcp_servers()

Return names of configured MCP servers.

Source code in llm_expose/config/loader.py
274
275
276
277
def list_mcp_servers() -> list[str]:
    """Return names of configured MCP servers."""
    config = load_mcp_config()
    return sorted(server.name for server in config.servers)

list_models()

Return names of all saved model configs.

Returns:

Type Description
list[str]

A sorted list of model config names.

Source code in llm_expose/config/loader.py
140
141
142
143
144
145
146
147
148
149
def list_models() -> list[str]:
    """Return names of all saved model configs.

    Returns:
        A sorted list of model config names.
    """
    directory = get_models_dir()
    if not directory.exists():
        return []
    return sorted(p.stem for p in directory.glob("*.yaml"))

list_pairs(channel_name=None)

Return channel-scoped pair IDs.

Parameters:

Name Type Description Default
channel_name str | None

Optional channel name to filter by.

None
Source code in llm_expose/config/loader.py
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
def list_pairs(channel_name: str | None = None) -> dict[str, list[str]]:
    """Return channel-scoped pair IDs.

    Args:
        channel_name: Optional channel name to filter by.
    """
    config = load_pairings_config()
    if channel_name is None:
        return config.pairs_by_channel

    normalized_channel_name = channel_name.strip()
    return {
        normalized_channel_name: config.pairs_by_channel.get(
            normalized_channel_name, []
        )
    }

load_channel(name)

Load a channel configuration from disk by name.

Parameters:

Name Type Description Default
name str

The name used when the channel was saved.

required

Returns:

Type Description
ClientConfig

The deserialized client config (routed via client_type discriminator).

Raises:

Type Description
FileNotFoundError

If no channel with the given name exists.

ValueError

If the YAML is malformed or validation fails.

Source code in llm_expose/config/loader.py
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
def load_channel(name: str) -> ClientConfig:
    """Load a channel configuration from disk by name.

    Args:
        name: The name used when the channel was saved.

    Returns:
        The deserialized client config (routed via ``client_type`` discriminator).

    Raises:
        FileNotFoundError: If no channel with the given name exists.
        ValueError: If the YAML is malformed or validation fails.
    """
    path = get_channels_dir() / f"{name}.yaml"
    if not path.exists():
        raise FileNotFoundError(f"No channel configuration named '{name}' found")
    with path.open("r", encoding="utf-8") as fh:
        data = yaml.safe_load(fh)
    # Remove the 'name' key as it's not part of any ClientConfig model
    data.pop("name", None)
    # Pydantic's discriminated union routes via client_type field
    from pydantic import TypeAdapter

    _adapter: TypeAdapter[ClientConfig] = TypeAdapter(ClientConfig)
    return _adapter.validate_python(data)

load_mcp_config()

Load MCP configuration from disk.

Returns defaults when the config file does not exist yet.

Source code in llm_expose/config/loader.py
250
251
252
253
254
255
def load_mcp_config() -> MCPConfig:
    """Load MCP configuration from disk.

    Returns defaults when the config file does not exist yet.
    """
    return _merge_builtin_mcp_servers(_load_persisted_mcp_config())

load_mcp_settings()

Return global MCP runtime settings.

Source code in llm_expose/config/loader.py
321
322
323
def load_mcp_settings() -> MCPSettingsConfig:
    """Return global MCP runtime settings."""
    return load_mcp_config().settings

load_model(name)

Load a model configuration from disk by name.

Parameters:

Name Type Description Default
name str

The name used when the model was saved.

required

Returns:

Type Description
ProviderConfig

The deserialized :class:ProviderConfig.

Raises:

Type Description
FileNotFoundError

If no model with the given name exists.

ValueError

If the YAML is malformed or validation fails.

Source code in llm_expose/config/loader.py
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
def load_model(name: str) -> ProviderConfig:
    """Load a model configuration from disk by name.

    Args:
        name: The name used when the model was saved.

    Returns:
        The deserialized :class:`ProviderConfig`.

    Raises:
        FileNotFoundError: If no model with the given name exists.
        ValueError: If the YAML is malformed or validation fails.
    """
    path = get_models_dir() / f"{name}.yaml"
    if not path.exists():
        raise FileNotFoundError(f"No model configuration named '{name}' found")
    with path.open("r", encoding="utf-8") as fh:
        data = yaml.safe_load(fh)
    # Remove the 'name' key as it's not part of ProviderConfig
    data.pop("name", None)
    return ProviderConfig.model_validate(data)

load_pairings_config()

Load channel pairing configuration from disk.

Returns defaults when the config file does not exist yet.

Source code in llm_expose/config/loader.py
338
339
340
341
342
343
344
345
346
347
348
def load_pairings_config() -> PairingsConfig:
    """Load channel pairing configuration from disk.

    Returns defaults when the config file does not exist yet.
    """
    path = get_pairs_config_path()
    if not path.exists():
        return PairingsConfig()
    with path.open("r", encoding="utf-8") as fh:
        data = yaml.safe_load(fh) or {}
    return PairingsConfig.model_validate(data)

save_channel(name, config)

Persist a channel configuration to a YAML file.

Parameters:

Name Type Description Default
name str

Unique name for this channel configuration.

required
config TelegramClientConfig | DiscordClientConfig

The client config instance to save (Telegram or Discord).

required

Returns:

Name Type Description
The Path

class:~pathlib.Path where the config was written.

Source code in llm_expose/config/loader.py
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
def save_channel(name: str, config: TelegramClientConfig | DiscordClientConfig) -> Path:
    """Persist a channel configuration to a YAML file.

    Args:
        name: Unique name for this channel configuration.
        config: The client config instance to save (Telegram or Discord).

    Returns:
        The :class:`~pathlib.Path` where the config was written.
    """
    directory = get_channels_dir()
    directory.mkdir(parents=True, exist_ok=True)
    path = directory / f"{name}.yaml"
    data = {"name": name, **config.model_dump()}
    with path.open("w", encoding="utf-8") as fh:
        yaml.safe_dump(data, fh, allow_unicode=True, sort_keys=False)
    return path

save_mcp_config(config)

Persist MCP configuration to disk.

Parameters:

Name Type Description Default
config MCPConfig

Full MCP configuration object.

required

Returns:

Type Description
Path

The path where the YAML file was written.

Source code in llm_expose/config/loader.py
258
259
260
261
262
263
264
265
266
267
268
269
270
271
def save_mcp_config(config: MCPConfig) -> Path:
    """Persist MCP configuration to disk.

    Args:
        config: Full MCP configuration object.

    Returns:
        The path where the YAML file was written.
    """
    path = get_mcp_config_path()
    path.parent.mkdir(parents=True, exist_ok=True)
    with path.open("w", encoding="utf-8") as fh:
        yaml.safe_dump(config.model_dump(), fh, allow_unicode=True, sort_keys=False)
    return path

save_mcp_server(server)

Create or update a single MCP server entry by name.

Source code in llm_expose/config/loader.py
293
294
295
296
297
298
299
300
301
302
303
304
def save_mcp_server(server: MCPServerConfig) -> Path:
    """Create or update a single MCP server entry by name."""
    config = _load_persisted_mcp_config()
    updated = False
    for idx, current in enumerate(config.servers):
        if current.name == server.name:
            config.servers[idx] = server
            updated = True
            break
    if not updated:
        config.servers.append(server)
    return save_mcp_config(config)

save_mcp_settings(settings)

Persist only global MCP runtime settings.

Source code in llm_expose/config/loader.py
326
327
328
329
330
def save_mcp_settings(settings: MCPSettingsConfig) -> Path:
    """Persist only global MCP runtime settings."""
    config = _load_persisted_mcp_config()
    config.settings = settings
    return save_mcp_config(config)

save_model(name, config)

Persist a model configuration to a YAML file.

Parameters:

Name Type Description Default
name str

Unique name for this model configuration.

required
config ProviderConfig

The :class:ProviderConfig instance to save.

required

Returns:

Name Type Description
The Path

class:~pathlib.Path where the config was written.

Source code in llm_expose/config/loader.py
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
def save_model(name: str, config: ProviderConfig) -> Path:
    """Persist a model configuration to a YAML file.

    Args:
        name: Unique name for this model configuration.
        config: The :class:`ProviderConfig` instance to save.

    Returns:
        The :class:`~pathlib.Path` where the config was written.
    """
    directory = get_models_dir()
    directory.mkdir(parents=True, exist_ok=True)
    path = directory / f"{name}.yaml"
    data = {"name": name, **config.model_dump()}
    with path.open("w", encoding="utf-8") as fh:
        yaml.safe_dump(data, fh, allow_unicode=True, sort_keys=False)
    return path

save_pairings_config(config)

Persist channel pairing configuration to disk.

Source code in llm_expose/config/loader.py
351
352
353
354
355
356
357
def save_pairings_config(config: PairingsConfig) -> Path:
    """Persist channel pairing configuration to disk."""
    path = get_pairs_config_path()
    path.parent.mkdir(parents=True, exist_ok=True)
    with path.open("w", encoding="utf-8") as fh:
        yaml.safe_dump(config.model_dump(), fh, allow_unicode=True, sort_keys=False)
    return path