mlx-code 0.0.1a1__tar.gz → 0.0.1a2__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.
- {mlx_code-0.0.1a1 → mlx_code-0.0.1a2}/PKG-INFO +2 -2
- {mlx_code-0.0.1a1 → mlx_code-0.0.1a2}/main.py +44 -93
- {mlx_code-0.0.1a1 → mlx_code-0.0.1a2}/mlx_code.egg-info/PKG-INFO +2 -2
- {mlx_code-0.0.1a1 → mlx_code-0.0.1a2}/setup.py +2 -2
- {mlx_code-0.0.1a1 → mlx_code-0.0.1a2}/README.md +0 -0
- {mlx_code-0.0.1a1 → mlx_code-0.0.1a2}/mlx_code.egg-info/SOURCES.txt +0 -0
- {mlx_code-0.0.1a1 → mlx_code-0.0.1a2}/mlx_code.egg-info/dependency_links.txt +0 -0
- {mlx_code-0.0.1a1 → mlx_code-0.0.1a2}/mlx_code.egg-info/entry_points.txt +0 -0
- {mlx_code-0.0.1a1 → mlx_code-0.0.1a2}/mlx_code.egg-info/requires.txt +0 -0
- {mlx_code-0.0.1a1 → mlx_code-0.0.1a2}/mlx_code.egg-info/top_level.txt +0 -0
- {mlx_code-0.0.1a1 → mlx_code-0.0.1a2}/setup.cfg +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mlx-code
|
|
3
|
-
Version: 0.0.
|
|
4
|
-
Summary: Local Claude Code
|
|
3
|
+
Version: 0.0.1a2
|
|
4
|
+
Summary: Local Claude Code for Mac
|
|
5
5
|
Home-page: https://github.com/JosefAlbers/mlx-code
|
|
6
6
|
Author: J Joe
|
|
7
7
|
Author-email: albersj66@gmail.com
|
|
@@ -2,6 +2,9 @@ import argparse
|
|
|
2
2
|
import json
|
|
3
3
|
import os
|
|
4
4
|
import re
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
import threading
|
|
5
8
|
import time
|
|
6
9
|
import uuid
|
|
7
10
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
@@ -14,18 +17,27 @@ except ImportError:
|
|
|
14
17
|
|
|
15
18
|
DEFAULT_MODEL = "mlx-community/Qwen3.5-4B-OptiQ-4bit"
|
|
16
19
|
DEFAULT_SKILL_DIRS = ["./skills", os.path.expanduser("~/.claude/skills")]
|
|
17
|
-
|
|
20
|
+
LOG_FILE = "mlx_trace.log"
|
|
18
21
|
|
|
19
22
|
model = None
|
|
20
23
|
tokenizer = None
|
|
21
24
|
model_id = None
|
|
22
25
|
skills = {}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
_call_counter = 0
|
|
27
|
+
|
|
28
|
+
def trace(prompt: str, raw: str, elapsed: float):
|
|
29
|
+
global _call_counter
|
|
30
|
+
_call_counter += 1
|
|
31
|
+
sep = "=" * 80
|
|
32
|
+
entry = (
|
|
33
|
+
f"\n{sep}\n"
|
|
34
|
+
f"CALL {_call_counter} {time.strftime('%Y-%m-%d %H:%M:%S')} ({elapsed:.1f}s)\n"
|
|
35
|
+
f"{sep}\n"
|
|
36
|
+
f"--- PROMPT ---\n{prompt}\n"
|
|
37
|
+
f"--- OUTPUT ---\n{raw}\n"
|
|
38
|
+
)
|
|
39
|
+
with open(LOG_FILE, "a") as f:
|
|
40
|
+
f.write(entry)
|
|
29
41
|
|
|
30
42
|
def parse_frontmatter(text):
|
|
31
43
|
m = re.match(r"^---\n(.*?)\n---\n", text, re.DOTALL)
|
|
@@ -71,7 +83,6 @@ def skill_body(name):
|
|
|
71
83
|
except Exception as e:
|
|
72
84
|
return f"Error reading skill: {e}"
|
|
73
85
|
|
|
74
|
-
|
|
75
86
|
def load_model(path):
|
|
76
87
|
global model, tokenizer, model_id
|
|
77
88
|
from mlx_lm import load
|
|
@@ -82,37 +93,21 @@ def load_model(path):
|
|
|
82
93
|
|
|
83
94
|
def generate(prompt, max_tokens, temp, top_p):
|
|
84
95
|
from mlx_lm import generate as mlx_gen
|
|
85
|
-
|
|
96
|
+
t0 = time.time()
|
|
97
|
+
raw = mlx_gen(
|
|
86
98
|
model, tokenizer,
|
|
87
99
|
prompt=prompt,
|
|
88
100
|
max_tokens=max_tokens,
|
|
89
101
|
verbose=False,
|
|
90
102
|
)
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
READ_SKILL_TOOL = {
|
|
94
|
-
"type": "function",
|
|
95
|
-
"function": {
|
|
96
|
-
"name": "read_skill",
|
|
97
|
-
"description": (
|
|
98
|
-
"Read the full SKILL.md for a skill before using it. "
|
|
99
|
-
"Call this whenever a task matches a skill description."
|
|
100
|
-
),
|
|
101
|
-
"parameters": {
|
|
102
|
-
"type": "object",
|
|
103
|
-
"properties": {
|
|
104
|
-
"name": {"type": "string", "description": "Skill name from available_skills"}
|
|
105
|
-
},
|
|
106
|
-
"required": ["name"],
|
|
107
|
-
},
|
|
108
|
-
},
|
|
109
|
-
}
|
|
103
|
+
trace(prompt, raw, time.time() - t0)
|
|
104
|
+
return raw
|
|
110
105
|
|
|
111
106
|
def skills_system_addon():
|
|
112
107
|
if not skills:
|
|
113
108
|
return ""
|
|
114
109
|
entries = "\n".join(
|
|
115
|
-
f"<skill><
|
|
110
|
+
f"<skill><n>{n}</n><description>{s['description']}</description></skill>"
|
|
116
111
|
for n, s in skills.items()
|
|
117
112
|
)
|
|
118
113
|
return (
|
|
@@ -189,12 +184,10 @@ def build_messages(body, extra=None):
|
|
|
189
184
|
msgs.extend(extra)
|
|
190
185
|
return msgs
|
|
191
186
|
|
|
192
|
-
|
|
193
187
|
def build_prompt(body, extra=None):
|
|
194
188
|
msgs = build_messages(body, extra=extra)
|
|
195
189
|
return tokenizer.apply_chat_template(msgs, tokenize=False, add_generation_prompt=True)
|
|
196
190
|
|
|
197
|
-
|
|
198
191
|
def parse_output(raw):
|
|
199
192
|
blocks = []
|
|
200
193
|
remaining = raw
|
|
@@ -212,7 +205,7 @@ def parse_output(raw):
|
|
|
212
205
|
tool_blocks = []
|
|
213
206
|
leftover = remaining
|
|
214
207
|
for m in re.finditer(r"<tool_call>(.*?)</tool_call>", remaining, re.DOTALL):
|
|
215
|
-
inner
|
|
208
|
+
inner = m.group(1).strip()
|
|
216
209
|
parsed = None
|
|
217
210
|
|
|
218
211
|
try:
|
|
@@ -275,27 +268,21 @@ def parse_output(raw):
|
|
|
275
268
|
blocks.append({"type": "text", "text": remaining})
|
|
276
269
|
return blocks or [{"type": "text", "text": raw}], "end_turn"
|
|
277
270
|
|
|
278
|
-
|
|
279
271
|
def resolve_read_skill(blocks, body, max_tokens, temp, top_p):
|
|
280
272
|
extra = []
|
|
281
273
|
for _ in range(5):
|
|
282
274
|
skill_calls = [b for b in blocks if b.get("type")=="tool_use" and b["name"]=="read_skill"]
|
|
283
275
|
if not skill_calls:
|
|
284
276
|
break
|
|
285
|
-
log("↻ SKILL TOOL", "\n".join(f" read_skill({c['input']})" for c in skill_calls))
|
|
286
277
|
for c in skill_calls:
|
|
287
278
|
name = c["input"].get("name","")
|
|
288
279
|
content = skill_body(name)
|
|
289
280
|
args = f"<parameter=name>\n{name}\n</parameter>"
|
|
290
281
|
extra.append({"role": "assistant", "content": f"<tool_call>\n<function=read_skill>\n{args}</function>\n</tool_call>"})
|
|
291
|
-
log(f"↻ SKILL BODY [{name}]", content[:500] + ("…" if len(content)>500 else ""))
|
|
292
282
|
extra.append({"role": "user", "content": f"<tool_response>\n{content}\n</tool_response>"})
|
|
293
283
|
prompt = build_prompt(body, extra=extra)
|
|
294
|
-
|
|
295
|
-
raw = generate(prompt, max_tokens, temp, top_p)
|
|
296
|
-
log("← MLX RAW", raw)
|
|
284
|
+
raw = generate(prompt, max_tokens, temp, top_p)
|
|
297
285
|
blocks, stop_reason = parse_output(raw)
|
|
298
|
-
log("← PARSED", json.dumps(blocks, indent=2))
|
|
299
286
|
stop_reason = "tool_use" if any(b.get("type")=="tool_use" for b in blocks) else "end_turn"
|
|
300
287
|
return blocks, stop_reason
|
|
301
288
|
|
|
@@ -362,11 +349,10 @@ def blocks_to_sse(blocks, msg_id, stop_reason, in_tokens, out_tokens):
|
|
|
362
349
|
|
|
363
350
|
return bytes(out)
|
|
364
351
|
|
|
365
|
-
|
|
366
352
|
class Handler(BaseHTTPRequestHandler):
|
|
367
353
|
|
|
368
354
|
def log_message(self, fmt, *args):
|
|
369
|
-
|
|
355
|
+
pass
|
|
370
356
|
|
|
371
357
|
def send_json(self, code, obj):
|
|
372
358
|
body = json.dumps(obj).encode()
|
|
@@ -404,44 +390,17 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
404
390
|
return
|
|
405
391
|
|
|
406
392
|
body = self.read_json()
|
|
407
|
-
body["model"] = model_id
|
|
393
|
+
body["model"] = model_id
|
|
408
394
|
|
|
409
395
|
max_tokens = body.get("max_tokens", 8192)
|
|
410
396
|
temp = body.get("temperature", 0.7)
|
|
411
397
|
top_p = body.get("top_p", 0.9)
|
|
412
398
|
msg_id = f"msg_{uuid.uuid4().hex}"
|
|
413
399
|
|
|
414
|
-
tools_requested = [t["name"] for t in body.get("tools", [])]
|
|
415
|
-
last_msg = body.get("messages", [{}])[-1]
|
|
416
|
-
last_content = last_msg.get("content", "")
|
|
417
|
-
if isinstance(last_content, list):
|
|
418
|
-
last_content = " | ".join(
|
|
419
|
-
b.get("text", b.get("type","?"))[:80]
|
|
420
|
-
for b in last_content
|
|
421
|
-
)
|
|
422
|
-
tool_schemas = body.get("tools", [])
|
|
423
|
-
if len(body.get("messages", [])) <= 1:
|
|
424
|
-
tools_summary = json.dumps(tool_schemas, indent=2)
|
|
425
|
-
else:
|
|
426
|
-
tools_summary = str(tools_requested)
|
|
427
|
-
|
|
428
|
-
log("→ CC REQUEST", (
|
|
429
|
-
f"tools: {tools_summary}\n"
|
|
430
|
-
f"msgs: {len(body.get('messages',[]))}\n"
|
|
431
|
-
f"last: {str(last_content)[:200]}"
|
|
432
|
-
))
|
|
433
|
-
|
|
434
400
|
prompt = build_prompt(body)
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
t0 = time.time()
|
|
438
|
-
raw = generate(prompt, max_tokens, temp, top_p)
|
|
439
|
-
dt = time.time() - t0
|
|
440
|
-
|
|
441
|
-
log(f"← MLX RAW ({dt:.1f}s)", raw)
|
|
401
|
+
raw = generate(prompt, max_tokens, temp, top_p)
|
|
442
402
|
|
|
443
403
|
blocks, stop_reason = parse_output(raw)
|
|
444
|
-
log("← PARSED", json.dumps(blocks, indent=2))
|
|
445
404
|
|
|
446
405
|
if any(b.get("type")=="tool_use" and b["name"]=="read_skill" for b in blocks):
|
|
447
406
|
blocks, stop_reason = resolve_read_skill(blocks, body, max_tokens, temp, top_p)
|
|
@@ -451,13 +410,6 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
451
410
|
|
|
452
411
|
sse_bytes = blocks_to_sse(blocks, msg_id, stop_reason, in_tokens, out_tokens)
|
|
453
412
|
|
|
454
|
-
log("→ CC RESPONSE", (
|
|
455
|
-
f"stop_reason: {stop_reason}\n"
|
|
456
|
-
f"blocks: {[b['type'] for b in blocks]}\n"
|
|
457
|
-
f"tokens: {in_tokens} in / {out_tokens} out\n"
|
|
458
|
-
f"sse_bytes: {len(sse_bytes)}"
|
|
459
|
-
))
|
|
460
|
-
|
|
461
413
|
self.send_response(200)
|
|
462
414
|
self.send_header("Content-Type", "text/event-stream")
|
|
463
415
|
self.send_header("Cache-Control", "no-cache")
|
|
@@ -475,32 +427,31 @@ def main():
|
|
|
475
427
|
parser.add_argument("--port", type=int, default=8000)
|
|
476
428
|
parser.add_argument("--host", default="127.0.0.1")
|
|
477
429
|
parser.add_argument("--skills", action="append", metavar="DIR")
|
|
478
|
-
args = parser.
|
|
430
|
+
args, claude_args = parser.parse_known_args()
|
|
479
431
|
|
|
480
432
|
skill_dirs = args.skills or DEFAULT_SKILL_DIRS
|
|
481
433
|
print("Scanning skills …", flush=True)
|
|
482
434
|
skills = scan_skills(skill_dirs)
|
|
483
|
-
print(f"Found {len(skills)} skill(s).\n", flush=True)
|
|
484
435
|
|
|
485
436
|
load_model(args.model)
|
|
486
437
|
|
|
438
|
+
server = HTTPServer((args.host, args.port), Handler)
|
|
439
|
+
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
440
|
+
thread.start()
|
|
441
|
+
|
|
487
442
|
print(f"🍎 mlx-claude-server http://{args.host}:{args.port}")
|
|
488
|
-
print(f" model
|
|
489
|
-
print(f"
|
|
490
|
-
print()
|
|
491
|
-
print(" Other terminal:")
|
|
492
|
-
print(f" export ANTHROPIC_BASE_URL=http://{args.host}:{args.port}")
|
|
493
|
-
print( " export ANTHROPIC_AUTH_TOKEN=local")
|
|
494
|
-
print(f" export ANTHROPIC_MODEL={args.model}")
|
|
495
|
-
print(f" export ANTHROPIC_SMALL_FAST_MODEL={args.model}")
|
|
496
|
-
print( " claude")
|
|
443
|
+
print(f" model : {args.model}")
|
|
444
|
+
print(f" trace : {LOG_FILE}")
|
|
497
445
|
print()
|
|
498
446
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
447
|
+
env = os.environ.copy()
|
|
448
|
+
env["ANTHROPIC_BASE_URL"] = f"http://{args.host}:{args.port}"
|
|
449
|
+
env["ANTHROPIC_AUTH_TOKEN"] = "local"
|
|
450
|
+
env["ANTHROPIC_MODEL"] = args.model
|
|
451
|
+
env["ANTHROPIC_SMALL_FAST_MODEL"] = args.model
|
|
452
|
+
|
|
453
|
+
result = subprocess.run(["claude"] + claude_args, env=env)
|
|
454
|
+
sys.exit(result.returncode)
|
|
504
455
|
|
|
505
456
|
if __name__ == "__main__":
|
|
506
457
|
main()
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mlx-code
|
|
3
|
-
Version: 0.0.
|
|
4
|
-
Summary: Local Claude Code
|
|
3
|
+
Version: 0.0.1a2
|
|
4
|
+
Summary: Local Claude Code for Mac
|
|
5
5
|
Home-page: https://github.com/JosefAlbers/mlx-code
|
|
6
6
|
Author: J Joe
|
|
7
7
|
Author-email: albersj66@gmail.com
|
|
@@ -6,9 +6,9 @@ setup(
|
|
|
6
6
|
author_email="albersj66@gmail.com",
|
|
7
7
|
author="J Joe",
|
|
8
8
|
license="Apache-2.0",
|
|
9
|
-
version="0.0.
|
|
9
|
+
version="0.0.1a2",
|
|
10
10
|
readme="README.md",
|
|
11
|
-
description="Local Claude Code
|
|
11
|
+
description="Local Claude Code for Mac",
|
|
12
12
|
long_description=open("README.md").read(),
|
|
13
13
|
long_description_content_type="text/markdown",
|
|
14
14
|
python_requires=">=3.11",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|