cortexgrid-infer 0.3.6__tar.gz → 0.3.7__tar.gz
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-0.3.6 → cortexgrid_infer-0.3.7}/PKG-INFO +1 -1
- {cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.7}/cortexgrid_infer/core.py +21 -0
- cortexgrid_infer-0.3.7/cortexgrid_infer/protocols/completion.py +92 -0
- {cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.7}/cortexgrid_infer/serve_apps/anthropic.py +20 -8
- {cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.7}/cortexgrid_infer/serve_apps/gemini/text2text.py +24 -13
- cortexgrid_infer-0.3.7/cortexgrid_infer/serve_apps/streamed_completion.py +165 -0
- {cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.7}/cortexgrid_infer/serve_apps/text2text.py +143 -6
- {cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.7}/pyproject.toml +1 -1
- cortexgrid_infer-0.3.6/cortexgrid_infer/protocols/completion.py +0 -222
- {cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.7}/.gitignore +0 -0
- {cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.7}/LICENSE +0 -0
- {cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.7}/README.md +0 -0
- {cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.7}/cortexgrid_infer/__init__.py +0 -0
- {cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.7}/cortexgrid_infer/compiling.py +0 -0
- {cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.7}/cortexgrid_infer/device.py +0 -0
- {cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.7}/cortexgrid_infer/importers/__init__.py +0 -0
- {cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.7}/cortexgrid_infer/importers/base.py +0 -0
- {cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.7}/cortexgrid_infer/importers/huggingface.py +0 -0
- {cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.7}/cortexgrid_infer/protocols/__init__.py +0 -0
- {cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.7}/cortexgrid_infer/protocols/imaging.py +0 -0
- {cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.7}/cortexgrid_infer/protocols/meshing.py +0 -0
- {cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.7}/cortexgrid_infer/protocols/rewriting.py +0 -0
- {cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.7}/cortexgrid_infer/py.typed +0 -0
- {cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.7}/cortexgrid_infer/registry.py +0 -0
- {cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.7}/cortexgrid_infer/serve_apps/__init__.py +0 -0
- {cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.7}/cortexgrid_infer/serve_apps/base.py +0 -0
- {cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.7}/cortexgrid_infer/serve_apps/gemini/__init__.py +0 -0
- {cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.7}/cortexgrid_infer/serve_apps/gemini/base.py +0 -0
- {cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.7}/cortexgrid_infer/serve_apps/gemini/text2image.py +0 -0
- {cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.7}/cortexgrid_infer/serve_apps/image2mesh.py +0 -0
- {cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.7}/cortexgrid_infer/serve_apps/text2image.py +0 -0
- {cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.7}/cortexgrid_infer/serve_apps/text_rewriter.py +0 -0
- {cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.7}/cortexgrid_infer/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: cortexgrid-infer
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.7
|
|
4
4
|
Summary: Serve apps per task (text-to-text, text-to-image, image-to-mesh) and importers per source (HuggingFace) for running models on cortexgrid
|
|
5
5
|
Project-URL: Homepage, https://github.com/robodatalab/cortexgrid-infer
|
|
6
6
|
Project-URL: Repository, https://github.com/robodatalab/cortexgrid-infer
|
|
@@ -37,6 +37,27 @@ class CompletionChunk:
|
|
|
37
37
|
def has_tool_calls(self) -> bool:
|
|
38
38
|
return bool(self.tool_calls)
|
|
39
39
|
|
|
40
|
+
@classmethod
|
|
41
|
+
def from_wire(
|
|
42
|
+
cls, answered: dict[str, Any], tools_by_name: dict[str, Callable[..., Any]]
|
|
43
|
+
) -> CompletionChunk:
|
|
44
|
+
tool_calls = []
|
|
45
|
+
for called in answered["tool_calls"]:
|
|
46
|
+
tool = tools_by_name.get(called["name"])
|
|
47
|
+
if tool is None:
|
|
48
|
+
continue
|
|
49
|
+
bound = partial(tool, **called["arguments"])
|
|
50
|
+
tool_call = ToolCall(
|
|
51
|
+
id=called["id"], name=called["name"], arguments=called["arguments"], _func=bound
|
|
52
|
+
)
|
|
53
|
+
tool_calls.append(tool_call)
|
|
54
|
+
return cls(
|
|
55
|
+
content=answered["content"],
|
|
56
|
+
thinking=answered["thinking"],
|
|
57
|
+
tool_calls=tool_calls,
|
|
58
|
+
finish_reason=answered["finish_reason"],
|
|
59
|
+
)
|
|
60
|
+
|
|
40
61
|
|
|
41
62
|
Message = dict[str, Any]
|
|
42
63
|
ToolSpec = dict[str, Any]
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""The wire protocol between a cortexgrid-served completion app and its client.
|
|
2
|
+
|
|
3
|
+
A completion serve app streams plain text from `POST /complete`, with tool calls
|
|
4
|
+
inlined in the text as `<tool_call>{"name": ..., "arguments": {...}}</tool_call>`.
|
|
5
|
+
That is the format an instruct-tuned causal LM emits on its own, so a serve app
|
|
6
|
+
wrapping one forwards its tokens untouched; a serve app wrapping a provider with
|
|
7
|
+
structured tool calls encodes them into the same form with `encode_tool_call`.
|
|
8
|
+
|
|
9
|
+
Either way the client is the same `ServedCompletingModel`, which streams text
|
|
10
|
+
back as `CompletionChunk`s and reassembles the tool calls.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from collections.abc import AsyncIterator
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
import json
|
|
18
|
+
from typing import Any, Sequence
|
|
19
|
+
|
|
20
|
+
import httpx
|
|
21
|
+
|
|
22
|
+
from cortexgrid_infer.core import CompletingModel, CompletionChunk, Message, Tool
|
|
23
|
+
from cortexgrid_infer.utils import build_tool_map, normalize_tools
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class ServedCompletingModel(CompletingModel):
|
|
27
|
+
"""Client for a completion app cortexgrid has deployed at `url`.
|
|
28
|
+
|
|
29
|
+
Provider-agnostic: every completion serve app speaks the protocol above, so
|
|
30
|
+
which model is behind it only shows up in `model_id`."""
|
|
31
|
+
|
|
32
|
+
url: str
|
|
33
|
+
model_id: str
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def name(self) -> str:
|
|
37
|
+
return self.model_id
|
|
38
|
+
|
|
39
|
+
async def loglikelihoods(
|
|
40
|
+
self, messages: list[Message], continuations: list[str]
|
|
41
|
+
) -> list[float]:
|
|
42
|
+
body = {"messages": messages, "continuations": continuations}
|
|
43
|
+
loglikelihoods_url = f"{self.url}/loglikelihoods"
|
|
44
|
+
async with httpx.AsyncClient(timeout=None) as client:
|
|
45
|
+
response = await client.post(loglikelihoods_url, json=body)
|
|
46
|
+
response.raise_for_status()
|
|
47
|
+
answered = response.json()
|
|
48
|
+
return answered["loglikelihoods"]
|
|
49
|
+
|
|
50
|
+
async def complete(
|
|
51
|
+
self,
|
|
52
|
+
messages: list[Message],
|
|
53
|
+
tools: Sequence[Tool] | None = None,
|
|
54
|
+
max_new_tokens: int = 16 * 1024,
|
|
55
|
+
temperature: float = 0.7,
|
|
56
|
+
top_p: float = 0.9,
|
|
57
|
+
top_k: int = 50,
|
|
58
|
+
repetition_penalty: float = 1.0,
|
|
59
|
+
**kwargs: Any,
|
|
60
|
+
) -> AsyncIterator[CompletionChunk]:
|
|
61
|
+
tool_specs = normalize_tools(tools)
|
|
62
|
+
tools_by_name = build_tool_map(tools)
|
|
63
|
+
body: dict[str, Any] = {
|
|
64
|
+
"messages": messages,
|
|
65
|
+
"tools": tool_specs,
|
|
66
|
+
"max_new_tokens": max_new_tokens,
|
|
67
|
+
"temperature": temperature,
|
|
68
|
+
"top_p": top_p,
|
|
69
|
+
"top_k": top_k,
|
|
70
|
+
"repetition_penalty": repetition_penalty,
|
|
71
|
+
**kwargs,
|
|
72
|
+
}
|
|
73
|
+
complete_url = f"{self.url}/complete"
|
|
74
|
+
async with httpx.AsyncClient(timeout=None) as client:
|
|
75
|
+
async with client.stream("POST", complete_url, json=body) as response:
|
|
76
|
+
response.raise_for_status()
|
|
77
|
+
async for line in response.aiter_lines():
|
|
78
|
+
answered = json.loads(line)
|
|
79
|
+
chunk = CompletionChunk.from_wire(answered, tools_by_name)
|
|
80
|
+
yield chunk
|
|
81
|
+
|
|
82
|
+
async def last_hidden_states(
|
|
83
|
+
self, parts: list[str], continuations_of_each_part: list[list[str]]
|
|
84
|
+
) -> AsyncIterator[list[list[float]]]:
|
|
85
|
+
body = {"parts": parts, "continuations_of_each_part": continuations_of_each_part}
|
|
86
|
+
last_hidden_states_url = f"{self.url}/last_hidden_states"
|
|
87
|
+
async with httpx.AsyncClient(timeout=None) as client:
|
|
88
|
+
async with client.stream("POST", last_hidden_states_url, json=body) as response:
|
|
89
|
+
response.raise_for_status()
|
|
90
|
+
async for line in response.aiter_lines():
|
|
91
|
+
answered = json.loads(line)
|
|
92
|
+
yield answered["last_hidden_states"]
|
|
@@ -19,12 +19,13 @@ from typing import Any
|
|
|
19
19
|
import cortexgrid
|
|
20
20
|
from anthropic import AsyncAnthropic
|
|
21
21
|
from cortexgrid import serve
|
|
22
|
-
from fastapi import FastAPI
|
|
22
|
+
from fastapi import FastAPI, HTTPException, status
|
|
23
23
|
from fastapi.responses import StreamingResponse
|
|
24
24
|
|
|
25
25
|
from cortexgrid_infer.core import Message, ToolSpec
|
|
26
|
-
from cortexgrid_infer.protocols.completion import ServedCompletingModel
|
|
26
|
+
from cortexgrid_infer.protocols.completion import ServedCompletingModel
|
|
27
27
|
from cortexgrid_infer.serve_apps.base import HostedModel
|
|
28
|
+
from cortexgrid_infer.serve_apps.streamed_completion import StreamedCompletionWriter
|
|
28
29
|
|
|
29
30
|
_app = FastAPI()
|
|
30
31
|
|
|
@@ -162,19 +163,30 @@ class AnthropicText2Text(HostedModel):
|
|
|
162
163
|
create_kwargs["tools"] = tools
|
|
163
164
|
|
|
164
165
|
async def stream() -> AsyncIterator[bytes]:
|
|
166
|
+
writer = StreamedCompletionWriter()
|
|
165
167
|
async with self._client.messages.stream(**create_kwargs) as events:
|
|
166
168
|
async for event in events:
|
|
167
169
|
if (
|
|
168
170
|
event.type == "content_block_delta"
|
|
169
171
|
and event.delta.type == "text_delta"
|
|
170
172
|
):
|
|
171
|
-
|
|
173
|
+
line = writer.read_content(event.delta.text)
|
|
174
|
+
yield line
|
|
172
175
|
|
|
173
176
|
elif event.type == "content_block_stop":
|
|
174
177
|
block = events.current_message_snapshot.content[event.index]
|
|
175
178
|
if block.type == "tool_use":
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
179
|
+
arguments = dict(block.input)
|
|
180
|
+
line = writer.read_tool_call(block.name, arguments, block.id)
|
|
181
|
+
yield line
|
|
182
|
+
for line in writer.finish():
|
|
183
|
+
yield line
|
|
184
|
+
|
|
185
|
+
return StreamingResponse(stream(), media_type="application/x-ndjson")
|
|
186
|
+
|
|
187
|
+
@_app.post("/last_hidden_states")
|
|
188
|
+
async def last_hidden_states(self, body: dict[str, Any]) -> None:
|
|
189
|
+
raise HTTPException(
|
|
190
|
+
status_code=status.HTTP_501_NOT_IMPLEMENTED,
|
|
191
|
+
detail="Anthropic does not expose its models' hidden states",
|
|
192
|
+
)
|
{cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.7}/cortexgrid_infer/serve_apps/gemini/text2text.py
RENAMED
|
@@ -12,17 +12,14 @@ from collections.abc import AsyncIterator
|
|
|
12
12
|
from typing import Any
|
|
13
13
|
|
|
14
14
|
from cortexgrid import serve
|
|
15
|
-
from fastapi import FastAPI
|
|
15
|
+
from fastapi import FastAPI, HTTPException, status
|
|
16
16
|
from fastapi.responses import StreamingResponse
|
|
17
17
|
|
|
18
18
|
from cortexgrid_infer.core import Message, ToolSpec
|
|
19
|
-
from cortexgrid_infer.protocols.completion import
|
|
20
|
-
ServedCompletingModel,
|
|
21
|
-
encode_thinking,
|
|
22
|
-
encode_tool_call,
|
|
23
|
-
)
|
|
19
|
+
from cortexgrid_infer.protocols.completion import ServedCompletingModel
|
|
24
20
|
from cortexgrid_infer.serve_apps.base import ENABLE_THINKING_PARAM
|
|
25
21
|
from cortexgrid_infer.serve_apps.gemini.base import DEFAULT_API_KEY_SECRET, GeminiModel
|
|
22
|
+
from cortexgrid_infer.serve_apps.streamed_completion import StreamedCompletionWriter
|
|
26
23
|
|
|
27
24
|
_app = FastAPI()
|
|
28
25
|
|
|
@@ -150,6 +147,7 @@ class GeminiText2Text(GeminiModel):
|
|
|
150
147
|
generate_config["tools"] = tools
|
|
151
148
|
|
|
152
149
|
async def stream() -> AsyncIterator[bytes]:
|
|
150
|
+
writer = StreamedCompletionWriter()
|
|
153
151
|
chunks = await self._client.aio.models.generate_content_stream(
|
|
154
152
|
model=self._model, contents=contents, config=generate_config
|
|
155
153
|
)
|
|
@@ -158,14 +156,27 @@ class GeminiText2Text(GeminiModel):
|
|
|
158
156
|
continue
|
|
159
157
|
for part in chunk.candidates[0].content.parts or []:
|
|
160
158
|
if part.function_call:
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
159
|
+
arguments = dict(part.function_call.args or {})
|
|
160
|
+
line = writer.read_tool_call(
|
|
161
|
+
part.function_call.name, arguments, part.function_call.id
|
|
162
|
+
)
|
|
163
|
+
yield line
|
|
164
164
|
|
|
165
165
|
elif part.text and part.thought:
|
|
166
|
-
|
|
166
|
+
line = writer.read_thinking(part.text)
|
|
167
|
+
yield line
|
|
167
168
|
|
|
168
169
|
elif part.text:
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
170
|
+
line = writer.read_content(part.text)
|
|
171
|
+
yield line
|
|
172
|
+
for line in writer.finish():
|
|
173
|
+
yield line
|
|
174
|
+
|
|
175
|
+
return StreamingResponse(stream(), media_type="application/x-ndjson")
|
|
176
|
+
|
|
177
|
+
@_app.post("/last_hidden_states")
|
|
178
|
+
async def last_hidden_states(self, body: dict[str, Any]) -> None:
|
|
179
|
+
raise HTTPException(
|
|
180
|
+
status_code=status.HTTP_501_NOT_IMPLEMENTED,
|
|
181
|
+
detail="Gemini does not expose its models' hidden states",
|
|
182
|
+
)
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import itertools
|
|
4
|
+
import json
|
|
5
|
+
import re
|
|
6
|
+
from typing import Any, Sequence
|
|
7
|
+
|
|
8
|
+
_tool_call_id_counter = itertools.count()
|
|
9
|
+
|
|
10
|
+
TOOL_CALL_OPENERS = ["<tool_call>", "<|tool_call|>", "```tool_call"]
|
|
11
|
+
|
|
12
|
+
THINKING_OPENER = "<think>"
|
|
13
|
+
THINKING_CLOSER = "</think>"
|
|
14
|
+
|
|
15
|
+
TOOL_CALL_PATTERNS = [
|
|
16
|
+
re.compile(r"<tool_call>\s*(\{.*?\})\s*</tool_call>", re.DOTALL),
|
|
17
|
+
re.compile(r"<\|tool_call\|>\s*(\{.*?\})\s*<\|/tool_call\|>", re.DOTALL),
|
|
18
|
+
re.compile(r"```tool_call\s*(\{.*?\})\s*```", re.DOTALL),
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def new_tool_call_id() -> str:
|
|
23
|
+
number = next(_tool_call_id_counter)
|
|
24
|
+
return f"call_{number}"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def completion_chunk(
|
|
28
|
+
content: str = "",
|
|
29
|
+
thinking: str = "",
|
|
30
|
+
tool_calls: Sequence[dict[str, Any]] = (),
|
|
31
|
+
finish_reason: str | None = None,
|
|
32
|
+
) -> bytes:
|
|
33
|
+
chunk = {
|
|
34
|
+
"content": content,
|
|
35
|
+
"thinking": thinking,
|
|
36
|
+
"tool_calls": list(tool_calls),
|
|
37
|
+
"finish_reason": finish_reason,
|
|
38
|
+
}
|
|
39
|
+
line = json.dumps(chunk)
|
|
40
|
+
return f"{line}\n".encode("utf-8")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _find_opener(text: str, openers: Sequence[str]) -> int | None:
|
|
44
|
+
"""Find the earliest position of any of `openers` in text, or None."""
|
|
45
|
+
earliest = None
|
|
46
|
+
for opener in openers:
|
|
47
|
+
idx = text.find(opener)
|
|
48
|
+
if idx != -1 and (earliest is None or idx < earliest):
|
|
49
|
+
earliest = idx
|
|
50
|
+
return earliest
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _split_at_potential_prefix(text: str, openers: Sequence[str]) -> tuple[str, str]:
|
|
54
|
+
"""Split text into (safe_to_stream, potential_opener_prefix).
|
|
55
|
+
|
|
56
|
+
The second part is a suffix that could be the beginning of one of
|
|
57
|
+
`openers`, so it must be held back until more text arrives.
|
|
58
|
+
"""
|
|
59
|
+
for opener in openers:
|
|
60
|
+
for length in range(1, len(opener)):
|
|
61
|
+
if text.endswith(opener[:length]):
|
|
62
|
+
return text[:-length], text[-length:]
|
|
63
|
+
return text, ""
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def tool_calls_in(text: str) -> tuple[str, list[dict[str, Any]]]:
|
|
67
|
+
tool_calls = []
|
|
68
|
+
clean_text = text
|
|
69
|
+
for pattern in TOOL_CALL_PATTERNS:
|
|
70
|
+
matches = pattern.findall(text)
|
|
71
|
+
for match in matches:
|
|
72
|
+
try:
|
|
73
|
+
data = json.loads(match)
|
|
74
|
+
name = data["name"]
|
|
75
|
+
except (json.JSONDecodeError, KeyError):
|
|
76
|
+
continue
|
|
77
|
+
arguments = data.get("arguments", {})
|
|
78
|
+
call_id = new_tool_call_id()
|
|
79
|
+
tool_calls.append({"id": call_id, "name": name, "arguments": arguments})
|
|
80
|
+
clean_text = pattern.sub("", clean_text)
|
|
81
|
+
stripped = clean_text.strip()
|
|
82
|
+
return stripped, tool_calls
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class StreamedCompletionWriter:
|
|
86
|
+
def __init__(self) -> None:
|
|
87
|
+
self._pending = ""
|
|
88
|
+
self._in_thinking = False
|
|
89
|
+
self._in_tool_call = False
|
|
90
|
+
self._tool_call_text = ""
|
|
91
|
+
self._made_a_tool_call = False
|
|
92
|
+
|
|
93
|
+
def read_content(self, text: str) -> bytes:
|
|
94
|
+
return completion_chunk(content=text)
|
|
95
|
+
|
|
96
|
+
def read_thinking(self, text: str) -> bytes:
|
|
97
|
+
return completion_chunk(thinking=text)
|
|
98
|
+
|
|
99
|
+
def read_tool_call(
|
|
100
|
+
self, name: str, arguments: dict[str, Any], call_id: str | None = None
|
|
101
|
+
) -> bytes:
|
|
102
|
+
self._made_a_tool_call = True
|
|
103
|
+
tool_call_id = call_id or new_tool_call_id()
|
|
104
|
+
tool_call = {"id": tool_call_id, "name": name, "arguments": arguments}
|
|
105
|
+
return completion_chunk(tool_calls=[tool_call])
|
|
106
|
+
|
|
107
|
+
def read(self, text: str) -> list[bytes]:
|
|
108
|
+
if not text:
|
|
109
|
+
return []
|
|
110
|
+
if self._in_tool_call:
|
|
111
|
+
self._tool_call_text += text
|
|
112
|
+
return []
|
|
113
|
+
self._pending += text
|
|
114
|
+
lines: list[bytes] = []
|
|
115
|
+
openers = [THINKING_OPENER, *TOOL_CALL_OPENERS]
|
|
116
|
+
while self._pending and not self._in_tool_call:
|
|
117
|
+
if self._in_thinking:
|
|
118
|
+
closer_pos = self._pending.find(THINKING_CLOSER)
|
|
119
|
+
if closer_pos == -1:
|
|
120
|
+
thought, self._pending = _split_at_potential_prefix(
|
|
121
|
+
self._pending, [THINKING_CLOSER]
|
|
122
|
+
)
|
|
123
|
+
if thought:
|
|
124
|
+
lines.append(completion_chunk(thinking=thought))
|
|
125
|
+
break
|
|
126
|
+
if closer_pos:
|
|
127
|
+
thought = self._pending[:closer_pos]
|
|
128
|
+
lines.append(completion_chunk(thinking=thought))
|
|
129
|
+
self._pending = self._pending[closer_pos + len(THINKING_CLOSER) :]
|
|
130
|
+
self._in_thinking = False
|
|
131
|
+
continue
|
|
132
|
+
|
|
133
|
+
opener_pos = _find_opener(self._pending, openers)
|
|
134
|
+
if opener_pos is None:
|
|
135
|
+
safe, self._pending = _split_at_potential_prefix(self._pending, openers)
|
|
136
|
+
if safe:
|
|
137
|
+
lines.append(completion_chunk(content=safe))
|
|
138
|
+
break
|
|
139
|
+
|
|
140
|
+
before = self._pending[:opener_pos]
|
|
141
|
+
if before:
|
|
142
|
+
lines.append(completion_chunk(content=before))
|
|
143
|
+
if self._pending.startswith(THINKING_OPENER, opener_pos):
|
|
144
|
+
self._pending = self._pending[opener_pos + len(THINKING_OPENER) :]
|
|
145
|
+
self._in_thinking = True
|
|
146
|
+
else:
|
|
147
|
+
self._tool_call_text = self._pending[opener_pos:]
|
|
148
|
+
self._in_tool_call = True
|
|
149
|
+
self._pending = ""
|
|
150
|
+
return lines
|
|
151
|
+
|
|
152
|
+
def finish(self) -> list[bytes]:
|
|
153
|
+
lines: list[bytes] = []
|
|
154
|
+
if self._pending and self._in_thinking:
|
|
155
|
+
lines.append(completion_chunk(thinking=self._pending))
|
|
156
|
+
elif self._pending:
|
|
157
|
+
lines.append(completion_chunk(content=self._pending))
|
|
158
|
+
clean_text, tool_calls = tool_calls_in(self._tool_call_text)
|
|
159
|
+
if clean_text:
|
|
160
|
+
lines.append(completion_chunk(content=clean_text))
|
|
161
|
+
if tool_calls:
|
|
162
|
+
lines.append(completion_chunk(tool_calls=tool_calls))
|
|
163
|
+
elif not self._made_a_tool_call:
|
|
164
|
+
lines.append(completion_chunk(finish_reason="stop"))
|
|
165
|
+
return lines
|
|
@@ -10,8 +10,10 @@ its tokens are forwarded untouched.
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
12
|
import asyncio
|
|
13
|
+
import copy
|
|
14
|
+
import json
|
|
13
15
|
import logging
|
|
14
|
-
from collections.abc import AsyncIterator
|
|
16
|
+
from collections.abc import AsyncIterator, Callable
|
|
15
17
|
from threading import Thread
|
|
16
18
|
from typing import Any
|
|
17
19
|
|
|
@@ -22,7 +24,9 @@ from fastapi.responses import StreamingResponse
|
|
|
22
24
|
from transformers import (
|
|
23
25
|
AutoModelForCausalLM,
|
|
24
26
|
AutoTokenizer,
|
|
27
|
+
Cache,
|
|
25
28
|
CompileConfig,
|
|
29
|
+
PreTrainedModel,
|
|
26
30
|
TextIteratorStreamer,
|
|
27
31
|
)
|
|
28
32
|
import torch
|
|
@@ -35,6 +39,7 @@ from cortexgrid_infer.serve_apps.base import (
|
|
|
35
39
|
ENABLE_THINKING_PARAM,
|
|
36
40
|
LocalModel,
|
|
37
41
|
)
|
|
42
|
+
from cortexgrid_infer.serve_apps.streamed_completion import StreamedCompletionWriter
|
|
38
43
|
|
|
39
44
|
|
|
40
45
|
log = logging.getLogger(__name__)
|
|
@@ -51,6 +56,9 @@ _app = FastAPI()
|
|
|
51
56
|
MAX_TOTAL_TOKENS_PARAM = "max_total_tokens"
|
|
52
57
|
DEFAULT_MAX_TOTAL_TOKENS = 4096
|
|
53
58
|
|
|
59
|
+
DTYPE_PARAM = "dtype"
|
|
60
|
+
DEFAULT_DTYPE = "float16"
|
|
61
|
+
|
|
54
62
|
_RESERVED_BODY_KEYS = {
|
|
55
63
|
"messages",
|
|
56
64
|
"tools",
|
|
@@ -59,9 +67,55 @@ _RESERVED_BODY_KEYS = {
|
|
|
59
67
|
"top_p",
|
|
60
68
|
"top_k",
|
|
61
69
|
"repetition_penalty",
|
|
70
|
+
"choices",
|
|
62
71
|
}
|
|
63
72
|
|
|
64
73
|
|
|
74
|
+
def allowed_next_tokens(
|
|
75
|
+
choices: list[list[int]], generated: list[int], end_of_text: int
|
|
76
|
+
) -> list[int]:
|
|
77
|
+
matching = [choice for choice in choices if choice[: len(generated)] == generated]
|
|
78
|
+
continuing = {choice[len(generated)] for choice in matching if len(choice) > len(generated)}
|
|
79
|
+
finished = any(choice == generated for choice in matching)
|
|
80
|
+
allowed = sorted(continuing | {end_of_text}) if finished else sorted(continuing)
|
|
81
|
+
return allowed or [end_of_text]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def continuation_loglikelihood(
|
|
85
|
+
model: PreTrainedModel, prompt_ids: list[int], continuation_ids: list[int]
|
|
86
|
+
) -> float:
|
|
87
|
+
read = torch.tensor([prompt_ids + continuation_ids], device=model.device)
|
|
88
|
+
with torch.inference_mode():
|
|
89
|
+
outputs = model(input_ids=read, logits_to_keep=len(continuation_ids) + 1)
|
|
90
|
+
predicting_the_continuation = outputs.logits[0, :-1].float()
|
|
91
|
+
log_probabilities = torch.log_softmax(predicting_the_continuation, dim=-1)
|
|
92
|
+
positions = torch.arange(len(continuation_ids), device=log_probabilities.device)
|
|
93
|
+
continuation = torch.tensor(continuation_ids, device=log_probabilities.device)
|
|
94
|
+
return float(log_probabilities[positions, continuation].sum())
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def read_further(model: PreTrainedModel, read: Cache | None, ids: list[int]) -> Cache:
|
|
98
|
+
unread = torch.tensor([ids], device=model.device)
|
|
99
|
+
with torch.inference_mode():
|
|
100
|
+
outputs = model(input_ids=unread, past_key_values=read, use_cache=True, logits_to_keep=1)
|
|
101
|
+
return outputs.past_key_values
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def last_hidden_state_after(model: PreTrainedModel, read: Cache, ids: list[int]) -> torch.Tensor:
|
|
105
|
+
read_for_the_continuation = copy.deepcopy(read)
|
|
106
|
+
continuation = torch.tensor([ids], device=model.device)
|
|
107
|
+
with torch.inference_mode():
|
|
108
|
+
outputs = model(
|
|
109
|
+
input_ids=continuation,
|
|
110
|
+
past_key_values=read_for_the_continuation,
|
|
111
|
+
use_cache=True,
|
|
112
|
+
output_hidden_states=True,
|
|
113
|
+
logits_to_keep=1,
|
|
114
|
+
)
|
|
115
|
+
last_layer = outputs.hidden_states[-1]
|
|
116
|
+
return last_layer[0, -1].float()
|
|
117
|
+
|
|
118
|
+
|
|
65
119
|
@serve.ingress(_app)
|
|
66
120
|
class Text2Text(LocalModel):
|
|
67
121
|
@classmethod
|
|
@@ -76,15 +130,15 @@ class Text2Text(LocalModel):
|
|
|
76
130
|
path = cortexgrid.load_model(
|
|
77
131
|
deployment.family, deployment.suffix, deployment.run_name
|
|
78
132
|
)
|
|
133
|
+
settings = cortexgrid.model_config(deployment)
|
|
79
134
|
self._device = detect_device()
|
|
80
135
|
self._tokenizer = AutoTokenizer.from_pretrained(path)
|
|
81
136
|
if self._tokenizer.pad_token is None:
|
|
82
137
|
self._tokenizer.pad_token = self._tokenizer.eos_token
|
|
83
138
|
self._model = AutoModelForCausalLM.from_pretrained(
|
|
84
|
-
str(path), torch_dtype=torch.
|
|
139
|
+
str(path), torch_dtype=getattr(torch, settings.get(DTYPE_PARAM, DEFAULT_DTYPE))
|
|
85
140
|
)
|
|
86
141
|
self._model.to(self._device) # type: ignore
|
|
87
|
-
settings = cortexgrid.model_config(deployment)
|
|
88
142
|
context = getattr(
|
|
89
143
|
self._model.config, "max_position_embeddings", DEFAULT_MAX_TOTAL_TOKENS
|
|
90
144
|
)
|
|
@@ -127,6 +181,82 @@ class Text2Text(LocalModel):
|
|
|
127
181
|
inputs[key] = value[..., -limit:]
|
|
128
182
|
return inputs
|
|
129
183
|
|
|
184
|
+
def _prompt_ids(self, messages: list[dict[str, Any]]) -> list[int]:
|
|
185
|
+
prompt = self._tokenizer.apply_chat_template(
|
|
186
|
+
messages,
|
|
187
|
+
tokenize=False,
|
|
188
|
+
add_generation_prompt=True,
|
|
189
|
+
enable_thinking=self._enable_thinking,
|
|
190
|
+
)
|
|
191
|
+
return self._tokenizer(prompt)["input_ids"]
|
|
192
|
+
|
|
193
|
+
def _continuation_ids(self, text: str) -> list[int]:
|
|
194
|
+
return self._tokenizer(text, add_special_tokens=False)["input_ids"]
|
|
195
|
+
|
|
196
|
+
def _only_the_choices(
|
|
197
|
+
self, choices: list[str], prompt_length: int
|
|
198
|
+
) -> Callable[[int, torch.Tensor], list[int]]:
|
|
199
|
+
choice_ids = [self._continuation_ids(choice) for choice in choices]
|
|
200
|
+
end_of_text = self._tokenizer.eos_token_id
|
|
201
|
+
|
|
202
|
+
def allowed(_batch: int, sequence: torch.Tensor) -> list[int]:
|
|
203
|
+
generated = sequence[prompt_length:].tolist()
|
|
204
|
+
return allowed_next_tokens(choice_ids, generated, end_of_text)
|
|
205
|
+
|
|
206
|
+
return allowed
|
|
207
|
+
|
|
208
|
+
def _loglikelihoods(
|
|
209
|
+
self, messages: list[dict[str, Any]], continuations: list[str]
|
|
210
|
+
) -> list[float]:
|
|
211
|
+
prompt_ids = self._prompt_ids(messages)
|
|
212
|
+
continuation_ids = [self._continuation_ids(continuation) for continuation in continuations]
|
|
213
|
+
return [
|
|
214
|
+
continuation_loglikelihood(self._model, prompt_ids, ids) for ids in continuation_ids
|
|
215
|
+
]
|
|
216
|
+
|
|
217
|
+
@_app.post("/loglikelihoods")
|
|
218
|
+
async def loglikelihoods(self, body: dict[str, Any]) -> dict[str, list[float]]:
|
|
219
|
+
loop = asyncio.get_running_loop()
|
|
220
|
+
loglikelihoods = await loop.run_in_executor(
|
|
221
|
+
None, self._loglikelihoods, body["messages"], body["continuations"]
|
|
222
|
+
)
|
|
223
|
+
return {"loglikelihoods": loglikelihoods}
|
|
224
|
+
|
|
225
|
+
def _read_part(
|
|
226
|
+
self, read: Cache | None, part: str, continuations: list[str]
|
|
227
|
+
) -> tuple[Cache, list[list[float]]]:
|
|
228
|
+
tokenized = self._tokenizer(part, add_special_tokens=read is None)
|
|
229
|
+
part_ids = tokenized["input_ids"]
|
|
230
|
+
read = read_further(self._model, read, part_ids)
|
|
231
|
+
continuation_ids = [self._continuation_ids(continuation) for continuation in continuations]
|
|
232
|
+
last_hidden_states = [
|
|
233
|
+
last_hidden_state_after(self._model, read, ids) for ids in continuation_ids
|
|
234
|
+
]
|
|
235
|
+
answered = [hidden_state.tolist() for hidden_state in last_hidden_states]
|
|
236
|
+
return read, answered
|
|
237
|
+
|
|
238
|
+
async def _last_hidden_states_of_each_part(
|
|
239
|
+
self, parts: list[str], continuations_of_each_part: list[list[str]]
|
|
240
|
+
) -> AsyncIterator[bytes]:
|
|
241
|
+
loop = asyncio.get_running_loop()
|
|
242
|
+
read: Cache | None = None
|
|
243
|
+
for part, continuations in zip(parts, continuations_of_each_part):
|
|
244
|
+
read, last_hidden_states = await loop.run_in_executor(
|
|
245
|
+
None, self._read_part, read, part, continuations
|
|
246
|
+
)
|
|
247
|
+
answered = json.dumps({"last_hidden_states": last_hidden_states})
|
|
248
|
+
line = f"{answered}\n".encode("utf-8")
|
|
249
|
+
yield line
|
|
250
|
+
|
|
251
|
+
@_app.post("/last_hidden_states")
|
|
252
|
+
async def last_hidden_states(self, body: dict[str, Any]) -> StreamingResponse:
|
|
253
|
+
last_hidden_states_of_each_part = self._last_hidden_states_of_each_part(
|
|
254
|
+
body["parts"], body["continuations_of_each_part"]
|
|
255
|
+
)
|
|
256
|
+
return StreamingResponse(
|
|
257
|
+
last_hidden_states_of_each_part, media_type="application/x-ndjson"
|
|
258
|
+
)
|
|
259
|
+
|
|
130
260
|
@_app.post("/complete")
|
|
131
261
|
async def complete(self, body: dict[str, Any]) -> StreamingResponse:
|
|
132
262
|
messages = body["messages"]
|
|
@@ -138,6 +268,7 @@ class Text2Text(LocalModel):
|
|
|
138
268
|
top_p = body.get("top_p", 0.9)
|
|
139
269
|
top_k = body.get("top_k", 50)
|
|
140
270
|
repetition_penalty = body.get("repetition_penalty", 1.0)
|
|
271
|
+
choices = body.get("choices")
|
|
141
272
|
extra = {k: v for k, v in body.items() if k not in _RESERVED_BODY_KEYS}
|
|
142
273
|
|
|
143
274
|
prompt = self._tokenizer.apply_chat_template(
|
|
@@ -151,6 +282,9 @@ class Text2Text(LocalModel):
|
|
|
151
282
|
self._tokenizer(prompt, return_tensors="pt"),
|
|
152
283
|
self._max_total_tokens - max_new_tokens,
|
|
153
284
|
).to(self._model.device)
|
|
285
|
+
if choices:
|
|
286
|
+
prompt_length = inputs["input_ids"].shape[-1]
|
|
287
|
+
extra["prefix_allowed_tokens_fn"] = self._only_the_choices(choices, prompt_length)
|
|
154
288
|
|
|
155
289
|
queue: asyncio.Queue[str | None] = asyncio.Queue()
|
|
156
290
|
error: list[BaseException] = []
|
|
@@ -192,13 +326,16 @@ class Text2Text(LocalModel):
|
|
|
192
326
|
loop.run_in_executor(None, generate)
|
|
193
327
|
|
|
194
328
|
async def stream() -> AsyncIterator[bytes]:
|
|
329
|
+
writer = StreamedCompletionWriter()
|
|
195
330
|
while True:
|
|
196
331
|
text = await queue.get()
|
|
197
332
|
if text is None:
|
|
198
333
|
break
|
|
199
|
-
|
|
200
|
-
yield
|
|
334
|
+
for line in writer.read(text):
|
|
335
|
+
yield line
|
|
201
336
|
if error:
|
|
202
337
|
raise error[0]
|
|
338
|
+
for line in writer.finish():
|
|
339
|
+
yield line
|
|
203
340
|
|
|
204
|
-
return StreamingResponse(stream(), media_type="
|
|
341
|
+
return StreamingResponse(stream(), media_type="application/x-ndjson")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "cortexgrid-infer"
|
|
3
|
-
version = "0.3.
|
|
3
|
+
version = "0.3.7"
|
|
4
4
|
description = "Serve apps per task (text-to-text, text-to-image, image-to-mesh) and importers per source (HuggingFace) for running models on cortexgrid"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "Apache-2.0"
|
|
@@ -1,222 +0,0 @@
|
|
|
1
|
-
"""The wire protocol between a cortexgrid-served completion app and its client.
|
|
2
|
-
|
|
3
|
-
A completion serve app streams plain text from `POST /complete`, with tool calls
|
|
4
|
-
inlined in the text as `<tool_call>{"name": ..., "arguments": {...}}</tool_call>`.
|
|
5
|
-
That is the format an instruct-tuned causal LM emits on its own, so a serve app
|
|
6
|
-
wrapping one forwards its tokens untouched; a serve app wrapping a provider with
|
|
7
|
-
structured tool calls encodes them into the same form with `encode_tool_call`.
|
|
8
|
-
|
|
9
|
-
Either way the client is the same `ServedCompletingModel`, which streams text
|
|
10
|
-
back as `CompletionChunk`s and reassembles the tool calls.
|
|
11
|
-
"""
|
|
12
|
-
|
|
13
|
-
from __future__ import annotations
|
|
14
|
-
|
|
15
|
-
from collections.abc import AsyncIterator
|
|
16
|
-
from dataclasses import dataclass
|
|
17
|
-
from functools import partial
|
|
18
|
-
import itertools
|
|
19
|
-
import json
|
|
20
|
-
import re
|
|
21
|
-
from typing import Any, Callable, Sequence
|
|
22
|
-
|
|
23
|
-
import httpx
|
|
24
|
-
|
|
25
|
-
from cortexgrid_infer.core import (
|
|
26
|
-
CompletingModel,
|
|
27
|
-
CompletionChunk,
|
|
28
|
-
Message,
|
|
29
|
-
Tool,
|
|
30
|
-
ToolCall,
|
|
31
|
-
)
|
|
32
|
-
from cortexgrid_infer.utils import build_tool_map, normalize_tools
|
|
33
|
-
|
|
34
|
-
_tool_call_id_counter = itertools.count()
|
|
35
|
-
|
|
36
|
-
TOOL_CALL_OPENERS = ["<tool_call>", "<|tool_call|>", "```tool_call"]
|
|
37
|
-
|
|
38
|
-
THINKING_OPENER = "<think>"
|
|
39
|
-
THINKING_CLOSER = "</think>"
|
|
40
|
-
|
|
41
|
-
TOOL_CALL_PATTERNS = [
|
|
42
|
-
re.compile(r"<tool_call>\s*(\{.*?\})\s*</tool_call>", re.DOTALL),
|
|
43
|
-
re.compile(r"<\|tool_call\|>\s*(\{.*?\})\s*<\|/tool_call\|>", re.DOTALL),
|
|
44
|
-
re.compile(r"```tool_call\s*(\{.*?\})\s*```", re.DOTALL),
|
|
45
|
-
]
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def encode_tool_call(name: str, arguments: dict[str, Any]) -> str:
|
|
49
|
-
"""Render a tool call in the form the client parses back out of the stream.
|
|
50
|
-
|
|
51
|
-
For serve apps whose upstream reports tool calls as structured data rather
|
|
52
|
-
than as text the model generated - they are re-encoded here so one client
|
|
53
|
-
handles every completion app."""
|
|
54
|
-
return f'<tool_call>{json.dumps({"name": name, "arguments": arguments})}</tool_call>'
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def encode_thinking(text: str) -> str:
|
|
58
|
-
return f"{THINKING_OPENER}{text}{THINKING_CLOSER}"
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def _find_opener(text: str, openers: Sequence[str]) -> int | None:
|
|
62
|
-
"""Find the earliest position of any of `openers` in text, or None."""
|
|
63
|
-
earliest = None
|
|
64
|
-
for opener in openers:
|
|
65
|
-
idx = text.find(opener)
|
|
66
|
-
if idx != -1 and (earliest is None or idx < earliest):
|
|
67
|
-
earliest = idx
|
|
68
|
-
return earliest
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
def _split_at_potential_prefix(text: str, openers: Sequence[str]) -> tuple[str, str]:
|
|
72
|
-
"""Split text into (safe_to_stream, potential_opener_prefix).
|
|
73
|
-
|
|
74
|
-
The second part is a suffix that could be the beginning of one of
|
|
75
|
-
`openers`, so it must be held back until more text arrives.
|
|
76
|
-
"""
|
|
77
|
-
for opener in openers:
|
|
78
|
-
for length in range(1, len(opener)):
|
|
79
|
-
if text.endswith(opener[:length]):
|
|
80
|
-
return text[:-length], text[-length:]
|
|
81
|
-
return text, ""
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
def parse_tool_calls(
|
|
85
|
-
text: str, tool_map: dict[str, Callable[..., Any]]
|
|
86
|
-
) -> tuple[str, list[ToolCall]]:
|
|
87
|
-
"""Parse tool calls from generated text and bind them to their functions."""
|
|
88
|
-
tool_calls: list[ToolCall] = []
|
|
89
|
-
clean_text = text
|
|
90
|
-
|
|
91
|
-
for pattern in TOOL_CALL_PATTERNS:
|
|
92
|
-
matches = pattern.findall(text)
|
|
93
|
-
for match in matches:
|
|
94
|
-
try:
|
|
95
|
-
data = json.loads(match)
|
|
96
|
-
name = data["name"]
|
|
97
|
-
arguments = data.get("arguments", {})
|
|
98
|
-
if name in tool_map:
|
|
99
|
-
tool_calls.append(
|
|
100
|
-
ToolCall(
|
|
101
|
-
id=f"call_{next(_tool_call_id_counter)}",
|
|
102
|
-
name=name,
|
|
103
|
-
arguments=arguments,
|
|
104
|
-
_func=partial(tool_map[name], **arguments),
|
|
105
|
-
)
|
|
106
|
-
)
|
|
107
|
-
except (json.JSONDecodeError, KeyError):
|
|
108
|
-
continue
|
|
109
|
-
clean_text = pattern.sub("", clean_text)
|
|
110
|
-
|
|
111
|
-
return clean_text.strip(), tool_calls
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
@dataclass
|
|
115
|
-
class ServedCompletingModel(CompletingModel):
|
|
116
|
-
"""Client for a completion app cortexgrid has deployed at `url`.
|
|
117
|
-
|
|
118
|
-
Provider-agnostic: every completion serve app speaks the protocol above, so
|
|
119
|
-
which model is behind it only shows up in `model_id`."""
|
|
120
|
-
|
|
121
|
-
url: str
|
|
122
|
-
model_id: str
|
|
123
|
-
|
|
124
|
-
@property
|
|
125
|
-
def name(self) -> str:
|
|
126
|
-
return self.model_id
|
|
127
|
-
|
|
128
|
-
async def complete(
|
|
129
|
-
self,
|
|
130
|
-
messages: list[Message],
|
|
131
|
-
tools: Sequence[Tool] | None = None,
|
|
132
|
-
max_new_tokens: int = 16 * 1024,
|
|
133
|
-
temperature: float = 0.7,
|
|
134
|
-
top_p: float = 0.9,
|
|
135
|
-
top_k: int = 50,
|
|
136
|
-
repetition_penalty: float = 1.0,
|
|
137
|
-
**kwargs: Any,
|
|
138
|
-
) -> AsyncIterator[CompletionChunk]:
|
|
139
|
-
tool_specs = normalize_tools(tools)
|
|
140
|
-
tool_map = build_tool_map(tools)
|
|
141
|
-
|
|
142
|
-
body: dict[str, Any] = {
|
|
143
|
-
"messages": messages,
|
|
144
|
-
"tools": tool_specs,
|
|
145
|
-
"max_new_tokens": max_new_tokens,
|
|
146
|
-
"temperature": temperature,
|
|
147
|
-
"top_p": top_p,
|
|
148
|
-
"top_k": top_k,
|
|
149
|
-
"repetition_penalty": repetition_penalty,
|
|
150
|
-
**kwargs,
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
pending = ""
|
|
154
|
-
in_thinking = False
|
|
155
|
-
in_tool_call = False
|
|
156
|
-
tool_call_text = ""
|
|
157
|
-
openers = [THINKING_OPENER, *TOOL_CALL_OPENERS]
|
|
158
|
-
|
|
159
|
-
async with httpx.AsyncClient(timeout=None) as client:
|
|
160
|
-
async with client.stream(
|
|
161
|
-
"POST", f"{self.url}/complete", json=body
|
|
162
|
-
) as response:
|
|
163
|
-
response.raise_for_status()
|
|
164
|
-
async for text in response.aiter_text():
|
|
165
|
-
if not text:
|
|
166
|
-
continue
|
|
167
|
-
|
|
168
|
-
if in_tool_call:
|
|
169
|
-
tool_call_text += text
|
|
170
|
-
continue
|
|
171
|
-
|
|
172
|
-
pending += text
|
|
173
|
-
|
|
174
|
-
while pending and not in_tool_call:
|
|
175
|
-
if in_thinking:
|
|
176
|
-
closer_pos = pending.find(THINKING_CLOSER)
|
|
177
|
-
if closer_pos == -1:
|
|
178
|
-
thought, pending = _split_at_potential_prefix(
|
|
179
|
-
pending, [THINKING_CLOSER]
|
|
180
|
-
)
|
|
181
|
-
if thought:
|
|
182
|
-
yield CompletionChunk(thinking=thought)
|
|
183
|
-
break
|
|
184
|
-
if closer_pos:
|
|
185
|
-
yield CompletionChunk(thinking=pending[:closer_pos])
|
|
186
|
-
pending = pending[closer_pos + len(THINKING_CLOSER) :]
|
|
187
|
-
in_thinking = False
|
|
188
|
-
continue
|
|
189
|
-
|
|
190
|
-
opener_pos = _find_opener(pending, openers)
|
|
191
|
-
if opener_pos is None:
|
|
192
|
-
safe, pending = _split_at_potential_prefix(pending, openers)
|
|
193
|
-
if safe:
|
|
194
|
-
yield CompletionChunk(content=safe)
|
|
195
|
-
break
|
|
196
|
-
|
|
197
|
-
before = pending[:opener_pos]
|
|
198
|
-
if before:
|
|
199
|
-
yield CompletionChunk(content=before)
|
|
200
|
-
if pending.startswith(THINKING_OPENER, opener_pos):
|
|
201
|
-
pending = pending[opener_pos + len(THINKING_OPENER) :]
|
|
202
|
-
in_thinking = True
|
|
203
|
-
else:
|
|
204
|
-
tool_call_text = pending[opener_pos:]
|
|
205
|
-
in_tool_call = True
|
|
206
|
-
pending = ""
|
|
207
|
-
|
|
208
|
-
if pending and in_thinking:
|
|
209
|
-
yield CompletionChunk(thinking=pending)
|
|
210
|
-
elif pending:
|
|
211
|
-
yield CompletionChunk(content=pending)
|
|
212
|
-
|
|
213
|
-
if tool_call_text:
|
|
214
|
-
clean_text, tool_calls = parse_tool_calls(tool_call_text, tool_map)
|
|
215
|
-
if clean_text:
|
|
216
|
-
yield CompletionChunk(content=clean_text)
|
|
217
|
-
if tool_calls:
|
|
218
|
-
yield CompletionChunk(tool_calls=tool_calls)
|
|
219
|
-
else:
|
|
220
|
-
yield CompletionChunk(finish_reason="stop")
|
|
221
|
-
else:
|
|
222
|
-
yield CompletionChunk(finish_reason="stop")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.7}/cortexgrid_infer/serve_apps/gemini/__init__.py
RENAMED
|
File without changes
|
{cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.7}/cortexgrid_infer/serve_apps/gemini/base.py
RENAMED
|
File without changes
|
{cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.7}/cortexgrid_infer/serve_apps/gemini/text2image.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.7}/cortexgrid_infer/serve_apps/text_rewriter.py
RENAMED
|
File without changes
|
|
File without changes
|