pi-leo-bridge 0.1.1

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/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "pi-leo-bridge",
3
+ "version": "0.1.1",
4
+ "description": "Bring Pi-configured models and authentication into Brave Leo",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "pi-coding-agent",
9
+ "brave",
10
+ "brave-leo",
11
+ "byom",
12
+ "openai-compatible",
13
+ "macos"
14
+ ],
15
+ "homepage": "https://github.com/omaclaren/pi-leo-bridge#readme",
16
+ "bugs": {
17
+ "url": "https://github.com/omaclaren/pi-leo-bridge/issues"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/omaclaren/pi-leo-bridge.git"
22
+ },
23
+ "os": [
24
+ "darwin"
25
+ ],
26
+ "engines": {
27
+ "node": ">=22.19.0"
28
+ },
29
+ "bin": {
30
+ "pi-leo": "./bin/pi-leo",
31
+ "pi-leo-bridge": "./bin/pi-leo"
32
+ },
33
+ "files": [
34
+ "dist/src",
35
+ "bin",
36
+ "scripts/*.sh",
37
+ "scripts/*.py",
38
+ "docs",
39
+ "README.md",
40
+ "CONTRIBUTING.md",
41
+ "LICENSE",
42
+ "CHANGELOG.md",
43
+ "SECURITY.md"
44
+ ],
45
+ "scripts": {
46
+ "clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
47
+ "build": "tsc -p tsconfig.json",
48
+ "typecheck": "tsc -p tsconfig.json --noEmit",
49
+ "check:scripts": "bash -n scripts/install.sh scripts/uninstall.sh bin/pi-leo && python3 -c \"from pathlib import Path; [compile(path.read_text(), str(path), 'exec') for path in map(Path, ['scripts/configure-install.py','scripts/remove-brave-models.py','scripts/set-brave-default.py','scripts/doctor.py'])]\"",
50
+ "test": "npm run build && node --test dist/test/*.test.js",
51
+ "check": "npm run typecheck && npm run test && npm run check:scripts",
52
+ "prepack": "npm run clean && npm run check"
53
+ },
54
+ "dependencies": {
55
+ "@earendil-works/pi-agent-core": "0.84.4",
56
+ "@earendil-works/pi-ai": "0.84.4",
57
+ "@earendil-works/pi-coding-agent": "0.84.4"
58
+ },
59
+ "devDependencies": {
60
+ "@types/node": "22.20.1",
61
+ "typescript": "5.9.2"
62
+ }
63
+ }
@@ -0,0 +1,530 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import hashlib
6
+ import json
7
+ import os
8
+ import plistlib
9
+ import re
10
+ import secrets
11
+ import shutil
12
+ import stat
13
+ import tempfile
14
+ from datetime import datetime
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
+ LABEL = "com.ojm.pi-leo-bridge"
19
+ DEFAULT_PROVIDER = "openai-codex"
20
+ DEFAULT_MODEL = "gpt-5.6-sol"
21
+ DEFAULT_PORT = 43127
22
+ DEFAULT_LEVELS = ("low", "medium", "high")
23
+ VALID_LEVELS = ("off", "minimal", "low", "medium", "high", "xhigh", "max")
24
+ ENDPOINT_RE = re.compile(
25
+ r"^http://127\.0\.0\.1:(\d+)/auth/([A-Za-z0-9_-]{32,})/v1/chat/completions$"
26
+ )
27
+
28
+
29
+ def atomic_bytes(path: Path, payload: bytes, mode: int) -> None:
30
+ path.parent.mkdir(parents=True, exist_ok=True)
31
+ fd, temporary = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
32
+ try:
33
+ with os.fdopen(fd, "wb") as stream:
34
+ stream.write(payload)
35
+ stream.flush()
36
+ os.fsync(stream.fileno())
37
+ os.chmod(temporary, mode)
38
+ os.replace(temporary, path)
39
+ finally:
40
+ if os.path.exists(temporary):
41
+ os.unlink(temporary)
42
+
43
+
44
+ def read_object(path: Path) -> dict[str, Any]:
45
+ if not path.is_file():
46
+ return {}
47
+ value = json.loads(path.read_text())
48
+ if not isinstance(value, dict):
49
+ raise RuntimeError(f"Expected a JSON object: {path}")
50
+ return value
51
+
52
+
53
+ def parse_levels(raw: str | None, existing: dict[str, Any]) -> list[str]:
54
+ if raw is None:
55
+ existing_profiles = existing.get("profiles")
56
+ if isinstance(existing_profiles, list):
57
+ values = [
58
+ profile.get("thinkingLevel")
59
+ for profile in existing_profiles
60
+ if isinstance(profile, dict)
61
+ ]
62
+ if values and all(isinstance(value, str) for value in values):
63
+ raw_values = values
64
+ else:
65
+ raw_values = list(DEFAULT_LEVELS)
66
+ else:
67
+ raw_values = list(DEFAULT_LEVELS)
68
+ else:
69
+ raw_values = [part.strip().lower() for part in raw.split(",") if part.strip()]
70
+
71
+ if not raw_values:
72
+ raise RuntimeError("At least one thinking level is required")
73
+ if len(set(raw_values)) != len(raw_values):
74
+ raise RuntimeError("Thinking levels must be unique")
75
+ invalid = [value for value in raw_values if value not in VALID_LEVELS]
76
+ if invalid:
77
+ raise RuntimeError(f"Unsupported thinking level(s): {', '.join(invalid)}")
78
+ return list(raw_values)
79
+
80
+
81
+ def title_model(model_id: str) -> str:
82
+ words = re.split(r"[-_/]+", model_id)
83
+ titled: list[str] = []
84
+ start = 0
85
+ if len(words) >= 2 and words[0].lower() == "gpt":
86
+ titled.append(f"GPT-{words[1]}")
87
+ start = 2
88
+ for word in words[start:]:
89
+ lower = word.lower()
90
+ if lower in {"gpt", "llm", "ai"}:
91
+ titled.append(lower.upper())
92
+ elif lower == "codex":
93
+ titled.append("Codex")
94
+ else:
95
+ titled.append(word[:1].upper() + word[1:])
96
+ return " ".join(titled)
97
+
98
+
99
+ def public_base_id(provider: str, model_id: str) -> str:
100
+ model_slug = re.sub(r"[^A-Za-z0-9._-]+", "-", model_id).strip("-._").lower()
101
+ provider_slug = re.sub(r"[^A-Za-z0-9._-]+", "-", provider).strip("-._").lower()
102
+ if not model_slug:
103
+ raise RuntimeError("The model id cannot be converted into a Leo model name")
104
+ prefix = "pi" if provider == DEFAULT_PROVIDER else f"pi-{provider_slug}"
105
+ return f"{prefix}-{model_slug}"[:120]
106
+
107
+
108
+ def build_profiles(
109
+ provider: str,
110
+ model_id: str,
111
+ display_name: str,
112
+ levels: list[str],
113
+ primary_level: str,
114
+ existing: dict[str, Any],
115
+ ) -> list[dict[str, str]]:
116
+ existing_ids: dict[str, str] = {}
117
+ if existing.get("provider") == provider and existing.get("modelId") == model_id:
118
+ for profile in existing.get("profiles", []):
119
+ if isinstance(profile, dict):
120
+ level = profile.get("thinkingLevel")
121
+ public_id = profile.get("publicModelId")
122
+ if isinstance(level, str) and isinstance(public_id, str):
123
+ existing_ids[level] = public_id
124
+
125
+ base_id = public_base_id(provider, model_id)
126
+ profiles = []
127
+ for level in levels:
128
+ generated_id = base_id if level == primary_level else f"{base_id}-{level}"
129
+ profiles.append(
130
+ {
131
+ "public_model_id": existing_ids.get(level, generated_id),
132
+ "label": f"Pi — {display_name} ({level.title()})",
133
+ "thinking_level": level,
134
+ }
135
+ )
136
+ if len({profile["public_model_id"] for profile in profiles}) != len(profiles):
137
+ raise RuntimeError("Generated Leo model names are not unique")
138
+ return profiles
139
+
140
+
141
+ def is_bridge_model(
142
+ model: dict[str, Any],
143
+ owned_keys: set[str],
144
+ owned_ids: set[str],
145
+ ) -> bool:
146
+ key = str(model.get("key", ""))
147
+ request_name = str(model.get("model_request_name", ""))
148
+ # Ownership comes only from the installed manifest. Never infer ownership
149
+ # from a localhost port or a user-chosen model-name prefix.
150
+ return key in owned_keys or request_name in owned_ids
151
+
152
+
153
+ def new_model_key(existing_keys: set[str]) -> str:
154
+ key = f"custom:{secrets.token_hex(4)}"
155
+ while key in existing_keys:
156
+ key = f"custom:{secrets.token_hex(4)}"
157
+ existing_keys.add(key)
158
+ return key
159
+
160
+
161
+ def configure_preferences(
162
+ path: Path,
163
+ *,
164
+ port: int,
165
+ profiles: list[dict[str, str]],
166
+ existing_config: dict[str, Any],
167
+ context_size: int,
168
+ vision_support: bool,
169
+ rotate_token: bool,
170
+ ) -> tuple[str, Path, list[str], str | None]:
171
+ raw = path.read_bytes()
172
+ preferences = json.loads(raw)
173
+ ai_chat = preferences.setdefault("brave", {}).setdefault("ai_chat", {})
174
+ models = ai_chat.setdefault("custom_models", [])
175
+ if not isinstance(models, list) or not all(isinstance(model, dict) for model in models):
176
+ raise RuntimeError("Unexpected Brave custom-model preference structure")
177
+
178
+ owned_keys = {
179
+ str(key)
180
+ for key in existing_config.get("braveModelKeys", [])
181
+ if isinstance(key, str)
182
+ }
183
+ owned_ids: set[str] = set()
184
+ # Current installations identify entries by their random Brave keys. Model
185
+ # ids are used only to migrate the original private build, which predated
186
+ # the managed-key manifest.
187
+ if not owned_keys:
188
+ owned_ids.update(
189
+ str(profile.get("publicModelId"))
190
+ for profile in existing_config.get("profiles", [])
191
+ if isinstance(profile, dict) and isinstance(profile.get("publicModelId"), str)
192
+ )
193
+ if (
194
+ existing_config.get("provider") == DEFAULT_PROVIDER
195
+ and existing_config.get("modelId") == DEFAULT_MODEL
196
+ ):
197
+ owned_ids.update(
198
+ {
199
+ "pi-gpt-5.6-sol-low",
200
+ "pi-gpt-5.6-sol",
201
+ "pi-gpt-5.6-sol-high",
202
+ }
203
+ )
204
+
205
+ bridge_models = [
206
+ model for model in models if is_bridge_model(model, owned_keys, owned_ids)
207
+ ]
208
+ old_default_key = ai_chat.get("default_model_key")
209
+ old_default_profile_id: str | None = None
210
+ old_default_level: str | None = None
211
+ for model in bridge_models:
212
+ if model.get("key") == old_default_key:
213
+ value = model.get("model_request_name")
214
+ old_default_profile_id = value if isinstance(value, str) else None
215
+ break
216
+ if old_default_profile_id is not None:
217
+ for profile in existing_config.get("profiles", []):
218
+ if (
219
+ isinstance(profile, dict)
220
+ and profile.get("publicModelId") == old_default_profile_id
221
+ and isinstance(profile.get("thinkingLevel"), str)
222
+ ):
223
+ old_default_level = profile["thinkingLevel"]
224
+ break
225
+
226
+ token: str | None = None
227
+ if not rotate_token:
228
+ for bridge_model in bridge_models:
229
+ match = ENDPOINT_RE.match(str(bridge_model.get("endpoint_url", "")))
230
+ if match:
231
+ token = match.group(2)
232
+ break
233
+ if token is None:
234
+ token = secrets.token_urlsafe(32)
235
+
236
+ existing_keys = {str(model.get("key")) for model in models}
237
+ existing_by_request = {
238
+ str(model.get("model_request_name")): model for model in bridge_models
239
+ }
240
+ old_primary_id = existing_config.get("publicModelId")
241
+ legacy_primary = next(
242
+ (
243
+ model
244
+ for model in bridge_models
245
+ if model.get("model_request_name") == old_primary_id and model.get("key")
246
+ ),
247
+ next((model for model in bridge_models if model.get("key")), None),
248
+ )
249
+
250
+ endpoint = f"http://127.0.0.1:{port}/auth/{token}/v1/chat/completions"
251
+ replacements: list[dict[str, Any]] = []
252
+ model_keys: list[str] = []
253
+ for profile in profiles:
254
+ old = existing_by_request.get(profile["public_model_id"])
255
+ if old is None and profile["thinking_level"] == existing_config.get("thinkingLevel"):
256
+ old = legacy_primary
257
+ model_key = (
258
+ str(old["key"])
259
+ if old is not None and old.get("key")
260
+ else new_model_key(existing_keys)
261
+ )
262
+ model_keys.append(model_key)
263
+ replacements.append(
264
+ {
265
+ "api_key": "",
266
+ "context_size": context_size,
267
+ "endpoint_url": endpoint,
268
+ "key": model_key,
269
+ "label": profile["label"],
270
+ "model_request_name": profile["public_model_id"],
271
+ "model_system_prompt": (
272
+ "Be concise unless asked for detail. Use attached page context when relevant."
273
+ ),
274
+ "supports_tools": False,
275
+ "vision_support": vision_support,
276
+ }
277
+ )
278
+
279
+ if bridge_models:
280
+ first_index = next(
281
+ index
282
+ for index, model in enumerate(models)
283
+ if is_bridge_model(model, owned_keys, owned_ids)
284
+ )
285
+ insertion_index = sum(
286
+ 1
287
+ for model in models[:first_index]
288
+ if not is_bridge_model(model, owned_keys, owned_ids)
289
+ )
290
+ retained = [
291
+ model for model in models if not is_bridge_model(model, owned_keys, owned_ids)
292
+ ]
293
+ models[:] = retained[:insertion_index] + replacements + retained[insertion_index:]
294
+ else:
295
+ models.extend(replacements)
296
+
297
+ if old_default_profile_id is not None:
298
+ replacement_by_id = {
299
+ model["model_request_name"]: model["key"] for model in replacements
300
+ }
301
+ replacement_by_level = {
302
+ profile["thinking_level"]: model["key"]
303
+ for profile, model in zip(profiles, replacements, strict=True)
304
+ }
305
+ ai_chat["default_model_key"] = replacement_by_id.get(
306
+ old_default_profile_id,
307
+ replacement_by_level.get(old_default_level, replacements[0]["key"]),
308
+ )
309
+
310
+ previous_default = existing_config.get("previousDefaultModelKey")
311
+ if not isinstance(previous_default, str):
312
+ removed_keys = {str(model.get("key")) for model in bridge_models}
313
+ previous_default = (
314
+ str(old_default_key)
315
+ if isinstance(old_default_key, str) and old_default_key not in removed_keys
316
+ else None
317
+ )
318
+
319
+ stamp = datetime.now().strftime("%Y%m%d-%H%M%S-%f")
320
+ backup = path.with_name(f"Preferences.backup-{stamp}-before-pi-leo-bridge")
321
+ shutil.copy2(path, backup)
322
+ if backup.read_bytes() != raw:
323
+ raise RuntimeError("Brave Preferences backup verification failed")
324
+
325
+ mode = stat.S_IMODE(path.stat().st_mode)
326
+ encoded = json.dumps(preferences, ensure_ascii=False, separators=(",", ":")).encode()
327
+ check = json.loads(encoded)
328
+ if not isinstance(check, dict):
329
+ raise RuntimeError("Updated Brave Preferences failed validation")
330
+ atomic_bytes(path, encoded, mode)
331
+ return token, backup, model_keys, previous_default
332
+
333
+
334
+ def configure_runtime(
335
+ *,
336
+ home: Path,
337
+ project: Path,
338
+ node: Path,
339
+ token: str,
340
+ preferences: Path,
341
+ app_name: str,
342
+ provider: str,
343
+ model_id: str,
344
+ display_name: str,
345
+ profiles: list[dict[str, str]],
346
+ primary_level: str,
347
+ port: int,
348
+ context_size: int,
349
+ vision_support: bool,
350
+ model_keys: list[str],
351
+ previous_default: str | None,
352
+ ) -> tuple[Path, Path]:
353
+ config_dir = home / ".config" / "pi-leo-bridge"
354
+ workspace = home / ".local" / "share" / "pi-leo-bridge" / "workspace"
355
+ logs = home / "Library" / "Logs"
356
+ config_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
357
+ workspace.mkdir(parents=True, exist_ok=True, mode=0o700)
358
+ logs.mkdir(parents=True, exist_ok=True)
359
+ os.chmod(config_dir, 0o700)
360
+ os.chmod(workspace, 0o700)
361
+
362
+ primary = next(
363
+ profile for profile in profiles if profile["thinking_level"] == primary_level
364
+ )
365
+ config_path = config_dir / "config.json"
366
+ config: dict[str, Any] = {
367
+ "version": 1,
368
+ "host": "127.0.0.1",
369
+ "port": port,
370
+ "tokenSha256": hashlib.sha256(token.encode()).hexdigest(),
371
+ "publicModelId": primary["public_model_id"],
372
+ "provider": provider,
373
+ "modelId": model_id,
374
+ "displayName": display_name,
375
+ "thinkingLevel": primary_level,
376
+ "profiles": [
377
+ {
378
+ "publicModelId": profile["public_model_id"],
379
+ "thinkingLevel": profile["thinking_level"],
380
+ }
381
+ for profile in profiles
382
+ ],
383
+ "workspace": str(workspace),
384
+ "agentDir": str(home / ".pi" / "agent"),
385
+ "maxBodyBytes": 12 * 1024 * 1024,
386
+ "maxConcurrentRequests": 2,
387
+ "contextSize": context_size,
388
+ "visionSupport": vision_support,
389
+ "bravePreferencesPath": str(preferences),
390
+ "braveApplicationName": app_name,
391
+ "braveModelKeys": model_keys,
392
+ "previousDefaultModelKey": previous_default,
393
+ }
394
+ atomic_bytes(config_path, (json.dumps(config, indent=2) + "\n").encode(), 0o600)
395
+
396
+ plist_path = home / "Library" / "LaunchAgents" / f"{LABEL}.plist"
397
+ plist = {
398
+ "Label": LABEL,
399
+ "ProgramArguments": [
400
+ str(node),
401
+ str(project / "dist" / "src" / "index.js"),
402
+ "--config",
403
+ str(config_path),
404
+ ],
405
+ "WorkingDirectory": str(project),
406
+ "RunAtLoad": True,
407
+ "KeepAlive": True,
408
+ "ThrottleInterval": 10,
409
+ "ProcessType": "Background",
410
+ "Umask": 0o077,
411
+ "StandardOutPath": str(logs / "pi-leo-bridge.log"),
412
+ "StandardErrorPath": str(logs / "pi-leo-bridge.error.log"),
413
+ "EnvironmentVariables": {
414
+ "HOME": str(home),
415
+ "NODE_ENV": "production",
416
+ "PATH": f"{node.parent}:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin",
417
+ },
418
+ }
419
+ plist_path.parent.mkdir(parents=True, exist_ok=True)
420
+ payload = plistlib.dumps(plist, fmt=plistlib.FMT_XML, sort_keys=True)
421
+ atomic_bytes(plist_path, payload, 0o600)
422
+ return config_path, plist_path
423
+
424
+
425
+ def main() -> None:
426
+ parser = argparse.ArgumentParser()
427
+ parser.add_argument("--project", type=Path, required=True)
428
+ parser.add_argument("--node", type=Path, required=True)
429
+ parser.add_argument("--preferences", type=Path, required=True)
430
+ parser.add_argument("--app-name", default="Brave Browser")
431
+ parser.add_argument("--provider")
432
+ parser.add_argument("--model")
433
+ parser.add_argument("--display-name")
434
+ parser.add_argument("--levels", help="Comma-separated Pi thinking levels")
435
+ parser.add_argument("--primary-level")
436
+ parser.add_argument("--port", type=int)
437
+ parser.add_argument("--context-size", type=int)
438
+ parser.add_argument("--vision-support", choices=("true", "false"))
439
+ parser.add_argument("--rotate-token", action="store_true")
440
+ args = parser.parse_args()
441
+
442
+ home = Path.home().resolve()
443
+ project = args.project.resolve()
444
+ node = args.node.resolve()
445
+ preferences = args.preferences.resolve()
446
+ config_path = home / ".config" / "pi-leo-bridge" / "config.json"
447
+ existing = read_object(config_path)
448
+
449
+ provider = args.provider or str(existing.get("provider") or DEFAULT_PROVIDER)
450
+ model_id = args.model or str(existing.get("modelId") or DEFAULT_MODEL)
451
+ display_name = args.display_name or str(existing.get("displayName") or title_model(model_id))
452
+ levels = parse_levels(args.levels, existing)
453
+ primary_level = args.primary_level or str(existing.get("thinkingLevel") or "medium")
454
+ if primary_level not in levels:
455
+ primary_level = "medium" if "medium" in levels else levels[0]
456
+ port = args.port if args.port is not None else int(existing.get("port") or DEFAULT_PORT)
457
+ context_size = (
458
+ args.context_size
459
+ if args.context_size is not None
460
+ else int(existing.get("contextSize") or 100_000)
461
+ )
462
+ vision_support = (
463
+ args.vision_support == "true"
464
+ if args.vision_support is not None
465
+ else bool(existing.get("visionSupport", True))
466
+ )
467
+
468
+ if not display_name.strip() or len(display_name) > 120 or any(
469
+ ord(character) < 32 for character in display_name
470
+ ):
471
+ raise RuntimeError("Display name must be 1-120 printable characters")
472
+ if not 1024 <= port <= 65535:
473
+ raise RuntimeError("Port must be between 1024 and 65535")
474
+ if not 1_024 <= context_size <= 2_000_000:
475
+ raise RuntimeError("Context size must be between 1,024 and 2,000,000")
476
+ if not node.is_file():
477
+ raise RuntimeError(f"Node executable not found: {node}")
478
+ if not (project / "dist" / "src" / "index.js").is_file():
479
+ raise RuntimeError("Build output is missing")
480
+ if not preferences.is_file():
481
+ raise RuntimeError(f"Brave Preferences not found: {preferences}")
482
+
483
+ profiles = build_profiles(
484
+ provider,
485
+ model_id,
486
+ display_name,
487
+ levels,
488
+ primary_level,
489
+ existing,
490
+ )
491
+ token, backup, model_keys, previous_default = configure_preferences(
492
+ preferences,
493
+ port=port,
494
+ profiles=profiles,
495
+ existing_config=existing,
496
+ context_size=context_size,
497
+ vision_support=vision_support,
498
+ rotate_token=args.rotate_token,
499
+ )
500
+ config_path, plist_path = configure_runtime(
501
+ home=home,
502
+ project=project,
503
+ node=node,
504
+ token=token,
505
+ preferences=preferences,
506
+ app_name=args.app_name,
507
+ provider=provider,
508
+ model_id=model_id,
509
+ display_name=display_name,
510
+ profiles=profiles,
511
+ primary_level=primary_level,
512
+ port=port,
513
+ context_size=context_size,
514
+ vision_support=vision_support,
515
+ model_keys=model_keys,
516
+ previous_default=previous_default,
517
+ )
518
+
519
+ # Never print the capability token or endpoint URL.
520
+ print(f"Brave backup: {backup}")
521
+ print("Brave models:")
522
+ for profile in profiles:
523
+ print(f" {profile['label']}: {profile['thinking_level']}")
524
+ print(f"Bridge model: {provider}/{model_id}")
525
+ print(f"Bridge config: {config_path}")
526
+ print(f"LaunchAgent: {plist_path}")
527
+
528
+
529
+ if __name__ == "__main__":
530
+ main()