$npx -y skills add glitternetwork/pinme --skill pinme-llmUse 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.
| 1 | # PinMe Worker OpenRouter API Integration |
| 2 | |
| 3 | 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. |
| 4 | |
| 5 | ## Environment Variables |
| 6 | |
| 7 | The following environment variables are automatically injected when the Worker is created — no manual configuration needed: |
| 8 | |
| 9 | ```typescript |
| 10 | // backend/src/worker.ts |
| 11 | export interface Env { |
| 12 | DB: D1Database; |
| 13 | API_KEY: string; // Project API Key from create_worker |
| 14 | PROJECT_NAME: string; // Actual project_name from create_worker; must match API_KEY |
| 15 | BASE_URL?: string; // Optional override for PinMe API base URL, defaults to https://pinme.cloud |
| 16 | } |
| 17 | ``` |
| 18 | |
| 19 | > `API_KEY` authenticates the Worker to PinMe. `PROJECT_NAME` is required for `chat/completions` and must belong to the same project as `API_KEY`. When `BASE_URL` is not set, use `https://pinme.cloud`. |
| 20 | |
| 21 | --- |
| 22 | |
| 23 | ## Models API |
| 24 | |
| 25 | **Endpoint:** `GET {BASE_URL}/api/v1/models` |
| 26 | **Authentication:** `X-API-Key` header (using `env.API_KEY`) |
| 27 | **Request Body:** none |
| 28 | |
| 29 | Use this when the Worker needs to list available OpenRouter models. The response body, status, and headers are passed through from OpenRouter `/models`. |
| 30 | |
| 31 | ```typescript |
| 32 | async function listModels(env: Env): Promise<unknown> { |
| 33 | const baseUrl = env.BASE_URL ?? 'https://pinme.cloud'; |
| 34 | const resp = await fetch(`${baseUrl}/api/v1/models`, { |
| 35 | headers: { 'X-API-Key': env.API_KEY }, |
| 36 | }); |
| 37 | |
| 38 | if (!resp.ok) { |
| 39 | throw new Error(await extractPinmeOpenRouterError(resp)); |
| 40 | } |
| 41 | |
| 42 | return await resp.json(); |
| 43 | } |
| 44 | ``` |
| 45 | |
| 46 | --- |
| 47 | |
| 48 | ## Chat Completions API |
| 49 | |
| 50 | **Endpoint:** `POST {BASE_URL}/api/v1/chat/completions?project_name={project_name}` |
| 51 | **Authentication:** `X-API-Key` header (using `env.API_KEY`) |
| 52 | **Request Body:** OpenRouter chat/completions format, passed through as-is after a 1MB size check |
| 53 | **Streaming:** Supports SSE (`stream: true`) |
| 54 | **Web Search:** Supports OpenRouter `openrouter:web_search` server tool via the `tools` array |
| 55 | |
| 56 | ### Request Format |
| 57 | |
| 58 | ```json |
| 59 | { |
| 60 | "model": "openai/gpt-4o-mini", |
| 61 | "messages": [ |
| 62 | { "role": "system", "content": "You are a helpful assistant." }, |
| 63 | { "role": "user", "content": "Hello!" } |
| 64 | ], |
| 65 | "stream": true |
| 66 | } |
| 67 | ``` |
| 68 | |
| 69 | > Use `env.PROJECT_NAME` from `create_worker`; always URL-encode it in the query string. For available models, call `GET /api/v1/models` or refer to OpenRouter model IDs. |
| 70 | |
| 71 | ### OpenRouter Web Search |
| 72 | |
| 73 | 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. |
| 74 | |
| 75 | Always set `max_results` and `max_total_results` to keep search volume and cost bounded. |
| 76 | |
| 77 | ```typescript |
| 78 | async function searchWithLLM(env: Env, query: string): Promise<string> { |
| 79 | const baseUrl = env.BASE_URL ?? 'https://pinme.cloud'; |
| 80 | const resp = await fetch( |
| 81 | `${baseUrl}/api/v1/chat/completions?project_name=${encodeURIComponent(env.PROJECT_NAME)}`, |
| 82 | { |
| 83 | method: 'POST', |
| 84 | headers: { |
| 85 | 'Content-Type': 'application/json', |
| 86 | 'X-API-Key': env.API_KEY, |
| 87 | }, |
| 88 | body: JSON.stringify({ |
| 89 | model: 'openai/gpt-5.2', |
| 90 | messages: [{ role: 'user', content: query }], |
| 91 | tools: [ |
| 92 | { |
| 93 | type: 'openrouter:web_search', |
| 94 | parameters: { |
| 95 | engine: 'auto', |
| 96 | max_results: 5, |
| 97 | max_total_results: 10, |
| 98 | }, |
| 99 | }, |
| 100 | ], |
| 101 | }), |
| 102 | }, |
| 103 | ); |
| 104 | |
| 105 | if (!resp.ok) { |
| 106 | throw new Error(await extractPinmeOpenRouterError(resp)); |
| 107 | } |
| 108 | |
| 109 | const data = await resp.json() as { choices: Array<{ message?: { content?: string } }> }; |
| 110 | return data.choices[0]?.message?.content ?? ''; |
| 111 | } |
| 112 | ``` |
| 113 | |
| 114 | ### Response Format |
| 115 | |
| 116 | Successful requests return OpenRouter's raw response body. |
| 117 | |
| 118 | **Non-streaming Success (200):** |
| 119 | ```json |
| 120 | { |
| 121 | "id": "chatcmpl-...", |
| 122 | "choices": [{ "message": { "role": "assistant", "content": "Hello!" }, "finish_reason": "stop" }], |
| 123 | "usage": { "prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15 } |
| 124 | } |
| 125 | ``` |
| 126 | |
| 127 | **Streaming Success (200):** SSE format |
| 128 | ``` |
| 129 | data: {"choices":[{"delta":{"content":"Hello"}}]} |
| 130 | data: {"choices":[{"delta":{"content":" there"}}]} |
| 131 | data: [DONE] |
| 132 | ``` |
| 133 | |
| 134 | **Errors:** |
| 135 | |
| 136 | | HTTP Status | Meaning | data.error Example | |
| 137 | |-------------|---------|-------------------| |
| 138 | | 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"` | |
| 139 | | 400 | project_name missing or OpenRouter key not configured | `"project_name is required"` / `"LLM service not configured for this project"` | |
| 140 | | 403 | LLM balance insufficient or disabled | `"Insufficient balance, please recharge to continue using LLM service"` | |
| 141 | | 413 | Request body exce |