diff --git a/README.md b/README.md
index f25d9ee208..e550132968 100644
--- a/README.md
+++ b/README.md
@@ -112,6 +112,7 @@ CyberStrike integrates with the entire AI ecosystem through 23 bundled SDK provi
| **Cerebras** | LLaMA on Cerebras | Fastest inference available |
| **Cohere** | Command R+ | RAG-optimized models |
| **OpenRouter** | 300+ models | Single API, any model |
+| **LLMTR** | 180+ models | Türkiye-hosted models, local pricing |
| **Together AI** | Open-source models | Fine-tuning support |
| **DeepInfra** | Open-source models | Pay-per-token, no GPU needed |
| **Perplexity** | Sonar models | Search-augmented generation |
@@ -150,7 +151,7 @@ Most AI security tools are thin wrappers that send your prompt to an API. CyberS
**150+ Providers, Zero Lock-in**
-Anthropic, OpenAI, Google, Amazon Bedrock, Azure, Groq, Mistral, xAI, DeepSeek, Cerebras, Cohere, OpenRouter, Together AI, GitHub Copilot — or run fully offline with Ollama and LM Studio. 150+ providers, 5,300+ models. You choose the model. You own the results. As AI models get better and cheaper, CyberStrike gets better with them. Switch providers in seconds without reconfiguring anything.
+Anthropic, OpenAI, Google, Amazon Bedrock, Azure, Groq, Mistral, xAI, DeepSeek, Cerebras, Cohere, OpenRouter, LLMTR, Together AI, GitHub Copilot — or run fully offline with Ollama and LM Studio. 150+ providers, 5,300+ models. You choose the model. You own the results. As AI models get better and cheaper, CyberStrike gets better with them. Switch providers in seconds without reconfiguring anything.
diff --git a/README.tr.md b/README.tr.md
index 3295c10580..426789a19b 100644
--- a/README.tr.md
+++ b/README.tr.md
@@ -100,7 +100,7 @@ Diger yapay zeka araclari siradaki ne yapacaginizi soylemenizi bekler. CyberStri
**Herhangi Bir LLM, Bagimlilik Yok**
-Kutudan ciktigi gibi 15+ saglayici: Anthropic, OpenAI, Google, Amazon Bedrock, Azure, Groq, Mistral, OpenRouter — hatta OpenAI uyumlu uc noktalar araciligiyla yerel modeller bile. Claude, GPT, Gemini veya kendi barindiginiz LLM ile calistirin. Modeller iyilestikce ve ucuzladikca, CyberStrike de onlarla birlikte gelisir.
+Kutudan ciktigi gibi 15+ saglayici: Anthropic, OpenAI, Google, Amazon Bedrock, Azure, Groq, Mistral, OpenRouter, LLMTR (Turkiye'de barindirilan modeller) — hatta OpenAI uyumlu uc noktalar araciligiyla yerel modeller bile. Claude, GPT, Gemini veya kendi barindiginiz LLM ile calistirin. Modeller iyilestikce ve ucuzladikca, CyberStrike de onlarla birlikte gelisir.
|
diff --git a/packages/app/src/components/settings-providers.tsx b/packages/app/src/components/settings-providers.tsx
index 43de1dbb06..3214b85a48 100644
--- a/packages/app/src/components/settings-providers.tsx
+++ b/packages/app/src/components/settings-providers.tsx
@@ -23,6 +23,7 @@ const PROVIDER_NOTES = [
{ match: (id: string) => id === "openai", key: "dialog.provider.openai.note" },
{ match: (id: string) => id === "google", key: "dialog.provider.google.note" },
{ match: (id: string) => id === "openrouter", key: "dialog.provider.openrouter.note" },
+ { match: (id: string) => id === "llmtr", key: "dialog.provider.llmtr.note" },
{ match: (id: string) => id === "vercel", key: "dialog.provider.vercel.note" },
] as const
diff --git a/packages/app/src/hooks/use-providers.ts b/packages/app/src/hooks/use-providers.ts
index 8bc861466d..cf38906991 100644
--- a/packages/app/src/hooks/use-providers.ts
+++ b/packages/app/src/hooks/use-providers.ts
@@ -10,6 +10,7 @@ export const popularProviders = [
"openai",
"google",
"openrouter",
+ "llmtr",
"vercel",
]
const popularProviderSet = new Set(popularProviders)
diff --git a/packages/app/src/i18n/ar.ts b/packages/app/src/i18n/ar.ts
index 349eac702e..f74e126d35 100644
--- a/packages/app/src/i18n/ar.ts
+++ b/packages/app/src/i18n/ar.ts
@@ -94,6 +94,7 @@ export const dict = {
"dialog.provider.openai.note": "اتصل باستخدام ChatGPT Pro/Plus أو مفتاح API",
"dialog.provider.google.note": "نماذج Gemini لاستجابات سريعة ومنظمة",
"dialog.provider.openrouter.note": "الوصول إلى جميع النماذج المدعومة من موفر واحد",
+ "dialog.provider.llmtr.note": "نماذج مستضافة في تركيا وأسعار مخفّضة على النماذج العالمية",
"dialog.provider.vercel.note": "وصول موحد إلى نماذج الذكاء الاصطناعي مع توجيه ذكي",
"dialog.model.select.title": "تحديد نموذج",
"dialog.model.search.placeholder": "البحث عن نماذج",
diff --git a/packages/app/src/i18n/br.ts b/packages/app/src/i18n/br.ts
index 998aa35f2f..391c0137bf 100644
--- a/packages/app/src/i18n/br.ts
+++ b/packages/app/src/i18n/br.ts
@@ -94,6 +94,7 @@ export const dict = {
"dialog.provider.openai.note": "Conectar com ChatGPT Pro/Plus ou chave de API",
"dialog.provider.google.note": "Modelos Gemini para respostas rápidas e estruturadas",
"dialog.provider.openrouter.note": "Acesse todos os modelos suportados de um único provedor",
+ "dialog.provider.llmtr.note": "Modelos hospedados na Türkiye e preços com desconto nos globais",
"dialog.provider.vercel.note": "Acesso unificado a modelos de IA com roteamento inteligente",
"dialog.model.select.title": "Selecionar modelo",
"dialog.model.search.placeholder": "Buscar modelos",
diff --git a/packages/app/src/i18n/bs.ts b/packages/app/src/i18n/bs.ts
index 3002d5ac4e..81534b4932 100644
--- a/packages/app/src/i18n/bs.ts
+++ b/packages/app/src/i18n/bs.ts
@@ -102,6 +102,7 @@ export const dict = {
"dialog.provider.openai.note": "GPT modeli za brze, sposobne opšte AI zadatke",
"dialog.provider.google.note": "Gemini modeli za brze, strukturirane odgovore",
"dialog.provider.openrouter.note": "Pristup svim podržanim modelima preko jednog provajdera",
+ "dialog.provider.llmtr.note": "Modeli hostovani u Turskoj i snižene cijene globalnih modela",
"dialog.provider.vercel.note": "Jedinstven pristup AI modelima uz pametno rutiranje",
"dialog.model.select.title": "Odaberi model",
diff --git a/packages/app/src/i18n/da.ts b/packages/app/src/i18n/da.ts
index e9a7eed223..7c23cc21ed 100644
--- a/packages/app/src/i18n/da.ts
+++ b/packages/app/src/i18n/da.ts
@@ -102,6 +102,7 @@ export const dict = {
"dialog.provider.openai.note": "GPT-modeller til hurtige, kompetente generelle AI-opgaver",
"dialog.provider.google.note": "Gemini-modeller til hurtige, strukturerede svar",
"dialog.provider.openrouter.note": "Få adgang til alle understøttede modeller fra én udbyder",
+ "dialog.provider.llmtr.note": "Modeller hostet i Tyrkiet og rabatterede priser på globale modeller",
"dialog.provider.vercel.note": "Samlet adgang til AI-modeller med smart routing",
"dialog.model.select.title": "Vælg model",
diff --git a/packages/app/src/i18n/de.ts b/packages/app/src/i18n/de.ts
index 0ac48cd619..6db4e6c8e4 100644
--- a/packages/app/src/i18n/de.ts
+++ b/packages/app/src/i18n/de.ts
@@ -98,6 +98,7 @@ export const dict = {
"dialog.provider.openai.note": "Mit ChatGPT Pro/Plus oder API-Schlüssel verbinden",
"dialog.provider.google.note": "Gemini-Modelle für schnelle, strukturierte Antworten",
"dialog.provider.openrouter.note": "Zugriff auf alle unterstützten Modelle über einen Anbieter",
+ "dialog.provider.llmtr.note": "In der Türkei gehostete Modelle und vergünstigte Preise für globale Modelle",
"dialog.provider.vercel.note": "Einheitlicher Zugriff auf KI-Modelle mit intelligentem Routing",
"dialog.model.select.title": "Modell auswählen",
"dialog.model.search.placeholder": "Modelle durchsuchen",
diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts
index 15f6c9b380..185bd936fa 100644
--- a/packages/app/src/i18n/en.ts
+++ b/packages/app/src/i18n/en.ts
@@ -118,6 +118,7 @@ export const dict = {
"dialog.provider.openai.note": "GPT models for fast, capable general AI tasks",
"dialog.provider.google.note": "Gemini models for fast, structured responses",
"dialog.provider.openrouter.note": "Access all supported models from one provider",
+ "dialog.provider.llmtr.note": "Türkiye-hosted models and discounted pricing on global ones",
"dialog.provider.vercel.note": "Unified access to AI models with smart routing",
"dialog.model.select.title": "Select model",
diff --git a/packages/app/src/i18n/es.ts b/packages/app/src/i18n/es.ts
index 816a8bc8b3..0ac51ffe7b 100644
--- a/packages/app/src/i18n/es.ts
+++ b/packages/app/src/i18n/es.ts
@@ -102,6 +102,7 @@ export const dict = {
"dialog.provider.openai.note": "Modelos GPT para tareas de IA generales rápidas y capaces",
"dialog.provider.google.note": "Modelos Gemini para respuestas rápidas y estructuradas",
"dialog.provider.openrouter.note": "Accede a todos los modelos soportados desde un solo proveedor",
+ "dialog.provider.llmtr.note": "Modelos alojados en Türkiye y precios con descuento en los globales",
"dialog.provider.vercel.note": "Acceso unificado a modelos de IA con enrutamiento inteligente",
"dialog.model.select.title": "Seleccionar modelo",
diff --git a/packages/app/src/i18n/fr.ts b/packages/app/src/i18n/fr.ts
index 5afde92844..9af6e208ed 100644
--- a/packages/app/src/i18n/fr.ts
+++ b/packages/app/src/i18n/fr.ts
@@ -94,6 +94,7 @@ export const dict = {
"dialog.provider.openai.note": "Connectez-vous avec ChatGPT Pro/Plus ou une clé API",
"dialog.provider.google.note": "Modèles Gemini pour des réponses rapides et structurées",
"dialog.provider.openrouter.note": "Accédez à tous les modèles pris en charge depuis un seul fournisseur",
+ "dialog.provider.llmtr.note": "Modèles hébergés en Türkiye et tarifs réduits sur les modèles mondiaux",
"dialog.provider.vercel.note": "Accès unifié aux modèles d'IA avec routage intelligent",
"dialog.model.select.title": "Sélectionner un modèle",
"dialog.model.search.placeholder": "Rechercher des modèles",
diff --git a/packages/app/src/i18n/ja.ts b/packages/app/src/i18n/ja.ts
index 71b547e994..55375c3f18 100644
--- a/packages/app/src/i18n/ja.ts
+++ b/packages/app/src/i18n/ja.ts
@@ -94,6 +94,7 @@ export const dict = {
"dialog.provider.openai.note": "ChatGPT Pro/PlusまたはAPIキーで接続",
"dialog.provider.google.note": "高速で構造化された応答のためのGeminiモデル",
"dialog.provider.openrouter.note": "1つのプロバイダーからすべてのサポートされているモデルにアクセス",
+ "dialog.provider.llmtr.note": "トルコでホストされるモデルと、グローバルモデルの割引価格",
"dialog.provider.vercel.note": "スマートルーターによるAIモデルへの統合アクセス",
"dialog.model.select.title": "モデルを選択",
"dialog.model.search.placeholder": "モデルを検索",
diff --git a/packages/app/src/i18n/ko.ts b/packages/app/src/i18n/ko.ts
index 5ba8cc918d..7785e0a723 100644
--- a/packages/app/src/i18n/ko.ts
+++ b/packages/app/src/i18n/ko.ts
@@ -98,6 +98,7 @@ export const dict = {
"dialog.provider.openai.note": "ChatGPT Pro/Plus 또는 API 키로 연결",
"dialog.provider.google.note": "빠르고 구조화된 응답을 위한 Gemini 모델",
"dialog.provider.openrouter.note": "모든 지원 모델을 단일 공급자에서 액세스",
+ "dialog.provider.llmtr.note": "튀르키예에서 호스팅되는 모델과 글로벌 모델 할인 가격",
"dialog.provider.vercel.note": "스마트 라우팅을 통한 AI 모델 통합 액세스",
"dialog.model.select.title": "모델 선택",
"dialog.model.search.placeholder": "모델 검색",
diff --git a/packages/app/src/i18n/no.ts b/packages/app/src/i18n/no.ts
index 93dd5b8757..504ad76726 100644
--- a/packages/app/src/i18n/no.ts
+++ b/packages/app/src/i18n/no.ts
@@ -105,6 +105,7 @@ export const dict = {
"dialog.provider.openai.note": "GPT-modeller for raske, dyktige generelle AI-oppgaver",
"dialog.provider.google.note": "Gemini-modeller for raske, strukturerte svar",
"dialog.provider.openrouter.note": "Tilgang til alle støttede modeller fra én leverandør",
+ "dialog.provider.llmtr.note": "Modeller driftet i Tyrkia og rabatterte priser på globale modeller",
"dialog.provider.vercel.note": "Enhetlig tilgang til AI-modeller med smart ruting",
"dialog.model.select.title": "Velg modell",
diff --git a/packages/app/src/i18n/pl.ts b/packages/app/src/i18n/pl.ts
index c9bb6cf566..717c4f1f5d 100644
--- a/packages/app/src/i18n/pl.ts
+++ b/packages/app/src/i18n/pl.ts
@@ -94,6 +94,7 @@ export const dict = {
"dialog.provider.openai.note": "Modele GPT do szybkich i wszechstronnych zadań AI",
"dialog.provider.google.note": "Modele Gemini do szybkich i ustrukturyzowanych odpowiedzi",
"dialog.provider.openrouter.note": "Dostęp do wszystkich obsługiwanych modeli od jednego dostawcy",
+ "dialog.provider.llmtr.note": "Modele hostowane w Turcji i niższe ceny modeli globalnych",
"dialog.provider.vercel.note": "Ujednolicony dostęp do modeli AI z inteligentnym routingiem",
"dialog.model.select.title": "Wybierz model",
"dialog.model.search.placeholder": "Szukaj modeli",
diff --git a/packages/app/src/i18n/ru.ts b/packages/app/src/i18n/ru.ts
index d6433de90b..f678c4314a 100644
--- a/packages/app/src/i18n/ru.ts
+++ b/packages/app/src/i18n/ru.ts
@@ -102,6 +102,7 @@ export const dict = {
"dialog.provider.openai.note": "Модели GPT для быстрых и мощных задач общего ИИ",
"dialog.provider.google.note": "Модели Gemini для быстрых и структурированных ответов",
"dialog.provider.openrouter.note": "Доступ ко всем поддерживаемым моделям через одного провайдера",
+ "dialog.provider.llmtr.note": "Модели, размещённые в Турции, и сниженные цены на глобальные",
"dialog.provider.vercel.note": "Единый доступ к ИИ-моделям с умной маршрутизацией",
"dialog.model.select.title": "Выбрать модель",
diff --git a/packages/app/src/i18n/th.ts b/packages/app/src/i18n/th.ts
index 041955b0fa..8247b197ad 100644
--- a/packages/app/src/i18n/th.ts
+++ b/packages/app/src/i18n/th.ts
@@ -102,6 +102,7 @@ export const dict = {
"dialog.provider.openai.note": "โมเดล GPT สำหรับงาน AI ทั่วไปที่รวดเร็วและมีความสามารถ",
"dialog.provider.google.note": "โมเดล Gemini สำหรับการตอบสนองที่รวดเร็วและมีโครงสร้าง",
"dialog.provider.openrouter.note": "เข้าถึงโมเดลที่รองรับทั้งหมดจากผู้ให้บริการเดียว",
+ "dialog.provider.llmtr.note": "โมเดลที่โฮสต์ในตุรกีและราคาส่วนลดสำหรับโมเดลระดับโลก",
"dialog.provider.vercel.note": "การเข้าถึงโมเดล AI แบบรวมด้วยการกำหนดเส้นทางอัจฉริยะ",
"dialog.model.select.title": "เลือกโมเดล",
diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts
index 1eee007d85..e586ae78ac 100644
--- a/packages/app/src/i18n/zh.ts
+++ b/packages/app/src/i18n/zh.ts
@@ -128,6 +128,7 @@ export const dict = {
"dialog.provider.openai.note": "使用 ChatGPT Pro/Plus 或 API 密钥连接",
"dialog.provider.google.note": "使用 Google 账号或 API 密钥连接",
"dialog.provider.openrouter.note": "使用 OpenRouter 账号或 API 密钥连接",
+ "dialog.provider.llmtr.note": "土耳其本地托管模型,全球模型享折扣价",
"dialog.provider.vercel.note": "使用 Vercel 账号或 API 密钥连接",
"dialog.model.select.title": "选择模型",
diff --git a/packages/app/src/i18n/zht.ts b/packages/app/src/i18n/zht.ts
index f3fff4f578..de7b59a04b 100644
--- a/packages/app/src/i18n/zht.ts
+++ b/packages/app/src/i18n/zht.ts
@@ -106,6 +106,7 @@ export const dict = {
"dialog.provider.copilot.note": "使用 Copilot 或 API 金鑰連線",
"dialog.provider.google.note": "Gemini 模型,提供快速且結構化的回應",
"dialog.provider.openrouter.note": "從單一提供者存取所有支援的模型",
+ "dialog.provider.llmtr.note": "土耳其在地託管模型,全球模型享折扣價",
"dialog.provider.vercel.note": "透過智慧路由統一存取 AI 模型",
"dialog.model.select.title": "選擇模型",
diff --git a/packages/cyberstrike/src/cli/cmd/auth.ts b/packages/cyberstrike/src/cli/cmd/auth.ts
index ee72102cbd..895742c38c 100644
--- a/packages/cyberstrike/src/cli/cmd/auth.ts
+++ b/packages/cyberstrike/src/cli/cmd/auth.ts
@@ -3,6 +3,7 @@ import { cmd } from "./cmd"
import * as prompts from "@clack/prompts"
import { UI } from "../ui"
import { ModelsDev } from "../../provider/models"
+import { LLMTR } from "../../provider/llmtr"
import { map, pipe, sortBy, values } from "remeda"
import path from "path"
import os from "os"
@@ -252,6 +253,7 @@ export const AuthLoginCommand = cmd({
return
}
await ModelsDev.refresh().catch(() => {})
+ await LLMTR.refresh().catch(() => {})
const config = await Config.get()
@@ -275,7 +277,8 @@ export const AuthLoginCommand = cmd({
openai: 3,
google: 4,
openrouter: 5,
- vercel: 6,
+ llmtr: 6,
+ vercel: 7,
}
let provider = await prompts.autocomplete({
message: "Select provider",
@@ -295,6 +298,7 @@ export const AuthLoginCommand = cmd({
cyberstrike: "recommended",
anthropic: "Claude Max or API key",
openai: "ChatGPT Plus/Pro or API key",
+ llmtr: "Türkiye-hosted models, discounted pricing",
}[x.id],
})),
),
@@ -348,6 +352,10 @@ export const AuthLoginCommand = cmd({
prompts.log.info("Create an api key at https://cyberstrike.io/auth")
}
+ if (provider === LLMTR.ID) {
+ prompts.log.info("Create an api key from your LLMTR account - https://llmtr.com/docs")
+ }
+
if (provider === "vercel") {
prompts.log.info("You can create an api key at https://vercel.link/ai-gateway-token")
}
diff --git a/packages/cyberstrike/src/flag/flag.ts b/packages/cyberstrike/src/flag/flag.ts
index f602c4dba4..c8de9b88a3 100644
--- a/packages/cyberstrike/src/flag/flag.ts
+++ b/packages/cyberstrike/src/flag/flag.ts
@@ -57,6 +57,9 @@ export namespace Flag {
export const CYBERSTRIKE_EXPERIMENTAL_MARKDOWN = truthy("CYBERSTRIKE_EXPERIMENTAL_MARKDOWN")
export const CYBERSTRIKE_MODELS_URL = process.env["CYBERSTRIKE_MODELS_URL"]
export const CYBERSTRIKE_MODELS_PATH = process.env["CYBERSTRIKE_MODELS_PATH"]
+ export const CYBERSTRIKE_LLMTR_URL = process.env["CYBERSTRIKE_LLMTR_URL"]
+ export const CYBERSTRIKE_LLMTR_PATH = process.env["CYBERSTRIKE_LLMTR_PATH"]
+ export const CYBERSTRIKE_DISABLE_LLMTR = truthy("CYBERSTRIKE_DISABLE_LLMTR")
function number(key: string) {
const value = process.env[key]
diff --git a/packages/cyberstrike/src/provider/llmtr.ts b/packages/cyberstrike/src/provider/llmtr.ts
new file mode 100644
index 0000000000..520b96d63c
--- /dev/null
+++ b/packages/cyberstrike/src/provider/llmtr.ts
@@ -0,0 +1,264 @@
+// LLMTR (https://llmtr.com) is a Türkiye-based, OpenAI-compatible AI gateway.
+//
+// It is not published in the models.dev catalog, so the provider entry is built
+// here from the gateway's own `/v1/models` endpoint (public, no key required)
+// and merged into the catalog by ModelsDev.get(). Everything downstream —
+// `cyberstrike auth login`, the model picker, the app's provider settings —
+// then sees LLMTR exactly like any models.dev provider.
+//
+// Two things make LLMTR worth a first-class entry instead of a config snippet:
+// models hosted inside Türkiye (Trendyol Asure, Magibu, Muse Glimmer, local
+// Qwen/Gemma builds) that no other provider carries, and per-model pricing that
+// is often below the upstream list price. Both are catalog facts, so they only
+// reach the user if the live catalog is what populates the model list.
+
+import path from "path"
+import { Global } from "../global"
+import { Log } from "../util/log"
+import { Flag } from "../flag/flag"
+import { Installation } from "../installation"
+import { lazy } from "@/util/lazy"
+import type { ModelsDev } from "./models"
+
+export namespace LLMTR {
+ const log = Log.create({ service: "llmtr" })
+
+ export const ID = "llmtr"
+ export const NAME = "LLMTR"
+ export const ENV = "LLMTR_API_KEY"
+ export const NPM = "@ai-sdk/openai-compatible"
+
+ const filepath = path.join(Global.Path.cache, "llmtr.json")
+
+ export function url() {
+ return Flag.CYBERSTRIKE_LLMTR_URL || "https://llmtr.com/v1"
+ }
+
+ // The gateway returns an OpenRouter-shaped catalog: pricing in dollars per
+ // token, modalities under `architecture`, per-model `supported_parameters`.
+ type Entry = {
+ id: string
+ name?: string
+ created?: number
+ context_length?: number
+ supported_operations?: string[]
+ architecture?: {
+ input_modalities?: string[]
+ output_modalities?: string[]
+ }
+ pricing?: Record
+ top_provider?: { context_length?: number; max_completion_tokens?: number }
+ supported_parameters?: string[]
+ reasoning?: { supported_efforts?: string[] }
+ }
+
+ // models.dev has no "file" modality; LLMTR uses it for document input.
+ const MODALITY = {
+ text: "text",
+ image: "image",
+ audio: "audio",
+ video: "video",
+ file: "pdf",
+ pdf: "pdf",
+ } as const
+
+ function modalities(input: string[] | undefined) {
+ return (input ?? []).flatMap((item) => MODALITY[item as keyof typeof MODALITY] ?? [])
+ }
+
+ // Catalog prices are per token; models.dev costs are per million tokens.
+ // Scaling by a million leaves binary float noise ($1e-7/token lands on
+ // 0.09999999999999999 rather than 0.1), so round back to a precision far
+ // finer than any real price - cost is rendered to the user per session.
+ function price(input: string | undefined) {
+ const value = Number(input)
+ if (!Number.isFinite(value)) return 0
+ return Math.round(value * 1_000_000 * 1e6) / 1e6
+ }
+
+ function model(entry: Entry): ModelsDev.Model {
+ const params = new Set(entry.supported_parameters ?? [])
+ const input = modalities(entry.architecture?.input_modalities)
+ const context = entry.context_length ?? entry.top_provider?.context_length ?? 0
+ const efforts = entry.reasoning?.supported_efforts ?? []
+ return {
+ id: entry.id,
+ name: entry.name ?? entry.id,
+ family: entry.id.split("/")[0],
+ release_date: new Date((entry.created ?? 0) * 1000).toISOString().slice(0, 10),
+ attachment: input.some((item) => item !== "text"),
+ reasoning: Boolean(entry.reasoning) || params.has("reasoning") || params.has("include_reasoning"),
+ temperature: params.has("temperature"),
+ tool_call: params.has("tools"),
+ cost: {
+ input: price(entry.pricing?.prompt),
+ output: price(entry.pricing?.completion),
+ cache_read: price(entry.pricing?.input_cache_read),
+ cache_write: price(entry.pricing?.input_cache_write),
+ },
+ limit: {
+ context,
+ output: entry.top_provider?.max_completion_tokens ?? context,
+ },
+ modalities: {
+ input,
+ output: modalities(entry.architecture?.output_modalities),
+ },
+ options: {},
+ reasoning_options: efforts.length ? [{ type: "effort", values: efforts }] : undefined,
+ }
+ }
+
+ // The catalog also carries embedding and image models, which the chat
+ // pipeline cannot drive — keep only what /v1/chat/completions can serve.
+ function chat(entry: Entry) {
+ return (
+ (entry.supported_operations ?? ["CHAT_COMPLETIONS"]).includes("CHAT_COMPLETIONS") &&
+ modalities(entry.architecture?.output_modalities).includes("text")
+ )
+ }
+
+ export function convert(entries: Entry[]): ModelsDev.Provider {
+ return {
+ id: ID,
+ name: NAME,
+ api: url(),
+ npm: NPM,
+ env: [ENV],
+ models: Object.fromEntries(entries.filter(chat).map((entry) => [entry.id, model(entry)])),
+ }
+ }
+
+ // Enough of the catalog to pick a model before the first refresh lands (fresh
+ // install, offline, or CYBERSTRIKE_DISABLE_MODELS_FETCH): every Türkiye-hosted
+ // chat model, plus one flagship per major upstream vendor.
+ const SEED: Entry[] = [
+ {
+ id: "llmtr/gemma-4",
+ name: "Gemma 4",
+ created: 1776880304,
+ context_length: 131072,
+ architecture: { input_modalities: ["text", "image"], output_modalities: ["text"] },
+ pricing: { prompt: "0.000002", completion: "0.000005", input_cache_read: "0.0000005" },
+ top_provider: { context_length: 131072, max_completion_tokens: 131072 },
+ supported_parameters: ["max_tokens", "parallel_tool_calls", "reasoning", "temperature", "tools", "top_p"],
+ },
+ {
+ id: "llmtr/qwen3-6-35b",
+ name: "Qwen 3.6 35B-A3B",
+ created: 1776880304,
+ context_length: 262144,
+ architecture: { input_modalities: ["text"], output_modalities: ["text"] },
+ pricing: { prompt: "0.000005", completion: "0.000005" },
+ top_provider: { context_length: 262144, max_completion_tokens: 65536 },
+ supported_parameters: ["max_tokens", "parallel_tool_calls", "reasoning", "temperature", "tools", "top_p"],
+ },
+ {
+ id: "llmtr/qwen3-5-4b",
+ name: "Qwen 3.5 4B",
+ created: 1776880304,
+ context_length: 131072,
+ architecture: { input_modalities: ["text"], output_modalities: ["text"] },
+ pricing: { prompt: "0.000001", completion: "0.000003" },
+ top_provider: { context_length: 131072, max_completion_tokens: 32768 },
+ supported_parameters: ["max_tokens", "temperature", "tools", "top_p"],
+ },
+ {
+ id: "llmtr/muse-glimmer-30b-tr",
+ name: "Muse Glimmer 30B (Türkiye)",
+ created: 1776880304,
+ context_length: 131072,
+ architecture: { input_modalities: ["text"], output_modalities: ["text"] },
+ pricing: { prompt: "0.000002", completion: "0.000005" },
+ top_provider: { context_length: 131072, max_completion_tokens: 32768 },
+ supported_parameters: ["max_tokens", "temperature", "tools", "top_p"],
+ },
+ {
+ id: "llmtr/trendyol-asure-12b",
+ name: "Trendyol Asure 12B",
+ created: 1786690403,
+ context_length: 40960,
+ architecture: { input_modalities: ["text", "image"], output_modalities: ["text"] },
+ pricing: { prompt: "0.0000001", completion: "0.0000005", input_cache_read: "0.000000025" },
+ top_provider: { context_length: 40960, max_completion_tokens: 40960 },
+ supported_parameters: ["max_tokens", "temperature", "top_p"],
+ },
+ {
+ id: "llmtr/magibu-11b-v8",
+ name: "Magibu 11B v8",
+ created: 1780689763,
+ context_length: 8192,
+ architecture: { input_modalities: ["text"], output_modalities: ["text"] },
+ pricing: { prompt: "0.0000001", completion: "0.0000005" },
+ top_provider: { context_length: 8192, max_completion_tokens: 8192 },
+ supported_parameters: ["max_tokens", "temperature", "top_p"],
+ },
+ {
+ id: "anthropic/claude-opus-5",
+ name: "Claude Opus 5",
+ created: 1776880299,
+ context_length: 200000,
+ architecture: { input_modalities: ["text", "image", "file"], output_modalities: ["text"] },
+ pricing: { prompt: "0.000005", completion: "0.000025" },
+ top_provider: { context_length: 200000, max_completion_tokens: 64000 },
+ supported_parameters: ["max_tokens", "parallel_tool_calls", "temperature", "tool_choice", "tools", "top_p"],
+ },
+ {
+ id: "anthropic/claude-sonnet-5",
+ name: "Claude Sonnet 5",
+ created: 1776880299,
+ context_length: 200000,
+ architecture: { input_modalities: ["text", "image", "file"], output_modalities: ["text"] },
+ pricing: { prompt: "0.000003", completion: "0.000015" },
+ top_provider: { context_length: 200000, max_completion_tokens: 64000 },
+ supported_parameters: ["max_tokens", "parallel_tool_calls", "temperature", "tool_choice", "tools", "top_p"],
+ },
+ {
+ id: "openai/gpt-5.6-terra",
+ name: "GPT-5.6 Terra",
+ created: 1776880298,
+ context_length: 400000,
+ architecture: { input_modalities: ["text", "image", "file"], output_modalities: ["text"] },
+ pricing: { prompt: "0.00000125", completion: "0.00001" },
+ top_provider: { context_length: 400000, max_completion_tokens: 128000 },
+ supported_parameters: ["max_tokens", "parallel_tool_calls", "temperature", "tool_choice", "tools", "top_p"],
+ },
+ {
+ id: "google/gemini-3.6-flash",
+ name: "Gemini 3.6 Flash",
+ created: 1784702661,
+ context_length: 1048576,
+ architecture: { input_modalities: ["text", "image", "file", "audio"], output_modalities: ["text"] },
+ pricing: { prompt: "0.0000015", completion: "0.0000075", input_cache_read: "0.00000015" },
+ top_provider: { context_length: 1048576, max_completion_tokens: 65536 },
+ supported_parameters: ["include_reasoning", "max_tokens", "reasoning", "temperature", "tools", "top_p"],
+ reasoning: { supported_efforts: ["minimal", "low", "medium", "high"] },
+ },
+ ]
+
+ export const Data = lazy(async () => {
+ const cached = await Bun.file(Flag.CYBERSTRIKE_LLMTR_PATH ?? filepath)
+ .json()
+ .catch(() => undefined)
+ const entries = cached?.data as Entry[] | undefined
+ return convert(entries?.length ? entries : SEED)
+ })
+
+ export async function get() {
+ return Data()
+ }
+
+ export async function refresh() {
+ const result = await fetch(`${url()}/models`, {
+ headers: { "User-Agent": Installation.USER_AGENT },
+ signal: AbortSignal.timeout(10 * 1000),
+ }).catch((e) => {
+ log.error("Failed to fetch llmtr catalog", { error: e })
+ })
+ if (!result || !result.ok) return
+ const body = (await result.json().catch(() => undefined)) as { data?: Entry[] } | undefined
+ if (!body?.data?.length) return
+ await Bun.write(filepath, JSON.stringify(body))
+ Data.reset()
+ }
+}
diff --git a/packages/cyberstrike/src/provider/models.ts b/packages/cyberstrike/src/provider/models.ts
index a8df7841a6..9b7615fda1 100644
--- a/packages/cyberstrike/src/provider/models.ts
+++ b/packages/cyberstrike/src/provider/models.ts
@@ -4,6 +4,7 @@ import path from "path"
import z from "zod"
import { Installation } from "../installation"
import { Flag } from "../flag/flag"
+import { LLMTR } from "./llmtr"
import { lazy } from "@/util/lazy"
// Try to import bundled snapshot (generated at build time)
@@ -135,8 +136,12 @@ export namespace ModelsDev {
})
export async function get() {
- const result = await Data()
- return result as Record
+ const result = (await Data()) as Record
+ // LLMTR is not published on models.dev, so its entry is built from the
+ // gateway's own catalog and merged here — that keeps every consumer
+ // (auth login, model picker, app settings) on a single provider list.
+ if (Flag.CYBERSTRIKE_DISABLE_LLMTR) return result
+ return { ...result, [LLMTR.ID]: await LLMTR.get() }
}
export async function refresh() {
@@ -159,11 +164,10 @@ export namespace ModelsDev {
}
if (!Flag.CYBERSTRIKE_DISABLE_MODELS_FETCH && !process.argv.includes("--get-yargs-completions")) {
- ModelsDev.refresh()
- setInterval(
- async () => {
- await ModelsDev.refresh()
- },
- 60 * 1000 * 60,
- ).unref()
+ const refresh = async () => {
+ await ModelsDev.refresh()
+ if (!Flag.CYBERSTRIKE_DISABLE_LLMTR) await LLMTR.refresh()
+ }
+ refresh()
+ setInterval(refresh, 60 * 1000 * 60).unref()
}
diff --git a/packages/cyberstrike/src/provider/provider.ts b/packages/cyberstrike/src/provider/provider.ts
index 16dbfd9318..064ec3c1bd 100644
--- a/packages/cyberstrike/src/provider/provider.ts
+++ b/packages/cyberstrike/src/provider/provider.ts
@@ -381,6 +381,17 @@ export namespace Provider {
},
}
},
+ llmtr: async () => {
+ return {
+ autoload: false,
+ options: {
+ headers: {
+ "HTTP-Referer": "https://cyberstrike.io/",
+ "X-Title": "cyberstrike",
+ },
+ },
+ }
+ },
vercel: async () => {
return {
autoload: false,
diff --git a/packages/cyberstrike/src/provider/transform.ts b/packages/cyberstrike/src/provider/transform.ts
index a8e457121b..13945b8db8 100644
--- a/packages/cyberstrike/src/provider/transform.ts
+++ b/packages/cyberstrike/src/provider/transform.ts
@@ -4,6 +4,7 @@ import type { JSONSchema7 } from "@ai-sdk/provider"
import type { JSONSchema } from "zod/v4/core"
import type { Provider } from "./provider"
import type { ModelsDev } from "./models"
+import { LLMTR } from "./llmtr"
import { iife } from "@/util/iife"
import { Flag } from "@/flag/flag"
@@ -338,6 +339,16 @@ export namespace ProviderTransform {
export function variants(model: Provider.Model): Record> {
if (!model.capabilities.reasoning) return {}
+ // LLMTR publishes the exact effort list every model accepts, so read it
+ // instead of inferring from the npm package (shared with every other
+ // openai-compatible provider) or from the model id (the same upstream model
+ // is offered with different effort sets). Each effort maps to the standard
+ // `reasoning_effort` body field, which the gateway advertises for all of them.
+ if (model.providerID === LLMTR.ID) {
+ const efforts = model.reasoning_options?.flatMap((o) => (o.type === "effort" ? o.values : [])) ?? []
+ return Object.fromEntries(efforts.map((effort) => [effort, { reasoningEffort: effort }]))
+ }
+
const id = model.id.toLowerCase()
if (
id.includes("minimax") ||
@@ -848,6 +859,14 @@ export namespace ProviderTransform {
if (keys.length > 0) return v[keys[0]]
return {}
}
+ if (model.providerID === LLMTR.ID) {
+ // Title/summarize calls should not pay for reasoning. The catalog effort
+ // list is ordered weakest-first, so the first supported one is the
+ // cheapest the model actually accepts.
+ const v = variants(model)
+ const first = Object.keys(v)[0]
+ return first ? v[first] : {}
+ }
if (model.providerID === "deepseek") {
return { thinking: { type: "disabled" } }
}
diff --git a/packages/cyberstrike/test/preload.ts b/packages/cyberstrike/test/preload.ts
index 891c360076..e3238963d6 100644
--- a/packages/cyberstrike/test/preload.ts
+++ b/packages/cyberstrike/test/preload.ts
@@ -45,6 +45,7 @@ delete process.env["AWS_PROFILE"]
delete process.env["AWS_REGION"]
delete process.env["AWS_BEARER_TOKEN_BEDROCK"]
delete process.env["OPENROUTER_API_KEY"]
+delete process.env["LLMTR_API_KEY"]
delete process.env["GROQ_API_KEY"]
delete process.env["MISTRAL_API_KEY"]
delete process.env["PERPLEXITY_API_KEY"]
diff --git a/packages/cyberstrike/test/provider/llmtr.test.ts b/packages/cyberstrike/test/provider/llmtr.test.ts
new file mode 100644
index 0000000000..60454a22b5
--- /dev/null
+++ b/packages/cyberstrike/test/provider/llmtr.test.ts
@@ -0,0 +1,169 @@
+import { test, expect, describe } from "bun:test"
+import path from "path"
+
+import { tmpdir } from "../fixture/fixture"
+import { Instance } from "../../src/project/instance"
+import { Provider } from "../../src/provider/provider"
+import { ProviderTransform } from "../../src/provider/transform"
+import { ModelsDev } from "../../src/provider/models"
+import { LLMTR } from "../../src/provider/llmtr"
+import { Env } from "../../src/env"
+
+// A trimmed slice of the shape https://llmtr.com/v1/models returns.
+const catalog = [
+ {
+ id: "llmtr/trendyol-asure-12b",
+ name: "Trendyol Asure 12B",
+ created: 1786690403,
+ context_length: 40960,
+ supported_operations: ["CHAT_COMPLETIONS"],
+ architecture: { input_modalities: ["text", "image"], output_modalities: ["text"] },
+ pricing: { prompt: "0.0000001", completion: "0.0000005", input_cache_read: "0.000000025" },
+ top_provider: { context_length: 40960, max_completion_tokens: 8192 },
+ supported_parameters: ["max_tokens", "temperature", "tools", "top_p"],
+ },
+ {
+ id: "google/gemini-3.6-flash",
+ name: "Gemini 3.6 Flash",
+ created: 1784702661,
+ context_length: 1048576,
+ supported_operations: ["CHAT_COMPLETIONS", "RESPONSES"],
+ architecture: { input_modalities: ["text", "image", "file"], output_modalities: ["text"] },
+ pricing: { prompt: "0.0000015", completion: "0.0000075" },
+ top_provider: { context_length: 1048576, max_completion_tokens: 65536 },
+ supported_parameters: ["reasoning", "reasoning_effort", "temperature", "tools"],
+ reasoning: { supported_efforts: ["minimal", "low", "high"] },
+ },
+ {
+ id: "openai/text-embedding-3-large",
+ name: "Text Embedding 3 Large",
+ created: 1776880298,
+ context_length: 8191,
+ supported_operations: ["EMBEDDINGS"],
+ architecture: { input_modalities: ["text"], output_modalities: ["embedding"] },
+ pricing: { prompt: "0.00000013" },
+ top_provider: { context_length: 8191 },
+ supported_parameters: [],
+ },
+]
+
+describe("LLMTR catalog", () => {
+ const provider = LLMTR.convert(catalog)
+
+ test("only exposes models the chat endpoint can serve", () => {
+ expect(Object.keys(provider.models)).toEqual(["llmtr/trendyol-asure-12b", "google/gemini-3.6-flash"])
+ })
+
+ test("routes through the gateway with an openai-compatible sdk", () => {
+ expect(provider.id).toBe("llmtr")
+ expect(provider.api).toBe("https://llmtr.com/v1")
+ expect(provider.npm).toBe("@ai-sdk/openai-compatible")
+ expect(provider.env).toEqual(["LLMTR_API_KEY"])
+ })
+
+ test("converts per-token pricing to per-million-token cost", () => {
+ const model = provider.models["llmtr/trendyol-asure-12b"]
+ // Exact, not close-to: scaling per-token prices by a million must not leave
+ // float noise behind, since these values are rendered as session cost.
+ expect(model.cost?.input).toBe(0.1)
+ expect(model.cost?.output).toBe(0.5)
+ expect(model.cost?.cache_read).toBe(0.025)
+ })
+
+ test("carries limits, modalities and capabilities across", () => {
+ const model = provider.models["llmtr/trendyol-asure-12b"]
+ expect(model.limit).toEqual({ context: 40960, output: 8192 })
+ expect(model.modalities).toEqual({ input: ["text", "image"], output: ["text"] })
+ expect(model.attachment).toBe(true)
+ expect(model.tool_call).toBe(true)
+ expect(model.temperature).toBe(true)
+ expect(model.reasoning).toBe(false)
+ })
+
+ test("maps the file modality onto pdf", () => {
+ expect(provider.models["google/gemini-3.6-flash"].modalities?.input).toEqual(["text", "image", "pdf"])
+ })
+
+ test("keeps the advertised reasoning efforts", () => {
+ const model = provider.models["google/gemini-3.6-flash"]
+ expect(model.reasoning).toBe(true)
+ expect(model.reasoning_options).toEqual([{ type: "effort", values: ["minimal", "low", "high"] }])
+ })
+})
+
+describe("LLMTR reasoning variants", () => {
+ const model = {
+ id: "google/gemini-3.6-flash",
+ providerID: "llmtr",
+ api: { id: "google/gemini-3.6-flash", url: "https://llmtr.com/v1", npm: "@ai-sdk/openai-compatible" },
+ capabilities: { reasoning: true },
+ reasoning_options: [{ type: "effort", values: ["minimal", "low", "high"] }],
+ } as any
+
+ test("uses the efforts the gateway advertises, not the openai-compatible default", () => {
+ expect(ProviderTransform.variants(model)).toEqual({
+ minimal: { reasoningEffort: "minimal" },
+ low: { reasoningEffort: "low" },
+ high: { reasoningEffort: "high" },
+ })
+ })
+
+ test("background calls fall back to the cheapest supported effort", () => {
+ expect(ProviderTransform.smallOptions(model)).toEqual({ reasoningEffort: "minimal" })
+ })
+
+ test("non-reasoning models get no variants", () => {
+ expect(ProviderTransform.variants({ ...model, capabilities: { reasoning: false } })).toEqual({})
+ })
+})
+
+test("llmtr is published in the model catalog", async () => {
+ const database = await ModelsDev.get()
+ expect(database["llmtr"]).toBeDefined()
+ expect(database["llmtr"].env).toEqual(["LLMTR_API_KEY"])
+ // The seeded catalog always carries the Türkiye-hosted models, which is the
+ // reason the provider is bundled rather than left to a config snippet.
+ expect(Object.keys(database["llmtr"].models).some((id) => id.startsWith("llmtr/"))).toBe(true)
+})
+
+test("provider loaded from LLMTR_API_KEY", async () => {
+ await using tmp = await tmpdir({
+ init: async (dir) => {
+ await Bun.write(
+ path.join(dir, "cyberstrike.json"),
+ JSON.stringify({ $schema: "https://cyberstrike.io/config.json" }),
+ )
+ },
+ })
+ await Instance.provide({
+ directory: tmp.path,
+ init: async () => {
+ Env.set("LLMTR_API_KEY", "llmtr-test-key")
+ },
+ fn: async () => {
+ const providers = await Provider.list()
+ expect(providers["llmtr"]).toBeDefined()
+ expect(providers["llmtr"].source).toBe("env")
+ expect(providers["llmtr"].key).toBe("llmtr-test-key")
+ // The custom loader still attaches attribution headers on top.
+ expect(providers["llmtr"].options.headers["X-Title"]).toBe("cyberstrike")
+ },
+ })
+})
+
+test("llmtr stays out of the provider list without a key", async () => {
+ await using tmp = await tmpdir({
+ init: async (dir) => {
+ await Bun.write(
+ path.join(dir, "cyberstrike.json"),
+ JSON.stringify({ $schema: "https://cyberstrike.io/config.json" }),
+ )
+ },
+ })
+ await Instance.provide({
+ directory: tmp.path,
+ fn: async () => {
+ expect(await Provider.list().then((x) => x["llmtr"])).toBeUndefined()
+ },
+ })
+})
|