@caupulican/pi-adaptative 0.81.13 → 0.81.16
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 +19 -0
- package/dist/bundled-resources/runtimes/hf-transformers-openai-server.py +427 -0
- package/dist/bundled-resources/skills/tool-call-repair/SKILL.md +13 -11
- package/dist/bundled-resources/skills/tool-call-repair/references/failure-grammar.md +16 -10
- package/dist/bundled-resources/skills/tool-call-repair/references/text-protocol-grammar.md +14 -7
- package/dist/core/agent-session.d.ts +11 -3
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +132 -23
- 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 +2 -0
- package/dist/core/models/adaptation-store.d.ts.map +1 -1
- package/dist/core/models/adaptation-store.js +4 -0
- package/dist/core/models/adaptation-store.js.map +1 -1
- 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 +77 -1
- package/dist/core/models/local-runtime.d.ts.map +1 -1
- package/dist/core/models/local-runtime.js +295 -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/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 +133 -19
- package/dist/modes/interactive/local-model-commands.js.map +1 -1
- package/docs/models.md +19 -1
- package/docs/tool-repair.md +2 -0
- 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,22 @@
|
|
|
1
|
+
## [0.81.16] - 2026-07-08
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
- 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.
|
|
5
|
+
- Added MiniCPM5-1B to the live acceptance fleet as a full-base native tool-calling target through the pi-managed Transformers sidecar.
|
|
6
|
+
- 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.
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
- 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.
|
|
10
|
+
- Documented the Ollama serving-context requirement for local OpenAI-compatible models.
|
|
11
|
+
|
|
12
|
+
## [0.81.15] - 2026-07-07
|
|
13
|
+
|
|
14
|
+
## [0.81.14] - 2026-07-07
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- Fixed text protocol model gating so persisted probe verdicts opt in only text-protocol models after env/settings/model gates, while unflagged native models do not receive the primer.
|
|
18
|
+
- Documented the `propertyCaseNormalize` and `jsonObjectPropertySalvage` repair modes in the operator docs and bundled tool-repair skill.
|
|
19
|
+
|
|
1
20
|
## [0.81.13] - 2026-07-07
|
|
2
21
|
|
|
3
22
|
### Fixed
|
|
@@ -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()
|
|
@@ -103,17 +103,19 @@ as code (not prose):
|
|
|
103
103
|
|---|---|---|---|
|
|
104
104
|
| 1 | nullOptionalDrop | `null` for an optional field | delete key |
|
|
105
105
|
| 2 | nullRequiredBounce | `null` for a required field | no repair; bounce with required-value feedback |
|
|
106
|
-
| 3 | jsonStringParse | `"[...]"`/`"{...}"` string where container expected | JSON.parse; keep if it matches + checks |
|
|
107
|
-
| 4 |
|
|
108
|
-
| 5 |
|
|
109
|
-
| 6 |
|
|
110
|
-
| 7 |
|
|
111
|
-
| 8 |
|
|
112
|
-
| 9 |
|
|
113
|
-
| 10 |
|
|
114
|
-
| 11 |
|
|
115
|
-
| 12 |
|
|
116
|
-
| 13 |
|
|
106
|
+
| 3 | jsonStringParse | `"[...]"`/`"{...}"` string where container expected | JSON.parse; guarded smart-quote delimiter fallback; keep if it matches + checks |
|
|
107
|
+
| 4 | jsonObjectPropertySalvage | malformed object string with recoverable declared properties | keep declared property values if the whole object checks |
|
|
108
|
+
| 5 | singleObjectWrap | single object where array-of-objects expected | wrap `[obj]` if it passes `items` |
|
|
109
|
+
| 6 | bareScalarWrap | bare scalar where array expected | wrap `[v]` if it passes `items` |
|
|
110
|
+
| 7 | emptyObjectPlaceholder | `{}` placeholder where scalar expected | delete if optional (default applies); else bounce |
|
|
111
|
+
| 8 | numberFromString | `"42"` where number expected | `Number(s)` if finite |
|
|
112
|
+
| 9 | boolFromString | `"true"`/`"false"` where bool expected | exact map (never truthiness) |
|
|
113
|
+
| 10 | enumCaseNormalize | case/space enum variant | match to the one member, else bounce |
|
|
114
|
+
| 11 | propertyCaseNormalize | root argument key casing differs from schema casing | rename to the schema key when unique |
|
|
115
|
+
| 12 | singleElementUnwrap | `[v]` where scalar expected | unwrap if 1 elem and checks |
|
|
116
|
+
| 13 | stringifiedNumberInArray | `["1","2"]` where number[] expected | map Number if all finite |
|
|
117
|
+
| 14 | bashCommandArgvJoin | bash `command` sent as an argv list | join string values with spaces |
|
|
118
|
+
| 15 | bashCommandUnwrap | bash `command` sent as a single-key object wrapper | unwrap the string-valued wrapper |
|
|
117
119
|
|
|
118
120
|
Every entry is a NAMED registry entry
|
|
119
121
|
`{name, errorSignature, transform, guard, noteTemplate}` — one table powers
|
|
@@ -18,15 +18,17 @@ is in its parent's `required`; `def(P)` = P has a schema `default`.
|
|
|
18
18
|
|---|---|---|---|---|---|---|
|
|
19
19
|
| 1 | nullOptionalDrop | type mismatch at P | `null`, `!req(P)` | delete key P | always (absence is valid for optional) | "sent null for optional `P` → omit the field instead" |
|
|
20
20
|
| 1b | nullRequiredBounce | type mismatch at P | `null`, `req(P)`, `!def(P)` | none | never (bounce) | "`P` is required and cannot be null → send a real value" |
|
|
21
|
-
| 2 | jsonStringParse | expect array\|object at P, got string | `"[...]"`/`"{...}"
|
|
21
|
+
| 2 | jsonStringParse | expect array\|object at P, got string | `"[...]"`/`"{...}"`, including text-protocol smart-quote delimiter drift | `JSON.parse(s)`; if strict parse fails, normalize `“`/`”` delimiters and the observed missing object-key quote pattern, then parse | parsed matches expect(P) AND sub-checks | "sent `P` as a quoted JSON string → send a raw JSON array/object" |
|
|
22
|
+
| 2b | jsonObjectPropertySalvage | expect object at P, got malformed object string | e.g. `{"path":"x" extra:1000}` | extract schema-declared JSON literal property values into a new object | each declared property appears at most once, no undeclared value is kept, and whole-object Check passes | "sent `P` as malformed JSON with recoverable declared properties → keep the schema-declared properties" |
|
|
22
23
|
| 3 | singleObjectWrap | expect array at P, got object | `{...}` | `[obj]` | `[obj]` passes `items(P)` | "sent one object where `P` takes a list → wrap it in `[ ]`" |
|
|
23
24
|
| 4 | bareScalarWrap | expect array at P, got scalar | string/number/bool | `[v]` | `[v]` passes `items(P)` | "sent a single value where `P` takes a list → wrap it in `[ ]`" |
|
|
24
25
|
| 5 | emptyObjectPlaceholder | expect scalar at P (or array whose `items` reject `{}`), got object | `{}` | delete key P | `!req(P)` (schema default applies) else bounce | "sent `{}` as a placeholder → omit `P`; its default applies" |
|
|
25
26
|
| 6 | numberFromString | expect number/integer at P, got string | `"42"`, numeric | `Number(s)` | finite (and integer if integer(P)) | "sent `P` as a quoted number → send a bare number" |
|
|
26
27
|
| 7 | boolFromString | expect boolean at P, got string | `"true"`/`"false"` | `s === "true"` | exact match only (never truthiness) | "sent `P` as a quoted boolean → send bare true/false" |
|
|
27
28
|
| 8 | enumCaseNormalize | expect enum at P, got string not in set | case/space variant | match case-insensitively/trimmed to one enum member | exactly one member matches | "`P` must be one of `a|b|c` → matched `<value>`" |
|
|
28
|
-
| 9 |
|
|
29
|
-
| 10 |
|
|
29
|
+
| 9 | propertyCaseNormalize | root object has key K whose casing differs from a declared schema property | e.g. `Path` for `path` | rename K to the schema key | exactly one declared root property matches case-insensitively and the canonical key is absent | "sent `P` with different property-key casing → use the schema key casing" |
|
|
30
|
+
| 10 | singleElementUnwrap | expect scalar at P, got 1-elem array | `[v]` | `v` | `v` passes expect(P) AND `length===1` | "sent `P` as a 1-item list where a single value was expected → send the value" |
|
|
31
|
+
| 11 | stringifiedNumberInArray | expect number[] at P, got string[] | `["1","2"]` | map `Number` | all finite | "list `P` holds quoted numbers → send bare numbers" |
|
|
30
32
|
|
|
31
33
|
Rules that keep this deterministic and safe:
|
|
32
34
|
- **Repairs never invent a value.** 5 relies on the schema `default`; nothing
|
|
@@ -34,13 +36,14 @@ Rules that keep this deterministic and safe:
|
|
|
34
36
|
- **Guard is mandatory.** Every transform runs on a clone and is kept ONLY if
|
|
35
37
|
it Checks against the sub-schema at P. A transform whose guard fails leaves
|
|
36
38
|
args untouched and falls through to the next applicable mode, then bounce.
|
|
37
|
-
- **Order within one path:** 2 (parse) → 8/6/7 (coerce scalar) → 3/4/
|
|
38
|
-
→
|
|
39
|
+
- **Order within one path:** 2 (strict/quote-normalized parse) → 2b (declared-property salvage) → 8/6/7 (coerce scalar) → 3/4/11 (wrap)
|
|
40
|
+
→ 10 (unwrap) → 5 (placeholder-drop) → 1 (null-drop). Parse before wrap so a
|
|
39
41
|
stringified array becomes an array, not a wrapped string. Across paths:
|
|
40
|
-
instance-path order.
|
|
42
|
+
instance-path order. Root property-key casing repair runs before these per-path
|
|
43
|
+
transforms because required-property validator errors do not carry the misspelled key path.
|
|
41
44
|
- **One re-Check.** After all per-path transforms, Check the whole args once.
|
|
42
45
|
Pass → return repaired; fail → bounce. Never loop transforms.
|
|
43
|
-
- Modes 6–
|
|
46
|
+
- Modes 2b and 6–11 are the increment past the original four; each is guard-gated so
|
|
44
47
|
it can only ever turn an invalid call into a valid one, never change a
|
|
45
48
|
valid call (the hot path never reaches them — see Performance).
|
|
46
49
|
|
|
@@ -104,10 +107,13 @@ The failure grammar only ever runs on ALREADY-FAILED calls. The hot path
|
|
|
104
107
|
`validator.Check(args)` (cached compiled validator) and returns the SAME
|
|
105
108
|
object — no clone, no walk, no grammar lookup. This is the ~99% path for
|
|
106
109
|
strong models and it is unchanged from today's cost.
|
|
107
|
-
2. **Repairs are
|
|
110
|
+
2. **Repairs are bounded on the failed path.** The analyzer walks the
|
|
108
111
|
validator's error list (already produced by the failed Check), not the
|
|
109
|
-
schema tree
|
|
110
|
-
|
|
112
|
+
schema tree; the exceptions are `propertyCaseNormalize` and
|
|
113
|
+
`jsonObjectPropertySalvage`, which scan only root object keys or declared
|
|
114
|
+
object properties after a failed Check. Each
|
|
115
|
+
path error maps to at most a few candidate modes by a static dispatch table
|
|
116
|
+
keyed on `(expect, got)` — a Map lookup, not a scan.
|
|
111
117
|
3. **No per-call compilation.** errorSignature matchers are precompiled
|
|
112
118
|
constants; note templates are format strings; the registry is built once
|
|
113
119
|
at module load. Nothing in the repair path constructs a RegExp, compiles a
|
|
@@ -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 {
|
|
@@ -374,6 +376,7 @@ export declare class AgentSession {
|
|
|
374
376
|
private readonly _repairModeSessionCounts;
|
|
375
377
|
private readonly _textProtocolParseFailures;
|
|
376
378
|
private _textProtocolParseObservedThisTurn;
|
|
379
|
+
private _textProtocolValidationOutcomeThisTurn;
|
|
377
380
|
/** Assembles the session's base system prompt from live session state (see
|
|
378
381
|
* system-prompt-builder.ts); owns the paired _baseSystemPromptOptions. */
|
|
379
382
|
private readonly _systemPromptBuilder;
|
|
@@ -507,8 +510,11 @@ export declare class AgentSession {
|
|
|
507
510
|
private _textProtocolFlag;
|
|
508
511
|
private _streamForToolProbe;
|
|
509
512
|
private _textProtocolCalibrationContext;
|
|
510
|
-
private
|
|
511
|
-
private
|
|
513
|
+
private _messageHasToolCallWithStringArgument;
|
|
514
|
+
private _nativeToolProbeSystemPrompt;
|
|
515
|
+
private _runNativeReadTaskProbeTrial;
|
|
516
|
+
private _runNativeEchoToolProbeTrial;
|
|
517
|
+
private _gradeNativeToolCallingForModel;
|
|
512
518
|
private _runTextProtocolTrial;
|
|
513
519
|
private _calibrateTextToolProtocolForModel;
|
|
514
520
|
private _ensureTextToolProtocolForActiveModel;
|
|
@@ -519,6 +525,7 @@ export declare class AgentSession {
|
|
|
519
525
|
private _resolveToolProbeModels;
|
|
520
526
|
probeToolCalling(target?: string): Promise<ToolProbeReport>;
|
|
521
527
|
private _handleTextToolProtocolParse;
|
|
528
|
+
private _handleTextToolProtocolValidationOutcome;
|
|
522
529
|
private _recordTextToolProtocolParseOutcomeFromLastAssistant;
|
|
523
530
|
private _looksLikeTextToolProtocolAttempt;
|
|
524
531
|
private _recordToolValidationBounce;
|
|
@@ -764,6 +771,7 @@ export declare class AgentSession {
|
|
|
764
771
|
* untracked child. Delegates to {@link LocalRuntimeController}.
|
|
765
772
|
*/
|
|
766
773
|
getLocalRuntime(baseUrl?: string): OllamaRuntime;
|
|
774
|
+
getTransformersRuntime(modelId: string, baseUrl?: string): TransformersRuntime;
|
|
767
775
|
/** models.json registers a local model's baseUrl as `<server>/v1` (OpenAI-compat); the runtime's
|
|
768
776
|
* own health/boot endpoints are on the Ollama-native server root. Delegates to
|
|
769
777
|
* {@link LocalRuntimeController}; kept here for `_warnIfManualModelChoiceIsRisky`'s own use. */
|