feat: 添加 /v1/models 端点支持

- 新增 ModelsEndpoint 类,返回当前 token 可用的所有模型列表
- 兼容 OpenAI API 格式
- 支持 Authorization 和 x-api-key 两种认证方式

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
dreamhunter2333andClaude Opus 4.5 committed 2026-01-30 23:32:48 +08:00
1 parent 93d404af5e
commit 59149af970
2 files changed
+132

No files matched your search

+4
View File
@@ -282,3 +282,7 @@ class ResponsesProxyEndpoint extends OpenAPIRoute {
}
api.post("/v1/responses", ResponsesProxyEndpoint)
// Models endpoint
import { ModelsEndpoint } from "./models"
api.get("/v1/models", ModelsEndpoint)
+128
View File
@@ -0,0 +1,128 @@
import { Context } from "hono";
import { OpenAPIRoute } from "chanfana";
import { z } from "zod";
const getApiKeyFromHeaders = (c: Context<HonoCustomType>): string | null => {
const authHeader = c.req.raw.headers.get('Authorization');
const xApiKey = c.req.raw.headers.get('x-api-key');
if (authHeader) {
return authHeader.replace("Bearer ", "").trim();
}
if (xApiKey) {
return xApiKey.trim();
}
return null;
}
const fetchTokenData = async (c: Context<HonoCustomType>, apiKey: string) => {
const tokenResult = await c.env.DB.prepare(
`SELECT * FROM api_token WHERE key = ?`
).bind(apiKey).first();
if (!tokenResult || !tokenResult.value) {
return null;
}
return {
tokenData: JSON.parse(tokenResult.value as string) as ApiTokenData,
usage: tokenResult.usage as number || 0,
};
}
const fetchChannelsForToken = async (
c: Context<HonoCustomType>,
tokenData: ApiTokenData
) => {
const channelKeys = tokenData.channel_keys;
if (!channelKeys || channelKeys.length === 0) {
return await c.env.DB.prepare(
`SELECT key, value FROM channel_config`
).all<ChannelConfigRow>();
}
const channelQuery = channelKeys.map(() => '?').join(',');
return await c.env.DB.prepare(
`SELECT key, value FROM channel_config WHERE key IN (${channelQuery})`
).bind(...channelKeys).all<ChannelConfigRow>();
}
export class ModelsEndpoint extends OpenAPIRoute {
schema = {
tags: ['OpenAI Proxy'],
summary: 'List available models',
request: {
headers: z.object({
'Authorization': z.string().optional().describe("Token for authentication (OpenAI format)"),
'x-api-key': z.string().optional().describe("API key for authentication (Claude format)"),
}),
},
responses: {
200: {
description: 'List of available models',
content: {
'application/json': {
schema: z.object({
object: z.string(),
data: z.array(z.object({
id: z.string(),
object: z.string(),
created: z.number(),
owned_by: z.string(),
})),
}),
},
},
},
},
};
async handle(c: Context<HonoCustomType>) {
const apiKey = getApiKeyFromHeaders(c);
if (!apiKey) {
return c.text("Authorization header or x-api-key not found", 401);
}
const tokenInfo = await fetchTokenData(c, apiKey);
if (!tokenInfo) {
return c.text("Invalid API key", 401);
}
const { tokenData } = tokenInfo;
const channelsResult = await fetchChannelsForToken(c, tokenData);
if (!channelsResult.results || channelsResult.results.length === 0) {
return c.json({
object: "list",
data: [],
});
}
// Collect all unique models from all available channels
const modelsSet = new Set<string>();
for (const row of channelsResult.results) {
const config = JSON.parse(row.value) as ChannelConfig;
if (config.deployment_mapper) {
for (const modelName of Object.keys(config.deployment_mapper)) {
modelsSet.add(modelName);
}
}
}
// Convert to OpenAI models format
const models = Array.from(modelsSet).sort().map((modelId) => ({
id: modelId,
object: "model" as const,
created: 1700000000,
owned_by: "system",
}));
return c.json({
object: "list",
data: models,
});
}
}