{% raw %}
# Console API

[English](console-api.md) | [简体中文](console-api.zh-CN.md)

## Scope

Console endpoints are mounted under `/console/api`.

Typical flow:

1. `POST /console/api/login`
2. Receive a bearer session token
3. Use that token for subsequent console requests

## Endpoints

### Auth

- `POST /console/api/login` — authenticate with console secret, returns `{ "token": "..." }`
- `POST /console/api/logout` — revoke current session
- `POST /console/api/auth/revoke-all-sessions` — revoke all active sessions. Useful after rotating the console password to force all logged-in users to re-authenticate.

### Monitoring (read-only)

- `GET /console/api/providers` — per-protocol credential summary: `name` (protocol display name), `total`, `active`, `error`
- `GET /console/api/upstreams` — list all upstreams with full configuration (all fields including `api_keys`, `headers`, `user_agent`, `request_payload`, `request_transform_before`, `request_transform_after`, `response_transform`, `script_error_mode`, `count_token`, `enabled`, `status`). The `status` field is computed from auth store state. API keys are returned in masked format (first/last 4 characters visible). When editing an upstream, the frontend calls `GET /upstreams/{name}/keys` to retrieve the full keys. Supports pagination via `page` and `per_page` query params.
- `GET /console/api/upstreams/names` — list all upstream names (for dropdowns)
- `GET /console/api/models` — list all models with full configuration (all fields including `upstream`, `upstream_model_id`, `upstream_inference_profile`, `base_url`, `aliases`, `protocol`, `protocol_label`, `request_payload`, `direct_path`, `request_transform_before`, `request_transform_after`, `response_transform`, `script_error_mode`, `count_token`, `enabled`, `hide_name`). Supports pagination via `page` and `per_page` query params.
- `GET /console/api/models/names` — list all model names (for dropdowns)
- `GET /console/api/rate-limit-status` — rate limiter snapshots: `upstream`, `model`, `config`, `rpm_tokens`, `concurrency_permits`, `tpm_tokens`, `ebp_ceiling`, `ebp_cooloff`
- `GET /console/api/stats` — summary statistics: `total_upstreams`, `active_protocols`, `active_models`, `active_access_keys`
- `GET /console/api/request-log` — paginated request log history (query params: `page`, `per_page`)
- `GET /console/api/logs/stream` — SSE stream of real-time request logs. Requires a valid session token passed as `?token=<session_token>` query parameter.
- `GET /console/api/statistics` — persistent request statistics (SQLite/PostgreSQL), query params: `from` (Unix timestamp), `to` (Unix timestamp), `group_by` (`model` / `access_key` / `access_key_group` / `model_access_key` / `model_access_key_group` / `model_access_key_access_key_group`). The `model` field uses `[upstream_name]::[upstream_model_id]` format.
- `GET /console/api/statistics/timeseries` — time-series data for charts (SQLite/PostgreSQL), query params: `preset` (`12h` / `24h` / `7d`, default `12h`) or `from` / `to` (Unix timestamps). Returns `{ data: {...}, from: <timestamp>, to: <timestamp> }`. Granularity is auto-selected based on time range (60s for 12h, 300s for 24h, 3600s for 7d).
- `GET /console/api/check-status` — License authorization status (commercial builds only). Returns `{ "valid": bool, "expired": bool, "message": string, "customer": string|null, "memory_cap_mb": number|null, "restrict_distributed": bool }`. `memory_cap_mb` is `null` when licensed (no cap), non-null when unlicensed (current cap in MB). `restrict_distributed` is `true` when unlicensed (Redis and PostgreSQL are blocked, single-instance mode only). OSS builds return 404; the frontend banner auto-hides.

### Admin

- `POST /console/api/scripts/test` — test run a JavaScript, returns execution result

### Access Key Management (fine-grained API)

These endpoints modify the database (SQLite/PostgreSQL) with optimistic locking and automatic hot-reload via polling.

