commit
7e8a9cd55b
3 changed files with 270 additions and 0 deletions
@ -0,0 +1,48 @@
|
||||
# Llama.cpp Provider Extension for Pi |
||||
|
||||
This extension dynamically registers models from a llama.cpp `llama-server` instance with their context window sizes. |
||||
|
||||
## Installation |
||||
|
||||
### Option 1: Quick Test |
||||
```bash |
||||
pi -e /path/to/pi_agent_llama_provider/llama-cpp-provider.ts |
||||
``` |
||||
|
||||
### Option 2: Auto-Discovery |
||||
Copy the extension to your global extensions directory: |
||||
```bash |
||||
cp llama-cpp-provider.ts ~/.pi/agent/extensions/ |
||||
``` |
||||
|
||||
Then run `/reload` in pi to load it automatically. |
||||
|
||||
### Option 3: Custom Server URL |
||||
Set the `LLAMA_SERVER_URL` environment variable before starting pi: |
||||
```bash |
||||
export LLAMA_SERVER_URL="http://your-server:port/v1" |
||||
pi |
||||
``` |
||||
|
||||
Or modify the `LLAMA_SERVER_URL` constant in the extension file. |
||||
|
||||
## Usage |
||||
|
||||
Once loaded, the extension will: |
||||
|
||||
1. Fetch all models from the llama.cpp server |
||||
2. Parse the context window size from each model's preset |
||||
3. Register them as a `llama-cpp` provider |
||||
4. Make them available via `/model` or `Ctrl+L` |
||||
|
||||
### Commands |
||||
|
||||
- `/llama-reload` - Reload and re-load models from the server |
||||
- `/llama-list` - List all available models with their context sizes |
||||
|
||||
### Known Issues |
||||
|
||||
If you run this extension as your only provider for pi agent, you'll get a |
||||
warning about no loaded models. My fix was to use an empty model list in the |
||||
default models.ini file: |
||||
https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/docs/models.md |
||||
@ -0,0 +1 @@
|
||||
{"data":[{"id":"GLM-4.7-Flash","aliases":[],"tags":[],"object":"model","owned_by":"llamacpp","created":1773431100,"status":{"value":"unloaded","args":["/home/llama/llama.cpp/build/bin/llama-server","--host","127.0.0.1","--jinja","--port","0","--alias","GLM-4.7-Flash","--ctx-size","32000","--model","./models/unsloth_GLM-4.7-Flash-GGUF_GLM-4.7-Flash-UD-Q3_K_XL.gguf","--parallel","1"],"preset":"[GLM-4.7-Flash]\njinja = 1\nctx-size = 32000\nmodel = ./models/unsloth_GLM-4.7-Flash-GGUF_GLM-4.7-Flash-UD-Q3_K_XL.gguf\nparallel = 1\n\n"}},{"id":"Qwen3-4B-Instruct","aliases":[],"tags":[],"object":"model","owned_by":"llamacpp","created":1773431100,"status":{"value":"unloaded","args":["/home/llama/llama.cpp/build/bin/llama-server","--host","127.0.0.1","--jinja","--port","0","--alias","Qwen3-4B-Instruct","--ctx-size","32000","--model","./models/unsloth_Qwen3-4B-Instruct-2507-GGUF_Qwen3-4B-Instruct-2507-UD-Q4_K_XL.gguf","--parallel","1"],"preset":"[Qwen3-4B-Instruct]\njinja = 1\nctx-size = 32000\nmodel = ./models/unsloth_Qwen3-4B-Instruct-2507-GGUF_Qwen3-4B-Instruct-2507-UD-Q4_K_XL.gguf\nparallel = 1\n\n"}},{"id":"Qwen3-Coder-30B","aliases":[],"tags":[],"object":"model","owned_by":"llamacpp","created":1773431100,"status":{"value":"unloaded","args":["/home/llama/llama.cpp/build/bin/llama-server","--host","127.0.0.1","--jinja","--port","0","--alias","Qwen3-Coder-30B","--ctx-size","32000","--model","./models/Qwen3-Coder-30B-A3B-Instruct-UD-Q4_K_XL.gguf","--parallel","1"],"preset":"[Qwen3-Coder-30B]\njinja = 1\nctx-size = 32000\nmodel = ./models/Qwen3-Coder-30B-A3B-Instruct-UD-Q4_K_XL.gguf\nparallel = 1\n\n"}},{"id":"Qwen3.5-35B","aliases":[],"tags":[],"object":"model","owned_by":"llamacpp","created":1773431100,"status":{"value":"loaded","args":["/home/llama/llama.cpp/build/bin/llama-server","--chat-template-kwargs","{\"enable_thinking\":false}","--host","127.0.0.1","--jinja","--min-p","0.0","--port","40051","--temperature","0.6","--top-k","20","--top-p","0.95","--alias","Qwen3.5-35B","--ctx-size","64000","--model","./models/unsloth/unsloth_Qwen3.5-35B-A3B-GGUF_Qwen3.5-35B-A3B-UD-Q4_K_XL.gguf","--parallel","1"],"preset":"[Qwen3.5-35B]\nchat-template-kwargs = {\"enable_thinking\":false}\njinja = 1\nmin-p = 0.0\ntemperature = 0.6\ntop-k = 20\ntop-p = 0.95\nctx-size = 64000\nmodel = ./models/unsloth/unsloth_Qwen3.5-35B-A3B-GGUF_Qwen3.5-35B-A3B-UD-Q4_K_XL.gguf\nparallel = 1\n\n"}},{"id":"default","aliases":[],"tags":[],"object":"model","owned_by":"llamacpp","created":1773431100,"status":{"value":"unloaded","args":["/home/llama/llama.cpp/build/bin/llama-server","--host","127.0.0.1","--jinja","--port","0","--alias","default","--ctx-size","32000","--parallel","1"],"preset":"[default]\njinja = 1\nctx-size = 32000\nparallel = 1\n\n"}}],"object":"list"} |
||||
@ -0,0 +1,221 @@
|
||||
/** |
||||
* 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" |
||||
); |
||||
}, |
||||
}); |
||||
} |
||||
Loading…
Reference in new issue