Skip to content
Closed
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
8 changes: 4 additions & 4 deletions implementations/examples/coding_agent/config.yaml
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
llm:
provider: openrouter
model: deepseek/deepseek-v3.2
api_base: https://openrouter.ai/api/v1
provider: azure
model: azure/gpt-4.1-mini
# api_base: https://openrouter.ai/api/v1
# API key should be set via OPENROUTER_API_KEY environment variable

agent:
framework: langchain # Options: langchain, adk, raw_sdk
framework: raw_sdk # Options: langchain, adk, raw_sdk
system_prompt: |
You are a helpful coding assistant with the ability to read, write, and edit files,
run commands, and help with programming tasks.
Expand Down
46 changes: 35 additions & 11 deletions implementations/frameworks/raw_sdk_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,30 @@
Raw SDK agent implementation using abstracted LLM providers.

This is a THIN ADAPTER that:
1. Converts unified tool definitions to OpenAI function format
2. Handles the tool calling loop manually
1. Converts unified tool definitions to OpenAI function-calling format
2. Handles the tool-calling loop manually
3. Delegates LLM calls to the appropriate provider based on model name

Tool definitions and policy enforcement are handled by core modules.
DO NOT define tools or policy logic here.

Supported model string format: "<provider>/<model-name>"

openai/gpt-4o
anthropic/claude-3-5-sonnet-20241022
google/gemini-2.0-flash (also: gemini/...)
azure/<deployment-name>
ollama/llama3.2
vllm/meta-llama/Llama-3.3-70B-Instruct
bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0 (also: aws/...)
together/meta-llama/Llama-3-70b-chat-hf
openrouter/anthropic/claude-3.5-sonnet

Provider-specific config keys (under llm: in config.yaml):
api_key – override env-var API key
api_base – override API endpoint URL (base_url)
api_version – Azure API version (default: 2024-02-01)
region – AWS region for Bedrock (default: us-east-1)
"""

import json
Expand All @@ -29,8 +47,8 @@ class RawSDKAgent(BaseAgent):
"""
Agent implementation using abstracted LLM providers.

Supports 'provider/model-name' format for model specification.
Currently supports 'openai' and 'together' providers.
Accepts any provider registered in implementations.llms.
Model must be specified as 'provider/model-name'.
"""

def __init__(
Expand All @@ -52,15 +70,21 @@ def __init__(
self.provider_name = provider_parts[0]
self.model_name = provider_parts[1]
else:
# Default to openai if no provider specified
self.provider_name = "openai"
self.model_name = full_model_name
# print a helpful error message
print("Error: Invalid model name. Please use the format 'provider/model-name'.")

# Provider configuration
# Provider configuration — collect all optional provider-specific keys
provider_kwargs = {}
api_base = llm_config.get("api_base")
if api_base:
provider_kwargs["base_url"] = api_base
if llm_config.get("api_base"):
provider_kwargs["base_url"] = llm_config["api_base"]
if llm_config.get("api_key"):
provider_kwargs["api_key"] = llm_config["api_key"]
# Azure: deployment API version
if llm_config.get("api_version"):
provider_kwargs["api_version"] = llm_config["api_version"]
# Bedrock: AWS region
if llm_config.get("region"):
provider_kwargs["region"] = llm_config["region"]

# Initialize the provider
try:
Expand Down
62 changes: 53 additions & 9 deletions implementations/llms/__init__.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,77 @@
from typing import Dict, Type

from implementations.llms.azure_provider import AzureOpenAIProvider
from implementations.llms.base_provider import BaseLLMProvider
from implementations.llms.ollama_provider import OllamaProvider
from implementations.llms.openai_provider import OpenAIProvider
from implementations.llms.openrouter_provider import OpenRouterProvider
from implementations.llms.together_provider import TogetherProvider
from implementations.llms.vllm_provider import VLLMProvider

# Non-OpenAI providers are imported lazily inside get_llm_provider so that
# missing optional dependencies (anthropic, boto3, google-genai) do not
# break imports for users who only install a subset of extras.
# The PROVIDERS dict below maps provider name → class for the eager-import
# group; the lazy group is handled in get_llm_provider directly.

PROVIDERS: Dict[str, Type[BaseLLMProvider]] = {
# ── OpenAI-compatible (no extra deps beyond base) ─────────────────
"openai": OpenAIProvider,
"together": TogetherProvider,
"openrouter": OpenRouterProvider,
"azure": AzureOpenAIProvider,
"ollama": OllamaProvider,
"vllm": VLLMProvider,
}

# Human-friendly aliases for providers that need lazy imports
_LAZY_PROVIDERS = {
# Anthropic (requires: uv add anthropic)
"anthropic": "implementations.llms.anthropic_provider.AnthropicProvider",
# Google Gemini (requires: uv add google-genai)
"google": "implementations.llms.google_provider.GoogleProvider",
# AWS Bedrock (requires: uv add boto3)
"bedrock": "implementations.llms.bedrock_provider.BedrockProvider",
}


def get_llm_provider(provider_name: str, **kwargs) -> BaseLLMProvider:
"""
Get an instance of an LLM provider.
Return an initialised LLM provider instance.

