Skip to content
Merged
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
56 changes: 55 additions & 1 deletion bin/cli/commands/quota.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { apiFetch, isServerUp } from "../api.mjs";
import { t } from "../i18n.mjs";

export function registerQuota(program) {
program
const quota = program
.command("quota")
.description(t("quota.description"))
.option("--provider <id>", "Filter by provider")
Expand All @@ -12,6 +12,60 @@ export function registerQuota(program) {
const exitCode = await runQuotaCommand({ ...opts, output: globalOpts.output });
if (exitCode !== 0) process.exit(exitCode);
});

quota
.command("status")
.description("Show truthful OmniRoute gateway, quota, pool, and circuit state")
.action(async (opts, cmd) => runBoundedJson("/api/omniroute/status", cmd.optsWithGlobals()));

quota
.command("preview")
.description("Preview allocation enforcement without an upstream request")
.requiredOption("--api-key-id <id>", "API key id")
.requiredOption("--pool-id <id>", "quota pool id")
.option("--tokens <n>", "estimated token usage")
.action(async (opts, cmd) => {
const params = new URLSearchParams({ apiKeyId: opts.apiKeyId, poolId: opts.poolId });
if (opts.tokens != null) params.set("estimatedTokens", opts.tokens);
await runBoundedJson(`/api/quota/preview?${params}`, cmd.optsWithGlobals());
});

quota
.command("ensure <json>")
.description("Idempotently create or update a quota pool from a JSON object")
.action(async (json, opts, cmd) => {
let body;
try {
body = JSON.parse(json);
} catch {
console.error("Invalid pool JSON");
process.exit(2);
}
await runBoundedJson("/api/quota/pools?ensure=true", cmd.optsWithGlobals(), {
method: "POST",
body,
});
});
}

async function runBoundedJson(path, opts, request = {}) {
const started = performance.now();
const res = await apiFetch(path, {
...request,
retry: false,
timeout: Math.min(opts.timeout ?? 5000, 5000),
acceptNotOk: true,
});
const elapsed = Math.round(performance.now() - started);
if (process.env.OMNIROUTE_DEBUG === "1") {
console.error(`[omniroute] ${request.method ?? "GET"} ${path} completed in ${elapsed}ms`);
}
const payload = await res.json().catch(() => ({ error: `HTTP ${res.status}` }));
if (!res.ok) {
console.error(JSON.stringify(payload));
process.exit(res.exitCode ?? 1);
}
console.log(JSON.stringify(payload, null, 2));
}

export async function runQuotaCommand(opts = {}) {
Expand Down
12 changes: 5 additions & 7 deletions bin/cli/utils/cliToken.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,11 @@ let _cached = null;
export async function getCliToken() {
if (_cached !== null) return _cached;
try {
const { machineIdSync } = await import("node-machine-id");
const mid = machineIdSync();
_cached = crypto
.createHash("sha256")
.update(mid + SALT)
.digest("hex")
.substring(0, 32);
const module = await import("node-machine-id");
const machineIdSync = module.machineIdSync ?? module.default?.machineIdSync;
if (typeof machineIdSync !== "function") throw new Error("machine-id API unavailable");
const mid = machineIdSync(true);
_cached = crypto.createHmac("sha256", mid).update(SALT).digest("hex");
} catch {
_cached = "";
}
Expand Down
20 changes: 0 additions & 20 deletions config/quality/eslint-suppressions.json
Original file line number Diff line number Diff line change
Expand Up @@ -408,11 +408,6 @@
"count": 1
}
},
"src/app/api/combos/route.ts": {
"no-restricted-imports": {
"count": 1
}
},
"src/app/api/combos/test/route.ts": {
"no-restricted-imports": {
"count": 1
Expand Down Expand Up @@ -963,11 +958,6 @@
"count": 1
}
},
"src/app/api/v1/models/catalog.ts": {
"no-restricted-imports": {
"count": 1
}
},
"src/app/api/v1/rerank/route.ts": {
"no-restricted-imports": {
"count": 1
Expand Down Expand Up @@ -1043,11 +1033,6 @@
"count": 1
}
},
"src/lib/combos/controlCenter.ts": {
"no-restricted-syntax": {
"count": 1
}
},
"src/lib/container.ts": {
"no-restricted-imports": {
"count": 1
Expand Down Expand Up @@ -2046,11 +2031,6 @@
"count": 5
}
},
"tests/unit/combo-context-length.test.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 2
}
},
"tests/unit/combo-health-route.test.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 3
Expand Down
9 changes: 9 additions & 0 deletions docs/OMNIROUTE_ALLOCATION_HANDOFF.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# OmniRoute Allocation Handoff

Allocation is not provider quota.

Quota pools define which API keys may consume a provider pool and how hard, soft, or burst policies apply. Provider quota is external capacity reported by a provider or an explicitly configured source. Ghostlight internal budgets are governance limits defined by the administrator.

