Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,31 @@ Config lives at `.pentestcode/pentestcode.jsonc`:

Providers: Anthropic, OpenAI, Google, Azure, AWS Bedrock, Ollama, Together, Groq, Fireworks, DeepSeek, Mistral, and more via [ai-sdk](https://github.com/vercel/ai).

### LLMTR (Turkey-hosted gateway)

[LLMTR](https://llmtr.com) is a built-in, OpenAI-compatible AI gateway that fronts 200+ models
(global providers plus Turkey-hosted models with a data-residency guarantee) behind a single
endpoint. It ships as a first-class provider — no custom config needed.

```bash
pentestcode auth login # pick "LLMTR", paste your API key
# or:
export LLMTR_API_KEY=sk-... # env var works too
```

```jsonc
{
"provider": {
"llmtr": {
"model": "openai/gpt-5.5" // any model id from https://llmtr.com/v1/models
}
}
}
```

The model catalog is discovered live from `https://llmtr.com/v1/models` at startup (with a
curated offline fallback). Set `LLMTR_BASE_URL` to point at a self-hosted or staging gateway.

## Contributing

Bug reports from real usage are the most valuable thing you can send. Run PentestCode on a CTF box, an HTB machine, or an authorized pentest, and when something goes wrong — it loops, misses an obvious path, chokes on tool output, or wastes tokens — open an issue with:
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/plugin/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { GoogleVertexAnthropicPlugin, GoogleVertexPlugin } from "./provider/goog
import { GroqPlugin } from "./provider/groq"
import { KiloPlugin } from "./provider/kilo"
import { LLMGatewayPlugin } from "./provider/llmgateway"
import { LLMTRPlugin } from "./provider/llmtr"
import { MistralPlugin } from "./provider/mistral"
import { NvidiaPlugin } from "./provider/nvidia"
import { OpenAIPlugin } from "./provider/openai"
Expand Down Expand Up @@ -51,6 +52,7 @@ export const ProviderPlugins: PluginInternal.Plugin<PluginInternal.Requirements
GroqPlugin,
KiloPlugin,
LLMGatewayPlugin,
LLMTRPlugin,
MistralPlugin,
NvidiaPlugin,
OpencodePlugin,
Expand Down
208 changes: 208 additions & 0 deletions packages/core/src/plugin/provider/llmtr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import { Effect } from "effect"
import type { ModelV2Info } from "@pentestcode/sdk/v2/types"
import { define } from "../internal"
import { Integration } from "../../integration"
import { InstallationVersion } from "../../installation/version"
import { ModelV2 } from "../../model"
import { ProviderV2 } from "../../provider"

// LLMTR (https://llmtr.com) is a Turkey-hosted, OpenAI-compatible AI gateway that
// fronts 200+ models (global providers + Turkey-hosted models) behind a single
// `/v1/chat/completions` endpoint. It plugs in exactly like the other gateway
// providers (openrouter/nvidia/zenmux): an `@ai-sdk/openai-compatible` provider
// whose bearer key is resolved from the `llmtr` integration (API key / env var).
//
// Models are discovered live from the public `/v1/models` catalog (OpenRouter-style
// schema). The fetch is best-effort and forked, so registration never blocks on the
// network; a curated seed of Turkey-hosted flagships keeps the provider usable
// offline and guarantees they are always present.

const PROVIDER_ID = "llmtr"
const PROVIDER = ProviderV2.ID.make(PROVIDER_ID)
const INTEGRATION_ID = Integration.ID.make(PROVIDER_ID)
const DISPLAY_NAME = "LLMTR"
const PACKAGE = "@ai-sdk/openai-compatible"
const DEFAULT_BASE_URL = "https://llmtr.com/v1"
const ENV_KEY = "LLMTR_API_KEY"

// Catalog costs are expressed per 1M tokens; LLMTR (like OpenRouter) prices per
// token as decimal strings, so scale up by 1e6.
const PRICE_SCALE = 1_000_000

const KNOWN_MODALITIES = new Set(["text", "image", "audio", "video", "pdf"])

/** Subset of the OpenRouter-style model object returned by `GET /v1/models`. */
interface RemoteModel {
id: string
name?: string
created?: number
context_length?: number
architecture?: {
input_modalities?: readonly string[]
output_modalities?: readonly string[]
}
pricing?: {
prompt?: string
completion?: string
input_cache_read?: string
input_cache_write?: string
}
top_provider?: {
context_length?: number
max_completion_tokens?: number
}
supported_parameters?: readonly string[]
}

// Real values pulled from the live LLMTR catalog. Used offline and merged with the
// live fetch (live entries win on id collisions so pricing stays current).
const SEED_MODELS: RemoteModel[] = [
{
id: "llmtr/muse-glimmer-30b-tr",
name: "Muse Glimmer 30B (Türkiye)",
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: ["tools", "tool_choice", "reasoning", "temperature", "top_p"],
},
{
id: "llmtr/gemma-4",
name: "Gemma 4",
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: ["tools", "tool_choice", "reasoning", "temperature", "top_p"],
},
{
id: "llmtr/qwen3-6-35b",
name: "Qwen 3.6 35B-A3B",
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: ["tools", "tool_choice", "reasoning", "temperature", "top_p"],
},
{
id: "llmtr/trendyol-asure-12b",
name: "Trendyol Asure 12B",
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: ["temperature", "top_p"],
},
{
id: "llmtr/magibu-11b-v8",
name: "Magibu 11B v8",
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: ["temperature", "top_p"],
},
]

const baseURL = () => process.env.LLMTR_BASE_URL?.trim() || DEFAULT_BASE_URL

const price = (value: string | undefined) => {
const parsed = value === undefined ? Number.NaN : Number(value)
return Number.isFinite(parsed) && parsed > 0 ? parsed * PRICE_SCALE : 0
}

const positiveInt = (value: number | undefined) =>
typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : 0

const modalities = (input: readonly string[] | undefined) => {
const list = (input ?? []).filter((item) => KNOWN_MODALITIES.has(item))
return list.length ? list : ["text"]
}

/** Projects an LLMTR model description onto a catalog model draft. */
function applyModel(draft: ModelV2Info, model: RemoteModel) {
const context = positiveInt(model.context_length ?? model.top_provider?.context_length)
const output = positiveInt(model.top_provider?.max_completion_tokens ?? model.context_length) || context
draft.name = model.name ?? model.id
draft.api = { id: model.id, type: "aisdk", package: PACKAGE }
draft.capabilities = {
tools: model.supported_parameters?.includes("tools") ?? false,
input: modalities(model.architecture?.input_modalities),
output: modalities(model.architecture?.output_modalities),
}
draft.cost = [
{
input: price(model.pricing?.prompt),
output: price(model.pricing?.completion),
cache: {
read: price(model.pricing?.input_cache_read),
write: price(model.pricing?.input_cache_write),
},
},
]
draft.time.released = model.created ? model.created * 1000 : 0
draft.status = "active"
draft.enabled = true
draft.limit = { context, output }
}

function mergeModels(seed: readonly RemoteModel[], fetched: readonly RemoteModel[]) {
const byId = new Map<string, RemoteModel>()
for (const model of seed) byId.set(model.id, model)
for (const model of fetched) byId.set(model.id, model)
return [...byId.values()]
}

const fetchModels = () =>
Effect.tryPromise({
try: async (signal) => {
const response = await fetch(`${baseURL()}/models`, {
headers: { Accept: "application/json", "User-Agent": `pentestcode/${InstallationVersion}` },
signal,
})
if (!response.ok) throw new Error(`LLMTR models request failed: ${response.status}`)
const body = (await response.json()) as { data?: RemoteModel[] }
return Array.isArray(body.data) ? body.data.filter((model) => typeof model?.id === "string") : []
},
catch: (cause) => cause,
})

export const LLMTRPlugin = define({
id: PROVIDER_ID,
effect: Effect.fn(function* (ctx) {
let models: readonly RemoteModel[] = SEED_MODELS

yield* ctx.integration.transform((draft) => {
draft.update(INTEGRATION_ID, (integration) => {
integration.name = DISPLAY_NAME
})
draft.method.update({ integrationID: INTEGRATION_ID, method: { type: "key" } })
draft.method.update({ integrationID: INTEGRATION_ID, method: { type: "env", names: [ENV_KEY] } })
})

yield* ctx.catalog.transform((catalog) => {
catalog.provider.update(PROVIDER, (provider) => {
provider.name = DISPLAY_NAME
provider.integrationID = INTEGRATION_ID
provider.api = { type: "aisdk", package: PACKAGE, url: baseURL() }
provider.request.headers["HTTP-Referer"] ??= "https://github.com/s0ld13rr/pentestcode"
provider.request.headers["X-Title"] ??= "pentestcode"
})
for (const model of models) {
catalog.model.update(PROVIDER, ModelV2.ID.make(model.id), (draft) => applyModel(draft, model))
}
})

// Opt-out hook for hermetic tests / fully offline runs.
if (process.env.LLMTR_SKIP_REMOTE_MODELS === "1") return

yield* Effect.forkScoped(
Effect.gen(function* () {
const fetched = yield* fetchModels().pipe(Effect.catch(() => Effect.succeed([] as RemoteModel[])))
if (fetched.length === 0) return
models = mergeModels(SEED_MODELS, fetched)
yield* ctx.catalog.reload()
}),
)
}),
})
95 changes: 95 additions & 0 deletions packages/core/test/plugin/provider-llmtr.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { Catalog } from "@pentestcode/core/catalog"
import { Integration } from "@pentestcode/core/integration"
import { ModelV2 } from "@pentestcode/core/model"
import { PluginV2 } from "@pentestcode/core/plugin"
import { PluginHost } from "@pentestcode/core/plugin/host"
import { ProviderPlugins } from "@pentestcode/core/plugin/provider"
import { LLMTRPlugin } from "@pentestcode/core/plugin/provider/llmtr"
import { ProviderV2 } from "@pentestcode/core/provider"
import { testEffect } from "../lib/effect"
import { PluginTestLayer } from "./fixture"

// Keep the plugin fully offline: assert only the synchronously registered
// provider/integration/seed models, never the forked live `/v1/models` fetch.
process.env.LLMTR_SKIP_REMOTE_MODELS = "1"

const it = testEffect(PluginTestLayer)

const addPlugin = Effect.fn(function* () {
const plugin = yield* PluginV2.Service
const host = yield* PluginHost.make(plugin)
yield* LLMTRPlugin.effect(host)
})

function required<T>(value: T | undefined): T {
if (value === undefined) throw new Error("Expected value")
return value
}

const LLMTR = ProviderV2.ID.make("llmtr")

describe("LLMTRPlugin", () => {
it.effect("is registered in the provider plugin set", () =>
Effect.sync(() => expect(ProviderPlugins.map((item) => item.id)).toContain(PluginV2.ID.make("llmtr"))),
)

it.effect("injects an OpenAI-compatible llmtr provider with branding headers", () =>
Effect.gen(function* () {
const catalog = yield* Catalog.Service
yield* addPlugin()
const provider = required(yield* catalog.provider.get(LLMTR))
expect(provider.name).toBe("LLMTR")
expect(provider.integrationID).toBe(Integration.ID.make("llmtr"))
expect(provider.api).toEqual({
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://llmtr.com/v1",
})
expect(provider.request.headers).toEqual({
"HTTP-Referer": "https://github.com/s0ld13rr/pentestcode",
"X-Title": "pentestcode",
})
}),
)

it.effect("registers an llmtr integration with key and env auth methods", () =>
Effect.gen(function* () {
const integrations = yield* Integration.Service
yield* addPlugin()
const integration = required(yield* integrations.get(Integration.ID.make("llmtr")))
expect(integration.name).toBe("LLMTR")
const types = integration.methods.map((method) => method.type).sort()
expect(types).toEqual(["env", "key"])
const env = integration.methods.find((method) => method.type === "env")
expect(env && "names" in env ? env.names : []).toEqual(["LLMTR_API_KEY"])
}),
)

it.effect("converts seed model pricing from per-token to per-1M and detects tool support", () =>
Effect.gen(function* () {
const catalog = yield* Catalog.Service
yield* addPlugin()

const gemma = required(yield* catalog.model.get(LLMTR, ModelV2.ID.make("llmtr/gemma-4")))
expect(gemma.name).toBe("Gemma 4")
// provider url is inherited by the model at projection time
expect(gemma.api).toMatchObject({
type: "aisdk",
package: "@ai-sdk/openai-compatible",
id: "llmtr/gemma-4",
url: "https://llmtr.com/v1",
})
expect(gemma.capabilities.tools).toBe(true)
expect([...gemma.capabilities.input]).toEqual(["text", "image"])
// "0.000002"/tok * 1e6 = 2.0/1M ; "0.000005"/tok -> 5.0 ; cache "0.0000005" -> 0.5
expect(gemma.cost[0]).toEqual({ input: 2, output: 5, cache: { read: 0.5, write: 0 } })
expect(gemma.limit).toEqual({ context: 131072, output: 131072 })

const asure = required(yield* catalog.model.get(LLMTR, ModelV2.ID.make("llmtr/trendyol-asure-12b")))
expect(asure.capabilities.tools).toBe(false)
expect(asure.cost[0].input).toBeCloseTo(0.1, 10)
}),
)
})