@caupulican/pi-adaptative 0.81.15 → 0.81.17
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.
- package/CHANGELOG.md +23 -0
- package/dist/bundled-resources/runtimes/hf-transformers-openai-server.py +427 -0
- package/dist/bundled-resources/skills/tool-call-repair/references/repair-catalogue.md +18 -0
- package/dist/bundled-resources/skills/tool-call-repair/references/text-protocol-grammar.md +14 -7
- package/dist/core/agent-session.d.ts +15 -3
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +201 -25
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/local-runtime-controller.d.ts +17 -9
- package/dist/core/local-runtime-controller.d.ts.map +1 -1
- package/dist/core/local-runtime-controller.js +124 -20
- package/dist/core/local-runtime-controller.js.map +1 -1
- package/dist/core/models/adaptation-store.d.ts +5 -0
- package/dist/core/models/adaptation-store.d.ts.map +1 -1
- package/dist/core/models/adaptation-store.js +16 -0
- package/dist/core/models/adaptation-store.js.map +1 -1
- package/dist/core/models/context-sizing.d.ts +30 -0
- package/dist/core/models/context-sizing.d.ts.map +1 -0
- package/dist/core/models/context-sizing.js +90 -0
- package/dist/core/models/context-sizing.js.map +1 -0
- package/dist/core/models/default-model-suggestions.d.ts +4 -4
- package/dist/core/models/default-model-suggestions.d.ts.map +1 -1
- package/dist/core/models/default-model-suggestions.js +9 -0
- package/dist/core/models/default-model-suggestions.js.map +1 -1
- package/dist/core/models/local-registration.d.ts +12 -0
- package/dist/core/models/local-registration.d.ts.map +1 -1
- package/dist/core/models/local-registration.js +68 -0
- package/dist/core/models/local-registration.js.map +1 -1
- package/dist/core/models/local-runtime.d.ts +96 -1
- package/dist/core/models/local-runtime.d.ts.map +1 -1
- package/dist/core/models/local-runtime.js +338 -4
- package/dist/core/models/local-runtime.js.map +1 -1
- package/dist/core/models/model-ref.d.ts +4 -0
- package/dist/core/models/model-ref.d.ts.map +1 -1
- package/dist/core/models/model-ref.js +12 -3
- package/dist/core/models/model-ref.js.map +1 -1
- package/dist/core/models/perf-profile.d.ts +36 -0
- package/dist/core/models/perf-profile.d.ts.map +1 -0
- package/dist/core/models/perf-profile.js +163 -0
- package/dist/core/models/perf-profile.js.map +1 -0
- package/dist/core/models/runtime-arbiter.d.ts +57 -0
- package/dist/core/models/runtime-arbiter.d.ts.map +1 -0
- package/dist/core/models/runtime-arbiter.js +49 -0
- package/dist/core/models/runtime-arbiter.js.map +1 -0
- package/dist/core/tool-repair-health.d.ts.map +1 -1
- package/dist/core/tool-repair-health.js +2 -1
- package/dist/core/tool-repair-health.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +1 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +4 -0
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/local-model-commands.d.ts +3 -1
- package/dist/modes/interactive/local-model-commands.d.ts.map +1 -1
- package/dist/modes/interactive/local-model-commands.js +163 -22
- package/dist/modes/interactive/local-model-commands.js.map +1 -1
- package/docs/models.md +19 -1
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +12 -12
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,26 @@
|
|
|
1
|
+
## [0.81.17] - 2026-07-08
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
- Added host-local model perf profiles that record streamed prefill/decode rates and adapt quiet stall bounds from measured throughput.
|
|
5
|
+
- Added local OpenAI-compatible prefix warming and longer managed Ollama residency defaults for local-model startup.
|
|
6
|
+
- Added local-model context sizing that derives `num_ctx` from host RAM and model metadata, then creates a sized Ollama Modelfile alias for managed installs.
|
|
7
|
+
- Added a runtime residency arbiter core for fitting, evicting, refusing, and reserving local model loads across runtime adapters.
|
|
8
|
+
- Added local acceptance/bench perf-profile hydration so live protocol checks and opt-in local benches feed adaptive stall bounds.
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- Documented every tool-repair registry mode in the bundled repair catalogue and covered the registry/docs link with tests.
|
|
12
|
+
|
|
13
|
+
## [0.81.16] - 2026-07-08
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
- Added graded `/toolprobe` native capability reporting and persistence so native verdicts require both an echo probe and a task-scale read probe, while echo-only or absent native support can calibrate the text protocol.
|
|
17
|
+
- Added MiniCPM5-1B to the live acceptance fleet as a full-base native tool-calling target through the pi-managed Transformers sidecar.
|
|
18
|
+
- Added a pi-managed Hugging Face Transformers runtime path for curated full-base local model suggestions, starting with `openbmb/MiniCPM5-1B`, using an isolated venv and pi-owned HF cache.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- Fixed tool probing so a real task-scale native tool call wins without requiring a separate echo-only probe, and the MiniCPM sidecar stops generation after a complete native function call.
|
|
22
|
+
- Documented the Ollama serving-context requirement for local OpenAI-compatible models.
|
|
23
|
+
|
|
1
24
|
## [0.81.15] - 2026-07-07
|
|
2
25
|
|
|
3
26
|
## [0.81.14] - 2026-07-07
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Tiny OpenAI-compatible chat sidecar for pi-managed Hugging Face Transformers models.
|
|
3
|
+
|
|
4
|
+
Intentionally stdlib-only at the server layer: the managed venv supplies torch,
|
|
5
|
+
transformers, and huggingface_hub, while HTTP serving stays dependency-free so pi
|
|
6
|
+
does not need FastAPI/Uvicorn just to run a suggested local model.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import html
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import re
|
|
16
|
+
import sys
|
|
17
|
+
import time
|
|
18
|
+
import traceback
|
|
19
|
+
import uuid
|
|
20
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
_MODEL_ID = ""
|
|
24
|
+
_TOKENIZER: Any = None
|
|
25
|
+
_MODEL: Any = None
|
|
26
|
+
_TORCH: Any = None
|
|
27
|
+
_DEVICE = "cpu"
|
|
28
|
+
_FUNCTION_CALL_RE = re.compile(r"<function\b[^>]*>.*?</function>", re.DOTALL)
|
|
29
|
+
_FUNCTION_NAME_RE = re.compile(r"<function\b[^>]*\bname=(['\"])(.*?)\1", re.DOTALL)
|
|
30
|
+
_PARAM_RE = re.compile(r"<param\b[^>]*\bname=(['\"])(.*?)\1[^>]*>(.*?)</param>", re.DOTALL)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _json_response(handler: BaseHTTPRequestHandler, status: int, body: dict[str, Any]) -> None:
|
|
34
|
+
payload = json.dumps(body).encode("utf-8")
|
|
35
|
+
handler.send_response(status)
|
|
36
|
+
handler.send_header("content-type", "application/json")
|
|
37
|
+
handler.send_header("content-length", str(len(payload)))
|
|
38
|
+
handler.end_headers()
|
|
39
|
+
handler.wfile.write(payload)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _message_text(content: Any) -> str:
|
|
43
|
+
if isinstance(content, str):
|
|
44
|
+
return content
|
|
45
|
+
if isinstance(content, list):
|
|
46
|
+
parts: list[str] = []
|
|
47
|
+
for item in content:
|
|
48
|
+
if isinstance(item, dict):
|
|
49
|
+
text = item.get("text")
|
|
50
|
+
if isinstance(text, str):
|
|
51
|
+
parts.append(text)
|
|
52
|
+
return "\n".join(parts)
|
|
53
|
+
return "" if content is None else str(content)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _decode_arguments(arguments: Any) -> Any:
|
|
57
|
+
if isinstance(arguments, str):
|
|
58
|
+
try:
|
|
59
|
+
return json.loads(arguments)
|
|
60
|
+
except json.JSONDecodeError as error:
|
|
61
|
+
raise ValueError(f"tool_call function.arguments must be valid JSON: {error.msg}") from error
|
|
62
|
+
return arguments
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _normalize_tool_calls(tool_calls: Any) -> list[dict[str, Any]]:
|
|
66
|
+
normalized: list[dict[str, Any]] = []
|
|
67
|
+
if not isinstance(tool_calls, list):
|
|
68
|
+
return normalized
|
|
69
|
+
for tool_call in tool_calls:
|
|
70
|
+
if not isinstance(tool_call, dict):
|
|
71
|
+
continue
|
|
72
|
+
function = tool_call.get("function")
|
|
73
|
+
if not isinstance(function, dict):
|
|
74
|
+
continue
|
|
75
|
+
name = function.get("name")
|
|
76
|
+
if not isinstance(name, str) or not name:
|
|
77
|
+
continue
|
|
78
|
+
normalized_call: dict[str, Any] = {
|
|
79
|
+
"type": "function",
|
|
80
|
+
"function": {"name": name, "arguments": _decode_arguments(function.get("arguments"))},
|
|
81
|
+
}
|
|
82
|
+
tool_call_id = tool_call.get("id")
|
|
83
|
+
if isinstance(tool_call_id, str) and tool_call_id:
|
|
84
|
+
normalized_call["id"] = tool_call_id
|
|
85
|
+
normalized.append(normalized_call)
|
|
86
|
+
return normalized
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _normalize_messages(messages: Any) -> list[dict[str, Any]]:
|
|
90
|
+
normalized: list[dict[str, Any]] = []
|
|
91
|
+
if not isinstance(messages, list):
|
|
92
|
+
return normalized
|
|
93
|
+
for message in messages:
|
|
94
|
+
if not isinstance(message, dict):
|
|
95
|
+
continue
|
|
96
|
+
role = message.get("role")
|
|
97
|
+
if role == "developer":
|
|
98
|
+
role = "system"
|
|
99
|
+
if role not in {"system", "user", "assistant", "tool"}:
|
|
100
|
+
role = "user"
|
|
101
|
+
normalized_message: dict[str, Any] = {"role": str(role), "content": _message_text(message.get("content"))}
|
|
102
|
+
if role == "assistant":
|
|
103
|
+
tool_calls = _normalize_tool_calls(message.get("tool_calls"))
|
|
104
|
+
if tool_calls:
|
|
105
|
+
normalized_message["tool_calls"] = tool_calls
|
|
106
|
+
if role == "tool":
|
|
107
|
+
tool_call_id = message.get("tool_call_id")
|
|
108
|
+
if isinstance(tool_call_id, str) and tool_call_id:
|
|
109
|
+
normalized_message["tool_call_id"] = tool_call_id
|
|
110
|
+
normalized.append(normalized_message)
|
|
111
|
+
return normalized
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _render_prompt(messages: list[dict[str, Any]], tools: Any) -> str:
|
|
115
|
+
tokenizer = _TOKENIZER
|
|
116
|
+
kwargs: dict[str, Any] = {
|
|
117
|
+
"tokenize": False,
|
|
118
|
+
"add_generation_prompt": True,
|
|
119
|
+
}
|
|
120
|
+
if isinstance(tools, list) and tools:
|
|
121
|
+
kwargs["tools"] = tools
|
|
122
|
+
return tokenizer.apply_chat_template(messages, **kwargs)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _generation_options(request: dict[str, Any]) -> dict[str, Any]:
|
|
126
|
+
max_tokens = request.get("max_completion_tokens", request.get("max_tokens", 512))
|
|
127
|
+
try:
|
|
128
|
+
max_new_tokens = max(1, min(int(max_tokens), 2048))
|
|
129
|
+
except Exception:
|
|
130
|
+
max_new_tokens = 512
|
|
131
|
+
|
|
132
|
+
temperature_value = request.get("temperature", 0.0)
|
|
133
|
+
try:
|
|
134
|
+
temperature = float(temperature_value)
|
|
135
|
+
except Exception:
|
|
136
|
+
temperature = 0.0
|
|
137
|
+
do_sample = temperature > 0.0
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
"max_new_tokens": max_new_tokens,
|
|
141
|
+
"do_sample": do_sample,
|
|
142
|
+
**({"temperature": temperature} if do_sample else {}),
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _clean_generated_text(text: str) -> str:
|
|
147
|
+
tokenizer = _TOKENIZER
|
|
148
|
+
eos = getattr(tokenizer, "eos_token", None)
|
|
149
|
+
if eos:
|
|
150
|
+
text = text.replace(eos, "")
|
|
151
|
+
pad = getattr(tokenizer, "pad_token", None)
|
|
152
|
+
if pad and pad != eos:
|
|
153
|
+
text = text.replace(pad, "")
|
|
154
|
+
return text.strip()
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _decode_param_value(raw: str) -> Any:
|
|
158
|
+
stripped = raw.strip()
|
|
159
|
+
if stripped.startswith("{") or stripped.startswith("["):
|
|
160
|
+
try:
|
|
161
|
+
return json.loads(stripped)
|
|
162
|
+
except Exception:
|
|
163
|
+
return stripped
|
|
164
|
+
return stripped
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _extract_native_function_calls(text: str) -> tuple[str, list[dict[str, Any]]]:
|
|
168
|
+
calls: list[dict[str, Any]] = []
|
|
169
|
+
spans: list[tuple[int, int]] = []
|
|
170
|
+
for match in _FUNCTION_CALL_RE.finditer(text):
|
|
171
|
+
block = match.group(0)
|
|
172
|
+
name_match = _FUNCTION_NAME_RE.search(block)
|
|
173
|
+
if not name_match:
|
|
174
|
+
continue
|
|
175
|
+
name = html.unescape(name_match.group(2).strip())
|
|
176
|
+
if not name:
|
|
177
|
+
continue
|
|
178
|
+
arguments: dict[str, Any] = {}
|
|
179
|
+
for param_match in _PARAM_RE.finditer(block):
|
|
180
|
+
param_name = html.unescape(param_match.group(2).strip())
|
|
181
|
+
if not param_name:
|
|
182
|
+
continue
|
|
183
|
+
arguments[param_name] = _decode_param_value(html.unescape(param_match.group(3)))
|
|
184
|
+
calls.append(
|
|
185
|
+
{
|
|
186
|
+
"id": f"call_{uuid.uuid4().hex}",
|
|
187
|
+
"type": "function",
|
|
188
|
+
"function": {"name": name, "arguments": json.dumps(arguments, ensure_ascii=False)},
|
|
189
|
+
}
|
|
190
|
+
)
|
|
191
|
+
spans.append(match.span())
|
|
192
|
+
if not calls:
|
|
193
|
+
return text, []
|
|
194
|
+
remaining_parts: list[str] = []
|
|
195
|
+
cursor = 0
|
|
196
|
+
for start, end in spans:
|
|
197
|
+
remaining_parts.append(text[cursor:start])
|
|
198
|
+
cursor = end
|
|
199
|
+
remaining_parts.append(text[cursor:])
|
|
200
|
+
return "".join(remaining_parts).strip(), calls
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _function_call_stopping_criteria(prompt_token_count: int) -> Any:
|
|
204
|
+
from transformers import StoppingCriteria, StoppingCriteriaList
|
|
205
|
+
|
|
206
|
+
class StopAfterFunctionCall(StoppingCriteria):
|
|
207
|
+
def __call__(self, input_ids: Any, _scores: Any, **_kwargs: Any) -> bool:
|
|
208
|
+
generated_ids = input_ids[0][prompt_token_count:]
|
|
209
|
+
if int(generated_ids.shape[-1]) == 0:
|
|
210
|
+
return False
|
|
211
|
+
text = _TOKENIZER.decode(generated_ids, skip_special_tokens=False)
|
|
212
|
+
return bool(_FUNCTION_CALL_RE.search(text))
|
|
213
|
+
|
|
214
|
+
return StoppingCriteriaList([StopAfterFunctionCall()])
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _generate(request: dict[str, Any]) -> str:
|
|
218
|
+
tokenizer = _TOKENIZER
|
|
219
|
+
model = _MODEL
|
|
220
|
+
torch = _TORCH
|
|
221
|
+
messages = _normalize_messages(request.get("messages"))
|
|
222
|
+
prompt = _render_prompt(messages, request.get("tools"))
|
|
223
|
+
encoded = tokenizer(prompt, return_tensors="pt")
|
|
224
|
+
encoded = {key: value.to(_DEVICE) for key, value in encoded.items()}
|
|
225
|
+
input_length = int(encoded["input_ids"].shape[-1])
|
|
226
|
+
options = _generation_options(request)
|
|
227
|
+
eos_token_id = getattr(tokenizer, "eos_token_id", None)
|
|
228
|
+
pad_token_id = getattr(tokenizer, "pad_token_id", None) or eos_token_id
|
|
229
|
+
generation_args: dict[str, Any] = {
|
|
230
|
+
**encoded,
|
|
231
|
+
"eos_token_id": eos_token_id,
|
|
232
|
+
"pad_token_id": pad_token_id,
|
|
233
|
+
**options,
|
|
234
|
+
}
|
|
235
|
+
if isinstance(request.get("tools"), list) and request.get("tools"):
|
|
236
|
+
generation_args["stopping_criteria"] = _function_call_stopping_criteria(input_length)
|
|
237
|
+
with torch.inference_mode():
|
|
238
|
+
output = model.generate(**generation_args)
|
|
239
|
+
generated = output[0][input_length:]
|
|
240
|
+
return _clean_generated_text(tokenizer.decode(generated, skip_special_tokens=False))
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _stream_chunk(handler: BaseHTTPRequestHandler, chunk: dict[str, Any]) -> None:
|
|
244
|
+
handler.wfile.write(f"data: {json.dumps(chunk)}\n\n".encode("utf-8"))
|
|
245
|
+
handler.wfile.flush()
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class Handler(BaseHTTPRequestHandler):
|
|
249
|
+
server_version = "pi-hf-transformers/1"
|
|
250
|
+
|
|
251
|
+
def log_message(self, fmt: str, *args: Any) -> None:
|
|
252
|
+
sys.stderr.write("[%s] %s\n" % (self.log_date_time_string(), fmt % args))
|
|
253
|
+
|
|
254
|
+
def do_GET(self) -> None: # noqa: N802 - stdlib handler API
|
|
255
|
+
if self.path == "/health":
|
|
256
|
+
_json_response(self, 200, {"ok": True, "model": _MODEL_ID})
|
|
257
|
+
return
|
|
258
|
+
if self.path == "/v1/models":
|
|
259
|
+
_json_response(
|
|
260
|
+
self,
|
|
261
|
+
200,
|
|
262
|
+
{"object": "list", "data": [{"id": _MODEL_ID, "object": "model", "owned_by": "pi"}]},
|
|
263
|
+
)
|
|
264
|
+
return
|
|
265
|
+
_json_response(self, 404, {"error": "not found"})
|
|
266
|
+
|
|
267
|
+
def do_POST(self) -> None: # noqa: N802 - stdlib handler API
|
|
268
|
+
if self.path != "/v1/chat/completions":
|
|
269
|
+
_json_response(self, 404, {"error": "not found"})
|
|
270
|
+
return
|
|
271
|
+
try:
|
|
272
|
+
length = int(self.headers.get("content-length", "0"))
|
|
273
|
+
request = json.loads(self.rfile.read(length).decode("utf-8"))
|
|
274
|
+
text = _generate(request)
|
|
275
|
+
content, tool_calls = _extract_native_function_calls(text)
|
|
276
|
+
if request.get("stream", False):
|
|
277
|
+
self.send_response(200)
|
|
278
|
+
self.send_header("content-type", "text/event-stream")
|
|
279
|
+
self.send_header("cache-control", "no-cache")
|
|
280
|
+
self.end_headers()
|
|
281
|
+
chunk_id = f"chatcmpl-pi-{uuid.uuid4().hex}"
|
|
282
|
+
created = int(time.time())
|
|
283
|
+
_stream_chunk(
|
|
284
|
+
self,
|
|
285
|
+
{
|
|
286
|
+
"id": chunk_id,
|
|
287
|
+
"object": "chat.completion.chunk",
|
|
288
|
+
"created": created,
|
|
289
|
+
"model": _MODEL_ID,
|
|
290
|
+
"choices": [{"index": 0, "delta": {"role": "assistant"}, "finish_reason": None}],
|
|
291
|
+
},
|
|
292
|
+
)
|
|
293
|
+
if content:
|
|
294
|
+
_stream_chunk(
|
|
295
|
+
self,
|
|
296
|
+
{
|
|
297
|
+
"id": chunk_id,
|
|
298
|
+
"object": "chat.completion.chunk",
|
|
299
|
+
"created": created,
|
|
300
|
+
"model": _MODEL_ID,
|
|
301
|
+
"choices": [{"index": 0, "delta": {"content": content}, "finish_reason": None}],
|
|
302
|
+
},
|
|
303
|
+
)
|
|
304
|
+
for index, tool_call in enumerate(tool_calls):
|
|
305
|
+
_stream_chunk(
|
|
306
|
+
self,
|
|
307
|
+
{
|
|
308
|
+
"id": chunk_id,
|
|
309
|
+
"object": "chat.completion.chunk",
|
|
310
|
+
"created": created,
|
|
311
|
+
"model": _MODEL_ID,
|
|
312
|
+
"choices": [
|
|
313
|
+
{
|
|
314
|
+
"index": 0,
|
|
315
|
+
"delta": {
|
|
316
|
+
"tool_calls": [
|
|
317
|
+
{
|
|
318
|
+
"index": index,
|
|
319
|
+
"id": tool_call["id"],
|
|
320
|
+
"type": "function",
|
|
321
|
+
"function": tool_call["function"],
|
|
322
|
+
}
|
|
323
|
+
]
|
|
324
|
+
},
|
|
325
|
+
"finish_reason": None,
|
|
326
|
+
}
|
|
327
|
+
],
|
|
328
|
+
},
|
|
329
|
+
)
|
|
330
|
+
_stream_chunk(
|
|
331
|
+
self,
|
|
332
|
+
{
|
|
333
|
+
"id": chunk_id,
|
|
334
|
+
"object": "chat.completion.chunk",
|
|
335
|
+
"created": created,
|
|
336
|
+
"model": _MODEL_ID,
|
|
337
|
+
"choices": [
|
|
338
|
+
{"index": 0, "delta": {}, "finish_reason": "tool_calls" if tool_calls else "stop"}
|
|
339
|
+
],
|
|
340
|
+
},
|
|
341
|
+
)
|
|
342
|
+
self.wfile.write(b"data: [DONE]\n\n")
|
|
343
|
+
self.wfile.flush()
|
|
344
|
+
return
|
|
345
|
+
|
|
346
|
+
_json_response(
|
|
347
|
+
self,
|
|
348
|
+
200,
|
|
349
|
+
{
|
|
350
|
+
"id": f"chatcmpl-pi-{uuid.uuid4().hex}",
|
|
351
|
+
"object": "chat.completion",
|
|
352
|
+
"created": int(time.time()),
|
|
353
|
+
"model": _MODEL_ID,
|
|
354
|
+
"choices": [
|
|
355
|
+
{
|
|
356
|
+
"index": 0,
|
|
357
|
+
"message": {
|
|
358
|
+
"role": "assistant",
|
|
359
|
+
"content": content if content else None,
|
|
360
|
+
**({"tool_calls": tool_calls} if tool_calls else {}),
|
|
361
|
+
},
|
|
362
|
+
"finish_reason": "tool_calls" if tool_calls else "stop",
|
|
363
|
+
}
|
|
364
|
+
],
|
|
365
|
+
},
|
|
366
|
+
)
|
|
367
|
+
except Exception as exc: # pragma: no cover - surfaced to the TypeScript caller as HTTP 500
|
|
368
|
+
traceback.print_exc()
|
|
369
|
+
_json_response(self, 500, {"error": str(exc)})
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _download_only(model_id: str, cache_dir: str) -> None:
|
|
373
|
+
from huggingface_hub import snapshot_download
|
|
374
|
+
|
|
375
|
+
snapshot_download(repo_id=model_id, cache_dir=cache_dir)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _load_model(model_id: str, cache_dir: str, device: str) -> None:
|
|
379
|
+
global _MODEL_ID, _TOKENIZER, _MODEL, _TORCH, _DEVICE
|
|
380
|
+
|
|
381
|
+
import torch
|
|
382
|
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
|
383
|
+
|
|
384
|
+
threads = os.environ.get("PI_TRANSFORMERS_THREADS")
|
|
385
|
+
if threads:
|
|
386
|
+
try:
|
|
387
|
+
torch.set_num_threads(max(1, int(threads)))
|
|
388
|
+
except Exception:
|
|
389
|
+
pass
|
|
390
|
+
|
|
391
|
+
dtype_name = os.environ.get("PI_TRANSFORMERS_TORCH_DTYPE", "float32")
|
|
392
|
+
torch_dtype = getattr(torch, dtype_name, torch.float32)
|
|
393
|
+
tokenizer = AutoTokenizer.from_pretrained(model_id, cache_dir=cache_dir)
|
|
394
|
+
model = AutoModelForCausalLM.from_pretrained(model_id, cache_dir=cache_dir, torch_dtype=torch_dtype)
|
|
395
|
+
model.to(device)
|
|
396
|
+
model.eval()
|
|
397
|
+
|
|
398
|
+
_MODEL_ID = model_id
|
|
399
|
+
_TOKENIZER = tokenizer
|
|
400
|
+
_MODEL = model
|
|
401
|
+
_TORCH = torch
|
|
402
|
+
_DEVICE = device
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def main() -> None:
|
|
406
|
+
parser = argparse.ArgumentParser()
|
|
407
|
+
parser.add_argument("--model-id", required=True)
|
|
408
|
+
parser.add_argument("--host", default="127.0.0.1")
|
|
409
|
+
parser.add_argument("--port", type=int, default=18100)
|
|
410
|
+
parser.add_argument("--cache-dir", required=True)
|
|
411
|
+
parser.add_argument("--device", default=os.environ.get("PI_TRANSFORMERS_DEVICE", "cpu"))
|
|
412
|
+
parser.add_argument("--download-only", action="store_true")
|
|
413
|
+
args = parser.parse_args()
|
|
414
|
+
|
|
415
|
+
os.makedirs(args.cache_dir, exist_ok=True)
|
|
416
|
+
if args.download_only:
|
|
417
|
+
_download_only(args.model_id, args.cache_dir)
|
|
418
|
+
return
|
|
419
|
+
|
|
420
|
+
_load_model(args.model_id, args.cache_dir, args.device)
|
|
421
|
+
server = ThreadingHTTPServer((args.host, args.port), Handler)
|
|
422
|
+
print(f"pi hf-transformers server ready model={args.model_id} url=http://{args.host}:{args.port}", flush=True)
|
|
423
|
+
server.serve_forever()
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
if __name__ == "__main__":
|
|
427
|
+
main()
|
|
@@ -17,6 +17,24 @@ will repair; the same entry powers the repair, the teach note, the telemetry
|
|
|
17
17
|
tag, and the docs table. Adding a repair means adding one entry plus its
|
|
18
18
|
fixtures, nothing else.
|
|
19
19
|
|
|
20
|
+
Registry mode names currently in force:
|
|
21
|
+
|
|
22
|
+
- `nullOptionalDrop`
|
|
23
|
+
- `nullRequiredBounce`
|
|
24
|
+
- `jsonStringParse`
|
|
25
|
+
- `jsonObjectPropertySalvage`
|
|
26
|
+
- `singleObjectWrap`
|
|
27
|
+
- `bareScalarWrap`
|
|
28
|
+
- `emptyObjectPlaceholder`
|
|
29
|
+
- `numberFromString`
|
|
30
|
+
- `boolFromString`
|
|
31
|
+
- `enumCaseNormalize`
|
|
32
|
+
- `propertyCaseNormalize`
|
|
33
|
+
- `singleElementUnwrap`
|
|
34
|
+
- `stringifiedNumberInArray`
|
|
35
|
+
- `bashCommandArgvJoin`
|
|
36
|
+
- `bashCommandUnwrap`
|
|
37
|
+
|
|
20
38
|
### 1. null-for-optional (`nullOptionalDrop`)
|
|
21
39
|
- Error signature: type mismatch at path P, received `null`, P not in the
|
|
22
40
|
parent's `required` list.
|
|
@@ -54,11 +54,18 @@ pre-trained on other stacks' conventions.
|
|
|
54
54
|
2. `<tool_call>{"name":"X","arguments":{...}}</tool_call>` (common OSS
|
|
55
55
|
convention; `arguments` may itself be a JSON STRING — that is exactly
|
|
56
56
|
R31 mode 2, handed straight to the repairer).
|
|
57
|
-
3. Fenced block tagged `tool`/`tool_call`:
|
|
57
|
+
3. Fenced block tagged `tool`/`tool_call`/`json`:
|
|
58
58
|
```` ```tool\n{"name":"X","arguments":{...}}\n``` ````
|
|
59
|
+
4. XML function-call convention:
|
|
60
|
+
`<function name="X"><param name="k">value</param></function>`.
|
|
61
|
+
The scanner requires an explicit `</function>` close tag. Each `param`
|
|
62
|
+
becomes a string-valued argument; R31 owns string-to-number, string-to-bool,
|
|
63
|
+
and JSON-string coercion after parsing. Nested `<function>` bodies,
|
|
64
|
+
duplicate param names, or non-whitespace text inside the function body are
|
|
65
|
+
ambiguous and are refused rather than guessed.
|
|
59
66
|
Everything else is prose. Ambiguous text is NEVER guessed into a call
|
|
60
|
-
(doctrine: no heuristic soup). Unknown `tool-name` →
|
|
61
|
-
|
|
67
|
+
(doctrine: no heuristic soup). Unknown `tool-name` → a text-protocol bounce with
|
|
68
|
+
an "unknown tool" note listing valid names.
|
|
62
69
|
|
|
63
70
|
## 3. Schema -> primer projection (how the dictionary is generated)
|
|
64
71
|
|
|
@@ -135,7 +142,7 @@ string }` where:
|
|
|
135
142
|
|
|
136
143
|
The trial tool is `echo(data:string)` (harness-provided, side-effect-free).
|
|
137
144
|
Calibration asks the model to `echo` a known token via the protocol and
|
|
138
|
-
checks the parser round-trips it. Grammar variants tried on failure
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
145
|
+
checks the parser round-trips it. Grammar variants tried on failure are the
|
|
146
|
+
parser's supported variants in deterministic order: canonical `<pi:call>`,
|
|
147
|
+
`<tool_call>`, fenced JSON/tool blocks, then XML `<function>`. The variant that
|
|
148
|
+
first round-trips is persisted per model (R46).
|
|
@@ -28,8 +28,9 @@ import { type CurationProposals } from "./learning/skill-curator.ts";
|
|
|
28
28
|
import type { MemoryProvider } from "./memory/memory-provider.ts";
|
|
29
29
|
import { type ModelCapabilityProfile } from "./model-capability.ts";
|
|
30
30
|
import type { ModelRegistry } from "./model-registry.ts";
|
|
31
|
+
import { type NativeToolProbeGrade } from "./models/adaptation-store.ts";
|
|
31
32
|
import type { StoredFitnessReport } from "./models/fitness-store.ts";
|
|
32
|
-
import type { LocalRuntimeDeps, OllamaRuntime } from "./models/local-runtime.ts";
|
|
33
|
+
import type { LocalRuntimeDeps, OllamaRuntime, TransformersRuntime } from "./models/local-runtime.ts";
|
|
33
34
|
import { type PromptTemplate } from "./prompt-templates.ts";
|
|
34
35
|
import type { ModelFitnessReport } from "./research/model-fitness.ts";
|
|
35
36
|
import type { ResearchRunResult } from "./research/research-runner.ts";
|
|
@@ -203,6 +204,7 @@ export interface ToolProbeResult {
|
|
|
203
204
|
model: string;
|
|
204
205
|
verdict: ToolProbeVerdict;
|
|
205
206
|
variant?: TextToolProtocolVariant;
|
|
207
|
+
nativeGrade?: NativeToolProbeGrade;
|
|
206
208
|
diagnostic?: string;
|
|
207
209
|
}
|
|
208
210
|
export interface ToolProbeReport {
|
|
@@ -371,6 +373,8 @@ export declare class AgentSession {
|
|
|
371
373
|
private _collectWorkspaceSources;
|
|
372
374
|
private readonly _localRuntimeController;
|
|
373
375
|
private readonly _modelAdaptationStore;
|
|
376
|
+
private _prefixWarmer;
|
|
377
|
+
private readonly _completedPrefixWarms;
|
|
374
378
|
private readonly _repairModeSessionCounts;
|
|
375
379
|
private readonly _textProtocolParseFailures;
|
|
376
380
|
private _textProtocolParseObservedThisTurn;
|
|
@@ -443,6 +447,10 @@ export declare class AgentSession {
|
|
|
443
447
|
constructor(config: AgentSessionConfig);
|
|
444
448
|
/** Model registry for API key resolution and model discovery */
|
|
445
449
|
get modelRegistry(): ModelRegistry;
|
|
450
|
+
private _scheduleLocalPrefixWarm;
|
|
451
|
+
private _cancelPrefixWarm;
|
|
452
|
+
private _runLocalPrefixWarm;
|
|
453
|
+
private _isWarmableLocalModel;
|
|
446
454
|
/**
|
|
447
455
|
* True when the session's stream fn is the raw `streamSimple` provider entry (directly, or as the
|
|
448
456
|
* base wrapped by the idle watchdog at construction). Callers use this to decide whether request
|
|
@@ -508,8 +516,11 @@ export declare class AgentSession {
|
|
|
508
516
|
private _textProtocolFlag;
|
|
509
517
|
private _streamForToolProbe;
|
|
510
518
|
private _textProtocolCalibrationContext;
|
|
511
|
-
private
|
|
512
|
-
private
|
|
519
|
+
private _messageHasToolCallWithStringArgument;
|
|
520
|
+
private _nativeToolProbeSystemPrompt;
|
|
521
|
+
private _runNativeReadTaskProbeTrial;
|
|
522
|
+
private _runNativeEchoToolProbeTrial;
|
|
523
|
+
private _gradeNativeToolCallingForModel;
|
|
513
524
|
private _runTextProtocolTrial;
|
|
514
525
|
private _calibrateTextToolProtocolForModel;
|
|
515
526
|
private _ensureTextToolProtocolForActiveModel;
|
|
@@ -766,6 +777,7 @@ export declare class AgentSession {
|
|
|
766
777
|
* untracked child. Delegates to {@link LocalRuntimeController}.
|
|
767
778
|
*/
|
|
768
779
|
getLocalRuntime(baseUrl?: string): OllamaRuntime;
|
|
780
|
+
getTransformersRuntime(modelId: string, baseUrl?: string): TransformersRuntime;
|
|
769
781
|
/** models.json registers a local model's baseUrl as `<server>/v1` (OpenAI-compat); the runtime's
|
|
770
782
|
* own health/boot endpoints are on the Ollama-native server root. Delegates to
|
|
771
783
|
* {@link LocalRuntimeController}; kept here for `_warnIfManualModelChoiceIsRisky`'s own use. */
|