deepparallel 0.4.2__tar.gz → 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. {deepparallel-0.4.2 → deepparallel-0.5.0}/PKG-INFO +1 -1
  2. {deepparallel-0.4.2 → deepparallel-0.5.0}/deepparallel/__init__.py +1 -1
  3. {deepparallel-0.4.2 → deepparallel-0.5.0}/deepparallel/backend.py +232 -5
  4. deepparallel-0.5.0/deepparallel/branding.py +396 -0
  5. {deepparallel-0.4.2 → deepparallel-0.5.0}/deepparallel/cli.py +19 -2
  6. {deepparallel-0.4.2 → deepparallel-0.5.0}/deepparallel/config.py +35 -1
  7. deepparallel-0.5.0/deepparallel/crowe_id.py +75 -0
  8. deepparallel-0.5.0/deepparallel/dsml.py +129 -0
  9. {deepparallel-0.4.2 → deepparallel-0.5.0}/deepparallel/licensing.py +1 -1
  10. {deepparallel-0.4.2 → deepparallel-0.5.0}/deepparallel/renderer.py +28 -6
  11. deepparallel-0.5.0/deepparallel/research/provider.py +125 -0
  12. deepparallel-0.5.0/deepparallel/routing.example.json +32 -0
  13. deepparallel-0.5.0/deepparallel/routing.py +135 -0
  14. deepparallel-0.5.0/deepparallel/serve.py +354 -0
  15. deepparallel-0.5.0/deepparallel/system_prompt.txt +7 -0
  16. {deepparallel-0.4.2 → deepparallel-0.5.0}/deepparallel/tools/__init__.py +1 -0
  17. deepparallel-0.5.0/deepparallel/tools/mcp.py +280 -0
  18. deepparallel-0.5.0/deepparallel/userinput.py +135 -0
  19. {deepparallel-0.4.2 → deepparallel-0.5.0}/deepparallel.egg-info/PKG-INFO +1 -1
  20. {deepparallel-0.4.2 → deepparallel-0.5.0}/deepparallel.egg-info/SOURCES.txt +21 -1
  21. {deepparallel-0.4.2 → deepparallel-0.5.0}/pyproject.toml +2 -2
  22. {deepparallel-0.4.2 → deepparallel-0.5.0}/tests/test_backend.py +60 -0
  23. deepparallel-0.5.0/tests/test_crowe_backend.py +79 -0
  24. deepparallel-0.5.0/tests/test_crowe_gateway_backend.py +99 -0
  25. deepparallel-0.5.0/tests/test_crowe_id_auth.py +112 -0
  26. deepparallel-0.5.0/tests/test_crowe_payment_required.py +70 -0
  27. deepparallel-0.5.0/tests/test_dsml.py +104 -0
  28. {deepparallel-0.4.2 → deepparallel-0.5.0}/tests/test_renderer.py +64 -2
  29. deepparallel-0.5.0/tests/test_research_provider.py +96 -0
  30. deepparallel-0.5.0/tests/test_routing.py +169 -0
  31. deepparallel-0.5.0/tests/test_serve.py +259 -0
  32. deepparallel-0.5.0/tests/test_spinner_color.py +45 -0
  33. deepparallel-0.5.0/tests/test_tools_mcp.py +47 -0
  34. deepparallel-0.5.0/tests/test_userinput.py +40 -0
  35. deepparallel-0.5.0/tests/test_userinput_paste.py +71 -0
  36. deepparallel-0.4.2/deepparallel/branding.py +0 -211
  37. deepparallel-0.4.2/deepparallel/system_prompt.txt +0 -4
  38. {deepparallel-0.4.2 → deepparallel-0.5.0}/README.md +0 -0
  39. {deepparallel-0.4.2 → deepparallel-0.5.0}/deepparallel/agent.py +0 -0
  40. {deepparallel-0.4.2 → deepparallel-0.5.0}/deepparallel/fusion.py +0 -0
  41. {deepparallel-0.4.2 → deepparallel-0.5.0}/deepparallel/registry.json +0 -0
  42. {deepparallel-0.4.2 → deepparallel-0.5.0}/deepparallel/research/__init__.py +0 -0
  43. {deepparallel-0.4.2 → deepparallel-0.5.0}/deepparallel/research/conduit.py +0 -0
  44. {deepparallel-0.4.2 → deepparallel-0.5.0}/deepparallel/supply_chain.py +0 -0
  45. {deepparallel-0.4.2 → deepparallel-0.5.0}/deepparallel/tools/codeast.py +0 -0
  46. {deepparallel-0.4.2 → deepparallel-0.5.0}/deepparallel/tools/edit.py +0 -0
  47. {deepparallel-0.4.2 → deepparallel-0.5.0}/deepparallel/tools/files.py +0 -0
  48. {deepparallel-0.4.2 → deepparallel-0.5.0}/deepparallel/tools/registry.py +0 -0
  49. {deepparallel-0.4.2 → deepparallel-0.5.0}/deepparallel/tools/sandbox.py +0 -0
  50. {deepparallel-0.4.2 → deepparallel-0.5.0}/deepparallel/tools/search.py +0 -0
  51. {deepparallel-0.4.2 → deepparallel-0.5.0}/deepparallel/tools/shell.py +0 -0
  52. {deepparallel-0.4.2 → deepparallel-0.5.0}/deepparallel/tools/vision.py +0 -0
  53. {deepparallel-0.4.2 → deepparallel-0.5.0}/deepparallel/tools/web.py +0 -0
  54. {deepparallel-0.4.2 → deepparallel-0.5.0}/deepparallel.egg-info/dependency_links.txt +0 -0
  55. {deepparallel-0.4.2 → deepparallel-0.5.0}/deepparallel.egg-info/entry_points.txt +0 -0
  56. {deepparallel-0.4.2 → deepparallel-0.5.0}/deepparallel.egg-info/requires.txt +0 -0
  57. {deepparallel-0.4.2 → deepparallel-0.5.0}/deepparallel.egg-info/top_level.txt +0 -0
  58. {deepparallel-0.4.2 → deepparallel-0.5.0}/setup.cfg +0 -0
  59. {deepparallel-0.4.2 → deepparallel-0.5.0}/tests/test_agent.py +0 -0
  60. {deepparallel-0.4.2 → deepparallel-0.5.0}/tests/test_backend_chat.py +0 -0
  61. {deepparallel-0.4.2 → deepparallel-0.5.0}/tests/test_backend_stream.py +0 -0
  62. {deepparallel-0.4.2 → deepparallel-0.5.0}/tests/test_branding.py +0 -0
  63. {deepparallel-0.4.2 → deepparallel-0.5.0}/tests/test_cli.py +0 -0
  64. {deepparallel-0.4.2 → deepparallel-0.5.0}/tests/test_config.py +0 -0
  65. {deepparallel-0.4.2 → deepparallel-0.5.0}/tests/test_fusion.py +0 -0
  66. {deepparallel-0.4.2 → deepparallel-0.5.0}/tests/test_issuer_signer.py +0 -0
  67. {deepparallel-0.4.2 → deepparallel-0.5.0}/tests/test_licensing.py +0 -0
  68. {deepparallel-0.4.2 → deepparallel-0.5.0}/tests/test_research.py +0 -0
  69. {deepparallel-0.4.2 → deepparallel-0.5.0}/tests/test_supply_chain.py +0 -0
  70. {deepparallel-0.4.2 → deepparallel-0.5.0}/tests/test_tool_registry.py +0 -0
  71. {deepparallel-0.4.2 → deepparallel-0.5.0}/tests/test_tools_codeast.py +0 -0
  72. {deepparallel-0.4.2 → deepparallel-0.5.0}/tests/test_tools_edit.py +0 -0
  73. {deepparallel-0.4.2 → deepparallel-0.5.0}/tests/test_tools_files.py +0 -0
  74. {deepparallel-0.4.2 → deepparallel-0.5.0}/tests/test_tools_sandbox.py +0 -0
  75. {deepparallel-0.4.2 → deepparallel-0.5.0}/tests/test_tools_search.py +0 -0
  76. {deepparallel-0.4.2 → deepparallel-0.5.0}/tests/test_tools_shell.py +0 -0
  77. {deepparallel-0.4.2 → deepparallel-0.5.0}/tests/test_tools_vision.py +0 -0
  78. {deepparallel-0.4.2 → deepparallel-0.5.0}/tests/test_tools_web.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deepparallel
