screamingface 0.1.0__py3-none-any.whl

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 (84) hide show
  1. screamingface/__init__.py +109 -0
  2. screamingface/_candidate_policy.py +80 -0
  3. screamingface/_client_connections.py +123 -0
  4. screamingface/_core/__init__.py +3 -0
  5. screamingface/_core/ports.py +59 -0
  6. screamingface/_core/wire.py +36 -0
  7. screamingface/_default_client.py +181 -0
  8. screamingface/_engine/__init__.py +3 -0
  9. screamingface/_engine/access_contract.py +206 -0
  10. screamingface/_engine/auth.py +465 -0
  11. screamingface/_engine/auth_base.py +32 -0
  12. screamingface/_engine/benchmark.py +158 -0
  13. screamingface/_engine/catalog.py +288 -0
  14. screamingface/_engine/catalog_contract.py +145 -0
  15. screamingface/_engine/connections.py +396 -0
  16. screamingface/_engine/contract.py +536 -0
  17. screamingface/_engine/model_parameters.py +233 -0
  18. screamingface/_engine/run_lifecycle.py +70 -0
  19. screamingface/_engine/transport.py +662 -0
  20. screamingface/_evaluation/__init__.py +3 -0
  21. screamingface/_evaluation/benchmark.py +76 -0
  22. screamingface/_evaluation/candidate.py +411 -0
  23. screamingface/_evaluation/compilation.py +71 -0
  24. screamingface/_evaluation/linking.py +100 -0
  25. screamingface/_evaluation/model.py +330 -0
  26. screamingface/_evaluation/model_parameters.py +88 -0
  27. screamingface/_evaluation/policy.py +32 -0
  28. screamingface/_evaluation/progress.py +142 -0
  29. screamingface/_evaluation/results.py +482 -0
  30. screamingface/_evaluation/runner.py +352 -0
  31. screamingface/_evaluation/topology.py +188 -0
  32. screamingface/_evaluation/url4.py +322 -0
  33. screamingface/_immutable_json.py +63 -0
  34. screamingface/_named_values.py +88 -0
  35. screamingface/_operation_projection.py +54 -0
  36. screamingface/_report_primitives.py +198 -0
  37. screamingface/_scoreboard/__init__.py +3 -0
  38. screamingface/_scoreboard/leaderboards.py +531 -0
  39. screamingface/_ui/__init__.py +3 -0
  40. screamingface/_ui/assets/provider_icons/NOTICE.md +36 -0
  41. screamingface/_ui/assets/provider_icons/anthropic/icon-dark.svg +1 -0
  42. screamingface/_ui/assets/provider_icons/anthropic/icon.svg +1 -0
  43. screamingface/_ui/assets/provider_icons/gemini-cli/icon.svg +1 -0
  44. screamingface/_ui/assets/provider_icons/huggingface/icon.svg +8 -0
  45. screamingface/_ui/assets/provider_icons/ollama/icon-dark.svg +7 -0
  46. screamingface/_ui/assets/provider_icons/ollama/icon.svg +7 -0
  47. screamingface/_ui/assets/provider_icons/openrouter/icon-dark.svg +21 -0
  48. screamingface/_ui/assets/provider_icons/openrouter/icon.svg +21 -0
  49. screamingface/_ui/card_style.py +95 -0
  50. screamingface/_ui/cards.py +198 -0
  51. screamingface/_ui/catalog.py +133 -0
  52. screamingface/_ui/connection_state.py +57 -0
  53. screamingface/_ui/connection_view.py +468 -0
  54. screamingface/_ui/connections.py +446 -0
  55. screamingface/_ui/evaluation_state.py +258 -0
  56. screamingface/_ui/evaluation_view.py +312 -0
  57. screamingface/_ui/leaderboard_style.py +142 -0
  58. screamingface/_ui/leaderboard_view.py +251 -0
  59. screamingface/_ui/provider_icons.py +48 -0
  60. screamingface/_ui/report_view.py +805 -0
  61. screamingface/_ui/style.py +76 -0
  62. screamingface/benchmarks.py +21 -0
  63. screamingface/case_result.py +492 -0
  64. screamingface/client.py +589 -0
  65. screamingface/connections.py +243 -0
  66. screamingface/discovery.py +417 -0
  67. screamingface/errors.py +187 -0
  68. screamingface/events.py +269 -0
  69. screamingface/fusion.py +67 -0
  70. screamingface/leaderboard.py +276 -0
  71. screamingface/leaderboards.py +35 -0
  72. screamingface/model.py +68 -0
  73. screamingface/models.py +21 -0
  74. screamingface/operation.py +104 -0
  75. screamingface/pipeline.py +72 -0
  76. screamingface/py.typed +1 -0
  77. screamingface/recipe.py +78 -0
  78. screamingface/report.py +554 -0
  79. screamingface/url4.py +328 -0
  80. screamingface/warnings.py +12 -0
  81. screamingface-0.1.0.dist-info/METADATA +512 -0
  82. screamingface-0.1.0.dist-info/RECORD +84 -0
  83. screamingface-0.1.0.dist-info/WHEEL +4 -0
  84. screamingface-0.1.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,109 @@