- `GET /console/api/access-keys` — list all access keys, returns `api_key`, `name`, `groups`, `enabled`, `anonymous` fields. API keys are returned in masked format (first/last 4 characters visible, e.g. `sk-a***xxxx`) to avoid exposing full secrets in listings.
- `GET /console/api/access-keys/{name}/api-key` — retrieve the full API key for a specific access key (requires auth). Called when the frontend "Copy" button is clicked; the full key is immediately copied to clipboard and cleared from memory. Anonymous access keys (without a `name`) are not supported.
- `PUT /console/api/access-keys/{name}/api-key` — update the API key for a specific access key by `name`, body: `{ "api_key": "..." }`. Validates that the new API key is not already in use by another access key. Response `api_key` is masked. Anonymous access keys can have their API key updated, though it has no effect on authentication.
- `POST /console/api/access-keys` — create an access key, body: `{ "api_key": "...", "name": "...", "groups": ["..."] }`. `groups` is a string array (one access key may belong to multiple groups); omit or use `[]` for no group. Response `api_key` is masked.
- `PUT /console/api/access-keys` — update an existing access key by `api_key`, body: `{ "api_key": "...", "name": "...", "groups": ["..."] }`. Response `api_key` is masked.
- `DELETE /console/api/access-keys` — delete an access key, body: `{ "name": "..." }`
- `POST /console/api/access-keys/batch` — atomically bulk-update multiple access keys (single transaction + single hot-reload; any validation or authorization failure rolls back the whole batch with no partial application). Body: `{ "names": ["..."], "groups_op": { "mode": "set" | "add" | "remove", "groups": ["..."] }, "enabled": true | false }`. `names` must be non-empty, and at least one of `groups_op` / `enabled` must be provided. `groups_op.mode` semantics: `set` replaces the existing groups with `groups`; `add` unions `groups` into the existing groups, deduplicating while preserving the original order then appending new ones; `remove` drops the listed groups from the existing groups; `enabled` batch-enables/disables. Per-row validation is identical to single edit (`PUT /console/api/access-keys`): group-name length, the combined `name`/`api_key` uniqueness and anonymous-singleton rules, and group-reference existence — any failure returns 422 and nothing is written. **RBAC**: admins may batch-change any key's groups and enabled state; normal users may only batch enable/disable keys they created — a request carrying `groups_op` returns 403 wholesale, as does any request touching another user's key. Response: `{ "message": "...", "updated": <n> }`. Error codes: 400 missing params or empty `names`, 403 unauthorized, 404 a name in `names` does not exist, 409 optimistic-lock version conflict, 422 validation failure.
- `POST /console/api/access-key-groups` — create a group, body: `{ "name": "...", "models": [...], "load_balancers": [...] }`. `models` and `load_balancers` are two independent dimensions: `"*"` in a dimension allows everything in that dimension, an empty array denies everything in it. `models` accepts model names (or `"*"`) only; `load_balancers` accepts load balancer names (or `"*"`) only, with aliases canonicalized on write. **Hard cut**: putting a load balancer name in `models` returns 422 (the legacy mixed-list style is no longer accepted).
- `PUT /console/api/access-key-groups` — update an existing group by `name`, same body as create. Both the response and the list endpoint include the `models` and `load_balancers` fields.
- `DELETE /console/api/access-key-groups` — delete a group, body: `{ "name": "..." }`

### Upstream Management (fine-grained API)

These endpoints modify the database (SQLite/PostgreSQL) with optimistic locking and automatic hot-reload via polling.

- `GET /console/api/upstreams/{name}/keys` — retrieve full API keys for a specific upstream (requires auth). Called when opening the upstream edit modal to populate the form with real keys instead of masked ones.
- `POST /console/api/upstreams` — create an upstream, body: `{ name, protocol, base_url, api_keys: [{key, weight}], headers: {}, user_agent, dns_servers: ["ip" | "tls://ip" | "https://ip", ...], request_payload: [{path, value, mode}], request_transform_before, request_transform_after, response_transform, script_error_mode, count_token, enabled, extra_config: {...} }` (`dns_servers` appears both top-level and under `extra_config`; the DB persistence path is `extra_config`. Protocol and TLS SNI are auto-inferred from the first URI: `tls://` → TLS, `https://` → HTTPS, plain IP → UDP. Well-known providers like Cloudflare, Google, Alibaba, and Quad9 have auto-detected SNI hostnames)
- `PUT /console/api/upstreams` — update an existing upstream by `name`, same body as POST. The `name` field is immutable.
- `DELETE /console/api/upstreams` — delete an upstream, body: `{ name: "..." }`. Returns 422 if any models reference this upstream.

