@thinkyou0714/fugu
v0.2.1
Published
Zero-dependency TypeScript client + CLI for the Sakana Fugu OpenAI-compatible API (Responses + Chat Completions).
Maintainers
Readme
@thinkyou0714/fugu
Zero-dependency TypeScript client + CLI for the Sakana Fugu OpenAI-compatible API.
Fugu is Sakana AI's multi-agent orchestration system exposed as a single model behind one OpenAI-compatible endpoint: you send one request and Fugu decides internally whether to answer directly or to delegate to and synthesize a team of expert frontier models.
Status: unit-tested offline (mocked
fetch) + e2e against a local socket stub. A live call againstapi.sakana.aineeds a realSAKANA_API_KEY— supply one and runnpm run smoketo confirm end-to-end.
Highlights
- Zero runtime dependencies. Built-in
fetch; ESM-only; Node >= 22.9. - Typed error hierarchy (
FuguAuthError,FuguRateLimitError,FuguTimeoutError, …) withcode/status/requestId/retryAfterMs/isRetryable. - Secret-safe: the raw response body is never stored on errors or logged; messages and
--jsonoutput are redacted (Bearer …/sk-…/ deny-listed keys). - Typed usage + cost, including Fugu's hidden, billed orchestration tokens.
- Effort-scaled timeouts (
reasoning.effort∈high | xhigh | max) andstatus/incomplete/finishReasonsurfaced (no silent empty strings). - Resilient transport: retries with exponential backoff + jitter honoring
Retry-After, a stableIdempotency-Keyacross retries, timeout/abort/network classification. - Streaming (
respondStream/chatStream) over SSE — zero-dep. - Cost controls: a
BudgetGuardspend circuit-breaker, output-token + input-size caps,chooseModel()routing (fugu↔fugu-ultra), and an opt-in request cache (MemoryCache, LRU+TTL) — identical calls skip the network and the budget charge. - Strategy patterns: a confidence-gated
Cascade(cheap model → judge → escalate), an eval harness (runEval+ neutralllmGrader) to measure quality/cost/latency, and concurrency primitives (WorkPoolwith priority lanes,SingleFlightde-dup) for bulk runs. - Tool calling (
tools+runToolsagentic loop, built-inweb_search), structured output (respondJsonwith validate-and-repair), statefulConversationchaining, and observability hooks (onRequest/onResponse/logger— wire pino/OpenTelemetry; the core stays dependency-free). - Ecosystem: a multi-provider
FuguRouter(failover) and an OpenAI-compatible proxy (createProxyServer/fugu-proxybin) so Cursor / n8n / any OpenAI-SDK tool can target Fugu — with failover — at alocalhostendpoint; a Fugu MCP server (integrations/mcp/,fugu_respond/fugu_chat) for Claude Code / Cursor / Codex; a zero-depfugu-obsidianCLI (integrations/obsidian/) that answers questions about your Obsidian notes via the Local REST API; an n8n community node (integrations/n8n/) — a declarativeFugunode +Fugu APIcredential; and a Claude Code/fuguskill +fugusubagent (.claude/, mirrored as a Cursor command) that routes "second opinion" / adversarial-review asks to the MCP server.
Install
npm install @thinkyou0714/fugu # core (zero deps)
npm install @thinkyou0714/fugu openai # optional: the ./openai adapterCLI
export SAKANA_API_KEY=... # from https://console.sakana.ai/get-started
npx fugu "Explain Sakana Fugu in one sentence." --usage
npx fugu "Refactor this..." --model fugu-ultra --effort high
echo "summarize this" | npx fugu --chat
npx fugu --help--usage prints token usage (incl. orchestration) + estimated cost to stderr; --json
prints the (redacted) raw response.
Verify your setup (live)
Once you have a key, confirm a real round-trip works:
export SAKANA_API_KEY=... # or: cp .env.example .env && edit
npm run smoke # one minimal call to api.sakana.aiIt prints a PASS/FAIL banner with latency + token usage. On failure it maps the error
to a concrete fix (401 → re-copy the key, 403 → plan/model access, 429 → wait,
connection/parse → check SAKANA_BASE_URL, timeout → raise/retry). Exit codes: 0 pass,
1 reached the API but failed, 2 not configured (no key) — so CI can skip cleanly.
Troubleshooting
npm run smoke and the CLI map every typed error to a concrete fix. The same diagnosis
applies in code — branch on err.code, or use the exported type guards (isAuthError,
isRetryable, …):
| Error (code / status) | Likely cause | Fix |
|---|---|---|
| auth (401) | key missing / invalid / expired | re-copy SAKANA_API_KEY from the console |
| permission (403) | plan or model not allowed | check tier + model access (fugu-ultra may need a higher plan) |
| rate_limit (429) | too many requests | wait retryAfterMs, lower concurrency |
| connection / parse | wrong base URL / non-Fugu endpoint | verify SAKANA_BASE_URL (default https://api.sakana.ai/v1) |
| timeout | slow run or wrong host | raise the timeout / retry; instant timeouts ⇒ check the host |
| bad_request (400/404/422) | bad model id or params | check the model (fugu / fugu-ultra) and request body |
Programmatic
import { createClient, loadConfig, FuguRateLimitError } from "@thinkyou0714/fugu";
const client = createClient(loadConfig());
try {
const r = await client.respond("Give me a haiku about pufferfish.", { reasoningEffort: "high" });
console.log(r.text);
console.log(r.usage, r.costUsd); // typed usage + estimated cost
} catch (e) {
if (e instanceof FuguRateLimitError) await sleep(e.retryAfterMs ?? 1000);
}Streaming + budget + routing
import { createClient, loadConfig, BudgetGuard, chooseModel } from "@thinkyou0714/fugu";
const client = createClient(loadConfig(), { budget: new BudgetGuard({ limitUsd: 5 }) });
const model = chooseModel({ chars: prompt.length, task: "code" }); // fugu ↔ fugu-ultra
for await (const ev of client.respondStream(prompt, { model })) {
if (ev.type === "delta") process.stdout.write(ev.textDelta ?? "");
else console.log("\n", ev.result?.costUsd, ev.result?.usage);
}Tools, structured output, observability
import { createClient, loadConfig, functionTool, webSearchTool } from "@thinkyou0714/fugu";
const client = createClient(loadConfig(), { onResponse: (e) => metrics.record(e) });
// agentic tool loop
const res = await client.runTools([{ role: "user", content: "weather in Tokyo?" }], {
tools: [functionTool("getWeather", { parameters: { type: "object", properties: { city: { type: "string" } } } }), webSearchTool()],
handlers: { getWeather: async (args) => fetchWeather((args as { city: string }).city) },
});
// structured output with validate-and-repair
const { data } = await client.respondJson<{ score: number }>("Rate this 1-10", {
schema: { type: "object", properties: { score: { type: "number" } }, required: ["score"] },
validate: (v) => {
const o = v as { score?: unknown };
if (typeof o.score !== "number") throw new Error("score must be a number");
return o as { score: number };
},
});Cache, confidence cascade & evals
import { createClient, loadConfig, MemoryCache, Cascade, llmJudge, runEval, llmGrader } from "@thinkyou0714/fugu";
// opt-in cache: identical requests are free (no network, no budget charge)
const cache = new MemoryCache({ maxEntries: 1000, ttlMs: 60 * 60_000 });
const client = createClient(loadConfig(), { cache });
// confidence cascade: try fugu, escalate to fugu-ultra only when the judge isn't sure
const cascade = new Cascade(client, {
stages: [{ model: "fugu" }, { model: "fugu-ultra", effort: "high" }],
judge: llmJudge(client, { threshold: 0.7 }), // or the zero-cost default `statusJudge`
});
const { result, stageIndex, escalations } = await cascade.run("A hard question…");
// evals: measure quality/cost/latency over a golden set (neutral judge ≠ system under test)
const report = await runEval(client, goldenSet, { grader: llmGrader(judgeClient), concurrency: 4 });
console.log(report.passRate, report.totalCostUsd, report.avgLatencyMs);Router + proxy (use Fugu from any OpenAI-SDK tool, with failover)
import { FuguClient, FuguRouter, createProxyServer, loadConfig } from "@thinkyou0714/fugu";
const router = new FuguRouter({
providers: [
{ name: "fugu", client: new FuguClient(loadConfig()), model: "fugu-ultra" },
{ name: "backup", client: new FuguClient({ ...loadConfig(), baseUrl: "https://backup/v1" }) },
],
});
// one localhost OpenAI endpoint any tool (Cursor, n8n, scripts) can target:
createProxyServer({ backend: router, token: "local-secret" }).listen(4141);
// or just run the bin: npx fugu-proxy -> http://localhost:4141/v1Optional OpenAI-SDK adapter
import { createFuguOpenAI } from "@thinkyou0714/fugu/openai"; // requires `openai` installed
const openai = await createFuguOpenAI();
const res = await openai.responses.create({ model: "fugu-ultra", input: "hi" });Develop
Source runs directly via node --experimental-strip-types — no build needed for dev/tests.
npm test # offline (mocked fetch) + real timeout wiring
npm run smoke # live: one real round-trip to api.sakana.ai (needs SAKANA_API_KEY)
npm run coverage # tests + coverage thresholds
npm run typecheck # tsc --noEmit (erasableSyntaxOnly)
npm run lint # biome
npm run build # tsdown -> dist (ESM + .d.ts)
npm run check:exports # build + publint + are-the-types-wrong (esm-only)Layout
fugu/
├── src/
│ ├── index.ts # public API barrel (the supported entry point)
│ ├── config.ts # env loading, defaults, effort-scaled timeout
│ ├── errors.ts # typed FuguError hierarchy + HTTP→error mapping
│ ├── redact.ts # secret redaction
│ ├── types.ts # FuguResult / FuguUsage + tolerant parsers
│ ├── pricing.ts # price table + cost estimation
│ ├── fugu-client.ts # FuguClient: respond() / chat() / *Stream()
│ ├── retry.ts # backoff + jitter + Retry-After
│ ├── budget.ts # BudgetGuard spend circuit-breaker
│ ├── routing.ts # chooseModel() policy
│ ├── stream.ts # SSE parsing
│ ├── tools.ts # tool-calling types + parsing
│ ├── json.ts # loose JSON extraction (structured output)
│ ├── observe.ts # logging / metrics hooks
│ ├── conversation.ts # stateful Responses chaining
│ ├── cache.ts # request cache (MemoryCache LRU+TTL; RequestCache)
│ ├── pool.ts # WorkPool (bounded concurrency) + SingleFlight
│ ├── cascade.ts # confidence-gated model cascade + judges
│ ├── evals.ts # golden-set eval harness + graders
│ ├── router.ts # multi-provider failover (FuguRouter)
│ ├── proxy.ts # OpenAI-compatible proxy (bin: fugu-proxy)
│ ├── cli.ts # CLI (bin: fugu)
│ └── openai.ts # optional ./openai adapter
├── test/ # client / timeout / p2-p5 / mcp / obsidian / strategy tests
├── integrations/
│ ├── mcp/ # Fugu MCP server (own package; @modelcontextprotocol/sdk + zod)
│ ├── obsidian/ # fugu-obsidian CLI (own package; zero-dep, Local REST API)
│ └── n8n/ # n8n-nodes-fugu (own package; declarative node + credential)
├── .claude/ # /fugu skill + fugu subagent (delegate to the MCP server)
├── .cursor/ # mirrored Cursor command
├── .github/workflows/ # ci / release (changesets + npm OIDC) / codeql (templates)
└── tsdown.config.ts · biome.json · .changeset · tsconfig.jsonPublishing
Releases use Changesets + npm Trusted
Publishing (OIDC, no long-lived token, automatic provenance). Add a changeset per
user-facing change with npm run changeset. See .github/workflows/release.yml.
API reference (as of 2026-06)
Compiled from Sakana's site/console and public guides (see Sources); not verified against a live call in this repo — confirm against your console dashboard.
| Item | Value |
|-----------|-------------------------------------------------------------------|
| Base URL | https://api.sakana.ai/v1 (copy the exact value from your console) |
| Models | fugu (fast), fugu-ultra (max quality) |
| Auth | Authorization: Bearer $SAKANA_API_KEY |
| Endpoints | /responses (recommended), /chat/completions, /models |
| Effort | reasoning.effort ∈ high / xhigh / max |
Sources: Sakana Fugu · console · Apidog guide