1
+ """ScreamingFace — evaluate composable Candidate Recipes on research Benchmarks."""
2
+
3
+ from screamingface import benchmarks, connections, events, leaderboards, models
4
+ from screamingface._default_client import close, configure, connect, disconnect, evaluate
5
+ from screamingface._ui.connections import ConnectionPanel
6
+ from screamingface.client import AsyncClient, Client
7
+ from screamingface.connections import AsyncOAuthFlow, Connection, OAuthFlow
8
+ from screamingface.discovery import (
9
+ Benchmark,
10
+ BenchmarkInfo,
11
+ ModelCapability,
12
+ ModelDetails,
13
+ ModelInfo,
14
+ ModelParameter,
15
+ ModelParameterSchema,
16
+ )
17
+ from screamingface.errors import (
18
+ AuthenticationError,
19
+ EngineUnavailableError,
20
+ ExecutionError,
21
+ LeaderboardError,
22
+ PlanningError,
23
+ ProviderConnectionError,
24
+ ScreamingFaceError,
25
+ )
26
+ from screamingface.events import Event
27
+ from screamingface.fusion import Fusion
28
+ from screamingface.leaderboard import (
29
+ Leaderboard,
30
+ LeaderboardBaseline,
31
+ LeaderboardEntry,
32
+ LeaderboardInfo,
33
+ LeaderboardScore,
34
+ )
35
+ from screamingface.model import Model
36
+ from screamingface.operation import OperationInfo
37
+ from screamingface.pipeline import Pipeline
38
+ from screamingface.recipe import Recipe
39
+ from screamingface.report import (
40
+ CandidateResult,
41
+ CaseGrade,
42
+ CaseResult,
43
+ Check,
44
+ Evidence,
45
+ EvidenceProducer,
46
+ Failure,
47
+ MemberResult,
48
+ Report,
49
+ Usage,
50
+ )
51
+ from screamingface.url4 import Url4
52
+ from screamingface.warnings import CoverageWarning, EvaluationWarning
53
+
54
+ __all__ = [
55
+ "AsyncClient",
56
+ "AuthenticationError",
57
+ "Benchmark",
58
+ "BenchmarkInfo",
59
+ "CaseGrade",
60
+ "CaseResult",
61
+ "CandidateResult",
62
+ "Check",
63
+ "Client",
64
+ "Connection",
65
+ "ConnectionPanel",
66
+ "AsyncOAuthFlow",
67
+ "CoverageWarning",
68
+ "close",
69
+ "configure",
70
+ "connect",
71
+ "connections",
72
+ "disconnect",
73
+ "Evidence",
74
+ "EvidenceProducer",
75
+ "Event",
76
+ "EngineUnavailableError",
77
+ "ExecutionError",
78
+ "EvaluationWarning",
79
+ "evaluate",
80
+ "Failure",
81
+ "Fusion",
82
+ "Leaderboard",
83
+ "LeaderboardBaseline",
84
+ "LeaderboardEntry",
85
+ "LeaderboardError",
86
+ "LeaderboardInfo",
87
+ "LeaderboardScore",
88
+ "MemberResult",
89
+ "Model",
90
+ "ModelCapability",
91
+ "ModelDetails",
92
+ "ModelInfo",
93
+ "ModelParameter",
94
+ "ModelParameterSchema",
95
+ "OperationInfo",
96
+ "OAuthFlow",
97
+ "PlanningError",
98
+ "Pipeline",
99
+ "ProviderConnectionError",
100
+ "Recipe",
101
+ "Report",
102
+ "ScreamingFaceError",
103
+ "Usage",
104
+ "Url4",
105
+ "benchmarks",
106
+ "events",
107
+ "leaderboards",
108
+ "models",
109
+ ]
@@ -0,0 +1,80 @@
1
+ """Validated immutable Candidate generation overrides."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from collections.abc import Mapping
7
+ from types import MappingProxyType
8
+
9
+ type GenerationScalar = str | int | float | bool
10
+ type GenerationParams = Mapping[str, GenerationScalar]
11
+
12
+ _EXECUTION_OWNED_PARAMS = frozenset(
13
+ {
14
+ "input",
15
+ "messages",
16
+ "model",
17
+ "plugins",
18
+ "provider",
19
+ "q",
20
+ "stream",
21
+ "tool_choice",
22
+ "tools",
23
+ "web_search",
24
+ "web_search_exclude",
25
+ "web_search_excluded_domains",
26
+ "web_search_policy",
27
+ }
28
+ )
29
+ """Transport, routing, tool, and Benchmark protocol fields users cannot override."""
30
+
31
+
32
+ def prompt(value: object | None, label: str) -> str | None:
33
+ if value is None:
34
+ return None
35
+ if not isinstance(value, str):
36
+ raise TypeError(f"{label} must be a string or None")
37
+ if not value.strip():
38
+ raise ValueError(f"{label} must not be empty")
39
+ return value
40
+
41
+
42
+ def params(value: object | None, label: str) -> GenerationParams:
43
+ if value is None:
44
+ return MappingProxyType({})
45
+ if not isinstance(value, Mapping):
46
+ raise TypeError(f"{label} must be a mapping or None")
47
+ selected: dict[str, GenerationScalar] = {}
48
+ for name, item in value.items():
49
+ selected_name = _parameter_name(name, label)
50
+ selected[selected_name] = _parameter_value(selected_name, item, label)
51
+ return MappingProxyType(selected)
52
+
53
+
54
+ def _parameter_name(value: object, label: str) -> str:
55
+ if not isinstance(value, str) or not value.strip():
56
+ raise ValueError(f"{label} names must be non-empty strings")
57
+ if not value.isascii() or not all(
58
+ character.isalnum() or character in "._" for character in value
59
+ ):
60
+ raise ValueError(f"{label} parameter {value!r} cannot be encoded")
61
+ if value in _EXECUTION_OWNED_PARAMS:
62
+ raise ValueError(
63
+ f"{label} parameter {value!r} is reserved for Benchmark and Engine execution"
64
+ )
65
+ return value
66
+
67
+
68
+ def _parameter_value(name: str, value: object, label: str) -> GenerationScalar:
69
+ if not isinstance(value, str | int | float | bool):
70
+ raise TypeError(f"{label} value {name!r} must be a scalar")
71
+ if isinstance(value, float) and not math.isfinite(value):
72
+ raise ValueError(f"{label} value {name!r} must be finite")
73
+ if isinstance(value, str) and any(
74
+ character in "';(),&" or ord(character) < 32 or ord(character) == 127 for character in value
75
+ ):
76
+ raise ValueError(f"{label} value {name!r} cannot be encoded")
77
+ return value
78
+
79
+
80
+ __all__: list[str] = []
@@ -0,0 +1,123 @@
1
+ """Shared Client origin, auth-listener, and provider-connection support."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import threading
6
+ from collections.abc import Callable
7
+ from typing import TYPE_CHECKING, Any, Literal
8
+ from urllib.parse import urlsplit, urlunsplit
9
+
10
+ from screamingface.errors import ProviderConnectionError
11
+
12
+ if TYPE_CHECKING:
13
+ from screamingface._engine.connections import AsyncConnections, Connections
14
+ from screamingface.connections import AsyncOAuthFlow, Connection, OAuthFlow
15
+
16
+
17
+ class _AuthListeners:
18
+ """Thread-safe observer set shared by the sync and async Client facades."""
19
+
20
+ def __init__(self) -> None:
21
+ self._lock = threading.Lock()
22
+ self._callbacks: set[Callable[[], None]] = set()
23
+
24
+ def subscribe(self, callback: Callable[[], None]) -> Callable[[], None]:
25
+ with self._lock:
26
+ self._callbacks.add(callback)
27
+
28
+ def unsubscribe() -> None:
29
+ with self._lock:
30
+ self._callbacks.discard(callback)
31
+
32
+ return unsubscribe
33
+
34
+ def notify(self) -> None:
35
+ with self._lock:
36
+ callbacks = tuple(self._callbacks)
37
+ for callback in callbacks:
38
+ callback()
39
+
40
+
41
+ def _engine_origin(value: Any) -> str:
42
+ return _http_origin(value, "engine_url")
43
+
44
+
45
+ def _scoreboard_origin(value: Any) -> str:
46
+ return _http_origin(value, "scoreboard_url")
47
+
48
+
49
+ def _http_origin(value: Any, name: str) -> str:
50
+ if not isinstance(value, str) or not value.strip():
51
+ raise ValueError(f"{name} must be an HTTP(S) origin")
52
+ parts = urlsplit(value.strip())
53
+ if (
54
+ parts.scheme not in {"http", "https"}
55
+ or not parts.netloc
56
+ or parts.path not in {"", "/"}
57
+ or parts.query
58
+ or parts.fragment
59
+ or parts.username is not None
60
+ or parts.password is not None
61
+ ):
62
+ raise ValueError(f"{name} must be an HTTP(S) origin without credentials or a path")
63
+ return urlunsplit((parts.scheme, parts.netloc, "", "", ""))
64
+
65
+
66
+ def _require_secure_connection_origin(engine_url: str) -> None:
67
+ parts = urlsplit(engine_url)
68
+ if parts.scheme == "https" or (
69
+ parts.scheme == "http" and parts.hostname in {"localhost", "127.0.0.1", "::1"}
70
+ ):
71
+ return
72
+ raise ProviderConnectionError(
73
+ "Provider connection operations require HTTPS outside a loopback SF Engine",
74
+ code="secure_transport_required",
75
+ permanent=True,
76
+ )
77
+
78
+
79
+ def _connect_sync(
80
+ connections: Connections,
81
+ provider: str,
82
+ api_key: str | None,
83
+ method: Literal["api_key", "oauth"] | None,
84
+ ) -> Connection | OAuthFlow:
85
+ _validate_auth_method(method)
86
+ if api_key is not None:
87
+ _require_api_key_action(method)
88
+ return connections.connect(provider, api_key)
89
+ if method == "api_key":
90
+ raise ValueError("api_key is required for API-key authentication")
91
+ if method == "oauth":
92
+ return connections.start_oauth(provider)
93
+ raise ValueError("api_key is required unless method='oauth' is selected")
94
+
95
+
96
+ async def _connect_async(
97
+ connections: AsyncConnections,
98
+ provider: str,
99
+ api_key: str | None,
100
+ method: Literal["api_key", "oauth"] | None,
101
+ ) -> Connection | AsyncOAuthFlow:
102
+ _validate_auth_method(method)
103
+ if api_key is not None:
104
+ _require_api_key_action(method)
105
+ return await connections.connect(provider, api_key)
106
+ if method == "api_key":
107
+ raise ValueError("api_key is required for API-key authentication")
108
+ if method == "oauth":
109
+ return await connections.start_oauth(provider)
110
+ raise ValueError("api_key is required unless method='oauth' is selected")
111
+
112
+
113
+ def _validate_auth_method(method: object) -> None:
114
+ if method not in {None, "api_key", "oauth"}:
115
+ raise ValueError("method must be 'api_key', 'oauth', or None")
116
+
117
+
118
+ def _require_api_key_action(method: object) -> None:
119
+ if method == "oauth":
120
+ raise ValueError("api_key cannot be combined with OAuth")
121
+
122
+
123
+ __all__: list[str] = []
@@ -0,0 +1,3 @@
1
+ """Internal domain ports shared by SDK adapters."""
2
+
3
+ __all__: list[str] = []
@@ -0,0 +1,59 @@
1
+ """Client-owned ports for SF Engine integration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Awaitable, Callable
6
+ from dataclasses import dataclass
7
+ from datetime import datetime
8
+ from typing import TYPE_CHECKING, Protocol
9
+
10
+ if TYPE_CHECKING:
11
+ from screamingface._evaluation.model import Candidate
12
+ from screamingface.events import Event
13
+ from screamingface.report import Usage
14
+
15
+ type SyncEventObserver = Callable[[Event], None]
16
+ type AsyncEventObserver = Callable[[Event], None | Awaitable[None]]
17
+
18
+
19
+ @dataclass(frozen=True, slots=True)
20
+ class _RunOutcome:
21
+ """Transport-neutral root result retained for strict Report decoding."""
22
+
23
+ run_id: str
24
+ started_at: datetime
25
+ completed_at: datetime
26
+ result_body: str
27
+ media_type: str | None
28
+ root_usage: Usage | None
29
+
30
+
31
+ class SyncRunTransport(Protocol):
32
+ """Execute one inspected Candidate through an SF Engine."""
33
+
34
+ def run(
35
+ self,
36
+ candidate: Candidate,
37
+ on_event: SyncEventObserver | None,
38
+ ) -> _RunOutcome: ...
39
+
40
+ def cancel_active(self) -> None: ...
41
+
42
+ def close(self) -> None: ...
43
+
44
+
45
+ class AsyncRunTransport(Protocol):
46
+ """Asynchronous counterpart of :class:`SyncRunTransport`."""
47
+
48
+ async def run(
49
+ self,
50
+ candidate: Candidate,
51
+ on_event: AsyncEventObserver | None,
52
+ ) -> _RunOutcome: ...
53
+
54
+ async def cancel_active(self) -> None: ...
55
+
56
+ async def close(self) -> None: ...
57
+
58
+
59
+ __all__: list[str] = []
@@ -0,0 +1,36 @@
1
+ """Small, policy-free primitives for decoding external wire values."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable, Mapping
6
+ from typing import NoReturn, cast
7
+
8
+ type _Invalid = Callable[[str], NoReturn]
9
+
10
+ # httpx request-extension key marking a request as safe to re-send after an interactive
11
+ # Cloudflare Access login.
12
+ # INVARIANT: default-deny. Replay safety is a property of the REQUEST, not of the response
13
+ # status, so a call site that forgets the marker pays one extra round trip — never an extra
14
+ # paid Run. "GET /?q=" starts billable work despite being a GET and is never marked.
15
+ # WHY here: this module has no dependencies, so marking a call site never drags the Access
16
+ # stack (and its native crypto) into an import path that must stay cheap.
17
+ _REPLAY_SAFE = "screamingface_replay_safe"
18
+
19
+
20
+ def mapping(value: object, label: str, invalid: _Invalid) -> Mapping[str, object]:
21
+ """Return a JSON-like mapping or delegate the domain-specific failure."""
22
+
23
+ if not isinstance(value, Mapping):
24
+ invalid(f"{label} must be an object")
25
+ return cast(Mapping[str, object], value)
26
+
27
+
28
+ def text(value: object, label: str, invalid: _Invalid) -> str:
29
+ """Return stripped non-blank wire text or delegate failure policy."""
30
+
31
+ if not isinstance(value, str) or not value.strip():
32
+ invalid(f"{label} must be non-blank text")
33
+ return cast(str, value).strip()
34
+
35
+
36
+ __all__: list[str] = []
@@ -0,0 +1,181 @@
1
+ """One lazily constructed synchronous module-level Client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from threading import Lock
7
+ from typing import TYPE_CHECKING, Literal, overload
8
+
9
+ from screamingface.client import DEFAULT_ENGINE_URL, DEFAULT_SCOREBOARD_URL, Client
10
+
11
+ if TYPE_CHECKING:
12
+ from collections.abc import Callable, Sequence
13
+
14
+ from screamingface._ui.connections import ConnectionPanel
15
+ from screamingface.connections import Connection, OAuthFlow
16
+ from screamingface.events import Event
17
+ from screamingface.recipe import Recipe
18
+ from screamingface.report import Report
19
+
20
+ _client: Client | None = None
21
+ _lock = Lock()
22
+
23
+
24
+ def default_client() -> Client:
25
+ """Return the process-wide Client, constructing it on first use."""
26
+
27
+ global _client
28
+ if _client is not None:
29
+ return _client
30
+ with _lock:
31
+ if _client is None:
32
+ _client = Client(
33
+ engine_url=os.environ.get("SCREAMINGFACE_ENGINE_URL", DEFAULT_ENGINE_URL),
34
+ scoreboard_url=os.environ.get(
35
+ "SCREAMINGFACE_SCOREBOARD_URL",
36
+ DEFAULT_SCOREBOARD_URL,
37
+ ),
38
+ )
39
+ return _client
40
+
41
+
42
+ def configure(
43
+ *,
44
+ engine_url: str = DEFAULT_ENGINE_URL,
45
+ scoreboard_url: str = DEFAULT_SCOREBOARD_URL,
46
+ ) -> Client:
47
+ """Replace the process-wide Client used by module-level convenience functions."""
48
+
49
+ global _client
50
+ replacement = Client(engine_url=engine_url, scoreboard_url=scoreboard_url)
51
+ with _lock:
52
+ previous = _client
53
+ _client = replacement
54
+ if previous is not None:
55
+ previous.close()
56
+ return replacement
57
+
58
+
59
+ def close() -> None:
60
+ """Close and forget the process-wide Client, if it has been created."""
61
+
62
+ global _client
63
+ with _lock:
64
+ previous = _client
65
+ _client = None
66
+ if previous is not None:
67
+ previous.close()
68
+
69
+
70
+ @overload
71
+ def evaluate(
72
+ candidates: str,
73
+ *,
74
+ benchmark: None = None,
75
+ limit: None = None,
76
+ on_event: Callable[[Event], None] | None = None,
77
+ progress: bool | None = None,
78
+ ) -> Report: ...
79
+
80
+
81
+ @overload
82
+ def evaluate(
83
+ candidates: Recipe | Sequence[Recipe],
84
+ *,
85
+ benchmark: str,
86
+ limit: int | None = None,
87
+ on_event: Callable[[Event], None] | None = None,
88
+ progress: bool | None = None,
89
+ ) -> Report: ...
90
+
91
+
92
+ def evaluate(
93
+ candidates: Recipe | Sequence[Recipe] | str,
94
+ *,
95
+ benchmark: str | None = None,
96
+ limit: int | None = None,
97
+ on_event: Callable[[Event], None] | None = None,
98
+ progress: bool | None = None,
99
+ ) -> Report:
100
+ """Evaluate Recipes or a complete URL4 through the lazy default Client."""
101
+
102
+ client = default_client()
103
+ if isinstance(candidates, str):
104
+ if benchmark is not None:
105
+ raise TypeError("benchmark must not be passed when evaluating a complete URL4")
106
+ if limit is not None:
107
+ raise TypeError("limit must not be passed when evaluating a complete URL4")
108
+ return client.evaluate(candidates, on_event=on_event, progress=progress)
109
+ if benchmark is None:
110
+ raise TypeError("benchmark is required when evaluating Recipes")
111
+ return client.evaluate(
112
+ candidates,
113
+ benchmark=benchmark,
114
+ limit=limit,
115
+ on_event=on_event,
116
+ progress=progress,
117
+ )
118
+
119
+
120
+ @overload
121
+ def connect(
122
+ provider: None = None,
123
+ *,
124
+ api_key: None = None,
125
+ method: None = None,
126
+ ) -> ConnectionPanel: ...
127
+
128
+
129
+ @overload
130
+ def connect(
131
+ provider: str,
132
+ *,
133
+ api_key: str,
134
+ method: Literal["api_key"] | None = None,
135
+ ) -> Connection: ...
136
+
137
+
138
+ @overload
139
+ def connect(
140
+ provider: str,
141
+ *,
142
+ api_key: None = None,
143
+ method: Literal["oauth"],
144
+ ) -> OAuthFlow: ...
145
+
146
+
147
+ def connect(
148
+ provider: str | None = None,
149
+ *,
150
+ api_key: str | None = None,
151
+ method: Literal["api_key", "oauth"] | None = None,
152
+ ) -> Connection | ConnectionPanel | OAuthFlow:
153
+ """Open the provider panel or connect through the lazy default Client."""
154
+
155
+ if provider is None:
156
+ if api_key is not None or method is not None:
157
+ raise TypeError("provider is required when api_key or method is supplied")
158
+ from screamingface._ui.connections import ConnectionPanel
159
+
160
+ return ConnectionPanel(default_client())
161
+ client = default_client()
162
+ if api_key is not None:
163
+ if method == "oauth":
164
+ raise ValueError("api_key cannot be combined with OAuth")
165
+ result = client.connect(provider, api_key=api_key, method=method)
166
+ elif method == "api_key":
167
+ raise ValueError("api_key is required for API-key authentication")
168
+ elif method == "oauth":
169
+ result = client.connect(provider, method="oauth")
170
+ else:
171
+ raise ValueError("api_key is required unless method='oauth' is selected")
172
+ return result
173
+
174
+
175
+ def disconnect(provider: str) -> Connection:
176
+ """Disconnect a provider through the lazy default Client."""
177
+
178
+ return default_client().disconnect(provider)
179
+
180
+
181
+ __all__ = ["close", "configure", "connect", "disconnect", "evaluate"]
@@ -0,0 +1,3 @@
1
+ """Internal Engine adapters and wire contracts."""
2
+
3
+ __all__: list[str] = []