Skip to main content
Glama

mcp-model-router

MCP-palvelin, joka täydentää LiteLLM:ää eikä korvaa sitä. LiteLLM/OpenRouter hoitavat jo agenttikohtaiset avaimet ja kulukatot - tämä palvelin ei rakenna niitä uudelleen. Se tekee kolme asiaa:

  1. Mallin valinta tehtävätyypin mukaan - lukee config.yaml:sta tehtävätyyppikohtaiset mallit (esim. general, code, writing, orchestration) ja kutsuu niitä OpenAI-yhteensopivan /chat/completions -rajapinnan kautta. Base URL ja avain tulevat ympäristömuuttujista, ja rajapinta voi olla joko LiteLLM-proxy tai suoraan OpenRouter. Ei suoria Anthropic-kutsuja. Jos päämalli epäonnistuu, käytetään konfiguroitua varamallia.

  2. Kulutus ja budjetti - lukee ne suoraan LiteLLM:n (/user/info) tai OpenRouterin (/credits) rajapinnasta. Ei omaa kulutuskirjanpitoa.

  3. Eval-vertailu - ajaa saman prompt-sarjan (JSON-tiedosto) usealla mallilla ja raportoi hinnan, viiveen ja vastauksen rinnakkain, jotta halvempi malli voidaan valita mitattuun laatuun perustuen. Laatu arvioidaan yksinkertaisella sisältää/vastaa-tarkistuksella ja valinnaisella LLM-tuomarilla.

SQLite-tietokantaa käytetään vain kutsulokille ja eval-tuloksille - ei kulutuksen tai budjetin kirjanpitoon.

Nopea kokeilu

Neljällä komennolla ensimmäinen route_and_complete-kutsu HTTP-tilassa (oleta, että LiteLLM pyörii jo osoitteessa http://localhost:4000 ja siinä on ainakin general-tehtävän malli konfiguroitu):

npm install && npm run build
cp config.example.yaml config.yaml   # muokkaa tarpeen mukaan
LLM_BASE_URL=http://localhost:4000 LLM_API_KEY=sk-... MCP_TRANSPORT=http MCP_HTTP_PORT=3100 npm start &
curl -s -X POST http://127.0.0.1:3100/mcp \
  -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"route_and_complete","arguments":{"taskType":"general","prompt":"Sano hei suomeksi"}}}'

Neljäs komento tekee tools/call-pyynnön palvelimen /mcp-päätepisteeseen ja tulostaa vastauksen (streamable HTTP -tapahtumavirtana). Tarkemmat ympäristömuuttujat, stdio-siirtotapa ja GET /healthz -terveystarkastus: ks. Asennus ja Ajaminen.

Related MCP server: ComputeGauge MCP

Arkkitehtuuri

                       ┌─────────────────────────┐
 AgentScope-agentti ──▶│   mcp-model-router (MCP) │
 (tai muu MCP-klientti) │                          │
                       │  route_and_complete       │
                       │  list_models              │
                       │  get_usage                │──▶ LiteLLM-proxy / OpenRouter
                       │  run_eval                 │      (OpenAI-yhteensopiva API)
                       └───────────┬──────────────┘
                                   │
                                   ▼
                         SQLite (call_log, eval_result)
  • Siirtotavat: stdio (paikalliseen käyttöön, esim. Claude Desktop/Cursor) ja streamable HTTP (jotta muut kontit tavoittavat palvelimen Tailscale- verkossa). HTTP-tila on tilaton (sessionIdGenerator: undefined) - jokainen pyyntö saa oman transport-instanssinsa, joten useat agentit voivat kutsua palvelinta yhtäaikaisesti. Palvelin ei toteuta omaa kirjautumista - pääsy rajataan verkkotasolla (Tailscale ACL:t).

  • Konfiguraatio: config.yaml (ympäristömuuttujaviittauksin, ei salaisuuksia tiedostossa) + varsinaiset arvot ympäristömuuttujista.

  • Kontekstin kokoa ei rajoiteta keinotekoisesti - maxTokens on valinnainen parametri per kutsu, ei kovakoodattu yläraja.

