Skip to content

Cadenza Agent

Jung Hyun, Nam edited this page May 28, 2026 · 2 revisions

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 ⚠️ No — Microsoft.Extensions.AI's tool middleware uses reflection. For AOT, compose Cadenza Web + a manual IChatClient instead.

Minimal example

#!/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 BYOK

Tier 1 surface

Tool(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 completion

And 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, ServedModelName are property assignments, not function calls.

Backends

Local: Ollama

UseOllama("llama3.2");                                    // localhost:11434
UseOllama("qwen2.5-coder", "http://gpu-box:11434");       // remote

Hosted

UseOpenAi("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");  // Azure

OpenRouter (via UseChatClient)

OpenRouter 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.).

Custom

UseChatClient accepts any Microsoft.Extensions.AI.IChatClient. Wrap your own LLM here.

Endpoints exposed

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.

REPL and single-shot

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);

When to use it

  • 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

When not to use it

  • You want MCP semantics (tools as the protocol, not a chat wrapper) → use Cadenza Mcp
  • You need NativeAOT → compose Cadenza Web + IChatClient manually

Container caveat

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

  • samples/agent-basic.cs — Ollama + filesystem tools
  • samples/agent-console-repl.cs — drive via ChatLoop()
  • samples/agent-multi-llm.cs — switch among backends at runtime
  • samples/agent-openrouter.cs — OpenRouter via UseChatClient
  • 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.

See also

Clone this wiki locally