Protocol values: `openai`, `openai_response`, `openai_images`, `openai_embeddings`, `openai_audio`, `openai_rerank`, `anthropic`, `google`, `aws_converse`, `aws_invoke`, `dashscope`, `passthrough`.

Transform scripts: a string for inline scripts, or `{ file: "path.js" }` for file references.

Payload rule modes: `overwrite`, `remove`, `add-if-absent`, `append`, `append-if-missing`, `remove-matching`, `strip-lines`, `filter-content-types`, `filter-tools`. In value-writing modes, a string `value` containing `{{...}}` is resolved as an expression: alternatives joined by `|` form a fallback chain whose first non-empty result wins, and an all-empty chain omits the write. Available variables are `header:Name` (case-insensitive; leading/trailing whitespace counts as absent), `request_id`, `request_access_key_name`, `request_access_key_group`, and `system_prompt_hash` (`access_key_hash` is empty in this context). The typical use is injecting a dynamic cache key such as `prompt_cache_key` via `{{header:x-claude-code-session-id|system_prompt_hash}}`.

See Monitoring section above for read-only listing endpoints (`/upstreams`, `/upstreams/names`).

### Model Management (fine-grained API)

These endpoints modify the database (SQLite/PostgreSQL) with optimistic locking and automatic hot-reload via polling.

- `POST /console/api/models` — create a model, body: `{ name, upstream: [upstream_name], upstream_model_id?, upstream_inference_profile?, base_url?, aliases?, protocol?, direct_path?, request_payload?, request_transform_before?, request_transform_after?, response_transform?, script_error_mode?, count_token?, enabled?, hide_name? }`
- `PUT /console/api/models` — update an existing model by `name`, same body as POST. The `name` field is immutable.
- `DELETE /console/api/models` — delete a model, body: `{ name: "..." }`. Uses lenient parsing so delete works even on already-invalid configs; validates the result before saving.

See Monitoring section above for read-only listing endpoints (`/models`, `/models/names`).

### Load Balancer Management (DB-only)

These endpoints manage load balancers stored in the database (SQLite/PostgreSQL). Load balancers are **not** supported in file-based config — they are DB-only.

- `GET /console/api/load-balancers` — list all load balancers, each with `name`, `entries` (array of `{upstream, model, weight}`), `enabled`, `retry_on_different_node`, `binding_ttl_secs`, `binding_key_template`, `sticky_session_enabled`
- `GET /console/api/load-balancers/names` — list all load balancer names (for dropdowns)
- `POST /console/api/load-balancers` — create a load balancer, body: `{ name, entries: [{upstream, model, weight}], enabled?, retry_on_different_node?, binding_ttl_secs?, binding_key_template?, sticky_session_enabled? }`. `sticky_session_enabled` defaults to `true`; `binding_ttl_secs` only takes effect when `sticky_session_enabled=true`.
- `PUT /console/api/load-balancers` — update an existing load balancer by `name`, same body as POST. The `name` field is immutable. Existing static bindings are preserved. Updating an LB automatically clears all dynamic dynamic bindings so clients are re-balanced.
- `DELETE /console/api/load-balancers` — delete a load balancer, body: `{ name: "..." }`. Static bindings are cascade-deleted automatically. Orphaned dynamic sessions are cleaned up.
- `GET /console/api/load-balancers/{name}/status` — LB runtime status: `entries` (array with `upstream`, `model`, `config_weight`, `effective_weight`, `status`), `sticky_sessions` (dynamic bindings with `binding_key` (masked), `binding_key_hash` (HMAC-SHA256 hex, used as URL identifier), `display_name`, `access_key_group`, `upstream`, `model`, `last_seen`; automatically filtered to exclude entries colliding with static bindings, guaranteeing union uniqueness), `static_bindings` (same shape as sticky_sessions plus `created_at`, `updated_at`, `is_static`), and `total_sessions` (deduplicated count).
- `DELETE /console/api/load-balancers/{name}/dynamic-bindings` — clear all dynamic bindings for this LB. Returns `{ deleted: <count> }`.
- `DELETE /console/api/load-balancers/{name}/dynamic-bindings/{binding_key_hash}` — delete a single dynamic binding by its HMAC-SHA256 hash. Returns `{ deleted: true }` or 404 if the hash doesn't match any active binding.
- `PATCH /console/api/load-balancers/{name}/dynamic-bindings/{binding_key_hash}` — reassign a dynamic binding to a different upstream+model, body: `{ upstream: "...", model: "..." }`. Uses the LB's `binding_ttl_secs` for the new binding (falls back to permanent/0 if LB not found). Returns `{ updated: true }` or 404 if the binding no longer exists.
- `POST /console/api/load-balancers/{name}/dynamic-bindings/{binding_key_hash}/pin` — convert a dynamic binding into a persistent static binding. Optionally override upstream/model in the body (`{ upstream?, model? }`). Uses the binding's current upstream/model if not overridden. Returns 409 if the binding_key already has a static binding.