Asennus

Vaatii Node.js 20, 22 tai 24.

npm install
cp config.example.yaml config.yaml   # muokkaa tarpeen mukaan
npm run build

Ympäristömuuttujat (esimerkkiarvot, ei salaisuuksia repossa):

Muuttuja

Kuvaus

Oletus

LLM_BASE_URL

OpenAI-yhteensopivan rajapinnan base URL

http://localhost:4000

LLM_API_KEY

Avain edelliseen rajapintaan

(tyhjä)

LLM_PROVIDER_KIND

litellm tai openrouter - katso Mallitunnisteet: LiteLLM vs. suora OpenRouter

litellm

USAGE_BASE_URL

Kulutusrajapinnan base URL (yleensä sama kuin LLM_BASE_URL)

http://localhost:4000

USAGE_API_KEY

Avain kulutusrajapintaan

(tyhjä)

CONFIG_PATH

Polku config.yaml-tiedostoon

config.yaml

DB_PATH

SQLite-tiedoston polku

./data/mcp-model-router.sqlite

MCP_TRANSPORT

stdio tai http

stdio

MCP_HTTP_HOST

HTTP-siirtotavan bind-osoite

127.0.0.1

MCP_HTTP_PORT

HTTP-siirtotavan portti (oletus vaihdettu 3100:aan, koska 3000 on usein varattu esim. Open WebUI:lle)

3100

MCP_ALLOWED_HOSTS

Pilkulla eroteltu lista sallittuja Host-otsikoita (DNS-rebinding-suojaus)

(ei asetettu)

Ajaminen

# stdio (esim. Claude Desktop / Cursor)
npm start

# streamable HTTP (muut kontit Tailscale-verkossa)
MCP_TRANSPORT=http MCP_HTTP_HOST=0.0.0.0 MCP_HTTP_PORT=3100 npm start

HTTP-tilassa palvelin vastaa osoitteessa http://<host>:<port>/mcp ja tarjoaa lisäksi GET /healthz -terveystarkastuksen. Stdio-tilassa vastaava tools/call-pyyntö syötetään palvelimelle stdinistä yksittäisenä JSON-RPC-viestinä (ks. Nopea kokeilu HTTP-versiosta).

config.yaml-esimerkki

Ks. koko esimerkki tiedostossa config.example.yaml. Tiivistettynä:

