Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 16 additions & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ program
// Legacy per-provider key flags for muscle memory
.option("--nim-key <key>", "NVIDIA NIM API key (shorthand)")
.option("--openrouter-key <key>", "OpenRouter API key (shorthand)")
.option("--groq-key <key>", "Groq API key (shorthand)")
.option("--model <model>", "Model to use (overrides per-provider default)")
.option("--port <port>", "Proxy server port (default: auto)")
.option("--proxy-only", "Start proxy server only (don't launch claude)")
Expand All @@ -72,7 +73,8 @@ program
const cliKey =
opts.key ??
(provider === "nim" ? opts.nimKey : undefined) ??
(provider === "openrouter" ? opts.openrouterKey : undefined);
(provider === "openrouter" ? opts.openrouterKey : undefined) ??
(provider === "groq" ? opts.groqKey : undefined);

let apiKey = resolveApiKey(provider, cliKey);

Expand Down Expand Up @@ -176,6 +178,7 @@ program
"--key", cliKey,
"--nim-key", opts.nimKey,
"--openrouter-key", opts.openrouterKey,
"--groq-key", opts.groqKey,
"--model", opts.model,
"--port", opts.port,
"--no-waifu",
Expand Down Expand Up @@ -232,6 +235,7 @@ program
.option("--key <key>", "Set API key for the current/specified provider")
.option("--nim-key <key>", "Set NVIDIA NIM API key")
.option("--openrouter-key <key>", "Set OpenRouter API key")
.option("--groq-key <key>", "Set Groq API key")
.option("--model <model>", "Set default model")
.option("--reset", "Reset all configuration")
.action((opts) => {
Expand All @@ -248,6 +252,7 @@ program
if (opts.model) { cfg.model = opts.model; changed = true; }
if (opts.nimKey) { cfg.nimApiKey = opts.nimKey; changed = true; }
if (opts.openrouterKey) { cfg.openrouterApiKey = opts.openrouterKey; changed = true; }
if (opts.groqKey) { cfg.groqApiKey = opts.groqKey; changed = true; }
if (opts.key) {
const provider = cfg.provider ?? "nim";
saveApiKey(provider, opts.key);
Expand All @@ -272,6 +277,7 @@ program
console.log(`\n API Keys:`);
console.log(` NIM : ${mask(cfg.nimApiKey)}`);
console.log(` OpenRouter : ${mask(cfg.openrouterApiKey)}`);
console.log(` Groq : ${mask(cfg.groqApiKey)}`);
console.log(` Ollama : (no key needed)`);
console.log("");
});
Expand Down Expand Up @@ -320,6 +326,10 @@ program
url: "https://openrouter.ai/keys",
free: "Free models available",
},
groq: {
url: "https://console.groq.com/keys",
free: "Free tier available",
},
ollama: {
url: "https://ollama.com",
free: "Fully local — no API key needed",
Expand Down Expand Up @@ -352,6 +362,11 @@ function keyHelpText(provider: ProviderName): string {
" waifu --provider openrouter --key sk-or-xxx\n" +
" export OPENROUTER_API_KEY=sk-or-xxx\n\n" +
"Get a free key at: \x1b[36mhttps://openrouter.ai/keys\x1b[0m",
groq:
"Provide your key:\n" +
" waifu --provider groq --key gsk_xxx\n" +
" export GROQ_API_KEY=gsk_xxx\n\n" +
"Get a free key at: \x1b[36mhttps://console.groq.com/keys\x1b[0m",
ollama:
"Ollama runs locally and needs no API key.\n" +
"Install from: \x1b[36mhttps://ollama.com\x1b[0m\n" +
Expand Down
11 changes: 9 additions & 2 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,26 @@ import { join } from "node:path";
import { DEFAULT_MODEL as NIM_DEFAULT_MODEL } from "./providers/nim.js";
import { OPENROUTER_DEFAULT_MODEL } from "./providers/openrouter.js";
import { OLLAMA_DEFAULT_MODEL } from "./providers/ollama.js";
import { GROQ_DEFAULT_MODEL } from "./providers/groq.js";

// ── Provider names ────────────────────────────────────────────────────────────

export type ProviderName = "nim" | "openrouter" | "ollama";
export type ProviderName = "nim" | "openrouter" | "ollama" | "groq";

export const PROVIDER_NAMES: ProviderName[] = ["nim", "openrouter", "ollama"];
export const PROVIDER_NAMES: ProviderName[] = ["nim", "openrouter", "ollama", "groq"];

export const PROVIDER_DEFAULT_MODELS: Record<ProviderName, string> = {
nim: NIM_DEFAULT_MODEL,
openrouter: OPENROUTER_DEFAULT_MODEL,
ollama: OLLAMA_DEFAULT_MODEL,
groq: GROQ_DEFAULT_MODEL,
};

export const PROVIDER_KEY_ENV: Record<ProviderName, string[]> = {
nim: ["NIM_API_KEY", "NVIDIA_NIM_API_KEY"],
openrouter: ["OPENROUTER_API_KEY"],
ollama: [], // no key needed
groq: ["GROQ_API_KEY"],
};

// ── Config shape ──────────────────────────────────────────────────────────────
Expand All @@ -37,6 +40,7 @@ export interface WaifuConfig {
// Per-provider API keys
nimApiKey?: string;
openrouterApiKey?: string;
groqApiKey?: string;
// Ollama needs no key but supports a custom host
ollamaBaseUrl?: string;
}
Expand Down Expand Up @@ -77,6 +81,7 @@ export function resolveProvider(cliProvider?: string): ProviderName {
}
// Auto-detect from available keys
const cfg = loadConfig();
if (cfg.groqApiKey || process.env.GROQ_API_KEY) return "groq";
if (cfg.openrouterApiKey || process.env.OPENROUTER_API_KEY) return "openrouter";
return "nim"; // default
}
Expand All @@ -102,6 +107,7 @@ export function resolveApiKey(provider: ProviderName, cliKey?: string): string |
switch (provider) {
case "nim": return cfg.nimApiKey ?? null;
case "openrouter": return cfg.openrouterApiKey ?? null;
case "groq": return cfg.groqApiKey ?? null;
case "ollama": return null; // no key
}
}
Expand All @@ -112,6 +118,7 @@ export function saveApiKey(provider: ProviderName, key: string): void {
switch (provider) {
case "nim": cfg.nimApiKey = key; break;
case "openrouter": cfg.openrouterApiKey = key; break;
case "groq": cfg.groqApiKey = key; break;
case "ollama": break;
}
saveConfig(cfg);
Expand Down
38 changes: 38 additions & 0 deletions src/providers/groq.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* Groq provider.
*
* Groq provides extremely fast inference through their LPU inference engines.
* It uses an OpenAI-compatible endpoint.
*
* Get a free API key at: https://console.groq.com/keys
*/

import { streamProviderResponse, type ProviderConfig } from "./base.js";
import type { EventDetector } from "../observer/eventDetector.js";

export const GROQ_BASE_URL = "https://api.groq.com/openai/v1";
export const GROQ_DEFAULT_MODEL = "llama-3.3-70b-versatile";

export interface GroqConfig {
apiKey: string;
model?: string;
maxTokens?: number;
}

export async function* streamGroqResponse(
requestData: unknown,
config: GroqConfig,
inputTokens: number,
requestId?: string,
detector?: EventDetector
): AsyncGenerator<string> {
const cfg: ProviderConfig = {
name: "Groq",
baseUrl: GROQ_BASE_URL,
apiKey: config.apiKey,
model: config.model ?? GROQ_DEFAULT_MODEL,
maxTokens: config.maxTokens,
};

yield* streamProviderResponse(requestData, cfg, inputTokens, requestId, detector);
}
9 changes: 9 additions & 0 deletions src/proxy/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import type { ProviderName } from "../config.js";
import { streamNimResponse, type NimConfig } from "../providers/nim.js";
import { streamOpenRouterResponse, type OpenRouterConfig } from "../providers/openrouter.js";
import { streamOllamaResponse, type OllamaConfig } from "../providers/ollama.js";
import { streamGroqResponse, type GroqConfig } from "../providers/groq.js";

// ── Context passed from server.ts ─────────────────────────────────────────────

Expand Down Expand Up @@ -99,6 +100,14 @@ function streamForProvider(
return streamOllamaResponse(requestData, cfg, inputTokens, requestId, detector);
}

case "groq": {
const cfg: GroqConfig = {
apiKey: apiKey!,
model,
};
return streamGroqResponse(requestData, cfg, inputTokens, requestId, detector);
}

case "nim":
default: {
const cfg: NimConfig = { apiKey: apiKey!, model };
Expand Down