3
- Version: 0.4.2
3
+ Version: 0.5.0
4
4
  Summary: DeepParallel - a multi-model agentic coding CLI with cross-model Guardian review, served via Crowe Logic.
5
5
  Author-email: Michael Crowe <michael@crowelogic.com>
6
6
  License: Apache-2.0
@@ -1,3 +1,3 @@
1
1
  """DeepParallel CLI package."""
2
2
 
3
- __version__ = "0.4.2"
3
+ __version__ = "0.5.0"
@@ -16,9 +16,14 @@ from urllib.parse import urlparse
16
16
 
17
17
  import httpx
18
18
 
19
+ from . import crowe_id
20
+
19
21
  Chunk = tuple[str, str] # (channel, text)
20
22
 
21
23
  _STREAM_TIMEOUT = httpx.Timeout(120.0, connect=10.0)
24
+ # Modal scale-to-zero cold start can take 2-3 min before the first byte, so the
25
+ # read timeout must be generous; connect stays short.
26
+ _MODAL_TIMEOUT = httpx.Timeout(600.0, connect=15.0)
22
27
  _CHECK_TIMEOUT = 4.0
23
28
 
24
29
 
@@ -210,17 +215,24 @@ class AzureBackend:
210
215
  class FoundryBackend:
211
216
  label = "Foundry control plane"