The `ensurePool` operation is idempotent: an identical pool is unchanged, a changed allocation is updated, and a missing pool is created. This is intended for automation and bounded API callers.

The read-only status endpoint is `GET /api/omniroute/status`. The verification command is `npm run omniroute:verify`; it makes no live model request.
9 changes: 9 additions & 0 deletions docs/OMNIROUTE_PROVIDER_FAILOVER.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# OmniRoute Provider Failover

Failures are classified before retry decisions are made.

Transient failures such as timeouts, network errors, rate limits, and provider 5xx responses may fail over. Authentication errors, permission errors, invalid requests, unavailable models, and unknown failures are not retried blindly.

The default cross-provider policy allows up to three provider attempts, retries rate limits and timeouts, and keeps administrative disablement separate from temporary circuit state.

Circuit states are `closed`, `open`, and `half_open`. A cooldown schedules a bounded probe; a successful probe closes the circuit and a failed probe reopens it.
17 changes: 17 additions & 0 deletions docs/OMNIROUTE_QUOTA_TELEMETRY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# OmniRoute Quota Telemetry

OmniRoute separates provider quota telemetry from Ghostlight accounting.

## Truthful states

- `healthy` means a source reported usable remaining capacity.
- `approaching_limit` means a source reported remaining capacity at or below the configured threshold.
- `exhausted` is emitted only when a source reports zero capacity or usage at its limit.
- `unavailable` means a supported source failed to return data.
- `unknown` means no supported source exists or no provider limit is known.

Unknown is not exhausted and does not disable a provider.

Sources are preferred in this order: official provider API, authenticated usage API, explicitly mapped response headers, administrator configuration, local estimates, unknown. Local estimates are never presented as provider billing data.

Response headers are parsed only through an explicit provider mapping. Generic header names are not assumed globally.
11 changes: 11 additions & 0 deletions docs/OMNIROUTE_ROUTING_POLICY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# OmniRoute Routing Policy

Routing preserves the existing capability and combo selection logic, then applies allocation, health, circuit, quota, latency, reliability, model preference, and cost preference factors.

The adaptive score is explainable and returns both the selected candidate and all ranked candidates. Exhausted quota, denied allocation, and open circuits are ineligible. Unknown quota remains eligible with a neutral quota factor.

Route preview is deterministic and performs zero upstream model requests:

`POST /api/omniroute/route/preview`

The response includes candidate scores, factors, reasons, the selected provider, and `liveRequestExecuted: false`.
34 changes: 34 additions & 0 deletions docs/architecture/RESILIENCE_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,40 @@ Before #7274, `resolveSessionAffinityTtlMs()` hard-bailed to `0` for every provi

The three session-affinity headers are never forwarded upstream — executors build their own upstream headers from scratch rather than passing client headers through, so this stays an internal correlation id only.

### Exclusive managed session connection leases

**Scope:** one active managed HTTP client/session owns one eligible OmniRoute connection.

**Purpose:** provide durable exclusive connection ownership for clients that need a hard routing
fence across requests. This differs from session affinity, which is a soft continuity preference:
an exclusive lease persists lifecycle state in SQLite, enforces global active-owner and
active-connection uniqueness, and rejects a stale generation before provider dispatch.

The feature is opt-in per API key. A managed key must have the `lease:exclusive` scope and an
explicit non-empty `allowedConnections` list. Any HTTP client can use the lifecycle endpoint; no
client name, user-agent, provider, OAuth method, or model is required. The lease owns a connection,
not a model, so a model change retains the binding while the connection remains ordinarily
eligible. Normal model, quota, health, cooldown, and allowlist rules remain authoritative and may
transition the same generation to another free eligible connection.

The lifecycle is `POST /api/v1/session-leases` with JSON actions `acquire`, `renew`, and `release`.
Managed inference requests present the opaque `X-OmniRoute-Lease-Owner` value and exact
`X-OmniRoute-Lease-Generation`. The owner uses `vlo_` followed by 43 base64url characters; only
its SHA-256 hash is stored. Every final dispatch fence also binds the authenticated API key ID and
active connection ID. Lease control headers are removed from logs, retained request snapshots, and
upstream executor headers.

If ordinary routing has eligible managed candidates but every free candidate is occupied by a
foreign active lease, OmniRoute returns HTTP `429`, lease-capacity-unavailable code, a
waiting-for-capacity state, and a bounded `Retry-After` derived from the earliest relevant expiry.
Ordinary empty eligibility is not lease contention and keeps its existing routing error semantics.

Related mechanisms remain separate:

- OAuth session occupancy is process-local soft distribution for OAuth accounts.
- Account semaphores grant request-concurrency permits and end when a request completes.
- Exclusive managed session leases are durable lifecycle ownership with a generation fence.

---

## 3. Model Lockout
Expand Down
Loading
Loading