Co-Authored-By: Clément Drouin <clement.drouin@mistral.ai>
This commit is contained in:
Michel Thomazo 2025-12-11 09:39:20 +01:00
parent af43abea60
commit a340a721ea
13 changed files with 234 additions and 80 deletions

2
.vscode/launch.json vendored
View file

@ -1,5 +1,5 @@
{ {
"version": "1.1.1", "version": "1.1.2",
"configurations": [ "configurations": [
{ {
"name": "ACP Server", "name": "ACP Server",

View file

@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.1.2] - 2025-12-11
### Changed
- add `terminal-auth` auth method to ACP agent only if the client supports it
- fix `user-agent` header when using Mistral backend, using SDK hook
## [1.1.1] - 2025-12-10 ## [1.1.1] - 2025-12-10
### Changed ### Changed

View file

@ -1,7 +1,7 @@
id = "mistral-vibe" id = "mistral-vibe"
name = "Mistral Vibe" name = "Mistral Vibe"
description = "Mistral's open-source coding assistant" description = "Mistral's open-source coding assistant"
version = "1.1.1" version = "1.1.2"
schema_version = 1 schema_version = 1
authors = ["Mistral AI"] authors = ["Mistral AI"]
repository = "https://github.com/mistralai/mistral-vibe" repository = "https://github.com/mistralai/mistral-vibe"
@ -11,25 +11,25 @@ name = "Mistral Vibe"
icon = "./icons/mistral_vibe.svg" icon = "./icons/mistral_vibe.svg"
[agent_servers.mistral-vibe.targets.darwin-aarch64] [agent_servers.mistral-vibe.targets.darwin-aarch64]
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v1.1.1/vibe-acp-darwin-aarch64-1.1.1.zip" archive = "https://github.com/mistralai/mistral-vibe/releases/download/v1.1.2/vibe-acp-darwin-aarch64-1.1.2.zip"
cmd = "./vibe-acp" cmd = "./vibe-acp"
[agent_servers.mistral-vibe.targets.darwin-x86_64] [agent_servers.mistral-vibe.targets.darwin-x86_64]
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v1.1.1/vibe-acp-darwin-x86_64-1.1.1.zip" archive = "https://github.com/mistralai/mistral-vibe/releases/download/v1.1.2/vibe-acp-darwin-x86_64-1.1.2.zip"
cmd = "./vibe-acp" cmd = "./vibe-acp"
# [agent_servers.mistral-vibe.targets.linux-aarch64] # [agent_servers.mistral-vibe.targets.linux-aarch64]
# archive = "https://github.com/mistralai/mistral-vibe/releases/download/v1.1.1/vibe-acp-linux-aarch64-1.1.1.zip" # archive = "https://github.com/mistralai/mistral-vibe/releases/download/v1.1.2/vibe-acp-linux-aarch64-1.1.2.zip"
# cmd = "./vibe-acp" # cmd = "./vibe-acp"
[agent_servers.mistral-vibe.targets.linux-x86_64] [agent_servers.mistral-vibe.targets.linux-x86_64]
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v1.1.1/vibe-acp-linux-x86_64-1.1.1.zip" archive = "https://github.com/mistralai/mistral-vibe/releases/download/v1.1.2/vibe-acp-linux-x86_64-1.1.2.zip"
cmd = "./vibe-acp" cmd = "./vibe-acp"
# [agent_servers.mistral-vibe.targets.windows-aarch64] # [agent_servers.mistral-vibe.targets.windows-aarch64]
# archive = "https://github.com/mistralai/mistral-vibe/releases/download/v1.1.1/vibe-acp-windows-aarch64-1.1.1.zip" # archive = "https://github.com/mistralai/mistral-vibe/releases/download/v1.1.2/vibe-acp-windows-aarch64-1.1.2.zip"
# cmd = "./vibe-acp.exe" # cmd = "./vibe-acp.exe"
[agent_servers.mistral-vibe.targets.windows-x86_64] [agent_servers.mistral-vibe.targets.windows-x86_64]
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v1.1.1/vibe-acp-windows-x86_64-1.1.1.zip" archive = "https://github.com/mistralai/mistral-vibe/releases/download/v1.1.2/vibe-acp-windows-x86_64-1.1.2.zip"
cmd = "./vibe-acp.exe" cmd = "./vibe-acp.exe"

View file

@ -1,6 +1,6 @@
[project] [project]
name = "mistral-vibe" name = "mistral-vibe"
version = "1.1.1" version = "1.1.2"
description = "Minimal CLI coding agent by Mistral" description = "Minimal CLI coding agent by Mistral"
readme = "README.md" readme = "README.md"
requires-python = ">=3.12" requires-python = ">=3.12"

View file

@ -134,9 +134,9 @@ Examples:
"vibe/core/__init__.py", "vibe/core/__init__.py",
[(f'__version__ = "{current_version}"', f'__version__ = "{new_version}"')], [(f'__version__ = "{current_version}"', f'__version__ = "{new_version}"')],
) )
# Update tests/acp/test_acp.py # Update tests/acp/test_initialize.py
update_hard_values_files( update_hard_values_files(
"tests/acp/test_acp.py", "tests/acp/test_initialize.py",
[(f'version="{current_version}"', f'version="{new_version}"')], [(f'version="{current_version}"', f'version="{new_version}"')],
) )

View file

@ -17,14 +17,10 @@ from acp import (
WriteTextFileRequest, WriteTextFileRequest,
) )
from acp.schema import ( from acp.schema import (
AgentCapabilities,
AllowedOutcome, AllowedOutcome,
DeniedOutcome, DeniedOutcome,
Implementation,
InitializeResponse, InitializeResponse,
McpCapabilities,
NewSessionResponse, NewSessionResponse,
PromptCapabilities,
PromptResponse, PromptResponse,
SessionNotification, SessionNotification,
TextContentBlock, TextContentBlock,
@ -324,48 +320,6 @@ async def initialize_session(acp_agent_process: asyncio.subprocess.Process) -> s
return session_response_obj.result.sessionId return session_response_obj.result.sessionId
class TestInitialization:
@pytest.mark.asyncio
async def test_initialize_request_response(self) -> None:
mock_env = get_mocking_env()
async for process in get_acp_agent_process(mock_env=mock_env):
await send_json_rpc(
process,
InitializeJsonRpcRequest(
id=1, params=InitializeRequest(protocolVersion=1)
),
)
text_response = await read_response(process, timeout=10.0)
assert text_response is not None, "No response to initialize"
response_json = json.loads(text_response)
response = InitializeJsonRpcResponse.model_validate(response_json)
assert response.error is None, f"JSON-RPC error: {response.error}"
assert response.result is not None, "No result in response"
assert response.result.protocolVersion == 1
assert response.result.agentCapabilities == AgentCapabilities(
loadSession=False,
promptCapabilities=PromptCapabilities(
audio=False, embeddedContext=True, image=False
),
mcpCapabilities=McpCapabilities(http=False, sse=False),
)
assert response.result.agentInfo == Implementation(
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="1.1.1"
)
vibe_setup_method = next(
(
method
for method in response.result.authMethods or []
if method.id == "vibe-setup"
),
None,
)
assert vibe_setup_method is not None, "vibe-setup auth not found"
assert vibe_setup_method.field_meta is not None
assert "terminal-auth" in vibe_setup_method.field_meta.keys()
class TestSessionManagement: class TestSessionManagement:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_multiple_sessions_unique_ids(self) -> None: async def test_multiple_sessions_unique_ids(self) -> None:

View file

@ -0,0 +1,80 @@
from __future__ import annotations
from acp import PROTOCOL_VERSION, AgentSideConnection, InitializeRequest
from acp.schema import (
AgentCapabilities,
ClientCapabilities,
Implementation,
PromptCapabilities,
)
import pytest
from tests.stubs.fake_connection import FakeAgentSideConnection
from vibe.acp.acp_agent import VibeAcpAgent
@pytest.fixture
def acp_agent() -> VibeAcpAgent:
vibe_acp_agent: VibeAcpAgent | None = None
def _create_agent(connection: AgentSideConnection) -> VibeAcpAgent:
nonlocal vibe_acp_agent
vibe_acp_agent = VibeAcpAgent(connection)
return vibe_acp_agent
FakeAgentSideConnection(_create_agent)
return vibe_acp_agent # pyright: ignore[reportReturnType]
class TestACPInitialize:
@pytest.mark.asyncio
async def test_initialize(self, acp_agent: VibeAcpAgent) -> None:
"""Test regular initialize without terminal-auth capabilities."""
request = InitializeRequest(protocolVersion=PROTOCOL_VERSION)
response = await acp_agent.initialize(request)
assert response.protocolVersion == PROTOCOL_VERSION
assert response.agentCapabilities == AgentCapabilities(
loadSession=False,
promptCapabilities=PromptCapabilities(
audio=False, embeddedContext=True, image=False
),
)
assert response.agentInfo == Implementation(
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="1.1.2"
)
assert response.authMethods == []
@pytest.mark.asyncio
async def test_initialize_with_terminal_auth(self, acp_agent: VibeAcpAgent) -> None:
"""Test initialize with terminal-auth capabilities to check it was included."""
client_capabilities = ClientCapabilities(field_meta={"terminal-auth": True})
request = InitializeRequest(
protocolVersion=PROTOCOL_VERSION, clientCapabilities=client_capabilities
)
response = await acp_agent.initialize(request)
assert response.protocolVersion == PROTOCOL_VERSION
assert response.agentCapabilities == AgentCapabilities(
loadSession=False,
promptCapabilities=PromptCapabilities(
audio=False, embeddedContext=True, image=False
),
)
assert response.agentInfo == Implementation(
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="1.1.2"
)
assert response.authMethods is not None
assert len(response.authMethods) == 1
auth_method = response.authMethods[0]
assert auth_method.id == "vibe-setup"
assert auth_method.name == "Register your API Key"
assert auth_method.description == "Register your API Key inside Mistral Vibe"
assert auth_method.field_meta is not None
assert "terminal-auth" in auth_method.field_meta
terminal_auth_meta = auth_method.field_meta["terminal-auth"]
assert "command" in terminal_auth_meta
assert "args" in terminal_auth_meta
assert terminal_auth_meta["label"] == "Mistral Vibe Setup"

View file

@ -29,12 +29,14 @@ from tests.backend.data.mistral import (
STREAMED_TOOL_CONVERSATION_PARAMS as MISTRAL_STREAMED_TOOL_CONVERSATION_PARAMS, STREAMED_TOOL_CONVERSATION_PARAMS as MISTRAL_STREAMED_TOOL_CONVERSATION_PARAMS,
TOOL_CONVERSATION_PARAMS as MISTRAL_TOOL_CONVERSATION_PARAMS, TOOL_CONVERSATION_PARAMS as MISTRAL_TOOL_CONVERSATION_PARAMS,
) )
from vibe.core.config import ModelConfig, ProviderConfig from vibe.core.config import Backend, ModelConfig, ProviderConfig
from vibe.core.llm.backend.factory import BACKEND_FACTORY
from vibe.core.llm.backend.generic import GenericBackend from vibe.core.llm.backend.generic import GenericBackend
from vibe.core.llm.backend.mistral import MistralBackend from vibe.core.llm.backend.mistral import MistralBackend
from vibe.core.llm.exceptions import BackendError from vibe.core.llm.exceptions import BackendError
from vibe.core.llm.types import BackendLike from vibe.core.llm.types import BackendLike
from vibe.core.types import LLMChunk, LLMMessage, Role, ToolCall from vibe.core.types import LLMChunk, LLMMessage, Role, ToolCall
from vibe.core.utils import get_user_agent
class TestBackend: class TestBackend:
@ -246,3 +248,99 @@ class TestBackend:
assert e.value.status == response.status_code assert e.value.status == response.status_code
assert e.value.reason == response.reason_phrase assert e.value.reason == response.reason_phrase
assert e.value.parsed_error is None assert e.value.parsed_error is None
@pytest.mark.asyncio
@pytest.mark.parametrize("backend_type", [Backend.MISTRAL, Backend.GENERIC])
async def test_backend_user_agent(self, backend_type: Backend):
user_agent = get_user_agent(backend_type)
base_url = "https://api.example.com"
json_response = {
"id": "fake_id_1234",
"created": 1234567890,
"model": "devstral-latest",
"usage": {
"prompt_tokens": 100,
"total_tokens": 300,
"completion_tokens": 200,
},
"object": "chat.completion",
"choices": [
{
"index": 0,
"finish_reason": "stop",
"message": {
"role": "assistant",
"tool_calls": None,
"content": "Hey",
},
}
],
}
with respx.mock(base_url=base_url) as mock_api:
mock_api.post("/v1/chat/completions").mock(
return_value=httpx.Response(status_code=200, json=json_response)
)
provider = ProviderConfig(
name="provider_name",
api_base=f"{base_url}/v1",
api_key_env_var="API_KEY",
)
backend = BACKEND_FACTORY[backend_type](provider=provider)
model = ModelConfig(
name="model_name", provider="provider_name", alias="model_alias"
)
messages = [LLMMessage(role=Role.user, content="Just say hi")]
await backend.complete(
model=model,
messages=messages,
temperature=0.2,
tools=None,
max_tokens=None,
tool_choice=None,
extra_headers={"user-agent": user_agent},
)
assert mock_api.calls.last.request.headers["user-agent"] == user_agent
@pytest.mark.asyncio
@pytest.mark.parametrize("backend_type", [Backend.MISTRAL, Backend.GENERIC])
async def test_backend_user_agent_when_streaming(self, backend_type: Backend):
user_agent = get_user_agent(backend_type)
base_url = "https://api.example.com"
with respx.mock(base_url=base_url) as mock_api:
chunks = [
rb'data: {"id":"fake_id_1234","object":"chat.completion.chunk","created":1234567890,"model":"devstral-latest","choices":[{"index":0,"delta":{"role":"assistant","content":"Hey"},"finish_reason":"stop"}]}'
]
mock_response = httpx.Response(
status_code=200,
stream=httpx.ByteStream(stream=b"\n\n".join(chunks)),
headers={"Content-Type": "text/event-stream"},
)
mock_api.post("/v1/chat/completions").mock(return_value=mock_response)
provider = ProviderConfig(
name="provider_name",
api_base=f"{base_url}/v1",
api_key_env_var="API_KEY",
)
backend = BACKEND_FACTORY[backend_type](provider=provider)
model = ModelConfig(
name="model_name", provider="provider_name", alias="model_alias"
)
messages = [LLMMessage(role=Role.user, content="Just say hi")]
async for _ in backend.complete_streaming(
model=model,
messages=messages,
temperature=0.2,
tools=None,
max_tokens=None,
tool_choice=None,
extra_headers={"user-agent": user_agent},
):
pass
assert mock_api.calls.last.request.headers["user-agent"] == user_agent

2
uv.lock generated
View file

@ -661,7 +661,7 @@ wheels = [
[[package]] [[package]]
name = "mistral-vibe" name = "mistral-vibe"
version = "1.1.1" version = "1.1.2"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "agent-client-protocol" }, { name = "agent-client-protocol" },

View file

@ -100,7 +100,14 @@ class VibeAcpAgent(AcpAgent):
script_name = sys.argv[0] script_name = sys.argv[0]
args = [script_name, "--setup"] args = [script_name, "--setup"]
auth_methods = [ supports_terminal_auth = (
self.client_capabilities
and self.client_capabilities.field_meta
and self.client_capabilities.field_meta.get("terminal-auth") is True
)
auth_methods = (
[
AuthMethod( AuthMethod(
id="vibe-setup", id="vibe-setup",
name="Register your API Key", name="Register your API Key",
@ -114,6 +121,9 @@ class VibeAcpAgent(AcpAgent):
}, },
) )
] ]
if supports_terminal_auth
else []
)
response = InitializeResponse( response = InitializeResponse(
agentCapabilities=AgentCapabilities( agentCapabilities=AgentCapabilities(

View file

@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
__all__ = ["__version__", "run_programmatic"] __all__ = ["__version__", "run_programmatic"]
__version__ = "1.1.1" __version__ = "1.1.2"
from vibe.core.programmatic import run_programmatic from vibe.core.programmatic import run_programmatic

View file

@ -625,7 +625,7 @@ class Agent:
tools=available_tools, tools=available_tools,
tool_choice=tool_choice, tool_choice=tool_choice,
extra_headers={ extra_headers={
"User-Agent": get_user_agent(), "user-agent": get_user_agent(provider.backend),
"x-affinity": self.session_id, "x-affinity": self.session_id,
}, },
max_tokens=max_tokens, max_tokens=max_tokens,
@ -682,7 +682,7 @@ class Agent:
tools=available_tools, tools=available_tools,
tool_choice=tool_choice, tool_choice=tool_choice,
extra_headers={ extra_headers={
"User-Agent": get_user_agent(), "user-agent": get_user_agent(provider.backend),
"x-affinity": self.session_id, "x-affinity": self.session_id,
}, },
max_tokens=max_tokens, max_tokens=max_tokens,
@ -903,6 +903,7 @@ class Agent:
self.messages = [system_message, summary_message] self.messages = [system_message, summary_message]
active_model = self.config.get_active_model() active_model = self.config.get_active_model()
provider = self.config.get_provider_for_model(active_model)
async with self.backend as backend: async with self.backend as backend:
actual_context_tokens = await backend.count_tokens( actual_context_tokens = await backend.count_tokens(
@ -911,7 +912,7 @@ class Agent:
tools=self.format_handler.get_available_tools( tools=self.format_handler.get_available_tools(
self.tool_manager, self.config self.tool_manager, self.config
), ),
extra_headers={"User-Agent": get_user_agent()}, extra_headers={"user-agent": get_user_agent(provider.backend)},
) )
self.stats.context_tokens = actual_context_tokens self.stats.context_tokens = actual_context_tokens

View file

@ -14,7 +14,7 @@ from typing import Any
import httpx import httpx
from vibe.core import __version__ from vibe.core import __version__
from vibe.core.config import CONFIG_DIR, CONFIG_FILE, GLOBAL_CONFIG_FILE from vibe.core.config import CONFIG_DIR, CONFIG_FILE, GLOBAL_CONFIG_FILE, Backend
from vibe.core.types import BaseEvent, ToolResultEvent from vibe.core.types import BaseEvent, ToolResultEvent
@ -160,8 +160,12 @@ if CONFIG_FILE != GLOBAL_CONFIG_FILE and GLOBAL_CONFIG_FILE.is_file():
) )
def get_user_agent() -> str: def get_user_agent(backend: Backend) -> str:
return f"Mistral-Vibe/{__version__}" user_agent = f"Mistral-Vibe/{__version__}"
if backend == Backend.MISTRAL:
mistral_sdk_prefix = "mistral-client-python/"
user_agent = f"{mistral_sdk_prefix}{user_agent}"
return user_agent
def _is_retryable_http_error(e: Exception) -> bool: def _is_retryable_http_error(e: Exception) -> bool: