Skip to content
Open
36 changes: 36 additions & 0 deletions docs/en/reference/orchestration-et-supervision.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<!-- fr-synced: c3027d43be98ae750a20d8c860fdb5d9cf01023c -->
# Orchestrate several models and monitor calls

The `@ai-swiss/base-llm` package exposes a single model port: every model offers the same `complete` interface. Because meta-models respect this port, they compose: each wraps one or more models and is itself a model. It therefore drops in anywhere a single model is used, from settings to routing. Choosing a model stays an explicit decision.

## Mixture of agents

`createMoaModel` queries several proposers in parallel, then an aggregator synthesizes their drafts into one answer. Proposers work without tools and produce text; the aggregator receives the drafts as private guidance and keeps the original tools. Token usage sums across all calls, and synthesis continues even if some proposers fail.

```mermaid
flowchart TD
R[Request] --> P1[Proposer 1]
R --> P2[Proposer 2]
P1 --> D[Drafts]
P2 --> D
D --> A[Aggregator]
A --> O[Final answer]
```

## Triumvirat

`createTriumviratModel` follows the architecture of Sakana Fugu and TRINITY. Each turn, a coordinator picks a model from a swappable pool and assigns it a role: the thinker plans, the worker produces or fixes the answer and alone receives the tools, the verifier judges the draft and decides when to stop. The loop runs until acceptance or a turn budget.

The default coordinator relies on a model from the pool, with a deterministic thinker, worker, verifier fallback. A caller-supplied `decide` function replaces that coordinator without changing the interface.

## Configure ensembles from settings

An `ensembles` block in `.ai/studio.settings.json` makes these compositions available without code. Each entry names a `type` (`moa` or `triumvirat`) and member references in the `<provider>/<model>` form. The ensemble name then works anywhere a model reference works, for example in `routing.model`. Bad configuration fails with a clear message.

## Monitoring with Langfuse

`createLangfuseModel` wraps any model and traces every call to Langfuse: input, output, tokens, latency, and errors. The wrapper adds no dependency: it writes to the public ingestion interface through `fetch`. The send runs in the background and adds no latency to the call; `flush()` drains in-flight sends before a short process ends. A monitoring failure never interrupts the wrapped call. Keys come from the environment variables `LANGFUSE_PUBLIC_KEY` and `LANGFUSE_SECRET_KEY`, and a self-hosted host is declared through `LANGFUSE_HOST`.

## Choosing between mixture and triumvirat

