pool-anything 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +280 -0
- package/bin/pool-anything.js +4 -0
- package/data/providers.json +38 -0
- package/dist/admin/branding.js +34 -0
- package/dist/admin/components.js +189 -0
- package/dist/admin/pages.js +1891 -0
- package/dist/admin/shell.js +129 -0
- package/dist/admin/tokens.js +32 -0
- package/dist/cli/nl.js +147 -0
- package/dist/cli/repl.js +470 -0
- package/dist/cli/ui.js +81 -0
- package/dist/cli.js +199 -0
- package/dist/common/http.js +55 -0
- package/dist/config/env.js +94 -0
- package/dist/config/paths.js +112 -0
- package/dist/data/providers.json +38 -0
- package/dist/db/index.js +20 -0
- package/dist/db/init.js +11 -0
- package/dist/logo.png +0 -0
- package/dist/media/capabilities.js +103 -0
- package/dist/middleware/auth.js +37 -0
- package/dist/observability/health.js +31 -0
- package/dist/pool/index.js +407 -0
- package/dist/pool/rotation.js +123 -0
- package/dist/proxy/dns.js +39 -0
- package/dist/proxy/forward.js +350 -0
- package/dist/public/logos/anthropic.svg +1 -0
- package/dist/public/logos/anyscale.svg +1 -0
- package/dist/public/logos/appwrite.svg +1 -0
- package/dist/public/logos/brave.svg +1 -0
- package/dist/public/logos/cartesia.svg +4 -0
- package/dist/public/logos/claude.svg +1 -0
- package/dist/public/logos/cloudflare.svg +1 -0
- package/dist/public/logos/cohere-command.svg +1 -0
- package/dist/public/logos/cohere.svg +1 -0
- package/dist/public/logos/deepgram.svg +1 -0
- package/dist/public/logos/elevenlabs.svg +1 -0
- package/dist/public/logos/finnhub.png +0 -0
- package/dist/public/logos/fireworks.svg +1 -0
- package/dist/public/logos/gemini.svg +1 -0
- package/dist/public/logos/google.svg +1 -0
- package/dist/public/logos/googlemaps.svg +1 -0
- package/dist/public/logos/groq.svg +1 -0
- package/dist/public/logos/huggingface.svg +1 -0
- package/dist/public/logos/mailgun.svg +1 -0
- package/dist/public/logos/manifest.json +197 -0
- package/dist/public/logos/mapbox.svg +1 -0
- package/dist/public/logos/mistral.svg +1 -0
- package/dist/public/logos/neon.svg +1 -0
- package/dist/public/logos/openai.svg +1 -0
- package/dist/public/logos/openrouter.svg +1 -0
- package/dist/public/logos/openweather.png +0 -0
- package/dist/public/logos/perplexity.svg +1 -0
- package/dist/public/logos/pool-anything.svg +111 -0
- package/dist/public/logos/postmark.svg +4 -0
- package/dist/public/logos/replicate.svg +1 -0
- package/dist/public/logos/resend.svg +1 -0
- package/dist/public/logos/scrapingbee.svg +72 -0
- package/dist/public/logos/sendgrid.svg +1 -0
- package/dist/public/logos/serper.png +0 -0
- package/dist/public/logos/supabase.svg +1 -0
- package/dist/public/logos/tavily.svg +1 -0
- package/dist/public/logos/together.svg +1 -0
- package/dist/public/logos/turso.svg +1 -0
- package/dist/public/logos/twilio.svg +1 -0
- package/dist/public/logos/upstash.svg +1 -0
- package/dist/router/api.js +20 -0
- package/dist/server.js +431 -0
- package/dist/upstream/index.js +137 -0
- package/package.json +68 -0
- package/public/logos/anthropic.svg +1 -0
- package/public/logos/anyscale.svg +1 -0
- package/public/logos/appwrite.svg +1 -0
- package/public/logos/brave.svg +1 -0
- package/public/logos/cartesia.svg +4 -0
- package/public/logos/claude.svg +1 -0
- package/public/logos/cloudflare.svg +1 -0
- package/public/logos/cohere-command.svg +1 -0
- package/public/logos/cohere.svg +1 -0
- package/public/logos/deepgram.svg +1 -0
- package/public/logos/elevenlabs.svg +1 -0
- package/public/logos/finnhub.png +0 -0
- package/public/logos/fireworks.svg +1 -0
- package/public/logos/gemini.svg +1 -0
- package/public/logos/google.svg +1 -0
- package/public/logos/googlemaps.svg +1 -0
- package/public/logos/groq.svg +1 -0
- package/public/logos/huggingface.svg +1 -0
- package/public/logos/mailgun.svg +1 -0
- package/public/logos/manifest.json +197 -0
- package/public/logos/mapbox.svg +1 -0
- package/public/logos/mistral.svg +1 -0
- package/public/logos/neon.svg +1 -0
- package/public/logos/openai.svg +1 -0
- package/public/logos/openrouter.svg +1 -0
- package/public/logos/openweather.png +0 -0
- package/public/logos/perplexity.svg +1 -0
- package/public/logos/pool-anything.svg +111 -0
- package/public/logos/postmark.svg +4 -0
- package/public/logos/replicate.svg +1 -0
- package/public/logos/resend.svg +1 -0
- package/public/logos/scrapingbee.svg +72 -0
- package/public/logos/sendgrid.svg +1 -0
- package/public/logos/serper.png +0 -0
- package/public/logos/supabase.svg +1 -0
- package/public/logos/tavily.svg +1 -0
- package/public/logos/together.svg +1 -0
- package/public/logos/turso.svg +1 -0
- package/public/logos/twilio.svg +1 -0
- package/public/logos/upstash.svg +1 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { sdb, getPool, mask, maskCredentials, parseCredentials, quotaForProvider, quotaLimitsForProvider, quotaWindowFilter } from "./index.js";
|
|
2
|
+
const MAX_TOKENS = 1_000_000;
|
|
3
|
+
/** Per-key windowed total for one quota limit. */
|
|
4
|
+
function keyWindowedTotal(keyId, window) {
|
|
5
|
+
return sdb.prepare(`SELECT COALESCE(SUM(tokens),0) AS t FROM usage WHERE key_id = ?${quotaWindowFilter(window, "created_at")}`).get(keyId).t;
|
|
6
|
+
}
|
|
7
|
+
/** Eligible keys: not cooling down, under every quota limit. Ordered by latency (unknown first), id. */
|
|
8
|
+
export function eligibleKeys(poolId) {
|
|
9
|
+
const pool = getPool(poolId);
|
|
10
|
+
if (!pool)
|
|
11
|
+
return [];
|
|
12
|
+
const now = Date.now();
|
|
13
|
+
const { limit: quota, window } = quotaForProvider(pool.provider);
|
|
14
|
+
const rows = sdb
|
|
15
|
+
.prepare(`SELECT k.id AS key_id, k.label, k.api_key, COALESCE(k.credentials,'{}') AS credentials, k.info, COALESCE(SUM(u.tokens),0) AS used
|
|
16
|
+
FROM pool_keys k LEFT JOIN usage u ON u.key_id = k.id${quotaWindowFilter(window)}
|
|
17
|
+
WHERE k.pool_id = ? AND k.cooldown_until <= ?
|
|
18
|
+
GROUP BY k.id ORDER BY k.latency_ms ASC, k.id ASC`)
|
|
19
|
+
.all(poolId, now);
|
|
20
|
+
const limits = quotaLimitsForProvider(pool.provider);
|
|
21
|
+
return rows
|
|
22
|
+
.filter((k) => quota === null || k.used < quota)
|
|
23
|
+
.filter((k) => {
|
|
24
|
+
// Primary-window check above covers the primary limit; enforce the rest (daily).
|
|
25
|
+
for (const { limit, window: w } of limits) {
|
|
26
|
+
if (w === window)
|
|
27
|
+
continue;
|
|
28
|
+
if (keyWindowedTotal(k.key_id, w) >= limit)
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
return true;
|
|
32
|
+
})
|
|
33
|
+
.map(({ latency_ms: _l, ...k }) => {
|
|
34
|
+
const creds = parseCredentials(k.credentials);
|
|
35
|
+
return {
|
|
36
|
+
...k,
|
|
37
|
+
masked: mask(k.api_key),
|
|
38
|
+
maskedCredentials: Object.keys(creds).length > 0 ? maskCredentials(creds) : undefined,
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
function advance(poolId) {
|
|
43
|
+
sdb.prepare("UPDATE pools SET cursor = cursor + 1 WHERE id = ?").run(poolId);
|
|
44
|
+
}
|
|
45
|
+
/** Round-robin over eligible keys. Raw key stays server-side; use nextKey for the masked form. */
|
|
46
|
+
export function nextKeyRaw(poolId) {
|
|
47
|
+
const pool = getPool(poolId);
|
|
48
|
+
if (!pool)
|
|
49
|
+
return { error: "pool not found" };
|
|
50
|
+
const keys = eligibleKeys(poolId);
|
|
51
|
+
if (keys.length === 0) {
|
|
52
|
+
const total = sdb.prepare("SELECT COUNT(*) AS n FROM pool_keys WHERE pool_id = ?").get(poolId);
|
|
53
|
+
if (total.n === 0)
|
|
54
|
+
return { error: "pool has no keys" };
|
|
55
|
+
return { error: "all keys cooling down or quota-exhausted" };
|
|
56
|
+
}
|
|
57
|
+
const cursor = Number.isSafeInteger(pool.cursor) && pool.cursor >= 0 ? pool.cursor : 0;
|
|
58
|
+
const pick = keys[((cursor % keys.length) + keys.length) % keys.length];
|
|
59
|
+
advance(poolId);
|
|
60
|
+
return pick;
|
|
61
|
+
}
|
|
62
|
+
export function nextKey(poolId) {
|
|
63
|
+
const sel = nextKeyRaw(poolId);
|
|
64
|
+
if ("error" in sel)
|
|
65
|
+
return sel;
|
|
66
|
+
const { api_key: _raw, credentials: _creds, ...masked } = sel;
|
|
67
|
+
return masked;
|
|
68
|
+
}
|
|
69
|
+
function validTokens(tokens) {
|
|
70
|
+
return Number.isInteger(tokens) && tokens > 0 && tokens <= MAX_TOKENS;
|
|
71
|
+
}
|
|
72
|
+
export function recordUsage(poolId, tokens, keyId) {
|
|
73
|
+
if (!validTokens(tokens))
|
|
74
|
+
return { error: "tokens must be a positive integer <= 1000000" };
|
|
75
|
+
const pool = getPool(poolId);
|
|
76
|
+
if (!pool)
|
|
77
|
+
return { error: "pool not found" };
|
|
78
|
+
const limits = quotaLimitsForProvider(pool.provider);
|
|
79
|
+
if (keyId !== undefined) {
|
|
80
|
+
// Attribute to a specific key without advancing the rotation cursor.
|
|
81
|
+
// Still enforces eligibility: no writing to cooling or quota-exhausted keys.
|
|
82
|
+
const row = sdb
|
|
83
|
+
.prepare("SELECT id, label, api_key, cooldown_until FROM pool_keys WHERE id = ? AND pool_id = ?")
|
|
84
|
+
.get(keyId, poolId);
|
|
85
|
+
if (!row)
|
|
86
|
+
return { error: "key not found" };
|
|
87
|
+
if (row.cooldown_until > Date.now())
|
|
88
|
+
return { error: "key is cooling down" };
|
|
89
|
+
for (const { limit, window } of limits) {
|
|
90
|
+
const used = keyWindowedTotal(keyId, window);
|
|
91
|
+
if (used >= limit)
|
|
92
|
+
return { error: "key quota-exhausted" };
|
|
93
|
+
if (used + tokens > limit)
|
|
94
|
+
return { error: "tokens would exceed key quota" };
|
|
95
|
+
}
|
|
96
|
+
sdb.prepare("INSERT INTO usage (pool_id, key_id, tokens) VALUES (?, ?, ?)").run(poolId, keyId, tokens);
|
|
97
|
+
return { key_id: row.id, label: row.label, masked: mask(row.api_key), tokens };
|
|
98
|
+
}
|
|
99
|
+
const sel = nextKey(poolId);
|
|
100
|
+
if ("error" in sel)
|
|
101
|
+
return sel;
|
|
102
|
+
for (const { limit, window } of limits) {
|
|
103
|
+
if (keyWindowedTotal(sel.key_id, window) + tokens > limit)
|
|
104
|
+
return { error: "tokens would exceed key quota" };
|
|
105
|
+
}
|
|
106
|
+
sdb.prepare("INSERT INTO usage (pool_id, key_id, tokens) VALUES (?, ?, ?)").run(poolId, sel.key_id, tokens);
|
|
107
|
+
return { ...sel, tokens };
|
|
108
|
+
}
|
|
109
|
+
export function markSuccess(keyId, ms) {
|
|
110
|
+
if (!Number.isFinite(ms) || ms < 0)
|
|
111
|
+
return;
|
|
112
|
+
sdb.prepare("UPDATE pool_keys SET latency_ms = ?, consec_fail = 0, cooldown_until = 0 WHERE id = ?").run(ms, keyId);
|
|
113
|
+
}
|
|
114
|
+
export function markFailure(keyId) {
|
|
115
|
+
// Atomic increment: avoids lost updates on concurrent failover paths.
|
|
116
|
+
sdb.prepare("UPDATE pool_keys SET consec_fail = consec_fail + 1 WHERE id = ?").run(keyId);
|
|
117
|
+
const row = sdb.prepare("SELECT consec_fail AS f FROM pool_keys WHERE id = ?").get(keyId);
|
|
118
|
+
if (!row)
|
|
119
|
+
return;
|
|
120
|
+
const fails = row.f;
|
|
121
|
+
const backoff = Math.min(30 * 2 ** (Math.min(fails, 10) - 1), 600) * 1000;
|
|
122
|
+
sdb.prepare("UPDATE pool_keys SET cooldown_until = ? WHERE id = ?").run(Date.now() + backoff, keyId);
|
|
123
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { lookup } from "node:dns/promises";
|
|
2
|
+
import { isPrivateLiteral } from "./forward.js";
|
|
3
|
+
/** Resolve a hostname and reject if any address is loopback/private. */
|
|
4
|
+
export async function resolveAndCheck(hostname) {
|
|
5
|
+
let h = hostname.toLowerCase();
|
|
6
|
+
if (h.startsWith("[") && h.endsWith("]"))
|
|
7
|
+
h = h.slice(1, -1);
|
|
8
|
+
if (h.endsWith(".") && h.length > 1)
|
|
9
|
+
h = h.slice(0, -1);
|
|
10
|
+
// Fast path: literal check, no DNS needed.
|
|
11
|
+
if (isPrivateLiteral(h))
|
|
12
|
+
return { ok: false, error: "upstream host blocked" };
|
|
13
|
+
// Hostnames without dots that are not localhost (e.g. intranet names) are
|
|
14
|
+
// overwhelmingly internal: fail closed without a DNS round-trip.
|
|
15
|
+
if (!h.includes(".") && !h.includes(":"))
|
|
16
|
+
return { ok: false, error: "upstream host blocked" };
|
|
17
|
+
try {
|
|
18
|
+
const addrs = await lookup(h, { all: true });
|
|
19
|
+
const ips = addrs.map((a) => a.address);
|
|
20
|
+
if (ips.length === 0)
|
|
21
|
+
return { ok: false, error: "upstream host did not resolve" };
|
|
22
|
+
for (const ip of ips) {
|
|
23
|
+
if (isPrivateLiteral(ip))
|
|
24
|
+
return { ok: false, error: "upstream host resolves to a private address" };
|
|
25
|
+
}
|
|
26
|
+
return { ok: true, ips };
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return { ok: false, error: "upstream host did not resolve" };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function hostnameOf(baseUrl) {
|
|
33
|
+
try {
|
|
34
|
+
return new URL(baseUrl).hostname;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import { sdb, getPool } from "../pool/index.js";
|
|
2
|
+
import { nextKeyRaw, markSuccess, markFailure } from "../pool/rotation.js";
|
|
3
|
+
import { resolveTarget, resolveTargetForKey, effectiveApiKey, buildUpstreamRequest, validateQueryParams } from "../upstream/index.js";
|
|
4
|
+
import { allowPrivateUpstream, proxyTimeoutMs } from "../config/env.js";
|
|
5
|
+
import { resolveAndCheck, hostnameOf } from "./dns.js";
|
|
6
|
+
import { MAX_PROXY_BODY_BYTES, MAX_RELAY_CHARS, MAX_UPSTREAM_MEDIA_BYTES } from "../media/capabilities.js";
|
|
7
|
+
/** Hard per-request abuse cap (see disk-fill note in SECURITY.md). */
|
|
8
|
+
export const MAX_TOKENS_PER_REQUEST = 1_000_000;
|
|
9
|
+
/** Cap on buffered upstream bytes before truncating (media-aware; JSON text is relayed up to MAX_RELAY_CHARS). */
|
|
10
|
+
export const MAX_UPSTREAM_BYTES = MAX_UPSTREAM_MEDIA_BYTES;
|
|
11
|
+
const METHODS = new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
|
|
12
|
+
/** Parse dotted IPv4 with inet_aton semantics (decimal, 0x hex, 0 octal, 1-4 parts). */
|
|
13
|
+
function parseIPv4(host) {
|
|
14
|
+
const parts = host.split(".");
|
|
15
|
+
if (parts.length < 1 || parts.length > 4)
|
|
16
|
+
return null;
|
|
17
|
+
const nums = [];
|
|
18
|
+
for (const p of parts) {
|
|
19
|
+
if (p.length === 0)
|
|
20
|
+
return null;
|
|
21
|
+
let v;
|
|
22
|
+
if (/^0[xX][0-9a-fA-F]+$/.test(p))
|
|
23
|
+
v = parseInt(p, 16);
|
|
24
|
+
else if (/^0[0-9]+$/.test(p)) {
|
|
25
|
+
if (!/^[0-7]+$/.test(p))
|
|
26
|
+
return null;
|
|
27
|
+
v = parseInt(p, 8);
|
|
28
|
+
}
|
|
29
|
+
else if (/^[0-9]+$/.test(p))
|
|
30
|
+
v = parseInt(p, 10);
|
|
31
|
+
else
|
|
32
|
+
return null;
|
|
33
|
+
if (!Number.isSafeInteger(v) || v < 0 || v > 0xffffffff)
|
|
34
|
+
return null;
|
|
35
|
+
nums.push(v);
|
|
36
|
+
}
|
|
37
|
+
const widths = { 1: [32], 2: [8, 24], 3: [8, 8, 16], 4: [8, 8, 8, 8] }[nums.length];
|
|
38
|
+
let addr = 0;
|
|
39
|
+
for (let i = 0; i < nums.length; i++) {
|
|
40
|
+
if (nums[i] >= 2 ** widths[i])
|
|
41
|
+
return null;
|
|
42
|
+
addr = addr * 2 ** widths[i] + nums[i];
|
|
43
|
+
}
|
|
44
|
+
return addr >>> 0;
|
|
45
|
+
}
|
|
46
|
+
/** True when a hostname is a loopback/private/unspecified literal (v4, v6, or mapped). */
|
|
47
|
+
export function isPrivateLiteral(host) {
|
|
48
|
+
let h = host.toLowerCase();
|
|
49
|
+
if (h.startsWith("[") && h.endsWith("]"))
|
|
50
|
+
h = h.slice(1, -1); // URL keeps IPv6 brackets
|
|
51
|
+
// Strip a single trailing dot (FQDN form): "127.0.0.1." still resolves to loopback.
|
|
52
|
+
if (h.endsWith(".") && h.length > 1)
|
|
53
|
+
h = h.slice(0, -1);
|
|
54
|
+
if (h === "localhost" || h.endsWith(".localhost") || h === "::1" || h === "::" || h === "0.0.0.0")
|
|
55
|
+
return true;
|
|
56
|
+
if (h === "0:0:0:0:0:0:0:1" || h === "0:0:0:0:0:0:0:0")
|
|
57
|
+
return true;
|
|
58
|
+
if (h.includes(":")) {
|
|
59
|
+
// IPv6: check ::ffff:<v4> mapped forms (compressed or full).
|
|
60
|
+
const afterLastColon = h.slice(h.lastIndexOf(":") + 1);
|
|
61
|
+
const tail = afterLastColon.includes(".") ? parseIPv4(afterLastColon) : null;
|
|
62
|
+
if (tail !== null)
|
|
63
|
+
return isPrivateV4(tail);
|
|
64
|
+
// Block non-routable IPv6: fe80::/10 link-local, fc00::/7 ULA, ff00::/8 multicast.
|
|
65
|
+
// Public global unicast (e.g. 2606:4700::) is allowed.
|
|
66
|
+
const first = h.split(":")[0] || "";
|
|
67
|
+
if (/^fe[89ab]/i.test(first))
|
|
68
|
+
return true;
|
|
69
|
+
if (/^(fc|fd)/i.test(first))
|
|
70
|
+
return true;
|
|
71
|
+
if (/^ff/i.test(first))
|
|
72
|
+
return true;
|
|
73
|
+
return false;
|
|
74
|
+
// DNS names resolving to private IPs remain a residual risk (no DNS pinning).
|
|
75
|
+
// See residual-risk note in SECURITY.md.
|
|
76
|
+
}
|
|
77
|
+
const v4 = parseIPv4(h);
|
|
78
|
+
if (v4 === null)
|
|
79
|
+
return false;
|
|
80
|
+
return isPrivateV4(v4);
|
|
81
|
+
}
|
|
82
|
+
function isPrivateV4(addr) {
|
|
83
|
+
const b0 = (addr >>> 24) & 0xff;
|
|
84
|
+
const b1 = (addr >>> 16) & 0xff;
|
|
85
|
+
if (b0 === 127 || b0 === 10 || b0 === 0)
|
|
86
|
+
return true; // loopback, private, "this network"
|
|
87
|
+
if (b0 === 172 && b1 >= 16 && b1 <= 31)
|
|
88
|
+
return true;
|
|
89
|
+
if (b0 === 192 && b1 === 168)
|
|
90
|
+
return true;
|
|
91
|
+
if (b0 === 169 && b1 === 254)
|
|
92
|
+
return true; // link-local incl. cloud metadata
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
export const QUERY_PARAM_RE = /^[A-Za-z0-9_-]+$/;
|
|
96
|
+
export function checkTarget(baseUrl, keyHeader) {
|
|
97
|
+
let u;
|
|
98
|
+
try {
|
|
99
|
+
u = new URL(baseUrl);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return { ok: false, error: "baseUrl is not a URL" };
|
|
103
|
+
}
|
|
104
|
+
if (!allowPrivateUpstream()) {
|
|
105
|
+
if (u.protocol !== "https:")
|
|
106
|
+
return { ok: false, error: "baseUrl must be https" };
|
|
107
|
+
if (isPrivateLiteral(u.hostname))
|
|
108
|
+
return { ok: false, error: "baseUrl host blocked" };
|
|
109
|
+
}
|
|
110
|
+
if (/^[A-Za-z0-9-]+$/.test(keyHeader))
|
|
111
|
+
return { ok: true };
|
|
112
|
+
if (keyHeader.startsWith("query:")) {
|
|
113
|
+
const name = keyHeader.slice("query:".length);
|
|
114
|
+
if (QUERY_PARAM_RE.test(name))
|
|
115
|
+
return { ok: true };
|
|
116
|
+
return { ok: false, error: "keyHeader invalid" };
|
|
117
|
+
}
|
|
118
|
+
return { ok: false, error: "keyHeader invalid" };
|
|
119
|
+
}
|
|
120
|
+
/** Validate caller-supplied proxy fields before any key is touched. No key state changes on failure. */
|
|
121
|
+
export function validateProxyOpts(opts) {
|
|
122
|
+
const p = opts.path ?? "/";
|
|
123
|
+
if (typeof p !== "string" || !p.startsWith("/") || p.length > 2000 || /[\s\x00-\x1f\x7f]/.test(p))
|
|
124
|
+
return { ok: false, error: "path must start with / and contain no whitespace/control characters" };
|
|
125
|
+
// Reject query/fragment markers and path traversal: the proxy concatenates
|
|
126
|
+
// baseUrl + path, so "?"/"#" would inject query/fragment and ".." escapes prefixes.
|
|
127
|
+
if (p.includes("?") || p.includes("#"))
|
|
128
|
+
return { ok: false, error: "path must not contain ? or #" };
|
|
129
|
+
if (p.split("/").includes(".."))
|
|
130
|
+
return { ok: false, error: "path must not contain .." };
|
|
131
|
+
try {
|
|
132
|
+
decodeURIComponent(p);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return { ok: false, error: "path has invalid percent-encoding" };
|
|
136
|
+
}
|
|
137
|
+
if (opts.method !== undefined && (typeof opts.method !== "string" || !METHODS.has(opts.method.toUpperCase())))
|
|
138
|
+
return { ok: false, error: "method must be one of GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS" };
|
|
139
|
+
if (opts.headers !== undefined) {
|
|
140
|
+
if (typeof opts.headers !== "object" || opts.headers === null || Array.isArray(opts.headers))
|
|
141
|
+
return { ok: false, error: "headers must be an object" };
|
|
142
|
+
const entries = Object.entries(opts.headers);
|
|
143
|
+
if (entries.length > 50)
|
|
144
|
+
return { ok: false, error: "too many headers" };
|
|
145
|
+
for (const [k, v] of entries) {
|
|
146
|
+
// Names must be HTTP tokens: otherwise fetch throws and an innocent key cools down.
|
|
147
|
+
// Intentionally fail-closed: exotic non-token header names are rejected rather than
|
|
148
|
+
// risking a pool-wide cooldown on a network error.
|
|
149
|
+
if (typeof k !== "string" || k.length === 0 || k.length > 256 || !/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(k))
|
|
150
|
+
return { ok: false, error: "invalid header name" };
|
|
151
|
+
if (typeof v !== "string" || v.length > 8000 || /[\r\n\x00]/.test(k) || /[\r\n\x00]/.test(v))
|
|
152
|
+
return { ok: false, error: "invalid header value" };
|
|
153
|
+
const lk = k.toLowerCase();
|
|
154
|
+
// Hop-by-hop / routing headers must not be caller-controlled: fetch would
|
|
155
|
+
// forward them upstream alongside the victim key (vhost confusion, cookie leak).
|
|
156
|
+
if (lk === "host" || lk === "connection" || lk === "transfer-encoding" ||
|
|
157
|
+
lk === "content-length" || lk === "cookie" || lk === "proxy-authenticate" ||
|
|
158
|
+
lk === "proxy-authorization" || lk === "upgrade" || lk === "via" ||
|
|
159
|
+
lk === "forwarded" || lk === "x-forwarded-for" || lk === "x-forwarded-host" ||
|
|
160
|
+
lk === "x-forwarded-proto" || lk === "expect" || lk === "trailer" || lk === "te")
|
|
161
|
+
return { ok: false, error: `header not allowed: ${k}` };
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (opts.tokens !== undefined && (!Number.isInteger(opts.tokens) || opts.tokens <= 0 || opts.tokens > MAX_TOKENS_PER_REQUEST))
|
|
165
|
+
return { ok: false, error: `tokens must be a positive integer <= ${MAX_TOKENS_PER_REQUEST}` };
|
|
166
|
+
if (opts.query !== undefined) {
|
|
167
|
+
const q = validateQueryParams(opts.query);
|
|
168
|
+
if (!q.ok)
|
|
169
|
+
return { ok: false, error: q.error };
|
|
170
|
+
}
|
|
171
|
+
if (opts.body !== undefined) {
|
|
172
|
+
try {
|
|
173
|
+
const s = JSON.stringify(opts.body);
|
|
174
|
+
// Byte-accurate: JSON length undercounts non-ASCII, and readJson enforces bytes.
|
|
175
|
+
if (Buffer.byteLength(s, "utf8") > MAX_PROXY_BODY_BYTES)
|
|
176
|
+
return { ok: false, error: `proxy body too large (max ${Math.round(MAX_PROXY_BODY_BYTES / 1_000_000)}MB incl. base64 media)` };
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return { ok: false, error: "body must be JSON-serializable" };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return { ok: true };
|
|
183
|
+
}
|
|
184
|
+
/** Read at most `cap` bytes. Textual (json/text) bodies decode as text; binary media (audio/video/image bytes) base64-encodes so Studio can render it.
|
|
185
|
+
* Memory note: media caps are intentionally large (12MB upstream, 8M-char
|
|
186
|
+
* relay) so Studio can render audio/image bytes. The body is buffered once
|
|
187
|
+
* via Buffer.concat and relayed sliced to MAX_RELAY_CHARS — one large media
|
|
188
|
+
* proxy holds ~2x peak (raw + base64) transiently; concurrent large media
|
|
189
|
+
* proxies multiply that, so keep caps in capabilities.ts conservative. */
|
|
190
|
+
async function readCappedBody(res, cap) {
|
|
191
|
+
const contentType = res.headers.get("content-type") || "";
|
|
192
|
+
const textual = /json|text|event-stream|xml|urlencoded/i.test(contentType) || !contentType;
|
|
193
|
+
if (!res.body || typeof res.body.getReader !== "function") {
|
|
194
|
+
const buf = await res.arrayBuffer();
|
|
195
|
+
const truncated = buf.byteLength > cap;
|
|
196
|
+
const slice = truncated ? buf.slice(0, cap) : buf;
|
|
197
|
+
if (textual)
|
|
198
|
+
return { text: new TextDecoder().decode(slice), contentType, truncated };
|
|
199
|
+
return { b64: Buffer.from(slice).toString("base64"), contentType, truncated };
|
|
200
|
+
}
|
|
201
|
+
const reader = res.body.getReader();
|
|
202
|
+
const chunks = [];
|
|
203
|
+
let size = 0;
|
|
204
|
+
let truncated = false;
|
|
205
|
+
for (;;) {
|
|
206
|
+
const { done, value } = await reader.read();
|
|
207
|
+
if (done)
|
|
208
|
+
break;
|
|
209
|
+
size += value.byteLength;
|
|
210
|
+
if (size > cap) {
|
|
211
|
+
await reader.cancel().catch(() => undefined);
|
|
212
|
+
truncated = true;
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
chunks.push(value);
|
|
216
|
+
}
|
|
217
|
+
// Single concat (no intermediate flat copy retained): chunks are released here.
|
|
218
|
+
const buf = Buffer.concat(chunks.map((c) => Buffer.from(c.buffer, c.byteOffset, c.byteLength)));
|
|
219
|
+
const bounded = truncated ? buf.subarray(0, cap) : buf;
|
|
220
|
+
if (textual)
|
|
221
|
+
return { text: new TextDecoder().decode(bounded), contentType, truncated };
|
|
222
|
+
return { b64: bounded.toString("base64"), contentType, truncated };
|
|
223
|
+
}
|
|
224
|
+
export function timeoutMs() {
|
|
225
|
+
return proxyTimeoutMs();
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Forward one request through the pool: rotate, send, fail over on
|
|
229
|
+
* 429/5xx/network errors (key cools down, next key tried). Returns the
|
|
230
|
+
* upstream status/body plus which key served it. Quota-exhausted and
|
|
231
|
+
* cooling keys are skipped by rotation.
|
|
232
|
+
*/
|
|
233
|
+
export async function forward(poolId, opts) {
|
|
234
|
+
const pool = getPool(poolId);
|
|
235
|
+
if (!pool)
|
|
236
|
+
return { status: 404, error: "pool not found" };
|
|
237
|
+
const poolTarget = resolveTarget(pool);
|
|
238
|
+
const safe = checkTarget(poolTarget.baseUrl || "invalid:", poolTarget.keyHeader);
|
|
239
|
+
if (!safe.ok)
|
|
240
|
+
return { status: 400, ...safe };
|
|
241
|
+
if (!allowPrivateUpstream()) {
|
|
242
|
+
const host = hostnameOf(poolTarget.baseUrl);
|
|
243
|
+
if (host) {
|
|
244
|
+
const dns = await resolveAndCheck(host);
|
|
245
|
+
if (!dns.ok)
|
|
246
|
+
return { status: 400, error: dns.error, tried: [] };
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
const valid = validateProxyOpts(opts);
|
|
250
|
+
if (!valid.ok)
|
|
251
|
+
return { status: 400, error: valid.error };
|
|
252
|
+
// Bound failover: one client request must not fan out to every key in a
|
|
253
|
+
// large pool (100 keys x timeout each would starve the single-thread server).
|
|
254
|
+
const keyTotal = sdb.prepare("SELECT COUNT(*) AS n FROM pool_keys WHERE pool_id = ?").get(poolId).n;
|
|
255
|
+
const maxAttempts = Math.min(Math.max(keyTotal, 1), 5);
|
|
256
|
+
const tried = [];
|
|
257
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
258
|
+
const sel = nextKeyRaw(poolId);
|
|
259
|
+
if ("error" in sel) {
|
|
260
|
+
return { status: 400, error: sel.error, tried };
|
|
261
|
+
}
|
|
262
|
+
let req;
|
|
263
|
+
try {
|
|
264
|
+
const keyTarget = resolveTargetForKey(pool, sel);
|
|
265
|
+
const keySafe = checkTarget(keyTarget.baseUrl || "invalid:", keyTarget.keyHeader);
|
|
266
|
+
if (!keySafe.ok) {
|
|
267
|
+
tried.push({ key_id: sel.key_id, label: sel.label, detail: keySafe.error });
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
if (!allowPrivateUpstream()) {
|
|
271
|
+
const khost = hostnameOf(keyTarget.baseUrl);
|
|
272
|
+
if (khost) {
|
|
273
|
+
const kdns = await resolveAndCheck(khost);
|
|
274
|
+
if (!kdns.ok) {
|
|
275
|
+
tried.push({ key_id: sel.key_id, label: sel.label, detail: kdns.error });
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
req = buildUpstreamRequest(keyTarget, effectiveApiKey(pool, sel), opts);
|
|
281
|
+
// Fail closed on malformed URL/headers instead of cooling an innocent key.
|
|
282
|
+
new Request(req.url, { method: opts.method || "POST", headers: req.headers });
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
return { status: 400, error: "invalid upstream request", tried };
|
|
286
|
+
}
|
|
287
|
+
const { url, headers, body } = req;
|
|
288
|
+
const started = Date.now();
|
|
289
|
+
let upstream;
|
|
290
|
+
try {
|
|
291
|
+
// redirect: manual — never follow a Location off the validated baseUrl (SSRF).
|
|
292
|
+
upstream = await fetch(url, { method: opts.method || "POST", headers, body, redirect: "manual", signal: AbortSignal.timeout(timeoutMs()) });
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
markFailure(sel.key_id);
|
|
296
|
+
tried.push({ key_id: sel.key_id, label: sel.label, detail: "upstream network error" });
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
if (upstream.status >= 300 && upstream.status < 400) {
|
|
300
|
+
// Redirects are not followed and are not the key's fault: report without cooling.
|
|
301
|
+
await upstream.body?.cancel().catch(() => undefined);
|
|
302
|
+
tried.push({ key_id: sel.key_id, label: sel.label, status: upstream.status });
|
|
303
|
+
return { status: 400, error: `upstream redirected (HTTP ${upstream.status}, location not followed)`, tried };
|
|
304
|
+
}
|
|
305
|
+
let text = "";
|
|
306
|
+
let b64;
|
|
307
|
+
let contentType = "";
|
|
308
|
+
let truncated = false;
|
|
309
|
+
try {
|
|
310
|
+
const r = await readCappedBody(upstream, MAX_UPSTREAM_BYTES);
|
|
311
|
+
text = r.text ?? "";
|
|
312
|
+
b64 = r.b64;
|
|
313
|
+
contentType = r.contentType;
|
|
314
|
+
truncated = r.truncated;
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
markFailure(sel.key_id);
|
|
318
|
+
tried.push({ key_id: sel.key_id, label: sel.label, status: upstream.status, detail: "upstream body error" });
|
|
319
|
+
await upstream.body?.cancel().catch(() => undefined);
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
// Binary media (audio/video/image bytes) relays as base64 + content type so
|
|
323
|
+
// Studio can render a player; JSON/text relays as text up to MAX_RELAY_CHARS.
|
|
324
|
+
const rawLen = b64 !== undefined ? b64.length : text.length;
|
|
325
|
+
const relayBody = b64 !== undefined ? b64.slice(0, MAX_RELAY_CHARS) : text.slice(0, MAX_RELAY_CHARS);
|
|
326
|
+
truncated = truncated || rawLen > MAX_RELAY_CHARS;
|
|
327
|
+
const encoding = b64 !== undefined ? "base64" : "text";
|
|
328
|
+
if (upstream.status === 429 || upstream.status >= 500 || upstream.status === 401 || upstream.status === 403) {
|
|
329
|
+
// 401/403 = invalid/revoked key: cool it so rotation skips it instead of
|
|
330
|
+
// paying one dead upstream call on every subsequent request.
|
|
331
|
+
markFailure(sel.key_id);
|
|
332
|
+
tried.push({ key_id: sel.key_id, label: sel.label, status: upstream.status });
|
|
333
|
+
await upstream.body?.cancel().catch(() => undefined);
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
if (upstream.status < 200 || upstream.status >= 300) {
|
|
337
|
+
// Other 4xx: client/upstream error, not the key's fault. Passthrough
|
|
338
|
+
// without cooling and without charging quota.
|
|
339
|
+
tried.push({ key_id: sel.key_id, label: sel.label, status: upstream.status });
|
|
340
|
+
return { status: 200, key_id: sel.key_id, label: sel.label, masked: sel.masked, upstreamStatus: upstream.status, body: relayBody, encoding, contentType, truncated, tried };
|
|
341
|
+
}
|
|
342
|
+
markSuccess(sel.key_id, Date.now() - started);
|
|
343
|
+
const tok = opts.tokens;
|
|
344
|
+
if (Number.isInteger(tok) && tok > 0) {
|
|
345
|
+
sdb.prepare("INSERT INTO usage (pool_id, key_id, tokens) VALUES (?, ?, ?)").run(poolId, sel.key_id, tok);
|
|
346
|
+
}
|
|
347
|
+
return { status: 200, key_id: sel.key_id, label: sel.label, masked: sel.masked, upstreamStatus: upstream.status, body: relayBody, encoding, contentType, truncated, tried };
|
|
348
|
+
}
|
|
349
|
+
return { status: 400, error: "all keys failed or cooling down", tried };
|
|
350
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Anthropic</title><path d="M13.827 3.52h3.603L24 20h-3.603l-6.57-16.48zm-7.258 0h3.767L16.906 20h-3.674l-1.343-3.461H5.017l-1.344 3.46H0L6.57 3.522zm4.132 9.959L8.453 7.687 6.205 13.48H10.7z"></path></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Anyscale</title><path d="M17.583 12.344L14.606 17.5H20.6c.22 0 .424-.117.535-.308l2.799-4.848h-6.351zM23.934 11.656l-2.799-4.848A.616.616 0 0020.6 6.5h-5.994l2.977 5.156h6.35zM8.653 6.5h5.953l-2.997-5.191A.616.616 0 0011.074 1H5.476l3.176 5.5zM4.881 1.343L2.083 6.191a.618.618 0 000 .617l2.997 5.191 2.976-5.156-3.175-5.5zM8.057 17.155L5.081 12l-2.998 5.192a.618.618 0 000 .617l2.798 4.848 3.175-5.5h.001zM5.476 23h5.598c.22 0 .424-.117.535-.308l2.997-5.192H8.653L5.477 23z"></path></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="#FD366E" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Appwrite</title><path d="M24 17.291v5.29H10.557A10.58 10.58 0 0 1 0 12.715v-1.43c.048-.735.174-1.463.374-2.171C1.63 4.673 5.713 1.419 10.557 1.419c4.844 0 8.927 3.254 10.183 7.695h-5.749a5.283 5.283 0 0 0-4.434-2.404 5.282 5.282 0 0 0-4.434 2.404A5.23 5.23 0 0 0 5.267 12a5.27 5.27 0 0 0 1.66 3.848 5.27 5.27 0 0 0 3.63 1.443H24Zm0-6.734v5.291h-9.813A5.276 5.276 0 0 0 15.848 12c0-.5-.07-.984-.199-1.443H24Z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Brave</title><path d="M17.544 2.375c.017-.005 1.844-.5 2.712.361.872.872 1.588 1.642 1.588 1.642l-.565 1.38v-.003.006-.003L22 7.8c-.014.05-2.112 7.983-2.357 8.954-.488 1.924-.819 2.663-2.202 3.638a212.634 212.634 0 01-4.305 2.917c-.41.252-.92.691-1.383.691-.463 0-.974-.439-1.383-.691a213.099 213.099 0 01-4.306-2.917c-1.383-.975-1.72-1.714-2.2-3.632-.246-.977-2.35-8.904-2.364-8.96l.722-2.045-.566-1.383s.722-.764 1.594-1.63c.866-.872 2.712-.36 2.712-.36L8.066 0h7.373l2.105 2.375zm-5.797 12.557c-.138 0-1.04.318-1.762.691l-.457.234c-.487.253-.823.428-.956.506-.168.108-.066.306.09.414.15.103 2.195 1.684 2.394 1.865l.09.078c.186.168.432.391.607.391.174 0 .415-.223.607-.391l.084-.078c.2-.169 2.244-1.756 2.394-1.865.15-.108.258-.3.09-.408-.133-.084-.475-.253-.956-.506h-.006l-.457-.24c-.722-.373-1.623-.691-1.762-.691zm.006-11.276c-.35.02-.694.092-1.023.211l-.378.126c-.493.169-.969.331-1.21.331-.312 0-2.554-.428-2.584-.433 0 0-2.706 3.26-2.706 3.957 0 .577.228.805.504 1.07l.174.175 2.033 2.152.06.067c.204.204.5.498.29.998l-.043.102c-.228.535-.511 1.203-.15 1.876.384.716 1.046 1.19 1.467 1.118.42-.084 1.419-.601 1.78-.841.367-.229 1.52-1.19 1.521-1.551 0-.307-.829-.812-1.238-1.053l-.18-.12-.199-.12c-.367-.229-1.035-.644-1.047-.825-.018-.228-.017-.294.283-.853l.21-.379c.289-.487.602-1.029.536-1.426-.085-.433-.777-.685-1.36-.901l-.21-.078-.613-.229c-.583-.222-1.232-.463-1.34-.511-.145-.073-.11-.132.335-.174l.223-.025c.553-.06 1.582-.168 2.08-.03l.32.09c.564.145 1.25.337 1.316.445l.03.048c.067.09.109.145.037.53l-.121.607c-.15.806-.391 2.069-.421 2.351l-.012.115c-.042.312-.066.529.301.613.438.119.884.206 1.335.259.216 0 .824-.144 1.24-.24l.095-.025c.367-.078.343-.289.3-.602l-.011-.12c-.03-.282-.27-1.54-.42-2.345l-.122-.614c-.072-.384-.024-.439.036-.529l.03-.048c.067-.108.753-.294 1.318-.444l.318-.091c.5-.138 1.528-.03 2.081.03l.216.018c.451.048.493.108.343.18-.11.049-.758.29-1.341.512-.273.108-.547.21-.823.307-.583.216-1.275.468-1.36.907-.066.391.247.939.535 1.42l.21.379c.301.56.308.625.284.854-.012.18-.68.595-1.053.824l-.192.126-.181.108c-.41.247-1.238.758-1.238 1.059 0 .367 1.16 1.316 1.521 1.55.367.235 1.36.758 1.78.836.421.078 1.082-.396 1.467-1.112.36-.673.078-1.335-.15-1.876l-.042-.102c-.21-.5.084-.794.289-1.004l.065-.06 2.02-2.147.181-.181c.271-.265.505-.493.505-1.07 0-.698-2.706-3.957-2.706-3.957-.03.006-2.275.44-2.586.44l.007-.007c-.252 0-.722-.156-1.215-.337l-.379-.12c-.612-.21-1.02-.21-1.022-.21z"></path></svg>
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<svg width="416" height="426" viewBox="0 0 416 426" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<rect width="415.632" height="425.055" fill="#309D4B"/>
|
|
3
|
+
<path d="M317.301 182.114V237.681H289.766L286.153 232.679L277.49 220.689L271.591 212.526L261.508 198.571L264.557 212.526L268.964 232.679L270.056 237.681L273.37 252.833L277.49 271.685L277.774 272.987L282.178 293.138L286.585 313.292H224.406L219.999 293.138L217.769 282.929L215.595 272.987L211.189 252.833L207.877 237.681L204.565 252.833L200.158 272.987L197.864 283.481L195.754 293.138L191.347 313.292H129.048L133.455 293.138L137.738 273.533V273.53L138.143 273.231L138.475 272.987L158.05 258.489L165.69 252.833L177.957 243.747L186.149 237.681H98.332V182.114H157.722L158.05 180.617L159.887 172.221L164.291 152.067L168.698 131.914L173.102 111.763H242.531L246.935 131.914L247.858 136.131L237.676 143.671L226.336 152.067L217.769 158.413L199.123 172.221L197.864 173.152L185.764 182.114H317.301Z" fill="#F9F9F8"/>
|
|
4
|
+
</svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Claude</title><path d="M4.709 15.955l4.72-2.647.08-.23-.08-.128H9.2l-.79-.048-2.698-.073-2.339-.097-2.266-.122-.571-.121L0 11.784l.055-.352.48-.321.686.06 1.52.103 2.278.158 1.652.097 2.449.255h.389l.055-.157-.134-.098-.103-.097-2.358-1.596-2.552-1.688-1.336-.972-.724-.491-.364-.462-.158-1.008.656-.722.881.06.225.061.893.686 1.908 1.476 2.491 1.833.365.304.145-.103.019-.073-.164-.274-1.355-2.446-1.446-2.49-.644-1.032-.17-.619a2.97 2.97 0 01-.104-.729L6.283.134 6.696 0l.996.134.42.364.62 1.414 1.002 2.229 1.555 3.03.456.898.243.832.091.255h.158V9.01l.128-1.706.237-2.095.23-2.695.08-.76.376-.91.747-.492.584.28.48.685-.067.444-.286 1.851-.559 2.903-.364 1.942h.212l.243-.242.985-1.306 1.652-2.064.73-.82.85-.904.547-.431h1.033l.76 1.129-.34 1.166-1.064 1.347-.881 1.142-1.264 1.7-.79 1.36.073.11.188-.02 2.856-.606 1.543-.28 1.841-.315.833.388.091.395-.328.807-1.969.486-2.309.462-3.439.813-.042.03.049.061 1.549.146.662.036h1.622l3.02.225.79.522.474.638-.079.485-1.215.62-1.64-.389-3.829-.91-1.312-.329h-.182v.11l1.093 1.068 2.006 1.81 2.509 2.33.127.578-.322.455-.34-.049-2.205-1.657-.851-.747-1.926-1.62h-.128v.17l.444.649 2.345 3.521.122 1.08-.17.353-.608.213-.668-.122-1.374-1.925-1.415-2.167-1.143-1.943-.14.08-.674 7.254-.316.37-.729.28-.607-.461-.322-.747.322-1.476.389-1.924.315-1.53.286-1.9.17-.632-.012-.042-.14.018-1.434 1.967-2.18 2.945-1.726 1.845-.414.164-.717-.37.067-.662.401-.589 2.388-3.036 1.44-1.882.93-1.086-.006-.158h-.055L4.132 18.56l-1.13.146-.487-.456.061-.746.231-.243 1.908-1.312-.006.006z"></path></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Cloudflare</title><path d="M16.493 17.4c.135-.52.08-.983-.161-1.338-.215-.328-.592-.519-1.05-.519l-8.663-.109a.148.148 0 01-.135-.082c-.027-.054-.027-.109-.027-.163.027-.082.108-.164.189-.164l8.744-.11c1.05-.054 2.153-.9 2.556-1.937l.511-1.31c.027-.055.027-.11.027-.164C17.92 8.91 15.66 7 12.942 7c-2.503 0-4.628 1.638-5.381 3.903a2.432 2.432 0 00-1.803-.491c-1.21.109-2.153 1.092-2.287 2.32-.027.328 0 .628.054.9C1.56 13.688 0 15.326 0 17.319c0 .19.027.355.027.545 0 .082.08.137.161.137h15.983c.08 0 .188-.055.215-.164l.107-.437"></path><path d="M19.238 11.75h-.242c-.054 0-.108.054-.135.109l-.35 1.2c-.134.52-.08.983.162 1.338.215.328.592.518 1.05.518l1.855.11c.054 0 .108.027.135.082.027.054.027.109.027.163-.027.082-.108.164-.188.164l-1.91.11c-1.05.054-2.153.9-2.557 1.937l-.134.355c-.027.055.026.137.107.137h6.592c.081 0 .162-.055.162-.137.107-.41.188-.846.188-1.31-.027-2.62-2.153-4.777-4.762-4.777"></path></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>CommandA</title><path clip-rule="evenodd" d="M8.128 14.099c.592 0 1.77-.033 3.398-.703 1.897-.781 5.672-2.2 8.395-3.656 1.905-1.018 2.74-2.366 2.74-4.18A4.56 4.56 0 0018.1 1H7.549A6.55 6.55 0 001 7.55c0 3.617 2.745 6.549 7.128 6.549z"></path><path clip-rule="evenodd" d="M9.912 18.61a4.387 4.387 0 012.705-4.052l3.323-1.38c3.361-1.394 7.06 1.076 7.06 4.715a5.104 5.104 0 01-5.105 5.104l-3.597-.001a4.386 4.386 0 01-4.386-4.387z"></path><path d="M4.776 14.962A3.775 3.775 0 001 18.738v.489a3.776 3.776 0 007.551 0v-.49a3.775 3.775 0 00-3.775-3.775z"></path></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Cohere</title><path clip-rule="evenodd" d="M8.128 14.099c.592 0 1.77-.033 3.398-.703 1.897-.781 5.672-2.2 8.395-3.656 1.905-1.018 2.74-2.366 2.74-4.18A4.56 4.56 0 0018.1 1H7.549A6.55 6.55 0 001 7.55c0 3.617 2.745 6.549 7.128 6.549z"></path><path clip-rule="evenodd" d="M9.912 18.61a4.387 4.387 0 012.705-4.052l3.323-1.38c3.361-1.394 7.06 1.076 7.06 4.715a5.104 5.104 0 01-5.105 5.104l-3.597-.001a4.386 4.386 0 01-4.386-4.387z"></path><path d="M4.776 14.962A3.775 3.775 0 001 18.738v.489a3.776 3.776 0 007.551 0v-.49a3.775 3.775 0 00-3.775-3.775z"></path></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="#13EF93" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Deepgram</title><path d="M11.203 24H1.517a.364.364 0 0 1-.258-.62l6.239-6.275a.366.366 0 0 1 .259-.108h3.52c2.723 0 5.025-2.127 5.107-4.845a5.004 5.004 0 0 0-4.999-5.148H7.613v4.646c0 .2-.164.364-.365.364H.968a.365.365 0 0 1-.363-.364V.364C.605.164.768 0 .969 0h10.416c6.684 0 12.111 5.485 12.01 12.187C23.293 18.77 17.794 24 11.202 24z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>ElevenLabs</title><path d="M5 0h5v24H5V0zM14 0h5v24h-5V0z"></path></svg>
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Fireworks</title><path clip-rule="evenodd" d="M14.8 5l-2.801 6.795L9.195 5H7.397l3.072 7.428a1.64 1.64 0 003.038.002L16.598 5H14.8zm1.196 10.352l5.124-5.244-.699-1.669-5.596 5.739a1.664 1.664 0 00-.343 1.807 1.642 1.642 0 001.516 1.012L16 17l8-.02-.699-1.669-7.303.041h-.002zM2.88 10.104l.699-1.669 5.596 5.739c.468.479.603 1.189.343 1.807a1.643 1.643 0 01-1.516 1.012l-8-.018-.002.002.699-1.669 7.303.042-5.122-5.246z"></path></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Gemini</title><path d="M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678-6.452.503.503 0 00-.975 0 14.134 14.134 0 01-3.679 6.452 14.155 14.155 0 01-4.45 3.001c-.65.28-1.318.505-2.002.678a.502.502 0 000 .975c.684.172 1.35.397 2.002.677a14.147 14.147 0 014.45 3.001 14.112 14.112 0 013.679 6.453.502.502 0 00.975 0c.172-.685.397-1.351.677-2.003a14.145 14.145 0 013.001-4.45 14.113 14.113 0 016.453-3.678.503.503 0 000-.975 13.245 13.245 0 01-2.003-.678z"></path></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Google</title><path d="M23 12.245c0-.905-.075-1.565-.236-2.25h-10.54v4.083h6.186c-.124 1.014-.797 2.542-2.294 3.569l-.021.136 3.332 2.53.23.022C21.779 18.417 23 15.593 23 12.245z"></path><path d="M12.225 23c3.03 0 5.574-.978 7.433-2.665l-3.542-2.688c-.948.648-2.22 1.1-3.891 1.1a6.745 6.745 0 01-6.386-4.572l-.132.011-3.465 2.628-.045.124C4.043 20.531 7.835 23 12.225 23z"></path><path d="M5.84 14.175A6.65 6.65 0 015.463 12c0-.758.138-1.491.361-2.175l-.006-.147-3.508-2.67-.115.054A10.831 10.831 0 001 12c0 1.772.436 3.447 1.197 4.938l3.642-2.763z"></path><path d="M12.225 5.253c2.108 0 3.529.892 4.34 1.638l3.167-3.031C17.787 2.088 15.255 1 12.225 1 7.834 1 4.043 3.469 2.197 7.062l3.63 2.763a6.77 6.77 0 016.398-4.572z"></path></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="#4285F4" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Google Maps</title><path d="M19.527 4.799c1.212 2.608.937 5.678-.405 8.173-1.101 2.047-2.744 3.74-4.098 5.614-.619.858-1.244 1.75-1.669 2.727-.141.325-.263.658-.383.992-.121.333-.224.673-.34 1.008-.109.314-.236.684-.627.687h-.007c-.466-.001-.579-.53-.695-.887-.284-.874-.581-1.713-1.019-2.525-.51-.944-1.145-1.817-1.79-2.671L19.527 4.799zM8.545 7.705l-3.959 4.707c.724 1.54 1.821 2.863 2.871 4.18.247.31.494.622.737.936l4.984-5.925-.029.01c-1.741.601-3.691-.291-4.392-1.987a3.377 3.377 0 0 1-.209-.716c-.063-.437-.077-.761-.004-1.198l.001-.007zM5.492 3.149l-.003.004c-1.947 2.466-2.281 5.88-1.117 8.77l4.785-5.689-.058-.05-3.607-3.035zM14.661.436l-3.838 4.563a.295.295 0 0 1 .027-.01c1.6-.551 3.403.15 4.22 1.626.176.319.323.683.377 1.045.068.446.085.773.012 1.22l-.003.016 3.836-4.561A8.382 8.382 0 0 0 14.67.439l-.009-.003zM9.466 5.868L14.162.285l-.047-.012A8.31 8.31 0 0 0 11.986 0a8.439 8.439 0 0 0-6.169 2.766l-.016.018 3.665 3.084z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Groq</title><path d="M12.036 2c-3.853-.035-7 3-7.036 6.781-.035 3.782 3.055 6.872 6.908 6.907h2.42v-2.566h-2.292c-2.407.028-4.38-1.866-4.408-4.23-.029-2.362 1.901-4.298 4.308-4.326h.1c2.407 0 4.358 1.915 4.365 4.278v6.305c0 2.342-1.944 4.25-4.323 4.279a4.375 4.375 0 01-3.033-1.252l-1.851 1.818A7 7 0 0012.029 22h.092c3.803-.056 6.858-3.083 6.879-6.816v-6.5C18.907 4.963 15.817 2 12.036 2z"></path></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>HuggingFace</title><path d="M16.781 3.277c2.997 1.704 4.844 4.851 4.844 8.258 0 .995-.155 1.955-.443 2.857a1.332 1.332 0 011.125.4 1.41 1.41 0 01.2 1.723c.204.165.352.385.428.632l.017.062c.06.222.12.69-.2 1.166.244.37.279.836.093 1.236-.255.57-.893 1.018-2.128 1.5l-.202.078-.131.048c-.478.173-.89.295-1.061.345l-.086.024c-.89.243-1.808.375-2.732.394-1.32 0-2.3-.36-2.923-1.067a9.852 9.852 0 01-3.18.018C9.778 21.647 8.802 22 7.494 22a11.249 11.249 0 01-2.541-.343l-.221-.06-.273-.08a16.574 16.574 0 01-1.175-.405c-1.237-.483-1.875-.93-2.13-1.501-.186-.4-.151-.867.093-1.236a1.42 1.42 0 01-.2-1.166c.069-.273.226-.516.447-.694a1.41 1.41 0 01.2-1.722c.233-.248.557-.391.917-.407l.078-.001a9.385 9.385 0 01-.44-2.85c0-3.407 1.847-6.554 4.844-8.258a9.822 9.822 0 019.687 0zM4.188 14.758c.125.687 2.357 2.35 2.14 2.707-.19.315-.796-.239-.948-.386l-.041-.04-.168-.147c-.561-.479-2.304-1.9-2.74-1.432-.43.46.119.859 1.055 1.42l.784.467.136.083c1.045.643 1.12.84.95 1.113-.188.295-3.07-2.1-3.34-1.083-.27 1.011 2.942 1.304 2.744 2.006-.2.7-2.265-1.324-2.685-.537-.425.79 2.913 1.718 2.94 1.725l.16.04.175.042c1.227.284 3.565.65 4.435-.604.673-.973.64-1.709-.248-2.61l-.057-.057c-.945-.928-1.495-2.288-1.495-2.288l-.017-.058-.025-.072c-.082-.22-.284-.639-.63-.584-.46.073-.798 1.21.12 1.933l.05.038c.977.721-.195 1.21-.573.534l-.058-.104-.143-.25c-.463-.799-1.282-2.111-1.739-2.397-.532-.332-.907-.148-.782.541zm14.842-.541c-.533.335-1.563 2.074-1.94 2.751a.613.613 0 01-.687.302.436.436 0 01-.176-.098.303.303 0 01-.049-.06l-.014-.028-.008-.02-.007-.019-.003-.013-.003-.017a.289.289 0 01-.004-.048c0-.12.071-.266.25-.427.026-.024.054-.047.084-.07l.047-.036c.022-.016.043-.032.063-.049.883-.71.573-1.81.131-1.917l-.031-.006-.056-.004a.368.368 0 00-.062.006l-.028.005-.042.014-.039.017-.028.015-.028.019-.036.027-.023.02c-.173.158-.273.428-.31.542l-.016.054s-.53 1.309-1.439 2.234l-.054.054c-.365.358-.596.69-.702 1.018-.143.437-.066.868.21 1.353.055.097.117.195.187.296.882 1.275 3.282.876 4.494.59l.286-.07.25-.074c.276-.084.736-.233 1.2-.42l.188-.077.065-.028.064-.028.124-.056.081-.038c.529-.252.964-.543.994-.827l.001-.036a.299.299 0 00-.037-.139c-.094-.176-.271-.212-.491-.168l-.045.01c-.044.01-.09.024-.136.04l-.097.035-.054.022c-.559.23-1.238.705-1.607.745h.006a.452.452 0 01-.05.003h-.024l-.024-.003-.023-.005c-.068-.016-.116-.06-.14-.142a.22.22 0 01-.005-.1c.062-.345.958-.595 1.713-.91l.066-.028c.528-.224.97-.483.985-.832v-.04a.47.47 0 00-.016-.098c-.048-.18-.175-.251-.36-.251-.785 0-2.55 1.36-2.92 1.36-.025 0-.048-.007-.058-.024a.6.6 0 01-.046-.088c-.1-.238.068-.462 1.06-1.066l.209-.126c.538-.32 1.01-.588 1.341-.831.29-.212.475-.406.503-.6l.003-.028c.008-.113-.038-.227-.147-.344a.266.266 0 00-.07-.054l-.034-.015-.013-.005a.403.403 0 00-.13-.02c-.162 0-.369.07-.595.18-.637.313-1.431.952-1.826 1.285l-.249.215-.033.033c-.08.078-.288.27-.493.386l-.071.037-.041.019a.535.535 0 01-.122.036h.005a.346.346 0 01-.031.003l.01-.001-.013.001c-.079.005-.145-.021-.19-.095a.113.113 0 01-.014-.065c.027-.465 2.034-1.991 2.152-2.642l.009-.048c.1-.65-.271-.817-.791-.493zM11.938 2.984c-4.798 0-8.688 3.829-8.688 8.55 0 .692.083 1.364.24 2.008l.008-.009c.252-.298.612-.46 1.017-.46.355.008.699.117.993.312.22.14.465.384.715.694.261-.372.69-.598 1.15-.605.852 0 1.367.728 1.562 1.383l.047.105.06.127c.192.396.595 1.139 1.143 1.68 1.06 1.04 1.324 2.115.8 3.266a8.865 8.865 0 002.024-.014c-.505-1.12-.26-2.17.74-3.186l.066-.066c.695-.684 1.157-1.69 1.252-1.912.195-.655.708-1.383 1.56-1.383.46.007.889.233 1.15.605.25-.31.495-.553.718-.694a1.87 1.87 0 01.99-.312c.357 0 .682.126.925.36.14-.61.215-1.245.215-1.898 0-4.722-3.89-8.55-8.687-8.55zm1.857 8.926l.439-.212c.553-.264.89-.383.89.152 0 1.093-.771 3.208-3.155 3.262h-.184c-2.325-.052-3.116-2.06-3.156-3.175l-.001-.087c0-1.107 1.452.586 3.25.586.716 0 1.379-.272 1.917-.526zm4.017-3.143c.45 0 .813.358.813.8 0 .441-.364.8-.813.8a.806.806 0 01-.812-.8c0-.442.364-.8.812-.8zm-11.624 0c.448 0 .812.358.812.8 0 .441-.364.8-.812.8a.806.806 0 01-.813-.8c0-.442.364-.8.813-.8zm7.79-.841c.32-.384.846-.54 1.33-.394.483.146.83.564.878 1.06.048.495-.212.97-.659 1.203-.322.168-.447-.477-.767-.585l.002-.003c-.287-.098-.772.362-.925.079a1.215 1.215 0 01.14-1.36zm-4.323 0c.322.384.377.92.14 1.36-.152.283-.64-.177-.925-.079l.003.003c-.108.036-.194.134-.273.24l-.118.165c-.11.15-.22.262-.377.18a1.226 1.226 0 01-.658-1.204c.048-.495.395-.913.878-1.059a1.262 1.262 0 011.33.394z"></path></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Mailgun</title><path d="M11.837 0c6.602 0 11.984 5.381 11.984 11.994-.017 2.99-3.264 4.84-5.844 3.331a3.805 3.805 0 0 1-.06-.035l-.055-.033-.022.055c-2.554 4.63-9.162 4.758-11.894.232-2.732-4.527.46-10.313 5.746-10.416a6.868 6.868 0 0 1 7.002 6.866 1.265 1.265 0 0 0 2.52 0c0-5.18-4.197-9.38-9.377-9.387C4.611 2.594.081 10.41 3.683 16.673c3.238 5.632 11.08 6.351 15.289 1.402l1.997 1.686A11.95 11.95 0 0 1 11.837 24C2.6 23.72-2.87 13.543 1.992 5.684A12.006 12.006 0 0 1 11.837 0Zm0 7.745c-3.276-.163-5.5 3.281-4.003 6.2a4.26 4.26 0 0 0 4.014 2.31c3.276-.171 5.137-3.824 3.35-6.575a4.26 4.26 0 0 0-3.36-1.935Zm0 2.53c1.324 0 2.152 1.433 1.49 2.58a1.72 1.72 0 0 1-1.49.86 1.72 1.72 0 1 1 0-3.44Z"/></svg>
|