Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions livekit-agents/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ elevenlabs = ["livekit-plugins-elevenlabs>=1.6.10"]
fal = ["livekit-plugins-fal>=1.6.10"]
fishaudio = ["livekit-plugins-fishaudio>=1.6.10"]
fireworksai = ["livekit-plugins-fireworksai>=1.6.10"]
floe = ["livekit-plugins-floe>=1.6.10"]
gladia = ["livekit-plugins-gladia>=1.6.10"]
gnani = ["livekit-plugins-gnani>=1.6.10"]
google = ["livekit-plugins-google>=1.6.10"]
Expand Down
179 changes: 179 additions & 0 deletions livekit-plugins/livekit-plugins-floe/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
# Floe plugin for LiveKit Agents

Route LiveKit's LLM through [Floe](https://floelabs.xyz/) so agent inference is
metered against a spend budget. Drop-in OpenAI-compatible `LLM` plus a usage
reconciler that checks LiveKit-reported token usage against Floe pricing.

Two ways to connect:

- **Keyless gateway (default)** — Floe holds the upstream provider keys and
bills your Floe balance. You only need a Floe API key.
- **Bring your own key (BYOK)** — you supply an upstream provider key; Floe
forwards it and meters spend against your budget.

STT/TTS are intentionally not included — Floe's voice surfaces are not yet GA.
This plugin covers the LLM only.

## Installation

```bash
pip install livekit-plugins-floe
```

## Quickstart (keyless)

Set your Floe API key:

```bash
export FLOE_API_KEY=floe_...
```

Use it like any other LiveKit LLM:

```python
from livekit.agents import AgentSession
from livekit.plugins import floe

session = AgentSession(
llm=floe.LLM(model="openai/gpt-4o"),
# ... stt, tts, vad
)
```

The Floe API key can be passed directly instead of via the environment:

```python
floe.LLM(model="openai/gpt-4o", api_key="floe_...")
```

## Bring your own provider key (BYOK)

Supply an upstream provider key and Floe forwards it (via the
`X-Floe-Provider-Key` header) while still metering spend against your budget.
Requests default to the metered proxy at `https://credit-api.floelabs.xyz/v1/llm`.

```bash
export FLOE_API_KEY=floe_...
export FLOE_PROVIDER_KEY=sk-...
```

```python
from livekit.plugins import floe

llm = floe.LLM(model="openai/gpt-4o") # BYOK auto-detected from FLOE_PROVIDER_KEY
```

Or pass it explicitly:

```python
llm = floe.LLM(
model="openai/gpt-4o",
api_key="floe_...",
provider_key="sk-...",
)
```

## Usage reconciliation

`FloeUsageReconciler` tracks the per-model LLM token usage LiveKit reports during
a session (via the `session_usage_updated` event) and prices each served model
against the Floe cost map. The local estimate is advisory — Floe's billed amount
is authoritative — so a divergence between the two is the thing worth watching.
It reads the model id off each usage entry, so a session that swaps or fans out
across models is priced correctly; no model has to be named up front.

```python
from livekit.agents import AgentSession
from livekit.plugins import floe

session = AgentSession(llm=floe.LLM(model="openai/gpt-4o"))

reconciler = floe.FloeUsageReconciler()
reconciler.attach(session)

# ... run the session ...

report = reconciler.summary()
print("Floe-estimated USD:", report.total_estimated_usd)
for m in report.per_model:
print(f" {m.provider}/{m.model}: {m.input_tokens} in + {m.output_tokens} out -> ${m.estimated_usd}")
if report.unpriced_models:
print("unpriced (excluded from total):", report.unpriced_models)
```

## Per-turn cost receipt

For the "what did that call cost" moment, `enable_cost_receipts` logs a one-line
receipt after every Floe-routed turn — zero config:

```python
from livekit.plugins import floe

session = AgentSession(llm=floe.LLM(model="openai/gpt-4o"))
floe.enable_cost_receipts(session)
```

Each turn prints a line like:

```
floe · gpt-4o · $0.0012 est · left $99.88
```

The cost half is always shown (priced locally from the bundled cost map — free,
offline, no account). The `left $…` budget half appears when a `FLOE_API_KEY` is
set, read best-effort from hosted Floe; a failed read never breaks the session
(the cost still prints). A live-prod screenshot with a funded key is captured
separately.

If you configured the LLM with an in-code key — `floe.LLM(api_key="floe_…")` —
pass the same key so the balance is for the billed account:

```python
floe.enable_cost_receipts(session, api_key="floe_…")
```

## Fallback: export Floe cost over OpenTelemetry

If you'd rather ship Floe's numbers into your existing observability stack than
read them inline, the same reconciler feeds OpenTelemetry. LiveKit Agents already
emits OTel traces, so this lands Floe cost as standard OTLP metrics next to them —
`floe.cost.usd` and `floe.tokens`, tagged by agent. This is cost *observability*,
not enforcement: the budget guard stays in `floe-guard`; OTel just carries the
receipt to where ops already looks.

```python
from opentelemetry import metrics
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader

from livekit.agents import AgentSession
from livekit.plugins import floe

# Point OTLP at any backend via OTEL_EXPORTER_OTLP_ENDPOINT.
reader = PeriodicExportingMetricReader(OTLPMetricExporter())
metrics.set_meter_provider(MeterProvider(metric_readers=[reader]))
_meter = metrics.get_meter("floe")
_cost = _meter.create_counter("floe.cost.usd", unit="USD")
_tokens = _meter.create_counter("floe.tokens", unit="1")


def attach_floe_otel(session: AgentSession, *, agent: str) -> floe.FloeUsageReconciler:
reconciler = floe.FloeUsageReconciler()
reconciler.attach(session)

@session.on("close")
def _drain(_ev: object) -> None:
report = reconciler.summary()
tokens = sum(m.input_tokens + m.output_tokens for m in report.per_model)
_cost.add(report.total_estimated_usd, {"agent": agent})
_tokens.add(tokens, {"agent": agent})

return reconciler
```

## Pre-requisites

A Floe account and a Floe API key. For BYOK, also an upstream provider key.
Credentials can be passed directly or via the `FLOE_API_KEY` and
`FLOE_PROVIDER_KEY` environment variables.
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Copyright 2023 LiveKit, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Floe plugin for LiveKit Agents

Route LiveKit's LLM through Floe for metered spend and budget-guarded
inference, either keyless (Floe holds the provider keys) or BYOK (bring your own
provider key). Includes a usage reconciler that reconciles LiveKit-reported
token usage against Floe pricing.

See https://docs.livekit.io for more information.
"""

from livekit.agents import Plugin

from .log import logger
from .metering import FloeUsageReconciler
from .receipt import enable_cost_receipts
from .services import LLM
from .version import __version__

__all__ = ["LLM", "FloeUsageReconciler", "enable_cost_receipts", "__version__"]


class FloePlugin(Plugin):
def __init__(self) -> None:
super().__init__(__name__, __version__, __package__, logger)


Plugin.register_plugin(FloePlugin())

# Cleanup docs of unexported modules
_module = dir()
NOT_IN_ALL = [m for m in _module if m not in __all__]

__pdoc__ = {}

for n in NOT_IN_ALL:
__pdoc__[n] = False
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Copyright 2023 LiveKit, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import logging

logger = logging.getLogger("livekit.plugins.floe")
Loading