Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/open-api-docs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ openapi: 3.0.3
info:
title: The Agent's user-facing API
description: The user-facing parts of The Agent's API service (excluding system-level endpoints, chat completion, maintenance endpoints, etc.)
version: 5.15.0
version: 5.15.1
license:
name: MIT
url: https://opensource.org/licenses/MIT
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "the-agent"
version = "5.15.0"
version = "5.15.1"

[tool.setuptools]
package-dir = {"" = "src"}
Expand Down
16 changes: 11 additions & 5 deletions src/api/settings_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@
)
from util.errors import AuthorizationError, ConfigurationError, ValidationError

SettingsType: TypeAlias = Annotated[str, Literal["user", "chat"]]
SettingsType: TypeAlias = Annotated[str, Literal["user", "chat", "intelligence"]]
InvokerType: TypeAlias = Annotated[str, Literal["creator", "administrator"]]
DEF_SETTINGS_TYPE: SettingsType = "user"
DEF_SETTINGS_TYPE: SettingsType = "intelligence"
SETTINGS_TOKEN_VAR: str = "token"


Expand Down Expand Up @@ -82,7 +82,7 @@ def create_settings_link(
if settings_type == "chat":
# any member can access their per-chat settings where admin rights are not required
self.__di.chat_membership_service.sync(self.__di.invoker, chat_config)
resource_id = self.__di.invoker.id.hex if settings_type == "user" else chat_config.chat_id.hex
resource_id = self.__di.invoker.id.hex if settings_type in ("user", "intelligence") else chat_config.chat_id.hex
lang_iso_code = chat_config.language_iso_code or "en"
else:
# API context only supports user settings, we default to the basics
Expand All @@ -92,8 +92,14 @@ def create_settings_link(

jwt_token = self.__create_jwt_token(chat_type)
is_sponsored = self.__is_sponsored(self.__di.invoker.id)
page = "sponsorships" if (settings_type == "user" and is_sponsored) else "settings"
settings_url_base = f"{config.backoffice_url_base}/{lang_iso_code}/{settings_type}/{resource_id}/{page}"
url_type = "user" if settings_type in ("user", "intelligence") else settings_type
if is_sponsored and settings_type in ("user", "intelligence"):
page = "sponsorships"
elif settings_type == "intelligence":
page = "intelligence"
else:
page = "settings"
settings_url_base = f"{config.backoffice_url_base}/{lang_iso_code}/{url_type}/{resource_id}/{page}"
long_url = f"{settings_url_base}?{SETTINGS_TOKEN_VAR}={jwt_token}"

valid_until = datetime.now() + timedelta(minutes = config.jwt_expires_in_minutes * 10)
Expand Down
95 changes: 95 additions & 0 deletions src/db/alembic/versions/0ab60975e593_model_updates_may_2026.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
"""model_updates_may_2026

Revision ID: 0ab60975e593
Revises: 233cbdadb4b6
Create Date: 2026-05-24 14:22:43.581764

"""
from typing import Sequence, Union

from alembic import op
from sqlalchemy import text

# revision identifiers, used by Alembic.
revision: str = "0ab60975e593"
down_revision: Union[str, None] = "233cbdadb4b6"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


# Snapshot of supported model IDs as of 2026-05-24. Hardcoded on purpose:
# the migration must remain stable even if future code changes the model list.
SUPPORTED_MODEL_IDS: tuple[str, ...] = (
# OpenAI
"gpt-4.1", "gpt-4.1-mini", "gpt-4o", "gpt-4o-mini", "gpt-4o-transcribe", "gpt-4o-mini-transcribe",
"gpt-5", "gpt-5-mini", "gpt-5-nano", "gpt-5.1", "gpt-5.2", "gpt-5.4", "gpt-5.5",
"whisper-1", "text-embedding-3-small", "text-embedding-3-large",
# Anthropic
"claude-haiku-4-5", "claude-sonnet-4-5", "claude-opus-4-5",
"claude-sonnet-4-6", "claude-opus-4-6", "claude-opus-4-7",
# Google AI
"gemini-flash-lite-latest", "gemini-flash-latest", "gemini-pro-latest",
"gemini-2.5-flash-image", "gemini-3-pro-image-preview", "gemini-3.1-flash-image-preview",
# xAI
"grok-4.20-non-reasoning", "grok-4.20-reasoning", "grok-4.3",
"grok-imagine-image", "grok-imagine-image-quality",
# Perplexity
"sonar", "sonar-pro", "sonar-reasoning-pro", "sonar-deep-research",
# Replicate
"black-forest-labs/flux-1.1-pro", "black-forest-labs/flux-kontext-pro",
"black-forest-labs/flux-2-pro", "black-forest-labs/flux-2-max",
"openai/gpt-image-1.5", "openai/gpt-image-2",
"bytedance/seedream-4", "bytedance/seedream-4.5",
"google/nano-banana", "google/nano-banana-pro", "google/nano-banana-2",
# API Integrations
"currency-converter5.p.rapidapi.com", "x.api-v2-post.read", "v1.cryptocurrency.quotes.latest",
# Internal
"credit_transfer",
)

TOOL_CHOICE_COLUMNS: tuple[str, ...] = (
"tool_choice_chat",
"tool_choice_reasoning",
"tool_choice_copywriting",
"tool_choice_vision",
"tool_choice_hearing",
"tool_choice_images_gen",
"tool_choice_images_edit",
"tool_choice_search",
"tool_choice_embedding",
"tool_choice_api_fiat_exchange",
"tool_choice_api_crypto_exchange",
"tool_choice_api_twitter",
)

RENAMES: tuple[tuple[str, str], ...] = (
# grok-4-1-fast-* are now aliases of grok-4.3
("grok-4-1-fast-non-reasoning", "grok-4.3"),
("grok-4-1-fast-reasoning", "grok-4.3"),
# grok-imagine-image-pro was renamed to grok-imagine-image-quality
("grok-imagine-image-pro", "grok-imagine-image-quality"),
# gemini-3-flash-preview was the preview name for gemini-2.5-flash-image
("gemini-3-flash-preview", "gemini-2.5-flash-image"),
)


def upgrade() -> None:
for old_id, new_id in RENAMES:
for column in TOOL_CHOICE_COLUMNS:
op.execute(
text(f"UPDATE simulants SET {column} = '{new_id}' WHERE {column} = '{old_id}'"),
)

quoted_ids = ", ".join(f"'{id_}'" for id_ in SUPPORTED_MODEL_IDS)
for column in TOOL_CHOICE_COLUMNS:
op.execute(
text(
f"UPDATE simulants SET {column} = NULL "
f"WHERE {column} IS NOT NULL AND {column} NOT IN ({quoted_ids})",
),
)


def downgrade() -> None:
# No-op: renamed model IDs cannot be losslessly restored.
pass
9 changes: 4 additions & 5 deletions src/features/chat/llm_tools/llm_tool_library.py
Original file line number Diff line number Diff line change
Expand Up @@ -340,13 +340,12 @@ def configure_settings(
raw_settings_type: str,
) -> str:
"""
Launches the configuration screen. Configurations allow various profile settings, payments, API tokens/keys,
current chat's settings, language, response rate, release notifications, model options, etc. Profile settings also
serve as the initial setup for the agent (bot). In private chats, user settings are the default. The user will
probably not know which settings they need, so they must either be chosen for, or asked.
Launches the configuration screen with various profile and payment settings, credits, API tokens/keys, chat's settings,
language, response rate, release notifications, model options, etc. Intelligence settings should be the default page.
The user will probably not know which settings page they need, so they must either be chosen for, or asked.

Args:
raw_settings_type: [mandatory] The type of settings the user wants: [ 'user', 'chat' ]
raw_settings_type: [mandatory] The type of settings the user wants: [ 'intelligence', 'user', 'chat' ]
"""
try:
settings_link = di.settings_controller.create_settings_link(raw_settings_type).settings_link
Expand Down
54 changes: 21 additions & 33 deletions src/features/external_tools/external_tool_library.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,9 +285,9 @@
provider = GOOGLE_AI,
types = [ToolType.chat, ToolType.reasoning, ToolType.copywriting, ToolType.vision],
cost_estimate = CostEstimate(
input_1m_tokens = 50,
output_1m_tokens = 300,
search_1m_tokens = 25, # used with vision queries
input_1m_tokens = 150,
output_1m_tokens = 900,
search_1m_tokens = 75, # used with vision queries
),
)

Expand All @@ -304,7 +304,7 @@
)

