pool-anything 0.1.0 → 0.1.1
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/README.md +13 -4
- package/data/providers.json +1 -1
- package/dist/admin/branding.js +1 -0
- package/dist/admin/pages.js +632 -46
- package/dist/admin/seo.js +58 -0
- package/dist/admin/shell.js +45 -2
- package/dist/cli/commands.js +276 -0
- package/dist/cli/nl.js +10 -0
- package/dist/cli/repl.js +95 -331
- package/dist/cli/tui.js +254 -0
- package/dist/cli/ui.js +2 -0
- package/dist/cli.js +11 -3
- package/dist/data/providers.json +1 -1
- package/dist/playground/adapters.js +304 -0
- package/dist/pool/index.js +39 -0
- package/dist/proxy/forward.js +44 -2
- package/dist/router/api.js +2 -2
- package/dist/server.js +43 -4
- package/dist/upstream/index.js +10 -0
- package/dist/version.js +160 -0
- package/package.json +8 -3
package/dist/cli/tui.js
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Rich interactive shell built on Ink + React.
|
|
4
|
+
* Same command core as the plain frontend (commands.ts) — only rendering differs.
|
|
5
|
+
* `serve` exits the TUI first (Ink owns the screen) and boots the server after.
|
|
6
|
+
*/
|
|
7
|
+
import { useEffect, useRef, useState } from "react";
|
|
8
|
+
import { Box, Text, render, useApp, useInput } from "ink";
|
|
9
|
+
import { PROVIDERS, mask } from "../pool/index.js";
|
|
10
|
+
import { HOST, PORT } from "../config/env.js";
|
|
11
|
+
import { parse } from "./nl.js";
|
|
12
|
+
import { fmtCompact, fmtFull } from "./ui.js";
|
|
13
|
+
import { dispatch, insertKeys, liveStats, } from "./commands.js";
|
|
14
|
+
export let serveRequested = false;
|
|
15
|
+
let nextId = 1;
|
|
16
|
+
function useTranscript() {
|
|
17
|
+
const [entries, setEntries] = useState([]);
|
|
18
|
+
const push = (node) => setEntries((prev) => [...prev.slice(-60), { id: nextId++, node }]);
|
|
19
|
+
const clear = () => setEntries([]);
|
|
20
|
+
return { entries, push, clear };
|
|
21
|
+
}
|
|
22
|
+
function Banner({ version }) {
|
|
23
|
+
return (_jsxs(Box, { borderStyle: "round", borderColor: "cyan", paddingX: 2, paddingY: 0, flexDirection: "column", children: [_jsxs(Text, { bold: true, color: "cyan", children: ["pool-anything v", version, " \u2014 API key pooler"] }), _jsx(Text, { dimColor: true, children: "Type '.help' for commands, 'exit' to quit" })] }));
|
|
24
|
+
}
|
|
25
|
+
function PoolsView({ pools }) {
|
|
26
|
+
if (pools.length === 0) {
|
|
27
|
+
return _jsxs(Text, { dimColor: true, children: ["(no pools yet \u2014 try: create pool ", "<name>", " for ", "<provider>", ")"] });
|
|
28
|
+
}
|
|
29
|
+
return (_jsxs(Box, { borderStyle: "round", borderColor: "cyan", paddingX: 1, flexDirection: "column", children: [_jsxs(Text, { bold: true, children: [pools.length, " active pool", pools.length === 1 ? "" : "s"] }), pools.map((p) => {
|
|
30
|
+
const usage = p.quota === null
|
|
31
|
+
? `${fmtCompact(p.used)} tokens`
|
|
32
|
+
: `${fmtCompact(p.used)} / ${fmtCompact(p.quota * p.keys)} tokens`;
|
|
33
|
+
return (_jsxs(Text, { dimColor: !p.active, children: [_jsxs(Text, { color: "yellow", children: ["#", p.id] }), " ", _jsxs(Text, { bold: true, children: ["\"", p.name, "\""] }), " ", _jsxs(Text, { color: "cyan", children: ["(", p.provider, ")"] }), " \u2014 ", p.keys, " key", p.keys === 1 ? "" : "s", ",", " ", usage, p.active ? "" : ", inactive"] }, p.id));
|
|
34
|
+
})] }));
|
|
35
|
+
}
|
|
36
|
+
function ProvidersView() {
|
|
37
|
+
return (_jsx(Box, { flexDirection: "column", children: PROVIDERS.map((p) => (_jsxs(Text, { children: [_jsxs(Text, { color: "cyan", children: ["- ", p.id] }), " ", _jsxs(Text, { dimColor: true, children: ["\u2014 ", p.name, " (", p.quota || "no quota", ")"] })] }, p.id))) }));
|
|
38
|
+
}
|
|
39
|
+
function KeysView({ pool, keys }) {
|
|
40
|
+
if (keys.length === 0) {
|
|
41
|
+
return _jsxs(Text, { dimColor: true, children: ["Pool #", pool.id, " \"", pool.name, "\" has no keys. Try: add keys to ", pool.provider, "."] });
|
|
42
|
+
}
|
|
43
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, children: ["Keys in pool #", pool.id, " \"", pool.name, "\":"] }), keys.map((k) => (_jsxs(Text, { children: [" ", _jsxs(Text, { color: "yellow", children: ["#", k.id] }), " ", k.label, " ", _jsxs(Text, { dimColor: true, children: ["(", k.masked, ")"] })] }, k.id))), _jsx(Text, { dimColor: true, children: "Keys are shown masked \u2014 raw values never print." })] }));
|
|
44
|
+
}
|
|
45
|
+
function QuotaBar({ used, total }) {
|
|
46
|
+
const width = 28;
|
|
47
|
+
const filled = total <= 0 ? 0 : Math.min(width, Math.round((used / total) * width));
|
|
48
|
+
return (_jsxs(Text, { children: [_jsx(Text, { color: "green", children: "█".repeat(filled) }), _jsx(Text, { dimColor: true, children: "░".repeat(width - filled) })] }));
|
|
49
|
+
}
|
|
50
|
+
function UsageView({ view, perKey }) {
|
|
51
|
+
const total = view.quota === null ? null : view.quota * view.keys;
|
|
52
|
+
const pct = total === null || total === 0 ? null : Math.round((view.used / total) * 100);
|
|
53
|
+
return (_jsxs(Box, { borderStyle: "round", borderColor: "green", paddingX: 1, flexDirection: "column", children: [_jsxs(Text, { bold: true, children: ["Pool #", view.id, ": ", view.name, " ", _jsxs(Text, { color: "cyan", children: ["(", view.provider, ")"] })] }), _jsxs(Text, { children: ["Used: ", _jsx(Text, { bold: true, children: fmtFull(view.used) }), " tokens \u2502 Quota: ", total === null ? "unlimited" : fmtFull(total), " \u2502 Remaining:", " ", view.remaining === null ? "—" : fmtFull(view.remaining), pct !== null ? ` (${pct}% exhausted)` : ""] }), total !== null && _jsx(QuotaBar, { used: view.used, total: total }), _jsxs(Text, { children: ["Status: ", view.cooling > 0 ? _jsxs(Text, { color: "yellow", children: [view.cooling, " cooling"] }) : _jsx(Text, { color: "green", children: "OK" }), " \u2502 Keys:", " ", view.keys, " active, ", view.cooling, " cooling"] }), perKey.map((k) => (_jsxs(Text, { children: [" ", k.label, " ", _jsxs(Text, { dimColor: true, children: ["(", k.masked, ")"] }), ": ", fmtFull(k.used), " tok"] }, k.label)))] }));
|
|
54
|
+
}
|
|
55
|
+
function MessageView({ tone, text }) {
|
|
56
|
+
const color = tone === "ok" ? "green" : tone === "err" ? "red" : undefined;
|
|
57
|
+
const prefix = tone === "ok" ? "✓ " : tone === "err" ? "✗ " : "";
|
|
58
|
+
return _jsxs(Text, { color: color, children: [prefix, text] });
|
|
59
|
+
}
|
|
60
|
+
function WatchView({ pool, onDone }) {
|
|
61
|
+
const [tick, setTick] = useState(0);
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
const t = setInterval(() => setTick((n) => n + 1), 2000);
|
|
64
|
+
return () => clearInterval(t);
|
|
65
|
+
}, []);
|
|
66
|
+
useInput((_input, key) => {
|
|
67
|
+
if (key.escape || _input === "q" || _input === "Q")
|
|
68
|
+
onDone();
|
|
69
|
+
}, { isActive: true });
|
|
70
|
+
void tick;
|
|
71
|
+
const st = liveStats(pool.id);
|
|
72
|
+
return (_jsxs(Box, { borderStyle: "round", borderColor: "yellow", paddingX: 1, flexDirection: "column", children: [_jsxs(Text, { bold: true, children: ["\uD83D\uDD04 Pool #", pool.id, " \"", pool.name, "\" \u2014 live usage ", _jsx(Text, { dimColor: true, children: "[auto-refresh 2s, press q to exit]" })] }), _jsxs(Text, { children: ["Requests: ", _jsxs(Text, { bold: true, color: "green", children: [st.reqMin, "/min"] }), " \u2502 Tokens: ", _jsxs(Text, { bold: true, color: "green", children: [st.tokMin, "/min"] }), " \u2502 Avg latency:", " ", st.avgLatency === null ? "—" : `${(st.avgLatency / 1000).toFixed(1)}s`] }), st.perKey.map((k) => (_jsxs(Text, { children: [" ", k.label, " ", _jsxs(Text, { dimColor: true, children: ["(", mask(k.apiKey), ")"] }), ": ", fmtFull(k.used), " tok"] }, k.id)))] }));
|
|
73
|
+
}
|
|
74
|
+
function ResultView({ result }) {
|
|
75
|
+
switch (result.kind) {
|
|
76
|
+
case "message": return _jsx(MessageView, { tone: result.tone, text: result.text });
|
|
77
|
+
case "pools": return _jsx(PoolsView, { pools: result.pools });
|
|
78
|
+
case "providers": return _jsx(ProvidersView, {});
|
|
79
|
+
case "keys": return _jsx(KeysView, { pool: result.pool, keys: result.keys });
|
|
80
|
+
case "usage": return _jsx(UsageView, { view: result.view, perKey: result.perKey });
|
|
81
|
+
default: return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function Shell({ version }) {
|
|
85
|
+
const app = useApp();
|
|
86
|
+
const { entries, push, clear } = useTranscript();
|
|
87
|
+
const [input, setInput] = useState("");
|
|
88
|
+
const [history, setHistory] = useState([]);
|
|
89
|
+
const [histIdx, setHistIdx] = useState(-1);
|
|
90
|
+
const [mode, setMode] = useState({ t: "normal" });
|
|
91
|
+
const [collected, setCollected] = useState(0);
|
|
92
|
+
const importLines = useRef([]);
|
|
93
|
+
const questionResolve = useRef(null);
|
|
94
|
+
const ask = (q) => new Promise((resolve) => {
|
|
95
|
+
questionResolve.current = resolve;
|
|
96
|
+
setMode({ t: "question", prompt: q });
|
|
97
|
+
});
|
|
98
|
+
const submitImportLine = (line) => {
|
|
99
|
+
const st = mode;
|
|
100
|
+
if (st.t !== "import")
|
|
101
|
+
return;
|
|
102
|
+
if (line.trim() === "" || line.trim() === ".done" || line === "\x04") {
|
|
103
|
+
finishImport(st.pool);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
importLines.current.push(line);
|
|
107
|
+
setCollected(importLines.current.length);
|
|
108
|
+
if (st.expected !== null && importLines.current.length >= st.expected) {
|
|
109
|
+
finishImport(st.pool);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
const finishImport = (pool) => {
|
|
113
|
+
const lines = importLines.current;
|
|
114
|
+
importLines.current = [];
|
|
115
|
+
setCollected(0);
|
|
116
|
+
setMode({ t: "normal" });
|
|
117
|
+
if (lines.length === 0) {
|
|
118
|
+
push(_jsx(MessageView, { tone: "info", text: "No keys pasted \u2014 nothing added." }));
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const { added, skipped } = insertKeys(pool, lines);
|
|
122
|
+
push(_jsx(MessageView, { tone: "ok", text: `Added ${added} key${added === 1 ? "" : "s"} to pool #${pool.id}${skipped > 0 ? ` (${skipped} skipped: blank/duplicate/invalid)` : ""}.` }));
|
|
123
|
+
};
|
|
124
|
+
const runCommand = async (cmd) => {
|
|
125
|
+
push(_jsxs(Text, { children: [_jsx(Text, { color: "yellow", bold: true, children: "\u276F " }), _jsx(Text, { bold: true, children: cmd })] }));
|
|
126
|
+
// `serve` boots a long-lived server that logs to stdout — exit the TUI
|
|
127
|
+
// first (Ink owns the screen), then start the server after unmount.
|
|
128
|
+
if (parse(cmd).kind === "serve") {
|
|
129
|
+
push(_jsx(MessageView, { tone: "ok", text: `Starting server on http://${HOST}:${PORT} …` }));
|
|
130
|
+
serveRequested = true;
|
|
131
|
+
app.exit();
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
let r;
|
|
135
|
+
try {
|
|
136
|
+
r = await dispatch(cmd, ask);
|
|
137
|
+
}
|
|
138
|
+
catch (e) {
|
|
139
|
+
push(_jsx(MessageView, { tone: "err", text: `Error: ${e.message}` }));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (r.kind === "exit") {
|
|
143
|
+
app.exit();
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (r.kind === "clear") {
|
|
147
|
+
clear();
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
// A confirmed uninstall removes the binary — show the result, then leave
|
|
151
|
+
// the shell instead of prompting on a dead install.
|
|
152
|
+
if (parse(cmd).kind === "uninstall" && r.kind === "message" && r.tone === "ok") {
|
|
153
|
+
push(_jsx(ResultView, { result: r }));
|
|
154
|
+
app.exit();
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (r.kind === "watch") {
|
|
158
|
+
setMode({ t: "watch", pool: r.pool });
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (r.kind === "import") {
|
|
162
|
+
importLines.current = [];
|
|
163
|
+
setCollected(0);
|
|
164
|
+
setMode({ t: "import", pool: r.pool, expected: r.expected });
|
|
165
|
+
push(_jsx(MessageView, { tone: "ok", text: `Opening key import mode for "${r.pool.name}" (pool #${r.pool.id})` }));
|
|
166
|
+
push(_jsx(Text, { dimColor: true, children: r.hint }));
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (r.kind === "serve") {
|
|
170
|
+
// Unreachable: serve is intercepted above so Ink unmounts before the
|
|
171
|
+
// server (which logs to stdout) starts. Kept for exhaustiveness.
|
|
172
|
+
push(_jsx(MessageView, { tone: "ok", text: `Server on http://${HOST}:${PORT}` }));
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
push(_jsx(ResultView, { result: r }));
|
|
176
|
+
};
|
|
177
|
+
/** Submit one complete line from the prompt (shared by Return, pasted and cooked newlines). */
|
|
178
|
+
const submitLine = (line) => {
|
|
179
|
+
setInput("");
|
|
180
|
+
setHistIdx(-1);
|
|
181
|
+
if (mode.t === "question") {
|
|
182
|
+
const resolve = questionResolve.current;
|
|
183
|
+
questionResolve.current = null;
|
|
184
|
+
setMode({ t: "normal" });
|
|
185
|
+
push(_jsxs(Text, { dimColor: true, children: [mode.prompt, line] }));
|
|
186
|
+
resolve?.(line);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (mode.t === "import") {
|
|
190
|
+
submitImportLine(line);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (line.trim() === "")
|
|
194
|
+
return;
|
|
195
|
+
setHistory((h) => [...h.slice(-99), line]);
|
|
196
|
+
void runCommand(line);
|
|
197
|
+
};
|
|
198
|
+
useInput((ch, key) => {
|
|
199
|
+
if (mode.t === "watch")
|
|
200
|
+
return; // WatchView owns the keyboard.
|
|
201
|
+
if (ch === "\x04") {
|
|
202
|
+
// Ctrl+D: finish an import, or exit an empty prompt. (Before the
|
|
203
|
+
// ctrl guard: Ink reports ctrl+d with key.ctrl set.)
|
|
204
|
+
if (mode.t === "import")
|
|
205
|
+
submitImportLine(ch);
|
|
206
|
+
else if (input === "")
|
|
207
|
+
app.exit();
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (ch && /[\r\n]/.test(ch)) {
|
|
211
|
+
// Return, pasted newlines, or cooked-mode line delivery: every
|
|
212
|
+
// complete segment is its own submit, the tail stays on the prompt.
|
|
213
|
+
const segments = (input + ch).split(/[\r\n]/);
|
|
214
|
+
const rest = segments.pop() ?? "";
|
|
215
|
+
for (const seg of segments)
|
|
216
|
+
submitLine(seg);
|
|
217
|
+
setInput(rest);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (key.upArrow || key.downArrow) {
|
|
221
|
+
if (history.length === 0)
|
|
222
|
+
return;
|
|
223
|
+
const next = key.upArrow
|
|
224
|
+
? (histIdx === -1 ? history.length - 1 : Math.max(0, histIdx - 1))
|
|
225
|
+
: (histIdx === -1 ? -1 : Math.min(history.length - 1, histIdx + 1));
|
|
226
|
+
setHistIdx(next);
|
|
227
|
+
setInput(next === -1 ? "" : history[next]);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (key.backspace || key.delete) {
|
|
231
|
+
setInput((s) => s.slice(0, -1));
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (key.ctrl && (ch === "u" || ch === "U")) {
|
|
235
|
+
setInput("");
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (key.ctrl || key.meta || key.escape)
|
|
239
|
+
return;
|
|
240
|
+
if (ch)
|
|
241
|
+
setInput((s) => s + ch);
|
|
242
|
+
});
|
|
243
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Banner, { version: version }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: entries.map((e) => (_jsx(Box, { marginBottom: 0, children: e.node }, e.id))) }), mode.t === "watch" ? (_jsx(Box, { marginTop: 1, children: _jsx(WatchView, { pool: mode.pool, onDone: () => { setMode({ t: "normal" }); push(_jsx(Text, { dimColor: true, children: "Stopped watching." })); } }) })) : (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: mode.t === "import" ? "cyan" : "yellow", bold: true, children: mode.t === "import" ? "... " : mode.t === "question" ? "" : "❯ " }), mode.t === "question" ? (_jsxs(Text, { children: [mode.prompt, _jsx(Text, { bold: true, children: input }), _jsx(Text, { inverse: true, children: " " })] })) : (_jsxs(Text, { children: [_jsx(Text, { bold: true, children: input }), _jsx(Text, { inverse: true, children: " " })] }))] })), mode.t === "import" && (_jsxs(Text, { dimColor: true, children: [collected, " collected \u2014 empty line or Ctrl+D to finish"] }))] }));
|
|
244
|
+
}
|
|
245
|
+
export async function startTui(version) {
|
|
246
|
+
const { waitUntilExit } = render(_jsx(Shell, { version: version }));
|
|
247
|
+
await waitUntilExit();
|
|
248
|
+
if (serveRequested) {
|
|
249
|
+
serveRequested = false;
|
|
250
|
+
await import("../server.js");
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
console.log("Goodbye!");
|
|
254
|
+
}
|
package/dist/cli/ui.js
CHANGED
|
@@ -68,6 +68,8 @@ export function helpBox() {
|
|
|
68
68
|
["next [for <pool>]", "rotate: show the next key (masked)"],
|
|
69
69
|
["consume <n> [on <pool>]", "record token usage"],
|
|
70
70
|
["serve", "start the web UI + API server"],
|
|
71
|
+
["update", "check npm and upgrade to the latest version"],
|
|
72
|
+
["uninstall", "remove the global CLI (asks to confirm, CLI only)"],
|
|
71
73
|
["clear", "clear the screen"],
|
|
72
74
|
["exit", "quit"],
|
|
73
75
|
];
|
package/dist/cli.js
CHANGED
|
@@ -33,7 +33,8 @@ Usage:
|
|
|
33
33
|
Assistant commands (also inside the shell): list pools, list providers,
|
|
34
34
|
list keys [for <pool>], show usage for <pool>, add [N] keys to <pool>,
|
|
35
35
|
create pool <name> for <provider>, delete pool <name>, watch <pool>,
|
|
36
|
-
next [for <pool>], consume <n> [on <pool>],
|
|
36
|
+
next [for <pool>], consume <n> [on <pool>], update, uninstall (CLI only),
|
|
37
|
+
serve, exit. Type '.help' in-shell.
|
|
37
38
|
|
|
38
39
|
Options (server):
|
|
39
40
|
-p, --port <n> Port for the web UI + API (default: 3000, or $PORT)
|
|
@@ -192,8 +193,15 @@ if (!opts.serve && oneShot) {
|
|
|
192
193
|
process.exit(await execOnce(oneShot));
|
|
193
194
|
}
|
|
194
195
|
if (!opts.serve && process.stdin.isTTY && process.stdout.isTTY) {
|
|
195
|
-
|
|
196
|
-
|
|
196
|
+
try {
|
|
197
|
+
const { startTui } = await import("./cli/tui.js");
|
|
198
|
+
await startTui(version);
|
|
199
|
+
}
|
|
200
|
+
catch (e) {
|
|
201
|
+
console.error(`[warn] rich shell unavailable (${e.message}), falling back to plain mode.`);
|
|
202
|
+
const { startRepl } = await import("./cli/repl.js");
|
|
203
|
+
await startRepl(version);
|
|
204
|
+
}
|
|
197
205
|
process.exit(0);
|
|
198
206
|
}
|
|
199
207
|
await import("./server.js");
|
package/dist/data/providers.json
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
{"id":"perplexity","name":"Perplexity","baseUrl":"https://api.perplexity.ai","keyHeader":"Authorization","keyPrefix":"Bearer ","extraHeaders":{},"quota":"$5 credit on signup","docsUrl":"https://docs.perplexity.ai/getting-started/quickstart","keyFields":["api_key"],"hint":"perplexity.ai/settings/api, pplx-…","logo":"public/logos/perplexity.svg"},
|
|
14
14
|
{"id":"anyscale","name":"Anyscale Endpoints","baseUrl":"https://api.endpoints.anyscale.com/v1","keyHeader":"Authorization","keyPrefix":"Bearer ","extraHeaders":{},"quota":"legacy, deprecated","docsUrl":"https://docs.anyscale.com/endpoints/overview","keyFields":["api_key"],"hint":"app.endpoints.anyscale.com/credentials","logo":"public/logos/anyscale.svg"},
|
|
15
15
|
{"id":"elevenlabs","name":"ElevenLabs","baseUrl":"https://api.elevenlabs.io/v1","keyHeader":"xi-api-key","keyPrefix":"","extraHeaders":{},"quota":"10k credits/mo free","docsUrl":"https://elevenlabs.io/docs","keyFields":["api_key"],"hint":"elevenlabs.io API key","logo":"public/logos/elevenlabs.svg"},
|
|
16
|
-
{"id":"deepgram","name":"Deepgram","baseUrl":"https://api.deepgram.com
|
|
16
|
+
{"id":"deepgram","name":"Deepgram","baseUrl":"https://api.deepgram.com","keyHeader":"Authorization","keyPrefix":"Token ","extraHeaders":{},"quota":"$200 credit on signup","docsUrl":"https://developers.deepgram.com/docs","keyFields":["api_key"],"hint":"Authorization: Token …","logo":"public/logos/deepgram.svg"},
|
|
17
17
|
{"id":"cartesia","name":"Cartesia","baseUrl":"https://api.cartesia.ai","keyHeader":"X-API-Key","keyPrefix":"","extraHeaders":{"Cartesia-Version":"2024-06-10"},"quota":"20,000 free tokens","docsUrl":"https://docs.cartesia.ai","keyFields":["api_key"],"hint":"Paste a Cartesia API key","logo":"public/logos/cartesia.svg"},
|
|
18
18
|
{"id":"tavily","name":"Tavily","baseUrl":"https://api.tavily.com","keyHeader":"Authorization","keyPrefix":"Bearer ","extraHeaders":{},"quota":"1,000 credits/mo free","docsUrl":"https://docs.tavily.com/documentation/api-reference/endpoint/search","keyFields":["api_key"],"hint":"app.tavily.com/home, tvly-…","logo":"public/logos/tavily.svg"},
|
|
19
19
|
{"id":"brave-search","name":"Brave Search","baseUrl":"https://api.search.brave.com/res/v1","keyHeader":"X-Subscription-Token","keyPrefix":"","extraHeaders":{},"quota":"2,000 queries/mo free","docsUrl":"https://brave.com/search/api/","keyFields":["api_key"],"hint":"brave.com/search/api/","logo":"public/logos/brave.svg"},
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
// Playground provider adapters — single source of truth for how the admin
|
|
2
|
+
// Playground talks to each provider through the key-pool proxy.
|
|
3
|
+
//
|
|
4
|
+
// The proxy forwards {path, method, headers?, body?, query?, form?} to
|
|
5
|
+
// baseUrl + path with auth injected server-side, so each adapter only needs:
|
|
6
|
+
// which path to hit, which body shape to send, and where to find the output.
|
|
7
|
+
// Placeholders in templates: <<MODEL>> <<TEXT>> <<QUERY>> <<VOICE>> <<SIZE>>
|
|
8
|
+
// <<ORG>> <<SID>> <<DOMAIN>>. Filled client-side (see playgroundPage).
|
|
9
|
+
//
|
|
10
|
+
// Researched against official docs (2026) in 6 parallel batches; keep this
|
|
11
|
+
// table in sync with data/providers.json — every provider id must appear.
|
|
12
|
+
export const ADAPTERS = {
|
|
13
|
+
groq: {
|
|
14
|
+
kind: "openai-chat", chatPath: "/chat/completions", chatStyle: "openai",
|
|
15
|
+
modelsPath: "/models", modelsStyle: "openai", textPaths: ["choices.0.message.content"],
|
|
16
|
+
vision: true, audioIn: true, image: null, audio: null, video: null, probe: null,
|
|
17
|
+
defaults: { chat: "openai/gpt-oss-20b" },
|
|
18
|
+
notes: "temperature 0 becomes 1e-8; vision model-dependent.",
|
|
19
|
+
},
|
|
20
|
+
openrouter: {
|
|
21
|
+
kind: "openai-chat", chatPath: "/chat/completions", chatStyle: "openai",
|
|
22
|
+
modelsPath: "/models", modelsStyle: "openai", textPaths: ["choices.0.message.content"],
|
|
23
|
+
vision: true, audioIn: false, image: null, audio: null, video: null, probe: null,
|
|
24
|
+
defaults: { chat: "meta-llama/llama-3.3-70b-instruct" },
|
|
25
|
+
notes: "Router passthrough; vision model-dependent.",
|
|
26
|
+
},
|
|
27
|
+
mistral: {
|
|
28
|
+
kind: "openai-chat", chatPath: "/chat/completions", chatStyle: "openai",
|
|
29
|
+
modelsPath: "/models", modelsStyle: "openai", textPaths: ["choices.0.message.content"],
|
|
30
|
+
vision: true, audioIn: false, image: null, audio: null, video: null, probe: null,
|
|
31
|
+
defaults: { chat: "mistral-small-latest" },
|
|
32
|
+
notes: "Vision via image_url on capable models.",
|
|
33
|
+
},
|
|
34
|
+
cohere: {
|
|
35
|
+
kind: "openai-chat", chatPath: "/chat/completions", chatStyle: "openai",
|
|
36
|
+
modelsPath: "/models", modelsStyle: "openai", textPaths: ["choices.0.message.content"],
|
|
37
|
+
vision: false, audioIn: false, image: null, audio: null, video: null, probe: null,
|
|
38
|
+
defaults: { chat: "command-a-plus-05-2026" },
|
|
39
|
+
notes: "Compatibility API; no image_url on compat endpoint.",
|
|
40
|
+
},
|
|
41
|
+
fireworks: {
|
|
42
|
+
kind: "openai-chat", chatPath: "/chat/completions", chatStyle: "openai",
|
|
43
|
+
modelsPath: "/models", modelsStyle: "openai", textPaths: ["choices.0.message.content"],
|
|
44
|
+
vision: true, audioIn: false,
|
|
45
|
+
image: { path: "/images/generations", body: { model: "<<MODEL>>", prompt: "<<TEXT>>", n: 1 } },
|
|
46
|
+
audio: null, video: null, probe: null,
|
|
47
|
+
defaults: { chat: "accounts/fireworks/models/llama-v3p1-8b-instruct", image: "accounts/fireworks/models/flux-1-schnell" },
|
|
48
|
+
notes: "Model ids are full accounts/fireworks/models/... paths.",
|
|
49
|
+
},
|
|
50
|
+
together: {
|
|
51
|
+
kind: "openai-chat", chatPath: "/chat/completions", chatStyle: "openai",
|
|
52
|
+
modelsPath: "/models", modelsStyle: "openai", textPaths: ["choices.0.message.content"],
|
|
53
|
+
vision: true, audioIn: false,
|
|
54
|
+
image: { path: "/images/generations", body: { model: "<<MODEL>>", prompt: "<<TEXT>>", n: 1 } },
|
|
55
|
+
audio: null, video: null, probe: null,
|
|
56
|
+
defaults: { chat: "Qwen/Qwen3.5-9B", image: "FLUX.1-schnell" },
|
|
57
|
+
notes: "Namespaced model ids (org/Model).",
|
|
58
|
+
},
|
|
59
|
+
huggingface: {
|
|
60
|
+
kind: "openai-chat", chatPath: "/chat/completions", chatStyle: "openai",
|
|
61
|
+
modelsPath: "/models", modelsStyle: "openai", textPaths: ["choices.0.message.content"],
|
|
62
|
+
vision: true, audioIn: false,
|
|
63
|
+
image: { path: "/images/generations", body: { model: "<<MODEL>>", prompt: "<<TEXT>>", n: 1 } },
|
|
64
|
+
audio: null, video: null, probe: null,
|
|
65
|
+
defaults: { chat: "openai/gpt-oss-120b", image: "black-forest-labs/FLUX.1-schnell" },
|
|
66
|
+
notes: "Inference Providers router; chat-only + images.",
|
|
67
|
+
},
|
|
68
|
+
openai: {
|
|
69
|
+
kind: "openai-chat", chatPath: "/chat/completions", chatStyle: "openai",
|
|
70
|
+
modelsPath: "/models", modelsStyle: "openai", textPaths: ["choices.0.message.content"],
|
|
71
|
+
vision: true, audioIn: true,
|
|
72
|
+
image: { path: "/images/generations", body: { model: "<<MODEL>>", prompt: "<<TEXT>>", n: 1 } },
|
|
73
|
+
audio: { path: "/audio/speech", body: { model: "<<MODEL>>", input: "<<TEXT>>", voice: "<<VOICE>>" } },
|
|
74
|
+
video: null, probe: null, voiceDefault: "alloy",
|
|
75
|
+
defaults: { chat: "gpt-4o-mini", image: "gpt-image-1", audio: "gpt-4o-mini-tts" },
|
|
76
|
+
notes: "TTS returns binary audio, not JSON.",
|
|
77
|
+
},
|
|
78
|
+
perplexity: {
|
|
79
|
+
kind: "openai-chat", chatPath: "/chat/completions", chatStyle: "openai",
|
|
80
|
+
modelsPath: "/models", modelsStyle: "openai",
|
|
81
|
+
textPaths: ["choices.0.message.content", "citations", "search_results"],
|
|
82
|
+
vision: true, audioIn: false, image: null, audio: null, video: null, probe: null,
|
|
83
|
+
defaults: { chat: "sonar-pro" },
|
|
84
|
+
notes: "/chat/completions aliases the Sonar API.",
|
|
85
|
+
},
|
|
86
|
+
anyscale: {
|
|
87
|
+
kind: "openai-chat", chatPath: "/chat/completions", chatStyle: "openai",
|
|
88
|
+
modelsPath: "/models", modelsStyle: "openai", textPaths: ["choices.0.message.content"],
|
|
89
|
+
vision: false, audioIn: false, image: null, audio: null, video: null, probe: null,
|
|
90
|
+
defaults: { chat: "meta-llama/Llama-3.1-70B-Instruct" },
|
|
91
|
+
notes: "Model ids rotate; auto-list via GET /models.",
|
|
92
|
+
},
|
|
93
|
+
custom: {
|
|
94
|
+
kind: "openai-chat", chatPath: "/chat/completions", chatStyle: "openai",
|
|
95
|
+
modelsPath: "/models", modelsStyle: "openai", textPaths: ["choices.0.message.content"],
|
|
96
|
+
vision: true, audioIn: false, image: null, audio: null, video: null, probe: null,
|
|
97
|
+
defaults: { chat: "gpt-4o-mini" },
|
|
98
|
+
notes: "Generic OpenAI-chat default for user baseUrl.",
|
|
99
|
+
},
|
|
100
|
+
anthropic: {
|
|
101
|
+
kind: "native-chat", chatPath: "/messages", chatStyle: "anthropic",
|
|
102
|
+
modelsPath: null, textPaths: ["content.0.text"],
|
|
103
|
+
vision: true, audioIn: false, image: null, audio: null, video: null, probe: null,
|
|
104
|
+
defaults: { chat: "claude-sonnet-5" },
|
|
105
|
+
notes: "Body {model,max_tokens,messages[,system]}; version header injected server-side.",
|
|
106
|
+
},
|
|
107
|
+
gemini: {
|
|
108
|
+
kind: "native-chat", chatPath: "/models/<<MODEL>>:generateContent", chatStyle: "gemini",
|
|
109
|
+
modelsPath: "/models", modelsStyle: "gemini", textPaths: ["candidates.0.content.parts.0.text"],
|
|
110
|
+
vision: true, audioIn: true, videoIn: true, image: null, audio: null, video: null, probe: null,
|
|
111
|
+
defaults: { chat: "gemini-3.8-flash" },
|
|
112
|
+
notes: "Model embedded in path; key via x-goog-api-key header.",
|
|
113
|
+
},
|
|
114
|
+
replicate: {
|
|
115
|
+
kind: "image", chatPath: null, chatStyle: null,
|
|
116
|
+
modelsPath: null, textPaths: ["output", "output.0"],
|
|
117
|
+
vision: false, audioIn: false,
|
|
118
|
+
image: { path: "/models/black-forest-labs/flux-schnell/predictions", body: { input: { prompt: "<<TEXT>>" } } },
|
|
119
|
+
audio: null, video: null, probe: null,
|
|
120
|
+
defaults: { image: "black-forest-labs/flux-schnell" },
|
|
121
|
+
notes: "Async: POST returns {id,status}; poll GET /predictions/{id} until succeeded.",
|
|
122
|
+
},
|
|
123
|
+
elevenlabs: {
|
|
124
|
+
kind: "tts", chatPath: null, chatStyle: null,
|
|
125
|
+
modelsPath: null, textPaths: [],
|
|
126
|
+
vision: false, audioIn: false, image: null,
|
|
127
|
+
audio: { path: "/text-to-speech/<<VOICE>>", body: { text: "<<TEXT>>", model_id: "<<MODEL>>" } },
|
|
128
|
+
video: null, probe: null, voiceDefault: "JBFqnCBsd6RMkjVDRZzb",
|
|
129
|
+
defaults: { audio: "eleven_multilingual_v2" },
|
|
130
|
+
notes: "Returns binary audio; voice id in path.",
|
|
131
|
+
},
|
|
132
|
+
cartesia: {
|
|
133
|
+
kind: "tts", chatPath: null, chatStyle: null,
|
|
134
|
+
modelsPath: null, textPaths: [],
|
|
135
|
+
vision: false, audioIn: false, image: null,
|
|
136
|
+
audio: { path: "/tts/bytes", body: { model_id: "<<MODEL>>", transcript: "<<TEXT>>", voice: { mode: "id", id: "<<VOICE>>" }, output_format: { container: "mp3", sample_rate: 44100, bit_rate: 128000 } } },
|
|
137
|
+
video: null, probe: null, voiceDefault: "09f6bad8-2340-4eac-89ec-74885c34d189",
|
|
138
|
+
defaults: { audio: "sonic-3.6" },
|
|
139
|
+
notes: "Returns binary audio; Cartesia-Version header injected server-side.",
|
|
140
|
+
},
|
|
141
|
+
deepgram: {
|
|
142
|
+
kind: "tts", chatPath: null, chatStyle: null,
|
|
143
|
+
modelsPath: null, textPaths: ["results.channels.0.alternatives.0.transcript"],
|
|
144
|
+
vision: false, audioIn: true, image: null,
|
|
145
|
+
audio: { path: "/v1/speak", altPath: "/v2/speak", altMatch: "flux", query: { model: "<<MODEL>>" }, body: { text: "<<TEXT>>" } },
|
|
146
|
+
video: null, probe: null,
|
|
147
|
+
defaults: { audio: "aura-2-thalia-en" },
|
|
148
|
+
notes: "TTS model is a query param, returns binary audio. Paths carry the version explicitly because the base URL is the host root (so /v2/speak resolves correctly). Flux-variant models route to /v2/speak, which requires a flux- voice model. STT: POST /v1/listen (not wired).",
|
|
149
|
+
},
|
|
150
|
+
tavily: {
|
|
151
|
+
kind: "search", chatPath: null, chatStyle: null,
|
|
152
|
+
modelsPath: null, textPaths: ["answer", "results.0.content", "results.0.url", "results.0.title"],
|
|
153
|
+
vision: false, audioIn: false, image: null, audio: null, video: null,
|
|
154
|
+
probe: { path: "/search", method: "POST", body: { query: "<<QUERY>>", topic: "<<TOPIC>>", time_range: "<<TIME_RANGE>>", max_results: "<<MAX_RESULTS>>", include_answer: "<<ANSWER>>", chunks_per_source: "<<CHUNKS>>", auto_parameters: "<<AUTO>>", include_domains: "<<INCL_DOMAINS>>", exclude_domains: "<<EXCL_DOMAINS>>", search_depth: "<<DEPTH>>" } },
|
|
155
|
+
defaults: {},
|
|
156
|
+
notes: "Search-only; siblings /extract /crawl /map.",
|
|
157
|
+
},
|
|
158
|
+
"brave-search": {
|
|
159
|
+
kind: "search", chatPath: null, chatStyle: null,
|
|
160
|
+
modelsPath: null, textPaths: ["web.results.0.title", "web.results.0.url", "web.results.0.description"],
|
|
161
|
+
vision: false, audioIn: false, image: null, audio: null, video: null,
|
|
162
|
+
probe: { path: "/web/search", method: "GET", query: { q: "<<QUERY>>", count: "5" } },
|
|
163
|
+
defaults: {},
|
|
164
|
+
notes: "Token header injected server-side.",
|
|
165
|
+
},
|
|
166
|
+
serper: {
|
|
167
|
+
kind: "search", chatPath: null, chatStyle: null,
|
|
168
|
+
modelsPath: null, textPaths: ["organic.0.title", "organic.0.link", "organic.0.snippet"],
|
|
169
|
+
vision: false, audioIn: false, image: null, audio: null, video: null,
|
|
170
|
+
probe: { path: "/search", method: "POST", query: {}, body: { q: "<<QUERY>>" } },
|
|
171
|
+
defaults: {},
|
|
172
|
+
notes: "Response also carries knowledgeGraph/answerBox.",
|
|
173
|
+
},
|
|
174
|
+
scrapingbee: {
|
|
175
|
+
kind: "rest", chatPath: null, chatStyle: null,
|
|
176
|
+
modelsPath: null, textPaths: ["$"],
|
|
177
|
+
vision: false, audioIn: false, image: null, audio: null, video: null,
|
|
178
|
+
probe: { path: "/", method: "GET", query: { url: "<<QUERY>>", render_js: "false" } },
|
|
179
|
+
defaults: {},
|
|
180
|
+
notes: "Returns raw HTML, not JSON; api_key injected server-side as query param.",
|
|
181
|
+
},
|
|
182
|
+
finnhub: {
|
|
183
|
+
kind: "rest", chatPath: null, chatStyle: null,
|
|
184
|
+
modelsPath: null, textPaths: ["c", "pc", "d", "dp"],
|
|
185
|
+
vision: false, audioIn: false, image: null, audio: null, video: null,
|
|
186
|
+
probe: { path: "/quote", method: "GET", query: { symbol: "<<QUERY>>" } },
|
|
187
|
+
defaults: {},
|
|
188
|
+
notes: "Query is a stock symbol, e.g. AAPL.",
|
|
189
|
+
},
|
|
190
|
+
mapbox: {
|
|
191
|
+
kind: "rest", chatPath: null, chatStyle: null,
|
|
192
|
+
modelsPath: null, textPaths: ["features.0.place_name", "features.0.center"],
|
|
193
|
+
vision: false, audioIn: false, image: null, audio: null, video: null,
|
|
194
|
+
probe: { path: "/geocoding/v5/mapbox.places/<<QUERY>>.json", method: "GET", query: { limit: "5" } },
|
|
195
|
+
defaults: {},
|
|
196
|
+
notes: "access_token injected server-side as query param.",
|
|
197
|
+
},
|
|
198
|
+
googlemaps: {
|
|
199
|
+
kind: "rest", chatPath: null, chatStyle: null,
|
|
200
|
+
modelsPath: null, textPaths: ["status", "results.0.formatted_address", "results.0.geometry.location"],
|
|
201
|
+
vision: false, audioIn: false, image: null, audio: null, video: null,
|
|
202
|
+
probe: { path: "/geocode/json", method: "GET", query: { address: "<<QUERY>>" } },
|
|
203
|
+
defaults: {},
|
|
204
|
+
notes: "key injected server-side as query param.",
|
|
205
|
+
},
|
|
206
|
+
resend: {
|
|
207
|
+
kind: "rest", chatPath: null, chatStyle: null,
|
|
208
|
+
modelsPath: null, textPaths: ["id"],
|
|
209
|
+
vision: false, audioIn: false, image: null, audio: null, video: null,
|
|
210
|
+
probe: { path: "/emails", method: "POST", body: { from: "test@example.com", to: ["to@example.com"], subject: "<<TEXT>>", text: "<<TEXT>>" } },
|
|
211
|
+
defaults: {},
|
|
212
|
+
notes: "Sends a real email; 200 returns {id}.",
|
|
213
|
+
},
|
|
214
|
+
sendgrid: {
|
|
215
|
+
kind: "rest", chatPath: null, chatStyle: null,
|
|
216
|
+
modelsPath: null, textPaths: [],
|
|
217
|
+
vision: false, audioIn: false, image: null, audio: null, video: null,
|
|
218
|
+
probe: { path: "/mail/send", method: "POST", body: { personalizations: [{ to: [{ email: "to@example.com" }] }], from: { email: "test@example.com" }, subject: "<<TEXT>>", content: [{ type: "text/plain", value: "<<TEXT>>" }] } },
|
|
219
|
+
defaults: {},
|
|
220
|
+
notes: "Sends a real email; success is 202 with empty body.",
|
|
221
|
+
},
|
|
222
|
+
mailgun: {
|
|
223
|
+
kind: "rest", chatPath: null, chatStyle: null,
|
|
224
|
+
modelsPath: null, textPaths: ["id", "message"],
|
|
225
|
+
vision: false, audioIn: false, image: null, audio: null, video: null, formOnly: true,
|
|
226
|
+
probe: { path: "/<<DOMAIN>>/messages", method: "POST", body: { from: "test@example.com", to: "to@example.com", subject: "<<TEXT>>", text: "<<TEXT>>" } },
|
|
227
|
+
defaults: {},
|
|
228
|
+
notes: "Form-only upstream; proxy sends form. <<DOMAIN>> is your mailgun domain.",
|
|
229
|
+
},
|
|
230
|
+
postmark: {
|
|
231
|
+
kind: "rest", chatPath: null, chatStyle: null,
|
|
232
|
+
modelsPath: null, textPaths: ["MessageID"],
|
|
233
|
+
vision: false, audioIn: false, image: null, audio: null, video: null,
|
|
234
|
+
probe: { path: "/email", method: "POST", body: { From: "test@example.com", To: "to@example.com", Subject: "<<TEXT>>", TextBody: "<<TEXT>>", MessageStream: "outbound" } },
|
|
235
|
+
defaults: {},
|
|
236
|
+
notes: "Sends a real email; 200 returns {MessageID,...}.",
|
|
237
|
+
},
|
|
238
|
+
twilio: {
|
|
239
|
+
kind: "rest", chatPath: null, chatStyle: null,
|
|
240
|
+
modelsPath: null, textPaths: ["sid", "status", "uri"],
|
|
241
|
+
vision: false, audioIn: false, image: null, audio: null, video: null, formOnly: true,
|
|
242
|
+
probe: { path: "/Accounts/<<SID>>/Messages.json", method: "POST", body: { To: "+15558675310", From: "+15557122661", Body: "<<TEXT>>" } },
|
|
243
|
+
defaults: {},
|
|
244
|
+
notes: "Form-only upstream; proxy sends form. <<SID>> is your Account SID.",
|
|
245
|
+
},
|
|
246
|
+
openweather: {
|
|
247
|
+
kind: "search", chatPath: null, chatStyle: null,
|
|
248
|
+
modelsPath: null, textPaths: ["name", "main.temp", "weather"],
|
|
249
|
+
vision: false, audioIn: false, image: null, audio: null, video: null,
|
|
250
|
+
probe: { path: "/weather", method: "GET", query: { q: "<<QUERY>>", units: "metric" } },
|
|
251
|
+
defaults: {},
|
|
252
|
+
notes: "appid injected server-side as query param; query is a city name.",
|
|
253
|
+
},
|
|
254
|
+
supabase: {
|
|
255
|
+
kind: "rest", chatPath: null, chatStyle: null,
|
|
256
|
+
modelsPath: null, textPaths: ["0"],
|
|
257
|
+
vision: false, audioIn: false, image: null, audio: null, video: null,
|
|
258
|
+
probe: { path: "/tablename", method: "GET", query: { select: "*" } },
|
|
259
|
+
defaults: {},
|
|
260
|
+
notes: "PostgREST; replace tablename. Per-project URL resolved per key.",
|
|
261
|
+
},
|
|
262
|
+
neon: {
|
|
263
|
+
kind: "rest", chatPath: null, chatStyle: null,
|
|
264
|
+
modelsPath: null, textPaths: ["projects", "projects.0.id"],
|
|
265
|
+
vision: false, audioIn: false, image: null, audio: null, video: null,
|
|
266
|
+
probe: { path: "/projects", method: "GET", query: {} },
|
|
267
|
+
defaults: {},
|
|
268
|
+
notes: "Response {projects:[...]}.",
|
|
269
|
+
},
|
|
270
|
+
upstash: {
|
|
271
|
+
kind: "rest", chatPath: null, chatStyle: null,
|
|
272
|
+
modelsPath: null, textPaths: ["0", "0.database_id"],
|
|
273
|
+
vision: false, audioIn: false, image: null, audio: null, video: null,
|
|
274
|
+
probe: { path: "/redis/databases", method: "GET", query: {} },
|
|
275
|
+
defaults: {},
|
|
276
|
+
notes: "Bare array of Database objects.",
|
|
277
|
+
},
|
|
278
|
+
turso: {
|
|
279
|
+
kind: "rest", chatPath: null, chatStyle: null,
|
|
280
|
+
modelsPath: null, textPaths: ["databases", "databases.0.Name", "databases.0.DbId"],
|
|
281
|
+
vision: false, audioIn: false, image: null, audio: null, video: null,
|
|
282
|
+
probe: { path: "/organizations/<<ORG>>/databases", method: "GET", query: {} },
|
|
283
|
+
defaults: {},
|
|
284
|
+
notes: "<<ORG>> is your organization slug.",
|
|
285
|
+
},
|
|
286
|
+
appwrite: {
|
|
287
|
+
kind: "rest", chatPath: null, chatStyle: null,
|
|
288
|
+
modelsPath: null, textPaths: ["databases", "databases.0.$id", "total"],
|
|
289
|
+
vision: false, audioIn: false, image: null, audio: null, video: null,
|
|
290
|
+
probe: { path: "/databases", method: "GET", query: {} },
|
|
291
|
+
defaults: {},
|
|
292
|
+
notes: "Project header injected server-side; endpoint resolved per key.",
|
|
293
|
+
},
|
|
294
|
+
cloudflare: {
|
|
295
|
+
kind: "rest", chatPath: null, chatStyle: null,
|
|
296
|
+
modelsPath: null, textPaths: ["result", "result.0.id", "result_info", "success"],
|
|
297
|
+
vision: false, audioIn: false, image: null, audio: null, video: null,
|
|
298
|
+
probe: { path: "/zones", method: "GET", query: { per_page: "5" } },
|
|
299
|
+
defaults: {},
|
|
300
|
+
notes: "Envelope {success,result[],result_info}.",
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
/** Placeholder tokens filled client-side from playground inputs. */
|
|
304
|
+
export const PLACEHOLDERS = ["<<MODEL>>", "<<TEXT>>", "<<QUERY>>", "<<VOICE>>", "<<SIZE>>", "<<ORG>>", "<<SID>>", "<<DOMAIN>>"];
|