cortexgrid-infer 0.3.3__tar.gz → 0.3.4__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.
Files changed (31) hide show
  1. {cortexgrid_infer-0.3.3 → cortexgrid_infer-0.3.4}/PKG-INFO +2 -2
  2. {cortexgrid_infer-0.3.3 → cortexgrid_infer-0.3.4}/README.md +1 -1
  3. {cortexgrid_infer-0.3.3 → cortexgrid_infer-0.3.4}/cortexgrid_infer/core.py +1 -0
  4. {cortexgrid_infer-0.3.3 → cortexgrid_infer-0.3.4}/cortexgrid_infer/importers/base.py +3 -0
  5. {cortexgrid_infer-0.3.3 → cortexgrid_infer-0.3.4}/cortexgrid_infer/protocols/completion.py +50 -20
  6. {cortexgrid_infer-0.3.3 → cortexgrid_infer-0.3.4}/cortexgrid_infer/serve_apps/base.py +6 -0
  7. {cortexgrid_infer-0.3.3 → cortexgrid_infer-0.3.4}/cortexgrid_infer/serve_apps/gemini/base.py +1 -0
  8. {cortexgrid_infer-0.3.3 → cortexgrid_infer-0.3.4}/cortexgrid_infer/serve_apps/gemini/text2text.py +28 -3
  9. {cortexgrid_infer-0.3.3 → cortexgrid_infer-0.3.4}/cortexgrid_infer/serve_apps/text2text.py +7 -1
  10. {cortexgrid_infer-0.3.3 → cortexgrid_infer-0.3.4}/pyproject.toml +1 -1
  11. {cortexgrid_infer-0.3.3 → cortexgrid_infer-0.3.4}/.gitignore +0 -0
  12. {cortexgrid_infer-0.3.3 → cortexgrid_infer-0.3.4}/LICENSE +0 -0
  13. {cortexgrid_infer-0.3.3 → cortexgrid_infer-0.3.4}/cortexgrid_infer/__init__.py +0 -0
  14. {cortexgrid_infer-0.3.3 → cortexgrid_infer-0.3.4}/cortexgrid_infer/compiling.py +0 -0
  15. {cortexgrid_infer-0.3.3 → cortexgrid_infer-0.3.4}/cortexgrid_infer/device.py +0 -0
  16. {cortexgrid_infer-0.3.3 → cortexgrid_infer-0.3.4}/cortexgrid_infer/importers/__init__.py +0 -0
  17. {cortexgrid_infer-0.3.3 → cortexgrid_infer-0.3.4}/cortexgrid_infer/importers/huggingface.py +0 -0
  18. {cortexgrid_infer-0.3.3 → cortexgrid_infer-0.3.4}/cortexgrid_infer/protocols/__init__.py +0 -0
  19. {cortexgrid_infer-0.3.3 → cortexgrid_infer-0.3.4}/cortexgrid_infer/protocols/imaging.py +0 -0
  20. {cortexgrid_infer-0.3.3 → cortexgrid_infer-0.3.4}/cortexgrid_infer/protocols/meshing.py +0 -0
  21. {cortexgrid_infer-0.3.3 → cortexgrid_infer-0.3.4}/cortexgrid_infer/protocols/rewriting.py +0 -0
  22. {cortexgrid_infer-0.3.3 → cortexgrid_infer-0.3.4}/cortexgrid_infer/py.typed +0 -0
  23. {cortexgrid_infer-0.3.3 → cortexgrid_infer-0.3.4}/cortexgrid_infer/registry.py +0 -0
  24. {cortexgrid_infer-0.3.3 → cortexgrid_infer-0.3.4}/cortexgrid_infer/serve_apps/__init__.py +0 -0
  25. {cortexgrid_infer-0.3.3 → cortexgrid_infer-0.3.4}/cortexgrid_infer/serve_apps/anthropic.py +0 -0
  26. {cortexgrid_infer-0.3.3 → cortexgrid_infer-0.3.4}/cortexgrid_infer/serve_apps/gemini/__init__.py +0 -0
  27. {cortexgrid_infer-0.3.3 → cortexgrid_infer-0.3.4}/cortexgrid_infer/serve_apps/gemini/text2image.py +0 -0
  28. {cortexgrid_infer-0.3.3 → cortexgrid_infer-0.3.4}/cortexgrid_infer/serve_apps/image2mesh.py +0 -0
  29. {cortexgrid_infer-0.3.3 → cortexgrid_infer-0.3.4}/cortexgrid_infer/serve_apps/text2image.py +0 -0
  30. {cortexgrid_infer-0.3.3 → cortexgrid_infer-0.3.4}/cortexgrid_infer/serve_apps/text_rewriter.py +0 -0
  31. {cortexgrid_infer-0.3.3 → cortexgrid_infer-0.3.4}/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