Mixture aims for breadth in a single turn: parallel proposals then a synthesis, simple and economical. Triumvirat aims for depth in sequence: plan, produce, verify, repeat, with self-correction and tool use. Both share the same port, so Langfuse monitoring wraps them the same way.
47 changes: 47 additions & 0 deletions docs/reference/orchestration-et-supervision.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
schema_version: base.resource.v1
id: orchestration-supervision
type: document
title: Orchestrer plusieurs modèles et superviser les appels
description: Composer des modèles (mixture d'agents, coordination par rôles), les configurer depuis les réglages, et tracer chaque appel vers Langfuse, le tout via le même port de modèle.
scope: public
status: active
sensitivity: public
keywords: [orchestration, supervision, observabilité, mixture, agents, langfuse, routage, modèles]
---

# Orchestrer plusieurs modèles et superviser les appels

Le paquet `@ai-swiss/base-llm` expose un port de modèle unique: tout modèle offre la même interface `complete`. Comme les méta-modèles respectent ce port, ils se composent: chacun enveloppe un ou plusieurs modèles et reste lui-même un modèle. Il s'insère donc partout où un modèle simple s'utilise, des réglages au routage. Choisir un modèle reste toujours une décision explicite.

## Mixture d'agents

`createMoaModel` interroge plusieurs proposeurs en parallèle, puis un agrégateur synthétise leurs brouillons en une seule réponse. Les proposeurs travaillent sans outils et produisent du texte; l'agrégateur reçoit les brouillons comme guidage privé et garde les outils d'origine. L'usage des jetons s'additionne sur tous les appels, et la synthèse continue même si certains proposeurs échouent.

```mermaid
flowchart TD
R[Requête] --> P1[Proposeur 1]
R --> P2[Proposeur 2]
P1 --> D[Brouillons]
P2 --> D
D --> A[Agrégateur]
A --> O[Réponse finale]
```

## Triumvirat

`createTriumviratModel` reprend l'architecture de Sakana Fugu et de TRINITY. À chaque tour, un coordinateur choisit un modèle dans un vivier interchangeable et lui confie un rôle: le penseur planifie, l'exécutant produit ou corrige la réponse et reçoit seul les outils, le vérificateur juge le brouillon et décide de l'arrêt. La boucle tourne jusqu'à l'acceptation ou jusqu'à une limite de tours.

Le coordinateur par défaut s'appuie sur un modèle du vivier, avec un repli déterministe penseur, exécutant, vérificateur. Une fonction `decide` fournie par l'appelant remplace ce coordinateur sans changer l'interface.

## Configurer les ensembles depuis les réglages

Un bloc `ensembles` dans `.ai/studio.settings.json` rend ces compositions disponibles sans écrire de code. Chaque entrée nomme un `type` (`moa` ou `triumvirat`) et des références de membres au format `<fournisseur>/<modèle>`. Le nom de l'ensemble s'utilise ensuite partout où une référence de modèle s'utilise, par exemple dans `routing.model`. Une configuration incorrecte échoue avec un message clair.

## Supervision avec Langfuse

`createLangfuseModel` enveloppe n'importe quel modèle et trace chaque appel vers Langfuse: entrée, sortie, jetons, durée et erreurs. L'enveloppe n'ajoute aucune dépendance: elle écrit vers l'interface publique d'ingestion par `fetch`. L'envoi se fait en arrière-plan et n'ajoute pas de latence à l'appel; `flush()` vide les envois en cours avant la fin d'un processus court. Un échec de supervision n'interrompt jamais l'appel supervisé. Les clés viennent des variables d'environnement `LANGFUSE_PUBLIC_KEY` et `LANGFUSE_SECRET_KEY`, et un hôte auto-hébergé se déclare par `LANGFUSE_HOST`.

## Choisir entre mixture et triumvirat

La mixture vise la largeur en un seul tour: des propositions parallèles puis une synthèse, simple et économe. Le triumvirat vise la profondeur en séquence: planifier, produire, vérifier, recommencer, avec autocorrection et usage des outils. Les deux partagent le même port, donc la supervision Langfuse les enveloppe de la même façon.
11 changes: 11 additions & 0 deletions packages/base-llm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,14 @@ console.log(getText(message));
```

Tool-calling, typed errors (`.code`, `.retriable`), bounded retries with jitter, deadlines and abort are all handled in one tested transport layer (mirrors `@ai-swiss/base-ranker-semantic`). Choosing a model is always an explicit decision — there is no "default/best model" helper.

## Composition: ensembles and monitoring

Because every model is the same port, models compose. These meta-models each wrap one or more models and are themselves models, so they drop in anywhere a single model does (settings, Studio, the CLI):

- [Mixture of Agents](docs/moa.md) — `createMoaModel`: N proposers answer in parallel, one aggregator synthesizes their drafts.
- [Triumvirat](docs/triumvirat.md) — `createTriumviratModel`: a coordinator assigns Thinker / Worker / Verifier roles over a swappable pool, looping until a verifier accepts (the Sakana Fugu / TRINITY architecture).
- [Ensembles in settings](docs/ensembles-settings.md) — configure MoA and Triumvirat from `.ai/studio.settings.json` without code.
- [Langfuse monitoring](docs/langfuse.md) — `createLangfuseModel`: wrap any model to trace input, output, token usage, latency and errors to Langfuse, with zero added dependencies.

Each page includes a diagram, an options table, and a runnable snippet.
65 changes: 65 additions & 0 deletions packages/base-llm/docs/ensembles-settings.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Configuring ensembles in settings

MoA (`createMoaModel`) and Triumvirat (`createTriumviratModel`) are usable from
`.ai/studio.settings.json` without code. Add an `ensembles` block; each entry
names a `type` and member refs. A member ref is the same `<provider>/<model>`
string a normal model uses, so members are resolved through the existing
provider registry. The ensemble name then works anywhere a model ref does
(for example `routing.model`).

```mermaid
flowchart LR
REF[model ref] --> Q{name in<br/>ensembles?}
Q -->|no| P[provider/model<br/>single model]
Q -->|yes, moa| M[resolve members<br/>createMoaModel]
Q -->|yes, triumvirat| T[resolve pool<br/>createTriumviratModel]
```

## Example

```json
{
"providers": [
{ "id": "openai", "type": "openai-compatible", "baseUrl": "https://api.openai.com/v1", "apiKeyEnv": "OPENAI_API_KEY" },
{ "id": "openrouter", "type": "openai-compatible", "baseUrl": "https://openrouter.ai/api/v1", "apiKeyEnv": "OPENROUTER_API_KEY" }
],
"ensembles": {
"council": {
"type": "moa",
"proposers": ["openai/gpt-5.5", "openrouter/deepseek/deepseek-v4-pro"],
"aggregator": "openrouter/qwen3-max"
},
"trio": {
"type": "triumvirat",
"pool": {
"deepseek": "openrouter/deepseek/deepseek-v4-pro",
"gpt": "openai/gpt-5.5",
"qwen": "openrouter/qwen3-max"
},
"maxTurns": 6
}
}
}
```

## Fields

`type: "moa"`

| Field | Required | Meaning |
| --- | --- | --- |
| `proposers` | yes | List of member refs that draft in parallel. |
| `aggregator` | yes | Member ref that synthesizes the drafts. |
| `synthesisPrompt` | no | Override the aggregator guidance. |

`type: "triumvirat"`

| Field | Required | Meaning |
| --- | --- | --- |
| `pool` | yes | Object of member refs, keyed by name, cheapest-first. |
| `coordinator` | no | Member ref for a dedicated coordinator. Defaults to the cheapest pool member. |
| `maxTurns` | no | Turn budget (default 6). |

Member refs resolve through the same registry as any model, so a missing
provider, an unknown ensemble type, or a missing required field fails with a
clear `BAD_REQUEST` error.
75 changes: 75 additions & 0 deletions packages/base-llm/docs/langfuse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Langfuse monitoring

`createLangfuseModel` wraps any base-llm model and conforms to the same port
(`{ id, complete, stream? }`), so it composes over a single adapter, a MoA, or a
Triumvirat. Every call is traced to [Langfuse](https://langfuse.com): input,
output, token usage, latency, and errors.

Zero new dependencies: no Langfuse SDK, just native `fetch` against the public
ingestion API. Ingestion is fire-and-forget, so it never adds latency to the
model call. A telemetry failure never breaks the wrapped call.

## Flow

```mermaid
sequenceDiagram
participant App
participant W as Langfuse wrapper
participant M as Inner model
participant L as Langfuse API
App->>W: complete(request)
W->>M: complete(request)
M-->>W: completion (or error)
W-->>App: completion (or rethrow)
W-)L: POST trace + generation (fire-and-forget)
Note over W,L: flush() drains in-flight sends
```

## Usage

```js
import { createLangfuseModel, createOpenAICompatibleModel } from "@ai-swiss/base-llm";

