mirror of
https://github.com/affaan-m/ECC.git
synced 2026-08-17 21:15:40 +02:00
fix: docs/COMMAND-REGISTRY.json check fails on fresh Windows clone (missing .gitattributes) (#2437)
* fix: add .gitattributes to force LF line endings for text files npm run command-registry:check (part of npm test) fails on a fresh clone on Windows with the common core.autocrlf=true setting: git checks out docs/COMMAND-REGISTRY.json with CRLF, but generate-command-registry.js always writes LF, so the strict string comparison in checkRegistry() never matches. Forcing LF via .gitattributes makes checkouts consistent across platforms regardless of a contributor's local autocrlf setting. * fix: normalize CRLF line endings to LF per .gitattributes pyproject.toml, src/llm/__init__.py, src/llm/prompt/builder.py, src/llm/providers/claude.py, and tests/test_builder.py had CRLF line endings committed to the repo, inconsistent with the rest of the codebase. Renormalized via 'git add --renormalize .' now that .gitattributes enforces eol=lf. --------- Co-authored-by: Affaan Mustafa <me@affaanmustafa.com>
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
* text=auto eol=lf
|
||||
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
*.gif binary
|
||||
*.ico binary
|
||||
+79
-79
@@ -1,79 +1,79 @@
|
||||
[project]
|
||||
name = "llm-abstraction"
|
||||
version = "0.1.0"
|
||||
description = "Provider-agnostic LLM abstraction layer"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
license = {text = "MIT"}
|
||||
authors = [
|
||||
{name = "Affaan Mustafa", email = "affaan@example.com"}
|
||||
]
|
||||
keywords = ["llm", "openai", "anthropic", "ollama", "ai"]
|
||||
classifiers = [
|
||||
"Development Status :: 3 - Alpha",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
]
|
||||
|
||||
dependencies = [
|
||||
"anthropic>=0.111.0",
|
||||
"openai>=1.30.0",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"pytest>=9.1.1",
|
||||
"pytest-asyncio>=0.23",
|
||||
"pytest-cov>=7.1.0",
|
||||
"pytest-mock>=3.12",
|
||||
"ruff>=0.4",
|
||||
"mypy>=2.1.0",
|
||||
"pyyaml>=6.0",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/affaan-m/everything-claude-code"
|
||||
Repository = "https://github.com/affaan-m/everything-claude-code"
|
||||
|
||||
[project.scripts]
|
||||
llm-select = "llm.cli.selector:main"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src/llm"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
asyncio_mode = "auto"
|
||||
filterwarnings = ["ignore::DeprecationWarning"]
|
||||
|
||||
[tool.coverage.run]
|
||||
source = ["src/llm"]
|
||||
branch = true
|
||||
|
||||
[tool.coverage.report]
|
||||
exclude_lines = [
|
||||
"pragma: no cover",
|
||||
"if TYPE_CHECKING:",
|
||||
"raise NotImplementedError",
|
||||
]
|
||||
|
||||
[tool.ruff]
|
||||
src-path = ["src"]
|
||||
target-version = "py311"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "F", "I", "N", "W", "UP"]
|
||||
ignore = ["E501"]
|
||||
|
||||
[tool.mypy]
|
||||
python_version = "3.11"
|
||||
src_paths = ["src"]
|
||||
warn_return_any = true
|
||||
warn_unused_ignores = true
|
||||
[project]
|
||||
name = "llm-abstraction"
|
||||
version = "0.1.0"
|
||||
description = "Provider-agnostic LLM abstraction layer"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
license = {text = "MIT"}
|
||||
authors = [
|
||||
{name = "Affaan Mustafa", email = "affaan@example.com"}
|
||||
]
|
||||
keywords = ["llm", "openai", "anthropic", "ollama", "ai"]
|
||||
classifiers = [
|
||||
"Development Status :: 3 - Alpha",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
]
|
||||
|
||||
dependencies = [
|
||||
"anthropic>=0.111.0",
|
||||
"openai>=1.30.0",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"pytest>=9.1.1",
|
||||
"pytest-asyncio>=0.23",
|
||||
"pytest-cov>=7.1.0",
|
||||
"pytest-mock>=3.12",
|
||||
"ruff>=0.4",
|
||||
"mypy>=2.1.0",
|
||||
"pyyaml>=6.0",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/affaan-m/everything-claude-code"
|
||||
Repository = "https://github.com/affaan-m/everything-claude-code"
|
||||
|
||||
[project.scripts]
|
||||
llm-select = "llm.cli.selector:main"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src/llm"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
asyncio_mode = "auto"
|
||||
filterwarnings = ["ignore::DeprecationWarning"]
|
||||
|
||||
[tool.coverage.run]
|
||||
source = ["src/llm"]
|
||||
branch = true
|
||||
|
||||
[tool.coverage.report]
|
||||
exclude_lines = [
|
||||
"pragma: no cover",
|
||||
"if TYPE_CHECKING:",
|
||||
"raise NotImplementedError",
|
||||
]
|
||||
|
||||
[tool.ruff]
|
||||
src-path = ["src"]
|
||||
target-version = "py311"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "F", "I", "N", "W", "UP"]
|
||||
ignore = ["E501"]
|
||||
|
||||
[tool.mypy]
|
||||
python_version = "3.11"
|
||||
src_paths = ["src"]
|
||||
warn_return_any = true
|
||||
warn_unused_ignores = true
|
||||
|
||||
+33
-33
@@ -1,33 +1,33 @@
|
||||
"""
|
||||
LLM Abstraction Layer
|
||||
|
||||
Provider-agnostic interface for multiple LLM backends.
|
||||
"""
|
||||
|
||||
from llm.core.interface import LLMProvider
|
||||
from llm.core.types import LLMInput, LLMOutput, Message, ToolCall, ToolDefinition, ToolResult
|
||||
from llm.providers import get_provider
|
||||
from llm.tools import ToolExecutor, ToolRegistry
|
||||
from llm.cli.selector import interactive_select
|
||||
|
||||
__version__ = "0.1.0"
|
||||
|
||||
__all__ = (
|
||||
"LLMInput",
|
||||
"LLMOutput",
|
||||
"LLMProvider",
|
||||
"Message",
|
||||
"ToolCall",
|
||||
"ToolDefinition",
|
||||
"ToolResult",
|
||||
"ToolExecutor",
|
||||
"ToolRegistry",
|
||||
"get_provider",
|
||||
"interactive_select",
|
||||
)
|
||||
|
||||
|
||||
def gui() -> None:
|
||||
from llm.cli.selector import main
|
||||
main()
|
||||
|
||||
"""
|
||||
LLM Abstraction Layer
|
||||
|
||||
Provider-agnostic interface for multiple LLM backends.
|
||||
"""
|
||||
|
||||
from llm.core.interface import LLMProvider
|
||||
from llm.core.types import LLMInput, LLMOutput, Message, ToolCall, ToolDefinition, ToolResult
|
||||
from llm.providers import get_provider
|
||||
from llm.tools import ToolExecutor, ToolRegistry
|
||||
from llm.cli.selector import interactive_select
|
||||
|
||||
__version__ = "0.1.0"
|
||||
|
||||
__all__ = (
|
||||
"LLMInput",
|
||||
"LLMOutput",
|
||||
"LLMProvider",
|
||||
"Message",
|
||||
"ToolCall",
|
||||
"ToolDefinition",
|
||||
"ToolResult",
|
||||
"ToolExecutor",
|
||||
"ToolRegistry",
|
||||
"get_provider",
|
||||
"interactive_select",
|
||||
)
|
||||
|
||||
|
||||
def gui() -> None:
|
||||
from llm.cli.selector import main
|
||||
main()
|
||||
|
||||
|
||||
+99
-99
@@ -1,24 +1,24 @@
|
||||
"""Prompt builder for normalizing prompts across providers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from llm.core.types import LLMInput, Message, Role, ToolDefinition
|
||||
from llm.providers.claude import ClaudeProvider
|
||||
from llm.providers.openai import OpenAIProvider
|
||||
from llm.providers.ollama import OllamaProvider
|
||||
|
||||
|
||||
@dataclass
|
||||
class PromptConfig:
|
||||
system_template: str | None = None
|
||||
user_template: str | None = None
|
||||
include_tools_in_system: bool = True
|
||||
tool_format: str = "native"
|
||||
|
||||
|
||||
"""Prompt builder for normalizing prompts across providers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from llm.core.types import LLMInput, Message, Role, ToolDefinition
|
||||
from llm.providers.claude import ClaudeProvider
|
||||
from llm.providers.openai import OpenAIProvider
|
||||
from llm.providers.ollama import OllamaProvider
|
||||
|
||||
|
||||
@dataclass
|
||||
class PromptConfig:
|
||||
system_template: str | None = None
|
||||
user_template: str | None = None
|
||||
include_tools_in_system: bool = True
|
||||
tool_format: str = "native"
|
||||
|
||||
|
||||
class PromptBuilder:
|
||||
def __init__(
|
||||
self,
|
||||
@@ -45,81 +45,81 @@ class PromptBuilder:
|
||||
config = PromptConfig(**{key: value for key, value in overrides.items() if value is not None})
|
||||
|
||||
self.config = config
|
||||
|
||||
def build(self, messages: list[Message], tools: list[ToolDefinition] | None = None) -> list[Message]:
|
||||
if not messages:
|
||||
return []
|
||||
|
||||
result: list[Message] = []
|
||||
system_parts: list[str] = []
|
||||
|
||||
if self.config.system_template:
|
||||
system_parts.append(self.config.system_template)
|
||||
|
||||
if tools and self.config.include_tools_in_system:
|
||||
tools_desc = self._format_tools(tools)
|
||||
system_parts.append(f"\n\n## Available Tools\n{tools_desc}")
|
||||
|
||||
if messages[0].role == Role.SYSTEM:
|
||||
system_parts.insert(0, messages[0].content)
|
||||
result.insert(0, Message(role=Role.SYSTEM, content="\n\n".join(system_parts)))
|
||||
result.extend(messages[1:])
|
||||
else:
|
||||
if system_parts:
|
||||
result.insert(0, Message(role=Role.SYSTEM, content="\n\n".join(system_parts)))
|
||||
result.extend(messages)
|
||||
|
||||
return result
|
||||
|
||||
def _format_tools(self, tools: list[ToolDefinition]) -> str:
|
||||
lines = []
|
||||
for tool in tools:
|
||||
lines.append(f"### {tool.name}")
|
||||
lines.append(tool.description)
|
||||
if tool.parameters:
|
||||
lines.append("Parameters:")
|
||||
lines.append(self._format_parameters(tool.parameters))
|
||||
return "\n".join(lines)
|
||||
|
||||
def _format_parameters(self, params: dict[str, Any]) -> str:
|
||||
if "properties" not in params:
|
||||
return str(params)
|
||||
lines = []
|
||||
required = params.get("required", [])
|
||||
for name, spec in params["properties"].items():
|
||||
prop_type = spec.get("type", "any")
|
||||
desc = spec.get("description", "")
|
||||
required_mark = "(required)" if name in required else "(optional)"
|
||||
lines.append(f" - {name}: {prop_type} {required_mark} - {desc}")
|
||||
return "\n".join(lines) if lines else str(params)
|
||||
|
||||
|
||||
_PROVIDER_TEMPLATE_MAP: dict[str, dict[str, Any]] = {
|
||||
"claude": {
|
||||
"include_tools_in_system": False,
|
||||
"tool_format": "anthropic",
|
||||
},
|
||||
"openai": {
|
||||
"include_tools_in_system": False,
|
||||
"tool_format": "openai",
|
||||
},
|
||||
"ollama": {
|
||||
"include_tools_in_system": True,
|
||||
"tool_format": "text",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def get_provider_builder(provider_name: str) -> PromptBuilder:
|
||||
config_dict = _PROVIDER_TEMPLATE_MAP.get(provider_name.lower(), {})
|
||||
config = PromptConfig(**config_dict)
|
||||
return PromptBuilder(config)
|
||||
|
||||
|
||||
def adapt_messages_for_provider(
|
||||
messages: list[Message],
|
||||
provider: str,
|
||||
tools: list[ToolDefinition] | None = None,
|
||||
) -> list[Message]:
|
||||
builder = get_provider_builder(provider)
|
||||
return builder.build(messages, tools)
|
||||
|
||||
def build(self, messages: list[Message], tools: list[ToolDefinition] | None = None) -> list[Message]:
|
||||
if not messages:
|
||||
return []
|
||||
|
||||
result: list[Message] = []
|
||||
system_parts: list[str] = []
|
||||
|
||||
if self.config.system_template:
|
||||
system_parts.append(self.config.system_template)
|
||||
|
||||
if tools and self.config.include_tools_in_system:
|
||||
tools_desc = self._format_tools(tools)
|
||||
system_parts.append(f"\n\n## Available Tools\n{tools_desc}")
|
||||
|
||||
if messages[0].role == Role.SYSTEM:
|
||||
system_parts.insert(0, messages[0].content)
|
||||
result.insert(0, Message(role=Role.SYSTEM, content="\n\n".join(system_parts)))
|
||||
result.extend(messages[1:])
|
||||
else:
|
||||
if system_parts:
|
||||
result.insert(0, Message(role=Role.SYSTEM, content="\n\n".join(system_parts)))
|
||||
result.extend(messages)
|
||||
|
||||
return result
|
||||
|
||||
def _format_tools(self, tools: list[ToolDefinition]) -> str:
|
||||
lines = []
|
||||
for tool in tools:
|
||||
lines.append(f"### {tool.name}")
|
||||
lines.append(tool.description)
|
||||
if tool.parameters:
|
||||
lines.append("Parameters:")
|
||||
lines.append(self._format_parameters(tool.parameters))
|
||||
return "\n".join(lines)
|
||||
|
||||
def _format_parameters(self, params: dict[str, Any]) -> str:
|
||||
if "properties" not in params:
|
||||
return str(params)
|
||||
lines = []
|
||||
required = params.get("required", [])
|
||||
for name, spec in params["properties"].items():
|
||||
prop_type = spec.get("type", "any")
|
||||
desc = spec.get("description", "")
|
||||
required_mark = "(required)" if name in required else "(optional)"
|
||||
lines.append(f" - {name}: {prop_type} {required_mark} - {desc}")
|
||||
return "\n".join(lines) if lines else str(params)
|
||||
|
||||
|
||||
_PROVIDER_TEMPLATE_MAP: dict[str, dict[str, Any]] = {
|
||||
"claude": {
|
||||
"include_tools_in_system": False,
|
||||
"tool_format": "anthropic",
|
||||
},
|
||||
"openai": {
|
||||
"include_tools_in_system": False,
|
||||
"tool_format": "openai",
|
||||
},
|
||||
"ollama": {
|
||||
"include_tools_in_system": True,
|
||||
"tool_format": "text",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def get_provider_builder(provider_name: str) -> PromptBuilder:
|
||||
config_dict = _PROVIDER_TEMPLATE_MAP.get(provider_name.lower(), {})
|
||||
config = PromptConfig(**config_dict)
|
||||
return PromptBuilder(config)
|
||||
|
||||
|
||||
def adapt_messages_for_provider(
|
||||
messages: list[Message],
|
||||
provider: str,
|
||||
tools: list[ToolDefinition] | None = None,
|
||||
) -> list[Message]:
|
||||
builder = get_provider_builder(provider)
|
||||
return builder.build(messages, tools)
|
||||
|
||||
+137
-137
@@ -1,137 +1,137 @@
|
||||
"""Claude provider adapter."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
from anthropic import Anthropic
|
||||
|
||||
from llm.core.interface import (
|
||||
AuthenticationError,
|
||||
ContextLengthError,
|
||||
LLMProvider,
|
||||
RateLimitError,
|
||||
)
|
||||
from llm.core.types import LLMInput, LLMOutput, ModelInfo, ProviderType, Role, ToolCall
|
||||
|
||||
_DEFAULT_MODEL = "claude-sonnet-4-6"
|
||||
_OPUS_ADAPTIVE_ONLY_PREFIXES = ("claude-opus-4-7", "claude-opus-4-8")
|
||||
|
||||
|
||||
def _uses_adaptive_thinking_only(model: str) -> bool:
|
||||
return any(model.startswith(prefix) for prefix in _OPUS_ADAPTIVE_ONLY_PREFIXES)
|
||||
|
||||
|
||||
class ClaudeProvider(LLMProvider):
|
||||
provider_type = ProviderType.CLAUDE
|
||||
|
||||
def __init__(self, api_key: str | None = None, base_url: str | None = None) -> None:
|
||||
self.client = Anthropic(api_key=api_key or os.environ.get("ANTHROPIC_API_KEY"), base_url=base_url)
|
||||
self._models = [
|
||||
ModelInfo(
|
||||
name="claude-opus-4-8",
|
||||
provider=ProviderType.CLAUDE,
|
||||
supports_tools=True,
|
||||
supports_vision=True,
|
||||
max_tokens=64000,
|
||||
context_window=1_000_000,
|
||||
),
|
||||
ModelInfo(
|
||||
name="claude-sonnet-4-6",
|
||||
provider=ProviderType.CLAUDE,
|
||||
supports_tools=True,
|
||||
supports_vision=True,
|
||||
max_tokens=64000,
|
||||
context_window=1_000_000,
|
||||
),
|
||||
ModelInfo(
|
||||
name="claude-haiku-4-5",
|
||||
provider=ProviderType.CLAUDE,
|
||||
supports_tools=True,
|
||||
supports_vision=True,
|
||||
max_tokens=16000,
|
||||
context_window=200_000,
|
||||
),
|
||||
]
|
||||
|
||||
def generate(self, input: LLMInput) -> LLMOutput:
|
||||
try:
|
||||
model = input.model or _DEFAULT_MODEL
|
||||
system_parts = [msg.content for msg in input.messages if msg.role == Role.SYSTEM]
|
||||
api_messages = [
|
||||
msg.to_dict() for msg in input.messages if msg.role not in (Role.SYSTEM,)
|
||||
]
|
||||
|
||||
params: dict[str, Any] = {
|
||||
"model": model,
|
||||
"messages": api_messages,
|
||||
"max_tokens": input.max_tokens if input.max_tokens else 16000,
|
||||
"cache_control": {"type": "ephemeral"},
|
||||
}
|
||||
if system_parts:
|
||||
params["system"] = "\n\n".join(system_parts)
|
||||
if input.tools:
|
||||
params["tools"] = [tool.to_anthropic_tool() for tool in input.tools]
|
||||
if not _uses_adaptive_thinking_only(model):
|
||||
params["temperature"] = input.temperature
|
||||
if _uses_adaptive_thinking_only(model):
|
||||
params["thinking"] = {"type": "adaptive"}
|
||||
|
||||
response = self.client.messages.create(**params)
|
||||
|
||||
text_parts: list[str] = []
|
||||
tool_calls: list[ToolCall] = []
|
||||
for block in response.content or []:
|
||||
block_type = getattr(block, "type", None)
|
||||
if block_type == "text":
|
||||
text = getattr(block, "text", "")
|
||||
if text:
|
||||
text_parts.append(text)
|
||||
elif block_type == "tool_use":
|
||||
raw_arguments = getattr(block, "input", {})
|
||||
arguments = (
|
||||
raw_arguments.copy()
|
||||
if isinstance(raw_arguments, dict)
|
||||
else getattr(raw_arguments, "__dict__", {}).copy()
|
||||
)
|
||||
tool_calls.append(
|
||||
ToolCall(
|
||||
id=getattr(block, "id", ""),
|
||||
name=getattr(block, "name", ""),
|
||||
arguments=arguments,
|
||||
)
|
||||
)
|
||||
|
||||
return LLMOutput(
|
||||
content="".join(text_parts),
|
||||
tool_calls=tool_calls or None,
|
||||
model=response.model,
|
||||
usage={
|
||||
"input_tokens": response.usage.input_tokens,
|
||||
"output_tokens": response.usage.output_tokens,
|
||||
"cache_creation_input_tokens": getattr(
|
||||
response.usage, "cache_creation_input_tokens", 0
|
||||
),
|
||||
"cache_read_input_tokens": getattr(response.usage, "cache_read_input_tokens", 0),
|
||||
},
|
||||
stop_reason=response.stop_reason,
|
||||
)
|
||||
except Exception as e:
|
||||
msg = str(e)
|
||||
if "401" in msg or "authentication" in msg.lower():
|
||||
raise AuthenticationError(msg, provider=ProviderType.CLAUDE) from e
|
||||
if "429" in msg or "rate_limit" in msg.lower():
|
||||
raise RateLimitError(msg, provider=ProviderType.CLAUDE) from e
|
||||
if "context" in msg.lower() and "length" in msg.lower():
|
||||
raise ContextLengthError(msg, provider=ProviderType.CLAUDE) from e
|
||||
raise
|
||||
|
||||
def list_models(self) -> list[ModelInfo]:
|
||||
return self._models.copy()
|
||||
|
||||
def validate_config(self) -> bool:
|
||||
return bool(self.client.api_key)
|
||||
|
||||
def get_default_model(self) -> str:
|
||||
return _DEFAULT_MODEL
|
||||
"""Claude provider adapter."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
from anthropic import Anthropic
|
||||
|
||||
from llm.core.interface import (
|
||||
AuthenticationError,
|
||||
ContextLengthError,
|
||||
LLMProvider,
|
||||
RateLimitError,
|
||||
)
|
||||
from llm.core.types import LLMInput, LLMOutput, ModelInfo, ProviderType, Role, ToolCall
|
||||
|
||||
_DEFAULT_MODEL = "claude-sonnet-4-6"
|
||||
_OPUS_ADAPTIVE_ONLY_PREFIXES = ("claude-opus-4-7", "claude-opus-4-8")
|
||||
|
||||
|
||||
def _uses_adaptive_thinking_only(model: str) -> bool:
|
||||
return any(model.startswith(prefix) for prefix in _OPUS_ADAPTIVE_ONLY_PREFIXES)
|
||||
|
||||
|
||||
class ClaudeProvider(LLMProvider):
|
||||
provider_type = ProviderType.CLAUDE
|
||||
|
||||
def __init__(self, api_key: str | None = None, base_url: str | None = None) -> None:
|
||||
self.client = Anthropic(api_key=api_key or os.environ.get("ANTHROPIC_API_KEY"), base_url=base_url)
|
||||
self._models = [
|
||||
ModelInfo(
|
||||
name="claude-opus-4-8",
|
||||
provider=ProviderType.CLAUDE,
|
||||
supports_tools=True,
|
||||
supports_vision=True,
|
||||
max_tokens=64000,
|
||||
context_window=1_000_000,
|
||||
),
|
||||
ModelInfo(
|
||||
name="claude-sonnet-4-6",
|
||||
provider=ProviderType.CLAUDE,
|
||||
supports_tools=True,
|
||||
supports_vision=True,
|
||||
max_tokens=64000,
|
||||
context_window=1_000_000,
|
||||
),
|
||||
ModelInfo(
|
||||
name="claude-haiku-4-5",
|
||||
provider=ProviderType.CLAUDE,
|
||||
supports_tools=True,
|
||||
supports_vision=True,
|
||||
max_tokens=16000,
|
||||
context_window=200_000,
|
||||
),
|
||||
]
|
||||
|
||||
def generate(self, input: LLMInput) -> LLMOutput:
|
||||
try:
|
||||
model = input.model or _DEFAULT_MODEL
|
||||
system_parts = [msg.content for msg in input.messages if msg.role == Role.SYSTEM]
|
||||
api_messages = [
|
||||
msg.to_dict() for msg in input.messages if msg.role not in (Role.SYSTEM,)
|
||||
]
|
||||
|
||||
params: dict[str, Any] = {
|
||||
"model": model,
|
||||
"messages": api_messages,
|
||||
"max_tokens": input.max_tokens if input.max_tokens else 16000,
|
||||
"cache_control": {"type": "ephemeral"},
|
||||
}
|
||||
if system_parts:
|
||||
params["system"] = "\n\n".join(system_parts)
|
||||
if input.tools:
|
||||
params["tools"] = [tool.to_anthropic_tool() for tool in input.tools]
|
||||
if not _uses_adaptive_thinking_only(model):
|
||||
params["temperature"] = input.temperature
|
||||
if _uses_adaptive_thinking_only(model):
|
||||
params["thinking"] = {"type": "adaptive"}
|
||||
|
||||
response = self.client.messages.create(**params)
|
||||
|
||||
text_parts: list[str] = []
|
||||
tool_calls: list[ToolCall] = []
|
||||
for block in response.content or []:
|
||||
block_type = getattr(block, "type", None)
|
||||
if block_type == "text":
|
||||
text = getattr(block, "text", "")
|
||||
if text:
|
||||
text_parts.append(text)
|
||||
elif block_type == "tool_use":
|
||||
raw_arguments = getattr(block, "input", {})
|
||||
arguments = (
|
||||
raw_arguments.copy()
|
||||
if isinstance(raw_arguments, dict)
|
||||
else getattr(raw_arguments, "__dict__", {}).copy()
|
||||
)
|
||||
tool_calls.append(
|
||||
ToolCall(
|
||||
id=getattr(block, "id", ""),
|
||||
name=getattr(block, "name", ""),
|
||||
arguments=arguments,
|
||||
)
|
||||
)
|
||||
|
||||
return LLMOutput(
|
||||
content="".join(text_parts),
|
||||
tool_calls=tool_calls or None,
|
||||
model=response.model,
|
||||
usage={
|
||||
"input_tokens": response.usage.input_tokens,
|
||||
"output_tokens": response.usage.output_tokens,
|
||||
"cache_creation_input_tokens": getattr(
|
||||
response.usage, "cache_creation_input_tokens", 0
|
||||
),
|
||||
"cache_read_input_tokens": getattr(response.usage, "cache_read_input_tokens", 0),
|
||||
},
|
||||
stop_reason=response.stop_reason,
|
||||
)
|
||||
except Exception as e:
|
||||
msg = str(e)
|
||||
if "401" in msg or "authentication" in msg.lower():
|
||||
raise AuthenticationError(msg, provider=ProviderType.CLAUDE) from e
|
||||
if "429" in msg or "rate_limit" in msg.lower():
|
||||
raise RateLimitError(msg, provider=ProviderType.CLAUDE) from e
|
||||
if "context" in msg.lower() and "length" in msg.lower():
|
||||
raise ContextLengthError(msg, provider=ProviderType.CLAUDE) from e
|
||||
raise
|
||||
|
||||
def list_models(self) -> list[ModelInfo]:
|
||||
return self._models.copy()
|
||||
|
||||
def validate_config(self) -> bool:
|
||||
return bool(self.client.api_key)
|
||||
|
||||
def get_default_model(self) -> str:
|
||||
return _DEFAULT_MODEL
|
||||
|
||||
+53
-53
@@ -1,29 +1,29 @@
|
||||
import pytest
|
||||
from llm.core.types import LLMInput, Message, Role, ToolDefinition
|
||||
from llm.prompt import PromptBuilder, adapt_messages_for_provider
|
||||
from llm.prompt.builder import PromptConfig
|
||||
|
||||
|
||||
class TestPromptBuilder:
|
||||
def test_build_without_system(self):
|
||||
messages = [Message(role=Role.USER, content="Hello")]
|
||||
builder = PromptBuilder()
|
||||
result = builder.build(messages)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].role == Role.USER
|
||||
|
||||
def test_build_with_system(self):
|
||||
messages = [
|
||||
Message(role=Role.SYSTEM, content="You are helpful."),
|
||||
Message(role=Role.USER, content="Hello"),
|
||||
]
|
||||
builder = PromptBuilder()
|
||||
result = builder.build(messages)
|
||||
|
||||
assert len(result) == 2
|
||||
assert result[0].role == Role.SYSTEM
|
||||
|
||||
import pytest
|
||||
from llm.core.types import LLMInput, Message, Role, ToolDefinition
|
||||
from llm.prompt import PromptBuilder, adapt_messages_for_provider
|
||||
from llm.prompt.builder import PromptConfig
|
||||
|
||||
|
||||
class TestPromptBuilder:
|
||||
def test_build_without_system(self):
|
||||
messages = [Message(role=Role.USER, content="Hello")]
|
||||
builder = PromptBuilder()
|
||||
result = builder.build(messages)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].role == Role.USER
|
||||
|
||||
def test_build_with_system(self):
|
||||
messages = [
|
||||
Message(role=Role.SYSTEM, content="You are helpful."),
|
||||
Message(role=Role.USER, content="Hello"),
|
||||
]
|
||||
builder = PromptBuilder()
|
||||
result = builder.build(messages)
|
||||
|
||||
assert len(result) == 2
|
||||
assert result[0].role == Role.SYSTEM
|
||||
|
||||
def test_build_adds_system_from_keyword_options(self):
|
||||
messages = [Message(role=Role.USER, content="Hello")]
|
||||
builder = PromptBuilder(system_template="You are a pirate.")
|
||||
@@ -55,30 +55,30 @@ class TestPromptBuilder:
|
||||
assert result == messages
|
||||
|
||||
def test_build_with_tools(self):
|
||||
messages = [Message(role=Role.USER, content="Search for something")]
|
||||
tools = [
|
||||
ToolDefinition(name="search", description="Search the web", parameters={}),
|
||||
]
|
||||
builder = PromptBuilder(include_tools_in_system=True)
|
||||
result = builder.build(messages, tools)
|
||||
|
||||
assert len(result) == 2
|
||||
assert "search" in result[0].content
|
||||
assert "Available Tools" in result[0].content
|
||||
|
||||
|
||||
class TestAdaptMessagesForProvider:
|
||||
def test_adapt_for_claude(self):
|
||||
messages = [Message(role=Role.USER, content="Hello")]
|
||||
result = adapt_messages_for_provider(messages, "claude")
|
||||
assert len(result) == 1
|
||||
|
||||
def test_adapt_for_openai(self):
|
||||
messages = [Message(role=Role.USER, content="Hello")]
|
||||
result = adapt_messages_for_provider(messages, "openai")
|
||||
assert len(result) == 1
|
||||
|
||||
def test_adapt_for_ollama(self):
|
||||
messages = [Message(role=Role.USER, content="Hello")]
|
||||
result = adapt_messages_for_provider(messages, "ollama")
|
||||
assert len(result) == 1
|
||||
messages = [Message(role=Role.USER, content="Search for something")]
|
||||
tools = [
|
||||
ToolDefinition(name="search", description="Search the web", parameters={}),
|
||||
]
|
||||
builder = PromptBuilder(include_tools_in_system=True)
|
||||
result = builder.build(messages, tools)
|
||||
|
||||
assert len(result) == 2
|
||||
assert "search" in result[0].content
|
||||
assert "Available Tools" in result[0].content
|
||||
|
||||
|
||||
class TestAdaptMessagesForProvider:
|
||||
def test_adapt_for_claude(self):
|
||||
messages = [Message(role=Role.USER, content="Hello")]
|
||||
result = adapt_messages_for_provider(messages, "claude")
|
||||
assert len(result) == 1
|
||||
|
||||
def test_adapt_for_openai(self):
|
||||
messages = [Message(role=Role.USER, content="Hello")]
|
||||
result = adapt_messages_for_provider(messages, "openai")
|
||||
assert len(result) == 1
|
||||
|
||||
def test_adapt_for_ollama(self):
|
||||
messages = [Message(role=Role.USER, content="Hello")]
|
||||
result = adapt_messages_for_provider(messages, "ollama")
|
||||
assert len(result) == 1
|
||||
|
||||
Reference in New Issue
Block a user