3
+ Version: 0.3.4
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
@@ -245,7 +245,7 @@ it is the floor; only quantizing the weights or running fewer forwards moves it.
245
245
 
246
246
  ## Inference
247
247
 
248
- `complete` streams `CompletionChunk`s (`.content`, `.tool_calls`, `.finish_reason`):
248
+ `complete` streams `CompletionChunk`s (`.content`, `.thinking`, `.tool_calls`, `.finish_reason`):
249
249
 
250
250
  ```python
251
251
  imp = mg.HuggingFaceImporter("Qwen/Qwen2.5-7B-Instruct", mg.Text2Text)
@@ -217,7 +217,7 @@ it is the floor; only quantizing the weights or running fewer forwards moves it.
217
217
 
218
218
  ## Inference
219
219
 
220
- `complete` streams `CompletionChunk`s (`.content`, `.tool_calls`, `.finish_reason`):
220
+ `complete` streams `CompletionChunk`s (`.content`, `.thinking`, `.tool_calls`, `.finish_reason`):
221
221
 
222
222
  ```python
223
223
  imp = mg.HuggingFaceImporter("Qwen/Qwen2.5-7B-Instruct", mg.Text2Text)
@@ -25,6 +25,7 @@ class ToolCall:
25
25
  @dataclass
26
26
  class CompletionChunk:
27
27
  content: str = ""
28
+ thinking: str = ""
28
29
  tool_calls: list[ToolCall] = field(default_factory=list)
29
30
  finish_reason: str | None = None
30
31
 
@@ -95,5 +95,8 @@ class Importer(ModelEntry):
95
95
  have, so a hand-set value is never overwritten."""
96
96
  return self.serve_app.requirements(self.weights())
97
97
 
98
+ def config(self) -> dict[str, str]:
99
+ return self.serve_app.config()
100
+
98
101
  def client(self, url: str) -> DeployedModel:
99
102
  return self.serve_app.client(url, self.model_id)
@@ -35,6 +35,9 @@ _tool_call_id_counter = itertools.count()
35
35
 
36
36
  TOOL_CALL_OPENERS = ["<tool_call>", "<|tool_call|>", "```tool_call"]
37
37
 
38
+ THINKING_OPENER = "<think>"
39
+ THINKING_CLOSER = "</think>"
40
+
38
41
  TOOL_CALL_PATTERNS = [
39
42
  re.compile(r"<tool_call>\s*(\{.*?\})\s*</tool_call>", re.DOTALL),
40
43
  re.compile(r"<\|tool_call\|>\s*(\{.*?\})\s*<\|/tool_call\|>", re.DOTALL),
@@ -51,23 +54,27 @@ def encode_tool_call(name: str, arguments: dict[str, Any]) -> str:
51
54
  return f'<tool_call>{json.dumps({"name": name, "arguments": arguments})}</tool_call>'
52
55
 
53
56
 
54
- def _find_opener(text: str) -> int | None:
55
- """Find the earliest tool call opener position in text, or None."""
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."""
56
63
  earliest = None
57
- for opener in TOOL_CALL_OPENERS:
64
+ for opener in openers:
58
65
  idx = text.find(opener)
59
66
  if idx != -1 and (earliest is None or idx < earliest):
60
67
  earliest = idx
61
68
  return earliest
