diff --git a/docs/open-api-docs.yaml b/docs/open-api-docs.yaml index 300d40a1..67d1ae18 100644 --- a/docs/open-api-docs.yaml +++ b/docs/open-api-docs.yaml @@ -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 diff --git a/pyproject.toml b/pyproject.toml index 858b9faf..6549d19e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"} diff --git a/src/api/settings_controller.py b/src/api/settings_controller.py index fe2e0056..7b3e0384 100644 --- a/src/api/settings_controller.py +++ b/src/api/settings_controller.py @@ -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" @@ -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 @@ -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) diff --git a/src/db/alembic/versions/0ab60975e593_model_updates_may_2026.py b/src/db/alembic/versions/0ab60975e593_model_updates_may_2026.py new file mode 100644 index 00000000..7b58ae7c --- /dev/null +++ b/src/db/alembic/versions/0ab60975e593_model_updates_may_2026.py @@ -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 diff --git a/src/features/chat/llm_tools/llm_tool_library.py b/src/features/chat/llm_tools/llm_tool_library.py index 4496dec7..fadefe26 100644 --- a/src/features/chat/llm_tools/llm_tool_library.py +++ b/src/features/chat/llm_tools/llm_tool_library.py @@ -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 diff --git a/src/features/external_tools/external_tool_library.py b/src/features/external_tools/external_tool_library.py index 91cb8091..cfc7ac9b 100644 --- a/src/features/external_tools/external_tool_library.py +++ b/src/features/external_tools/external_tool_library.py @@ -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 ), ) @@ -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], @@ -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", @@ -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", @@ -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, @@ -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, diff --git a/src/features/images/image_api_utils.py b/src/features/images/image_api_utils.py index 81c25ee6..21844a7e 100644 --- a/src/features/images/image_api_utils.py +++ b/src/features/images/image_api_utils.py @@ -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, @@ -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: diff --git a/test/api/test_settings_controller.py b/test/api/test_settings_controller.py index 29b0d4b0..e9f9d08d 100644 --- a/test/api/test_settings_controller.py +++ b/test/api/test_settings_controller.py @@ -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)