llm-simple-router 0.9.15 → 0.9.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,4 @@
1
+ export declare const HTTP_OK = 200;
1
2
  export declare const HTTP_BAD_REQUEST = 400;
2
3
  export declare const HTTP_CREATED = 201;
3
4
  export declare const HTTP_FORBIDDEN = 403;
@@ -1,5 +1,6 @@
1
1
  // src/core/constants.ts
2
2
  // HTTP 状态码常量 — 全局唯一来源
3
+ export const HTTP_OK = 200;
3
4
  export const HTTP_BAD_REQUEST = 400;
4
5
  export const HTTP_CREATED = 201;
5
6
  export const HTTP_FORBIDDEN = 403;
@@ -1,12 +1,10 @@
1
1
  import { randomUUID } from "crypto";
2
2
  import fp from "fastify-plugin";
3
- import { getActiveProviders, insertRequestLog } from "../../db/index.js";
4
- import { getSetting } from "../../db/settings.js";
5
- import { decrypt } from "../../utils/crypto.js";
6
- import { proxyGetRequest, createErrorFormatter } from "../proxy-core.js";
3
+ import { getAllProviders, insertRequestLog } from "../../db/index.js";
4
+ import { createErrorFormatter } from "../proxy-core.js";
7
5
  import { handleProxyRequest } from "./proxy-handler.js";
8
6
  import { createOrchestrator } from "../orchestration/orchestrator.js";
9
- import { HTTP_NOT_FOUND, HTTP_BAD_GATEWAY } from "../../core/constants.js";
7
+ import { HTTP_OK, HTTP_BAD_GATEWAY, MS_PER_SECOND } from "../../core/constants.js";
10
8
  import { SERVICE_KEYS } from "../../core/container.js";
11
9
  const CHAT_COMPLETIONS_PATH = "/v1/chat/completions";
12
10
  const MODELS_PATH = "/v1/models";
