simbiot-harness/venv/lib/python3.12/site-packages/strands/models/llamaapi.py
Simbiot Harness b60439152b Simbiot Harness v0.1: тактовая лестница на Strands + CQRS/ES-спина
- harness/es.py: append-only event store + проекции (образец — кооп-портал)
- harness/levels.py: уровни 1с/1м/1ч/1д, контекст ∝ масштабу
- harness/scheduler.py: уровневые потоки, идемпотентные тики, absorbed-стабы
- harness/isolated_runner.py + _llm_worker.py: LLM-тик в сабпроцессе
  (глобальный asyncio-рантайм Strands не переживает 2+ create_harness)
- cli.py: init/task/tasks/status/events + requeue (написан самим минутным агентом)
- 9router: отдельный ключ sk-simbiot, модель auto (glm-5.3 первым)
2026-09-29 15:28:50 +00:00

479 lines
18 KiB
Python

# Copyright (c) Meta Platforms, Inc. and affiliates
"""Llama API model provider.
Deprecated: The Llama API service has been deprecated by Meta. This provider will be removed
in v2.0.0. Migrate to another provider (BedrockModel, OllamaModel, or OpenAIModel) that hosts
Llama or comparable models.
"""
import base64
import json
import logging
import mimetypes
from collections.abc import AsyncGenerator
from typing import Any, TypeVar, cast
import llama_api_client
from llama_api_client import LlamaAPIClient
from pydantic import BaseModel
from typing_extensions import Unpack, deprecated, override
from ..types.content import ContentBlock, Messages
from ..types.exceptions import ContextWindowOverflowException, ModelThrottledException
from ..types.streaming import StreamEvent, Usage
from ..types.tools import ToolChoice, ToolResult, ToolSpec, ToolUse
from ._validation import _has_location_source, validate_config_keys, warn_on_tool_choice_not_supported
from .model import BaseModelConfig, Model
logger = logging.getLogger(__name__)
T = TypeVar("T", bound=BaseModel)
@deprecated(
"LlamaAPIModel is deprecated and will be removed in v2.0.0. "
"The underlying Llama API service has been deprecated by Meta. "
"Use BedrockModel, OllamaModel, or OpenAIModel instead."
)
class LlamaAPIModel(Model):
"""Llama API model provider implementation.
Deprecated: The Llama API service has been deprecated by Meta. This class will be removed
in v2.0.0. Use BedrockModel, OllamaModel, or OpenAIModel instead.
"""
OVERFLOW_MESSAGES = {
"this model's maximum context length is",
"exceed context limit",
"model's maximum context limit",
"is longer than the model's context length",
"prompt is too long",
"too many tokens",
}
class LlamaConfig(BaseModelConfig, total=False):
"""Configuration options for Llama API models.
Attributes:
model_id: Model ID (e.g., "Llama-4-Maverick-17B-128E-Instruct-FP8").
repetition_penalty: Repetition penalty.
temperature: Temperature.
top_p: Top-p.
max_completion_tokens: Maximum completion tokens.
top_k: Top-k.
"""
model_id: str
repetition_penalty: float | None
temperature: float | None
top_p: float | None
max_completion_tokens: int | None
top_k: int | None
def __init__(
self,
*,
client_args: dict[str, Any] | None = None,
**model_config: Unpack[LlamaConfig],
) -> None:
"""Initialize provider instance.
Args:
client_args: Arguments for the Llama API client.
**model_config: Configuration options for the Llama API model.
"""
validate_config_keys(model_config, self.LlamaConfig)
self.config = LlamaAPIModel.LlamaConfig(**model_config)
logger.debug("config=<%s> | initializing", self.config)
if not client_args:
self.client = LlamaAPIClient()
else:
self.client = LlamaAPIClient(**client_args)
@override
def update_config(self, **model_config: Unpack[LlamaConfig]) -> None: # type: ignore
"""Update the Llama API Model configuration with the provided arguments.
Args:
**model_config: Configuration overrides.
"""
validate_config_keys(model_config, self.LlamaConfig)
self.config.update(model_config)
@override
def get_config(self) -> LlamaConfig:
"""Get the Llama API model configuration.
Returns:
The Llama API model configuration.
"""
return self.config
def _format_request_message_content(self, content: ContentBlock) -> dict[str, Any]:
"""Format a LlamaAPI content block.
- NOTE: "reasoningContent" and "video" are not supported currently.
Args:
content: Message content.
Returns:
LllamaAPI formatted content block.
Raises:
TypeError: If the content block type cannot be converted to a LlamaAPI-compatible format.
"""
if "image" in content:
mime_type = mimetypes.types_map.get(f".{content['image']['format']}", "application/octet-stream")
image_data = base64.b64encode(content["image"]["source"]["bytes"]).decode("utf-8")
return {
"image_url": {
"url": f"data:{mime_type};base64,{image_data}",
},
"type": "image_url",
}
if "text" in content:
return {"text": content["text"], "type": "text"}
raise TypeError(f"content_type=<{next(iter(content))}> | unsupported type")
def _format_request_message_tool_call(self, tool_use: ToolUse) -> dict[str, Any]:
"""Format a Llama API tool call.
Args:
tool_use: Tool use requested by the model.
Returns:
Llama API formatted tool call.
"""
return {
"function": {
"arguments": json.dumps(tool_use["input"], ensure_ascii=False),
"name": tool_use["name"],
},
"id": tool_use["toolUseId"],
}
def _format_request_tool_message(self, tool_result: ToolResult) -> dict[str, Any]:
"""Format a Llama API tool message.
Args:
tool_result: Tool result collected from a tool execution.
Returns:
Llama API formatted tool message.
"""
contents = cast(
list[ContentBlock],
[
{"text": json.dumps(content["json"], ensure_ascii=False)} if "json" in content else content
for content in tool_result["content"]
],
)
return {
"role": "tool",
"tool_call_id": tool_result["toolUseId"],
"content": [self._format_request_message_content(content) for content in contents],
}
def _format_request_messages(self, messages: Messages, system_prompt: str | None = None) -> list[dict[str, Any]]:
"""Format a LlamaAPI compatible messages array.
Args:
messages: List of message objects to be processed by the model.
system_prompt: System prompt to provide context to the model.
Returns:
An LlamaAPI compatible messages array.
"""
formatted_messages: list[dict[str, Any]]
formatted_messages = [{"role": "system", "content": system_prompt}] if system_prompt else []
for message in messages:
contents = message["content"]
# Filter out location sources and unsupported block types
filtered_contents = []
for content in contents:
if any(block_type in content for block_type in ["toolResult", "toolUse"]):
continue
if _has_location_source(content):
logger.warning("Location sources are not supported by LlamaAPI | skipping content block")
continue
filtered_contents.append(content)
formatted_contents: list[dict[str, Any]] | dict[str, Any] | str = ""
formatted_contents = [self._format_request_message_content(content) for content in filtered_contents]
formatted_tool_calls = [
self._format_request_message_tool_call(content["toolUse"])
for content in contents
if "toolUse" in content
]
formatted_tool_messages = [
self._format_request_tool_message(content["toolResult"])
for content in contents
if "toolResult" in content
]
if message["role"] == "assistant":
formatted_contents = formatted_contents[0] if formatted_contents else ""
formatted_message = {
"role": message["role"],
"content": formatted_contents if len(formatted_contents) > 0 else "",
**({"tool_calls": formatted_tool_calls} if formatted_tool_calls else {}),
}
formatted_messages.append(formatted_message)
formatted_messages.extend(formatted_tool_messages)
return [message for message in formatted_messages if message["content"] or "tool_calls" in message]
def format_request(
self, messages: Messages, tool_specs: list[ToolSpec] | None = None, system_prompt: str | None = None
) -> dict[str, Any]:
"""Format a Llama API chat streaming request.
Args:
messages: List of message objects to be processed by the model.
tool_specs: List of tool specifications to make available to the model.
system_prompt: System prompt to provide context to the model.
Returns:
An Llama API chat streaming request.
Raises:
TypeError: If a message contains a content block type that cannot be converted to a LlamaAPI-compatible
format.
"""
request = {
"messages": self._format_request_messages(messages, system_prompt),
"model": self.config["model_id"],
"stream": True,
"tools": [
{
"type": "function",
"function": {
"name": tool_spec["name"],
"description": tool_spec["description"],
"parameters": tool_spec["inputSchema"]["json"],
},
}
for tool_spec in tool_specs or []
],
}
if "temperature" in self.config:
request["temperature"] = self.config["temperature"]
if "top_p" in self.config:
request["top_p"] = self.config["top_p"]
if "repetition_penalty" in self.config:
request["repetition_penalty"] = self.config["repetition_penalty"]
if "max_completion_tokens" in self.config:
request["max_completion_tokens"] = self.config["max_completion_tokens"]
if "top_k" in self.config:
request["top_k"] = self.config["top_k"]
return request
def format_chunk(self, event: dict[str, Any]) -> StreamEvent:
"""Format the Llama API model response events into standardized message chunks.
Args:
event: A response event from the model.
Returns:
The formatted chunk.
"""
match event["chunk_type"]:
case "message_start":
return {"messageStart": {"role": "assistant"}}
case "content_start":
if event["data_type"] == "text":
return {"contentBlockStart": {"start": {}}}
return {
"contentBlockStart": {
"start": {
"toolUse": {
"name": event["data"].function.name,
"toolUseId": event["data"].id,
}
}
}
}
case "content_delta":
if event["data_type"] == "text":
return {"contentBlockDelta": {"delta": {"text": event["data"]}}}
return {"contentBlockDelta": {"delta": {"toolUse": {"input": event["data"].function.arguments}}}}
case "content_stop":
return {"contentBlockStop": {}}
case "message_stop":
match event["data"]:
case "tool_calls":
return {"messageStop": {"stopReason": "tool_use"}}
case "length":
return {"messageStop": {"stopReason": "max_tokens"}}
case _:
return {"messageStop": {"stopReason": "end_turn"}}
case "metadata":
usage = {}
for metrics in event["data"]:
if metrics.metric == "num_prompt_tokens":
usage["inputTokens"] = metrics.value
elif metrics.metric == "num_completion_tokens":
usage["outputTokens"] = metrics.value
elif metrics.metric == "num_total_tokens":
usage["totalTokens"] = metrics.value
usage_type = Usage(
inputTokens=usage["inputTokens"],
outputTokens=usage["outputTokens"],
totalTokens=usage["totalTokens"],
)
return {
"metadata": {
"usage": usage_type,
"metrics": {
"latencyMs": 0, # TODO
},
},
}
case _:
raise RuntimeError(f"chunk_type=<{event['chunk_type']} | unknown type")
@override
async def stream(
self,
messages: Messages,
tool_specs: list[ToolSpec] | None = None,
system_prompt: str | None = None,
*,
tool_choice: ToolChoice | None = None,
**kwargs: Any,
) -> AsyncGenerator[StreamEvent, None]:
"""Stream conversation with the LlamaAPI model.
Args:
messages: List of message objects to be processed by the model.
tool_specs: List of tool specifications to make available to the model.
system_prompt: System prompt to provide context to the model.
tool_choice: Selection strategy for tool invocation. **Note: This parameter is accepted for
interface consistency but is currently ignored for this model provider.**
**kwargs: Additional keyword arguments for future extensibility.
Yields:
Formatted message chunks from the model.
Raises:
ContextWindowOverflowException: If the input exceeds the model's context window.
ModelThrottledException: When the model service is throttling requests from the client.
"""
warn_on_tool_choice_not_supported(tool_choice)
logger.debug("formatting request")
request = self.format_request(messages, tool_specs, system_prompt)
logger.debug("request=<%s>", request)
logger.debug("invoking model")
try:
response = self.client.chat.completions.create(**request)
except llama_api_client.RateLimitError as e:
raise ModelThrottledException(str(e)) from e
except llama_api_client.BadRequestError as e:
if any(message in str(e).lower() for message in self.OVERFLOW_MESSAGES):
raise ContextWindowOverflowException(str(e)) from e
raise
logger.debug("got response from model")
yield self.format_chunk({"chunk_type": "message_start"})
stop_reason = None
tool_calls: dict[Any, list[Any]] = {}
curr_tool_call_id = None
metrics_event = None
for chunk in response:
if chunk.event.event_type == "start":
yield self.format_chunk({"chunk_type": "content_start", "data_type": "text"})
elif chunk.event.event_type in ["progress", "complete"] and chunk.event.delta.type == "text":
yield self.format_chunk(
{"chunk_type": "content_delta", "data_type": "text", "data": chunk.event.delta.text}
)
else:
if chunk.event.delta.type == "tool_call":
if chunk.event.delta.id:
curr_tool_call_id = chunk.event.delta.id
if curr_tool_call_id not in tool_calls:
tool_calls[curr_tool_call_id] = []
tool_calls[curr_tool_call_id].append(chunk.event.delta)
elif chunk.event.event_type == "metrics":
metrics_event = chunk.event.metrics
else:
yield self.format_chunk(chunk)
if stop_reason is None:
stop_reason = chunk.event.stop_reason
# stopped generation
if stop_reason:
yield self.format_chunk({"chunk_type": "content_stop", "data_type": "text"})
for tool_deltas in tool_calls.values():
tool_start, tool_deltas = tool_deltas[0], tool_deltas[1:]
yield self.format_chunk({"chunk_type": "content_start", "data_type": "tool", "data": tool_start})
for tool_delta in tool_deltas:
yield self.format_chunk({"chunk_type": "content_delta", "data_type": "tool", "data": tool_delta})
yield self.format_chunk({"chunk_type": "content_stop", "data_type": "tool"})
yield self.format_chunk({"chunk_type": "message_stop", "data": stop_reason})
# we may have a metrics event here
if metrics_event:
yield self.format_chunk({"chunk_type": "metadata", "data": metrics_event})
logger.debug("finished streaming response from model")
@override
def structured_output(
self, output_model: type[T], prompt: Messages, system_prompt: str | None = None, **kwargs: Any
) -> AsyncGenerator[dict[str, T | Any], None]:
"""Get structured output from the model.
Args:
output_model: The output model to use for the agent.
prompt: The prompt messages to use for the agent.
system_prompt: System prompt to provide context to the model.
**kwargs: Additional keyword arguments for future extensibility.
Yields:
Model events with the last being the structured output.
Raises:
NotImplementedError: Structured output is not currently supported for LlamaAPI models.
"""
# response_format: ResponseFormat = {
# "type": "json_schema",
# "json_schema": {
# "name": output_model.__name__,
# "schema": output_model.model_json_schema(),
# },
# }
# response = self.client.chat.completions.create(
# model=self.config["model_id"],
# messages=self.format_request(prompt)["messages"],
# response_format=response_format,
# )
raise NotImplementedError("Strands sdk-python does not implement this in the Llama API Preview.")