You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

221 lines
6.0 KiB

/**
* Llama.cpp Provider Extension
*
* Dynamically registers models from a llama.cpp llama-server instance
* by fetching /v1/models, loading them, and extracting context window sizes
*
* Usage:
* pi -e ./llama-cpp-provider.ts
*
* Or add to ~/.pi/agent/extensions/ for auto-discovery
*/
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import type { Model } from "@mariozechner/pi-ai";
// Configuration - can be overridden via environment variable
const LLAMA_SERVER_URL =
process.env.LLAMA_SERVER_URL || "http://example.com:8080/v1";
interface LlamaModel {
id: string;
aliases: string[];
tags: string[];
object: string;
owned_by: string;
created: number;
status: {
value: string;
args: string[];
preset: string;
};
}
interface LlamaModelsResponse {
data: LlamaModel[];
object: string;
}
/**
* Parse context window size from llama.cpp preset string
*
* Example preset:
* ```
* [Qwen3.5-35B]
* chat-template-kwargs = {"enable_thinking":false}
* jinja = 1
* min-p = 0.0
* temperature = 0.6
* top-k = 20
* top-p = 0.95
* ctx-size = 64000
* model = ./models/unsloth/unsloth_Qwen3.5-35B-A3B-GGUF_Qwen3.5-35B-A3B-UD-Q4_K_XL.gguf
* parallel = 1
* ```
*/
function parseContextWindow(preset: string): number {
const match = preset.match(/ctx-size\s*=\s*(\d+)/);
return match ? parseInt(match[1], 10) : 32000; // Default to 32k if not found
}
/**
* Fetch models from llama.cpp server
*/
async function fetchLlamaModels(): Promise<LlamaModel[]> {
try {
const response = await fetch(`${LLAMA_SERVER_URL}/models`);
if (!response.ok) {
throw new Error(`Failed to fetch models: ${response.status} ${response.statusText}`);
}
const data = (await response.json()) as LlamaModelsResponse;
return data.data || [];
} catch (error) {
console.error("[llama-cpp-provider] Error fetching models:", error);
return [];
}
}
/**
* Load a model on the llama.cpp server
* POST /models/load with { "model": "model_id" }
*/
async function loadLlamaModel(modelId: string): Promise<boolean> {
try {
const response = await fetch(`${LLAMA_SERVER_URL}/models/load`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ model: modelId }),
});
if (!response.ok) {
throw new Error(`Failed to load model ${modelId}: ${response.status} ${response.statusText}`);
}
const result = await response.json();
return result.success || false;
} catch (error) {
console.error(`[llama-cpp-provider] Error loading model ${modelId}:`, error);
return false;
}
}
/**
* Convert llama.cpp model to pi-ai Model configuration
*/
function llamaModelToPiModel(llamaModel: LlamaModel): Model<"openai-completions"> {
const contextWindow = parseContextWindow(llamaModel.status.preset);
return {
id: llamaModel.id,
name: llamaModel.id,
api: "openai-completions",
provider: "llama-cpp",
baseUrl: LLAMA_SERVER_URL.replace("/v1", ""),
reasoning: false, // llama.cpp doesn't support reasoning in the pi-ai sense
input: ["text"], // Check if model supports images based on ID or preset
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow,
maxTokens: Math.floor(contextWindow / 2), // Conservative estimate
};
}
/**
* Extension entry point
*/
export default function (pi: ExtensionAPI) {
let registered = false;
let models: Model<"openai-completions">[] = [];
/**
* Fetch and register models from llama.cpp server
*/
async function registerModels(ctx?: import("@mariozechner/pi-coding-agent").ExtensionContext) {
const llamaModels = await fetchLlamaModels();
if (llamaModels.length === 0) {
ctx?.ui.notify(
"No models found from llama.cpp server. Check URL and server status.",
"warning"
);
return;
}
models = llamaModels.map(llamaModelToPiModel);
pi.registerProvider("llama-cpp", {
baseUrl: LLAMA_SERVER_URL.replace("/v1", ""),
apiKey: "llama", // llama.cpp doesn't require auth, any value works
api: "openai-completions",
models,
});
ctx?.ui.notify(
`Registered ${models.length} models from llama.cpp server`,
"info"
);
registered = true;
}
/**
* Reload models (useful if models are added/removed from server)
*/
async function reloadModels(ctx?: import("@mariozechner/pi-coding-agent").ExtensionContext) {
if (!registered) {
await registerModels(ctx);
return;
}
// Unregister and re-register to update models
pi.unregisterProvider("llama-cpp");
await registerModels(ctx);
}
// Register models on load (no ctx available at extension load time)
registerModels().then(() => {
// Set runtime API key override after models are registered
// This prevents the "No models available" warning in the TUI
// Note: ctx is not available here, so we'll set it via session_start or commands
});
// Register a command to reload models
pi.registerCommand("llama-reload", {
description: "Reload models from llama.cpp server",
handler: async (_args: string, ctx) => {
ctx.ui.setWorkingMessage("Fetching and loading models from llama.cpp server...");
await reloadModels(ctx);
ctx.ui.setWorkingMessage();
},
});
// Also register a command to list models
pi.registerCommand("llama-list", {
description: "List available models from llama.cpp server",
handler: async (_args: string, ctx) => {
const llamaModels = await fetchLlamaModels();
if (llamaModels.length === 0) {
ctx.ui.notify("No models found from llama.cpp server", "warning");
return;
}
const lines = llamaModels.map((m) => {
const ctxSize = parseContextWindow(m.status.preset);
const status = m.status.value;
return ` ${m.id} - ${ctxSize} tokens - ${status}`;
});
ctx.ui.notify(
`Available models (${llamaModels.length}):\n${lines.join("\n")}`,
"info"
);
},
});
}