-
Notifications
You must be signed in to change notification settings - Fork 0
Cadenza Agent
A local AI agent in one file. Cadenza.Agent serves both the OpenAI Chat Completions API (/v1/chat/completions) and the OpenAI Responses API (/v1/responses) over the same IChatClient backend — so the same script talks to Aider, Continue, Cursor's BYOK, Copilot BYOK, and Codex CLI without changes.
#:sdk Cadenza.Agent@1.0.15| Base SDK | Microsoft.NET.Sdk.Web |
| Output | OpenAI-compatible HTTP server (default http://localhost:8080) |
| Endpoints |
/v1/chat/completions · /v1/responses · /v1/models
|
| Backends | Ollama, OpenAI, Anthropic, Azure OpenAI, any IChatClient
|
| AOT-clean | Microsoft.Extensions.AI's tool middleware uses reflection. For AOT, compose Cadenza Web + a manual IChatClient instead. |
#!/usr/bin/env dotnet run
#:sdk Cadenza.Agent@1.0.15
SystemPrompt("You are a helpful assistant with read-only filesystem access.");
Tool("read_file", "Read a UTF-8 text file from disk",
(string path) => ReadText(path));
Tool("list_files", "List files matching a glob pattern (e.g., **/*.cs)",
(string pattern) => Glob(pattern).ToArray());
UseOllama("llama3.2"); // or UseOpenAi / UseAnthropic / UseAzureOpenAi
await Run();Then point any OpenAI-compatible client at it:
export OPENAI_BASE_URL=http://localhost:8080/v1
export OPENAI_API_KEY=any-non-empty-string
codex # or: aider, continue, sgpt, cursor BYOK, copilot BYOKTool(name, description, handler)
SystemPrompt(text)
UseOllama(model, baseUrl = "http://localhost:11434")
UseOpenAi(model, apiKey = null) // OPENAI_API_KEY by default
UseAnthropic(model, apiKey = null) // ANTHROPIC_API_KEY by default
UseAzureOpenAi(endpoint, deployment, apiKey = null)
UseChatClient(IChatClient client) // escape hatch — any IChatClient
await Run() // start the HTTP server
await ChatLoop() // start a local REPL instead
var s = await Reply(prompt) // single-shot completionAnd these mutable properties on the Agent type (set them before Run()):
Agent.Port = 8080; // default 8080
Agent.HostName = "localhost"; // default "localhost" — change to "0.0.0.0" in containers
Agent.ServedModelName = "cadenza-agent"; // what shows up in /v1/models and editor pickers
Port,HostName,ServedModelNameare property assignments, not function calls.
UseOllama("llama3.2"); // localhost:11434
UseOllama("qwen2.5-coder", "http://gpu-box:11434"); // remoteUseOpenAi("gpt-4o-mini"); // OPENAI_API_KEY env var
UseOpenAi("gpt-4o", apiKey: "sk-..."); // explicit
UseAnthropic("claude-sonnet-4-5"); // ANTHROPIC_API_KEY env var
UseAzureOpenAi("https://my.openai.azure.com", "gpt-4o"); // AzureOpenRouter speaks the OpenAI wire format. There's no dedicated helper — plug it in through UseChatClient:
using System.ClientModel;
using OpenAI;
using Microsoft.Extensions.AI;
var apiKey = Env.Get("OPENROUTER_API_KEY")
?? throw new InvalidOperationException("OPENROUTER_API_KEY missing");
var model = Env.Get("OPENROUTER_MODEL") ?? "openai/gpt-4o-mini";
var opts = new OpenAIClientOptions { Endpoint = new Uri("https://openrouter.ai/api/v1") };
var client = new OpenAI.Chat.ChatClient(model, new ApiKeyCredential(apiKey), opts).AsIChatClient();
UseChatClient(client);
Agent.ServedModelName = $"openrouter:{model}";
await Run();The same pattern works for any OpenAI-wire-compatible endpoint (Groq, Together, Fireworks, etc.).
UseChatClient accepts any Microsoft.Extensions.AI.IChatClient. Wrap your own LLM here.
| Endpoint | What it speaks |
|---|---|
POST /v1/chat/completions |
OpenAI Chat Completions API (Aider / Continue / Cursor BYOK / Copilot BYOK) |
POST /v1/responses |
OpenAI Responses API (Codex CLI) |
GET /v1/models |
Returns Agent.ServedModelName
|
Both /v1/chat/completions and /v1/responses share the configured IChatClient. On the Responses path, client-supplied tools (e.g. Codex's shell / apply_patch) stream back to the client instead of being auto-invoked here — that's the right semantics for an agentic CLI.
When you don't want an HTTP server, drive the same agent from the console:
SystemPrompt("You are a code reviewer.");
UseOpenAi("gpt-4o-mini");
await ChatLoop(); // interactive
// or:
var summary = await Reply("Summarize this PR in 3 bullets…");
WriteLine(summary);- Local agents over your own filesystem, database, or APIs
- Drop-in OpenAI endpoint backed by tools you control
- Quick experiments needing a real chat surface (not just MCP)
- Codex / Aider / Continue all pointed at one local script
- You want MCP semantics (tools as the protocol, not a chat wrapper) → use Cadenza Mcp
- You need NativeAOT → compose Cadenza Web +
IChatClientmanually
The default HostName is localhost, so the server only binds to loopback. Inside a container, set Agent.HostName = "0.0.0.0" first or external requests can't reach it:
Agent.HostName = "0.0.0.0";
Agent.Port = 8080;
SystemPrompt("…");
UseOllama("llama3.2");
await Run();See Deployment Container.
-
samples/agent-basic.cs— Ollama + filesystem tools -
samples/agent-console-repl.cs— drive viaChatLoop() -
samples/agent-multi-llm.cs— switch among backends at runtime -
samples/agent-openrouter.cs— OpenRouter viaUseChatClient -
samples/agent-rag-folder.cs— lightweight RAG over a folder -
samples/agent-codex-backend.cs— Codex CLI over/v1/responses -
samples/agent-codex-openrouter.cs— Codex CLI + OpenRouter
See Samples for the full annotated list.
-
Tier 1 API —
Tool,SystemPrompt,Use*,Run,ChatLoop,Reply - Cadenza Mcp — MCP tool semantics when chat isn't what you want
- Deployment Container
Cadenza · MIT · .NET 10+ · github.com/rkttu/cadenza · No orchestra. No project. Just one file, playing solo.