nexus/server/src/services/puter-proxy.ts
Nexus Dev 526acbe8aa feat(31-01): implement puterProxyService, puterProxyRoutes, and unit tests
- puterProxyService with storeToken (create/rotate idempotent), resolveToken, chatStream
- chatStream relays to Puter OpenAI-compat endpoint with SSE streaming
- Cost recording with provider=puter, billingType=subscription_included, costCents=0
- Cost recording skipped when agentId is null/undefined (no FK violation)
- puterProxyRoutes with POST /puter-proxy/token and POST /puter-proxy/chat
- Board auth (assertBoard + assertCompanyAccess) on all routes
- All 10 TDD tests passing
2026-04-04 03:55:49 +00:00

127 lines
3.6 KiB
TypeScript

import type { Db } from "@paperclipai/db";
import { secretService } from "./secrets.js";
import { costService } from "./costs.js";
import { unprocessable } from "../errors.js";
const PUTER_BASE_URL = "https://api.puter.com/puterai/openai/v1";
const PUTER_DEFAULT_MODEL = "claude-3-5-haiku-20241022";
const PUTER_TOKEN_SECRET_NAME = "puter_auth_token";
export function puterProxyService(db: Db) {
const secrets = secretService(db);
const costs = costService(db);
async function storeToken(companyId: string, token: string) {
const existing = await secrets.getByName(companyId, PUTER_TOKEN_SECRET_NAME);
if (existing) {
return secrets.rotate(existing.id, { value: token });
}
return secrets.create(companyId, {
name: PUTER_TOKEN_SECRET_NAME,
provider: "local_encrypted",
value: token,
description: "Puter.com auth token for AI proxy",
});
}
async function resolveToken(companyId: string): Promise<string> {
const secret = await secrets.getByName(companyId, PUTER_TOKEN_SECRET_NAME);
if (!secret) {
throw unprocessable("Puter auth token not configured");
}
return secrets.resolveSecretValue(companyId, secret.id, "latest");
}
async function* chatStream(
companyId: string,
agentId: string | null | undefined,
messages: unknown[],
model: string | undefined,
signal: AbortSignal | undefined,
): AsyncGenerator<string> {
const token = await resolveToken(companyId);
const resolvedModel = model ?? PUTER_DEFAULT_MODEL;
const response = await fetch(`${PUTER_BASE_URL}/chat/completions`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: resolvedModel,
messages,
stream: true,
stream_options: { include_usage: true },
}),
signal,
});
if (!response.ok || !response.body) {
const text = await response.text();
throw new Error(`Puter API error ${response.status}: ${text}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let inputTokens = 0;
let outputTokens = 0;
let buffer = "";
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
// Keep last incomplete line in buffer
buffer = lines.pop() ?? "";
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed.startsWith("data: ")) continue;
const data = trimmed.slice("data: ".length);
if (data === "[DONE]") continue;
let chunk: any;
try {
chunk = JSON.parse(data);
} catch {
continue;
}
if (chunk.usage) {
inputTokens = chunk.usage.prompt_tokens ?? inputTokens;
outputTokens = chunk.usage.completion_tokens ?? outputTokens;
}
const content = chunk.choices?.[0]?.delta?.content;
if (content) {
yield content;
}
}
}
} finally {
reader.releaseLock();
if (agentId) {
costs
.createEvent(companyId, {
agentId,
provider: "puter",
biller: "puter",
billingType: "subscription_included",
model: resolvedModel,
inputTokens,
outputTokens,
costCents: 0,
occurredAt: new Date(),
})
.catch(() => {});
}
}
}
return { storeToken, resolveToken, chatStream };
}