mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-13 13:17:40 +02:00
Multi-provider support (WIP - not fully tested): - Owned agent loop replacing claude_agent_sdk (agent_loop.py, mcp_client.py) - Provider adapters: Anthropic (native), OpenAI-compat (any endpoint), Gemini (native + schema cleaning) - 19 models across 9 providers (Anthropic, OpenAI, Google, xAI, Meta, DeepSeek, Mistral, Qwen, Cohere) - OpenRouter integration for 300+ models via single API key - Builtin tool reimplementations (Read, Write, Edit, Glob, Grep, Bash, WebSearch, WebFetch, AskUserQuestion) - Standalone MCP client manager (stdio/sse/http) - Frontend: grouped model dropdown, provider selection, dynamic context windows Analytics (tested): - PostHog integration as single analytics source - Tracks: app.opened, session.started/completed, tool.called, tool.approval_resolved, error.occurred - Rich session data: user messages, assistant messages, session titles, tools used, MCP servers, task categories - PostHog dashboard with 14 insights created via API - Usage stats in Settings (Usage tab) with pixel-art bars Settings (tested): - 4 tabs: General, Models, Usage, Commands - Model Providers tab with OpenRouter (recommended), Anthropic, OpenAI, Google key fields - "Get key" links for each provider - Usage tab with session/cost/tool stats + analytics opt-in toggle - analytics_opt_in defaults to true, installation_id auto-generated Merged haik/updates-v1 (tested): - Sub-agent spawning, chat branching, browser control improvements - Settings: auto_select_mode, expand_new_chats, auto_reveal_sub_agents, dev_mode Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
136 lines
3.7 KiB
Python
136 lines
3.7 KiB
Python
"""Provider-agnostic base classes for multi-model support.
|
|
|
|
All provider adapters (Anthropic, OpenAI, Gemini, OpenAI-compatible)
|
|
implement BaseProvider, translating their native APIs into these
|
|
common data structures.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from abc import ABC, abstractmethod
|
|
from dataclasses import dataclass, field
|
|
from typing import Any, AsyncIterator
|
|
|
|
|
|
@dataclass
|
|
class ToolSchema:
|
|
"""Provider-agnostic tool definition."""
|
|
name: str
|
|
description: str
|
|
input_schema: dict[str, Any]
|
|
|
|
|
|
@dataclass
|
|
class ToolCall:
|
|
"""A tool invocation requested by the model."""
|
|
id: str
|
|
name: str
|
|
input: dict[str, Any]
|
|
|
|
|
|
@dataclass
|
|
class ContentBlock:
|
|
"""A block of content from the model response."""
|
|
type: str # "text" | "tool_use"
|
|
text: str = ""
|
|
tool_call: ToolCall | None = None
|
|
|
|
|
|
@dataclass
|
|
class ModelResponse:
|
|
"""Complete (non-streaming) response from a provider."""
|
|
content: list[ContentBlock]
|
|
stop_reason: str # "end_turn" | "tool_use" | "max_tokens"
|
|
usage: dict[str, int] = field(default_factory=dict)
|
|
|
|
|
|
@dataclass
|
|
class StreamEvent:
|
|
"""A single streaming event, normalized across providers.
|
|
|
|
The event types match what the frontend already expects via WebSocket:
|
|
content_block_start, content_block_delta, content_block_stop, message_stop.
|
|
"""
|
|
type: str
|
|
index: int = 0
|
|
block_type: str = "" # "text" | "tool_use"
|
|
delta_type: str = "" # "text_delta" | "input_json_delta"
|
|
text: str = ""
|
|
tool_name: str = ""
|
|
tool_id: str = ""
|
|
usage: dict[str, int] = field(default_factory=dict)
|
|
|
|
|
|
@dataclass
|
|
class ProviderMessage:
|
|
"""Provider-agnostic message for conversation history.
|
|
|
|
Each provider adapter converts these to/from its native format.
|
|
"""
|
|
role: str # "user" | "assistant" | "tool_result"
|
|
content: Any # str, list[dict], or provider-specific content
|
|
|
|
|
|
class BaseProvider(ABC):
|
|
"""Abstract base for LLM provider adapters."""
|
|
|
|
@abstractmethod
|
|
async def stream_message(
|
|
self,
|
|
model: str,
|
|
system: str | None,
|
|
messages: list[ProviderMessage],
|
|
tools: list[ToolSchema],
|
|
max_tokens: int = 8192,
|
|
) -> AsyncIterator[StreamEvent]:
|
|
"""Stream a model response, yielding normalized StreamEvents."""
|
|
...
|
|
|
|
@abstractmethod
|
|
async def create_message(
|
|
self,
|
|
model: str,
|
|
system: str | None,
|
|
messages: list[ProviderMessage],
|
|
tools: list[ToolSchema],
|
|
max_tokens: int = 8192,
|
|
) -> ModelResponse:
|
|
"""Non-streaming message creation."""
|
|
...
|
|
|
|
@abstractmethod
|
|
def format_tool_result(
|
|
self,
|
|
tool_use_id: str,
|
|
content: list[dict],
|
|
) -> dict:
|
|
"""Format a tool result in this provider's expected message format."""
|
|
...
|
|
|
|
@abstractmethod
|
|
def format_user_message(self, content: Any) -> ProviderMessage:
|
|
"""Wrap user content (str or multimodal blocks) into a ProviderMessage."""
|
|
...
|
|
|
|
@abstractmethod
|
|
def format_assistant_message(self, response: ModelResponse) -> ProviderMessage:
|
|
"""Convert a ModelResponse into a ProviderMessage for conversation history."""
|
|
...
|
|
|
|
@abstractmethod
|
|
def get_model_id(self, short_name: str) -> str:
|
|
"""Resolve a short model name to the full API model ID."""
|
|
...
|
|
|
|
def clean_tool_schema(self, schema: ToolSchema) -> dict:
|
|
"""Convert a ToolSchema to the provider's native tool format.
|
|
|
|
Default: Anthropic-style format. Override for providers that need
|
|
different formats or schema cleaning (e.g. Gemini).
|
|
"""
|
|
return {
|
|
"name": schema.name,
|
|
"description": schema.description,
|
|
"input_schema": schema.input_schema,
|
|
}
|