NANO_BANANA = ExternalTool(
id = "gemini-3-flash-preview",
id = "gemini-2.5-flash-image",
name = "Nano Banana",
provider = GOOGLE_AI,
types = [ToolType.images_gen, ToolType.images_edit],
Expand Down Expand Up @@ -350,28 +350,6 @@

### xAI ###

GROK_4_1_FAST_NON_REASONING = ExternalTool(
id = "grok-4-1-fast-non-reasoning",
name = "Grok 4.1 Fast",
provider = XAI,
types = [ToolType.chat, ToolType.copywriting, ToolType.vision],
cost_estimate = CostEstimate(
input_1m_tokens = 20,
output_1m_tokens = 50,
),
)

GROK_4_1_FAST_REASONING = ExternalTool(
id = "grok-4-1-fast-reasoning",
name = "Grok 4.1 Fast (Reasoning)",
provider = XAI,
types = [ToolType.chat, ToolType.reasoning, ToolType.copywriting, ToolType.vision],
cost_estimate = CostEstimate(
input_1m_tokens = 20,
output_1m_tokens = 50,
),
)

GROK_4_20_NON_REASONING = ExternalTool(
id = "grok-4.20-non-reasoning",
name = "Grok 4.20",
Expand All @@ -394,6 +372,17 @@
),
)

