Files
Classic298 7429093191 fix: coding agent models get ids made only of letters, digits, dashes and underscores (#34171)
Cursor and Pi model names carry brackets, equals signs, commas or slashes, and Open WebUI kept those in the ids of their agent models, so a pinned Cursor or Pi model opened a new chat on "Select a model". Agent models now get ids made only of letters, digits, dashes and underscores, with a few extra characters added when the name had to change, so two models never end up with the same id. The agent itself still runs on its own model name. Existing pins, default model choices and automations that point at an agent model whose name had other characters (dots included, as in most Codex models) stop matching and need the model picked again, and an existing chat with such an agent answers "Agent model is unavailable in the selected terminal" until the model is picked again.

Fixes #34168
2026-11-12 22:00:04 +09:00

65 lines
2.5 KiB
Python

"""Read native conversation state from signed, server-persisted chat messages."""
import hashlib
import re
from urllib.parse import quote
from open_webui.models.chats import ChatModel, Chats
from open_webui.utils.auth import decode_token
def model_id(state):
model = re.sub(r'[^a-zA-Z0-9_-]+', '-', state['model']).strip('-')
if model != state['model']:
# Keeps CLI model names apart that only differ in the replaced characters.
model = f'{model}-{hashlib.sha256(state["model"].encode()).hexdigest()[:8]}'
return f'agent:{quote(state["terminal_id"], safe="")}:{state["provider"]}:{model}'
async def ensure_chat_selections(chat):
if all(key in (chat.chat or {}) for key in ('models', 'terminal_id', 'working_directory')):
return chat
state = await get_session(chat)
if not state:
return chat
async with Chats._chat_transaction(chat.id) as (_, row):
if row is None:
return chat
# Check missing fields under the lock so concurrent selection changes win.
data = dict(row.chat or {})
data.setdefault('models', [model_id(state)])
data.setdefault('terminal_id', state['terminal_id'])
if data['terminal_id'] == state['terminal_id']:
data.setdefault('working_directory', state['cwd'])
row.chat = data
chat = ChatModel.model_validate(row)
return chat
async def get_session(chat, message_id=None):
message_id = (
message_id or getattr(chat, 'current_message_id', None) or (chat.chat or {}).get('history', {}).get('currentId')
)
if not message_id:
return None
messages = await Chats.get_messages_map_by_chat_id(chat.id) or {}
visited = set()
while message_id and message_id not in visited:
visited.add(message_id)
message = messages.get(message_id)
if not message:
return None
token = (message.get('meta') or {}).get('agent')
if token:
state = decode_token(token) if isinstance(token, str) else None
if state and state.get('purpose') == 'terminal-agent':
if state.get('chat') != chat.id:
return None # Forks and clones start independent native conversations.
if all(
isinstance(state.get(key), str) and state[key]
for key in ('terminal_id', 'provider', 'model', 'cwd', 'user')
):
return state
message_id = message.get('parentId')
return None