provider:
  baseUrl: ${LLM_BASE_URL:-http://localhost:4000}
  apiKey: ${LLM_API_KEY:-}
  kind: ${LLM_PROVIDER_KIND:-litellm}   # litellm | openrouter

tasks:
  general:
    model: openrouter/deepseek/deepseek-v4.1-flash
    fallbackModel: openrouter/qwen/qwen3-flash
  code:
    model: openrouter/qwen/qwen3-coder
    fallbackModel: openrouter/deepseek/deepseek-v4.1-flash
  orchestration:
    model: openrouter/openai/gpt-4o
    timeoutMs: 120000   # ohittaa provider.timeoutMs:n tälle tehtävätyypille

usage:
  provider: litellm   # litellm | openrouter
  baseUrl: ${USAGE_BASE_URL:-http://localhost:4000}
  apiKey: ${USAGE_API_KEY:-}

eval:
  judge:
    enabled: false
    model: openrouter/openai/gpt-4o-mini   # eri malli kuin arvioitavat mallit, ks. alla
  concurrency: 4   # montako (malli, prompt) -kutsua ajetaan samanaikaisesti
  pricing:
    openrouter/deepseek/deepseek-v4.1-flash:
      inputPerMillionUsd: 0.2
      outputPerMillionUsd: 0.8

database:
  path: ${DB_PATH:-./data/mcp-model-router.sqlite}

Mallitunnisteet: LiteLLM vs. suora OpenRouter

tasks-lohkon mallitunnisteet kirjoitetaan aina LiteLLM:n käyttämässä muodossa, esim. openrouter/deepseek/deepseek-v4.1-flash. openrouter/- etuliite on LiteLLM:n oma tapa kertoa, että pyyntö reititetään OpenRouterin kautta - se ei ole osa OpenRouterin omaa mallitunnistetta.

  • provider.kind: litellm (oletus, LLM_BASE_URL osoittaa LiteLLM- proxyyn): tunniste lähetetään sellaisenaan, etuliite mukaan lukien.

  • provider.kind: openrouter (LLM_BASE_URL osoittaa suoraan https://openrouter.ai/api/v1): reititin karsii openrouter/-etuliitteen automaattisesti ennen rajapintakutsua (ks. src/lib/modelId.ts), joten sama config.yaml toimii sellaisenaan molemmilla - vain LLM_BASE_URL/LLM_PROVIDER_KIND vaihtuvat.

Tehtävätyyppikohtainen aikakatkaisu (timeoutMs)

tasks.<taskType>.timeoutMs on valinnainen ja ohittaa provider.timeoutMs:n vain kyseiselle tehtävätyypille (vaikuttaa route_and_complete-kutsuihin, sekä päämalliin että varamalliin). Oletuksena kaikki tehtävätyypit käyttävät provider.timeoutMs:ää (oletus 60000 ms).

Tämä on hyödyllinen erityisesti päättelymalleille (reasoning-malleilla), jotka voivat kestää huomattavasti kauemmin monimutkaisissa pilkkomis- tai orkestrointitehtävissä kuin yksinkertaisissa yleistiedon tai lyhyen koodin tehtävissä - ks. Esimerkki: mitattu vertailu, jossa "pilkkominen"-tehtävätyyppi aiheutti aikakatkaisuja 60 s oletusaikakatkaisulla:

tasks:
  orchestration:
    model: openrouter/deepseek/deepseek-v4.1-flash
    timeoutMs: 120000   # 2 min, oletuksen sijaan 60000 ms (1 min)

MCP-työkalut

Työkalu

Kuvaus

route_and_complete

Valitsee mallin taskType-parametrin mukaan ja täydentää promptin. Käyttää varamallia tarvittaessa.

list_models

Listaa config.yaml:n tehtävätyypit ja niihin liitetyt mallit.

get_usage

Palauttaa kulutuksen ja budjetin tilan LiteLLM:stä/OpenRouterista.

run_eval

Ajaa JSON-prompt-sarjan usealla mallilla ja raportoi hinnan/viiveen/laadun.

Eval-prompt-tiedoston muoto, ks. esimerkki examples/eval-prompts.example.json:

[
  { "id": "p1", "prompt": "Mikä on Suomen pääkaupunki?", "expectedContains": "Helsinki" }
]

Laajempi, 15-20 suomenkielisen promptin esimerkkisarja neljälle tehtävätyypille (yleinen tieto, koodaus, kirjoitus/tiivistys, tehtävän pilkkominen) löytyy tiedostosta examples/eval-prompts.fi.json.

Eval-ajo (run_eval)

run_eval on MCP-työkalu, ei erillinen CLI-komento - se kutsutaan samalla tavalla kuin muutkin työkalut (tools/call, ks. Nopea kokeilu). Esimerkki HTTP-tilassa, kahdella mallilla ja eval-prompts.fi.json-sarjalla:

curl -s -X POST http://127.0.0.1:3100/mcp \
  -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
  -d '{
    "jsonrpc":"2.0","id":1,"method":"tools/call",
    "params":{
      "name":"run_eval",
      "arguments":{
        "promptSetPath":"examples/eval-prompts.fi.json",
        "models":["openrouter/deepseek/deepseek-v4.1-flash","openrouter/qwen/qwen3-flash"],
        "judge": false
      }
    }
  }'
  • models-lista käyttää samoja LiteLLM model_name-aliaksia kuin config.yaml:n tasks-lohko (ks. Tunnetut rajoitukset) - ei tarvitse olla sama malli kuin jonkin taskType:n oletusmalli.

  • judge-parametri (valinnainen) ohittaa config.yaml:n eval.judge.enabled-asetuksen yksittäiselle ajolle. Kun tuomari on päällä, jokainen vastaus lähetetään lisäksi eval.judge.model:lle arvioitavaksi asteikolla 1-5 (config.yaml:n eval.judge.systemPrompt tai oletusarvo) - tämä tekee saman määrän ylimääräisiä mallikutsuja kuin on prompt-tapauksia, joten se maksaa ja kestää enemmän.

  • Jos db-instanssi on käytössä (näin on aina, kun palvelin käynnistetään dist/index.js:llä - ks. src/index.ts), jokainen ajo ja tulosrivi tallennetaan SQLiteen (eval_run/eval_result-taulut) DB_PATH:n osoittamaan tiedostoon, joten ajoja voi vertailla jälkikäteen myös suoraan tietokannasta.

  • Eval voi tehdä paljon mallikutsuja: esim. 2 mallia × 20 promptia = 40 (malli, prompt) -paria, ja jokainen kutsu voi kestää useita sekunteja (tai provider.timeoutMs/tehtävätyypin oman timeoutMs:n verran, jos malli jumiutuu) - ks. Esimerkki: mitattu vertailu. judge: true lähes tuplaa ajoajan, koska jokainen vastaus arvioidaan vielä erikseen ylimääräisellä mallikutsulla.

Rinnakkaisajo (eval.concurrency)

run_eval ajaa (malli, prompt) -parit rinnakkain enintään config.yaml:n eval.concurrency-asetuksen verran samanaikaisesti (oletus 4, minimi 1). Toteutus on yksinkertainen rajoitettu työjono (mapWithConcurrency, ks. src/eval/runEval.ts) ilman uusia riippuvuuksia: kiinteä määrä "workereita" nostaa jonosta seuraavan käsittelemättömän tehtävän kerrallaan, kunnes jono on tyhjä.

Kutsut ovat käytännössä koko ajan I/O-odotusta (verkkovastaus mallilta), eivät CPU-työtä, joten rinnakkaisajo lyhentää kokonaisajoaikaa suoraan suhteessa concurrency-arvoon - esim. 2 mallia × 20 promptia × judge päällä (40 vastauskutsua + 40 judge-kutsua = 80 kutsua) kesti aiemmin peräkkäin ajettuna noin 12 minuuttia; concurrency: 4:llä samat kutsut menevät neljä kerrallaan, mikä lyhentää ajoaikaa merkittävästi samalla prompt-sarjalla ja mallivalinnalla.

Huomioita:

  • Tulosten järjestys säilyy riippumatta siitä, missä järjestyksessä yksittäiset kutsut valmistuvat: cases-taulukko on aina samassa järjestyksessä kuin peräkkäin ajettuna (malli ulompana, prompt sisempänä, input.models/prompt-tiedoston järjestyksessä).

  • Virheenkäsittely on edelleen per kutsu: yhden kutsun aikakatkaisu tai muu virhe (myös judge-kutsun virhe) ei keskeytä muita rinnakkaisia tai jonossa odottavia kutsuja - se näkyy vain kyseisen tapauksen error- kentässä, kuten peräkkäisessäkin ajossa.

  • Miksi oletus on 4, ei enemmän: liian suuri rinnakkaisuus voi ylikuormittaa LiteLLM-proxyn tai OpenRouterin rate limitit, jolloin osa kutsuista alkaa epäonnistua 429-virheillä sen sijaan, että eval oikeasti nopeutuisi. Nosta arvoa varovasti oman proxyn/API-avaimen rajojen mukaan, tai laske se 1:een, jos haluat täysin peräkkäisen (deterministisen ajoituksen kannalta yksinkertaisimman) ajon esim. vianetsintää varten.

Tulosten lukeminen (EvalReport, ks. src/eval/runEval.ts):

  • cases: yksi rivi per (malli, prompt) - sisältää latencyMs, response-tekstin, passed (true/false/null jos expectedContains-kenttää ei annettu kyseiselle promptille), judgeScore (1-5 tai null), priceUsd (null, jos mallille ei ole hinnastoa config.yaml:n eval.pricing-lohkossa) ja error (virheviesti tai null).

  • perModel: yhteenveto mallia kohden - avgLatencyMs, passRate (expectedContains-läpäisyosuus 0-1 virheelliset kutsut pois laskettuna, null jos yhdessäkään promptissa ei ollut expectedContains-kenttää), avgJudgeScore, avgPriceUsd ja errorCount/caseCount. Tämä on nopein tapa verrata malleja: matalampi avgPriceUsd ja avgLatencyMs samalla kun passRate/avgJudgeScore pysyy riittävän korkeana kertoo, mikä malli kannattaa valita config.yaml:n tasks-lohkoon.

  • perTaskType: sama yhteenveto ryhmiteltynä (malli, tehtävätyyppi) -pareittain. Tehtävätyyppi päätellään promptin id:n etuliitteestä pudottamalla lopusta -<numero>, esim. "yleinen-01" → "yleinen", "pilkkominen-05" → "pilkkominen" (ks. taskTypeFromPromptId tiedostossa src/eval/runEval.ts) - tämä ei liity config.yaml:n tasks-avaimiin, vaan on pelkkä nimeämiskäytäntö prompt-tiedostossa. Jokainen rivi sisältää:

    • caseCount, errorCount - montako tapausta ja niistä montako virheitä.

    • passedCount/expectedCount - montako läpäisi avainsanatarkistuksen niistä tapauksista, joissa expectedContains oli annettu (= "läpäisty/n").

    • passRateExcludingErrors - läpäisyosuus laskettuna vain onnistuneista kutsuista (virheelliset kutsut pois sekä osoittajasta että nimittäjästä). Kertoo vastauksen laadun, kun malli ylipäätään vastasi.

    • passRateIncludingErrors - läpäisyosuus laskettuna niin, että virheet (esim. aikakatkaisu) lasketaan läpäisemättömiksi (mukana nimittäjässä). Kertoo tehtävätyypin kokonaisluotettavuuden mallilla.

    • avgLatencyMs, avgPriceUsd - kuten perModel:ssa, mutta rajattuna tähän tehtävätyyppiin.

  • MCP-työkalun tekstivastaus tiivistää sekä perModel- että perTaskType-rivit ihmisluettavaksi; koko EvalReport (mukaan lukien cases) on saatavilla ohjelmallisesti structuredContent-kentässä.

Judge-tila (eval.judge) - luotettavampi laatumittari

expectedContains-avainsanatarkistus on karkea: se hylkää hyvänkin vastauksen, jos se ei sisällä täsmälleen odotettua sanaa (ks. Esimerkki: mitattu vertailu - erityisesti kirjoitus/tiivistystehtävissä tämä antaa harhaanjohtavan matalan läpäisyprosentin). LLM-tuomari arvioi vastauksen laadun asteikolla 1-5 riippumatta siitä, osuuko vastaus täsmälleen odotettuun sanamuotoon, ja on siksi luotettavampi mittari erityisesti avoimissa (kirjoitus-, tiivistys-, pilkkomis-) tehtävissä.

Ota judge käyttöön joko pysyvästi config.yaml:ssa tai yksittäiselle ajolle:

eval:
  judge:
    enabled: true
    model: openrouter/openai/gpt-4o-mini   # tuomarina toimiva malli
    systemPrompt: "Olet tiukka arvioija. Anna vastauksen laadulle asteikolla 1-5 pelkkä numero."

tai välitä "judge": true run_eval-kutsun arguments-kenttään (ks. esimerkki yllä) - tämä ohittaa config.yaml:n asetuksen vain kyseiselle ajolle. Tuloksena EvalCaseResult.judgeScore/EvalModelSummary.avgJudgeScore täyttyvät null:n sijaan. Huomaa hintavaikutus: judge tekee yhden ylimääräisen mallikutsun jokaista prompt-tapausta kohden, joten se sekä maksaa että kestää suunnilleen kaksinkertaisesti verrattuna avainsanatarkistukseen.

Valitse eval.judge.model eri malliksi kuin arvioitavat mallit. Malli suosii tyypillisesti omaa vastaustyyliään (esim. omaa tapaansa jäsentää vastaus tai muotoilla lauseet), jolloin sama malli tuomarina antaisi korkeampia pisteitä omaa tyyliään muistuttaville vastauksille - tämä vinouttaa vertailua juuri sitä mallia suosivaksi. Käytä siis tuomarina mallia, joka ei ole yksikään run_eval-kutsun models-listalla olevista.

Esimerkki: mitattu vertailu

Alla yhden oikean run_eval-ajon tulokset (examples/eval-prompts.fi.json, 20 promptia, judge pois päältä), joissa verrattiin kahta OpenRouterin kautta LiteLLM-proxyn taakse ajettua mallia: deepseek-v4.1-flash vs. qwen3-coder.

Kokonaisuus:

Malli

Läpäisy

Viive ka.

Hinta ka./kutsu

deepseek-v4.1-flash

83 % (15/18, 2 aikakatkaisua)

14 802 ms

0,000744 USD

qwen3-coder

85 % (17/20)

9 887 ms

0,000466 USD

Tehtävätyypeittäin (läpäisty/n, viive ka., hinta ka. USD/kutsu):

Tehtävätyyppi

deepseek-v4.1-flash

qwen3-coder

yleinen

5/5, 2 190 ms, 0,000119

5/5, 5 600 ms, 0,000159

koodaus

5/5, 4 025 ms, 0,000278

5/5, 3 742 ms, 0,000188

kirjoitus

3/5, 7 841 ms, 0,000416

3/5, 1 567 ms, 0,000121

pilkkominen

2/5 (+2 aikakatkaisua), 45 151 ms, 0,001867

4/5, 28 640 ms, 0,001395

Tulkinta: kokonaisläpäisy on lähellä molemmilla malleilla, mutta qwen3-coder on sekä nopeampi että halvempi joka tehtävätyypissä tässä ajossa, ja se selvisi pilkkomistehtävistä ilman aikakatkaisuja. deepseek-v4.1-flash:n kaksi aikakatkaisua osuivat molemmat pilkkomistehtäviin (60 s oletusaikakatkaisulla) - ks. Tehtävätyyppikohtainen aikakatkaisu, joka on juuri tätä varten.

Rajoitukset - lue nämä luvut varoen:

  • Pieni otos: vain 5 promptia per tehtävätyyppi (20 yhteensä). Yksikin tapaus muuttaa prosenttilukua 20 %-yksiköllä; tuloksia ei pidä yleistää suoraan muihin promptteihin tai käyttötapauksiin.

  • Avainsanatarkistus arvioi kirjoitustehtäviä huonosti: expectedContains hylkää muuten hyvän vastauksen, jos se ei sisällä täsmälleen odotettua sanaa - kirjoitus/tiivistys-sarakkeen 3/5-luvut molemmilla malleilla todennäköisesti aliarvioivat todellisen laadun. Ks. Judge-tila luotettavampaa vaihtoehtoa varten.

  • Deepseekin aikakatkaisut nostivat sen keskiviivettä ja hintaa pilkkomis-sarakkeessa merkittävästi (60 s per aikakatkaisu lasketaan mukaan viiveeseen) - ilman niitä keskiviive olisi ollut matalampi.

  • Tulokset ovat yhdestä ajosta (run be440639-9aa7-4330-8c66-8cb2ed27b9ef) tiettynä ajankohtana ja riippuvat mallien senhetkisistä versioista sekä OpenRouterin kuormasta/saatavuudesta - ne eivät ole pysyvä benchmark-tulos, vaan esimerkki siitä, miten run_eval:n tulosraporttia luetaan. Aja oma eval omilla prompteillasi ennen tuotantopäätöksiä.

Claude Desktop / Cursor -asetus

claude_desktop_config.json (tai vastaava Cursorin MCP-asetustiedosto), stdio-siirtotavalla:

{
  "mcpServers": {
    "model-router": {
      "command": "node",
      "args": ["/polku/mcp-model-router/dist/index.js"],
      "env": {
        "CONFIG_PATH": "/polku/mcp-model-router/config.yaml",
        "LLM_BASE_URL": "http://localhost:4000",
        "LLM_API_KEY": "sk-...",
        "DB_PATH": "/polku/mcp-model-router/data/mcp-model-router.sqlite"
      }
    }
  }
}

Jos palvelin pyörii jo HTTP-tilassa (esim. omassa LXC-kontissa), monet MCP-klientit tukevat myös suoraa streamable HTTP -yhteyttä osoitteeseen http://<tailscale-host>:3100/mcp ilman command-käynnistystä - katso oman klienttisi dokumentaatio.

Ajaminen Proxmox-LXC:ssä

Ei Dockeria - palvelin ajetaan suoraan Node-prosessina systemd-yksikkönä.

  1. Luo (tai käytä) Debian/Ubuntu-pohjainen LXC-kontti, asenna Node.js 20, 22 tai 24.

  2. Kloonaa repo /opt/mcp-model-router-hakemistoon ja aja:

    cd /opt/mcp-model-router
    npm ci
    npm run build
    cp config.example.yaml config.yaml   # muokkaa
  3. Luo palvelulle oma käyttäjä ja data-hakemisto:

    useradd --system --home /opt/mcp-model-router --shell /usr/sbin/nologin mcp-model-router
    mkdir -p /opt/mcp-model-router/data
    chown -R mcp-model-router:mcp-model-router /opt/mcp-model-router
  4. Kopioi ympäristötiedosto ja täytä arvot (ei versionhallintaan):

    cp systemd/mcp-model-router.env.example /opt/mcp-model-router/mcp-model-router.env
    chmod 600 /opt/mcp-model-router/mcp-model-router.env
  5. Asenna systemd-yksikkö, ks. valmis esimerkki systemd/mcp-model-router.service:

    cp systemd/mcp-model-router.service /etc/systemd/system/
    systemctl daemon-reload
    systemctl enable --now mcp-model-router
    systemctl status mcp-model-router

Yksikkö kuuntelee MCP_HTTP_HOST-osoitteessa (aseta env-tiedostoon kontin Tailscale-osoite tailscale ip -4), jolloin muut LXC-kontit tavoittavat palvelimen Tailscale-verkon yli ilman erillistä autentikointia - pääsy rajataan Tailscale ACL:eillä.

Kehitys

npm run dev          # tsx, stdio-siirtotapa suoraan lähdekoodista
npm run typecheck    # tsc --noEmit
npm test             # vitest, HTTP-kutsut mockattu undicilla

Tunnetut rajoitukset

  • Ei omaa autentikointia HTTP-siirtotavalle (luotetaan verkkotason rajaukseen, esim. Tailscale).

  • Ei automaattista mallilistan hakua LiteLLM:n /models-rajapinnasta - list_models lukee vain config.yaml:n.

  • Eval-hinta-arvio perustuu config.yaml:n eval.pricing-hinnastoon, ei automaattiseen hintojen hakuun OpenRouterista.

  • get_usage vaatii, että LiteLLM on käynnistetty tietokannan (Postgres) kanssa ja että USAGE_API_KEY on LiteLLM:n pääavain (LITELLM_MASTER_KEY), koska /user/info on hallintarajapinta. Ilman tietokantaa LiteLLM palauttaa "Database not connected" (HTTP 500), ja get_usage välittää tämän virheen sellaisenaan kutsujalle. Pelkkä OpenRouter-käyttö (usage.provider: openrouter) ei vaadi tietokantaa.

  • tasks-lohkon ja run_eval:n mallitunnisteet ovat LiteLLM:n model_name-aliaksia (LiteLLM-konfiguraatiossa/proxy-mallilistassa määriteltyjä nimiä), eivät suoraan OpenRouterin tai muun taustapalvelun omia mallitunnisteita - ks. Mallitunnisteet: LiteLLM vs. suora OpenRouter.

Related MCP Connectors

Related MCP Servers