212
217
 
213
- def __init__(self, base_url: str, api_key: str, model: str):
218
+ def __init__(self, base_url: str, api_key: str, model: str, token_provider=None):
214
219
  self._base_url = (base_url or "").rstrip("/")
215
220
  self._api_key = api_key or ""
216
221
  self._model = model
222
+ # Optional callable returning a fresh bearer (e.g. a Crowe ID
223
+ # client_credentials token). When set it takes precedence over the static
224
+ # api_key, so the gateway sees a sovereign agent identity per request.
225
+ self._token_provider = token_provider
217
226
 
218
227
  @property
219
228
  def _url(self) -> str:
220
229
  return f"{self._base_url}/v1/chat/completions"
221
230
 
231
+ def _bearer(self) -> str:
232
+ return self._token_provider() if self._token_provider else self._api_key
233
+
222
234
  def check(self) -> tuple[bool, str]:
223
- if not self._base_url or not self._api_key:
235
+ if not self._base_url or not (self._api_key or self._token_provider):
224
236
  return False, "Foundry base URL or API key not configured."
225
237
  try:
226
238
  httpx.get(_host(self._base_url), timeout=_CHECK_TIMEOUT)
@@ -237,7 +249,7 @@ class FoundryBackend:
237
249
  "max_tokens": max_tokens,
238
250
  }
239
251
  headers = {
240
- "authorization": f"Bearer {self._api_key}",
252
+ "authorization": f"Bearer {self._bearer()}",
241
253
  "content-type": "application/json",
242
254
  }