Args:
provider_name: The name of the provider (e.g., 'openai', 'together').
**kwargs: Arguments to pass to the provider constructor.
provider_name: Case-insensitive provider key, e.g. 'openai', 'anthropic',
'google', 'gemini', 'azure', 'azureopenai', 'ollama',
'vllm', 'bedrock', 'aws', 'together', 'openrouter'.
**kwargs: Forwarded to the provider constructor. Common keys:
- api_key (overrides env-var lookup)
- base_url (endpoint override)
- api_version (Azure only)
- region (Bedrock only)

Returns:
An instance of the requested LLM provider.
An initialised BaseLLMProvider instance.

Raises:
ValueError: If the provider is not supported.
ValueError: Unknown provider name.
ImportError: Optional dependency not installed.
"""
provider_class = PROVIDERS.get(provider_name.lower())
if not provider_class:
raise ValueError(f"Unsupported LLM provider: {provider_name}")
key = provider_name.lower()

# Eager providers (always importable)
if key in PROVIDERS:
return PROVIDERS[key](**kwargs)

# Lazy providers (may have optional deps)
if key in _LAZY_PROVIDERS:
module_path, class_name = _LAZY_PROVIDERS[key].rsplit(".", 1)
import importlib

module = importlib.import_module(module_path)
provider_class: Type[BaseLLMProvider] = getattr(module, class_name)
return provider_class(**kwargs)

return provider_class(**kwargs)
supported = sorted(set(list(PROVIDERS.keys()) + list(_LAZY_PROVIDERS.keys())))
raise ValueError(
f"Unsupported LLM provider: '{provider_name}'. Supported providers: {supported}"
)
185 changes: 185 additions & 0 deletions implementations/llms/anthropic_provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
"""
Anthropic (Claude) LLM Provider.

Anthropic uses a fundamentally different API shape from OpenAI:
- System prompt is a top-level parameter, not a message
- Tool definitions use `input_schema` instead of `parameters`
- Tool calls in history are `tool_use` content blocks (assistant turn)
- Tool results are `tool_result` content blocks inside a *user* turn
- Multiple consecutive tool results must be merged into one user message

This provider translates OpenAI-format messages → Anthropic format on the way
in, calls the Anthropic API, then normalises the response back to the unified
OpenAI-compatible shape expected by raw_sdk_agent.py.
"""

import json
import os
from typing import Any, Dict, List, Optional, Tuple

from implementations.llms.base_provider import BaseLLMProvider
from implementations.llms.response_types import Message, ToolCall, UnifiedResponse

try:
import anthropic

ANTHROPIC_AVAILABLE = True
except ImportError:
ANTHROPIC_AVAILABLE = False


class AnthropicProvider(BaseLLMProvider):
"""Anthropic Claude provider with OpenAI-format message translation."""

def __init__(self, api_key: Optional[str] = None, base_url: Optional[str] = None, **kwargs):
if not ANTHROPIC_AVAILABLE:
raise ImportError("anthropic package not installed. Install with: uv add anthropic")

self.api_key = api_key or os.environ.get("ANTHROPIC_API_KEY")
if not self.api_key:
raise ValueError("ANTHROPIC_API_KEY not set for AnthropicProvider")

client_kwargs: Dict[str, Any] = {"api_key": self.api_key}
if base_url:
client_kwargs["base_url"] = base_url

self.client = anthropic.Anthropic(**client_kwargs)

# ------------------------------------------------------------------
# Message translation: OpenAI format → Anthropic format
# ------------------------------------------------------------------

def _translate_messages(
self, messages: List[Dict[str, Any]]
) -> Tuple[Optional[str], List[Dict[str, Any]]]:
"""
Convert OpenAI-format message list to (system_str, anthropic_messages).