GROK_4_3 = ExternalTool(
id = "grok-4.3",
name = "Grok 4.3",
provider = XAI,
types = [ToolType.chat, ToolType.reasoning, ToolType.copywriting, ToolType.vision],
cost_estimate = CostEstimate(
input_1m_tokens = 200,
output_1m_tokens = 600,
),
)

IMAGE_GEN_GROK_IMAGINE = ExternalTool(
id = "grok-imagine-image",
name = "Grok Imagine Image",
Expand All @@ -412,13 +401,13 @@
max_input_images = 5,
)

IMAGE_GEN_GROK_IMAGINE_PRO = ExternalTool(
id = "grok-imagine-image-pro",
name = "Grok Imagine Image Pro",
IMAGE_GEN_GROK_IMAGINE_QUALITY = ExternalTool(
id = "grok-imagine-image-quality",
name = "Grok Imagine Image (Quality)",
provider = XAI,
types = [ToolType.images_gen, ToolType.images_edit],
cost_estimate = CostEstimate(
output_image_1k = 7,
output_image_1k = 5,
output_image_2k = 7,
output_image_4k = 7,
input_image_1k = 0.2,
Expand Down Expand Up @@ -718,12 +707,11 @@
NANO_BANANA_PRO,
NANO_BANANA_2,
# xAI
GROK_4_1_FAST_NON_REASONING,
GROK_4_1_FAST_REASONING,
GROK_4_20_NON_REASONING,
GROK_4_20_REASONING,
GROK_4_3,
IMAGE_GEN_GROK_IMAGINE,
IMAGE_GEN_GROK_IMAGINE_PRO,
IMAGE_GEN_GROK_IMAGINE_QUALITY,
# Perplexity
SONAR,
SONAR_PRO,
Expand Down
4 changes: 2 additions & 2 deletions src/features/images/image_api_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
IMAGE_GEN_EDIT_SEEDREAM_4_5,
IMAGE_GEN_FLUX_1_1,
IMAGE_GEN_GROK_IMAGINE,
IMAGE_GEN_GROK_IMAGINE_PRO,
IMAGE_GEN_GROK_IMAGINE_QUALITY,
NANO_BANANA,
NANO_BANANA_2,
NANO_BANANA_PRO,
Expand Down Expand Up @@ -141,7 +141,7 @@ def map_to_model_parameters(
elif tool == IMAGE_GEN_GROK_IMAGINE:
ar = unified_params.aspect_ratio if unified_params.aspect_ratio != "match_input_image" else None
return replace(unified_params, resolution = convert_size_to_k(unified_params.size).lower(), aspect_ratio = ar)
elif tool == IMAGE_GEN_GROK_IMAGINE_PRO:
elif tool == IMAGE_GEN_GROK_IMAGINE_QUALITY:
ar = unified_params.aspect_ratio if unified_params.aspect_ratio != "match_input_image" else None
return replace(unified_params, resolution = convert_size_to_k(unified_params.size).lower(), aspect_ratio = ar)
else:
Expand Down
14 changes: 13 additions & 1 deletion test/api/test_settings_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,13 +185,25 @@ def create_admin_member(telegram_user, is_manager = True):
can_delete_stories = False,
)

def test_create_settings_link_success_user_settings(self):
def test_create_settings_link_default_is_intelligence(self):
controller = SettingsController(self.mock_di)
link_response = controller.create_settings_link()

self.assertIsInstance(link_response, SettingsLinkResponse)
link = link_response.settings_link
self.assertIn("user", link)
self.assertIn("intelligence", link)
self.assertIn(self.invoker_user.id.hex, link)
self.assertIn("token=", link)

def test_create_settings_link_success_user_settings(self):
controller = SettingsController(self.mock_di)
link_response = controller.create_settings_link("user")

self.assertIsInstance(link_response, SettingsLinkResponse)
link = link_response.settings_link
self.assertIn("user", link)
self.assertIn("settings", link)
self.assertIn(self.invoker_user.id.hex, link)
self.assertIn("token=", link)

Expand Down
Loading