62
69
 
63
70
 
64
- def _split_at_potential_prefix(text: str) -> tuple[str, str]:
71
+ def _split_at_potential_prefix(text: str, openers: Sequence[str]) -> tuple[str, str]:
65
72
  """Split text into (safe_to_stream, potential_opener_prefix).
66
73
 
67
- The second part is a suffix that could be the beginning of a tool call
68
- opening tag, so it must be held back until more text arrives.
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.
69
76
  """
70
- for opener in TOOL_CALL_OPENERS:
77
+ for opener in openers:
71
78
  for length in range(1, len(opener)):
72
79
  if text.endswith(opener[:length]):
73
80
  return text[:-length], text[-length:]
@@ -144,8 +151,10 @@ class ServedCompletingModel(CompletingModel):
144
151
  }
145
152
 
146
153
  pending = ""
154
+ in_thinking = False
147
155
  in_tool_call = False
148
156
  tool_call_text = ""
157
+ openers = [THINKING_OPENER, *TOOL_CALL_OPENERS]
149
158
 
150
159
  async with httpx.AsyncClient(timeout=None) as client:
151
160
  async with client.stream(
@@ -162,22 +171,43 @@ class ServedCompletingModel(CompletingModel):
162
171
 
163
172
  pending += text
164
173
 
165
- opener_pos = _find_opener(pending)
166
- if opener_pos is not None:
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
+
167
197
  before = pending[:opener_pos]
168
198
  if before:
169
199
  yield CompletionChunk(content=before)
170
- tool_call_text = pending[opener_pos:]
171
- in_tool_call = True
172
- pending = ""
173
- continue
174
-
175
- safe, held = _split_at_potential_prefix(pending)
176
- if safe:
177
- yield CompletionChunk(content=safe)
178
- pending = held
179
-
180
- if pending:
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:
181
211
  yield CompletionChunk(content=pending)
182
212
 
183
213
  if tool_call_text:
@@ -28,6 +28,8 @@ from cortexgrid_infer.core import DeployedModel
28
28
 
29
29
  _GIB = float(1 << 30)
30
30
 
31
+ ENABLE_THINKING_PARAM = "enable_thinking"
32
+
31
33
  # Weights are only part of what a replica holds. The multiplier covers CUDA
32
34
  # context, activations and fragmentation; the floor covers the KV cache of a
33
35
  # long context, which does not scale with model size the way activations do.
@@ -94,6 +96,10 @@ class LocalModel:
94
96
  vram_gb=max(round(size * _VRAM_HEADROOM + _VRAM_FLOOR_GB, 1), cls.min_vram_gb),
95
97
  )
96
98
 
99
+ @classmethod
100
+ def config(cls) -> dict[str, str]:
101
+ return {}
102
+
97
103
  @classmethod
98
104
  def client(cls, url: str, name: str) -> DeployedModel:
99
105
  """A client for this app deployed at `url`, serving the model `name`."""
@@ -44,6 +44,7 @@ class GeminiModel(HostedModel):
44
44
  f"{family}/{suffix}/{run_name} is missing {sorted(missing)} from "
45
45
  "its config; set them on the model card"
46
46
  )
47
+ self._config = config
47
48
  self._model = config[MODEL_PARAM]