const model = createLangfuseModel({
model: createOpenAICompatibleModel({ model: "gpt-5.5" }),
// publicKey / secretKey default to env LANGFUSE_PUBLIC_KEY / LANGFUSE_SECRET_KEY
});

const out = await model.complete({ messages: [userMessage("Explain CRDTs.")] });

// Before a short-lived process exits, drain in-flight telemetry:
await model.flush();
```

Because it wraps any model, you can monitor an ensemble by wrapping it:

```js
const monitored = createLangfuseModel({ model: createMoaModel({ proposers, aggregator }) });
```

## Options

| Option | Default | Meaning |
| --- | --- | --- |
| `model` | (required) | The model to wrap. |
| `publicKey` | env `LANGFUSE_PUBLIC_KEY` | Langfuse public key. |
| `secretKey` | env `LANGFUSE_SECRET_KEY` | Langfuse secret key. |
| `baseUrl` | env `LANGFUSE_HOST` or `https://cloud.langfuse.com` | Langfuse host (self-hosted supported). |
| `fetch` | `globalThis.fetch` | Injectable fetch (used in tests). |
| `traceName` | the model id | Name on the emitted trace. |
| `metadata` | none | Static metadata merged into every trace. |
| `onError` | swallow | Called with any ingestion failure. |

`flush()` resolves once all in-flight ingestion sends complete. `stream()` is
exposed only when the wrapped model streams; it accumulates the streamed text and
the final usage chunk for the generation record.

