大模型应用
在 PinMe Worker (TypeScript)…
适用于 PinMe 项目 Worker TypeScript 需要通过平台 API_KEY 调用 OpenRouter 模型、聊天、流式或联网搜索的场景。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 38/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 55/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 59/100 ×0.15
适用场景
在 PinMe Worker (Type特定
解决思路
- 在 `backend/src/worker.ts` 的 `Env` 接口中声明 Worker 创建时自动注入的变量:`DB: D1Database`、`API_KEY: string`、`PROJECT_NAME: string`、`BASE_URL?: string`,无需手动配置。
- 使用 `env.API_KEY`(来自 `create_worker` 的项目 API Key)通过 `X-API-Key` 头认证 Worker 到 PinMe;Worker 不持有真实 OpenRouter API Key。
- 对 `chat/completions` 必须传 `env.PROJECT_NAME`(来自 `create_worker` 的 `project_name`),且该值必须与 `API_KEY` 属于同一项目。
- 当 `env.BASE_URL` 未设置时,PinMe API base URL 使用 `https://pinme.cloud`;仅需要覆盖时才设置 `BASE_URL`。
- 调用模型列表:`GET {BASE_URL}/api/v1/models`,无请求体,使用 `X-API-Key` 头并传入 `env.API_KEY`。
- 模型列表响应体、状态和 headers 由 OpenRouter `/models` 透传;成功时解析该原始响应。
- 模型列表代码:计算 `baseUrl = env.BASE_URL ?? 'https://pinme.cloud'`,执行 `fetch`,若 `!resp.ok` 则 `throw new Error(await extractPinmeOpenRouterError(resp))`,否则 `return await resp.json()`。
- 调用聊天补全:`POST {BASE_URL}/api/v1/chat/completions?project_name={project_name}`,使用 `X-API-Key` 头并传入 `env.API_KEY`。
- 聊天补全请求体采用 OpenRouter chat/completions 格式;PinMe 在 1MB 大小检查后原样透传。
- 请求体 JSON 使用 `model`、`messages` 数组(每项含 `role`、`content`)、可选 `stream: true`;可用模型通过 `GET /api/v1/models` 获取或参考 OpenRouter model ID。
- 生成 `project_name` 查询参数时使用 `encodeURIComponent(env.PROJECT_NAME)` 进行 URL 编码。
- 流式响应:支持 SSE,设置 `stream: true`。
- Web 搜索:PinMe 不提供原始搜索端点;在 `chat/completions` 的 `tools` 数组中传 OpenRouter 的 `openrouter:web_search` server tool,由模型决定是否、何时搜索。
- Web 搜索必须设置 `max_results` 和 `max_total_results`,以限制搜索量和成本。
- Web 搜索代码:向聊天补全端点 `fetch`,headers 包含 `Content-Type: application/json` 与 `X-API-Key`,body 包含 `model: 'openai/gpt-5.2'`、`messages`、`tools: [{ type: 'openrouter:web_search', parameters: { engine: 'auto', max_results: 5, max_total_results: 10 } }]`;失败时用 `extractPinmeOpenRouterError`,成功时取 `data.choices[0]?.message?.content ?? ''`。
- 非流式成功(200)返回 OpenRouter 原始响应体,包含 `id`、`choices[].message`、`usage` 等字段。
- 流式成功(200)返回 SSE 格式:`data: { choices: [{ delta: { content: '...' } }] }`,最后以 `data: [DONE]` 结束。
- 处理 401:API Key 缺失、无效或与 `project_name` 不匹配,data.error 可能为 `X-API-Key header is required`、`Invalid API key`、`Invalid API key or project name`。
- 处理 400:`project_name` 缺失或项目未配置 OpenRouter key,data.error 可能为 `project_name is required`、`LLM service not configured for this project`。
- 处理 403:LLM 余额不足或被禁用,data.error 可能为 `Insufficient balance, please recharge to continue using LLM service`。
- 处理 413:请求体超过 1MB,data.error 为 `Request body too large (max 1MB)`。
- 处理 500:代理在上游请求前失败,data.error 为 `Failed to build request`。
- 处理 502:LLM 服务不可用,data.error 为 `LLM service unavailable`。
- 如果 OpenRouter 已收到请求并返回 4xx/5xx,PinMe 透传 OpenRouter 的 status、headers 和响应体,不包装。
- 非流式 Worker 示例:实现 `callLLM(env, messages, model = 'openai/gpt-4o-mini')`,`fetch` 聊天补全端点,headers 含 `Content-Type` 与 `X-API-Key`,失败返回 `{ content: '', error: await extractPinmeOpenRouterError(resp) }`,成功返回 `data.choices[0]?.message?.content || ''`。
- 在路由 `handleChat` 中读取 `request.json()` 的 `question`,调用 `callLLM` 并传入 system 与 user 消息;若有 error 则 `json({ error }, 502)`,否则 `json({ answer: result.content })`。
- 流式 Worker 示例:`handleChatStream` 读取 `request.text()`,`JSON.parse` 后强制 `parsed.stream = true`,`fetch` 聊天补全端点;失败返回 `json({ error: await extractPinmeOpenRouterError(resp) }, resp.status)`。
- 流式成功时将 `resp.body` 直接作为 `new Response` 返回,status 为 200,headers 设置 `Content-Type: text/event-stream`、`Cache-Control: no-cache`、`Connection: keep-alive` 和 `CORS_HEADERS`。
- 前端 SSE 消费示例:`streamChat` 使用 `fetch(getApiUrl('/api/chat/stream'), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ question }) })`。
- 前端读取 `resp.body!.getReader()`,用 `TextDecoder` 以 `{ stream: true }` 解码,按换行符分行,并用 `buffer = lines.pop()!` 保留不完整行。
- 前端逐行处理:忽略不以 `data: ` 开头的行;`payload = line.slice(6)`;若 `payload === '[DONE]'` 则结束;否则 `JSON.parse(payload)` 后取 `chunk.choices[0]?.delta?.content`,有内容则调用 `onChunk(content)`。
- PinMe 包装错误格式:`PinmeResponse<T> { code: number; msg: string; data?: T; }`,`code` 为 200 表示成功,其他表示失败;`data` 失败时可能含 `{ error: string }`。
- 对 `/api/v1/models` 和 `/api/v1/chat/completions`,成功响应是原始 OpenRouter 响应;只有 OpenRouter 请求之前的代理失败才使用 PinMe 包装错误格式。
- 实现 `extractPinmeOpenRouterError(resp: Response): Promise<string>`:fallback 为 `HTTP ${resp.status}`;`resp.clone().json()` 后依次检查 `data.error`、`msg`、`error` 字符串或 `error.message`;catch 后尝试 `resp.text()`;最后返回 fallback。
- 可选 JSON helper:实现 `callOpenRouterJSON<T>(url, apiKey, body)`,try `fetch` POST,headers 含 `Content-Type: application/json` 与 `X-API-Key: apiKey`,body 为 `JSON.stringify(body)`;catch 返回 `{ error: 'Network error' }`;`!resp.ok` 返回 `{ error: await extractPinmeOpenRouterError(resp) }`;成功返回 `{ data: await resp.json() as T }`。
- 使用示例:`baseUrl = env.BASE_URL ?? 'https://pinme.cloud'`;调用 `callOpenRouterJSON` 请求 `${baseUrl}/api/v1/chat/completions?project_name=${encodeURIComponent(env.PROJECT_NAME)}`,body 为 `{ model: 'openai/gpt-4o-mini', messages: [{ role: 'user', content: 'Hi' }] }`;若返回 error,则 `json({ error }, 502)`。
适用边界 · 注意事项
- 不适用于非 PinMe Worker、没有 PinMe 项目 API_KEY / PROJECT_NAME 或没有配置 OpenRouter LLM 服务的环境。
- 不要尝试在 PinMe API 中寻找原始 Web 搜索端点;联网搜索必须通过 `chat/completions` 的 `openrouter:web_search` tool。
- 请求体超过 1MB、余额不足或 API_KEY 与 PROJECT_NAME 不匹配时无法正常调用。