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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,22 @@ Or run in the background process: `beecoder start &`

4. Enter **Ollama** under **Provider Dropdown** and select desired models.

### Pollinations Token
Create a token: [enter.pollinations.ai](https://enter.pollinations.ai/). Then create a file named `.beecoder` in your home directory and paste your token into it.
### Pollinations Login
Authorize BeeCoder to use your Pollinations account:

```sh
cd $HOME
echo "TOKEN" > .beecoder
beecoder login
```

Open the displayed URL, confirm the code, and choose the access you want to
grant. BeeCoder stores the delegated token in `~/.beecoder` with permissions
limited to your user account.

App developers can set a registered public client ID for attribution. It is
optional and is not a secret:

```sh
BEECODER_CLIENT_ID=pk_your_app_key beecoder login
```

### Development
Expand All @@ -50,4 +60,4 @@ npm start
npm run build
npm link
beecoder -v
```
```
23 changes: 21 additions & 2 deletions index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import Program from "./cli/Program.ts";
import { listen } from "./server/Server.ts";
import { accessToken, Logger } from "./utils.ts";
import {
accessToken,
authorizeDevice,
getAccessToken,
Logger
} from "./utils.ts";

import pkg from "./package.json" with { type: "json" };

Expand All @@ -11,6 +16,7 @@ import "./routes/v1.ts";
const prg = new Program();
prg.add("-v", () => Logger.log(pkg.version));
prg.add("--version", () => Logger.log(pkg.version));
prg.add("login", login);
prg.add("start", start);
prg.add("-h", help);
prg.add("--help", help);
Expand All @@ -27,6 +33,7 @@ Usage:
beecoder [flags]

Available Commands:
login Authorize BeeCoder to use your Pollinations account
start Start server

Flags:
Expand All @@ -37,11 +44,23 @@ Flags:
async function start() {
try {
await accessToken();
if (!getAccessToken()) {
throw Error("Not authenticated. Run `beecoder login` first.");
}
await listen();

Logger.log("Listening: http://localhost:11434");
} catch (err) {
Logger.error((err as Error).toString());
process.exit(1);
}
}

async function login() {
try {
await authorizeDevice();
} catch (err) {
Logger.error((err as Error).toString());
process.exit(1);
}
}
2 changes: 1 addition & 1 deletion routes/v1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ router.post("/v1/chat/completions", async (req, res) => {
headers["Authorization"] = `Bearer ${token}`;
}

const aiReq = await fetch("https://text.pollinations.ai/openai", {
const aiReq = await fetch("https://gen.pollinations.ai/v1/chat/completions", {
method: "POST",
body: req.body,
headers
Expand Down
113 changes: 107 additions & 6 deletions utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,24 @@
import { access, chmod, readFile, writeFile } from "node:fs/promises";
import { homedir } from "node:os";
import { join } from "node:path";
import { readFile, access } from "node:fs/promises";

const ENTER_URL = "https://enter.pollinations.ai";
const CONFIG_FILE = join(homedir(), ".beecoder");

interface IDeviceCode {
device_code: string;
user_code: string;
verification_uri: string;
verification_uri_complete?: string;
expires_in: number;
interval: number;
}

interface IDeviceToken {
access_token?: string;
error?: string;
error_description?: string;
}

interface IModel {
model: string;
Expand Down Expand Up @@ -54,15 +72,94 @@ const models: IModel[] = [
];

async function accessToken() {
const cfgFile = join(homedir(), ".beecoder");

try {
await access(cfgFile);
await access(CONFIG_FILE);
} catch {
return;
}

token = await readFile(cfgFile, { encoding: "utf8" });
token = (await readFile(CONFIG_FILE, { encoding: "utf8" })).trim();
}

async function authorizeDevice() {
const clientId = process.env.BEECODER_CLIENT_ID?.trim();
const codeRes = await fetch(`${ENTER_URL}/api/device/code`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(clientId ? { client_id: clientId } : {})
});

if (!codeRes.ok) {
throw Error(
`Could not start login: ${codeRes.status} ${await codeRes.text()}`
);
}

const device = (await codeRes.json()) as IDeviceCode;
if (
!device.device_code ||
!device.user_code ||
!device.verification_uri ||
!Number.isFinite(device.expires_in) ||
!Number.isFinite(device.interval)
) {
throw Error("Pollinations returned an invalid device code response.");
}

const verificationUrl =
device.verification_uri_complete ?? device.verification_uri;

Logger.log(`Open ${verificationUrl}`);
Logger.log(`Enter code: ${device.user_code}`);
Logger.log("Waiting for approval...");

const expiresAt = Date.now() + Math.max(device.expires_in, 1) * 1000;
let pollInterval = Math.max(device.interval, 1) * 1000;

while (Date.now() < expiresAt) {
await sleep(Math.min(pollInterval, expiresAt - Date.now()));
if (Date.now() >= expiresAt) break;

const tokenRes = await fetch(`${ENTER_URL}/api/device/token`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
device_code: device.device_code,
...(clientId && { client_id: clientId })
})
});
const result = (await tokenRes.json()) as IDeviceToken;

if (tokenRes.ok && result.access_token) {
const accessToken = result.access_token.trim();
await writeFile(CONFIG_FILE, accessToken, {
encoding: "utf8",
mode: 0o600
});
await chmod(CONFIG_FILE, 0o600);
token = accessToken;
Logger.log("Authenticated. Token stored in ~/.beecoder.");
return;
}

if (result.error === "authorization_pending") continue;
if (result.error === "slow_down") {
pollInterval += 5000;
continue;
}
if (result.error === "access_denied") {
throw Error("Login denied.");
}
if (result.error === "expired_token") {
throw Error("Login expired. Run `beecoder login` again.");
}

throw Error(
result.error_description ?? result.error ?? "Device login failed."
);
}

throw Error("Login expired. Run `beecoder login` again.");
}

function getAccessToken() {
Expand All @@ -87,4 +184,8 @@ class Logger {
}
}

export { accessToken, getAccessToken, getModel, Logger };
function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}

export { accessToken, authorizeDevice, getAccessToken, getModel, Logger };