@@ -55,54 +53,75 @@ const openaiProxyRaw = (app, opts, done) => {
55
53
  // 规范路径 + 兼容路径(不带 /v1 前缀)
56
54
  app.post(CHAT_COMPLETIONS_PATH, handleChatCompletions);
57
55
  app.post(CHAT_COMPLETIONS_COMPAT_PATH, handleChatCompletions);
56
+ const ANTHROPIC_DEFAULT_PAGE_SIZE = 20;
57
+ const ANTHROPIC_MAX_PAGE_SIZE = 1000;
58
58
  const handleModels = async (request, reply) => {
59
- const startTime = Date.now();
60
- const providers = getActiveProviders(db, "openai");
61
- if (providers.length === 0) {
62
- insertRequestLog(db, {
63
- id: randomUUID(), api_type: "openai", model: null,
64
- provider_id: null, status_code: HTTP_NOT_FOUND, latency_ms: Date.now() - startTime, is_stream: 0,
65
- error_message: "No active OpenAI provider configured",
66
- created_at: new Date().toISOString(),
67
- client_request: JSON.stringify({ headers: request.headers }),
68
- router_key_id: request.routerKey?.id ?? null,
69
- });
70
- return sendError(reply, {
71
- statusCode: HTTP_NOT_FOUND,
72
- body: { error: { message: "No active OpenAI provider configured", type: "invalid_request_error", code: "no_provider" } },
73
- });
74
- }
75
- const provider = providers[0];
76
- const apiKey = decrypt(provider.api_key, getSetting(db, "encryption_key"));
77
- const cliHdrs = request.headers;
78
- try {
79
- const result = await proxyGetRequest(provider, apiKey, cliHdrs, MODELS_PATH);
80
- insertRequestLog(db, {
81
- id: randomUUID(), api_type: "openai", model: null,
82
- provider_id: provider.id, status_code: result.statusCode, latency_ms: Date.now() - startTime, is_stream: 0,
83
- error_message: null, created_at: new Date().toISOString(),
84
- client_request: JSON.stringify({ headers: request.headers }),
85
- router_key_id: request.routerKey?.id ?? null,
86
- });
87
- for (const [k, v] of Object.entries(result.headers))
88
- reply.header(k, v);
89
- return reply.code(result.statusCode).send(result.body);
59
+ // 聚合所有活跃 provider 的模型列表
60
+ const allProviders = getAllProviders(db).filter(p => p.is_active);
61
+ const modelMeta = new Map();
62
+ for (const p of allProviders) {
63
+ try {
64
+ const models = JSON.parse(p.models || '[]');
65
+ for (const m of models) {
66
+ if (!modelMeta.has(m))
67
+ modelMeta.set(m, { providerName: p.name, createdAt: p.created_at });
68
+ }
69
+ }
70
+ catch {
71
+ // providers.models 有 NOT NULL 约束默认 '[]',此处防御开发期误配的非法 JSON
72
+ continue;
73
+ }
90
74
  }
91
- catch (err) {
92
- insertRequestLog(db, {
93
- id: randomUUID(), api_type: "openai", model: null,
94
- provider_id: provider.id, status_code: HTTP_BAD_GATEWAY, latency_ms: Date.now() - startTime, is_stream: 0,
95
- error_message: err instanceof Error ? err.message : String(err),
96
- created_at: new Date().toISOString(),
97
- client_request: JSON.stringify({ headers: request.headers }),
98
- router_key_id: request.routerKey?.id ?? null,
99
- });
100
- request.log.error({ err: err instanceof Error ? err.message : String(err) }, "Failed to reach OpenAI backend for /v1/models");
101
- return sendError(reply, {
102
- statusCode: HTTP_BAD_GATEWAY,
103
- body: { error: { message: "Failed to reach backend service", type: "server_error", code: "upstream_error" } },
75
+ const sortedIds = [...modelMeta.keys()].sort();
76
+ // 根据请求头判断响应格式:Anthropic 客户端发送 anthropic-version 头
77
+ const isAnthropicFormat = !!request.headers['anthropic-version'];
78
+ if (isAnthropicFormat) {
79
+ // Anthropic 格式: { data: [...], has_more, first_id, last_id }
80
+ const query = request.query;
81
+ const limit = Math.min(Math.max(parseInt(query.limit || String(ANTHROPIC_DEFAULT_PAGE_SIZE), 10) || ANTHROPIC_DEFAULT_PAGE_SIZE, 1), ANTHROPIC_MAX_PAGE_SIZE);
82
+ let sliced;
83
+ let hasMore;
84
+ if (query.after_id) {
85
+ const idx = sortedIds.indexOf(query.after_id);
86
+ const start = idx !== -1 ? idx + 1 : 0;
87
+ sliced = sortedIds.slice(start, start + limit);
88
+ hasMore = start + limit < sortedIds.length;
89
+ }
90
+ else if (query.before_id) {
91
+ const endIdx = sortedIds.indexOf(query.before_id);
92
+ const end = endIdx !== -1 ? endIdx : sortedIds.length;
93
+ const start = Math.max(0, end - limit);
94
+ sliced = sortedIds.slice(start, end);
95
+ hasMore = start > 0;
96
+ }
97
+ else {
98
+ sliced = sortedIds.slice(0, limit);
99
+ hasMore = limit < sortedIds.length;
100
+ }
101
+ const data = sliced.map(id => ({
102
+ type: 'model',
103
+ id,
104
+ display_name: id,
105
+ created_at: modelMeta.get(id).createdAt,
106
+ }));
107
+ return reply.code(HTTP_OK).send({
108
+ data,
109
+ has_more: hasMore,
110
+ first_id: data.length > 0 ? data[0].id : null,
111
+ last_id: data.length > 0 ? data[data.length - 1].id : null,
104
112
  });
105
113
  }
114
+ // OpenAI 格式: { object: "list", data: [...] }
115
+ const data = sortedIds.map(id => ({
116
+ id,
117
+ object: 'model',
118
+ created: Math.floor(new Date(modelMeta.get(id).createdAt).getTime() / MS_PER_SECOND),
119
+ owned_by: modelMeta.get(id).providerName,
120
+ }));
121
+ return reply.code(HTTP_OK).send({
122
+ object: 'list',
123
+ data,
124
+ });
106
125
  };
107
126
  // 规范路径 + 兼容路径
108
127
  app.get(MODELS_PATH, handleModels);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "llm-simple-router",
3
- "version": "0.9.15",
3
+ "version": "0.9.16",
4
4
  "description": "LLM API proxy router with OpenAI/Anthropic support, model mapping, retry strategies, and admin dashboard",
5
5
  "license": "MIT",
6
6
  "author": "ZZzzswszzZZ",