#### Static Bindings

Static bindings assign a specific sticky key (API key or header value) to a fixed upstream+model pair, bypassing the WRR algorithm entirely. When the bound upstream is unavailable (excluded), the request fails with 503 (admin intent: this key only goes to this upstream). 429 rate limits also do not trigger automatic failover for static bindings — the 429 is returned directly to the client. Sticky keys are encrypted at rest (AES-256-GCM) with HMAC blind index for uniqueness.

- `GET /console/api/load-balancers/{name}/static-bindings` — list all static bindings for a LB. `binding_key` is masked in the response; `binding_key_hash` (HMAC-SHA256 hex) is included for use as URL identifier.
- `POST /console/api/load-balancers/{name}/static-bindings` — create a static binding, body: `{ binding_key, upstream, model }`. Returns 409 if `binding_key` already exists, 422 if `(upstream, model)` is not in the LB entries. Automatically removes any colliding dynamic binding on success (self-heals via next LB update if removal fails).
- `PUT /console/api/load-balancers/{name}/static-bindings/{binding_key_hash}` — update a static binding (change upstream/model, not binding_key). `{binding_key_hash}` is the HMAC-SHA256 hash of the binding key. body: `{ upstream, model }`
- `DELETE /console/api/load-balancers/{name}/static-bindings/{binding_key_hash}` — delete a static binding and clean up any fallback dynamic bindings. `{binding_key_hash}` is the HMAC-SHA256 hash of the binding key.

### Model Metadata Management (DB Override Layer)

These endpoints manage the database override layer for model metadata, which overlays on top of the embedded static catalog (`meta.toml`).

The Model Metadata system uses a three-layer architecture:
1. **Static Catalog** (`meta.toml`): embedded at compile time, defines default metadata for model families (tokenizer, capability flags, etc.)
2. **Database Overrides** (`config_settings` table): managed via Console API, overrides specific fields from the static catalog at runtime
3. **In-Memory Registry** (`Arc<ArcSwap<ModelMetadataRegistry>>`): field-level overlay merge with hot-reload support

The override mechanism uses field-level overlay merge: entries are merged by priority from low to high, where higher-priority `Some` fields override lower-priority ones, while `None` fields preserve the lower layer. This allows adding new models by specifying only the differing fields in the database, without repeating the full configuration.

- `GET /console/api/settings/model-metadata` — list all metadata entries (static catalog + database overrides), each annotated with `source` (`"catalog"` or `"db"`)
- `GET /console/api/settings/model-metadata/resolve/{model_name}?protocol=openai_chat` — resolve the effective metadata for a specific model, showing the overlay merge result. Optional `protocol` query parameter (`openai_chat`, `anthropic`, `dashscope`, `google`, etc.) for protocol fallback inference
- `PUT /console/api/settings/model-metadata` — replace all database override entries, body is a `ModelMetadataEntry[]` JSON array. Each entry contains `patterns` (glob pattern list), `priority` (integer), and optional fields (`tokenizer_family`, `token_correction_factor`, `tiktoken_encoding`, `disable_thinking_by_default`, `supports_*` capability flags, `max_input_tokens`, `max_output_tokens`, `pipeline_hints`, etc.). Validates entries before saving and triggers hot-reload
- `DELETE /console/api/settings/model-metadata` — clear all database override entries (static catalog entries cannot be deleted). The registry falls back to using only the static catalog

**Common use cases**:
- Override token correction factor for specific models (e.g., `qwen-vl-max` uses a different factor)
- Add tokenizer_family for custom models (e.g., proxy models using the `gpt` family)
- Disable thinking mode for specific models (e.g., set `disable_thinking_by_default: true` for `qwen-plus`)

