NAME

Langertha::Skeid - Dynamic routing control-plane for multi-node LLM serving with normalized metrics and cost accounting

VERSION

version 0.001

SYNOPSIS

use Langertha::Skeid;

my $skeid = Langertha::Skeid->new(
  config_file => '/etc/skeid/config.yaml',
);

my $cost = $skeid->call_function('metrics.estimate_cost', {
  model => 'gpt-4o-mini',
  usage => { prompt_tokens => 1000, completion_tokens => 200 },
});

DESCRIPTION

Langertha::Skeid is a routing control-plane for provider-style LLM operations. It keeps a live node table, routes by model/health/capacity, and records normalized token/cost usage.

Skeid is commonly used as one API edge in front of many upstream APIs (cloud + local). With pricing and usage.record/report, you can build tenant billing from one consistent ledger.

Multi-API Billing Flow

1. Define multiple nodes in config (for example OpenAI-compatible cloud APIs and local vLLM/SGLang). 2. Set model pricing via pricing or pricing.set. 3. Forward tenant identity via x-skeid-key-id (or x-api-key-id). 4. Read totals by key/model/time with usage.report.

Engine IDs

nodes[].engine uses lowercased engine class names from Langertha. Examples: OpenAI => openai, OpenAIBase => openaibase, vLLM => vllm. Legacy aliases like openai-compatible are intentionally rejected.

Pluggable Usage Storage

The usage storage layer is pluggable. Built-in backends are jsonlog (recommended, no DBI required), sqlite, and postgresql. You can also replace the storage layer entirely via constructor callbacks or subclass override.

jsonlog backend (recommended — no DBI dependency):

# Directory mode: one JSON file per event (no collision risk)
my $skeid = Langertha::Skeid->new(
  usage_store => { backend => 'jsonlog', path => '/var/log/skeid/events/' },
);

# File mode: JSON-lines appended to a single file
my $skeid = Langertha::Skeid->new(
  usage_store => { backend => 'jsonlog', path => '/var/log/skeid/usage.jsonl', mode => 'file' },
);

Directory mode is auto-detected when the path is an existing directory or ends with /. It writes one .json file per event, which avoids file-level locking and concurrent-write collisions entirely.

Constructor callbacks (custom backend, no subclassing):

my $skeid = Langertha::Skeid->new(
  store_usage_event => sub {
    my ($self, $event) = @_;
    # $event is a hashref with all 22 normalized columns
    publish_to_nats($event);
    return { ok => 1 };
  },
  query_usage_report => sub {
    my ($self, $filters) = @_;
    # $filters has: since, api_key_id, model, limit
    return { ok => 1, enabled => 1, totals => { ... } };
  },
);

Option 2 – Subclass override:

package MyApp::Skeid;
use Moo;
extends 'Langertha::Skeid';

sub _store_usage_event {
  my ($self, $event) = @_;
  ...
  return { ok => 1 };
}

sub _query_usage_report {
  my ($self, $filters) = @_;
  ...
}

When a callback or override is provided, the DBI default is bypassed entirely and no database connection is created. DBI and DBD::SQLite are recommends dependencies — they are not required when usage is handled externally.

Admin API Key

admin.api_key (or admin_api_key) controls access to proxy admin routes. If empty, admin routes are effectively disabled by returning 404. If set, the proxy expects Authorization: Bearer .... This value can be changed through dynamic config reload.

SUPPORT

Issues

Please report bugs and feature requests on GitHub at https://github.com/Getty/langertha-skeid/issues.

CONTRIBUTING

Contributions are welcome! Please fork the repository and submit a pull request.

AUTHOR

Torsten Raudssus <torsten@raudssus.de> https://raudssus.de/

COPYRIGHT AND LICENSE

This software is copyright (c) 2026 by Torsten Raudssus.

This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.