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() + }, + }) +})