evidor 1.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
evidor/__init__.py ADDED
@@ -0,0 +1,26 @@
1
+ """Minimal, provider-agnostic LLM harness."""
2
+
3
+ from .agent import Agent
4
+ from .context import DEFAULT_CONTEXT_WINDOW, DEFAULT_MAX_MESSAGES
5
+ from .models import GenerationRequest, GenerationResponse, Message, ToolCall
6
+ from .providers import AnthropicProvider, GeminiProvider, ModelProvider, OpenAIProvider
7
+ from .tools import Tool, calculator, filesystem_tools, get_current_time, tool
8
+
9
+ __all__ = [
10
+ "Agent",
11
+ "AnthropicProvider",
12
+ "calculator",
13
+ "DEFAULT_CONTEXT_WINDOW",
14
+ "DEFAULT_MAX_MESSAGES",
15
+ "GeminiProvider",
16
+ "GenerationRequest",
17
+ "GenerationResponse",
18
+ "get_current_time",
19
+ "filesystem_tools",
20
+ "Message",
21
+ "ModelProvider",
22
+ "OpenAIProvider",
23
+ "Tool",
24
+ "ToolCall",
25
+ "tool",
26
+ ]
@@ -0,0 +1,5 @@
1
+ """Conversational-agent APIs and internal runtime components."""
2
+
3
+ from .agent import Agent
4
+
5
+ __all__ = ["Agent"]
evidor/agent/agent.py ADDED
@@ -0,0 +1,123 @@
1
+ """The public conversational agent facade."""
2
+
3
+ import asyncio
4
+ from collections.abc import Callable, Sequence
5
+ from typing import Any
6
+
7
+ from ..context import DEFAULT_CONTEXT_WINDOW, DEFAULT_MAX_MESSAGES, ConversationContext
8
+ from ..models import GenerationRequest, GenerationResponse, Message, ToolCall
9
+ from ..providers.base import ModelProvider
10
+ from ..tools import Tool
11
+ from .history import ConversationHistory
12
+ from .summarization import ConversationSummarizer, resolve_summary_provider
13
+ from .tool_executor import ToolExecutor
14
+
15
+
16
+ class Agent:
17
+ """A conversational session coordinating a provider, history, context, and tools."""
18
+
19
+ def __init__(
20
+ self,
21
+ provider: ModelProvider,
22
+ *,
23
+ tools: Sequence[Tool | Callable[..., Any]] | None = None,
24
+ max_tool_iterations: int = 10,
25
+ tool_timeout: float | None = None,
26
+ system_prompt: str | None = None,
27
+ context_window: int = DEFAULT_CONTEXT_WINDOW,
28
+ max_messages: int = DEFAULT_MAX_MESSAGES,
29
+ summarization_model: str | ModelProvider | None = None,
30
+ ) -> None:
31
+ self._provider = provider
32
+ self._summary_provider = resolve_summary_provider(provider, summarization_model)
33
+ self._summarizer = ConversationSummarizer(self._summary_provider)
34
+ self._context = ConversationContext(context_window, max_messages)
35
+ self._history = ConversationHistory(system_prompt)
36
+ # Retained for compatibility with callers that inspect or seed test history.
37
+ self._messages = self._history.messages
38
+ self._max_tool_iterations = max_tool_iterations
39
+ self._tool_timeout = tool_timeout
40
+ self._tool_executor = ToolExecutor(tools, tool_timeout)
41
+ self._tools = self._tool_executor.tools
42
+
43
+ @property
44
+ def messages(self) -> tuple[Message, ...]:
45
+ """The retained conversation, including any generated summary message."""
46
+ return tuple(self._messages)
47
+
48
+ @property
49
+ def tools(self) -> tuple[Tool, ...]:
50
+ """The tools configured on this agent."""
51
+ return self._tools
52
+
53
+ @property
54
+ def tool_timeout(self) -> float | None:
55
+ """Default per-tool execution timeout in seconds, if configured."""
56
+ return self._tool_timeout
57
+
58
+ @property
59
+ def summary_provider(self) -> ModelProvider:
60
+ """The provider used for compacting and summarizing conversations."""
61
+ return self._summary_provider
62
+
63
+ def send(self, message: str) -> GenerationResponse:
64
+ """Advance one user turn and execute any requested tool calls."""
65
+ self._history.add_user(message)
66
+ for _ in range(self._max_tool_iterations):
67
+ self._compact()
68
+ response = self._provider.generate(GenerationRequest(messages=self._messages, tools=self._tools))
69
+ self._history.add_assistant(response)
70
+ if not response.tool_calls:
71
+ return response
72
+ self._execute_tool_calls(response.tool_calls)
73
+ self._compact()
74
+ final_response = self._provider.generate(GenerationRequest(messages=self._messages, tools=()))
75
+ return self._finalize_loop_expiry(final_response, response)
76
+
77
+ async def send_async(self, message: str) -> GenerationResponse:
78
+ """Asynchronously advance one user turn and execute requested tool calls."""
79
+ self._history.add_user(message)
80
+ for _ in range(self._max_tool_iterations):
81
+ await self._compact_async()
82
+ response = await asyncio.to_thread(
83
+ self._provider.generate, GenerationRequest(messages=self._messages, tools=self._tools)
84
+ )
85
+ self._history.add_assistant(response)
86
+ if not response.tool_calls:
87
+ return response
88
+ await self._execute_tool_calls_async(response.tool_calls)
89
+ await self._compact_async()
90
+ final_response = await asyncio.to_thread(
91
+ self._provider.generate, GenerationRequest(messages=self._messages, tools=())
92
+ )
93
+ return self._finalize_loop_expiry(final_response, response)
94
+
95
+ def clear_history(self) -> None:
96
+ """Clear the conversation while retaining the optional system prompt."""
97
+ self._history.clear()
98
+
99
+ def _compact(self) -> None:
100
+ self._history.replace(self._context.compact(self._messages, self._summarizer.summarize))
101
+
102
+ async def _compact_async(self) -> None:
103
+ compacted = await asyncio.to_thread(self._context.compact, self._messages, self._summarizer.summarize)
104
+ self._history.replace(compacted)
105
+
106
+ def _execute_tool_calls(self, calls: Sequence[ToolCall]) -> None:
107
+ for call in calls:
108
+ self._history.add_tool_result(call, self._tool_executor.execute(call))
109
+
110
+ async def _execute_tool_calls_async(self, calls: Sequence[ToolCall]) -> None:
111
+ for call in calls:
112
+ result = await self._tool_executor.execute_async(call)
113
+ self._history.add_tool_result(call, result)
114
+
115
+ def _finalize_loop_expiry(
116
+ self, final_response: GenerationResponse, last_response: GenerationResponse
117
+ ) -> GenerationResponse:
118
+ final_text = (final_response.text or "").strip() or (last_response.text or "").strip()
119
+ if not final_text:
120
+ final_text = f"Reached maximum tool iterations ({self._max_tool_iterations}) without a final response."
121
+ self._messages.append(Message(role="assistant", content=final_text))
122
+ return GenerationResponse(text=final_text, model=final_response.model or last_response.model)
123
+
@@ -0,0 +1,33 @@
1
+ """Conversation message storage and recording helpers."""
2
+
3
+ from collections.abc import Sequence
4
+
5
+ from ..models import GenerationResponse, Message, ToolCall
6
+
7
+
8
+ class ConversationHistory:
9
+ """Owns the mutable transcript for a single agent session."""
10
+
11
+ def __init__(self, system_prompt: str | None = None) -> None:
12
+ self.messages: list[Message] = []
13
+ if system_prompt:
14
+ self.messages.append(Message(role="system", content=system_prompt))
15
+
16
+ def add_user(self, content: str) -> None:
17
+ self.messages.append(Message(role="user", content=content))
18
+
19
+ def add_assistant(self, response: GenerationResponse) -> None:
20
+ self.messages.append(Message(role="assistant", content=response.text, tool_calls=response.tool_calls))
21
+
22
+ def add_tool_result(self, call: ToolCall, result: str) -> None:
23
+ self.messages.append(Message(role="tool", content=result, tool_call_id=call.id, name=call.name))
24
+
25
+ def replace(self, messages: Sequence[Message]) -> None:
26
+ """Replace history in place so existing list references remain valid."""
27
+ self.messages[:] = messages
28
+
29
+ def clear(self) -> None:
30
+ """Remove conversation turns while retaining the original system prompt."""
31
+ self.messages[:] = [
32
+ message for message in self.messages if message.role == "system" and not message.is_summary
33
+ ]
@@ -0,0 +1,69 @@
1
+ """Conversation summarization and summary-provider configuration."""
2
+
3
+ from collections.abc import Sequence
4
+
5
+ from ..models import GenerationRequest, Message
6
+ from ..providers.base import ModelProvider
7
+
8
+
9
+ SUMMARY_INSTRUCTION = (
10
+ "Summarize the conversation below for a future assistant turn. Preserve user goals, "
11
+ "decisions, constraints, factual details, and unresolved questions. Be concise."
12
+ )
13
+
14
+
15
+ class ConversationSummarizer:
16
+ """Formats a transcript and asks a dedicated provider to summarize it."""
17
+
18
+ def __init__(self, provider: ModelProvider) -> None:
19
+ self.provider = provider
20
+
21
+ def summarize(self, messages: Sequence[Message]) -> str:
22
+ transcript_lines: list[str] = []
23
+ for message in messages:
24
+ if message.role == "tool":
25
+ transcript_lines.append(f"TOOL ({message.name or 'unknown'}): {message.content}")
26
+ elif message.tool_calls:
27
+ call_names = ", ".join(call.name for call in message.tool_calls)
28
+ prefix = f"ASSISTANT (called tools: {call_names}):"
29
+ transcript_lines.append(f"{prefix} {message.content}" if message.content else prefix)
30
+ else:
31
+ transcript_lines.append(f"{message.role.upper()}: {message.content}")
32
+ request = GenerationRequest(
33
+ messages=(
34
+ Message(role="system", content=SUMMARY_INSTRUCTION),
35
+ Message(role="user", content="\n".join(transcript_lines)),
36
+ )
37
+ )
38
+ return self.provider.generate(request).text
39
+
40
+
41
+ def resolve_summary_provider(
42
+ provider: ModelProvider, summarization_model: str | ModelProvider | None
43
+ ) -> ModelProvider:
44
+ """Select a summary provider from a provider instance or model-name override."""
45
+ if summarization_model is None:
46
+ return provider
47
+ if isinstance(summarization_model, ModelProvider):
48
+ return summarization_model
49
+ if isinstance(summarization_model, str):
50
+ if not summarization_model.strip():
51
+ raise ValueError("summarization_model must not be empty")
52
+ with_model = getattr(provider, "with_model", None)
53
+ if callable(with_model):
54
+ return with_model(summarization_model)
55
+ provider_type = type(provider)
56
+ api_key = getattr(provider, "_api_key", None)
57
+ try:
58
+ return provider_type(model=summarization_model, api_key=api_key) # type: ignore[call-arg]
59
+ except TypeError:
60
+ try:
61
+ return provider_type(model=summarization_model) # type: ignore[call-arg]
62
+ except TypeError as err:
63
+ raise ValueError(
64
+ f"Cannot configure summarization_model string '{summarization_model}' for provider {provider_type.__name__}. "
65
+ "Pass a ModelProvider instance or implement with_model(model)."
66
+ ) from err
67
+ raise TypeError(
68
+ f"summarization_model must be a model name string or ModelProvider, got {type(summarization_model).__name__}"
69
+ )
@@ -0,0 +1,48 @@
1
+ """Tool registration and provider-requested tool execution."""
2
+
3
+ from collections.abc import Callable, Sequence
4
+ from typing import Any
5
+
6
+ from ..models import ToolCall
7
+ from ..tools import Tool, tool
8
+ from .utils import format_tool_error, serialize_tool_result
9
+
10
+
11
+ class ToolExecutor:
12
+ """Normalizes configured tools and executes calls requested by a provider."""
13
+
14
+ def __init__(self, tools: Sequence[Tool | Callable[..., Any]] | None, default_timeout: float | None) -> None:
15
+ configured_tools: list[Tool] = []
16
+ for candidate in tools or ():
17
+ configured = candidate if isinstance(candidate, Tool) else tool(candidate)
18
+ if default_timeout is not None and configured.timeout is None:
19
+ configured = configured.with_timeout(default_timeout)
20
+ configured_tools.append(configured)
21
+ self.tools = tuple(configured_tools)
22
+ self._by_name = {configured.name: configured for configured in self.tools}
23
+
24
+ def execute(self, call: ToolCall) -> str:
25
+ target = self._resolve(call)
26
+ if isinstance(target, str):
27
+ return target
28
+ try:
29
+ return serialize_tool_result(target.execute(**call.arguments))
30
+ except Exception as err:
31
+ return format_tool_error(call.name, err)
32
+
33
+ async def execute_async(self, call: ToolCall) -> str:
34
+ target = self._resolve(call)
35
+ if isinstance(target, str):
36
+ return target
37
+ try:
38
+ return serialize_tool_result(await target.execute_async(**call.arguments))
39
+ except Exception as err:
40
+ return format_tool_error(call.name, err)
41
+
42
+ def _resolve(self, call: ToolCall) -> Tool | str:
43
+ if "__decode_error__" in call.arguments:
44
+ decode_error = call.arguments["__decode_error__"]
45
+ raw_input = call.arguments.get("__raw_args__", "")
46
+ return f"Error: Malformed JSON arguments for tool '{call.name}': {decode_error}. Received input: {raw_input}"
47
+ target = self._by_name.get(call.name)
48
+ return target if target is not None else f"Error: Tool '{call.name}' not found."
evidor/agent/utils.py ADDED
@@ -0,0 +1,18 @@
1
+ """Utilities used exclusively by agent-runtime components."""
2
+
3
+ import json
4
+ from typing import Any
5
+
6
+
7
+ def serialize_tool_result(result: Any) -> str:
8
+ """Serialize a tool execution result into a string suitable for LLM consumption."""
9
+ if isinstance(result, str):
10
+ return result
11
+ if isinstance(result, (dict, list, int, float, bool)):
12
+ return json.dumps(result)
13
+ return str(result)
14
+
15
+
16
+ def format_tool_error(name: str, err: Any) -> str:
17
+ """Format a standardized error string when tool execution fails."""
18
+ return f"Error executing tool '{name}': {err}"
evidor/context.py ADDED
@@ -0,0 +1,66 @@
1
+ """Conversation-window management and summarization."""
2
+
3
+ from collections.abc import Callable, Sequence
4
+
5
+ from .models import Message
6
+
7
+ DEFAULT_CONTEXT_WINDOW = 16_000
8
+ DEFAULT_MAX_MESSAGES = 50
9
+ _CHARS_PER_TOKEN = 4
10
+
11
+
12
+ class ConversationContext:
13
+ """Keeps recent messages and replaces older messages with a bounded summary."""
14
+
15
+ def __init__(self, context_window: int, max_messages: int) -> None:
16
+ if context_window < 128:
17
+ raise ValueError("context_window must be at least 128 tokens")
18
+ if max_messages < 3:
19
+ raise ValueError("max_messages must be at least 3")
20
+ self._context_window = context_window
21
+ self._max_messages = max_messages
22
+
23
+ def compact(
24
+ self,
25
+ messages: Sequence[Message],
26
+ summarize: Callable[[Sequence[Message]], str],
27
+ ) -> list[Message]:
28
+ """Return messages that fit the configured input budget."""
29
+ if not self._requires_compaction(messages):
30
+ return list(messages)
31
+
32
+ permanent = [message for message in messages if message.role == "system" and not message.is_summary]
33
+ existing_summaries = [message for message in messages if message.is_summary]
34
+ conversation = [message for message in messages if message.role != "system"]
35
+ recent: list[Message] = list(conversation)
36
+ summarized: list[Message] = list(existing_summaries)
37
+
38
+ recent_limit = max(1, self._max_messages - len(permanent) - 1)
39
+ while len(recent) > recent_limit or self._estimated_tokens(permanent + recent) > self._input_budget:
40
+ if len(recent) == 1:
41
+ break
42
+ summarized.append(recent.pop(0))
43
+
44
+ if not summarized:
45
+ return permanent + recent
46
+
47
+ summary = summarize(summarized).strip()
48
+ if not summary:
49
+ summary = "Earlier conversation was omitted because no summary was returned."
50
+ summary = summary[: self._summary_token_limit * _CHARS_PER_TOKEN]
51
+ return permanent + [Message(role="system", content=f"Conversation summary:\n{summary}", is_summary=True)] + recent
52
+
53
+ @property
54
+ def _input_budget(self) -> int:
55
+ return self._context_window - min(1_024, self._context_window // 4)
56
+
57
+ @property
58
+ def _summary_token_limit(self) -> int:
59
+ return min(1_024, max(32, self._context_window // 4))
60
+
61
+ def _requires_compaction(self, messages: Sequence[Message]) -> bool:
62
+ return len(messages) > self._max_messages or self._estimated_tokens(messages) > self._input_budget
63
+
64
+ @staticmethod
65
+ def _estimated_tokens(messages: Sequence[Message]) -> int:
66
+ return sum((len(message.content) + _CHARS_PER_TOKEN - 1) // _CHARS_PER_TOKEN + 4 for message in messages)
evidor/models.py ADDED
@@ -0,0 +1,87 @@
1
+ """Provider-neutral data structures."""
2
+
3
+ from collections.abc import Sequence
4
+ from dataclasses import dataclass
5
+ from typing import Any, Literal
6
+
7
+ from .tools import Tool
8
+
9
+
10
+ MessageRole = Literal["system", "user", "assistant", "tool"]
11
+
12
+
13
+ @dataclass(frozen=True, slots=True)
14
+ class ToolCall:
15
+ """A tool invocation requested by a model."""
16
+
17
+ id: str
18
+ name: str
19
+ arguments: dict[str, Any]
20
+
21
+ def __post_init__(self) -> None:
22
+ if not self.id:
23
+ raise ValueError("tool call id must not be empty")
24
+ if not self.name:
25
+ raise ValueError("tool call name must not be empty")
26
+
27
+
28
+ @dataclass(frozen=True, slots=True)
29
+ class Message:
30
+ """A single text message or tool call/response in a conversation."""
31
+
32
+ role: MessageRole
33
+ content: str = ""
34
+ tool_calls: tuple[ToolCall, ...] = ()
35
+ tool_call_id: str | None = None
36
+ name: str | None = None
37
+ is_summary: bool = False
38
+
39
+ def __post_init__(self) -> None:
40
+ if isinstance(self.tool_calls, (list, tuple)):
41
+ object.__setattr__(self, "tool_calls", tuple(self.tool_calls))
42
+ if (not self.content or not self.content.strip()) and not self.tool_calls and self.role != "tool":
43
+ raise ValueError("message content must not be empty")
44
+
45
+
46
+ @dataclass(frozen=True, slots=True, init=False)
47
+ class GenerationRequest:
48
+ """The complete provider-neutral input for one model generation."""
49
+
50
+ messages: tuple[Message, ...]
51
+ tools: tuple[Tool, ...]
52
+
53
+ def __init__(
54
+ self,
55
+ prompt: str | None = None,
56
+ *,
57
+ messages: Sequence[Message] | None = None,
58
+ tools: Sequence[Tool] | None = None,
59
+ ) -> None:
60
+ if prompt is not None and messages is not None:
61
+ raise ValueError("provide either prompt or messages, not both")
62
+ if messages is None:
63
+ if prompt is None or not prompt.strip():
64
+ raise ValueError("prompt must not be empty")
65
+ messages = (Message(role="user", content=prompt),)
66
+ if not messages:
67
+ raise ValueError("messages must not be empty")
68
+ object.__setattr__(self, "messages", tuple(messages))
69
+ object.__setattr__(self, "tools", tuple(tools or ()))
70
+
71
+ @property
72
+ def prompt(self) -> str:
73
+ """Compatibility alias for the newest message content."""
74
+ return self.messages[-1].content
75
+
76
+
77
+ @dataclass(frozen=True, slots=True)
78
+ class GenerationResponse:
79
+ """The normalized output returned by a model provider."""
80
+
81
+ text: str
82
+ model: str
83
+ tool_calls: tuple[ToolCall, ...] = ()
84
+
85
+ def __post_init__(self) -> None:
86
+ if isinstance(self.tool_calls, (list, tuple)):
87
+ object.__setattr__(self, "tool_calls", tuple(self.tool_calls))
@@ -0,0 +1,8 @@
1
+ """Concrete provider adapters and their shared abstraction."""
2
+
3
+ from .anthropic import AnthropicProvider
4
+ from .base import ModelProvider
5
+ from .gemini import GeminiProvider
6
+ from .openai import OpenAIProvider
7
+
8
+ __all__ = ["AnthropicProvider", "GeminiProvider", "ModelProvider", "OpenAIProvider"]
@@ -0,0 +1,99 @@
1
+ """Anthropic adapter."""
2
+
3
+ from typing import Any
4
+
5
+ from evidor.models import GenerationRequest, GenerationResponse, ToolCall
6
+
7
+
8
+ class AnthropicProvider:
9
+ def __init__(self, model: str, api_key: str | None = None, max_tokens: int = 1024) -> None:
10
+ self._model = model
11
+ self._api_key = api_key
12
+ self._max_tokens = max_tokens
13
+
14
+ @property
15
+ def model(self) -> str:
16
+ return self._model
17
+
18
+ def with_model(self, model: str) -> "AnthropicProvider":
19
+ return AnthropicProvider(model=model, api_key=self._api_key, max_tokens=self._max_tokens)
20
+
21
+ def generate(self, request: GenerationRequest) -> GenerationResponse:
22
+ try:
23
+ from anthropic import Anthropic
24
+ except ImportError as error:
25
+ raise ImportError("Install Anthropic support with: pip install 'evidor[anthropic]'") from error
26
+
27
+ client = Anthropic(api_key=self._api_key)
28
+ system_prompt = "\n\n".join(message.content for message in request.messages if message.role == "system")
29
+
30
+ formatted_messages: list[dict[str, Any]] = []
31
+ for message in request.messages:
32
+ if message.role == "system":
33
+ continue
34
+ if message.role == "tool":
35
+ formatted_messages.append({
36
+ "role": "user",
37
+ "content": [
38
+ {
39
+ "type": "tool_result",
40
+ "tool_use_id": message.tool_call_id,
41
+ "content": message.content,
42
+ }
43
+ ],
44
+ })
45
+ elif message.role == "assistant" and message.tool_calls:
46
+ content_blocks: list[dict[str, Any]] = []
47
+ if message.content:
48
+ content_blocks.append({"type": "text", "text": message.content})
49
+ for tc in message.tool_calls:
50
+ content_blocks.append({
51
+ "type": "tool_use",
52
+ "id": tc.id,
53
+ "name": tc.name,
54
+ "input": tc.arguments,
55
+ })
56
+ formatted_messages.append({"role": "assistant", "content": content_blocks})
57
+ else:
58
+ formatted_messages.append({"role": message.role, "content": message.content})
59
+
60
+ kwargs: dict[str, Any] = {
61
+ "model": self._model,
62
+ "max_tokens": self._max_tokens,
63
+ "system": system_prompt or None,
64
+ "messages": formatted_messages,
65
+ }
66
+
67
+ if request.tools:
68
+ kwargs["tools"] = [
69
+ {
70
+ "name": t.name,
71
+ "description": t.description,
72
+ "input_schema": t.parameters,
73
+ }
74
+ for t in request.tools
75
+ ]
76
+
77
+ response_msg = client.messages.create(**kwargs)
78
+
79
+ text_parts: list[str] = []
80
+ tool_calls: list[ToolCall] = []
81
+
82
+ for block in getattr(response_msg, "content", []):
83
+ block_type = getattr(block, "type", None)
84
+ if block_type == "text":
85
+ text_parts.append(getattr(block, "text", ""))
86
+ elif block_type == "tool_use":
87
+ tool_calls.append(
88
+ ToolCall(
89
+ id=getattr(block, "id", f"call_{len(tool_calls)+1}"),
90
+ name=getattr(block, "name", ""),
91
+ arguments=getattr(block, "input", {}),
92
+ )
93
+ )
94
+
95
+ return GenerationResponse(
96
+ text="".join(text_parts),
97
+ model=self._model,
98
+ tool_calls=tuple(tool_calls),
99
+ )
@@ -0,0 +1,18 @@
1
+ """The port implemented by every model-provider adapter."""
2
+
3
+ from typing import Protocol, runtime_checkable
4
+
5
+ from evidor.models import GenerationRequest, GenerationResponse
6
+
7
+
8
+ @runtime_checkable
9
+ class ModelProvider(Protocol):
10
+ """Produces one normalized response from one normalized request."""
11
+
12
+ def generate(self, request: GenerationRequest) -> GenerationResponse:
13
+ """Generate a response for the request."""
14
+ ...
15
+
16
+ def with_model(self, model: str) -> "ModelProvider":
17
+ """Return a copy of this provider configured with a different model."""
18
+ ...