The pickers offer GPT Sol and GPT Astra once, as provider-neutral models; Codex
is one of the backends a user's /openaiProvider can choose (see
openai_provider.md). Settings saved with the Codex ids
below load as the neutral ids. Codex itself remains access-controlled, and
these are its models:
openai-codex/gpt-sol-latest, the newest GPT Sol (GPT-6.1 Sol today)openai-codex/gpt-astra-latest, the newest GPT Astra (GPT-6 Astra today)openai-codex/gpt-reserve(Luna Reserve; see Codex Luna Reserve)
The first two, and the hidden openai-codex/gpt-luna-latest, are this repo's
own aliases (see "Aliases" below). The pickers name each after the model it
points at now.
The pickers show the Luna Reserve only to users with Codex access, and direct selection of any Codex id without access is rejected server-side. GPT Sol and Astra themselves are open to everyone, on whichever provider each user picked.
On first use, Betterborg creates ~/.borg/llm_chat_config.json5 if it does not
exist. Set LLM_CHAT_CONFIG_PATH to use another path. The default is:
{
codex_allowed_users: ["MAGIC_ADMINS"],
codex_imagegen_allowed_users: ["MAGIC_ADMINS"],
codex_users: [],
}The same file can hold the guest-mode policy; see guest_mode.md.
The two policy arrays accept numeric Telegram user IDs and the exact string
"MAGIC_ADMINS". The sentinel delegates to Betterborg's existing
util.isAdmin(event) check, including trusted-chat access. Remove it to remove
that automatic grant. Numeric IDs remain supported for older configurations.
Use codex_users for a permanent roster with names and independently enabled
personal grants. For example:
codex_users: [
{
id: 123456789,
name: "Example User",
codex_enabled: true,
imagegen_enabled: false,
},
],Each roster entry requires an integer id and both boolean fields; name is
optional. IDs must be unique within the roster. A roster entry replaces any
numeric grant for the same ID in the older arrays. A disabled personal grant
does not override MAGIC_ADMINS or trusted-chat access. Image generation
requires both effective Codex access and effective image access.
Both policy-array keys are required; the roster is optional for compatibility. Empty policy arrays and no enabled roster grants allow nobody. Betterborg reloads the file when it changes. Invalid or unreadable config disables all Codex access until corrected, logs a diagnostic, and leaves the file untouched. Other providers continue working. These grants do not confer bot-admin or group-settings privileges.
Bot admins can use .codex-users to browse configured non-admin users. Disabled
users stay in the menu. Legacy numeric IDs from either policy array are also
included, but MAGIC_ADMINS itself does not add users. The paginated list uses
separate bullet groups for each user, showing configured labels, Telegram display
names and usernames when known, numeric IDs, personal access flags, friendly
default-model names, and the names of personally configured API-key providers.
Unknown Telegram profile and contact fields are omitted from this overview.
Each page contains at most eight users and splits earlier if its text would
approach Telegram's message limit. The model picker shows full model IDs and the
selected model's supported reasoning-effort levels. Model and effort selections
use the same shared menu state as the user's normal model picker.
Successful Telegram refreshes update persisted identity, including removed
usernames; when refresh is unavailable, the last recorded identity is used.
The detail panel separates identity, model/access, and personal API-key metadata. It lists providers and their last-set UTC times without retrieving key values. Older keys display “Set date unknown”; inherited/shared credentials are excluded.
Contact status is scoped to this bot: “Started” means an incoming private message was recorded, including commands. The overview shows only the known “Started” state; the detail panel states when no private contact is recorded. Group activity, identity lookup, roster membership, keys, and shared history never count as private contact. If local metadata is missing, the panel also treats an existing private user dialog in the bot's direct Telegram dialog list as positive evidence; bots cannot initiate that dialog. This read-only scan is capped, sends nothing, and leaves absent or unavailable results unknown. See metadata storage.
.codex-users: browse the list and select a user..codex-users <user-id>: inspect access and the saved default, toggle the personal Codex or image grant, or open the model/effort picker. OpenAI share cycles the user's admin-share override (follows Codex grant, always on, always off), and Keys / providers opens their/keyspanel with their OpenAI and Gemini providers (key_sharing.md).- Share my OpenAI key on the list shares the pressing admin's OpenAI key with every Codex user, or stops sharing it.
.codex-users <user-id> <model-id>: immediately save a specific model as that user's personal default, including custom model IDs.
Choose Add user, then Choose user in a private chat. Telegram opens its native user picker and shares the selected user ID with the bot, without sharing a phone number. Select one person, review the preview, then confirm Add user. The picker is available only in private chats; group prompts still accept IDs and usernames with the existing direct-reply requirement.
You can also enter a positive numeric Telegram user ID or @username.
The bot resolves a username to its stable numeric ID before showing an identity
preview. Unknown numeric IDs are accepted with “Profile not known yet”; unknown
usernames, groups, channels, bots, and known bot admins are rejected.
Phone-number lookup is not supported by the bot: Telegram restricts
contacts.resolvePhone
to authenticated user accounts, and resolution also depends on the target's
privacy settings. A separate user-account resolver would require additional
account access and is not implemented. The
user picker
provides a bot-compatible alternative.
The preview starts both personal grants off. Add user confirms the insertion; Cancel clears the pending flow. No message is sent to the target, no contact status is inferred, and no saved model is changed. If another admin has already added the user, confirmation opens the existing record without changing its settings. Authorization, config validity, and current membership are checked again when confirming.
Each pending flow belongs to one admin in one chat. In groups, reply to the specific prompt; unrelated messages are ignored by the onboarding flow. Pending flows are held in memory and disappear on restart. Stale confirmation buttons cannot act on a newer flow. Picker results are also tied to their original request, admin, and chat. Old results and results from another chat cannot change the current preview.
Access buttons save an explicit enabled/disabled value to the JSON5 file and refresh the menu. Repeated clicks do not invert the state unexpectedly. Disabling Codex preserves the image flag: images are paused outside contexts that still grant Codex access. Enabling images never implicitly enables Codex. Turning either personal grant off leaves the configured admin/trusted-chat policies in effect. The menu states this distinction.
Config writes use a lock and atomic replacement, preserving file permissions and unrelated settings. Existing comments and formatting are retained when updating a flag or inserting a roster record. Invalid config, removed users, or detected concurrent manual edits prevent an update rather than replacing the config with defaults.
Model changes do not require the user's confirmation. They replace only the saved personal default; the user can subsequently change it again. An admin can save a Codex default while personal access is off, ready for re-enabling. Chat-specific models and per-message prefixes retain their usual precedence.
The command and every button check the caller's own bot-admin identity. Trusted chat membership alone does not authorize roster management. Existing-user controls also check the target's current roster membership. Bot admins are excluded as targets, and admin-only models cannot be assigned to non-admin users. Invalid config and removed membership prevent changes from previously opened menus. The existing trusted-chat rules still apply to model and image access independently.
The account's allowance is shared by every Codex user, so it runs out for everyone at once. A request that hits it is retried automatically on the Luna Reserve, which is metered separately, and the reply says when an answer came from there. Only if the Reserve is unavailable or also spent does the bot show a quota panel offering a temporary stand-in model from another provider.
The stand-in is per-user and opt-in, applies to saved defaults only, expires by
itself at the reset, and never rewrites saved settings. /codexStatus (also
.codex-status) reports the account's live meters and controls the stand-in.
See Codex quota fallback.
If a saved personal or chat model is Codex but the current request has no Codex access, ordinary messages fall back to the default Gemini Flash model. The bot sends a short notice identifying the fallback and explaining that saved model settings are unchanged. If the fallback needs an API key, its usual setup prompt follows. Re-enabling access restores use of the saved Codex model without rewriting preferences.
There is no fallback notice when trusted-chat or admin rules still grant
Codex access. Explicit Codex requests and .i receive an access denial rather
than falling back. .c* and .as* are not Codex requests: they select the GPT
families, which run on the user's provider. Requests already in flight retain their
authorization snapshot; toggles affect subsequent requests.
- Install
requirements.txtin the same Python environment that runsstdborg.py. - Run
codex loginfor that runtime user so~/.codex/auth.jsoncontains ChatGPT OAuth credentials.
The integration depends on the published llm-openai-via-codex pip package for
Codex OAuth token borrowing and refresh. Betterborg only owns the Telegram
message conversion, access checks, and streaming response handling.
The backend's catalog is GET https://chatgpt.com/backend-api/codex/models?client_version=…,
with the Codex OAuth token. On 2026-10-01 it listed, in its own priority
order:
gpt-6.1-sol, "Latest workhorse model for coding and everyday work";gpt-6-astra, "Frontier intelligence for the most demanding work";gpt-6-sol, "Previous generation workhorse model";gpt-6-luna, "Fast and affordable model for easier tasks";gpt-5.6-sol,gpt-5.6-terraandgpt-5.6-luna, described as older;gpt-5.5, "Legacy coding model", whoseupgradefield says it retires on 2026-10-14 and points atgpt-6.1-sol.
It also reports gpt-reserve and codex-auto-review with supported_in_api
true but visibility: hide. gpt-reserve is the Luna Reserve routing slug
and is used deliberately; codex-auto-review is not investigated.
There is no latest alias: every entry is a pinned slug. The only pointers
are a retiring model's upgrade field and the descriptions above. OpenRouter,
by contrast, has router aliases such as ~openai/gpt-sol-latest and
~openai/gpt-luna-latest.
GPT-6 Astra answers live requests (guest answers on the production bot, 2026-10-01).
Since Codex has none, the bot keeps its own (uniborg/codex_aliases.py):
openai-codex/gpt-sol-latest, gpt-astra-latest and gpt-luna-latest.
Each points at a slug of its family, gpt-<version>-<family>:
- At first, the pinned slug in
FAMILIES(gpt-6.1-sol,gpt-6-astra,gpt-6-luna). - At startup and every hour, llm_chat reads the catalog, and each alias moves
to the highest version of its family that the catalog lists
(
visibility: list) as served by the API (supported_in_api). Versions compare as numbers, so6.10is above6.9. codex_util.codex_model_namesends the slug an alias points at, so a request, the prompt cache key and the image path all follow it.- An alias's reasoning levels are the catalog's for that model, minus
ultra(which the API rejects), plusnoneonly for a family whose pinned model was probed to take it (Luna). The catalog never listsnone.
Whenever an alias moves, or its levels change, or its family vanishes from
the catalog (the alias then stays where it was), the admins get one message
in the log chat (Uniborg.send_log_alert). The resolution is kept in Redis
(borg:codex_aliases), so a restart, or a second process, neither forgets it
nor says it again. Without Redis, each restart compares against the pinned
slugs, so a move is announced again after every restart.
The OpenAI families are aliases: OpenRouter's own router alias
~openai/gpt-sol-latest for OR_OPENAI_SOL, and the Codex aliases above. So
a new model needs no change here. llm_models.RETIRED_MODELS maps each id
that was once registered to its replacement, and saved settings (personal,
chat and topic models, the title model, a quota stand-in, and the per-model
efforts) load with the replacement and are saved that way on their next write
(_SavedSettings in llm_chat.py). History:
- 2026-10-01: GPT-5.6 Sol moved to GPT-6.1 Sol, GPT-5.6 Luna to GPT-6 Luna, and GPT-5.6 Terra, which has no GPT-6 successor, to GPT-6.1 Sol.
- 2026-10-01: GPT-5.5 was dropped ahead of its retirement and moved to
GPT-6.1 Sol, the successor its
upgradefield names. - 2026-10-01: the pinned GPT-6 ids moved to the aliases, and OpenRouter's
GPT-6.1 Sol to
~openai/gpt-sol-latest.
A saved effort the new model lacks (none on Sol) is skipped, so the next
layer's effort applies.
Reasoning effort is a per-model preference. See docs/reasoning_effort.md.
The Responses API accepts none, minimal, low, medium, high, xhigh
and max, but not every model takes every level. Probed live on 2026-10-01:
GPT-6.1 Sol rejects none ("Supported values are: 'low', 'medium', 'high',
'xhigh', and 'max'"), as GPT-6 Astra does; GPT-6 Luna takes none through
max. No menu offers minimal.
The ultra level advertised by the Codex CLI model list is a Codex-app
subagent mode, not an API value. Sending it returns an "Invalid value" error,
so it is deliberately absent from the level sets.
All but .cr select a provider-neutral GPT family and run on the user's
/openaiProvider; .cr is Codex-only and access-controlled.
.c/.چand.cm/.چم: GPT-6.1 Sol withmediumreasoning..cl/.چل: GPT-6.1 Sol withlowreasoning..ch/.چه: GPT-6.1 Sol withhighreasoning..cx/.چخ: GPT-6.1 Sol withxhighreasoning..cxx/.چخخ: GPT-6.1 Sol withmaxreasoning..as/.اس,.asm/.اسم,.asl/.اسل,.ash/.اسه,.asx/.اسخ, and.asxx/.اسخخ: the same ladder for GPT-6 Astra..cr/.چر: Luna Reserve withmediumreasoning.
.a belongs to advanced_get, so Astra uses .as.
The googleSearch toggle maps to the OpenAI Responses web_search tool for
Codex models. The current-message .i prefix adds image_generation with
partial_images: 3 when both access policies allow it. There is no persistent
image-generation toggle. Other Gemini-specific tools are not mapped.
Codex requests are sent through the OpenAI Responses API. Betterborg only
forwards image attachments as input_image parts for Codex models; non-image
binary attachments such as videos, audio, PDFs, stickers, and unknown MIME types
are skipped before the request is sent. Inline data URLs are validated to ensure
they use an image/* MIME type.
See docs/codex_caching.md for Codex prompt caching behavior.
Use .i to enable image generation for a single request. It combines with
model and reasoning prefixes, such as .i .cl draw a fox. Both access policies
must allow the sender. An explicit Codex prefix takes precedence, otherwise the
selected Codex model is used, falling back to GPT-6.1 Sol when the selected
model is from another provider. Explicit non-Codex prefixes conflict with .i.
Every streamed preview and completed image is sent separately to Telegram,
replying to the request. Previews remain after completion. Text responses and
clarification questions continue to work, and image attachments remain
available for editing prompts. Historical .i messages and replies to generated
images do not enable the tool for the next request.
See Codex image generation for stream handling,
failure behavior, and the live OAuth verification with previews enabled.
The separate image_gen_plugins/image_gen.py plugin continues to use Google
Imagen.