## What gets sent

Per call, one batch with two events to `POST {baseUrl}/api/public/ingestion`
(HTTP Basic auth):

- `trace-create` with the input messages and the output text.
- `generation-create` with the model id, input, output, mapped token usage
(`{ input, output, total, unit: "TOKENS" }`), `startTime`/`endTime`, and on
failure `level: "ERROR"` plus the error message.
73 changes: 73 additions & 0 deletions packages/base-llm/docs/moa.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Mixture of Agents (MoA)

`createMoaModel` wraps N proposer models and one aggregator into a single model
that conforms to the base-llm port (`{ id, complete, stream? }`). It drops in
anywhere a normal model is used: settings, Studio, the CLI.

It follows the Mixture-of-Agents pattern (Wang et al. 2024), the shape Sakana
Hermes' `moa` provider uses: many models answer in parallel, then one model
synthesizes their drafts into the final answer. There is no ground-truth judge
in chat, so the aggregator LLM is the selector.

## Flow

```mermaid
flowchart TD
R[Request<br/>messages + tools] --> P1[Proposer 1<br/>tools stripped]
R --> P2[Proposer 2<br/>tools stripped]
R --> P3[Proposer N<br/>tools stripped]
P1 --> D[Drafts]
P2 --> D
P3 --> D
D --> A[Aggregator<br/>drafts folded into system message<br/>original tools kept]
A --> O[Final answer<br/>usage summed across all calls]
```

Key rules:

- Proposers run in parallel and receive the request with tools removed. They
draft prose; they do not act. Merging N independent tool-call sets is
undefined, so only the aggregator gets tools.
- The aggregator receives the original conversation plus the drafts as private
guidance folded into the system message (never shown to the user), with the
original tools intact so tool-calling still works.
- If some proposers fail, MoA synthesizes from the survivors. It throws only if
every proposer fails.
- `usage` is summed across all proposer and aggregator calls.

## Usage

```js
import {
createMoaModel,
createOpenAICompatibleModel,
} from "@ai-swiss/base-llm";

const moa = createMoaModel({
proposers: [
createOpenAICompatibleModel({ model: "gpt-5.5" }),
createOpenAICompatibleModel({ baseUrl: openrouter, model: "deepseek/deepseek-v4-pro" }),
],
aggregator: createOpenAICompatibleModel({ baseUrl: openrouter, model: "qwen3-max" }),
});

const out = await moa.complete({ messages: [userMessage("Explain CRDTs.")] });
```

## Options

| Option | Default | Meaning |
| --- | --- | --- |
| `proposers` | (required) | Non-empty list of models that draft in parallel. |
| `aggregator` | (required) | Model that synthesizes the drafts. |
| `id` | `"moa"` | Value reported as `model.id`. |
| `synthesisPrompt` | `DEFAULT_SYNTHESIS_PROMPT` | Aggregator guidance preamble. |

`stream()` is exposed only when the aggregator supports streaming; proposers are
gathered first, then the aggregator streams.

## MoA vs Triumvirat

MoA is one-shot breadth: parallel proposals, one synthesis pass, cheap. For
sequential plan / execute / verify with self-correction, see Triumvirat
(`createTriumviratModel`).
Loading