cortexgrid-infer 0.1.10__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.
- cortexgrid_infer/__init__.py +68 -0
- cortexgrid_infer/core.py +238 -0
- cortexgrid_infer/device.py +14 -0
- cortexgrid_infer/providers/__init__.py +0 -0
- cortexgrid_infer/providers/anthropic.py +186 -0
- cortexgrid_infer/providers/huggingface_complete.py +321 -0
- cortexgrid_infer/providers/huggingface_complete_serve.py +118 -0
- cortexgrid_infer/providers/huggingface_image.py +263 -0
- cortexgrid_infer/providers/huggingface_image_serve.py +125 -0
- cortexgrid_infer/py.typed +0 -0
- cortexgrid_infer/utils.py +125 -0
- cortexgrid_infer-0.1.10.dist-info/METADATA +167 -0
- cortexgrid_infer-0.1.10.dist-info/RECORD +15 -0
- cortexgrid_infer-0.1.10.dist-info/WHEEL +4 -0
- cortexgrid_infer-0.1.10.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from cortexgrid_infer.core import (
|
|
2
|
+
CompletionChunk,
|
|
3
|
+
DeployedModel,
|
|
4
|
+
CompletingModel,
|
|
5
|
+
GeneratedImage,
|
|
6
|
+
GeneratingModel,
|
|
7
|
+
Message,
|
|
8
|
+
Tool,
|
|
9
|
+
ToolCall,
|
|
10
|
+
ToolSpec,
|
|
11
|
+
complete,
|
|
12
|
+
delete_model,
|
|
13
|
+
deploy_model,
|
|
14
|
+
deployment_status,
|
|
15
|
+
generate,
|
|
16
|
+
register_deleter,
|
|
17
|
+
register_provider,
|
|
18
|
+
register_status_provider,
|
|
19
|
+
register_uploader,
|
|
20
|
+
upload_model,
|
|
21
|
+
)
|
|
22
|
+
from cortexgrid_infer.device import detect_device
|
|
23
|
+
from cortexgrid_infer.providers.anthropic import deploy_anthropic, AnthropicModel
|
|
24
|
+
from cortexgrid_infer.providers.huggingface_complete import (
|
|
25
|
+
delete_huggingface,
|
|
26
|
+
deploy_huggingface,
|
|
27
|
+
upload_huggingface,
|
|
28
|
+
HuggingFaceCompletingModel,
|
|
29
|
+
)
|
|
30
|
+
from cortexgrid_infer.providers.huggingface_image import (
|
|
31
|
+
delete_huggingface_image,
|
|
32
|
+
deploy_huggingface_image,
|
|
33
|
+
upload_huggingface_image,
|
|
34
|
+
HuggingFaceImageModel,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"CompletionChunk",
|
|
39
|
+
"DeployedModel",
|
|
40
|
+
"CompletingModel",
|
|
41
|
+
"GeneratedImage",
|
|
42
|
+
"GeneratingModel",
|
|
43
|
+
"Message",
|
|
44
|
+
"Tool",
|
|
45
|
+
"ToolCall",
|
|
46
|
+
"ToolSpec",
|
|
47
|
+
"complete",
|
|
48
|
+
"deploy_model",
|
|
49
|
+
"deployment_status",
|
|
50
|
+
"upload_model",
|
|
51
|
+
"delete_model",
|
|
52
|
+
"generate",
|
|
53
|
+
"detect_device",
|
|
54
|
+
"register_provider",
|
|
55
|
+
"register_status_provider",
|
|
56
|
+
"register_uploader",
|
|
57
|
+
"register_deleter",
|
|
58
|
+
"deploy_anthropic",
|
|
59
|
+
"AnthropicModel",
|
|
60
|
+
"deploy_huggingface",
|
|
61
|
+
"upload_huggingface",
|
|
62
|
+
"delete_huggingface",
|
|
63
|
+
"HuggingFaceCompletingModel",
|
|
64
|
+
"deploy_huggingface_image",
|
|
65
|
+
"upload_huggingface_image",
|
|
66
|
+
"delete_huggingface_image",
|
|
67
|
+
"HuggingFaceImageModel",
|
|
68
|
+
]
|
cortexgrid_infer/core.py
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""DeployedModel ABC, provider registry, and completion API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import abc
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from functools import partial
|
|
8
|
+
from collections.abc import AsyncIterator
|
|
9
|
+
from typing import (
|
|
10
|
+
Any,
|
|
11
|
+
Callable,
|
|
12
|
+
Sequence,
|
|
13
|
+
TypeVar,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class ToolCall:
|
|
19
|
+
id: str
|
|
20
|
+
name: str
|
|
21
|
+
arguments: dict[str, Any]
|
|
22
|
+
_func: partial[Any] = field(repr=False)
|
|
23
|
+
|
|
24
|
+
def __call__(self) -> Any:
|
|
25
|
+
return self._func(**self.arguments)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class CompletionChunk:
|
|
30
|
+
content: str = ""
|
|
31
|
+
tool_calls: list[ToolCall] = field(default_factory=list)
|
|
32
|
+
finish_reason: str | None = None
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def has_text(self) -> bool:
|
|
36
|
+
return bool(self.content)
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def has_tool_calls(self) -> bool:
|
|
40
|
+
return bool(self.tool_calls)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
Message = dict[str, Any]
|
|
44
|
+
ToolSpec = dict[str, Any]
|
|
45
|
+
Tool = Callable[..., Any] | ToolSpec
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class DeployedModel(abc.ABC):
|
|
49
|
+
@property
|
|
50
|
+
@abc.abstractmethod
|
|
51
|
+
def name(self) -> str: ...
|
|
52
|
+
|
|
53
|
+
def undeploy(self) -> None:
|
|
54
|
+
"""Release the underlying deployment, if this provider manages one.
|
|
55
|
+
|
|
56
|
+
The caller owns the model's lifecycle and calls this to tear it down
|
|
57
|
+
(e.g. on server shutdown). Hosted-API providers (e.g. Anthropic) have
|
|
58
|
+
nothing to release, so the default is a no-op; cluster-backed providers
|
|
59
|
+
override it to free their compute."""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class CompletingModel(DeployedModel):
|
|
63
|
+
@abc.abstractmethod
|
|
64
|
+
def complete(
|
|
65
|
+
self,
|
|
66
|
+
messages: list[Message],
|
|
67
|
+
tools: Sequence[Tool] | None = None,
|
|
68
|
+
max_new_tokens: int = 2048,
|
|
69
|
+
temperature: float = 0.7,
|
|
70
|
+
**kwargs: Any,
|
|
71
|
+
) -> AsyncIterator[CompletionChunk]: ...
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class GeneratedImage:
|
|
76
|
+
"""One generated image: the encoded file bytes (PNG) plus the resolved
|
|
77
|
+
parameters the deployment used to produce it."""
|
|
78
|
+
|
|
79
|
+
image: bytes
|
|
80
|
+
width: int
|
|
81
|
+
height: int
|
|
82
|
+
params: dict[str, Any] = field(default_factory=dict)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class GeneratingModel(DeployedModel):
|
|
86
|
+
"""A deployed image model. Unlike completion, generation is a single
|
|
87
|
+
request/response (no token streaming), so `generate` returns one result."""
|
|
88
|
+
|
|
89
|
+
@abc.abstractmethod
|
|
90
|
+
async def generate(
|
|
91
|
+
self,
|
|
92
|
+
prompt: str,
|
|
93
|
+
*,
|
|
94
|
+
image: bytes | None = None,
|
|
95
|
+
steps: int | None = None,
|
|
96
|
+
guidance: float | None = None,
|
|
97
|
+
size: int = 1024,
|
|
98
|
+
seed: int | None = None,
|
|
99
|
+
**kwargs: Any,
|
|
100
|
+
) -> GeneratedImage: ...
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
_providers: list[tuple[str, Callable[[str], DeployedModel | None]]] = []
|
|
104
|
+
|
|
105
|
+
T = TypeVar("T", bound=DeployedModel)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def register_provider(
|
|
109
|
+
prefix: str, factory: Callable[[str], DeployedModel | None]
|
|
110
|
+
) -> None:
|
|
111
|
+
"""Register a model provider that handles model IDs starting with *prefix*."""
|
|
112
|
+
_providers.append((prefix, factory))
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def deploy_model(model_id: str) -> DeployedModel:
|
|
116
|
+
for prefix, factory in _providers:
|
|
117
|
+
if not model_id.startswith(prefix):
|
|
118
|
+
continue
|
|
119
|
+
model = factory(model_id)
|
|
120
|
+
if model is None:
|
|
121
|
+
continue
|
|
122
|
+
return model
|
|
123
|
+
raise ValueError(
|
|
124
|
+
f"No provider registered for model '{model_id}'. "
|
|
125
|
+
f"Known prefixes: {[p for p, _ in _providers]}"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# Status providers mirror deploy providers: a prefix -> a function that reports the
|
|
130
|
+
# live deployment status for that model id (delegating to the platform, e.g.
|
|
131
|
+
# cortexgrid.model_serving_status). Kept separate from _providers because a status
|
|
132
|
+
# query must NOT construct/deploy anything - it just reads current state.
|
|
133
|
+
_status_providers: list[tuple[str, Callable[[str], Any]]] = []
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def register_status_provider(prefix: str, fn: Callable[[str], Any]) -> None:
|
|
137
|
+
"""Register a deployment-status reporter for model IDs starting with *prefix*."""
|
|
138
|
+
_status_providers.append((prefix, fn))
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def deployment_status(model_id: str) -> Any:
|
|
142
|
+
"""Report the current deployment status/phase for *model_id*, or None if no
|
|
143
|
+
status provider handles it (e.g. hosted-API models with nothing to schedule).
|
|
144
|
+
The returned object is whatever the platform reports - for cluster-backed
|
|
145
|
+
providers, a ``cortexgrid.ServingStatus`` (``phase`` + ``message``) once a
|
|
146
|
+
Serve app exists, or a ``cortexgrid.SavedModel`` while the weights are still
|
|
147
|
+
uploading to the registry."""
|
|
148
|
+
for prefix, fn in _status_providers:
|
|
149
|
+
if not model_id.startswith(prefix):
|
|
150
|
+
continue
|
|
151
|
+
result = fn(model_id)
|
|
152
|
+
if result is not None:
|
|
153
|
+
return result
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# Uploaders mirror deploy/status providers: a prefix -> a function that STARTS an
|
|
158
|
+
# ingest of the model's weights into the cortexgrid registry and returns the id of
|
|
159
|
+
# the background job doing it, or None when there is nothing to upload (already
|
|
160
|
+
# registered, or a hosted-API model with no weights). This is deliberately split
|
|
161
|
+
# from deploy: a model must be uploaded (registry phase `ready`) before
|
|
162
|
+
# `deploy_model` can schedule it, and the upload runs on the cluster - not the
|
|
163
|
+
# caller's machine - so large weights never round-trip through the client.
|
|
164
|
+
_uploaders: list[tuple[str, Callable[[str], str | None]]] = []
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def register_uploader(prefix: str, fn: Callable[[str], str | None]) -> None:
|
|
168
|
+
"""Register a registry-upload starter for model IDs starting with *prefix*."""
|
|
169
|
+
_uploaders.append((prefix, fn))
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def upload_model(model_id: str) -> str | None:
|
|
173
|
+
"""Start uploading *model_id*'s weights into the cortexgrid registry, returning
|
|
174
|
+
the id of the background job doing it, or None if nothing needs uploading
|
|
175
|
+
(already registered, or a hosted-API model with no weights to stage).
|
|
176
|
+
|
|
177
|
+
The upload runs on the cluster, not the caller's machine. Poll its progress
|
|
178
|
+
with ``deployment_status(model_id)`` (registry phase `uploading -> ready`);
|
|
179
|
+
once `ready`, call ``deploy_model(model_id)``."""
|
|
180
|
+
for prefix, fn in _uploaders:
|
|
181
|
+
if model_id.startswith(prefix):
|
|
182
|
+
return fn(model_id)
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
# Deleters mirror the other registries: a prefix -> a function that removes a model
|
|
187
|
+
# from the platform (undeploy its Serve app if running, then delete its weights +
|
|
188
|
+
# serve bundle from the registry). The inverse of upload+deploy; kept separate so a
|
|
189
|
+
# caller can tear a model down by id without holding a deployed-model handle.
|
|
190
|
+
_deleters: list[tuple[str, Callable[[str], None]]] = []
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def register_deleter(prefix: str, fn: Callable[[str], None]) -> None:
|
|
194
|
+
"""Register a registry-cleanup handler for model IDs starting with *prefix*."""
|
|
195
|
+
_deleters.append((prefix, fn))
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def delete_model(model_id: str) -> None:
|
|
199
|
+
"""Remove *model_id* from the platform: undeploy its Serve app if running, then
|
|
200
|
+
delete its weights and serve bundle from the registry.
|
|
201
|
+
|
|
202
|
+
Idempotent - safe whether or not the model is currently deployed or registered -
|
|
203
|
+
and a no-op for model ids no deleter handles (e.g. hosted-API models, which have
|
|
204
|
+
nothing on the cluster to clean up)."""
|
|
205
|
+
for prefix, fn in _deleters:
|
|
206
|
+
if model_id.startswith(prefix):
|
|
207
|
+
fn(model_id)
|
|
208
|
+
return
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
async def complete(
|
|
212
|
+
deployed_model: CompletingModel,
|
|
213
|
+
messages: list[Message],
|
|
214
|
+
tools: Sequence[Tool] | None = None,
|
|
215
|
+
max_new_tokens: int = 2048,
|
|
216
|
+
temperature: float = 0.7,
|
|
217
|
+
**kwargs: Any,
|
|
218
|
+
) -> AsyncIterator[CompletionChunk]:
|
|
219
|
+
"""Async completion request to the deployed model."""
|
|
220
|
+
async for chunk in deployed_model.complete(
|
|
221
|
+
messages,
|
|
222
|
+
tools,
|
|
223
|
+
max_new_tokens=max_new_tokens,
|
|
224
|
+
temperature=temperature,
|
|
225
|
+
**kwargs,
|
|
226
|
+
):
|
|
227
|
+
yield chunk
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
async def generate(
|
|
231
|
+
deployed_model: GeneratingModel,
|
|
232
|
+
prompt: str,
|
|
233
|
+
*,
|
|
234
|
+
image: bytes | None = None,
|
|
235
|
+
**kwargs: Any,
|
|
236
|
+
) -> GeneratedImage:
|
|
237
|
+
"""Single image-generation request to the deployed model."""
|
|
238
|
+
return await deployed_model.generate(prompt, image=image, **kwargs)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Device detection for PyTorch workloads."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import torch
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def detect_device() -> torch.device:
|
|
9
|
+
"""Return the best available accelerator (CUDA > MPS > CPU)."""
|
|
10
|
+
if torch.cuda.is_available():
|
|
11
|
+
return torch.device("cuda")
|
|
12
|
+
if torch.backends.mps.is_available():
|
|
13
|
+
return torch.device("mps")
|
|
14
|
+
return torch.device("cpu")
|
|
File without changes
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""Anthropic model deployment with streaming support."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from functools import partial
|
|
7
|
+
from collections.abc import AsyncIterator
|
|
8
|
+
from typing import Any, Sequence
|
|
9
|
+
|
|
10
|
+
from cortexgrid_infer.core import (
|
|
11
|
+
CompletingModel,
|
|
12
|
+
Message,
|
|
13
|
+
Tool,
|
|
14
|
+
ToolCall,
|
|
15
|
+
ToolSpec,
|
|
16
|
+
CompletionChunk,
|
|
17
|
+
register_provider,
|
|
18
|
+
)
|
|
19
|
+
from cortexgrid_infer.utils import build_tool_map, normalize_tools
|
|
20
|
+
|
|
21
|
+
from anthropic import AsyncAnthropic, Anthropic, NotFoundError
|
|
22
|
+
from dotenv import load_dotenv
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
_MODEL_PROVIDER_PREFIX = "Anthropic/"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _to_anthropic_messages(
|
|
29
|
+
messages: list[Message],
|
|
30
|
+
) -> tuple[str | None, list[dict[str, Any]]]:
|
|
31
|
+
"""Convert OpenAI-style messages to Anthropic format."""
|
|
32
|
+
system_prompt: str | None = None
|
|
33
|
+
anthropic_messages: list[dict[str, Any]] = []
|
|
34
|
+
|
|
35
|
+
for msg in messages:
|
|
36
|
+
role = msg["role"]
|
|
37
|
+
|
|
38
|
+
if role == "system":
|
|
39
|
+
system_prompt = msg["content"]
|
|
40
|
+
|
|
41
|
+
elif role == "user":
|
|
42
|
+
anthropic_messages.append({"role": "user", "content": msg["content"]})
|
|
43
|
+
|
|
44
|
+
elif role == "assistant":
|
|
45
|
+
content_blocks: list[dict[str, Any]] = []
|
|
46
|
+
if msg.get("content"):
|
|
47
|
+
content_blocks.append({"type": "text", "text": msg["content"]})
|
|
48
|
+
for tc in msg.get("tool_calls", []):
|
|
49
|
+
func = tc["function"]
|
|
50
|
+
content_blocks.append(
|
|
51
|
+
{
|
|
52
|
+
"type": "tool_use",
|
|
53
|
+
"id": tc["id"],
|
|
54
|
+
"name": func["name"],
|
|
55
|
+
"input": func["arguments"],
|
|
56
|
+
}
|
|
57
|
+
)
|
|
58
|
+
anthropic_messages.append({"role": "assistant", "content": content_blocks})
|
|
59
|
+
|
|
60
|
+
elif role == "tool":
|
|
61
|
+
tool_result_block = {
|
|
62
|
+
"type": "tool_result",
|
|
63
|
+
"tool_use_id": msg["tool_call_id"],
|
|
64
|
+
"content": msg["content"],
|
|
65
|
+
}
|
|
66
|
+
if (
|
|
67
|
+
anthropic_messages
|
|
68
|
+
and anthropic_messages[-1]["role"] == "user"
|
|
69
|
+
and isinstance(anthropic_messages[-1]["content"], list)
|
|
70
|
+
and anthropic_messages[-1]["content"]
|
|
71
|
+
and anthropic_messages[-1]["content"][0].get("type") == "tool_result"
|
|
72
|
+
):
|
|
73
|
+
anthropic_messages[-1]["content"].append(tool_result_block)
|
|
74
|
+
else:
|
|
75
|
+
anthropic_messages.append(
|
|
76
|
+
{
|
|
77
|
+
"role": "user",
|
|
78
|
+
"content": [tool_result_block],
|
|
79
|
+
}
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
return system_prompt, anthropic_messages
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _to_anthropic_tools(
|
|
86
|
+
tool_specs: list[ToolSpec] | None,
|
|
87
|
+
) -> list[dict[str, Any]] | None:
|
|
88
|
+
"""Convert OpenAI-style tool specs to Anthropic format."""
|
|
89
|
+
if not tool_specs:
|
|
90
|
+
return None
|
|
91
|
+
result = []
|
|
92
|
+
for spec in tool_specs:
|
|
93
|
+
func = spec["function"]
|
|
94
|
+
result.append(
|
|
95
|
+
{
|
|
96
|
+
"name": func["name"],
|
|
97
|
+
"description": func.get("description", ""),
|
|
98
|
+
"input_schema": func.get(
|
|
99
|
+
"parameters", {"type": "object", "properties": {}}
|
|
100
|
+
),
|
|
101
|
+
}
|
|
102
|
+
)
|
|
103
|
+
return result
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass
|
|
107
|
+
class AnthropicModel(CompletingModel):
|
|
108
|
+
client: Any
|
|
109
|
+
model_id: str
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def name(self) -> str:
|
|
113
|
+
return self.model_id
|
|
114
|
+
|
|
115
|
+
async def complete(
|
|
116
|
+
self,
|
|
117
|
+
messages: list[Message],
|
|
118
|
+
tools: Sequence[Tool] | None = None,
|
|
119
|
+
max_new_tokens: int = 16 * 1024,
|
|
120
|
+
temperature: float = 0.7,
|
|
121
|
+
**kwargs: Any,
|
|
122
|
+
) -> AsyncIterator[CompletionChunk]:
|
|
123
|
+
tool_specs = normalize_tools(tools)
|
|
124
|
+
tool_map = build_tool_map(tools)
|
|
125
|
+
anthropic_tools = _to_anthropic_tools(tool_specs)
|
|
126
|
+
|
|
127
|
+
system_prompt, anthropic_messages = _to_anthropic_messages(messages)
|
|
128
|
+
|
|
129
|
+
create_kwargs: dict[str, Any] = {
|
|
130
|
+
"model": self.model_id,
|
|
131
|
+
"messages": anthropic_messages,
|
|
132
|
+
"max_tokens": max_new_tokens,
|
|
133
|
+
"temperature": temperature,
|
|
134
|
+
}
|
|
135
|
+
if system_prompt:
|
|
136
|
+
create_kwargs["system"] = system_prompt
|
|
137
|
+
if anthropic_tools:
|
|
138
|
+
create_kwargs["tools"] = anthropic_tools
|
|
139
|
+
|
|
140
|
+
async with self.client.messages.stream(**create_kwargs) as stream:
|
|
141
|
+
async for event in stream:
|
|
142
|
+
if event.type == "content_block_delta":
|
|
143
|
+
if event.delta.type == "text_delta":
|
|
144
|
+
yield CompletionChunk(content=event.delta.text)
|
|
145
|
+
|
|
146
|
+
elif event.type == "content_block_stop":
|
|
147
|
+
block = stream.current_message_snapshot.content[event.index]
|
|
148
|
+
if block.type == "tool_use":
|
|
149
|
+
func = tool_map.get(block.name)
|
|
150
|
+
tc = ToolCall(
|
|
151
|
+
id=block.id,
|
|
152
|
+
name=block.name,
|
|
153
|
+
arguments=dict(block.input),
|
|
154
|
+
_func=(
|
|
155
|
+
partial(func, **block.input)
|
|
156
|
+
if func
|
|
157
|
+
else partial(lambda: None)
|
|
158
|
+
),
|
|
159
|
+
)
|
|
160
|
+
yield CompletionChunk(tool_calls=[tc])
|
|
161
|
+
|
|
162
|
+
elif event.type == "message_stop":
|
|
163
|
+
stop_reason = stream.current_message_snapshot.stop_reason
|
|
164
|
+
if stop_reason == "end_turn":
|
|
165
|
+
yield CompletionChunk(finish_reason="stop")
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _is_valid_anthropic_model(model_id: str) -> bool:
|
|
169
|
+
try:
|
|
170
|
+
Anthropic().models.retrieve(model_id)
|
|
171
|
+
return True
|
|
172
|
+
except NotFoundError:
|
|
173
|
+
return False
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def deploy_anthropic(model_id: str) -> AnthropicModel | None:
|
|
177
|
+
load_dotenv()
|
|
178
|
+
actual_model_id = model_id.removeprefix(_MODEL_PROVIDER_PREFIX)
|
|
179
|
+
if not _is_valid_anthropic_model(actual_model_id):
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
client = AsyncAnthropic()
|
|
183
|
+
return AnthropicModel(client=client, model_id=actual_model_id)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
register_provider(_MODEL_PROVIDER_PREFIX, deploy_anthropic)
|