mcpField
P
pinme-llm

pinme-llm

github.com/glitternetwork开发3.8k500 次安装15 周前更新
代码仓库

关于 pinme-llm

这份 TypeScript 指南介绍如何在 PinMe Worker 中调用 PinMe 的 OpenRouter 代理 API,说明模型列表、聊天补全、流式响应、网页搜索和错误处理;Worker 无需持有真实 OpenRouter 密钥。

主要功能

  1. 01列出可用的 OpenRouter 模型
  2. 02调用聊天补全 API
  3. 03通过 SSE 转发流式响应
  4. 04使用 OpenRouter 网页搜索工具
  5. 05处理代理及上游错误

适用场景

  1. 01构建基于 Worker 的聊天端点
  2. 02向前端流式传递模型响应
  3. 03为聊天流程添加模型驱动的网页搜索

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/glitternetwork/pinme --skill pinme-llm

安装后在 Claude Code 中输入/pinme-llm确认已加载。

详细介绍

PinMe Worker OpenRouter API 集成

介绍如何在 PinMe Worker(TypeScript)中调用 PinMe 平台的 OpenRouter 代理 API。Worker 使用 PinMe 项目 API 密钥;它们不会持有真实的 OpenRouter API 密钥。

环境变量

创建 Worker 时会自动注入以下环境变量,无需手动配置:

// 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_KEY 用于对 PinMe 验证 Worker 身份。chat/completions 需要 PROJECT_NAME,且该项目必须与 API_KEY 所属项目相同。未设置 BASE_URL 时,请使用 https://pinme.cloud。


Models API

端点: GET {BASE_URL}/api/v1/models 身份验证: X-API-Key 请求头(使用 env.API_KEY) 请求正文: 无

当 Worker 需要列出可用的 OpenRouter 模型时,请使用此接口。响应正文、状态码和请求头会从 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

端点: POST {BASE_URL}/api/v1/chat/completions?project_name={project_name} 身份验证: X-API-Key 请求头(使用 env.API_KEY) 请求正文: OpenRouter chat/completions 格式,经 1MB 大小检查后原样传递 流式传输: 支持 SSE(stream: true) 网页搜索: 通过 tools 数组支持 OpenRouter openrouter:web_search 服务端工具

请求格式
{
  "model": "openai/gpt-4o-mini",
  "messages": [
    { "role": "system", "content": "You are a helpful assistant." },
    { "role": "user", "content": "Hello!" }
  ],
  "stream": true
}

使用 create_worker 中的 env.PROJECT_NAME;务必在查询字符串中对其进行 URL 编码。要获取可用模型,请调用 GET /api/v1/models,或参考 OpenRouter 模型 ID。

OpenRouter 网页搜索

PinMe 不提供原始搜索端点。要搜索网页,请将 OpenRouter 的 openrouter:web_search 服务端工具传递给 chat/completions;由模型决定是否以及何时搜索。

请务必设置 max_results 和 max_total_results,以限制搜索量和成本。

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 ?? '';
}
响应格式

成功请求会返回 OpenRouter 的原始响应正文。

非流式成功(200):

{
  "id": "chatcmpl-...",
  "choices": [{ "message": { "role": "assistant", "content": "Hello!" }, "finish_reason": "stop" }],
  "usage": { "prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15 }
}

流式成功(200): SSE 格式

data: {"choices":[{"delta":{"content":"Hello"}}]}
data: {"choices":[{"delta":{"content":" there"}}]}
data: [DONE]

错误:

HTTP StatusMeaningdata.error Example
401API 密钥缺失、无效,或与 project_name 不匹配"X-API-Key header is required" / "Invalid API key" / "Invalid API key or project name"
400缺少 project_name,或未配置 OpenRouter 密钥"project_name is required" / "LLM service not configured for this project"
403大模型余额不足或服务已禁用"Insufficient balance, please recharge to continue using LLM service"
413请求正文超过 1MB"Request body too large (max 1MB)"
500上游请求发出前代理失败"Failed to build request"
502大模型服务不可用"LLM service unavailable"

如果 OpenRouter 收到请求并返回 4xx/5xx,PinMe 会直接传递 OpenRouter 的状态码、请求头和响应正文,而不会对其进行包装。

Worker 示例代码 — 非流式
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 示例代码 — 流式(SSE 透传)
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,
    },
  });
}
前端 SSE 流式数据使用示例
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 });
    const lines = buffer.split('\n');
    buffer = lines.pop()!; // Keep incomplete line

    for (const line of lines) {
      if (!line.startsWith('data: ')) continue;
      const payload = line.slice(6);
      if (payload === '[DONE]') return;

      const chunk = JSON.parse(payload) as { choices: Array<{ delta: { content?: string } }> };
      const content = chunk.choices[0]?.delta?.content;
      if (content) onChunk(content);
    }
  }
}

错误处理模式

对于 /api/v1/models 和 /api/v1/chat/completions,成功响应是 OpenRouter 的原始响应。发出 OpenRouter 请求前发生的代理错误会使用 PinMe 封装的错误格式:

interface PinmeResponse<T = unknown> {
  code: number;   // 200=success, other=failure
  msg: string;    // "ok" | "error" | "invalid params"
  data?: T;       // Business data on success, may contain { error: string } on failure
}
推荐的错误提取器
async function extractPinmeOpenRouterError(resp: Response): Promise<string> {
  const fallback = `HTTP ${resp.status}`;
  try {
    const body = await resp.clone().json() as PinmeResponse | { error?: { message?: string } } | { error?: string };
    if ('data' in body && body.data && typeof body.data === 'object' && 'error' in body.data) {
      return String((body.data as { error: unknown }).error);
    }
    if ('msg' in body && typeof body.msg === 'string' && body.msg) {
      return body.msg;
    }
    if ('error' in body) {
      const error = body.error;
      if (typeof error === 'string') return error;
      if (error && typeof error === 'object' && 'message' in error) {
        return String((error as { message: unknown }).message);
      }
    }
  } catch {
    try {
      const text = await resp.text();
      if (text) return text;
    } catch {
      // Ignore and return fallback below.
    }
  }
  return fallback;
}
可选的 JSON 辅助函数

对非流式 POST 调用使用此辅助函数。成功时,它会返回 OpenRouter 的原始 JSON。

async function callOpenRouterJSON<T>(url: string, apiKey: string, body: unknown): Promise<{ data?: T; error?: string }> {
  let resp: Response;
  try {
    resp = await fetch(url, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', 'X-API-Key': apiKey },
      body: JSON.stringify(body),
    });
  } catch {
    return { error: 'Network error' };
  }

  if (!resp.ok) {
    return { error: await extractPinmeOpenRouterError(resp) };
  }

  return { data: await resp.json() as T };
}
使用示例
const baseUrl = env.BASE_URL ?? 'https://pinme.cloud';

// Call LLM (non-streaming)
const llmResult = await callOpenRouterJSON<{ choices: Array<{ message: { content: string } }> }>(
  `${baseUrl}/api/v1/chat/completions?project_name=${encodeURIComponent(env.PROJECT_NAME)}`, env.API_KEY,
  { model: 'openai/gpt-4o-mini', messages: [{ role: 'user', content: 'Hi' }] },
);
if (llmResult.error) return json({ error: llmResult.error }, 502);

评论

支持 Markdown