48
49
  self._client = genai.Client(
49
50
  api_key=cortexgrid.get_secret(config[API_KEY_SECRET_PARAM])
@@ -16,8 +16,13 @@ from fastapi import FastAPI
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 ServedCompletingModel, encode_tool_call
20
- from cortexgrid_infer.serve_apps.gemini.base import GeminiModel
19
+ from cortexgrid_infer.protocols.completion import (
20
+ ServedCompletingModel,
21
+ encode_thinking,
22
+ encode_tool_call,
23
+ )
24
+ from cortexgrid_infer.serve_apps.base import ENABLE_THINKING_PARAM
25
+ from cortexgrid_infer.serve_apps.gemini.base import DEFAULT_API_KEY_SECRET, GeminiModel
21
26
 
22
27
  _app = FastAPI()
23
28
 
@@ -109,6 +114,18 @@ def to_gemini_tools(
109
114
 
110
115
  @serve.ingress(_app)
111
116
  class GeminiText2Text(GeminiModel):
117
+ @classmethod
118
+ def config(
119
+ cls,
120
+ model_id: str,
121
+ api_key_secret: str = DEFAULT_API_KEY_SECRET,
122
+ enable_thinking: str = "true",
123
+ ) -> dict[str, str]:
124
+ return {
125
+ **super().config(model_id, api_key_secret),
126
+ ENABLE_THINKING_PARAM: enable_thinking,
127
+ }
128
+
112
129
  @classmethod
113
130
  def client(cls, url: str, name: str) -> ServedCompletingModel:
114
131
  return ServedCompletingModel(url=url, model_id=name)
@@ -121,6 +138,11 @@ class GeminiText2Text(GeminiModel):
121
138
  generate_config: dict[str, Any] = {
122
139
  "max_output_tokens": body.get("max_new_tokens", 16 * 1024),
123
140
  "temperature": body.get("temperature", 0.7),
141
+ "thinking_config": (
142
+ {"include_thoughts": True}
143
+ if self._config.get(ENABLE_THINKING_PARAM, "true") == "true"
144
+ else {"thinking_budget": 0}
145
+ ),
124
146
  }
125
147
  if system_prompt:
126
148
  generate_config["system_instruction"] = system_prompt
@@ -140,7 +162,10 @@ class GeminiText2Text(GeminiModel):
140
162
  part.function_call.name, dict(part.function_call.args or {})
141
163
  ).encode("utf-8")
142
164
 
143
- elif part.text and not part.thought:
165
+ elif part.text and part.thought:
166
+ yield encode_thinking(part.text).encode("utf-8")
167
+
168
+ elif part.text:
144
169
  yield part.text.encode("utf-8")
145
170
 
146
171
  return StreamingResponse(stream(), media_type="text/plain")
@@ -30,7 +30,7 @@ import torch
30
30
  from cortexgrid_infer import compiling
31
31
  from cortexgrid_infer.device import detect_device
32
32
  from cortexgrid_infer.protocols.completion import ServedCompletingModel
33
- from cortexgrid_infer.serve_apps.base import LocalModel
33
+ from cortexgrid_infer.serve_apps.base import ENABLE_THINKING_PARAM, LocalModel
34
34
 
35
35
 
36
36
  log = logging.getLogger(__name__)
@@ -60,6 +60,10 @@ _RESERVED_BODY_KEYS = {
60
60
 
61
61
  @serve.ingress(_app)
62
62
  class Text2Text(LocalModel):
63
+ @classmethod
64
+ def config(cls) -> dict[str, str]:
65
+ return {ENABLE_THINKING_PARAM: "true"}
66
+
63
67
  @classmethod
64
68
  def client(cls, url: str, name: str) -> ServedCompletingModel:
65
69
  return ServedCompletingModel(url=url, model_id=name)
@@ -81,6 +85,7 @@ class Text2Text(LocalModel):
81
85
  self._max_total_tokens = min(
82
86
  int(settings.get(MAX_TOTAL_TOKENS_PARAM, DEFAULT_MAX_TOTAL_TOKENS)), context
83
87
  )
88
+ self._enable_thinking = settings.get(ENABLE_THINKING_PARAM, "true") == "true"
84
89
  # A static cache of fixed length is what makes decode compilable: it
85
90
  # pins the shape every forward sees, so the loop is captured once
86
91
  # instead of recompiled per token. transformers then compiles `generate`
@@ -133,6 +138,7 @@ class Text2Text(LocalModel):
133
138
  tools=tools,
134
139
  tokenize=False,
135
140
  add_generation_prompt=True,
141
+ enable_thinking=self._enable_thinking,
136
142
  )
137
143
  inputs = self._truncate(
138
144
  self._tokenizer(prompt, return_tensors="pt"),
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cortexgrid-infer"
3
- version = "0.3.3"
3
+ version = "0.3.4"
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"