243
255
  with httpx.stream(
@@ -257,7 +269,7 @@ class FoundryBackend:
257
269
  if tools:
258
270
  payload["tools"] = tools
259
271
  headers = {
260
- "authorization": f"Bearer {self._api_key}",
272
+ "authorization": f"Bearer {self._bearer()}",
261
273
  "content-type": "application/json",
262
274
  }
263
275
  r = httpx.post(self._url, json=payload, headers=headers, timeout=_STREAM_TIMEOUT)
@@ -275,7 +287,7 @@ class FoundryBackend:
275
287
  if tools:
276
288
  payload["tools"] = tools
277
289
  headers = {
278
- "authorization": f"Bearer {self._api_key}",
290
+ "authorization": f"Bearer {self._bearer()}",
279
291
  "content-type": "application/json",
280
292
  }
281
293
  with httpx.stream(
@@ -285,8 +297,208 @@ class FoundryBackend:
285
297
  return (yield from parse_sse_stream(r.iter_lines()))
286
298
 
287
299
 
300
+ class PaymentRequired(Exception):
301
+ """The agent's wallet can't cover the call — the x402 rail returned HTTP 402.
302
+
303
+ Carries the parsed x402 envelope so callers can see the price + accepted schemes
304
+ and decide how to fund (top-up) or pay (X-PAYMENT)."""
305
+
306
+ def __init__(self, envelope: dict):
307
+ self.envelope = envelope or {}
308
+ accepts = self.envelope.get("accepts", [])
309
+ price = accepts[0].get("maxAmountRequired", "?") if accepts else "?"
310
+ schemes = ", ".join(a.get("scheme", "") for a in accepts) or "?"
311
+ super().__init__(
312
+ f"payment required: {price} micro-USD via [{schemes}] — fund the agent wallet"
313
+ )
314
+
315
+
316
+ class CroweGatewayBackend:
317
+ """Foundry gateway PAID agent rail (/api/agent/v1/chat by default), Crowe ID auth.
318
+
319
+ Targets the x402 agent endpoint that debits the agent's wallet per call (override
320
+ with CROWE_AGENT_RESOURCE). Native GatewayResponse shape, not the OpenAI-compat
321
+ /v1 path (which 404s there); non-streaming with no server-side tool-calls, so we
322
+ adapt it to DeepParallel's streaming seam by yielding the full completion as a
323
+ single content chunk. The bearer is a Crowe ID client_credentials token from
324
+ ``token_provider`` (the agent's sovereign identity).
325
+ """
326
+
327
+ label = "Crowe ID agent (Foundry gateway)"
328
+
329
+ def __init__(self, base_url: str, model: str, token_provider=None):
330
+ self._base_url = (base_url or "").rstrip("/")
331
+ self._model = model
332
+ self._token_provider = token_provider
333
+
334
+ @property
335
+ def _url(self) -> str:
336
+ # The PAID x402 agent rail (debits the agent's wallet). Overridable via
337
+ # CROWE_AGENT_RESOURCE for the legacy non-paid /api/gateway/chat path.
338
+ import os
339
+
340
+ resource = os.environ.get("CROWE_AGENT_RESOURCE", "/api/agent/v1/chat")
341
+ return f"{self._base_url}{resource}"
342
+
343
+ def _bearer(self) -> str:
344
+ return self._token_provider() if self._token_provider else ""
345
+
346
+ def _headers(self) -> dict:
347
+ return {
348
+ "authorization": f"Bearer {self._bearer()}",
349
+ "content-type": "application/json",
350
+ }
351
+
352
+ def check(self) -> tuple[bool, str]:
353
+ if not self._base_url or not self._token_provider:
354
+ return False, "Crowe gateway URL or Crowe ID credentials not configured."
355
+ try:
356
+ httpx.get(_host(self._base_url), timeout=_CHECK_TIMEOUT)
357
+ except Exception as e: # noqa: BLE001 - reachability probe
358
+ return False, f"Crowe gateway unreachable ({e.__class__.__name__})"
359
+ return True, f"Crowe ID @ {_host(self._base_url)}"
360
+
361
+ def _complete(self, messages, temperature, max_tokens) -> str:
362
+ payload = {
363
+ "model": self._model,
364
+ "messages": messages,
365
+ "temperature": temperature,
366
+ "max_tokens": max_tokens,
367
+ }
368
+ r = httpx.post(self._url, json=payload, headers=self._headers(), timeout=_STREAM_TIMEOUT)
369
+ if r.status_code == 402:
370
+ # x402 payment-required: surface the machine-readable envelope as an
371
+ # actionable error (price + schemes) rather than a raw HTTP error.
372
+ try:
373
+ envelope = r.json()
374
+ except Exception: # noqa: BLE001 - tolerate a non-JSON 402 body
375
+ envelope = {}
376
+ raise PaymentRequired(envelope)
377
+ r.raise_for_status()
378
+ return r.json().get("content", "")
379
+
380
+ def stream_chat(self, messages, temperature, max_tokens):
381
+ yield ("content", self._complete(messages, temperature, max_tokens))
382
+
383
+ def chat(self, messages, tools, temperature, max_tokens) -> dict:
384
+ # Native gateway endpoint has no tool-calling; tools are ignored.
385
+ return {"role": "assistant", "content": self._complete(messages, temperature, max_tokens)}
386
+
387
+ def stream_chat_tools(self, messages, tools, temperature, max_tokens):
388
+ # No server-side tool-calls; yields content and returns the final message
389
+ # (matches the FoundryBackend generator-return contract used by the agent loop).
390
+ content = self._complete(messages, temperature, max_tokens)
391
+ yield ("content", content)
392
+ return {"role": "assistant", "content": content}
393
+
394
+
395
+ class ModalBackend:
396
+ """Gemma 4 Mycelium served on a Modal scale-to-zero GPU (the free base tier).
397
+
398
+ OpenAI-compatible /v1/chat/completions, but the Modal web endpoint requires
399
+ proxy-auth headers (Modal-Key / Modal-Secret) on every request — which is why
400
+ the gateway, not OWUI, must own this connection."""
401
+
402
+ label = "Modal (Mycelium)"
403
+
404
+ def __init__(self, endpoint: str, key: str, secret: str, model: str):
405
+ self._base_url = (endpoint or "").rstrip("/")
406
+ self._key = key or ""
407
+ self._secret = secret or ""
408
+ self._model = model
409
+
410
+ @property
411
+ def _url(self) -> str:
412
+ return f"{self._base_url}/v1/chat/completions"
413
+
414
+ def _headers(self) -> dict:
415
+ return {
416
+ "Modal-Key": self._key,
417
+ "Modal-Secret": self._secret,
418
+ "content-type": "application/json",
419
+ }
420
+
421
+ def check(self) -> tuple[bool, str]:
422
+ if not self._base_url or not self._key or not self._secret:
423
+ return False, "Modal Mycelium endpoint or proxy-auth token not configured."
424
+ try:
425
+ httpx.get(_host(self._base_url), timeout=_CHECK_TIMEOUT)
426
+ except Exception as e: # noqa: BLE001 - reachability probe
427
+ return False, f"Modal endpoint unreachable ({e.__class__.__name__})"
428
+ return True, f"Modal @ {_host(self._base_url)}"
429
+
430
+ def stream_chat(self, messages, temperature, max_tokens):
431
+ payload = {
432
+ "model": self._model,
433
+ "messages": messages,
434
+ "stream": True,
435
+ "temperature": temperature,
436
+ "max_tokens": max_tokens,
437
+ }
438
+ with httpx.stream(
439
+ "POST", self._url, json=payload, headers=self._headers(), timeout=_MODAL_TIMEOUT
440
+ ) as r:
441
+ r.raise_for_status()
442
+ yield from parse_sse_lines(r.iter_lines())
443
+
444
+ def chat(self, messages, tools, temperature, max_tokens) -> dict:
445
+ payload = {
446
+ "model": self._model,
447
+ "messages": messages,
448
+ "stream": False,
449
+ "temperature": temperature,
450
+ "max_tokens": max_tokens,
451
+ }
452
+ if tools:
453
+ payload["tools"] = tools
454
+ r = httpx.post(self._url, json=payload, headers=self._headers(), timeout=_MODAL_TIMEOUT)
455
+ r.raise_for_status()
456
+ return _message_from_choice(r.json()["choices"][0])
457
+
458
+ def stream_chat_tools(self, messages, tools, temperature, max_tokens):
459
+ payload = {
460
+ "model": self._model,
461
+ "messages": messages,
462
+ "stream": True,
463
+ "temperature": temperature,
464
+ "max_tokens": max_tokens,
465
+ }
466
+ if tools:
467
+ payload["tools"] = tools
468
+ with httpx.stream(
469
+ "POST", self._url, json=payload, headers=self._headers(), timeout=_MODAL_TIMEOUT
470
+ ) as r:
471
+ r.raise_for_status()
472
+ return (yield from parse_sse_stream(r.iter_lines()))
473
+
474
+
475
+ _crowe_providers: dict[tuple, crowe_id.CroweIDTokenProvider] = {}
476
+
477
+
478
+ def _crowe_token_provider(settings) -> crowe_id.CroweIDTokenProvider:
479
+ """One memoized token provider per (issuer, client_id) so fusion's many
480
+ per-deployment backends share a single cached Crowe ID token."""
481
+ key = (settings.crowe_id_issuer, settings.crowe_id_client_id)
482
+ provider = _crowe_providers.get(key)
483
+ if provider is None:
484
+ provider = crowe_id.CroweIDTokenProvider(
485
+ settings.crowe_id_issuer,
486
+ settings.crowe_id_client_id or "",
487
+ settings.crowe_id_client_secret or "",
488
+ audience=settings.crowe_id_audience,
489
+ )
490
+ _crowe_providers[key] = provider
491
+ return provider
492
+
493
+
288
494
  def resolve_backend(settings) -> Backend:
289
495
  """Factory keyed on settings.backend."""
496
+ if settings.backend == "crowe":
497
+ return CroweGatewayBackend(
498
+ settings.gateway_url or "",
499
+ settings.foundry_model,
500
+ token_provider=_crowe_token_provider(settings),
501
+ )
290
502
  if settings.backend == "foundry":
291
503
  return FoundryBackend(
292
504
  settings.foundry_base_url or "",
@@ -305,7 +517,22 @@ def backend_for_deployment(settings, deployment: str) -> Backend:
305
517
  """Build a backend targeting a specific deployment/model (for fusion).
306
518
 
307
519
  Uses the same transport as the active backend, just a different model id.
520
+ The Modal-served Mycelium model is the exception: it routes to its own
521
+ endpoint with proxy-auth headers, regardless of the active backend.
308
522
  """
523
+ if settings.mycelium_endpoint and deployment == settings.mycelium_model:
524
+ return ModalBackend(
525
+ settings.mycelium_endpoint,
526
+ settings.mycelium_key or "",
527
+ settings.mycelium_secret or "",
528
+ deployment,
529
+ )
530
+ if settings.backend == "crowe":
531
+ return CroweGatewayBackend(
532
+ settings.gateway_url or "",
533
+ deployment,
534
+ token_provider=_crowe_token_provider(settings),
535
+ )
309
536
  if settings.backend == "foundry":
310
537
  return FoundryBackend(
311
538
  settings.foundry_base_url or "", settings.foundry_api_key or "", deployment