feat(channel): 添加模型映射和获取模型的 API
- 在 ChannelConfig 中添加 models 字段以支持模型映射 - 新增 ChannelFetchModelsEndpoint 用于获取上游模型列表 - 更新 normalizeChannelConfig 函数以处理新模型结构 - 修改相关的数据库操作以适应新的配置结构 - 引入 buildPrefixedTargetUrl 函数以简化 URL 构建 - 更新 utils 函数以支持模型映射查找
This commit is contained in:
1 parent
a2c97959ba
commit
84cc346ebd
17 files changed
+951
-424
No files matched your search
@@ -161,6 +161,12 @@ export const apiClient = {
|
||||
body: JSON.stringify(config),
|
||||
}),
|
||||
|
||||
fetchChannelModels: (config: unknown) =>
|
||||
request<ApiResponse>('/api/admin/channel/models/fetch', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(config),
|
||||
}),
|
||||
|
||||
deleteChannel: (key: string) =>
|
||||
request(`/api/admin/channel/${encodeURIComponent(key)}`, { method: 'DELETE' }),
|
||||
|
||||
|
||||
+571
-370
File diff suppressed because it is too large.
Load diff
@@ -4,6 +4,11 @@ export interface Channel {
|
||||
usage?: number
|
||||
}
|
||||
|
||||
export interface ChannelModelMapping {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface ChannelConfig {
|
||||
name: string
|
||||
type: 'openai' | 'azure-openai' | 'openai-audio' | 'azure-openai-audio' | 'claude' | 'claude-to-openai' | 'openai-responses' | 'azure-openai-responses'
|
||||
@@ -12,8 +17,9 @@ export interface ChannelConfig {
|
||||
api_keys?: string[]
|
||||
auto_retry?: boolean
|
||||
auto_rotate?: boolean
|
||||
supported_models: string[]
|
||||
deployment_mapper: Record<string, string>
|
||||
models?: ChannelModelMapping[]
|
||||
supported_models?: string[]
|
||||
deployment_mapper?: Record<string, string>
|
||||
model_pricing?: Record<string, PricingModel>
|
||||
}
|
||||
|
||||
|
||||
+187
-3
@@ -3,7 +3,138 @@ import { contentJson, OpenAPIRoute } from 'chanfana';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { CommonErrorResponse, CommonSuccessfulResponse } from "../model";
|
||||
import { sanitizeChannelConfig } from "../channel-config";
|
||||
import {
|
||||
DEFAULT_CLAUDE_API_VERSION,
|
||||
type NormalizedChannelConfig,
|
||||
normalizeChannelConfig,
|
||||
sanitizeChannelConfig,
|
||||
} from "../channel-config";
|
||||
import { buildAzureTargetUrlFromPath } from "../providers/shared/azure-target-url";
|
||||
import { buildPrefixedTargetUrl } from "../providers/shared/prefixed-target-url";
|
||||
|
||||
const ChannelModelSchema = z.object({
|
||||
id: z.string().describe('Upstream model ID'),
|
||||
name: z.string().describe('External model name exposed by this proxy'),
|
||||
});
|
||||
|
||||
const parseFetchedModels = (payload: any): ChannelModelMapping[] => {
|
||||
const rawModels = Array.isArray(payload)
|
||||
? payload
|
||||
: Array.isArray(payload?.data)
|
||||
? payload.data
|
||||
: Array.isArray(payload?.models)
|
||||
? payload.models
|
||||
: [];
|
||||
|
||||
const normalizedModels: ChannelModelMapping[] = [];
|
||||
const seenIds = new Set<string>();
|
||||
|
||||
for (const rawModel of rawModels) {
|
||||
if (typeof rawModel === "string") {
|
||||
const id = rawModel.trim();
|
||||
if (!id || seenIds.has(id)) {
|
||||
continue;
|
||||
}
|
||||
seenIds.add(id);
|
||||
normalizedModels.push({ id, name: id });
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!rawModel || typeof rawModel !== "object") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const id = (
|
||||
typeof rawModel.id === "string" ? rawModel.id
|
||||
: typeof rawModel.model === "string" ? rawModel.model
|
||||
: typeof rawModel.name === "string" ? rawModel.name
|
||||
: ""
|
||||
).trim();
|
||||
|
||||
if (!id || seenIds.has(id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const name = (
|
||||
typeof rawModel.display_name === "string" ? rawModel.display_name
|
||||
: typeof rawModel.name === "string" ? rawModel.name
|
||||
: id
|
||||
).trim() || id;
|
||||
|
||||
seenIds.add(id);
|
||||
normalizedModels.push({ id, name: name || id });
|
||||
}
|
||||
|
||||
return normalizedModels;
|
||||
};
|
||||
|
||||
const buildModelsFetchRequest = (
|
||||
config: NormalizedChannelConfig,
|
||||
apiKey: string
|
||||
): Request => {
|
||||
let targetUrl: URL;
|
||||
const headers = new Headers({
|
||||
"Accept": "application/json",
|
||||
});
|
||||
|
||||
switch (config.type) {
|
||||
case "azure-openai":
|
||||
case "azure-openai-audio":
|
||||
case "azure-openai-responses":
|
||||
targetUrl = buildAzureTargetUrlFromPath(config.endpoint, "/v1/models");
|
||||
headers.set("api-key", apiKey);
|
||||
break;
|
||||
case "claude":
|
||||
targetUrl = new URL("v1/models", config.endpoint);
|
||||
headers.set("x-api-key", apiKey);
|
||||
headers.set("anthropic-version", DEFAULT_CLAUDE_API_VERSION);
|
||||
break;
|
||||
case "openai":
|
||||
case "openai-audio":
|
||||
case "openai-responses":
|
||||
case "claude-to-openai":
|
||||
default:
|
||||
targetUrl = buildPrefixedTargetUrl(config.endpoint, "/v1/models");
|
||||
headers.set("Authorization", `Bearer ${apiKey}`);
|
||||
break;
|
||||
}
|
||||
|
||||
return new Request(targetUrl, {
|
||||
method: "GET",
|
||||
headers,
|
||||
});
|
||||
};
|
||||
|
||||
const fetchModelsFromChannel = async (
|
||||
config: ChannelConfig
|
||||
): Promise<ChannelModelMapping[]> => {
|
||||
const normalizedConfig = normalizeChannelConfig(config);
|
||||
|
||||
if (!normalizedConfig.endpoint || !normalizedConfig.type) {
|
||||
throw new Error("Channel endpoint and type are required");
|
||||
}
|
||||
|
||||
if (normalizedConfig.api_keys.length === 0) {
|
||||
throw new Error("At least one API key is required");
|
||||
}
|
||||
|
||||
let lastError = "Failed to fetch models from upstream";
|
||||
|
||||
for (const apiKey of normalizedConfig.api_keys) {
|
||||
const response = await fetch(buildModelsFetchRequest(normalizedConfig, apiKey));
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
lastError = errorText || `Upstream returned ${response.status}`;
|
||||
continue;
|
||||
}
|
||||
|
||||
const responseJson = await response.json();
|
||||
return parseFetchedModels(responseJson);
|
||||
}
|
||||
|
||||
throw new Error(lastError);
|
||||
};
|
||||
|
||||
// 获取所有 Channel 配置
|
||||
export class ChannelGetEndpoint extends OpenAPIRoute {
|
||||
@@ -53,8 +184,9 @@ export class ChannelUpsertEndpoint extends OpenAPIRoute {
|
||||
api_keys: z.array(z.string()).optional().describe('API keys, one request will pick one randomly'),
|
||||
auto_retry: z.boolean().optional().describe('Automatically retry the same API key'),
|
||||
auto_rotate: z.boolean().optional().describe('Automatically rotate to other API keys on retryable failures'),
|
||||
supported_models: z.array(z.string()).describe('Supported request model list'),
|
||||
deployment_mapper: z.record(z.string()).describe('Model deployment mapping'),
|
||||
models: z.array(ChannelModelSchema).optional().describe('External model name to upstream model ID mappings'),
|
||||
supported_models: z.array(z.string()).optional().describe('Deprecated supported request model list'),
|
||||
deployment_mapper: z.record(z.string()).optional().describe('Deprecated model deployment mapping'),
|
||||
model_pricing: z.record(z.object({
|
||||
input: z.number().describe('Input token price per 1000 tokens'),
|
||||
output: z.number().describe('Output token price per 1000 tokens'),
|
||||
@@ -107,6 +239,58 @@ export class ChannelUpsertEndpoint extends OpenAPIRoute {
|
||||
}
|
||||
}
|
||||
|
||||
export class ChannelFetchModelsEndpoint extends OpenAPIRoute {
|
||||
schema = {
|
||||
tags: ['Admin API'],
|
||||
summary: 'Fetch upstream model list for a channel config',
|
||||
request: {
|
||||
body: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: z.object({
|
||||
name: z.string().optional(),
|
||||
type: z.string().optional(),
|
||||
endpoint: z.string().optional(),
|
||||
api_key: z.string().optional(),
|
||||
api_keys: z.array(z.string()).optional(),
|
||||
auto_retry: z.boolean().optional(),
|
||||
auto_rotate: z.boolean().optional(),
|
||||
models: z.array(ChannelModelSchema).optional(),
|
||||
supported_models: z.array(z.string()).optional(),
|
||||
deployment_mapper: z.record(z.string()).optional(),
|
||||
model_pricing: z.record(z.object({
|
||||
input: z.number(),
|
||||
output: z.number(),
|
||||
cache: z.number().optional(),
|
||||
request: z.number().optional(),
|
||||
})).optional(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
...CommonSuccessfulResponse(z.array(ChannelModelSchema)),
|
||||
...CommonErrorResponse,
|
||||
},
|
||||
};
|
||||
|
||||
async handle(c: Context<HonoCustomType>) {
|
||||
const rawConfig = await c.req.json<ChannelConfig>();
|
||||
|
||||
try {
|
||||
const models = await fetchModelsFromChannel(rawConfig);
|
||||
return {
|
||||
success: true,
|
||||
data: models,
|
||||
} as CommonResponse;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Failed to fetch models";
|
||||
return c.text(message, 502);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 删除 Channel 配置
|
||||
export class ChannelDeleteEndpoint extends OpenAPIRoute {
|
||||
schema = {
|
||||
|
||||
+2
-1
@@ -3,7 +3,7 @@ import { fromHono } from 'chanfana';
|
||||
import { DBInitializeEndpoint } from "./db_api"
|
||||
import db from "../db"
|
||||
import {
|
||||
ChannelGetEndpoint, ChannelUpsertEndpoint, ChannelDeleteEndpoint
|
||||
ChannelGetEndpoint, ChannelUpsertEndpoint, ChannelDeleteEndpoint, ChannelFetchModelsEndpoint
|
||||
} from "./channel_api"
|
||||
import {
|
||||
TokenListEndpoint, TokenUpsertEndpoint, TokenDeleteEndpoint, TokenResetUsageEndpoint
|
||||
@@ -36,6 +36,7 @@ api.post("/api/admin/db_initialize", DBInitializeEndpoint)
|
||||
api.get("/api/admin/channel", ChannelGetEndpoint)
|
||||
api.post("/api/admin/channel/:key", ChannelUpsertEndpoint)
|
||||
api.delete("/api/admin/channel/:key", ChannelDeleteEndpoint)
|
||||
api.post("/api/admin/channel/models/fetch", ChannelFetchModelsEndpoint)
|
||||
|
||||
// Token management routes
|
||||
api.get("/api/admin/token", TokenListEndpoint)
|
||||
|
||||
+69
-5
@@ -4,10 +4,13 @@ export const DEFAULT_CLAUDE_API_VERSION = "2023-06-01";
|
||||
export const MAX_RETRIES_PER_KEY = 3;
|
||||
export const MAX_ROTATION_ATTEMPTS = 3;
|
||||
|
||||
export type NormalizedChannelConfig = Omit<ChannelConfig, "api_keys" | "auto_retry" | "auto_rotate"> & {
|
||||
export type NormalizedChannelConfig = Omit<ChannelConfig, "api_keys" | "auto_retry" | "auto_rotate" | "models" | "supported_models" | "deployment_mapper"> & {
|
||||
api_keys: string[];
|
||||
auto_retry: boolean;
|
||||
auto_rotate: boolean;
|
||||
models: ChannelModelMapping[];
|
||||
supported_models: string[];
|
||||
deployment_mapper: Record<string, string>;
|
||||
};
|
||||
|
||||
const normalizeApiKeys = (config: Partial<ChannelConfig>): string[] => {
|
||||
@@ -34,9 +37,70 @@ const normalizeApiKeys = (config: Partial<ChannelConfig>): string[] => {
|
||||
return normalizedKeys;
|
||||
};
|
||||
|
||||
const normalizeLegacyModels = (config: Partial<ChannelConfig>): ChannelModelMapping[] => {
|
||||
const normalizedModels: ChannelModelMapping[] = [];
|
||||
const seenNames = new Set<string>();
|
||||
|
||||
const pushModel = (modelId: string, modelName?: string) => {
|
||||
const id = modelId.trim();
|
||||
const name = (modelName || modelId).trim();
|
||||
|
||||
if (!id || !name || seenNames.has(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
seenNames.add(name);
|
||||
normalizedModels.push({ id, name });
|
||||
};
|
||||
|
||||
if (Array.isArray(config.models)) {
|
||||
for (const model of config.models) {
|
||||
if (!model || typeof model !== "object") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const id = typeof model.id === "string" ? model.id : "";
|
||||
const name = typeof model.name === "string" ? model.name : id;
|
||||
pushModel(id, name);
|
||||
}
|
||||
}
|
||||
|
||||
const deploymentMapper = config.deployment_mapper || {};
|
||||
const supportedModels = Array.isArray(config.supported_models)
|
||||
? config.supported_models
|
||||
: [];
|
||||
|
||||
for (const supportedModel of supportedModels) {
|
||||
if (typeof supportedModel !== "string") {
|
||||
continue;
|
||||
}
|
||||
|
||||
pushModel(
|
||||
typeof deploymentMapper[supportedModel] === "string"
|
||||
? deploymentMapper[supportedModel]
|
||||
: supportedModel,
|
||||
supportedModel,
|
||||
);
|
||||
}
|
||||
|
||||
for (const [modelName, modelId] of Object.entries(deploymentMapper)) {
|
||||
if (typeof modelId !== "string") {
|
||||
continue;
|
||||
}
|
||||
pushModel(modelId, modelName);
|
||||
}
|
||||
|
||||
return normalizedModels;
|
||||
};
|
||||
|
||||
export const normalizeChannelConfig = (
|
||||
config: Partial<ChannelConfig>
|
||||
): NormalizedChannelConfig => {
|
||||
const models = normalizeLegacyModels(config);
|
||||
const deploymentMapper = Object.fromEntries(
|
||||
models.map((model) => [model.name, model.id])
|
||||
);
|
||||
|
||||
return {
|
||||
name: config.name || "",
|
||||
type: config.type,
|
||||
@@ -45,8 +109,9 @@ export const normalizeChannelConfig = (
|
||||
api_keys: normalizeApiKeys(config),
|
||||
auto_retry: config.auto_retry ?? DEFAULT_CHANNEL_AUTO_RETRY,
|
||||
auto_rotate: config.auto_rotate ?? DEFAULT_CHANNEL_AUTO_ROTATE,
|
||||
supported_models: Array.isArray(config.supported_models) ? config.supported_models : [],
|
||||
deployment_mapper: config.deployment_mapper || {},
|
||||
models,
|
||||
supported_models: models.map((model) => model.name),
|
||||
deployment_mapper: deploymentMapper,
|
||||
model_pricing: config.model_pricing,
|
||||
};
|
||||
};
|
||||
@@ -63,8 +128,7 @@ export const sanitizeChannelConfig = (
|
||||
api_keys: normalized.api_keys,
|
||||
auto_retry: normalized.auto_retry,
|
||||
auto_rotate: normalized.auto_rotate,
|
||||
supported_models: normalized.supported_models,
|
||||
deployment_mapper: normalized.deployment_mapper,
|
||||
models: normalized.models,
|
||||
model_pricing: normalized.model_pricing,
|
||||
};
|
||||
};
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
export const CONSTANTS = {
|
||||
VERSION: "v0.0.1",
|
||||
DB_VERSION_KEY: "db_version",
|
||||
DB_VERSION: "v0.0.2",
|
||||
DB_VERSION: "v0.0.3",
|
||||
MODEL_PRICING_KEY: "MODEL_PRICING_CONFIG",
|
||||
}
|
||||
+3
-2
@@ -1,6 +1,7 @@
|
||||
import { Context } from "hono";
|
||||
import { CONSTANTS } from "../constants";
|
||||
import { getSetting, saveSetting } from "../utils";
|
||||
import { sanitizeChannelConfig } from "../channel-config";
|
||||
|
||||
const DB_INIT_QUERIES = `
|
||||
CREATE TABLE IF NOT EXISTS channel_config (
|
||||
@@ -65,13 +66,13 @@ const dbOperations = {
|
||||
continue;
|
||||
}
|
||||
|
||||
config.supported_models = Object.keys(config.deployment_mapper || {});
|
||||
const sanitizedConfig = sanitizeChannelConfig(config);
|
||||
|
||||
await c.env.DB.prepare(
|
||||
`UPDATE channel_config
|
||||
SET value = ?, updated_at = datetime('now')
|
||||
WHERE key = ?`
|
||||
).bind(JSON.stringify(config), row.key).run();
|
||||
).bind(JSON.stringify(sanitizedConfig), row.key).run();
|
||||
}
|
||||
|
||||
await saveSetting(c, CONSTANTS.DB_VERSION_KEY, CONSTANTS.DB_VERSION);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Context } from "hono"
|
||||
import { normalizeUsage as normalizeResponsesUsage } from "./shared/usage-utils"
|
||||
import { buildPrefixedTargetUrl } from "./shared/prefixed-target-url"
|
||||
|
||||
type OpenAIStreamChoice = {
|
||||
index?: number;
|
||||
@@ -78,7 +79,7 @@ const buildProxyRequest = (
|
||||
reqJson: any,
|
||||
config: ChannelConfig
|
||||
): Request => {
|
||||
const targetUrl = new URL("v1/chat/completions", config.endpoint);
|
||||
const targetUrl = buildPrefixedTargetUrl(config.endpoint, "/v1/chat/completions");
|
||||
const targetHeaders = new Headers(request.headers);
|
||||
const apiKey = config.api_key || "";
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Context } from "hono";
|
||||
import { OpenAPIRoute } from "chanfana";
|
||||
import { z } from "zod";
|
||||
import { getApiKeyFromHeaders, fetchTokenData, fetchChannelsForToken } from "./shared/auth";
|
||||
import { getSupportedModels } from "../utils";
|
||||
import { getChannelModels } from "../utils";
|
||||
import { normalizeChannelConfig } from "../channel-config";
|
||||
|
||||
export class ModelsEndpoint extends OpenAPIRoute {
|
||||
@@ -59,8 +59,8 @@ export class ModelsEndpoint extends OpenAPIRoute {
|
||||
|
||||
for (const row of channelsResult.results) {
|
||||
const config = normalizeChannelConfig(JSON.parse(row.value) as ChannelConfig);
|
||||
for (const modelName of getSupportedModels(config)) {
|
||||
modelsSet.add(modelName);
|
||||
for (const model of getChannelModels(config)) {
|
||||
modelsSet.add(model.name);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Context } from "hono"
|
||||
import { checkoutUsageData, handleStreamResponse } from "./shared/openai-stream-utils"
|
||||
import { buildPrefixedTargetUrl } from "./shared/prefixed-target-url"
|
||||
|
||||
const buildProxyRequest = (
|
||||
request: Request,
|
||||
@@ -7,11 +8,9 @@ const buildProxyRequest = (
|
||||
config: ChannelConfig
|
||||
): Request => {
|
||||
const url = new URL(request.url)
|
||||
const targetUrl = new URL(config.endpoint)
|
||||
const targetUrl = buildPrefixedTargetUrl(config.endpoint, url.pathname)
|
||||
const apiKey = config.api_key || ""
|
||||
|
||||
targetUrl.pathname = url.pathname
|
||||
|
||||
const targetHeaders = new Headers(request.headers)
|
||||
targetHeaders.delete("Host")
|
||||
targetHeaders.delete("Cookie")
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
handleStreamResponse,
|
||||
checkoutResponsesUsageData,
|
||||
} from "./shared/responses-stream-utils"
|
||||
import { buildPrefixedTargetUrl } from "./shared/prefixed-target-url"
|
||||
|
||||
const buildProxyRequest = (
|
||||
request: Request,
|
||||
@@ -10,11 +11,9 @@ const buildProxyRequest = (
|
||||
config: ChannelConfig
|
||||
): Request => {
|
||||
const url = new URL(request.url)
|
||||
const targetUrl = new URL(config.endpoint)
|
||||
const targetUrl = buildPrefixedTargetUrl(config.endpoint, url.pathname)
|
||||
const apiKey = config.api_key || ""
|
||||
|
||||
targetUrl.pathname = url.pathname
|
||||
|
||||
const targetHeaders = new Headers(request.headers)
|
||||
targetHeaders.delete("Host")
|
||||
targetHeaders.delete("Cookie")
|
||||
|
||||
@@ -15,13 +15,20 @@ export const buildAzureTargetUrl = (
|
||||
endpoint: string
|
||||
): URL => {
|
||||
const requestUrl = new URL(request.url);
|
||||
return buildAzureTargetUrlFromPath(endpoint, requestUrl.pathname);
|
||||
};
|
||||
|
||||
export const buildAzureTargetUrlFromPath = (
|
||||
endpoint: string,
|
||||
path: string
|
||||
): URL => {
|
||||
const targetUrl = new URL(endpoint);
|
||||
|
||||
if (endpoint.endsWith("#")) {
|
||||
return targetUrl;
|
||||
}
|
||||
|
||||
const requestPath = requestUrl.pathname.replace(/^\/v1/, "");
|
||||
const requestPath = path.replace(/^\/v1/, "");
|
||||
const currentBasePath = trimSlashes(targetUrl.pathname);
|
||||
const azureBasePath = currentBasePath.endsWith("openai/v1")
|
||||
? currentBasePath
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Context } from "hono"
|
||||
import { getApiKeyFromHeaders, fetchTokenData, fetchChannelsForToken } from "./auth"
|
||||
import { RouteId, getRoutePolicy } from "./route-policy"
|
||||
import { findDeploymentMapping, findSupportedModel, getSupportedModels } from "../../utils"
|
||||
import { findChannelModelMapping } from "../../utils"
|
||||
import { TokenUtils } from "../../admin/token_utils"
|
||||
import { normalizeChannelConfig } from "../../channel-config"
|
||||
|
||||
@@ -43,15 +43,15 @@ export const resolveChannel = async (
|
||||
} catch (error) {
|
||||
return c.text("Invalid JSON body", 400);
|
||||
}
|
||||
const model = requestBody.model;
|
||||
if (!model) {
|
||||
const requestedModel = requestBody.model;
|
||||
if (!requestedModel) {
|
||||
return c.text("Model is required", 400);
|
||||
}
|
||||
|
||||
const policy = getRoutePolicy(routeId);
|
||||
const allowedTypes = policy.allowedTypes;
|
||||
|
||||
const availableChannels: Array<{ key: string, config: ChannelConfig, mapping: { pattern: string, deployment: string } }> = [];
|
||||
const availableChannels: Array<{ key: string, config: ChannelConfig, mapping: ChannelModelMapping }> = [];
|
||||
|
||||
for (const row of channelsResult.results) {
|
||||
const config = (() => {
|
||||
@@ -70,28 +70,20 @@ export const resolveChannel = async (
|
||||
continue;
|
||||
}
|
||||
|
||||
const supportedPattern = findSupportedModel(getSupportedModels(config), model)
|
||||
|
||||
if (!supportedPattern) {
|
||||
const mapping = findChannelModelMapping(config, requestedModel);
|
||||
if (!mapping) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const mapping = findDeploymentMapping(config.deployment_mapper, model) || {
|
||||
pattern: supportedPattern,
|
||||
deployment: model,
|
||||
};
|
||||
|
||||
if (mapping) {
|
||||
availableChannels.push({
|
||||
key: row.key,
|
||||
config: config,
|
||||
mapping: mapping
|
||||
});
|
||||
}
|
||||
availableChannels.push({
|
||||
key: row.key,
|
||||
config: config,
|
||||
mapping,
|
||||
});
|
||||
}
|
||||
|
||||
if (availableChannels.length === 0) {
|
||||
return c.text(`Model not supported: ${model}. Please configure supported_models or deployment_mapper.`, 400);
|
||||
return c.text(`Model not supported: ${requestedModel}. Please configure models.`, 400);
|
||||
}
|
||||
|
||||
const randomIndex = Math.floor(Math.random() * availableChannels.length);
|
||||
@@ -99,7 +91,7 @@ export const resolveChannel = async (
|
||||
const targetChannelKey = selectedChannel.key;
|
||||
const targetChannelConfig = selectedChannel.config;
|
||||
|
||||
requestBody.model = selectedChannel.mapping.deployment;
|
||||
requestBody.model = selectedChannel.mapping.id;
|
||||
|
||||
if (!targetChannelConfig.type) {
|
||||
return c.text("Channel type invalid", 400);
|
||||
@@ -107,7 +99,7 @@ export const resolveChannel = async (
|
||||
|
||||
const saveUsage = async (usage: Usage) => {
|
||||
try {
|
||||
await TokenUtils.processUsage(c, apiKey, requestBody.model, targetChannelKey, targetChannelConfig, usage);
|
||||
await TokenUtils.processUsage(c, apiKey, requestedModel, targetChannelKey, targetChannelConfig, usage);
|
||||
} catch (error) {
|
||||
console.error('Error processing usage:', error);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
const trimSlashes = (value: string): string => {
|
||||
return value.replace(/^\/+|\/+$/g, "");
|
||||
};
|
||||
|
||||
const joinPath = (...segments: string[]): string => {
|
||||
const cleanedSegments = segments
|
||||
.map((segment) => trimSlashes(segment))
|
||||
.filter((segment) => segment.length > 0);
|
||||
|
||||
return `/${cleanedSegments.join("/")}`;
|
||||
};
|
||||
|
||||
export const buildPrefixedTargetUrl = (
|
||||
endpoint: string,
|
||||
requestPath: string,
|
||||
prefixToStrip = "/v1"
|
||||
): URL => {
|
||||
const targetUrl = new URL(endpoint);
|
||||
|
||||
if (endpoint.endsWith("#")) {
|
||||
return targetUrl;
|
||||
}
|
||||
|
||||
const currentBasePath = trimSlashes(targetUrl.pathname);
|
||||
const normalizedPrefix = trimSlashes(prefixToStrip);
|
||||
const baseAlreadyContainsPrefix = normalizedPrefix.length > 0
|
||||
&& currentBasePath.endsWith(normalizedPrefix);
|
||||
|
||||
let normalizedRequestPath = requestPath;
|
||||
if (baseAlreadyContainsPrefix && normalizedRequestPath.startsWith(prefixToStrip)) {
|
||||
normalizedRequestPath = normalizedRequestPath.slice(prefixToStrip.length);
|
||||
}
|
||||
|
||||
targetUrl.pathname = joinPath(currentBasePath, normalizedRequestPath);
|
||||
return targetUrl;
|
||||
};
|
||||
+30
-6
@@ -1,4 +1,5 @@
|
||||
import { Context } from "hono";
|
||||
import { normalizeChannelConfig } from "./channel-config";
|
||||
|
||||
const getJsonObjectValue = <T = any>(
|
||||
value: string | any
|
||||
@@ -107,6 +108,33 @@ export const findDeploymentMapping = (
|
||||
return null;
|
||||
}
|
||||
|
||||
export const getChannelModels = (
|
||||
config: Partial<Pick<ChannelConfig, "models" | "supported_models" | "deployment_mapper">>
|
||||
): ChannelModelMapping[] => {
|
||||
return normalizeChannelConfig(config as Partial<ChannelConfig>).models;
|
||||
}
|
||||
|
||||
export const findChannelModelMapping = (
|
||||
config: Partial<Pick<ChannelConfig, "models" | "supported_models" | "deployment_mapper">>,
|
||||
model: string
|
||||
): ChannelModelMapping | null => {
|
||||
const channelModels = getChannelModels(config);
|
||||
|
||||
for (const channelModel of channelModels) {
|
||||
if (channelModel.name === model) {
|
||||
return channelModel;
|
||||
}
|
||||
}
|
||||
|
||||
for (const channelModel of channelModels) {
|
||||
if (channelModel.name.includes('*') && wildcardMatch(channelModel.name, model)) {
|
||||
return channelModel;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export const findSupportedModel = (
|
||||
supportedModels: string[] | undefined,
|
||||
model: string
|
||||
@@ -127,11 +155,7 @@ export const findSupportedModel = (
|
||||
}
|
||||
|
||||
export const getSupportedModels = (
|
||||
config: Pick<ChannelConfig, "supported_models" | "deployment_mapper">
|
||||
config: Pick<ChannelConfig, "models" | "supported_models" | "deployment_mapper">
|
||||
): string[] => {
|
||||
if (config.supported_models && config.supported_models.length > 0) {
|
||||
return config.supported_models;
|
||||
}
|
||||
|
||||
return Object.keys(config.deployment_mapper || {});
|
||||
return getChannelModels(config).map((channelModel) => channelModel.name);
|
||||
}
|
||||
@@ -45,6 +45,11 @@ type ChannelType =
|
||||
| undefined
|
||||
| null;
|
||||
|
||||
type ChannelModelMapping = {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
type ChannelConfig = {
|
||||
name: string;
|
||||
type: ChannelType;
|
||||
@@ -53,8 +58,9 @@ type ChannelConfig = {
|
||||
api_keys?: string[];
|
||||
auto_retry?: boolean;
|
||||
auto_rotate?: boolean;
|
||||
models?: ChannelModelMapping[];
|
||||
supported_models?: string[];
|
||||
deployment_mapper: Record<string, string>;
|
||||
deployment_mapper?: Record<string, string>;
|
||||
model_pricing?: Record<string, ModelPricing>;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user