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
76 changes: 76 additions & 0 deletions packages/core/src/plugin/provider/llmtr-reasoning.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Thinking-control handling for LLMTR models, kept in a dependency-free module so
// the LLMTR plugin (which populates the per-model mode) and the request path (which
// applies the body rewrite) can share it without an import cycle through the
// provider-plugin registry.
//
// LLMTR fronts many upstreams whose "thinking" controls differ: some accept
// OpenAI-style graded reasoning_effort ("effort"), some only an on/off `reasoning`
// flag ("boolean"), and some take no reasoning parameter at all ("none"). The
// rewriter is registered generically (see request-transform.ts) so the shared
// request path carries no LLMTR-specific branch.

import { registerBodyRewriter } from "./request-transform"

export type ReasoningMode = "effort" | "boolean" | "none"

// Per-model reasoning mode, keyed by catalog model id. Populated as models are
// projected (seed at startup, then the live `/v1/models` fetch).
const REASONING_MODES = new Map<string, ReasoningMode>()

/** Classifies a model's thinking control from its advertised parameters. */
export function classifyReasoning(supported?: readonly string[]): ReasoningMode {
if (supported?.includes("reasoning_effort")) return "effort"
if (supported?.includes("reasoning")) return "boolean"
return "none"
}

/** Records the reasoning mode for a catalog model id. */
export function setLLMTRReasoningMode(modelID: string, supported?: readonly string[]): void {
REASONING_MODES.set(modelID, classifyReasoning(supported))
}

/** Reasoning mode for an LLMTR catalog model id, or undefined when unknown. */
export function llmtrReasoningMode(modelID: string): ReasoningMode | undefined {
return REASONING_MODES.get(modelID)
}

function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value)
}

// Normalizes an outgoing reasoning_effort for a model that does NOT take
// OpenAI-style graded effort, given the model's reasoning mode:
// - "boolean": the model exposes an on/off `reasoning` flag — a graded effort
// maps to `reasoning: true`; an explicit off/none/minimal/disabled leaves
// thinking off; an existing `reasoning` flag is preserved.
// - "none": the model takes no reasoning parameter — strip reasoning_effort and
// add nothing.
// Effort-capable models are never passed here (their reasoning_effort is valid).
// The original string is returned unchanged when it is not JSON or carries no
// reasoning_effort. Exported for tests.
export function rewriteLLMTRReasoningBody(bodyText: string, mode: "boolean" | "none"): string {
let body: Record<string, unknown>
try {
body = JSON.parse(bodyText)
} catch {
return bodyText
}
if (!isRecord(body) || !("reasoning_effort" in body)) return bodyText
const effort = body["reasoning_effort"]
delete body["reasoning_effort"]
if (mode === "boolean") {
const disabled =
effort === false ||
(typeof effort === "string" && ["off", "none", "minimal", "disabled"].includes(effort.toLowerCase()))
if (!disabled && body["reasoning"] === undefined) body["reasoning"] = true
}
return JSON.stringify(body)
}

// Register the LLMTR body rewriter generically. Effort-capable ("effort") and
// unknown models return undefined, so only boolean/none models are rewritten.
registerBodyRewriter("llmtr", (modelID) => {
const mode = llmtrReasoningMode(modelID)
if (mode !== "boolean" && mode !== "none") return undefined
return (body) => rewriteLLMTRReasoningBody(body, mode)
})
Loading