### MCP Server Management (DB-only)

These endpoints manage MCP (Model Context Protocol) servers stored in the database (SQLite/PostgreSQL). MCP servers are **not** supported in file-based config — they are DB-only.

MCP servers enable LLM clients to discover and invoke tools from external services (GitHub, Slack, databases, etc.) through a unified proxy endpoint at `POST /mcp` (Streamable HTTP) or `GET /mcp/sse` (SSE legacy). Tool names use `{server_name}.{tool_name}` prefix format. See [MCP Gateway](mcp.md) for client usage.

- `GET /console/api/mcp-servers` — list all MCP servers, each with `name`, `description`, `transport`, `endpoint`, `auth_header`, `extra_headers`, `defer_loading`, `health_check`, `tags`, `priority`, `max_concurrency`, `rate_limit`, `connection_type`, `idle_timeout_secs`, `version`, `sort_order`, `enabled`
- `GET /console/api/mcp-servers/names` — list all MCP server names (for dropdowns)
- `POST /console/api/mcp-servers` — create an MCP server, body: `{ name, description?, transport, endpoint, auth_header?, extra_headers?, defer_loading?, health_check?, tags?, priority?, max_concurrency?, rate_limit?, connection_type, idle_timeout_secs?, enabled? }`
- `PUT /console/api/mcp-servers` — update an existing MCP server by `name`, same body as POST. The `name` field is immutable.
- `DELETE /console/api/mcp-servers` — delete an MCP server, body: `{ name: "..." }`

Transport values: `streamable_http` (standard HTTP JSON-RPC), `sse` (Server-Sent Events), `rest_bridge` (REST API via OpenAPI spec).

Connection type values: `stateful` (per-client isolated sessions, Type A), `stateless` (shared connection pool, Type B), `rest_bridge` (stateless REST API, Type C).

### ACL Rule Management (DB-only)