Key differences handled:
- `role: system` is extracted as a top-level `system` parameter
- `role: assistant` with tool_calls becomes assistant content blocks of
type `text` + `tool_use`
- `role: tool` results are merged into a single `role: user` message
containing `tool_result` content blocks
"""
system: Optional[str] = None
translated: List[Dict[str, Any]] = []

i = 0
while i < len(messages):
msg = messages[i]
role = msg["role"]

if role == "system":
system = msg["content"]
i += 1
continue

if role == "user":
translated.append({"role": "user", "content": msg["content"]})
i += 1
continue

if role == "assistant":
content_blocks: List[Dict[str, Any]] = []
if msg.get("content"):
content_blocks.append({"type": "text", "text": msg["content"]})
for tc in msg.get("tool_calls", []):
content_blocks.append(
{
"type": "tool_use",
"id": tc["id"],
"name": tc["function"]["name"],
"input": json.loads(tc["function"]["arguments"]),
}
)
translated.append({"role": "assistant", "content": content_blocks})
i += 1
continue

if role == "tool":
# Anthropic requires all tool results for a single assistant turn
# to be batched inside one user message.
tool_results: List[Dict[str, Any]] = []
while i < len(messages) and messages[i]["role"] == "tool":
tool_results.append(
{
"type": "tool_result",
"tool_use_id": messages[i]["tool_call_id"],
"content": messages[i]["content"],
}
)
i += 1
translated.append({"role": "user", "content": tool_results})
continue

i += 1

return system, translated

def _translate_tools(self, tools: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Convert OpenAI tool format → Anthropic tool format."""
return [
{
"name": t["function"]["name"],
"description": t["function"].get("description", ""),
"input_schema": t["function"]["parameters"],
}
for t in tools
]

# ------------------------------------------------------------------
# Response normalisation: Anthropic response → UnifiedResponse
# ------------------------------------------------------------------

def _normalize_response(self, response) -> UnifiedResponse:
"""Map an Anthropic Messages response to the unified OpenAI-compatible shape."""
content: Optional[str] = None
tool_calls: List[ToolCall] = []

for block in response.content:
if block.type == "text":
content = block.text
elif block.type == "tool_use":
tool_calls.append(
ToolCall(call_id=block.id, name=block.name, arguments=block.input)
)

message = Message(content=content, tool_calls=tool_calls if tool_calls else None)
return UnifiedResponse(message)

# ------------------------------------------------------------------
# Public interface
# ------------------------------------------------------------------

def generate(
self,
model_name: str,
messages: List[Dict[str, Any]],
tools: Optional[List[Dict[str, Any]]] = None,
tool_choice: str = "auto",
temperature: float = 0.1,
**kwargs,
) -> UnifiedResponse:
system, translated_messages = self._translate_messages(messages)

params: Dict[str, Any] = {
"model": model_name,
"messages": translated_messages,
"max_tokens": kwargs.pop("max_tokens", 4096),
"temperature": temperature,
**kwargs,
}

if system:
params["system"] = system

if tools:
params["tools"] = self._translate_tools(tools)
if tool_choice == "none":
params["tool_choice"] = {"type": "none"}
else:
params["tool_choice"] = {"type": "auto"}

response = self.client.messages.create(**params)
return self._normalize_response(response)
Loading
Loading