feat(ai-proxy): provider-agnostic upstream (enables a sats-only provider)
The proxy now reads its upstream from SASQUATCH_AI_BASE + SASQUATCH_AI_KEY (falling back to Netlify's injected OPENROUTER_* vars), and the model allowlist is overridable via SASQUATCH_MODELS. Default behaviour is unchanged (gateway). This makes switching to a sats-only provider a config change, e.g. Routstr: SASQUATCH_AI_BASE=https://api.routstr.com/v1 SASQUATCH_AI_KEY=cashuA… (raw token) or sk-… (session) NOTE: api.routstr.com was returning 404 (empty models) on 2026-09-27, so we are NOT switching yet. Documented in AGENTS.md.
This commit is contained in:
parent
f94297b196
commit
a5a134c6c0
2 changed files with 34 additions and 17 deletions
|
|
@ -122,6 +122,12 @@ session with **no other memory of this project**. So:
|
|||
(`useCrewAgentChat`). Callers without credentials 401 and fall back to
|
||||
keyless **Pollinations**. The pay-to address is single-sourced in
|
||||
`src/lib/platform.ts`.
|
||||
The upstream is **provider-agnostic**: `SASQUATCH_AI_BASE` + `SASQUATCH_AI_KEY`
|
||||
override the Netlify gateway (any OpenAI-compatible endpoint). For a
|
||||
**sats-only** provider use Routstr (`SASQUATCH_AI_BASE=https://api.routstr.com/v1`,
|
||||
key = a cashu…/sk- session token; `SASQUATCH_MODELS` must match `PROXY_MODELS`).
|
||||
⚠️ As of 2026-09-27 **`api.routstr.com` returned 404 for every endpoint and its
|
||||
model list was empty** — do not switch to it until it serves models.
|
||||
**AI answers are Pro-only**: free users get the static FAQ/glossary + local
|
||||
relay lookups only (`if (isPro)` gates the AI block in `SasquatchGuide`).
|
||||
Pro access (`useSasquatchPro`) is granted by a provider-authored zap receipt,
|
||||
|
|
|
|||
|
|
@ -15,22 +15,27 @@
|
|||
// When stream:true, the upstream SSE is passed straight through.
|
||||
import { verifyEvent } from 'nostr-tools';
|
||||
|
||||
// Netlify's AI Gateway injects OPENROUTER_API_KEY + OPENROUTER_BASE_URL into
|
||||
// Functions (server-side only), so no key is managed or shipped by us. If you
|
||||
// later set your OWN OPENROUTER_API_KEY in the project env, Netlify stops
|
||||
// injecting both, and this falls back to the public OpenRouter endpoint — so it
|
||||
// works either way.
|
||||
const OPENROUTER_BASE = (process.env.OPENROUTER_BASE_URL || 'https://openrouter.ai/api/v1').replace(
|
||||
/\/+$/,
|
||||
'',
|
||||
);
|
||||
const OPENROUTER_URL = `${OPENROUTER_BASE}/chat/completions`;
|
||||
// The upstream is provider-agnostic (any OpenAI-compatible endpoint):
|
||||
// • default: Netlify's AI Gateway (it injects OPENROUTER_API_KEY +
|
||||
// OPENROUTER_BASE_URL server-side, so no key is managed or shipped);
|
||||
// • or set your own key: SASQUATCH_AI_BASE + SASQUATCH_AI_KEY (overrides).
|
||||
// This is how you switch to a sats-only provider (e.g. Routstr:
|
||||
// SASQUATCH_AI_BASE=https://api.routstr.com/v1, SASQUATCH_AI_KEY=cashuA… or
|
||||
// sk-…) without touching code — just remember the model list must match.
|
||||
const AI_BASE = (
|
||||
process.env.SASQUATCH_AI_BASE ||
|
||||
process.env.OPENROUTER_BASE_URL ||
|
||||
'https://openrouter.ai/api/v1'
|
||||
).replace(/\/+$/, '');
|
||||
const AI_URL = `${AI_BASE}/chat/completions`;
|
||||
const AI_KEY = process.env.SASQUATCH_AI_KEY || process.env.OPENROUTER_API_KEY;
|
||||
|
||||
// Models Sasquatch may request. Curated on purpose: the client sends the model
|
||||
// slug, so an allowlist stops anyone using this endpoint as an open proxy for
|
||||
// arbitrary (potentially expensive) models. These are fast, cheap, capable slugs
|
||||
// served through Netlify's AI Gateway.
|
||||
const ALLOWED_MODELS = new Set([
|
||||
// arbitrary (potentially expensive) models. Override with SASQUATCH_MODELS
|
||||
// (comma-separated) when switching provider — it must match PROXY_MODELS in
|
||||
// sasquatchBrain.ts.
|
||||
const DEFAULT_MODELS = [
|
||||
'meta-llama/llama-3.3-70b-instruct',
|
||||
'deepseek/deepseek-chat-v3-0324',
|
||||
'x-ai/grok-4.5',
|
||||
|
|
@ -38,7 +43,13 @@ const ALLOWED_MODELS = new Set([
|
|||
'qwen/qwen-2.5-72b-instruct',
|
||||
'mistralai/mistral-nemo',
|
||||
'meta-llama/llama-3.1-8b-instruct',
|
||||
]);
|
||||
];
|
||||
const ALLOWED_MODELS = new Set(
|
||||
(process.env.SASQUATCH_MODELS || DEFAULT_MODELS.join(','))
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean),
|
||||
);
|
||||
|
||||
// ── Sasquatch Pro enforcement (env-gated) ────────────────────────────────────
|
||||
const REQUIRE_PRO = process.env.SASQUATCH_REQUIRE_PRO === '1';
|
||||
|
|
@ -205,7 +216,7 @@ export default async function handler(request) {
|
|||
}
|
||||
}
|
||||
|
||||
const key = process.env.OPENROUTER_API_KEY;
|
||||
const key = AI_KEY;
|
||||
if (!key) return json({ error: 'AI proxy not configured' }, 503, headers);
|
||||
|
||||
// Throttle per client IP (Netlify sets x-nf-client-connection-ip).
|
||||
|
|
@ -235,7 +246,7 @@ export default async function handler(request) {
|
|||
|
||||
let upstream;
|
||||
try {
|
||||
upstream = await fetch(OPENROUTER_URL, {
|
||||
upstream = await fetch(AI_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${key}`,
|
||||
|
|
@ -250,7 +261,7 @@ export default async function handler(request) {
|
|||
return json({ error: `Upstream failed: ${String(err)}` }, 502, headers);
|
||||
}
|
||||
|
||||
// Pass OpenRouter's response straight through (JSON or SSE).
|
||||
// Pass the upstream response straight through (JSON or SSE).
|
||||
const passthrough = { ...headers };
|
||||
const contentType = upstream.headers.get('content-type');
|
||||
if (contentType) passthrough['Content-Type'] = contentType;
|
||||
|
|
|
|||
Loading…
Reference in a new issue