90 lines
2.8 KiB
Python
90 lines
2.8 KiB
Python
from __future__ import annotations
|
|
|
|
from collections.abc import AsyncIterator
|
|
from dataclasses import dataclass, field
|
|
from typing import Any, Protocol, runtime_checkable
|
|
|
|
from pydantic import BaseModel
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class ToolCallResult:
|
|
"""Normalized tool call from any provider."""
|
|
|
|
id: str
|
|
name: str
|
|
input: dict[str, Any]
|
|
# Gemini returns this as raw bytes; other providers omit it.
|
|
thought_signature: str | bytes | None = None
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class CompletionResult:
|
|
"""Normalized completion result returned by provider backends."""
|
|
|
|
content: Any = ""
|
|
input_tokens: int = 0
|
|
output_tokens: int = 0
|
|
cache_creation_input_tokens: int = 0
|
|
cache_read_input_tokens: int = 0
|
|
finish_reason: str = "stop"
|
|
tool_calls: list[ToolCallResult] = field(default_factory=list)
|
|
thinking_content: str | None = None
|
|
thinking_blocks: list[dict[str, Any]] = field(default_factory=list)
|
|
reasoning_details: list[dict[str, Any]] = field(default_factory=list)
|
|
raw_response: Any = None
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class StreamChunk:
|
|
"""A single chunk in a streaming response."""
|
|
|
|
content: str = ""
|
|
is_done: bool = False
|
|
finish_reason: str | None = None
|
|
output_tokens: int | None = None
|
|
|
|
|
|
@runtime_checkable
|
|
class ProviderBackend(Protocol):
|
|
"""Transport-agnostic interface for LLM providers.
|
|
|
|
Credentials are baked into the underlying SDK client at backend construction
|
|
time (see src/llm/registry.py), so these method signatures deliberately do
|
|
not accept api_key / api_base.
|
|
"""
|
|
|
|
async def complete(
|
|
self,
|
|
*,
|
|
model: str,
|
|
messages: list[dict[str, Any]],
|
|
max_tokens: int,
|
|
temperature: float | None = None,
|
|
stop: list[str] | None = None,
|
|
tools: list[dict[str, Any]] | None = None,
|
|
tool_choice: str | dict[str, Any] | None = None,
|
|
response_format: type[BaseModel] | dict[str, Any] | None = None,
|
|
thinking_budget_tokens: int | None = None,
|
|
thinking_effort: str | None = None,
|
|
max_output_tokens: int | None = None,
|
|
extra_params: dict[str, Any] | None = None,
|
|
) -> CompletionResult: ...
|
|
|
|
def stream(
|
|
self,
|
|
*,
|
|
model: str,
|
|
messages: list[dict[str, Any]],
|
|
max_tokens: int,
|
|
temperature: float | None = None,
|
|
stop: list[str] | None = None,
|
|
tools: list[dict[str, Any]] | None = None,
|
|
tool_choice: str | dict[str, Any] | None = None,
|
|
response_format: type[BaseModel] | dict[str, Any] | None = None,
|
|
thinking_budget_tokens: int | None = None,
|
|
thinking_effort: str | None = None,
|
|
max_output_tokens: int | None = None,
|
|
extra_params: dict[str, Any] | None = None,
|
|
) -> AsyncIterator[StreamChunk]: ...
|