Pinme LLM
Use this skill when a PinMe project (Worker TypeScript) needs to call OpenRouter-backed LLM APIs, including models, chat/completions, streaming, or OpenRouter web search. Guides AI to generate correct Worker TS code.
- Skill ID
- glitternetwork/pinme/pinme-llm
- Publisher
- glitternetwork
- Repository
- pinme
- Installs
- 0
- Files
- 1
- Synced
- Sep 23, 2026
Open any RiverX project, open the Skills panel in the chat, and search for this identifier. The files are fetched from the source repository at install time.
glitternetwork/pinme/pinme-llmInstalls these files- SKILL.md
What this skill tells the agent
PinMe Worker OpenRouter API Integration
Guides how to call PinMe platform's OpenRouter proxy APIs in a PinMe Worker (TypeScript). Workers use the PinMe project API key; they never hold the real OpenRouter API key.
Environment Variables
The following environment variables are automatically injected when the Worker is created — no manual configuration needed:
// backend/src/worker.ts
export interface Env {
DB: D1Database;
API_KEY: string; // Project API Key from create_worker
PROJECT_NAME: string; // Actual project_name from create_worker; must match API_KEY
BASE_URL?: string; // Optional override for PinMe API base URL, defaults to https://pinme.cloud
}API_KEYauthenticates the Worker to PinMe.PROJECT_NAMEis required forchat/completionsand must belong to the same project asAPI_KEY. WhenBASE_URLis not set, usehttps://pinme.cloud.
Models API
Endpoint: GET {BASE_URL}/api/v1/models Authentication: X-API-Key header (using env.API_KEY) Request Body: none
Use this when the Worker needs to list available OpenRouter models. The response body, status, and headers are passed through from OpenRouter /models.
async function listModels(env: Env): Promise<unknown> {
const baseUrl = env.BASE_URL ?? 'https://pinme.cloud';
const resp = await fetch(`${baseUrl}/api/v1/models`, {
headers: { 'X-API-Key': env.API_KEY },
});
if (!resp.ok) {
throw new Error(await extractPinmeOpenRouterError(resp));
}
return await resp.json();
}Chat Completions API
Endpoint: POST {BASE_URL}/api/v1/chat/completions?project_name={project_name} Authentication: X-API-Key header (using env.API_KEY) Request Body: OpenRouter chat/completions format, passed through as-is after a 1MB size check Streaming: Supports SSE (stream: true) Web Search: Supports OpenRouter openrouter:web_search server tool via the tools array
Request Format
{
"model": "openai/gpt-4o-mini",
"messages": [
{ "role": "system", "content": "You are a helpful assistant." },
{ "role": "user", "content": "Hello!" }
],
"stream": true
}Useenv.PROJECT_NAMEfromcreate_worker; always URL-encode it in the query string. For available models, callGET /api/v1/modelsor refer to OpenRouter model IDs.
OpenRouter Web Search
PinMe does not provide a raw search endpoint. To search the web, pass OpenRouter's openrouter:web_search server tool to chat/completions; the model decides whether and when to search.
Always set max_results and max_total_results to keep search volume and cost bounded.
async function searchWithLLM(env: Env, query: string): Promise<string> {
const baseUrl = env.BASE_URL ?? 'https://pinme.cloud';
const resp = await fetch(
`${baseUrl}/api/v1/chat/completions?project_name=${encodeURIComponent(env.PROJECT_NAME)}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': env.API_KEY,
},
body: JSON.stringify({
model: 'openai/gpt-5.2',
messages: [{ role: 'user', content: query }],
tools: [
{
type: 'openrouter:web_search',
parameters: {
engine: 'auto',
max_results: 5,
max_total_results: 10,
},
},
],
}),
},
);
if (!resp.ok) {
throw new Error(await extractPinmeOpenRouterError(resp));
}
const data = await resp.json() as { choices: Array<{ message?: { content?: string } }> };
return data.choices[0]?.message?.content ?? '';
}Response Format
Successful requests return OpenRouter's raw response body.
Non-streaming Success (200):
{
"id": "chatcmpl-...",
"choices": [{ "message": { "role": "assistant", "content": "Hello!" }, "finish_reason": "stop" }],
"usage": { "prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15 }
}Streaming Success (200): SSE format
data: {"choices":[{"delta":{"content":"Hello"}}]}
data: {"choices":[{"delta":{"content":" there"}}]}
data: [DONE]Errors:
| HTTP Status | Meaning | data.error Example |
|---|---|---|
| 401 | API Key missing, invalid, or mismatched with project_name | "X-API-Key header is required" / "Invalid API key" / "Invalid API key or project name" |
| 400 | project_name missing or OpenRouter key not configured | "project_name is required" / "LLM service not configured for this project" |
| 403 | LLM balance insufficient or disabled | "Insufficient balance, please recharge to continue using LLM service" |
| 413 | Request body exceeds 1MB | "Request body too large (max 1MB)" |
| 500 | Proxy failed before upstream request | "Failed to build request" |
| 502 | LLM service unavailable | "LLM service unavailable" |
If OpenRouter receives the request and returns a 4xx/5xx, PinMe passes through OpenRouter's status, headers, and response body instead of wrapping it.
Worker Example Code — Non-streaming
async function callLLM(
env: Env,
messages: Array<{ role: string; content: string }>,
model = 'openai/gpt-4o-mini',
): Promise<{ content: string; error?: string }> {
const baseUrl = env.BASE_URL ?? 'https://pinme.cloud';
const resp = await fetch(
`${baseUrl}/api/v1/chat/completions?project_name=${encodeURIComponent(env.PROJECT_NAME)}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': env.API_KEY,
},
body: JSON.stringify({ model, messages }),
},
);
if (!resp.ok) {
return { content: '', error: await extractPinmeOpenRouterError(resp) };
}
const data = await resp.json() as { choices: Array<{ message: { content: string } }> };
return { content: data.choices[0]?.message?.content || '' };
}
// Usage in routes
async function handleChat(request: Request, env: Env): Promise<Response> {
const { question } = await request.json() as { question: string };
const result = await callLLM(env, [
{ role: 'system', content: 'You are a helpful assistant.' },
{ role: 'user', content: question },
]);
if (result.error) {
return json({ error: result.error }, 502);
}
return json({ answer: result.content });
}Worker Example Code — Streaming (SSE Passthrough)
async function handleChatStream(request: Request, env: Env): Promise<Response> {
const body = await request.text();
const baseUrl = env.BASE_URL ?? 'https://pinme.cloud';
// Ensure stream=true in the request
let parsed = JSON.parse(body);
parsed.stream = true;
const resp = await fetch(
`${baseUrl}/api/v1/chat/completions?project_name=${encodeURIComponent(env.PROJECT_NAME)}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': env.API_KEY,
},
body: JSON.stringify(parsed),
},
);
if (!resp.ok) {
return json({ error: await extractPinmeOpenRouterError(resp) }, resp.status);
}
// Pass through SSE stream directly
return new Response(resp.body, {
status: 200,
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
...CORS_HEADERS,
},
});
}Frontend SSE Stream Consumer Example
async function streamChat(question: string, onChunk: (text: string) => void): Promise<void> {
const resp = await fetch(getApiUrl('/api/chat/stream'), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ question }),
});
const reader = resp.body!.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });More from glitternetwork
Use this skill in a project
Open any RiverX project, search the Skills panel for glitternetwork/pinme/pinme-llm, and install it in one click.