Header-based ACL rules for request access control. Rules are stored in the database and compiled into an `AclPolicy` on every hot-reload. See [Architecture — Header ACL Rule Engine](architecture.md#header-acl-rule-engine) for evaluation semantics.

- `GET /console/api/acl-rules` — list all ACL rules, returns `{ "rules": [...] }`
- `POST /console/api/acl-rules` — create a rule, body: `{ name, rule_type?, condition_key, condition_type, condition_value?, action?, enabled?, sort_order?, scope_key?, scope_group? }`
- `PUT /console/api/acl-rules` — update a rule by `name`, same body as POST. The `name` field is immutable.
- `DELETE /console/api/acl-rules` — delete a rule, body: `{ name: "..." }`

Field constraints:

| Field | Type | Required | Notes |
|-------|------|----------|-------|
| `name` | string | yes | Globally unique, max 512 chars. Immutable after creation. |
| `rule_type` | string | no | Currently only `"header"` (default). |
| `condition_key` | string | conditional | HTTP header name, max 512 chars. Required except when `condition_type` is `any` (leave empty). |
| `condition_type` | string | yes | One of: `exact`, `prefix`, `regex`, `exists`, `absent`, `any`. `any` matches every request without inspecting headers (the rule fires on scope alone); `condition_key` and `condition_value` must be empty. |
| `condition_value` | string? | conditional | Required for `exact`/`prefix`/`regex`; must be absent for `exists`/`absent`/`any`. Max 1024 bytes. Regex patterns are validated at write time. |
| `action` | string | no | `"allow"` or `"deny"` (default `"allow"`). |
| `enabled` | bool | no | Default `true`. Disabled rules are skipped at compile time. |
| `sort_order` | int | no | Default `0`. Lower values evaluated first. On equal `sort_order`, scope order is global → group → key, so a key-scoped rule needs a lower value to override a global rule. |
| `scope_key` | string[] | no | Restrict rule to one or more access keys. Mutually exclusive with `scope_group`. Empty array = global. |
| `scope_group` | string[] | no | Restrict rule to one or more key groups. Mutually exclusive with `scope_key`. Empty array = global. |

## Access Control

- Console API endpoints (`/console/api/*`) are always mounted
- API access requires either a valid session token (from `/login`) or the configured `secret_key` as a Bearer token
- Web UI (`/console`) is served only when `web_console.enabled` is `true`
- Accessible from any IP by default. Set `web_console.allow_remote = false` to restrict to localhost only.
- Brute-force protection: IPs are automatically banned after `web_console.max_failures` consecutive login failures (default 5) for `web_console.ban_duration` seconds (default 300s/5 min). The failure counter uses a sliding window — resets if elapsed time since last failure exceeds the ban duration. There is no manual ban/unban API — that belongs in a WAF.
- IP ban can be disabled via `web_console.ip_ban_enabled` (default `true`). Useful when multiple users share the same IP (VPN, NAT, reverse proxy). When disabled, brute-force protection falls back to bcrypt cost (~200-400ms/attempt) + verified token cache.
- Console responses include security headers: `X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY`, `Referrer-Policy`, `Content-Security-Policy` to mitigate XSS, clickjacking, and other attacks.

## Console

The web console at `/console` is built with React (compiled with Vite and embedded into the binary via rust-embed).

Sidebar:
- **Overview** — summary stat cards (Upstreams, Protocols, Models, Access Keys), plus time-series charts (input tokens and requests over time) with 12h/24h/7d preset selector
- **Access Keys** — manage API access keys and access key groups with fine-grained CRUD (Access Keys tab: create/edit/delete access keys with API key, name, group; Groups tab: create/edit/delete groups with model allow-lists). Groups support `*` to grant access to all models
- **Statistics** — persistent request usage statistics (SQLite/PostgreSQL) with time range selector (24h/7d/30d/custom), group-by toggle (By Model / By Access Key / By Group / By Model + Access Key / By Model + Group), summary cards, and data table. Model column shows `upstream_name::upstream_model_id` format.
- **Upstreams** — full upstream management with CRUD: add/edit/delete upstreams (including API keys, headers, payload rules, transform scripts, and all advanced settings via modal forms). Models are displayed inline within each upstream section, with their own CRUD (add/edit/delete models with upstream selection, model ID override, aliases, protocol override, payload rules, and transforms).
- **Load Balancers** — manage DB-only load balancers with multi-provider routing, weighted traffic distribution, and dynamic bindings. Supports CRUD for LB entries (upstream+model+weight), static bindings (pin specific API keys to fixed upstreams), and runtime session management. The status page shows per-entry status (effective weight, status) and all active dynamic bindings (dynamic + static) with the ability to delete, reassign, or pin sessions directly.
- **MCP Servers** — manage DB-only MCP (Model Context Protocol) servers that enable LLM clients to invoke tools from external services (GitHub, Slack, databases, etc.). Supports CRUD for MCP server configurations including transport type (Streamable HTTP, SSE, REST Bridge), connection type (stateful, stateless, REST bridge), authentication, tags, priority, and rate limiting. Each server's tools are exposed to clients via `POST /mcp` (Streamable HTTP) and `GET /mcp/sse` (SSE). Tool names use `{server_name}.{tool_name}` prefix format.
- **Logs** — paginated request logs with real-time SSE streaming. The stream tab shows live request/response metadata (model, upstream, status, latency, tokens). Log entries display full request/response headers and body snippets (truncated at 1MB). Supports keyword filtering and auto-pause when the tab loses visibility to reduce resource usage.

Invalid or expired tokens are automatically cleared and redirect to the login page.

## Notes

- All credentials are config-backed and read-only via the console API.
- **Canonical section ordering**: When config is saved via fine-grained API endpoints (e.g. access key management), top-level sections are reordered to the canonical order: `server` → `web_console` → `cors` → `streaming` → `logging` → `rate_limit_per_user` → `retry` → `routing` → `access_key_groups` → `access_keys` → `upstreams` → `models`. Load balancers are DB-only and do not appear in file-based config — they are not part of the canonical file order. This convention balances human readability with programmatic consistency — operators editing by hand see a predictable structure, and the program always produces stable output regardless of the input file's section order. Within each section, comments, formatting, and key order are fully preserved. Unknown sections are rejected. Future config sections must define their position in this order.
- Request logs are operational metadata, not a full audit system.
- Statistics are persisted in SQLite/PostgreSQL at 1-minute granularity; query results may lag by up to 1 minute due to flush intervals.
- The `protocol` field in API responses uses the display name (e.g. `OpenAI Chat Completions`, `Anthropic`, `Gemini`).
{% endraw %}
