swarph-mesh 0.1.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pierre Samson and Claude Opus
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,107 @@
1
+ Metadata-Version: 2.4
2
+ Name: swarph-mesh
3
+ Version: 0.1.0
4
+ Summary: Model-agnostic Python substrate for the swarph-mesh ecosystem. v0.1.0 ships SwarphCall + GeminiAdapter (Phase 1 substrate per PLAN.md §13).
5
+ Author: Pierre Samson, Claude Opus
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/darw007d/swarph-mesh
8
+ Project-URL: Source, https://github.com/darw007d/swarph-mesh
9
+ Project-URL: CLI, https://github.com/darw007d/swarph-cli
10
+ Project-URL: Plugin, https://github.com/darw007d/swarph-meshlm
11
+ Project-URL: Spec, https://github.com/darw007d/hedge-fund-mcp/blob/main/research/swarph_cli/PLAN.md
12
+ Keywords: swarph,llm,mesh,cli,multi-llm,gemini,claude,deepseek
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: POSIX :: Linux
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Requires-Python: >=3.10
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Requires-Dist: swarph-shared>=0.2.0
27
+ Requires-Dist: pydantic>=2.0
28
+ Requires-Dist: langgraph-genai-bridge>=0.1.5
29
+ Requires-Dist: langchain-core>=0.3
30
+ Provides-Extra: dev
31
+ Requires-Dist: pytest>=7.0; extra == "dev"
32
+ Dynamic: license-file
33
+
34
+ # swarph-mesh
35
+
36
+ Model-agnostic Python substrate for the swarph-mesh ecosystem. Pure library, no CLI.
37
+
38
+ Designed to fill the gap left by existing tools (`aichat`, `mods`, Simon Willison's `llm`, `gemini-cli`, `claude-cli`): none expose mesh-gateway participation, per-caller attribution, structured-output discipline, or the cooperative-protocol patterns the swarph encodes.
39
+
40
+ This is one of three repos in the v0.3.x architecture:
41
+
42
+ | Repo | Role |
43
+ |---|---|
44
+ | [`swarph-mesh`](https://github.com/darw007d/swarph-mesh) | This package — typed Protocol + adapters + SwarphCall + MeshClient |
45
+ | [`swarph-cli`](https://github.com/darw007d/swarph-cli) | The `swarph` binary. Thin client over `swarph-mesh` |
46
+ | [`swarph-meshlm`](https://github.com/darw007d/swarph-meshlm) | Simon Willison `llm` plugin. Same primitives wired into `llm`'s plugin host |
47
+
48
+ All three sit on top of [`swarph-shared`](https://github.com/darw007d/swarph-shared) which provides the cross-billing-path attribution + subprocess env scrubbing + JSON-mode harness + peer-name registry primitives.
49
+
50
+ ## Status
51
+
52
+ **v0.1.0 — Phase 1 substrate.** Live `SwarphCall(provider="gemini")` works end-to-end against real Gemini API per PLAN.md §13 falsifiability gate.
53
+
54
+ Public surface:
55
+
56
+ - `LLMAdapter` Protocol (runtime-checkable) + `ChatMessage` + `LLMResponse`
57
+ - `SwarphCall` — public entry point with caller-convention validation, hooks, attribution
58
+ - `GeminiAdapter` — wraps `langgraph-genai-bridge` (Flex tier, context caching)
59
+ - JSON-mode harness — retry-once with [USER]-turn feedback (per swarph-shared invariant)
60
+ - Attribution: `FileAttributionWriter` default; `set_default_writer()` for production TSDB consumers
61
+
62
+ Tests: **43/43 passing** (42 offline + 1 live smoke gated on `GEMINI_API_KEY`).
63
+
64
+ ```python
65
+ from swarph_mesh import SwarphCall, ChatMessage
66
+
67
+ result = await SwarphCall(
68
+ provider="gemini",
69
+ caller="orchestrator.boss",
70
+ ).chat(
71
+ messages=[ChatMessage(role="user", content="hi")],
72
+ )
73
+ print(result.text, result.cost_usd, result.input_tokens)
74
+ ```
75
+
76
+ ## Spec
77
+
78
+ The canonical PLAN with sequencing, falsifiability gates, and design rationale lives at:
79
+
80
+ → [hedge-fund-mcp / research/swarph_cli/PLAN.md](https://github.com/darw007d/hedge-fund-mcp/blob/main/research/swarph_cli/PLAN.md)
81
+
82
+ ## Phase rollout
83
+
84
+ | Phase | Scope |
85
+ |---|---|
86
+ | **0** (v0.0.1) | Typed substrate — Protocol + dataclasses + exceptions |
87
+ | **1** (v0.1.0 — this release) | Gemini adapter + `SwarphCall` surface + caller convention import + JSON-mode harness + attribution hook |
88
+ | **3** | `MeshClient` — replaces hand-rolled curl in `lab_loop_drain.py` etc. |
89
+ | **4** | DeepSeek + Claude (subscription) + OpenAI adapters |
90
+ | **5.5** | `swarph onboard` + `swarph ratify` (lives in `swarph-cli`, depends on this) |
91
+ | **5.7** | `swarph daemon` + REPL drain coroutine (lives in `swarph-cli`) |
92
+ | **6** | PyPI publish |
93
+ | **7** | `swarph-meshlm` plugin (separate repo, this dep) |
94
+
95
+ ## Install (dev)
96
+
97
+ ```bash
98
+ git clone https://github.com/darw007d/swarph-mesh
99
+ cd swarph-mesh
100
+ python -m venv venv && source venv/bin/activate
101
+ pip install -e ".[dev]"
102
+ pytest
103
+ ```
104
+
105
+ ## License
106
+
107
+ MIT. Pierre Samson + Claude Opus, 2026.
@@ -0,0 +1,74 @@
1
+ # swarph-mesh
2
+
3
+ Model-agnostic Python substrate for the swarph-mesh ecosystem. Pure library, no CLI.
4
+
5
+ Designed to fill the gap left by existing tools (`aichat`, `mods`, Simon Willison's `llm`, `gemini-cli`, `claude-cli`): none expose mesh-gateway participation, per-caller attribution, structured-output discipline, or the cooperative-protocol patterns the swarph encodes.
6
+
7
+ This is one of three repos in the v0.3.x architecture:
8
+
9
+ | Repo | Role |
10
+ |---|---|
11
+ | [`swarph-mesh`](https://github.com/darw007d/swarph-mesh) | This package — typed Protocol + adapters + SwarphCall + MeshClient |
12
+ | [`swarph-cli`](https://github.com/darw007d/swarph-cli) | The `swarph` binary. Thin client over `swarph-mesh` |
13
+ | [`swarph-meshlm`](https://github.com/darw007d/swarph-meshlm) | Simon Willison `llm` plugin. Same primitives wired into `llm`'s plugin host |
14
+
15
+ All three sit on top of [`swarph-shared`](https://github.com/darw007d/swarph-shared) which provides the cross-billing-path attribution + subprocess env scrubbing + JSON-mode harness + peer-name registry primitives.
16
+
17
+ ## Status
18
+
19
+ **v0.1.0 — Phase 1 substrate.** Live `SwarphCall(provider="gemini")` works end-to-end against real Gemini API per PLAN.md §13 falsifiability gate.
20
+
21
+ Public surface:
22
+
23
+ - `LLMAdapter` Protocol (runtime-checkable) + `ChatMessage` + `LLMResponse`
24
+ - `SwarphCall` — public entry point with caller-convention validation, hooks, attribution
25
+ - `GeminiAdapter` — wraps `langgraph-genai-bridge` (Flex tier, context caching)
26
+ - JSON-mode harness — retry-once with [USER]-turn feedback (per swarph-shared invariant)
27
+ - Attribution: `FileAttributionWriter` default; `set_default_writer()` for production TSDB consumers
28
+
29
+ Tests: **43/43 passing** (42 offline + 1 live smoke gated on `GEMINI_API_KEY`).
30
+
31
+ ```python
32
+ from swarph_mesh import SwarphCall, ChatMessage
33
+
34
+ result = await SwarphCall(
35
+ provider="gemini",
36
+ caller="orchestrator.boss",
37
+ ).chat(
38
+ messages=[ChatMessage(role="user", content="hi")],
39
+ )
40
+ print(result.text, result.cost_usd, result.input_tokens)
41
+ ```
42
+
43
+ ## Spec
44
+
45
+ The canonical PLAN with sequencing, falsifiability gates, and design rationale lives at:
46
+
47
+ → [hedge-fund-mcp / research/swarph_cli/PLAN.md](https://github.com/darw007d/hedge-fund-mcp/blob/main/research/swarph_cli/PLAN.md)
48
+
49
+ ## Phase rollout
50
+
51
+ | Phase | Scope |
52
+ |---|---|
53
+ | **0** (v0.0.1) | Typed substrate — Protocol + dataclasses + exceptions |
54
+ | **1** (v0.1.0 — this release) | Gemini adapter + `SwarphCall` surface + caller convention import + JSON-mode harness + attribution hook |
55
+ | **3** | `MeshClient` — replaces hand-rolled curl in `lab_loop_drain.py` etc. |
56
+ | **4** | DeepSeek + Claude (subscription) + OpenAI adapters |
57
+ | **5.5** | `swarph onboard` + `swarph ratify` (lives in `swarph-cli`, depends on this) |
58
+ | **5.7** | `swarph daemon` + REPL drain coroutine (lives in `swarph-cli`) |
59
+ | **6** | PyPI publish |
60
+ | **7** | `swarph-meshlm` plugin (separate repo, this dep) |
61
+
62
+ ## Install (dev)
63
+
64
+ ```bash
65
+ git clone https://github.com/darw007d/swarph-mesh
66
+ cd swarph-mesh
67
+ python -m venv venv && source venv/bin/activate
68
+ pip install -e ".[dev]"
69
+ pytest
70
+ ```
71
+
72
+ ## License
73
+
74
+ MIT. Pierre Samson + Claude Opus, 2026.
@@ -0,0 +1,57 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "swarph-mesh"
7
+ version = "0.1.0"
8
+ description = "Model-agnostic Python substrate for the swarph-mesh ecosystem. v0.1.0 ships SwarphCall + GeminiAdapter (Phase 1 substrate per PLAN.md §13)."
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.10"
12
+ authors = [
13
+ { name = "Pierre Samson" },
14
+ { name = "Claude Opus" },
15
+ ]
16
+ keywords = ["swarph", "llm", "mesh", "cli", "multi-llm", "gemini", "claude", "deepseek"]
17
+ classifiers = [
18
+ "Development Status :: 3 - Alpha",
19
+ "Intended Audience :: Developers",
20
+ "License :: OSI Approved :: MIT License",
21
+ "Operating System :: POSIX :: Linux",
22
+ "Programming Language :: Python :: 3",
23
+ "Programming Language :: Python :: 3.10",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Programming Language :: Python :: 3.13",
27
+ "Topic :: Software Development :: Libraries :: Python Modules",
28
+ ]
29
+ dependencies = [
30
+ "swarph-shared>=0.2.0",
31
+ "pydantic>=2.0",
32
+ # Phase 1 — Gemini adapter wraps the bridge (PLAN.md §3 ship order #1)
33
+ "langgraph-genai-bridge>=0.1.5",
34
+ "langchain-core>=0.3",
35
+ ]
36
+
37
+ [project.urls]
38
+ Homepage = "https://github.com/darw007d/swarph-mesh"
39
+ Source = "https://github.com/darw007d/swarph-mesh"
40
+ CLI = "https://github.com/darw007d/swarph-cli"
41
+ Plugin = "https://github.com/darw007d/swarph-meshlm"
42
+ Spec = "https://github.com/darw007d/hedge-fund-mcp/blob/main/research/swarph_cli/PLAN.md"
43
+
44
+ [project.optional-dependencies]
45
+ dev = ["pytest>=7.0"]
46
+
47
+ # NOTE: this repo is the substrate Python package only — pure library.
48
+ # The `swarph` CLI binary lives in `darw007d/swarph-cli`; the Simon
49
+ # Willison `llm` plugin in `darw007d/swarph-meshlm`. All three import
50
+ # from this package as the bottom layer.
51
+
52
+ [tool.setuptools.packages.find]
53
+ where = ["src"]
54
+
55
+ [tool.pytest.ini_options]
56
+ testpaths = ["tests"]
57
+ addopts = "-v --tb=short"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,95 @@
1
+ """swarph-mesh — model-agnostic Python substrate for the swarph-mesh ecosystem.
2
+
3
+ The substrate gap the existing CLIs (``aichat`` / ``mods`` / Simon Willison's
4
+ ``llm`` / ``gemini-cli`` / ``claude-cli``) leave open: none expose
5
+ mesh-gateway participation, per-caller attribution, structured-output
6
+ discipline, or the cooperative-protocol patterns the swarph encodes.
7
+
8
+ This package fills it as a pure Python library. Three repos make up
9
+ the v0.3.x architecture:
10
+
11
+ * ``swarph-mesh`` (this package) — typed Protocol + adapters + SwarphCall.
12
+ Pure library, no CLI.
13
+ * ``swarph-cli`` (separate repo) — the ``swarph`` binary. Thin client
14
+ on top of ``swarph-mesh``.
15
+ * ``swarph-meshlm`` (separate) — Simon Willison ``llm`` plugin.
16
+
17
+ See the canonical PLAN at:
18
+ ``https://github.com/darw007d/hedge-fund-mcp/blob/main/research/swarph_cli/PLAN.md``
19
+
20
+ v0.1.0 — Phase 1 substrate. Ships:
21
+
22
+ * :class:`LLMAdapter` Protocol + ``ChatMessage`` + ``LLMResponse`` (from v0.0.1)
23
+ * :class:`SwarphCall` public surface — caller-validated, hook-wired entry-point
24
+ * :class:`GeminiAdapter` — wraps ``langgraph-genai-bridge`` (Flex tier, caching)
25
+ * JSON-mode harness — retry-once with [USER]-turn feedback (per PR #125 invariant)
26
+ * Attribution hooks + writers (FileAttributionWriter default;
27
+ ``set_default_writer`` for production TSDB consumers)
28
+
29
+ Future phases (per PLAN.md §13):
30
+ 3 MeshClient — mesh-gateway HTTP wrapper
31
+ 4 DeepSeek + Claude (subscription) + OpenAI + Grok adapters
32
+ 2.5+ ``swarph import`` (lives in swarph-cli)
33
+ """
34
+
35
+ from __future__ import annotations
36
+
37
+ # Public types
38
+ from swarph_mesh.exceptions import (
39
+ AdapterError,
40
+ SwarphMeshError,
41
+ UnknownProvider,
42
+ )
43
+ from swarph_mesh.types import (
44
+ ChatMessage,
45
+ LLMAdapter,
46
+ LLMResponse,
47
+ )
48
+
49
+ # Phase 1 surfaces
50
+ from swarph_mesh.swarph_call import SwarphCall
51
+ from swarph_mesh.adapters import get_adapter, register_adapter
52
+ from swarph_mesh.attribution import (
53
+ AttributionEvent,
54
+ AttributionWriter,
55
+ FileAttributionWriter,
56
+ NullAttributionWriter,
57
+ get_default_writer,
58
+ set_default_writer,
59
+ )
60
+ from swarph_mesh.hooks import (
61
+ CallContext,
62
+ HookSet,
63
+ attribution_post_call,
64
+ default_hooks,
65
+ )
66
+
67
+ __version__ = "0.1.0"
68
+
69
+ __all__ = [
70
+ "__version__",
71
+ # types
72
+ "ChatMessage",
73
+ "LLMResponse",
74
+ "LLMAdapter",
75
+ # exceptions
76
+ "SwarphMeshError",
77
+ "AdapterError",
78
+ "UnknownProvider",
79
+ # SwarphCall public surface
80
+ "SwarphCall",
81
+ "get_adapter",
82
+ "register_adapter",
83
+ # attribution
84
+ "AttributionEvent",
85
+ "AttributionWriter",
86
+ "FileAttributionWriter",
87
+ "NullAttributionWriter",
88
+ "get_default_writer",
89
+ "set_default_writer",
90
+ # hooks
91
+ "CallContext",
92
+ "HookSet",
93
+ "attribution_post_call",
94
+ "default_hooks",
95
+ ]
@@ -0,0 +1,56 @@
1
+ """Provider adapters — registry + dispatch.
2
+
3
+ Phase 1 ships only the Gemini adapter (PLAN.md §3 ship order).
4
+ Subsequent phases add DeepSeek / Claude / OpenAI / Grok by adding
5
+ modules here + registering them in :func:`get_adapter`.
6
+
7
+ Adapters are singletons per provider — instantiated on first
8
+ request, reused for the rest of the process. This matches the
9
+ "adapter registry" shape from PLAN.md §4.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from typing import Optional
15
+
16
+ from swarph_mesh.exceptions import UnknownProvider
17
+ from swarph_mesh.types import LLMAdapter
18
+
19
+ # Registry of instantiated singletons (one per provider name)
20
+ _REGISTRY: dict[str, LLMAdapter] = {}
21
+
22
+
23
+ def get_adapter(provider: str, *, api_key: Optional[str] = None) -> LLMAdapter:
24
+ """Return the adapter for ``provider``, instantiating on first request.
25
+
26
+ Phase 1: only ``"gemini"`` is registered. Other providers raise
27
+ :class:`UnknownProvider`. Phase 4+ adds DeepSeek, Claude, OpenAI, Grok.
28
+ """
29
+ if provider in _REGISTRY:
30
+ return _REGISTRY[provider]
31
+
32
+ if provider == "gemini":
33
+ from swarph_mesh.adapters.gemini import GeminiAdapter
34
+
35
+ adapter = GeminiAdapter(api_key=api_key)
36
+ _REGISTRY[provider] = adapter
37
+ return adapter
38
+
39
+ raise UnknownProvider(
40
+ f"no adapter registered for provider {provider!r}. "
41
+ "Phase 1 ships gemini only; DeepSeek/Claude/OpenAI/Grok ship in Phase 4+."
42
+ )
43
+
44
+
45
+ def register_adapter(provider: str, adapter: LLMAdapter) -> None:
46
+ """Programmatic adapter registration. Test fixtures use this to
47
+ inject mocks; production consumers normally don't need it."""
48
+ _REGISTRY[provider] = adapter
49
+
50
+
51
+ def reset_registry() -> None:
52
+ """Test-only: clear the registry. Not part of the public API."""
53
+ _REGISTRY.clear()
54
+
55
+
56
+ __all__ = ["get_adapter", "register_adapter", "reset_registry"]
@@ -0,0 +1,195 @@
1
+ """Gemini adapter — wraps ``langgraph-genai-bridge`` per PLAN.md §3.
2
+
3
+ Why the bridge and not raw ``google.genai``: the bridge ships
4
+ production-tested Flex tier handling, context caching (Pro tier),
5
+ and usage-metadata extraction in a clean abstraction. Re-implementing
6
+ those features in this adapter would duplicate ~200 LOC for no
7
+ substrate benefit. The bridge is a stable v0.1.5 PyPI package that
8
+ both lab and droplet have run in production for weeks.
9
+
10
+ Cost calculation uses Google's published per-Mtok pricing as of
11
+ 2026-04-29; update the ``PRICING`` table when Google revises rates.
12
+ Flex tier (``flex=True``) gets a 50% rebate per Google's announced
13
+ pricing — applied after the base cost computation.
14
+
15
+ The adapter exposes the swarph_mesh :class:`LLMAdapter` Protocol
16
+ shape: async ``chat`` + ``stream`` + sync ``cost_per_token``.
17
+ ``stream`` is a v0.2.0 stretch (PLAN.md doesn't gate Phase 1 on
18
+ streaming; the bridge supports it but we keep the surface simple
19
+ for now and raise ``NotImplementedError``).
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import asyncio
25
+ import os
26
+ import time
27
+ from typing import AsyncIterator, Optional
28
+
29
+ from swarph_mesh.exceptions import AdapterError
30
+ from swarph_mesh.types import ChatMessage, LLMResponse
31
+
32
+
33
+ # Gemini per-Mtok pricing (USD), 2026-04-29 baseline.
34
+ # Flex tier applies a 50% rebate after base computation.
35
+ PRICING: dict[str, tuple[float, float]] = {
36
+ # model_id: (input_per_mtok, output_per_mtok)
37
+ "gemini-2.5-flash": (0.075, 0.30),
38
+ "gemini-2.5-flash-lite": (0.019, 0.075),
39
+ "gemini-2.5-pro": (1.25, 5.00),
40
+ "gemini-2.5-pro-preview": (1.25, 5.00),
41
+ # Default fallback used when an unknown model id is requested
42
+ "_default": (0.075, 0.30),
43
+ }
44
+
45
+
46
+ def _to_langchain_messages(messages: list[ChatMessage]) -> list:
47
+ """Convert our ChatMessage list to LangChain BaseMessage list.
48
+
49
+ The bridge ``invoke()`` method takes LangChain messages; we
50
+ keep our package's user-facing surface as plain dataclasses
51
+ (no LangChain dep at the public API).
52
+ """
53
+ from langchain_core.messages import (
54
+ AIMessage,
55
+ HumanMessage,
56
+ SystemMessage,
57
+ )
58
+
59
+ out: list = []
60
+ for m in messages:
61
+ if m.role == "user":
62
+ out.append(HumanMessage(content=m.content))
63
+ elif m.role == "assistant":
64
+ out.append(AIMessage(content=m.content))
65
+ elif m.role == "system":
66
+ out.append(SystemMessage(content=m.content))
67
+ else:
68
+ # Unknown role — let the bridge surface the error
69
+ out.append(HumanMessage(content=f"[{m.role}] {m.content}"))
70
+ return out
71
+
72
+
73
+ def _compute_cost(
74
+ model: str, input_tokens: int, output_tokens: int, flex: bool
75
+ ) -> float:
76
+ """Per-Mtok cost using ``PRICING``. Flex applies 50% rebate."""
77
+ in_per_mtok, out_per_mtok = PRICING.get(model, PRICING["_default"])
78
+ cost = (input_tokens / 1_000_000.0) * in_per_mtok + (
79
+ output_tokens / 1_000_000.0
80
+ ) * out_per_mtok
81
+ if flex:
82
+ cost *= 0.5
83
+ return cost
84
+
85
+
86
+ class GeminiAdapter:
87
+ """``LLMAdapter`` implementation backed by ``langgraph-genai-bridge``."""
88
+
89
+ name = "gemini"
90
+ default_model = "gemini-2.5-flash"
91
+
92
+ def __init__(
93
+ self,
94
+ api_key: Optional[str] = None,
95
+ flex: bool = True,
96
+ ):
97
+ """``api_key`` falls back to ``GEMINI_API_KEY`` env. ``flex``
98
+ defaults to True per OMEGA's standard production config —
99
+ Flex tier costs 50% less for latency-tolerant workloads."""
100
+ self._api_key = api_key or os.environ.get("GEMINI_API_KEY")
101
+ self._flex = flex
102
+ # Per-model bridge instances are cached so we don't re-init
103
+ # the SDK client for every call. Keyed by (model, flex).
104
+ self._bridges: dict[tuple[str, bool], object] = {}
105
+
106
+ def _get_bridge(self, model: str, flex: bool) -> object:
107
+ from langgraph_genai_bridge import GenAIBridge
108
+
109
+ key = (model, flex)
110
+ if key in self._bridges:
111
+ return self._bridges[key]
112
+ if not self._api_key:
113
+ raise AdapterError(
114
+ "GeminiAdapter requires GEMINI_API_KEY env or api_key kwarg"
115
+ )
116
+ b = GenAIBridge(api_key=self._api_key, model=model, flex=flex)
117
+ self._bridges[key] = b
118
+ return b
119
+
120
+ async def chat(
121
+ self,
122
+ messages: list[ChatMessage],
123
+ model: str,
124
+ system_prompt: Optional[str] = None,
125
+ json_schema: Optional[dict] = None,
126
+ temperature: float = 0.7,
127
+ max_tokens: Optional[int] = None,
128
+ ) -> LLMResponse:
129
+ """Single multi-turn completion. Calls the bridge via
130
+ ``asyncio.to_thread`` so the sync bridge fits in async code."""
131
+ # json_schema enforcement is the JSON harness's job — this
132
+ # method just returns text. The harness handles the parse +
133
+ # retry orchestration.
134
+ del json_schema # not used directly here
135
+
136
+ lc_messages = _to_langchain_messages(messages)
137
+ bridge = self._get_bridge(model, self._flex)
138
+
139
+ start = time.monotonic()
140
+ try:
141
+ ai_message = await asyncio.to_thread(
142
+ bridge.invoke,
143
+ lc_messages,
144
+ system_prompt,
145
+ )
146
+ except Exception as exc:
147
+ duration_s = time.monotonic() - start
148
+ raise AdapterError(
149
+ f"GeminiAdapter.chat failed for model {model!r}: {exc}"
150
+ ) from exc
151
+ duration_s = time.monotonic() - start
152
+
153
+ # Extract usage from the AIMessage
154
+ usage = getattr(ai_message, "usage_metadata", None) or {}
155
+ input_tokens = int(usage.get("input_tokens", 0))
156
+ output_tokens = int(usage.get("output_tokens", 0))
157
+ cached_tokens_dict = usage.get("input_token_details", {}) or {}
158
+ cached_tokens = int(cached_tokens_dict.get("cache_read", 0))
159
+
160
+ cost = _compute_cost(model, input_tokens, output_tokens, self._flex)
161
+
162
+ return LLMResponse(
163
+ text=ai_message.content if isinstance(ai_message.content, str) else str(ai_message.content),
164
+ input_tokens=input_tokens,
165
+ output_tokens=output_tokens,
166
+ cost_usd=cost,
167
+ duration_s=duration_s,
168
+ cached=cached_tokens > 0,
169
+ raw_response={
170
+ "cached_tokens": cached_tokens,
171
+ "model": model,
172
+ "flex": self._flex,
173
+ },
174
+ )
175
+
176
+ async def stream(
177
+ self,
178
+ messages: list[ChatMessage],
179
+ model: str,
180
+ **kwargs,
181
+ ) -> AsyncIterator[str]:
182
+ """Token-by-token streaming. Phase 1 v0.1.0 raises
183
+ NotImplementedError; bridge supports streaming, this adapter
184
+ will wire it up in v0.2.0."""
185
+ raise NotImplementedError(
186
+ "GeminiAdapter.stream is v0.2.0 stretch; use chat() for now."
187
+ )
188
+ # Unreachable, but keeps the AsyncIterator return type valid for
189
+ # static analysis.
190
+ yield "" # pragma: no cover
191
+
192
+ def cost_per_token(self, model: str) -> tuple[float, float]:
193
+ """Return (input_per_mtok, output_per_mtok) USD for ``model``.
194
+ Flex tier rebate is applied at call time, not in this lookup."""
195
+ return PRICING.get(model, PRICING["_default"])