feat: OpenMesh 基础平台与 MD/PDF 转换技能
- 后端: coworker 智能体框架, WS API, 文件上传, 附件处理 - 前端: Open WebUI, 文件全量走 upload API (含 MD/TXT/JSON 等文本类) - 技能: md-to-office (pandoc + wkhtmltopdf) - 修复: 上传文件路径丢失, Agent 搜索浪费, 输出文件跑到 uploads/ - 打包: PyInstaller one-dir, 预打包 pandoc/wkhtmltopdf/chromium
This commit is contained in:
579
coworker/providers/bedrock_provider.py
Normal file
579
coworker/providers/bedrock_provider.py
Normal file
@@ -0,0 +1,579 @@
|
||||
"""AWS Bedrock provider — one entry in Settings, two wire paths by model family.
|
||||
|
||||
Routed ids look like `bedrock:<family>/<bedrock model id>`; the router strips `bedrock:`
|
||||
and this provider splits the family segment:
|
||||
|
||||
- `claude/…` → the native `AnthropicProvider` over the SDK's `AnthropicBedrock` client,
|
||||
so Claude-on-Bedrock gets everything direct Anthropic gets (thinking, refusal handling).
|
||||
- `other/…` → the Converse API (`bedrock-runtime.converse/converse_stream`), Bedrock's
|
||||
unified wire format across Llama, Nova, Mistral, Cohere, DeepSeek, …
|
||||
|
||||
An id with no family segment falls back to Converse as-is — Converse serves every Bedrock
|
||||
model (including Claude, minus the native extras), so a raw model id pasted without the
|
||||
add-model dropdown still works.
|
||||
|
||||
Auth is ONE method at a time, selected by the profile's `auth_method` (a segmented choice
|
||||
in Settings — owner call 2026-07-26, directness over field-precedence rules):
|
||||
|
||||
- `api_key` — a **Bedrock API key** (bearer token from the console, the no-CLI path);
|
||||
rides `AWS_BEARER_TOKEN_BEDROCK`, which boto3 prefers over SigV4 for Bedrock calls.
|
||||
- `profile` — a named `~/.aws` profile (covers `aws sso login`); blank → the default
|
||||
credential chain (env vars / ~/.aws / role).
|
||||
- `iam` — explicit access keys (+ optional STS session token).
|
||||
|
||||
Fields from non-selected methods are dropped at construction, so a stale stored value can
|
||||
never leak into a different auth path (`AnthropicBedrock` raises outright on a mix). A
|
||||
missing/unknown method falls back to whichever fields are present, api_key first.
|
||||
|
||||
boto3 is a lazy import (packaged via the `bedrock` extra) and returns PLAIN DICTS — every
|
||||
response/stream mapping here is dict-shaped, unlike the attribute objects other SDKs return.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from typing import Any, Optional
|
||||
|
||||
from .anthropic_provider import AnthropicProvider
|
||||
from .base import (
|
||||
AssistantTurn,
|
||||
ModelCapabilities,
|
||||
ProviderClient,
|
||||
StreamChunk,
|
||||
TokenUsage,
|
||||
ToolCall,
|
||||
)
|
||||
from .capabilities import capabilities_for
|
||||
|
||||
|
||||
def _usage_from(usage: Any) -> Optional[TokenUsage]:
|
||||
"""Converse `usage` dict → normalized counts (`inputTokens` excludes cache)."""
|
||||
if not isinstance(usage, dict):
|
||||
return None
|
||||
return TokenUsage(
|
||||
input=int(usage.get("inputTokens") or 0),
|
||||
output=int(usage.get("outputTokens") or 0),
|
||||
cache_read=int(usage.get("cacheReadInputTokens") or 0),
|
||||
cache_write=int(usage.get("cacheWriteInputTokens") or 0),
|
||||
)
|
||||
|
||||
# Converse has no required max token param but per-model defaults vary wildly (Meta's is
|
||||
# 512 — an agent turn gets truncated mid-tool-call); 4096 fits every family's ceiling.
|
||||
DEFAULT_MAX_TOKENS = 4096
|
||||
|
||||
# Converse stopReason → the engine's OpenAI-shaped finish_reason vocabulary.
|
||||
_STOP_REASON_MAP = {
|
||||
"end_turn": "stop",
|
||||
"tool_use": "tool_calls",
|
||||
"max_tokens": "length",
|
||||
"stop_sequence": "stop",
|
||||
"guardrail_intervened": "stop",
|
||||
"content_filtered": "stop",
|
||||
}
|
||||
|
||||
_DATA_URL_RE = re.compile(
|
||||
r"^data:image/([a-z0-9.+-]+);base64,(.+)$", re.IGNORECASE | re.DOTALL
|
||||
)
|
||||
_PDF_DATA_URL_RE = re.compile(
|
||||
r"^data:application/pdf;base64,(.+)$", re.IGNORECASE | re.DOTALL
|
||||
)
|
||||
|
||||
# Bedrock document names: alphanumeric, whitespace, hyphens, parens, brackets only.
|
||||
_DOC_NAME_RE = re.compile(r"[^A-Za-z0-9\s\-\(\)\[\]]+")
|
||||
|
||||
|
||||
def _session_kwargs(
|
||||
profile_name: Optional[str],
|
||||
access_key_id: Optional[str],
|
||||
secret_access_key: Optional[str],
|
||||
session_token: Optional[str],
|
||||
) -> dict[str, Any]:
|
||||
"""boto3.Session kwargs for the explicit → profile → ambient resolution order."""
|
||||
if access_key_id and secret_access_key:
|
||||
kwargs: dict[str, Any] = {
|
||||
"aws_access_key_id": access_key_id,
|
||||
"aws_secret_access_key": secret_access_key,
|
||||
}
|
||||
if session_token:
|
||||
kwargs["aws_session_token"] = session_token
|
||||
return kwargs
|
||||
if profile_name:
|
||||
return {"profile_name": profile_name}
|
||||
return {}
|
||||
|
||||
|
||||
def _parse_args(raw: Any) -> dict[str, Any]:
|
||||
if isinstance(raw, dict):
|
||||
return raw
|
||||
if not raw:
|
||||
return {}
|
||||
try:
|
||||
parsed = json.loads(raw)
|
||||
return parsed if isinstance(parsed, dict) else {"_raw": raw}
|
||||
except (TypeError, json.JSONDecodeError):
|
||||
return {"_raw": raw}
|
||||
|
||||
|
||||
def _user_blocks(content: Any) -> list[dict[str, Any]]:
|
||||
"""User content (str or OpenAI parts list) → Converse content blocks (bytes, not URLs)."""
|
||||
if isinstance(content, str):
|
||||
return [{"text": content}] if content else []
|
||||
blocks: list[dict[str, Any]] = []
|
||||
for part in content or []:
|
||||
kind = part.get("type") if isinstance(part, dict) else None
|
||||
if kind == "text":
|
||||
if part.get("text"):
|
||||
blocks.append({"text": part["text"]})
|
||||
elif kind == "image_url":
|
||||
url = (part.get("image_url") or {}).get("url") or ""
|
||||
match = _DATA_URL_RE.match(url)
|
||||
if match:
|
||||
fmt = match.group(1).lower()
|
||||
blocks.append(
|
||||
{
|
||||
"image": {
|
||||
"format": "jpeg" if fmt == "jpg" else fmt,
|
||||
"source": {"bytes": base64.b64decode(match.group(2))},
|
||||
}
|
||||
}
|
||||
)
|
||||
else: # Converse takes bytes only — no URL sources.
|
||||
blocks.append({"text": "[unsupported image attachment]"})
|
||||
elif kind == "file":
|
||||
file = part.get("file") or {}
|
||||
match = _PDF_DATA_URL_RE.match(file.get("file_data") or "")
|
||||
if match:
|
||||
name = _DOC_NAME_RE.sub("-", str(file.get("filename") or "document"))
|
||||
blocks.append(
|
||||
{
|
||||
"document": {
|
||||
"format": "pdf",
|
||||
"name": name or "document",
|
||||
"source": {"bytes": base64.b64decode(match.group(1))},
|
||||
}
|
||||
}
|
||||
)
|
||||
else:
|
||||
blocks.append({"text": "[unsupported file attachment]"})
|
||||
return blocks
|
||||
|
||||
|
||||
def convert_messages(
|
||||
messages: list[dict[str, Any]],
|
||||
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
||||
"""OpenAI-shaped history → (Converse `system`, Converse `messages`).
|
||||
|
||||
Same shape discipline as the Anthropic converter (it's the same API family): leading
|
||||
system messages become the top-level param, `role:"tool"` results become toolResult
|
||||
blocks inside a user message, and consecutive same-role messages fold together so all
|
||||
of a turn's parallel tool results land in the single next user message.
|
||||
"""
|
||||
system_parts: list[str] = []
|
||||
index = 0
|
||||
while index < len(messages) and messages[index].get("role") == "system":
|
||||
content = messages[index].get("content")
|
||||
if isinstance(content, str) and content:
|
||||
system_parts.append(content)
|
||||
index += 1
|
||||
|
||||
converted: list[dict[str, Any]] = []
|
||||
for message in messages[index:]:
|
||||
role = message.get("role")
|
||||
if role == "system":
|
||||
text = message.get("content") or ""
|
||||
if text:
|
||||
converted.append(
|
||||
{"role": "user", "content": [{"text": f"<system>\n{text}\n</system>"}]}
|
||||
)
|
||||
elif role == "user":
|
||||
blocks = _user_blocks(message.get("content"))
|
||||
if blocks:
|
||||
converted.append({"role": "user", "content": blocks})
|
||||
elif role == "assistant":
|
||||
blocks = []
|
||||
text = message.get("content")
|
||||
if isinstance(text, str) and text:
|
||||
blocks.append({"text": text})
|
||||
for call in message.get("tool_calls") or []:
|
||||
function = call.get("function") or {}
|
||||
blocks.append(
|
||||
{
|
||||
"toolUse": {
|
||||
"toolUseId": call.get("id") or "",
|
||||
"name": function.get("name") or "",
|
||||
"input": _parse_args(function.get("arguments")),
|
||||
}
|
||||
}
|
||||
)
|
||||
if blocks:
|
||||
converted.append({"role": "assistant", "content": blocks})
|
||||
elif role == "tool":
|
||||
converted.append(
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"toolResult": {
|
||||
"toolUseId": message.get("tool_call_id") or "",
|
||||
"content": [
|
||||
{"text": str(message.get("content") or "")}
|
||||
],
|
||||
}
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
folded: list[dict[str, Any]] = []
|
||||
for message in converted:
|
||||
if folded and folded[-1]["role"] == message["role"]:
|
||||
folded[-1]["content"].extend(message["content"])
|
||||
else:
|
||||
folded.append(message)
|
||||
|
||||
if not folded:
|
||||
raise ValueError("no convertible messages for the Bedrock Converse API")
|
||||
if folded[0]["role"] != "user":
|
||||
folded.insert(0, {"role": "user", "content": [{"text": "(continued)"}]})
|
||||
|
||||
system = [{"text": "\n\n".join(system_parts)}] if system_parts else []
|
||||
return system, folded
|
||||
|
||||
|
||||
def convert_tools(tools: Optional[list[dict[str, Any]]]) -> Optional[dict[str, Any]]:
|
||||
"""OpenAI function schemas → Converse `toolConfig` (None when there are no tools —
|
||||
Converse rejects an empty tool list)."""
|
||||
specs = []
|
||||
for tool in tools or []:
|
||||
function = tool.get("function") or {}
|
||||
parameters = function.get("parameters")
|
||||
if not isinstance(parameters, dict) or not parameters.get("type"):
|
||||
parameters = {"type": "object", "properties": {}}
|
||||
spec: dict[str, Any] = {
|
||||
"name": function.get("name") or "",
|
||||
"inputSchema": {"json": parameters},
|
||||
}
|
||||
if function.get("description"):
|
||||
spec["description"] = function["description"]
|
||||
specs.append({"toolSpec": spec})
|
||||
return {"tools": specs} if specs else None
|
||||
|
||||
|
||||
def _inference_config(settings: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Whitelisted engine settings → Converse `inferenceConfig` (camelCase)."""
|
||||
config: dict[str, Any] = {
|
||||
"maxTokens": int(settings.get("max_tokens") or DEFAULT_MAX_TOKENS)
|
||||
}
|
||||
if settings.get("temperature") is not None:
|
||||
config["temperature"] = settings["temperature"]
|
||||
if settings.get("top_p") is not None:
|
||||
config["topP"] = settings["top_p"]
|
||||
stop = settings.get("stop_sequences") or settings.get("stop")
|
||||
if stop:
|
||||
config["stopSequences"] = [stop] if isinstance(stop, str) else list(stop)
|
||||
return config
|
||||
|
||||
|
||||
class _BedrockConverseClient(ProviderClient):
|
||||
"""The `other/` family: any Bedrock model over the unified Converse API."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
client: Any = None,
|
||||
*,
|
||||
region: Optional[str] = None,
|
||||
bedrock_api_key: Optional[str] = None,
|
||||
profile_name: Optional[str] = None,
|
||||
access_key_id: Optional[str] = None,
|
||||
secret_access_key: Optional[str] = None,
|
||||
session_token: Optional[str] = None,
|
||||
):
|
||||
self._client = client # tests inject a dict-returning fake
|
||||
self._region = region
|
||||
self._bedrock_api_key = bedrock_api_key
|
||||
self._session_kwargs = _session_kwargs(
|
||||
profile_name, access_key_id, secret_access_key, session_token
|
||||
)
|
||||
|
||||
def _ensure_client(self) -> Any:
|
||||
if self._client is None:
|
||||
try:
|
||||
import boto3
|
||||
except ImportError as exc:
|
||||
raise RuntimeError(
|
||||
"AWS Bedrock support needs the boto3 package — "
|
||||
"install with `pip install 'openworker[bedrock]'`."
|
||||
) from exc
|
||||
# boto3 has no per-client bearer parameter — it only reads the env var, and
|
||||
# prefers bearer auth for Bedrock whenever it's set. The sidecar process is
|
||||
# ours, so publishing the configured key there is the supported path.
|
||||
if self._bedrock_api_key:
|
||||
os.environ["AWS_BEARER_TOKEN_BEDROCK"] = self._bedrock_api_key
|
||||
session = boto3.session.Session(**self._session_kwargs)
|
||||
self._client = session.client("bedrock-runtime", region_name=self._region)
|
||||
return self._client
|
||||
|
||||
def _request_kwargs(
|
||||
self,
|
||||
*,
|
||||
model: str,
|
||||
messages: list[dict[str, Any]],
|
||||
tools: Optional[list[dict[str, Any]]],
|
||||
settings: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
system, converted = convert_messages(messages)
|
||||
kwargs: dict[str, Any] = {
|
||||
"modelId": model,
|
||||
"messages": converted,
|
||||
"inferenceConfig": _inference_config(settings),
|
||||
}
|
||||
if system:
|
||||
kwargs["system"] = system
|
||||
tool_config = convert_tools(tools)
|
||||
if tool_config:
|
||||
kwargs["toolConfig"] = tool_config
|
||||
return kwargs
|
||||
|
||||
@staticmethod
|
||||
def _call(client: Any, method: str, kwargs: dict[str, Any]) -> Any:
|
||||
try:
|
||||
return getattr(client, method)(**kwargs)
|
||||
except Exception as exc:
|
||||
# boto3's "Unable to locate credentials" is famously cryptic — name the fix.
|
||||
if exc.__class__.__name__ == "NoCredentialsError":
|
||||
raise RuntimeError(
|
||||
"No AWS credentials found — add keys or a profile in Settings ▸ "
|
||||
"Models, or configure the AWS CLI (`aws configure` / `aws sso login`)."
|
||||
) from exc
|
||||
raise
|
||||
|
||||
def complete(
|
||||
self,
|
||||
*,
|
||||
model: str,
|
||||
messages: list[dict[str, Any]],
|
||||
tools: Optional[list[dict[str, Any]]] = None,
|
||||
**settings: Any,
|
||||
) -> AssistantTurn:
|
||||
kwargs = self._request_kwargs(
|
||||
model=model, messages=messages, tools=tools, settings=settings
|
||||
)
|
||||
response = self._call(self._ensure_client(), "converse", kwargs)
|
||||
|
||||
text_parts: list[str] = []
|
||||
reasoning_parts: list[str] = []
|
||||
tool_calls: list[ToolCall] = []
|
||||
content = ((response.get("output") or {}).get("message") or {}).get(
|
||||
"content"
|
||||
) or []
|
||||
for block in content:
|
||||
if "text" in block:
|
||||
text_parts.append(block["text"] or "")
|
||||
elif "toolUse" in block:
|
||||
tool = block["toolUse"]
|
||||
tool_calls.append(
|
||||
ToolCall(
|
||||
id=tool.get("toolUseId") or "",
|
||||
name=tool.get("name") or "",
|
||||
arguments=_parse_args(tool.get("input")),
|
||||
)
|
||||
)
|
||||
elif "reasoningContent" in block:
|
||||
text = (block["reasoningContent"].get("reasoningText") or {}).get(
|
||||
"text"
|
||||
) or ""
|
||||
if text:
|
||||
reasoning_parts.append(text)
|
||||
stop_reason = response.get("stopReason")
|
||||
return AssistantTurn(
|
||||
text="".join(text_parts) or None,
|
||||
tool_calls=tool_calls,
|
||||
finish_reason=_STOP_REASON_MAP.get(stop_reason, stop_reason),
|
||||
raw=response,
|
||||
reasoning="".join(reasoning_parts) or None,
|
||||
usage=_usage_from(response.get("usage")),
|
||||
)
|
||||
|
||||
def stream(
|
||||
self,
|
||||
*,
|
||||
model: str,
|
||||
messages: list[dict[str, Any]],
|
||||
tools: Optional[list[dict[str, Any]]] = None,
|
||||
**settings: Any,
|
||||
):
|
||||
kwargs = self._request_kwargs(
|
||||
model=model, messages=messages, tools=tools, settings=settings
|
||||
)
|
||||
response = self._call(self._ensure_client(), "converse_stream", kwargs)
|
||||
|
||||
text_parts: list[str] = []
|
||||
reasoning_parts: list[str] = []
|
||||
tool_accum: dict[int, dict[str, str]] = {}
|
||||
stop_reason = None
|
||||
usage: Optional[TokenUsage] = None
|
||||
|
||||
for event in response.get("stream") or []:
|
||||
if "contentBlockStart" in event:
|
||||
start = (event["contentBlockStart"].get("start") or {}).get("toolUse")
|
||||
if start:
|
||||
tool_accum[event["contentBlockStart"].get("contentBlockIndex", 0)] = {
|
||||
"id": start.get("toolUseId") or "",
|
||||
"name": start.get("name") or "",
|
||||
"json": "",
|
||||
}
|
||||
elif "contentBlockDelta" in event:
|
||||
block = event["contentBlockDelta"]
|
||||
delta = block.get("delta") or {}
|
||||
if delta.get("text"):
|
||||
text_parts.append(delta["text"])
|
||||
yield StreamChunk(text_delta=delta["text"])
|
||||
elif "toolUse" in delta:
|
||||
acc = tool_accum.get(block.get("contentBlockIndex", 0))
|
||||
if acc is not None:
|
||||
acc["json"] += delta["toolUse"].get("input") or ""
|
||||
elif "reasoningContent" in delta:
|
||||
thought = delta["reasoningContent"].get("text") or ""
|
||||
if thought:
|
||||
reasoning_parts.append(thought)
|
||||
yield StreamChunk(reasoning_delta=thought)
|
||||
elif "messageStop" in event:
|
||||
stop_reason = event["messageStop"].get("stopReason") or stop_reason
|
||||
elif "metadata" in event:
|
||||
usage = _usage_from(event["metadata"].get("usage")) or usage
|
||||
|
||||
tool_calls = [
|
||||
ToolCall(
|
||||
id=tool_accum[i]["id"],
|
||||
name=tool_accum[i]["name"],
|
||||
arguments=_parse_args(tool_accum[i]["json"]),
|
||||
)
|
||||
for i in sorted(tool_accum)
|
||||
]
|
||||
yield StreamChunk(
|
||||
turn=AssistantTurn(
|
||||
text="".join(text_parts) or None,
|
||||
tool_calls=tool_calls,
|
||||
finish_reason=_STOP_REASON_MAP.get(stop_reason, stop_reason),
|
||||
reasoning="".join(reasoning_parts) or None,
|
||||
usage=usage,
|
||||
)
|
||||
)
|
||||
|
||||
def capabilities(self, model: str) -> ModelCapabilities:
|
||||
return capabilities_for(f"bedrock:other/{model}")
|
||||
|
||||
|
||||
class BedrockProvider(ProviderClient):
|
||||
"""Family dispatcher: splits `<family>/<model id>` and delegates to the sub-client."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
region: Optional[str] = None,
|
||||
auth_method: Optional[str] = None,
|
||||
bedrock_api_key: Optional[str] = None,
|
||||
profile_name: Optional[str] = None,
|
||||
access_key_id: Optional[str] = None,
|
||||
secret_access_key: Optional[str] = None,
|
||||
session_token: Optional[str] = None,
|
||||
claude_client: Optional[ProviderClient] = None,
|
||||
converse_client: Optional[ProviderClient] = None,
|
||||
):
|
||||
# Narrow to the selected auth method here, once — stale values stored under a
|
||||
# previously-selected method must never reach a different credential path.
|
||||
if auth_method == "api_key":
|
||||
profile_name = access_key_id = secret_access_key = session_token = None
|
||||
elif auth_method == "profile":
|
||||
bedrock_api_key = access_key_id = secret_access_key = session_token = None
|
||||
elif auth_method == "iam":
|
||||
bedrock_api_key = profile_name = None
|
||||
self._region = region
|
||||
self._bedrock_api_key = bedrock_api_key
|
||||
self._profile_name = profile_name
|
||||
self._access_key_id = access_key_id
|
||||
self._secret_access_key = secret_access_key
|
||||
self._session_token = session_token
|
||||
# Test seams: pre-built sub-providers skip the SDK construction below.
|
||||
self._clients: dict[str, ProviderClient] = {}
|
||||
if claude_client is not None:
|
||||
self._clients["claude"] = claude_client
|
||||
if converse_client is not None:
|
||||
self._clients["other"] = converse_client
|
||||
|
||||
@staticmethod
|
||||
def _split(model: str) -> tuple[str, str]:
|
||||
"""`claude/<id>` → the native path; anything else (including a raw Bedrock id with
|
||||
no family segment) → Converse, which serves every Bedrock model."""
|
||||
if "/" in model:
|
||||
family, rest = model.split("/", 1)
|
||||
if family in ("claude", "other"):
|
||||
return family, rest
|
||||
return "other", model
|
||||
|
||||
def _family_client(self, family: str) -> ProviderClient:
|
||||
client = self._clients.get(family)
|
||||
if client is None:
|
||||
if family == "claude":
|
||||
from anthropic import AnthropicBedrock
|
||||
|
||||
# A Bedrock API key (field or ambient env) takes the bearer path and
|
||||
# EXCLUDES the SigV4 params — AnthropicBedrock raises on a mix.
|
||||
bearer = self._bedrock_api_key or os.environ.get(
|
||||
"AWS_BEARER_TOKEN_BEDROCK"
|
||||
)
|
||||
if bearer:
|
||||
sdk = AnthropicBedrock(api_key=bearer, aws_region=self._region)
|
||||
else:
|
||||
sdk = AnthropicBedrock(
|
||||
aws_region=self._region,
|
||||
aws_profile=self._profile_name,
|
||||
aws_access_key=self._access_key_id,
|
||||
aws_secret_key=self._secret_access_key,
|
||||
aws_session_token=self._session_token,
|
||||
)
|
||||
client = AnthropicProvider(client=sdk)
|
||||
else:
|
||||
client = _BedrockConverseClient(
|
||||
region=self._region,
|
||||
bedrock_api_key=self._bedrock_api_key,
|
||||
profile_name=self._profile_name,
|
||||
access_key_id=self._access_key_id,
|
||||
secret_access_key=self._secret_access_key,
|
||||
session_token=self._session_token,
|
||||
)
|
||||
self._clients[family] = client
|
||||
return client
|
||||
|
||||
def complete(
|
||||
self,
|
||||
*,
|
||||
model: str,
|
||||
messages: list[dict[str, Any]],
|
||||
tools: Optional[list[dict[str, Any]]] = None,
|
||||
**settings: Any,
|
||||
) -> AssistantTurn:
|
||||
family, rest = self._split(model)
|
||||
return self._family_client(family).complete(
|
||||
model=rest, messages=messages, tools=tools, **settings
|
||||
)
|
||||
|
||||
def stream(
|
||||
self,
|
||||
*,
|
||||
model: str,
|
||||
messages: list[dict[str, Any]],
|
||||
tools: Optional[list[dict[str, Any]]] = None,
|
||||
**settings: Any,
|
||||
):
|
||||
family, rest = self._split(model)
|
||||
return self._family_client(family).stream(
|
||||
model=rest, messages=messages, tools=tools, **settings
|
||||
)
|
||||
|
||||
def capabilities(self, model: str) -> ModelCapabilities:
|
||||
qualified = model if model.startswith("bedrock:") else f"bedrock:{model}"
|
||||
return capabilities_for(qualified)
|
||||
Reference in New Issue
Block a user