v2.13.0 (#733)
Co-authored-by: Maxime Dolores <maxime.dolores@ext.mistral.ai> Co-authored-by: Michel Thomazo <51709227+michelTho@users.noreply.github.com> Co-authored-by: Peter Evers <peter.evers@mistral.ai> Co-authored-by: Pierre Rossinès <pierre.rossines@mistral.ai> Co-authored-by: Quentin <quentin.torroba@mistral.ai> Co-authored-by: Val <102326092+vdeva@users.noreply.github.com> Co-authored-by: Vincent G <10739306+VinceOPS@users.noreply.github.com> Co-authored-by: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
parent
198277af3f
commit
ad0d5c9520
61 changed files with 969 additions and 461 deletions
|
|
@ -3,4 +3,4 @@ from __future__ import annotations
|
|||
from pathlib import Path
|
||||
|
||||
VIBE_ROOT = Path(__file__).parent
|
||||
__version__ = "2.12.1"
|
||||
__version__ = "2.13.0"
|
||||
|
|
|
|||
|
|
@ -1516,6 +1516,14 @@ class VibeAcpAgentLoop(AcpAgent):
|
|||
)
|
||||
return provider, auth_state
|
||||
|
||||
def _process_env_value_before_dotenv_load(
|
||||
self, provider: ProviderConfig
|
||||
) -> str | None:
|
||||
if not provider.api_key_env_var:
|
||||
return None
|
||||
|
||||
return self._environ_before_dotenv_load.get(provider.api_key_env_var)
|
||||
|
||||
def _handle_auth_status(self) -> dict[str, Any]:
|
||||
_, auth_state = self._assess_current_auth_state()
|
||||
return _auth_status_response_from_auth_state(auth_state).model_dump(
|
||||
|
|
@ -1531,6 +1539,14 @@ class VibeAcpAgentLoop(AcpAgent):
|
|||
|
||||
try:
|
||||
self._remove_api_key(provider)
|
||||
if (
|
||||
auth_state.kind
|
||||
== AuthStateKind.VIBE_HOME_ENV_FILE_OVERRIDES_PROCESS_ENV
|
||||
and auth_state.env_key
|
||||
):
|
||||
process_env_value = self._process_env_value_before_dotenv_load(provider)
|
||||
if process_env_value:
|
||||
os.environ[auth_state.env_key] = process_env_value
|
||||
except (OSError, ValueError) as exc:
|
||||
raise InternalError(f"Failed to sign out: {exc}") from exc
|
||||
|
||||
|
|
@ -1650,11 +1666,8 @@ class VibeAcpAgentLoop(AcpAgent):
|
|||
)
|
||||
|
||||
await session.agent_loop.compact(extra_instructions=cmd_args.strip())
|
||||
new_tokens = session.agent_loop.stats.context_tokens
|
||||
|
||||
end_event = CompactEndEvent(
|
||||
old_context_tokens=old_tokens or 0,
|
||||
new_context_tokens=new_tokens or 0,
|
||||
summary_length=0,
|
||||
old_session_id=old_session_id,
|
||||
new_session_id=session.agent_loop.session_id,
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ from vibe.core.config._settings import THINKING_LEVELS, ThinkingLevel
|
|||
from vibe.core.proxy_setup import SUPPORTED_PROXY_VARS, get_current_proxy_settings
|
||||
from vibe.core.tools.permissions import RequiredPermission
|
||||
from vibe.core.types import CompactEndEvent, CompactStartEvent, LLMMessage
|
||||
from vibe.core.utils import compact_reduction_display
|
||||
from vibe.core.utils import compact_complete_display
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from vibe.core.config import ModelConfig
|
||||
|
|
@ -230,9 +230,7 @@ def create_compact_end_session_update(event: CompactEndEvent) -> ToolCallProgres
|
|||
content=TextContentBlock(
|
||||
type="text",
|
||||
text=(
|
||||
compact_reduction_display(
|
||||
event.old_context_tokens,
|
||||
event.new_context_tokens,
|
||||
compact_complete_display(
|
||||
old_session_id=event.old_session_id,
|
||||
new_session_id=event.new_session_id,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ from vibe.cli.plan_offer.ports.whoami_gateway import (
|
|||
WhoAmIResponse,
|
||||
)
|
||||
from vibe.core.config import (
|
||||
DEFAULT_CONSOLE_BASE_URL,
|
||||
DEFAULT_MISTRAL_API_ENV_KEY,
|
||||
DEFAULT_VIBE_BASE_URL,
|
||||
ProviderConfig,
|
||||
)
|
||||
from vibe.core.types import Backend
|
||||
|
|
@ -97,20 +97,18 @@ def resolve_api_key_for_plan(provider: ProviderConfig) -> str | None:
|
|||
|
||||
|
||||
def plan_offer_cta(
|
||||
payload: PlanInfo | None, console_base_url: str = DEFAULT_CONSOLE_BASE_URL
|
||||
payload: PlanInfo | None, *, vibe_base_url: str = DEFAULT_VIBE_BASE_URL
|
||||
) -> str | None:
|
||||
if not payload:
|
||||
return
|
||||
console_cli_url = f"{console_base_url.rstrip('/')}/codestral/cli"
|
||||
switch_to_pro_key_url = console_cli_url
|
||||
upgrade_url = console_cli_url
|
||||
vibe_api_key_url = f"{vibe_base_url.rstrip('/')}/code/extensions?focus=key"
|
||||
if payload.prompt_switching_to_pro_plan:
|
||||
return f"### Switch to your [Le Chat Pro API key]({switch_to_pro_key_url})"
|
||||
return f"### Switch to your [Vibe Pro API key]({vibe_api_key_url})"
|
||||
if (
|
||||
payload.plan_type in {WhoAmIPlanType.API, WhoAmIPlanType.UNAUTHORIZED}
|
||||
or payload.is_free_mistral_code_plan()
|
||||
):
|
||||
return f"### Unlock more with Vibe - [Upgrade to Le Chat Pro]({upgrade_url})"
|
||||
return f"### Unlock more with Vibe - [Upgrade to Vibe Pro]({vibe_api_key_url})"
|
||||
|
||||
|
||||
def plan_title(payload: PlanInfo | None) -> str | None: # noqa: PLR0911
|
||||
|
|
|
|||
|
|
@ -2183,32 +2183,26 @@ class VibeApp(App): # noqa: PLR0904
|
|||
if not self.event_handler:
|
||||
return
|
||||
|
||||
old_tokens = self.agent_loop.stats.context_tokens
|
||||
old_session_id = self.agent_loop.session_id
|
||||
compact_msg = CompactMessage()
|
||||
self.event_handler.current_compact = compact_msg
|
||||
await self._mount_and_scroll(compact_msg)
|
||||
|
||||
self._agent_task = asyncio.create_task(
|
||||
self._run_compact(compact_msg, old_tokens, old_session_id, cmd_args.strip())
|
||||
self._run_compact(compact_msg, old_session_id, cmd_args.strip())
|
||||
)
|
||||
|
||||
async def _run_compact(
|
||||
self,
|
||||
compact_msg: CompactMessage,
|
||||
old_tokens: int,
|
||||
old_session_id: str,
|
||||
extra_instructions: str = "",
|
||||
) -> None:
|
||||
self._agent_running = True
|
||||
try:
|
||||
await self.agent_loop.compact(extra_instructions=extra_instructions)
|
||||
new_tokens = self.agent_loop.stats.context_tokens
|
||||
compact_msg.set_complete(
|
||||
old_tokens=old_tokens,
|
||||
new_tokens=new_tokens,
|
||||
old_session_id=old_session_id,
|
||||
new_session_id=self.agent_loop.session_id,
|
||||
old_session_id=old_session_id, new_session_id=self.agent_loop.session_id
|
||||
)
|
||||
|
||||
except asyncio.CancelledError:
|
||||
|
|
@ -2887,13 +2881,18 @@ class VibeApp(App): # noqa: PLR0904
|
|||
if self._chat_input_container:
|
||||
self._chat_input_container.switching_mode = True
|
||||
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
def schedule_switch() -> None:
|
||||
self._switch_agent_generation += 1
|
||||
my_gen = self._switch_agent_generation
|
||||
|
||||
def switch_agent_sync() -> None:
|
||||
try:
|
||||
asyncio.run(self.agent_loop.switch_agent(new_profile.name))
|
||||
future = asyncio.run_coroutine_threadsafe(
|
||||
self.agent_loop.switch_agent(new_profile.name), loop
|
||||
)
|
||||
future.result()
|
||||
self.agent_loop.set_approval_callback(self._approval_callback)
|
||||
self.agent_loop.set_user_input_callback(self._user_input_callback)
|
||||
finally:
|
||||
|
|
@ -3031,7 +3030,7 @@ class VibeApp(App): # noqa: PLR0904
|
|||
if content is not None:
|
||||
body = content
|
||||
plan_offer = plan_offer_cta(
|
||||
self._plan_info, console_base_url=self.config.console_base_url
|
||||
self._plan_info, vibe_base_url=self.config.vibe_base_url
|
||||
)
|
||||
if plan_offer is not None:
|
||||
body = f"{body}\n\n{plan_offer}"
|
||||
|
|
@ -3059,12 +3058,10 @@ class VibeApp(App): # noqa: PLR0904
|
|||
if not self._show_vscode_extension_promo:
|
||||
return
|
||||
promo_message = VscodeExtensionPromoMessage()
|
||||
if self._history_widget_indices:
|
||||
promo_message.add_class("after-history")
|
||||
messages_area = self._cached_messages_area or self.query_one("#messages")
|
||||
chat = self._cached_chat or self.query_one("#chat", ChatScroll)
|
||||
should_anchor = chat.is_at_bottom
|
||||
await chat.mount(promo_message, after=messages_area)
|
||||
await chat.mount(promo_message, before=messages_area)
|
||||
if should_anchor:
|
||||
chat.anchor()
|
||||
self.run_worker(self._record_vscode_extension_promo_shown(), exclusive=False)
|
||||
|
|
|
|||
|
|
@ -1281,10 +1281,6 @@ NarratorStatus {
|
|||
link-color-hover: $mistral_orange;
|
||||
}
|
||||
|
||||
.vscode-extension-promo-message.after-history {
|
||||
margin-top: 1;
|
||||
}
|
||||
|
||||
#sessionpicker-app {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
|
|
|
|||
|
|
@ -229,10 +229,7 @@ class EventHandler:
|
|||
async def _handle_compact_end(self, event: CompactEndEvent) -> None:
|
||||
if self.current_compact:
|
||||
self.current_compact.set_complete(
|
||||
old_tokens=event.old_context_tokens,
|
||||
new_tokens=event.new_context_tokens,
|
||||
old_session_id=event.old_session_id,
|
||||
new_session_id=event.new_session_id,
|
||||
old_session_id=event.old_session_id, new_session_id=event.new_session_id
|
||||
)
|
||||
self.current_compact = None
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from __future__ import annotations
|
|||
from textual.message import Message
|
||||
|
||||
from vibe.cli.textual_ui.widgets.status_message import StatusMessage
|
||||
from vibe.core.utils import compact_reduction_display
|
||||
from vibe.core.utils import compact_complete_display
|
||||
|
||||
|
||||
class CompactMessage(StatusMessage):
|
||||
|
|
@ -15,8 +15,6 @@ class CompactMessage(StatusMessage):
|
|||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.add_class("compact-message")
|
||||
self.old_tokens: int | None = None
|
||||
self.new_tokens: int | None = None
|
||||
self.old_session_id: str | None = None
|
||||
self.new_session_id: str | None = None
|
||||
self.error_message: str | None = None
|
||||
|
|
@ -28,23 +26,13 @@ class CompactMessage(StatusMessage):
|
|||
if self.error_message:
|
||||
return f"Error: {self.error_message}"
|
||||
|
||||
return compact_reduction_display(
|
||||
self.old_tokens,
|
||||
self.new_tokens,
|
||||
old_session_id=self.old_session_id,
|
||||
new_session_id=self.new_session_id,
|
||||
return compact_complete_display(
|
||||
old_session_id=self.old_session_id, new_session_id=self.new_session_id
|
||||
)
|
||||
|
||||
def set_complete(
|
||||
self,
|
||||
old_tokens: int | None = None,
|
||||
new_tokens: int | None = None,
|
||||
*,
|
||||
old_session_id: str | None = None,
|
||||
new_session_id: str | None = None,
|
||||
self, *, old_session_id: str | None = None, new_session_id: str | None = None
|
||||
) -> None:
|
||||
self.old_tokens = old_tokens
|
||||
self.new_tokens = new_tokens
|
||||
self.old_session_id = old_session_id
|
||||
self.new_session_id = new_session_id
|
||||
self.stop_spinning(success=True)
|
||||
|
|
|
|||
|
|
@ -810,7 +810,6 @@ class AgentLoop: # noqa: PLR0904
|
|||
)
|
||||
|
||||
compact_status: Literal["success", "failure", "cancelled"] = "success"
|
||||
new_tokens = self.stats.context_tokens
|
||||
try:
|
||||
summary = await self.compact()
|
||||
except asyncio.CancelledError:
|
||||
|
|
@ -820,10 +819,8 @@ class AgentLoop: # noqa: PLR0904
|
|||
compact_status = "failure"
|
||||
raise
|
||||
finally:
|
||||
new_tokens = self.stats.context_tokens
|
||||
self.telemetry_client.send_auto_compact_triggered(
|
||||
nb_context_tokens_before=old_tokens,
|
||||
nb_context_tokens_after=new_tokens,
|
||||
auto_compact_threshold=threshold,
|
||||
status=compact_status,
|
||||
session_id=old_session_id,
|
||||
|
|
@ -832,8 +829,6 @@ class AgentLoop: # noqa: PLR0904
|
|||
|
||||
yield CompactEndEvent(
|
||||
tool_call_id=tool_call_id,
|
||||
old_context_tokens=old_tokens,
|
||||
new_context_tokens=new_tokens,
|
||||
summary_length=len(summary),
|
||||
old_session_id=old_session_id,
|
||||
new_session_id=self.session_id,
|
||||
|
|
@ -1724,21 +1719,16 @@ class AgentLoop: # noqa: PLR0904
|
|||
|
||||
system_message = self.messages[0]
|
||||
wrapped_summary = f"{summary_prefix}\n{summary_content}"
|
||||
summary_message = LLMMessage(role=Role.user, content=wrapped_summary)
|
||||
summary_message = LLMMessage(
|
||||
role=Role.user, content=wrapped_summary, injected=True
|
||||
)
|
||||
self.messages.reset([system_message, *prior_user_messages, summary_message])
|
||||
|
||||
active_model = self.config.get_active_model()
|
||||
await self._reset_session()
|
||||
|
||||
actual_context_tokens = await self.backend.count_tokens(
|
||||
model=active_model,
|
||||
messages=self.messages,
|
||||
tools=self.format_handler.get_available_tools(self.tool_manager),
|
||||
extra_headers=self._get_extra_headers(),
|
||||
metadata=self._build_backend_metadata().model_dump(exclude_none=True),
|
||||
)
|
||||
|
||||
self.stats.context_tokens = actual_context_tokens
|
||||
# Context size is unknown without an API call; reset to 0. The next
|
||||
# LLM turn recomputes it accurately from real usage (_update_stats).
|
||||
self.stats.context_tokens = 0
|
||||
await self.session_logger.save_interaction(
|
||||
self.messages,
|
||||
self.stats,
|
||||
|
|
|
|||
|
|
@ -34,11 +34,13 @@ def collect_prior_user_messages(
|
|||
content = m.content or ""
|
||||
cost = approx_token_count(content)
|
||||
if cost <= remaining:
|
||||
selected.append(LLMMessage(role=Role.user, content=content))
|
||||
selected.append(LLMMessage(role=Role.user, content=content, injected=True))
|
||||
remaining -= cost
|
||||
else:
|
||||
truncated = truncate_middle_to_tokens(content, remaining)
|
||||
selected.append(LLMMessage(role=Role.user, content=truncated))
|
||||
selected.append(
|
||||
LLMMessage(role=Role.user, content=truncated, injected=True)
|
||||
)
|
||||
remaining = 0
|
||||
|
||||
selected.reverse()
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ from vibe.core.config._settings import (
|
|||
DEFAULT_TRANSCRIBE_PROVIDERS,
|
||||
DEFAULT_TTS_MODELS,
|
||||
DEFAULT_TTS_PROVIDERS,
|
||||
DEFAULT_VIBE_BASE_URL,
|
||||
THINKING_LEVELS,
|
||||
ConnectorConfig,
|
||||
ExperimentsConfig,
|
||||
|
|
@ -76,6 +77,7 @@ __all__ = [
|
|||
"DEFAULT_TRANSCRIBE_PROVIDERS",
|
||||
"DEFAULT_TTS_MODELS",
|
||||
"DEFAULT_TTS_PROVIDERS",
|
||||
"DEFAULT_VIBE_BASE_URL",
|
||||
"THINKING_LEVELS",
|
||||
"AppendToList",
|
||||
"ConfigDefinitionError",
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ from vibe.core.logger import logger
|
|||
from vibe.core.paths import GLOBAL_ENV_FILE, SESSION_LOG_DIR
|
||||
from vibe.core.prompts import UtilityPrompt, load_prompt, load_system_prompt
|
||||
from vibe.core.types import Backend
|
||||
from vibe.core.utils import get_server_url_from_api_base
|
||||
from vibe.core.utils import configure_ssl_context, get_server_url_from_api_base
|
||||
|
||||
|
||||
def _strip_bash_pattern_wildcard(pattern: str) -> str:
|
||||
|
|
@ -165,6 +165,7 @@ DEFAULT_MISTRAL_API_ENV_KEY = "MISTRAL_API_KEY"
|
|||
DEFAULT_MISTRAL_BROWSER_AUTH_BASE_URL = "https://console.mistral.ai"
|
||||
DEFAULT_MISTRAL_BROWSER_AUTH_API_BASE_URL = "https://console.mistral.ai/api"
|
||||
DEFAULT_CONSOLE_BASE_URL = "https://console.mistral.ai"
|
||||
DEFAULT_VIBE_BASE_URL = "https://chat.mistral.ai"
|
||||
|
||||
|
||||
class ProviderConfig(BaseModel):
|
||||
|
|
@ -520,6 +521,7 @@ class VibeConfig(BaseSettings):
|
|||
enable_update_checks: bool = True
|
||||
enable_auto_update: bool = True
|
||||
enable_notifications: bool = True
|
||||
enable_system_trust_store: bool = False
|
||||
api_timeout: float = 720.0
|
||||
auto_compact_threshold: int = 200_000
|
||||
|
||||
|
|
@ -537,6 +539,7 @@ class VibeConfig(BaseSettings):
|
|||
otel_endpoint: str = Field(default="", exclude=True)
|
||||
|
||||
console_base_url: str = Field(default=DEFAULT_CONSOLE_BASE_URL, exclude=True)
|
||||
vibe_base_url: str = Field(default=DEFAULT_VIBE_BASE_URL, exclude=True)
|
||||
|
||||
enable_experimental_hooks: bool = Field(default=False, exclude=True)
|
||||
|
||||
|
|
@ -1064,7 +1067,11 @@ class VibeConfig(BaseSettings):
|
|||
@classmethod
|
||||
def load(cls, **overrides: Any) -> VibeConfig:
|
||||
cls._migrate()
|
||||
return cls(**(overrides or {}))
|
||||
config = cls(**(overrides or {}))
|
||||
configure_ssl_context(
|
||||
enable_system_trust_store=config.enable_system_trust_store
|
||||
)
|
||||
return config
|
||||
|
||||
@classmethod
|
||||
def create_default(cls) -> dict[str, Any]:
|
||||
|
|
|
|||
|
|
@ -324,12 +324,6 @@ class AnthropicAdapter(APIAdapter):
|
|||
"prompt-caching-2024-07-31,"
|
||||
"context-1m-2025-08-07"
|
||||
)
|
||||
THINKING_BUDGETS: ClassVar[dict[str, int]] = {
|
||||
"low": 1024,
|
||||
"medium": 10_000,
|
||||
"high": 32_000,
|
||||
"max": 128_000,
|
||||
}
|
||||
DEFAULT_ADAPTIVE_MAX_TOKENS: ClassVar[int] = 32_768
|
||||
DEFAULT_MAX_TOKENS = 8192
|
||||
|
||||
|
|
@ -375,66 +369,32 @@ class AnthropicAdapter(APIAdapter):
|
|||
if last_block.get("type") in {"text", "image", "tool_result"}:
|
||||
last_block["cache_control"] = {"type": "ephemeral"}
|
||||
|
||||
# Anthropic models that require the `thinking={"type":"adaptive"}` +
|
||||
# `output_config.effort` shape and reject the older
|
||||
# `thinking={"type":"enabled","budget_tokens":...}` shape. Add new
|
||||
# adaptive-only model families here as Anthropic ships them.
|
||||
ADAPTIVE_MODEL_TAGS: ClassVar[frozenset[str]] = frozenset({"opus-4-6", "opus-4-7"})
|
||||
|
||||
# Anthropic models that have deprecated the `temperature` parameter and
|
||||
# reject any payload containing it. Add new families here as Anthropic
|
||||
# ships them.
|
||||
TEMPERATURE_DEPRECATED_MODEL_TAGS: ClassVar[frozenset[str]] = frozenset({
|
||||
"opus-4-7"
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def _is_adaptive_model(cls, model_name: str) -> bool:
|
||||
return any(tag in model_name for tag in cls.ADAPTIVE_MODEL_TAGS)
|
||||
|
||||
@classmethod
|
||||
def _is_temperature_deprecated_model(cls, model_name: str) -> bool:
|
||||
return any(tag in model_name for tag in cls.TEMPERATURE_DEPRECATED_MODEL_TAGS)
|
||||
|
||||
def _apply_thinking_config(
|
||||
self,
|
||||
payload: dict[str, Any],
|
||||
*,
|
||||
model_name: str,
|
||||
messages: list[dict[str, Any]],
|
||||
temperature: float,
|
||||
max_tokens: int | None,
|
||||
thinking: str,
|
||||
) -> None:
|
||||
has_thinking = self._has_thinking_content(messages)
|
||||
thinking_level = thinking
|
||||
temperature_deprecated = self._is_temperature_deprecated_model(model_name)
|
||||
|
||||
if thinking_level == "off" and not has_thinking:
|
||||
if not temperature_deprecated:
|
||||
payload["temperature"] = temperature
|
||||
if max_tokens is not None:
|
||||
payload["max_tokens"] = max_tokens
|
||||
else:
|
||||
payload["max_tokens"] = self.DEFAULT_MAX_TOKENS
|
||||
payload["max_tokens"] = (
|
||||
max_tokens if max_tokens is not None else self.DEFAULT_MAX_TOKENS
|
||||
)
|
||||
return
|
||||
|
||||
# Resolve effective level: use config, or fallback to "medium" when
|
||||
# forced by thinking content in history
|
||||
effective_level = thinking_level if thinking_level != "off" else "medium"
|
||||
|
||||
if self._is_adaptive_model(model_name):
|
||||
payload["thinking"] = {"type": "adaptive", "display": "summarized"}
|
||||
payload["output_config"] = {"effort": effective_level}
|
||||
default_max = self.DEFAULT_ADAPTIVE_MAX_TOKENS
|
||||
else:
|
||||
budget = self.THINKING_BUDGETS[effective_level]
|
||||
payload["thinking"] = {"type": "enabled", "budget_tokens": budget}
|
||||
default_max = budget + self.DEFAULT_MAX_TOKENS
|
||||
|
||||
if not temperature_deprecated:
|
||||
payload["temperature"] = 1
|
||||
payload["max_tokens"] = max_tokens if max_tokens is not None else default_max
|
||||
payload["thinking"] = {"type": "adaptive", "display": "summarized"}
|
||||
payload["output_config"] = {"effort": effective_level}
|
||||
payload["max_tokens"] = (
|
||||
max_tokens if max_tokens is not None else self.DEFAULT_ADAPTIVE_MAX_TOKENS
|
||||
)
|
||||
|
||||
def _build_payload(
|
||||
self,
|
||||
|
|
@ -442,7 +402,6 @@ class AnthropicAdapter(APIAdapter):
|
|||
model_name: str,
|
||||
system_prompt: str | None,
|
||||
messages: list[dict[str, Any]],
|
||||
temperature: float,
|
||||
tools: list[dict[str, Any]] | None,
|
||||
max_tokens: int | None,
|
||||
tool_choice: dict[str, Any] | None,
|
||||
|
|
@ -452,12 +411,7 @@ class AnthropicAdapter(APIAdapter):
|
|||
payload: dict[str, Any] = {"model": model_name, "messages": messages}
|
||||
|
||||
self._apply_thinking_config(
|
||||
payload,
|
||||
model_name=model_name,
|
||||
messages=messages,
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens,
|
||||
thinking=thinking,
|
||||
payload, messages=messages, max_tokens=max_tokens, thinking=thinking
|
||||
)
|
||||
|
||||
if system_blocks := self._build_system_blocks(system_prompt):
|
||||
|
|
@ -498,7 +452,6 @@ class AnthropicAdapter(APIAdapter):
|
|||
model_name=model_name,
|
||||
system_prompt=system_prompt,
|
||||
messages=converted_messages,
|
||||
temperature=temperature,
|
||||
tools=converted_tools,
|
||||
max_tokens=max_tokens,
|
||||
tool_choice=converted_tool_choice,
|
||||
|
|
|
|||
|
|
@ -416,35 +416,6 @@ class GenericBackend:
|
|||
return
|
||||
yield json.loads(value.strip())
|
||||
|
||||
async def count_tokens(
|
||||
self,
|
||||
*,
|
||||
model: ModelConfig,
|
||||
messages: Sequence[LLMMessage],
|
||||
temperature: float = 0.0,
|
||||
tools: list[AvailableTool] | None = None,
|
||||
tool_choice: StrToolChoice | AvailableTool | None = None,
|
||||
extra_headers: dict[str, str] | None = None,
|
||||
metadata: dict[str, str] | None = None,
|
||||
) -> int:
|
||||
probe_messages = list(messages)
|
||||
if not probe_messages or probe_messages[-1].role != Role.user:
|
||||
probe_messages.append(LLMMessage(role=Role.user, content=""))
|
||||
|
||||
result = await self.complete(
|
||||
model=model,
|
||||
messages=probe_messages,
|
||||
temperature=temperature,
|
||||
tools=tools,
|
||||
max_tokens=16, # Minimal amount for openrouter with openai models
|
||||
tool_choice=tool_choice,
|
||||
extra_headers=extra_headers,
|
||||
)
|
||||
if result.usage is None:
|
||||
raise ValueError("Missing usage in non streaming completion")
|
||||
|
||||
return result.usage.prompt_tokens
|
||||
|
||||
async def close(self) -> None:
|
||||
if self._owns_client and self._client:
|
||||
await self._client.aclose()
|
||||
|
|
|
|||
|
|
@ -426,29 +426,3 @@ class MistralBackend:
|
|||
has_tools=bool(tools),
|
||||
tool_choice=tool_choice,
|
||||
) from e
|
||||
|
||||
async def count_tokens(
|
||||
self,
|
||||
*,
|
||||
model: ModelConfig,
|
||||
messages: Sequence[LLMMessage],
|
||||
temperature: float = 0.0,
|
||||
tools: list[AvailableTool] | None = None,
|
||||
tool_choice: StrToolChoice | AvailableTool | None = None,
|
||||
extra_headers: dict[str, str] | None = None,
|
||||
metadata: dict[str, str] | None = None,
|
||||
) -> int:
|
||||
result = await self.complete(
|
||||
model=model,
|
||||
messages=messages,
|
||||
temperature=temperature,
|
||||
tools=tools,
|
||||
max_tokens=1,
|
||||
tool_choice=tool_choice,
|
||||
extra_headers=extra_headers,
|
||||
metadata=metadata,
|
||||
)
|
||||
if result.usage is None:
|
||||
raise ValueError("Missing usage in non streaming completion")
|
||||
|
||||
return result.usage.prompt_tokens
|
||||
|
|
|
|||
|
|
@ -96,9 +96,7 @@ class VertexAnthropicAdapter(AnthropicAdapter):
|
|||
}
|
||||
self._apply_thinking_config(
|
||||
payload,
|
||||
model_name=model_name,
|
||||
messages=converted_messages,
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens,
|
||||
thinking=thinking,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ class BackendLike(Protocol):
|
|||
"""Port protocol for dependency-injectable LLM backends.
|
||||
|
||||
Any backend used by AgentLoop should implement this async context manager
|
||||
interface with `complete`, `complete_streaming` and `count_tokens` methods.
|
||||
interface with `complete` and `complete_streaming` methods.
|
||||
"""
|
||||
|
||||
async def __aenter__(self) -> BackendLike: ...
|
||||
|
|
@ -92,35 +92,3 @@ class BackendLike(Protocol):
|
|||
BackendError: If the API request fails
|
||||
"""
|
||||
...
|
||||
|
||||
async def count_tokens(
|
||||
self,
|
||||
*,
|
||||
model: ModelConfig,
|
||||
messages: Sequence[LLMMessage],
|
||||
temperature: float = 0.0,
|
||||
tools: list[AvailableTool] | None,
|
||||
tool_choice: StrToolChoice | AvailableTool | None = None,
|
||||
extra_headers: dict[str, str] | None,
|
||||
metadata: dict[str, str] | None = None,
|
||||
) -> int:
|
||||
"""Count the number of tokens in the prompt without generating a real response.
|
||||
|
||||
This is useful for:
|
||||
- Determining system prompt token count
|
||||
- Checking context size after compaction
|
||||
- Pre-flight token validation
|
||||
|
||||
Args:
|
||||
model: Model configuration
|
||||
messages: List of messages to count tokens for
|
||||
temperature: Sampling temperature
|
||||
tools: Optional list of available tools
|
||||
tool_choice: How to choose tools
|
||||
extra_headers: Additional HTTP headers to include
|
||||
metadata: Optional metadata to attach to the request
|
||||
|
||||
Returns:
|
||||
The number of prompt tokens
|
||||
"""
|
||||
...
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ enable_telemetry = true
|
|||
enable_update_checks = true
|
||||
enable_auto_update = true
|
||||
enable_notifications = true
|
||||
enable_system_trust_store = false # Use OS trust store for outbound HTTPS
|
||||
api_timeout = 720.0 # API request timeout in seconds
|
||||
auto_compact_threshold = 200000 # Token count before auto-compaction
|
||||
|
||||
|
|
@ -267,6 +268,13 @@ browser_auth_base_url = "https://console.mistral.ai"
|
|||
browser_auth_api_base_url = "https://console.mistral.ai/api"
|
||||
```
|
||||
|
||||
Self-hosted deployments can point Vibe CLI upgrade and API-key links to their
|
||||
Le Chat web deployment, where the Vibe API key is managed:
|
||||
|
||||
```toml
|
||||
vibe_base_url = "https://chat.mistral.ai"
|
||||
```
|
||||
|
||||
### Hooks (Experimental)
|
||||
|
||||
Hooks let users run shell commands automatically at specific points during a
|
||||
|
|
|
|||
|
|
@ -245,7 +245,6 @@ class TelemetryClient:
|
|||
self,
|
||||
*,
|
||||
nb_context_tokens_before: int,
|
||||
nb_context_tokens_after: int,
|
||||
auto_compact_threshold: int,
|
||||
status: Literal["success", "failure", "cancelled"],
|
||||
session_id: str | None = None,
|
||||
|
|
@ -253,7 +252,6 @@ class TelemetryClient:
|
|||
) -> None:
|
||||
payload = {
|
||||
"nb_context_tokens_before": nb_context_tokens_before,
|
||||
"nb_context_tokens_after": nb_context_tokens_after,
|
||||
"auto_compact_threshold": auto_compact_threshold,
|
||||
"status": status,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ from vibe.core.tools.mcp.tools import (
|
|||
call_tool_stdio,
|
||||
create_mcp_http_proxy_tool_class,
|
||||
create_mcp_stdio_proxy_tool_class,
|
||||
create_vibe_mcp_http_client,
|
||||
list_tools_http,
|
||||
list_tools_stdio,
|
||||
)
|
||||
|
|
@ -26,6 +27,7 @@ __all__ = [
|
|||
"call_tool_stdio",
|
||||
"create_mcp_http_proxy_tool_class",
|
||||
"create_mcp_stdio_proxy_tool_class",
|
||||
"create_vibe_mcp_http_client",
|
||||
"list_tools_http",
|
||||
"list_tools_stdio",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -9,11 +9,12 @@ from pathlib import Path
|
|||
import threading
|
||||
from typing import TYPE_CHECKING, Any, ClassVar, TextIO
|
||||
|
||||
import httpx
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
|
||||
from mcp import ClientSession
|
||||
from mcp.client.stdio import StdioServerParameters, stdio_client
|
||||
from mcp.client.streamable_http import streamablehttp_client
|
||||
from mcp.client.streamable_http import streamable_http_client
|
||||
from vibe.core.logger import logger
|
||||
from vibe.core.tools.base import (
|
||||
BaseTool,
|
||||
|
|
@ -25,11 +26,18 @@ from vibe.core.tools.base import (
|
|||
from vibe.core.tools.mcp_sampling import MCPSamplingHandler
|
||||
from vibe.core.tools.ui import ToolResultDisplay, ToolUIData
|
||||
from vibe.core.types import ToolStreamEvent
|
||||
from vibe.core.utils.http import build_ssl_context
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from vibe.core.types import ToolResultEvent
|
||||
|
||||
|
||||
# Mirrors MCP's default Streamable HTTP timeout values while avoiding an import from
|
||||
# mcp.shared._httpx_utils, which is an internal module.
|
||||
_MCP_DEFAULT_TIMEOUT = 30.0
|
||||
_MCP_DEFAULT_SSE_READ_TIMEOUT = 300.0
|
||||
|
||||
|
||||
def _stderr_logger_thread(read_fd: int) -> None:
|
||||
with open(read_fd, "rb") as f:
|
||||
for line in iter(f.readline, b""):
|
||||
|
|
@ -167,6 +175,15 @@ def _parse_call_result(server: str, tool: str, result_obj: Any) -> MCPToolResult
|
|||
return MCPToolResult(server=server, tool=tool, text=text, structured=None)
|
||||
|
||||
|
||||
def create_vibe_mcp_http_client(headers: dict[str, str] | None) -> httpx.AsyncClient:
|
||||
return httpx.AsyncClient(
|
||||
follow_redirects=True,
|
||||
headers=headers,
|
||||
timeout=httpx.Timeout(_MCP_DEFAULT_TIMEOUT, read=_MCP_DEFAULT_SSE_READ_TIMEOUT),
|
||||
verify=build_ssl_context(),
|
||||
)
|
||||
|
||||
|
||||
async def list_tools_http(
|
||||
url: str,
|
||||
*,
|
||||
|
|
@ -174,11 +191,18 @@ async def list_tools_http(
|
|||
startup_timeout_sec: float | None = None,
|
||||
) -> list[RemoteTool]:
|
||||
timeout = timedelta(seconds=startup_timeout_sec) if startup_timeout_sec else None
|
||||
async with streamablehttp_client(url, headers=headers) as (read, write, _):
|
||||
async with ClientSession(read, write, read_timeout_seconds=timeout) as session:
|
||||
await session.initialize()
|
||||
tools_resp = await session.list_tools()
|
||||
return [RemoteTool.model_validate(t) for t in tools_resp.tools]
|
||||
async with create_vibe_mcp_http_client(headers) as http_client:
|
||||
async with streamable_http_client(url, http_client=http_client) as (
|
||||
read,
|
||||
write,
|
||||
_,
|
||||
):
|
||||
async with ClientSession(
|
||||
read, write, read_timeout_seconds=timeout
|
||||
) as session:
|
||||
await session.initialize()
|
||||
tools_resp = await session.list_tools()
|
||||
return [RemoteTool.model_validate(t) for t in tools_resp.tools]
|
||||
|
||||
|
||||
async def call_tool_http(
|
||||
|
|
@ -195,18 +219,23 @@ async def call_tool_http(
|
|||
timedelta(seconds=startup_timeout_sec) if startup_timeout_sec else None
|
||||
)
|
||||
call_timeout = timedelta(seconds=tool_timeout_sec) if tool_timeout_sec else None
|
||||
async with streamablehttp_client(url, headers=headers) as (read, write, _):
|
||||
async with ClientSession(
|
||||
async with create_vibe_mcp_http_client(headers) as http_client:
|
||||
async with streamable_http_client(url, http_client=http_client) as (
|
||||
read,
|
||||
write,
|
||||
read_timeout_seconds=init_timeout,
|
||||
sampling_callback=sampling_callback,
|
||||
) as session:
|
||||
await session.initialize()
|
||||
result = await session.call_tool(
|
||||
tool_name, arguments, read_timeout_seconds=call_timeout
|
||||
)
|
||||
return _parse_call_result(url, tool_name, result)
|
||||
_,
|
||||
):
|
||||
async with ClientSession(
|
||||
read,
|
||||
write,
|
||||
read_timeout_seconds=init_timeout,
|
||||
sampling_callback=sampling_callback,
|
||||
) as session:
|
||||
await session.initialize()
|
||||
result = await session.call_tool(
|
||||
tool_name, arguments, read_timeout_seconds=call_timeout
|
||||
)
|
||||
return _parse_call_result(url, tool_name, result)
|
||||
|
||||
|
||||
def create_mcp_http_proxy_tool_class(
|
||||
|
|
|
|||
|
|
@ -430,8 +430,6 @@ class CompactStartEvent(BaseEvent):
|
|||
|
||||
|
||||
class CompactEndEvent(BaseEvent):
|
||||
old_context_tokens: int
|
||||
new_context_tokens: int
|
||||
summary_length: int
|
||||
old_session_id: str | None = None
|
||||
new_session_id: str | None = None
|
||||
|
|
|
|||
|
|
@ -12,9 +12,10 @@ from vibe.core.utils.concurrency import (
|
|||
ConversationLimitException,
|
||||
run_sync,
|
||||
)
|
||||
from vibe.core.utils.display import compact_reduction_display
|
||||
from vibe.core.utils.display import compact_complete_display
|
||||
from vibe.core.utils.http import (
|
||||
build_ssl_context,
|
||||
configure_ssl_context,
|
||||
get_server_url_from_api_base,
|
||||
get_user_agent,
|
||||
)
|
||||
|
|
@ -55,7 +56,8 @@ __all__ = [
|
|||
"async_generator_retry",
|
||||
"async_retry",
|
||||
"build_ssl_context",
|
||||
"compact_reduction_display",
|
||||
"compact_complete_display",
|
||||
"configure_ssl_context",
|
||||
"get_platform_display_name",
|
||||
"get_platform_id",
|
||||
"get_server_url_from_api_base",
|
||||
|
|
|
|||
|
|
@ -3,23 +3,11 @@ from __future__ import annotations
|
|||
from vibe.core.session.session_id import shorten_session_id
|
||||
|
||||
|
||||
def compact_reduction_display(
|
||||
old_tokens: int | None,
|
||||
new_tokens: int | None,
|
||||
*,
|
||||
old_session_id: str | None = None,
|
||||
new_session_id: str | None = None,
|
||||
def compact_complete_display(
|
||||
old_session_id: str | None = None, new_session_id: str | None = None
|
||||
) -> str:
|
||||
|
||||
message = "Compaction complete"
|
||||
if old_tokens is not None and new_tokens is not None:
|
||||
reduction = old_tokens - new_tokens
|
||||
reduction_pct = (reduction / old_tokens * 100) if old_tokens > 0 else 0
|
||||
message = (
|
||||
f"{message}: {old_tokens:,} → "
|
||||
f"{new_tokens:,} tokens ({-reduction_pct:+#0.2g}%)"
|
||||
)
|
||||
|
||||
message = "Compaction completed."
|
||||
if old_session_id is not None and new_session_id is not None:
|
||||
short_old = shorten_session_id(old_session_id)
|
||||
short_new = shorten_session_id(new_session_id)
|
||||
|
|
|
|||
|
|
@ -6,14 +6,28 @@ import re
|
|||
import ssl
|
||||
|
||||
import certifi
|
||||
import truststore
|
||||
|
||||
from vibe import __version__
|
||||
from vibe.core.types import Backend
|
||||
|
||||
_use_system_trust_store = False
|
||||
|
||||
|
||||
def configure_ssl_context(*, enable_system_trust_store: bool) -> None:
|
||||
global _use_system_trust_store
|
||||
if _use_system_trust_store == enable_system_trust_store:
|
||||
return
|
||||
_use_system_trust_store = enable_system_trust_store
|
||||
build_ssl_context.cache_clear()
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=1)
|
||||
def build_ssl_context() -> ssl.SSLContext:
|
||||
ctx = ssl.create_default_context(cafile=certifi.where())
|
||||
if _use_system_trust_store:
|
||||
ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
||||
else:
|
||||
ctx = ssl.create_default_context(cafile=certifi.where())
|
||||
|
||||
# Custom certs are additive so private-CA users don't lose public roots.
|
||||
ssl_cert_file = os.getenv("SSL_CERT_FILE")
|
||||
|
|
|
|||
|
|
@ -128,6 +128,7 @@ def assess_auth_state(
|
|||
return _auth_state(
|
||||
AuthStateKind.VIBE_HOME_ENV_FILE_OVERRIDES_PROCESS_ENV,
|
||||
can_use_active_provider=True,
|
||||
sign_out_available=True,
|
||||
env_key=env_key,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ class OnboardingApp(App[str | None]):
|
|||
|
||||
self._config = config
|
||||
self._provider = config.provider
|
||||
self._vibe_base_url = config.vibe_base_url
|
||||
self._entrypoint_metadata = entrypoint_metadata
|
||||
self._browser_sign_in_success_delay = browser_sign_in_success_delay
|
||||
self._browser_sign_in_service_factory = self._resolve_browser_sign_in_factory(
|
||||
|
|
@ -58,7 +59,11 @@ class OnboardingApp(App[str | None]):
|
|||
ThemeSelectionScreen(next_screen=theme_next), "theme_selection"
|
||||
)
|
||||
self.install_screen(
|
||||
ApiKeyScreen(self._provider, entrypoint_metadata=self._entrypoint_metadata),
|
||||
ApiKeyScreen(
|
||||
self._provider,
|
||||
vibe_base_url=self._vibe_base_url,
|
||||
entrypoint_metadata=self._entrypoint_metadata,
|
||||
),
|
||||
"api_key",
|
||||
)
|
||||
if self._browser_sign_in_service_factory is not None:
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ from vibe.core.config._settings import (
|
|||
DEFAULT_ACTIVE_MODEL,
|
||||
DEFAULT_MODELS,
|
||||
DEFAULT_PROVIDERS,
|
||||
DEFAULT_VIBE_BASE_URL,
|
||||
)
|
||||
from vibe.core.config.harness_files import get_harness_files_manager
|
||||
from vibe.core.logger import logger
|
||||
|
|
@ -29,6 +30,7 @@ def _default_model_payloads() -> list[dict[str, Any]]:
|
|||
|
||||
class _OnboardingSnapshot(BaseModel):
|
||||
active_model: str = DEFAULT_ACTIVE_MODEL
|
||||
vibe_base_url: str = DEFAULT_VIBE_BASE_URL
|
||||
providers: list[Any] = Field(default_factory=_default_provider_payloads)
|
||||
models: list[Any] = Field(default_factory=_default_model_payloads)
|
||||
|
||||
|
|
@ -97,6 +99,12 @@ def _load_onboarding_env_payload_for_fields(
|
|||
and (models := _find_env_value("VIBE_MODELS")) is not None
|
||||
):
|
||||
payload["models"] = _ONBOARDING_LIST_ADAPTER.validate_json(models)
|
||||
if (
|
||||
"vibe_base_url" in field_names
|
||||
and (vibe_base_url := _find_env_value("VIBE_VIBE_BASE_URL")) is not None
|
||||
):
|
||||
payload["vibe_base_url"] = vibe_base_url
|
||||
|
||||
return payload
|
||||
|
||||
|
||||
|
|
@ -180,6 +188,7 @@ def _resolve_provider(
|
|||
@dataclass(frozen=True)
|
||||
class OnboardingContext:
|
||||
provider: ProviderConfig
|
||||
vibe_base_url: str = DEFAULT_VIBE_BASE_URL
|
||||
|
||||
@property
|
||||
def supports_browser_sign_in(self) -> bool:
|
||||
|
|
@ -187,7 +196,9 @@ class OnboardingContext:
|
|||
|
||||
@classmethod
|
||||
def from_config(cls, config: VibeConfig) -> OnboardingContext:
|
||||
return cls(provider=config.get_active_provider())
|
||||
return cls(
|
||||
provider=config.get_active_provider(), vibe_base_url=config.vibe_base_url
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def load(cls, **overrides: Any) -> OnboardingContext:
|
||||
|
|
@ -198,7 +209,8 @@ class OnboardingContext:
|
|||
return cls(
|
||||
provider=_resolve_provider(
|
||||
active_model=snapshot.active_model, snapshot=snapshot
|
||||
)
|
||||
),
|
||||
vibe_base_url=snapshot.vibe_base_url,
|
||||
)
|
||||
except (RuntimeError, ValidationError, ValueError):
|
||||
logger.warning(
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ from textual.widgets import Input, Link
|
|||
from vibe.cli.clipboard import copy_selection_to_clipboard
|
||||
from vibe.cli.textual_ui.widgets.banner.petit_chat import PetitChat
|
||||
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
|
||||
from vibe.core.config import ProviderConfig
|
||||
from vibe.core.config import DEFAULT_VIBE_BASE_URL, ProviderConfig
|
||||
from vibe.core.telemetry.types import EntrypointMetadata
|
||||
from vibe.setup.auth.api_key_persistence import (
|
||||
persist_api_key,
|
||||
|
|
@ -20,8 +20,8 @@ from vibe.setup.auth.api_key_persistence import (
|
|||
)
|
||||
from vibe.setup.onboarding.base import OnboardingScreen
|
||||
|
||||
# providers with a dedicated key creation page get a direct onboarding link
|
||||
PROVIDER_HELP = {"mistral": ("https://console.mistral.ai/codestral/cli", "AI Studio")}
|
||||
MISTRAL_PROVIDER_NAME = "mistral"
|
||||
MISTRAL_PROVIDER_HELP_NAME = "Mistral Vibe"
|
||||
CONFIG_DOCS_URL = (
|
||||
"https://github.com/mistralai/mistral-vibe?tab=readme-ov-file#configuration"
|
||||
)
|
||||
|
|
@ -39,17 +39,19 @@ class ApiKeyScreen(OnboardingScreen):
|
|||
self,
|
||||
provider: ProviderConfig | None = None,
|
||||
*,
|
||||
vibe_base_url: str = DEFAULT_VIBE_BASE_URL,
|
||||
entrypoint_metadata: EntrypointMetadata | None = None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.provider = resolve_api_key_provider(provider)
|
||||
self._vibe_base_url = vibe_base_url
|
||||
self._entrypoint_metadata = entrypoint_metadata
|
||||
|
||||
def _compose_provider_link(self) -> ComposeResult:
|
||||
if (help_info := PROVIDER_HELP.get(self.provider.name)) is None:
|
||||
if self.provider.name != MISTRAL_PROVIDER_NAME:
|
||||
return
|
||||
|
||||
help_url, _ = help_info
|
||||
help_url = f"{self._vibe_base_url.rstrip('/')}/code/extensions?focus=key"
|
||||
yield Link(help_url, url=help_url, id="api-key-provider-link")
|
||||
|
||||
def _compose_config_docs(self) -> ComposeResult:
|
||||
|
|
@ -60,8 +62,11 @@ class ApiKeyScreen(OnboardingScreen):
|
|||
|
||||
def compose(self) -> ComposeResult:
|
||||
provider_name = self.provider.name.capitalize()
|
||||
help_info = PROVIDER_HELP.get(self.provider.name)
|
||||
help_name = help_info[1] if help_info is not None else "your provider"
|
||||
help_name = (
|
||||
MISTRAL_PROVIDER_HELP_NAME
|
||||
if self.provider.name == MISTRAL_PROVIDER_NAME
|
||||
else "your provider"
|
||||
)
|
||||
|
||||
self.input_widget = Input(
|
||||
password=True,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# What's new in v2.12.1
|
||||
# What's new in v2.13.0
|
||||
|
||||
- **Custom compaction prompts**: Override the default `/compact` prompt by setting `compaction_prompt_id` and dropping a markdown file in `~/.vibe/prompts/` or `.vibe/prompts/`.
|
||||
- **Safer programmatic mode**: `-p` no longer auto-approves tool calls by default — pass `--auto-approve` to restore the previous behavior.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue