Add dedicated audio channel support (#11)

This commit is contained in:
Dream Hunter authored and GitHub committed 2026-03-15 18:32:50 +08:00
1 parent f7a1d95ec4
commit 1a90c280e0
20 files changed
+303 -37

No files matched your search

+9
View File
@@ -103,6 +103,15 @@ async function responseInterceptor(response: Response): Promise<any> {
return response.json()
}
if (contentType && contentType.startsWith('audio/')) {
const blob = await response.blob()
return {
object: 'audio',
contentType,
size: blob.size,
}
}
return response.text()
}
+6
View File
@@ -38,6 +38,11 @@ const requestTemplates: Record<string, any> = {
model: 'gpt-4o-mini',
input: 'Hello, Responses API!',
},
'/v1/audio/speech': {
model: 'gpt-4o-mini-tts',
input: 'The quick brown fox jumped over the lazy dog',
voice: 'alloy',
},
'/v1/completions': {
model: 'gpt-3.5-turbo-instruct',
prompt: 'Once upon a time',
@@ -137,6 +142,7 @@ export function ApiTest() {
<option value="/v1/chat/completions">/v1/chat/completions</option>
<option value="/v1/messages">/v1/messages</option>
<option value="/v1/responses">/v1/responses</option>
<option value="/v1/audio/speech">/v1/audio/speech</option>
<option value="/v1/completions">/v1/completions</option>
</Select>
</div>
+90 -5
View File
@@ -33,6 +33,8 @@ type EditMode = 'form' | 'json'
const channelTypes = [
{ value: 'azure-openai', label: 'Azure OpenAI' },
{ value: 'openai', label: 'OpenAI' },
{ value: 'azure-openai-audio', label: 'Azure OpenAI Audio' },
{ value: 'openai-audio', label: 'OpenAI Audio' },
{ value: 'claude', label: 'Claude' },
{ value: 'claude-to-openai', label: 'Claude → OpenAI' },
{ value: 'openai-responses', label: 'OpenAI Responses' },
@@ -49,10 +51,12 @@ export function Channels() {
endpoint: '',
api_key: '',
api_version: '',
supported_models: [],
deployment_mapper: {},
})
const [channelKey, setChannelKey] = useState('')
const [jsonValue, setJsonValue] = useState('')
const [supportedModelRows, setSupportedModelRows] = useState<Array<{ model: string }>>([])
const [mapperRows, setMapperRows] = useState<Array<{ request: string; deployment: string }>>([])
const [searchQuery, setSearchQuery] = useState('')
const [openMenu, setOpenMenu] = useState<string | null>(null)
@@ -109,10 +113,12 @@ export function Channels() {
endpoint: '',
api_key: '',
api_version: '',
supported_models: [],
deployment_mapper: {},
})
setChannelKey('')
setJsonValue('')
setSupportedModelRows([])
setMapperRows([])
setEditingKey(null)
setEditMode('form')
@@ -129,6 +135,7 @@ export function Channels() {
const config = typeof channel.value === 'string' ? JSON.parse(channel.value) : channel.value
setFormData(config)
setJsonValue(JSON.stringify(config, null, 2))
setSupportedModelRows((config.supported_models || []).map((model) => ({ model })))
if (config.deployment_mapper) {
const rows = Object.entries(config.deployment_mapper).map(([request, deployment]) => ({
@@ -166,7 +173,16 @@ export function Channels() {
}
})
config = { ...formData, deployment_mapper }
const supported_models = supportedModelRows
.map((row) => row.model.trim())
.filter((model) => model.length > 0)
if (supported_models.length === 0) {
addToast('请至少填写一个支持模型', 'error')
return
}
config = { ...formData, supported_models, deployment_mapper }
if (!formData.api_version) {
delete config.api_version
}
@@ -190,13 +206,17 @@ export function Channels() {
deployment_mapper[row.request] = row.deployment
}
})
const config = { ...formData, deployment_mapper }
const supported_models = supportedModelRows
.map((row) => row.model.trim())
.filter((model) => model.length > 0)
const config = { ...formData, supported_models, deployment_mapper }
setJsonValue(JSON.stringify(config, null, 2))
setEditMode('json')
} else {
try {
const config = JSON.parse(jsonValue)
setFormData(config)
setSupportedModelRows((config.supported_models || []).map((model: string) => ({ model })))
if (config.deployment_mapper) {
const rows = Object.entries(config.deployment_mapper).map(([request, deployment]) => ({
request,
@@ -215,6 +235,20 @@ export function Channels() {
setMapperRows([...mapperRows, { request: '', deployment: '' }])
}
const addSupportedModelRow = () => {
setSupportedModelRows([...supportedModelRows, { model: '' }])
}
const removeSupportedModelRow = (index: number) => {
setSupportedModelRows(supportedModelRows.filter((_, i) => i !== index))
}
const updateSupportedModelRow = (index: number, value: string) => {
const newRows = [...supportedModelRows]
newRows[index].model = value
setSupportedModelRows(newRows)
}
const removeMapperRow = (index: number) => {
setMapperRows(mapperRows.filter((_, i) => i !== index))
}
@@ -299,7 +333,7 @@ export function Channels() {
<div className="divide-y">
{filteredData?.map((channel) => {
const config = typeof channel.value === 'string' ? JSON.parse(channel.value) : channel.value
const modelCount = Object.keys(config.deployment_mapper || {}).length
const modelCount = (config.supported_models || []).length
const isMenuOpen = openMenu === channel.key
return (
@@ -490,7 +524,7 @@ export function Channels() {
placeholder="sk-..."
/>
</div>
{(formData.type === 'azure-openai' || formData.type === 'claude' || formData.type === 'azure-openai-responses') && (
{(formData.type === 'azure-openai' || formData.type === 'azure-openai-audio' || formData.type === 'claude' || formData.type === 'azure-openai-responses') && (
<div className="space-y-2">
<Label className="text-sm">API 版本</Label>
<Input
@@ -505,6 +539,57 @@ export function Channels() {
</CardContent>
</Card>
<Card>
<CardContent className="p-5">
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="font-medium flex items-center gap-2">
<Cpu className="h-4 w-4 text-muted-foreground" />
支持模型
</h3>
<p className="text-sm text-muted-foreground">用于声明这个频道接受哪些请求模型名,可填写精确值或通配符</p>
</div>
<Button type="button" variant="outline" size="sm" onClick={addSupportedModelRow}>
<Plus className="h-4 w-4 mr-1" />
添加
</Button>
</div>
{supportedModelRows.length === 0 ? (
<button
type="button"
onClick={addSupportedModelRow}
className="w-full py-8 border-2 border-dashed rounded-xl text-sm text-muted-foreground hover:text-foreground hover:border-primary/50 transition-colors flex flex-col items-center justify-center gap-2"
>
<Plus className="h-5 w-5" />
添加支持模型
</button>
) : (
<div className="space-y-2">
{supportedModelRows.map((row, index) => (
<div key={index} className="flex items-center gap-2 p-3 bg-muted/50 rounded-lg">
<Input
value={row.model}
onChange={(e) => updateSupportedModelRow(index, e.target.value)}
placeholder="gpt-4o-mini-tts 或 gpt-*"
className="flex-1 bg-background text-sm"
/>
<Button
type="button"
variant="ghost"
size="icon"
className="h-9 w-9 text-destructive hover:text-destructive flex-shrink-0"
onClick={() => removeSupportedModelRow(index)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
))}
</div>
)}
</CardContent>
</Card>
{/* Model Mappings */}
<Card>
<CardContent className="p-5">
@@ -514,7 +599,7 @@ export function Channels() {
<Cpu className="h-4 w-4 text-muted-foreground" />
模型映射
</h3>
<p className="text-sm text-muted-foreground">将请求模型名映射到实际部署名</p>
<p className="text-sm text-muted-foreground">可选。将请求模型名映射到实际上游 deployment;未填写时默认使用同名</p>
</div>
<Button type="button" variant="outline" size="sm" onClick={addMapperRow}>
<Plus className="h-4 w-4 mr-1" />
+19 -6
View File
@@ -27,7 +27,7 @@ export function Pricing() {
const [editMode, setEditMode] = useState<EditMode>('table')
const [jsonValue, setJsonValue] = useState('')
const [pricingRows, setPricingRows] = useState<
Array<{ model: string; input: number; output: number; cache: number }>
Array<{ model: string; input: number; output: number; cache: number; request: number }>
>([])
const [searchQuery, setSearchQuery] = useState('')
@@ -49,6 +49,7 @@ export function Pricing() {
input: pricing.input,
output: pricing.output,
cache: pricing.cache || 0,
request: pricing.request || 0,
}))
setPricingRows(rows)
setJsonValue(JSON.stringify(data, null, 2))
@@ -84,6 +85,7 @@ export function Pricing() {
input: row.input || 0,
output: row.output || 0,
cache: row.cache || 0,
request: row.request || 0,
}
}
})
@@ -108,6 +110,7 @@ export function Pricing() {
input: row.input || 0,
output: row.output || 0,
cache: row.cache || 0,
request: row.request || 0,
}
}
})
@@ -121,6 +124,7 @@ export function Pricing() {
input: (pricing as { input: number }).input || 0,
output: (pricing as { output: number }).output || 0,
cache: (pricing as { cache?: number }).cache || 0,
request: (pricing as { request?: number }).request || 0,
}))
setPricingRows(rows)
setEditMode('table')
@@ -131,14 +135,14 @@ export function Pricing() {
}
const addRow = () => {
setPricingRows([...pricingRows, { model: '', input: 0, output: 0, cache: 0 }])
setPricingRows([...pricingRows, { model: '', input: 0, output: 0, cache: 0, request: 0 }])
}
const removeRow = (index: number) => {
setPricingRows(pricingRows.filter((_, i) => i !== index))
}
const updateRow = (index: number, field: 'model' | 'input' | 'output' | 'cache', value: string | number) => {
const updateRow = (index: number, field: 'model' | 'input' | 'output' | 'cache' | 'request', value: string | number) => {
const newRows = [...pricingRows]
;(newRows[index] as any)[field] = value
setPricingRows(newRows)
@@ -173,7 +177,7 @@ export function Pricing() {
<Info className="h-4 w-4 mt-0.5 flex-shrink-0 text-primary" />
<span>
基础配额单位:<span className="font-mono text-foreground">1M tokens = $1.00</span>。
倍率表示相对成本,如输入倍率 30 = 每百万 token 消耗 $30 配额。
倍率表示相对成本,如输入倍率 30 = 每百万 token 消耗 $30 配额。`按次` 为固定扣费,同样使用该配额单位,例如 15000 = $0.015 / 次。
</span>
</div>
@@ -215,18 +219,19 @@ export function Pricing() {
{/* Table */}
<div className="rounded-xl border bg-card overflow-hidden">
{/* Header */}
<div className="grid gap-2 px-4 py-2.5 border-b bg-muted/30" style={{ gridTemplateColumns: '2fr 1fr 1fr 1fr 40px' }}>
<div className="grid gap-2 px-4 py-2.5 border-b bg-muted/30" style={{ gridTemplateColumns: '2fr 1fr 1fr 1fr 1fr 40px' }}>
<span className="text-xs font-medium text-muted-foreground">模型名称</span>
<span className="text-xs font-medium text-muted-foreground">输入倍率</span>
<span className="text-xs font-medium text-muted-foreground">输出倍率</span>
<span className="text-xs font-medium text-muted-foreground">缓存倍率</span>
<span className="text-xs font-medium text-muted-foreground">按次</span>
<span />
</div>
{/* Rows */}
<div className="divide-y">
{filteredRows.map((row) => (
<div key={row._i} className="grid gap-2 px-4 py-2 items-center hover:bg-muted/20 transition-colors" style={{ gridTemplateColumns: '2fr 1fr 1fr 1fr 40px' }}>
<div key={row._i} className="grid gap-2 px-4 py-2 items-center hover:bg-muted/20 transition-colors" style={{ gridTemplateColumns: '2fr 1fr 1fr 1fr 1fr 40px' }}>
<Input
value={row.model}
onChange={(e) => updateRow(row._i, 'model', e.target.value)}
@@ -257,6 +262,14 @@ export function Pricing() {
min="0"
className="font-mono text-sm h-8"
/>
<Input
type="number"
value={row.request}
onChange={(e) => updateRow(row._i, 'request', parseFloat(e.target.value) || 0)}
step="0.001"
min="0"
className="font-mono text-sm h-8"
/>
<Button
type="button"
variant="ghost"
+4 -2
View File
@@ -6,11 +6,12 @@ export interface Channel {
export interface ChannelConfig {
name: string
type: 'openai' | 'azure-openai' | 'claude' | 'claude-to-openai' | 'openai-responses' | 'azure-openai-responses'
type: 'openai' | 'azure-openai' | 'openai-audio' | 'azure-openai-audio' | 'claude' | 'claude-to-openai' | 'openai-responses' | 'azure-openai-responses'
endpoint: string
api_key: string
api_version?: string
deployment_mapper?: Record<string, string>
supported_models: string[]
deployment_mapper: Record<string, string>
}
export interface Token {
@@ -29,6 +30,7 @@ export interface PricingModel {
input: number
output: number
cache?: number
request?: number
}
export type PricingConfig = Record<string, PricingModel>
+3
View File
@@ -50,10 +50,13 @@ export class ChannelUpsertEndpoint extends OpenAPIRoute {
endpoint: z.string().describe('API endpoint'),
api_key: z.string().describe('API key'),
api_version: z.string().optional().describe('API version'),
supported_models: z.array(z.string()).describe('Supported request model list'),
deployment_mapper: z.record(z.string()).describe('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'),
cache: z.number().optional().describe('Cache token price per 1000 tokens'),
request: z.number().optional().describe('Fixed request price'),
})).optional().describe('Custom model pricing for this channel'),
}),
},
+4
View File
@@ -14,6 +14,8 @@ export class PricingGetEndpoint extends OpenAPIRoute {
...CommonSuccessfulResponse(z.record(z.object({
input: z.number(),
output: z.number(),
cache: z.number().optional(),
request: z.number().optional(),
}))),
...CommonErrorResponse,
},
@@ -44,6 +46,8 @@ export class PricingUpdateEndpoint extends OpenAPIRoute {
schema: z.record(z.object({
input: z.number(),
output: z.number(),
cache: z.number().optional(),
request: z.number().optional(),
})),
}
}
+7 -6
View File
@@ -33,17 +33,18 @@ export const TokenUtils = {
const pricing = await this.getPricing(c, model, targetChannelConfig);
const hasTokens = usage.prompt_tokens != null && usage.completion_tokens != null;
const requestCost = pricing?.request || 0
if (pricing && hasTokens) {
const inputCost = usage.prompt_tokens! * pricing.input;
const outputCost = usage.completion_tokens! * pricing.output;
if (pricing && (hasTokens || requestCost > 0)) {
const inputCost = hasTokens ? usage.prompt_tokens! * pricing.input : 0;
const outputCost = hasTokens ? usage.completion_tokens! * pricing.output : 0;
let cacheCost = 0;
if (usage.cached_tokens && usage.cached_tokens > 0 && pricing.cache) {
if (hasTokens && usage.cached_tokens && usage.cached_tokens > 0 && pricing.cache) {
cacheCost = usage.cached_tokens * pricing.cache;
}
const totalCost = inputCost + outputCost + cacheCost;
const totalCost = inputCost + outputCost + cacheCost + requestCost;
await this.updateUsage(c, apiKey, totalCost);
@@ -52,7 +53,7 @@ export const TokenUtils = {
+ '*'.repeat(apiKey.length / 3)
+ apiKey.slice((2 * apiKey.length) / 3)
);
console.log(`Model: ${model}, Channel: ${targetChannelKey}, apiKey: ${maskedApiKey}, Cost: ${totalCost} (input: ${inputCost}, cache: ${cacheCost}, output: ${outputCost})`);
console.log(`Model: ${model}, Channel: ${targetChannelKey}, apiKey: ${maskedApiKey}, Cost: ${totalCost} (request: ${requestCost}, input: ${inputCost}, cache: ${cacheCost}, output: ${outputCost})`);
} else {
console.warn(`No pricing found for model: ${model} in channel: ${targetChannelKey}`);
}
+1 -1
View File
@@ -1,6 +1,6 @@
export const CONSTANTS = {
VERSION: "v0.0.1",
DB_VERSION_KEY: "db_version",
DB_VERSION: "v0.0.1",
DB_VERSION: "v0.0.2",
MODEL_PRICING_KEY: "MODEL_PRICING_CONFIG",
}
+25 -1
View File
@@ -43,7 +43,31 @@ const dbOperations = {
migrate: async (c: Context<HonoCustomType>) => {
const version = await getSetting(c, CONSTANTS.DB_VERSION_KEY);
if (version != CONSTANTS.DB_VERSION) {
// TODO: Perform migration logic here
const channels = await c.env.DB.prepare(
"SELECT key, value FROM channel_config"
).all<Pick<ChannelConfigRow, "key" | "value">>();
for (const row of channels.results || []) {
const config = (() => {
try {
return JSON.parse(row.value) as ChannelConfig;
} catch {
return null;
}
})();
if (!config) {
continue;
}
config.supported_models = Object.keys(config.deployment_mapper || {});
await c.env.DB.prepare(
`UPDATE channel_config
SET value = ?, updated_at = datetime('now')
WHERE key = ?`
).bind(JSON.stringify(config), row.key).run();
}
// Update the version in the settings table
await saveSetting(c, CONSTANTS.DB_VERSION_KEY, CONSTANTS.DB_VERSION);
+1 -1
View File
@@ -64,7 +64,7 @@ export default {
}
if (response.ok) {
await checkoutUsageData(saveUsage, response)
await checkoutUsageData(saveUsage, response, requestBody)
}
return response
+1
View File
@@ -51,4 +51,5 @@ class UnifiedProxyEndpoint extends OpenAPIRoute {
api.post("/v1/chat/completions", UnifiedProxyEndpoint)
api.post("/v1/messages", UnifiedProxyEndpoint)
api.post("/v1/responses", UnifiedProxyEndpoint)
api.post("/v1/audio/speech", UnifiedProxyEndpoint)
api.get("/v1/models", ModelsEndpoint)
+3 -4
View File
@@ -2,6 +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";
export class ModelsEndpoint extends OpenAPIRoute {
schema = {
@@ -57,10 +58,8 @@ export class ModelsEndpoint extends OpenAPIRoute {
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);
}
for (const modelName of getSupportedModels(config)) {
modelsSet.add(modelName);
}
}
+1 -1
View File
@@ -53,7 +53,7 @@ export default {
}
if (response.ok) {
await checkoutUsageData(saveUsage, response)
await checkoutUsageData(saveUsage, response, requestBody)
}
return response
+13 -3
View File
@@ -1,7 +1,7 @@
import { Context } from "hono"
import { getApiKeyFromHeaders, fetchTokenData, fetchChannelsForToken } from "./auth"
import { RouteId, getRoutePolicy } from "./route-policy"
import { findDeploymentMapping } from "../../utils"
import { findDeploymentMapping, findSupportedModel, getSupportedModels } from "../../utils"
import { TokenUtils } from "../../admin/token_utils"
export type ChannelResolution = {
@@ -63,7 +63,17 @@ export const resolveChannel = async (
continue;
}
const mapping = findDeploymentMapping(config.deployment_mapper, model);
const supportedPattern = findSupportedModel(getSupportedModels(config), model)
if (!supportedPattern) {
continue;
}
const mapping = findDeploymentMapping(config.deployment_mapper, model) || {
pattern: supportedPattern,
deployment: model,
};
if (mapping) {
availableChannels.push({
key: row.key,
@@ -74,7 +84,7 @@ export const resolveChannel = async (
}
if (availableChannels.length === 0) {
return c.text(`Model not mapped: ${model}. Please configure deployment_mapper.`, 400);
return c.text(`Model not supported: ${model}. Please configure supported_models or deployment_mapper.`, 400);
}
const randomIndex = Math.floor(Math.random() * availableChannels.length);
+69 -4
View File
@@ -1,13 +1,78 @@
import { Context } from "hono"
const AUDIO_OUTPUT_TOKENS_PER_MINUTE = 1250
const TEXT_TOKENS_PER_CHAR = 0.25
const SPEECH_CHARS_PER_MINUTE = 750
const collectInputText = (input: unknown): string => {
if (typeof input === "string") {
return input
}
if (Array.isArray(input)) {
return input
.map((item) => collectInputText(item))
.filter((text) => text.length > 0)
.join(" ")
}
if (input && typeof input === "object") {
if ("text" in input && typeof input.text === "string") {
return input.text
}
if ("content" in input) {
return collectInputText(input.content)
}
}
return ""
}
const estimateAudioSpeechUsage = (requestBody: any): Usage | null => {
const inputText = collectInputText(requestBody?.input).trim()
if (!inputText) {
return null
}
const characterCount = Array.from(inputText).length
const promptTokens = Math.max(1, Math.ceil(characterCount * TEXT_TOKENS_PER_CHAR))
const estimatedMinutes = Math.max(1 / 60, characterCount / SPEECH_CHARS_PER_MINUTE)
const completionTokens = Math.max(1, Math.ceil(estimatedMinutes * AUDIO_OUTPUT_TOKENS_PER_MINUTE))
return {
prompt_tokens: promptTokens,
completion_tokens: completionTokens,
total_tokens: promptTokens + completionTokens,
}
}
export const checkoutUsageData = async (
saveUsage: (usage: Usage) => Promise<void>,
response: Response | OpenAIResponse
response: Response | OpenAIResponse,
requestBody?: any,
): Promise<void> => {
try {
const res = response instanceof Response
? await response.clone().json<OpenAIResponse>()
: response
if (response instanceof Response) {
const contentType = response.headers.get("content-type") || ""
if (contentType.startsWith("audio/")) {
const estimatedUsage = estimateAudioSpeechUsage(requestBody)
if (!estimatedUsage) return
await saveUsage(estimatedUsage)
return
}
if (!contentType.includes("application/json")) {
return
}
const res = await response.clone().json<OpenAIResponse>()
if (!res.usage) return;
await saveUsage(res.usage)
return
}
const res = response
if (!res.usage) return;
await saveUsage(res.usage)
} catch (error) {
@@ -17,6 +17,8 @@ type ProviderFetch = (
const providerMap: Record<string, ProviderFetch> = {
"azure-openai": azureOpenaiProxy.fetch,
"openai": openaiProxy.fetch,
"azure-openai-audio": azureOpenaiProxy.fetch,
"openai-audio": openaiProxy.fetch,
"claude": claudeProxy.fetch,
"claude-to-openai": claudeToOpenaiProxy.fetch,
"openai-responses": openaiResponsesProxy.fetch,
+12 -3
View File
@@ -1,19 +1,28 @@
export type RouteId = "chat-completions" | "messages" | "responses"
export type RouteId = "chat-completions" | "messages" | "responses" | "audio-speech"
type RoutePolicy = {
allowedTypes: ChannelType[] | null
}
const TEXT_CHANNEL_TYPES: ChannelType[] = [
"openai",
"azure-openai",
"claude",
"claude-to-openai",
]
const ROUTE_POLICIES: Record<RouteId, RoutePolicy> = {
"chat-completions": { allowedTypes: null },
"messages": { allowedTypes: null },
"chat-completions": { allowedTypes: TEXT_CHANNEL_TYPES },
"messages": { allowedTypes: TEXT_CHANNEL_TYPES },
"responses": { allowedTypes: ["openai-responses", "azure-openai-responses"] },
"audio-speech": { allowedTypes: ["openai-audio", "azure-openai-audio"] },
}
export const resolveRouteId = (pathname: string): RouteId | null => {
if (pathname.endsWith("/chat/completions")) return "chat-completions"
if (pathname.endsWith("/messages")) return "messages"
if (pathname.endsWith("/responses")) return "responses"
if (pathname.endsWith("/audio/speech")) return "audio-speech"
return null
}
+29
View File
@@ -106,3 +106,32 @@ export const findDeploymentMapping = (
return null;
}
export const findSupportedModel = (
supportedModels: string[] | undefined,
model: string
): string | null => {
if (!supportedModels || supportedModels.length === 0) return null;
if (supportedModels.includes(model)) {
return model;
}
for (const pattern of supportedModels) {
if (pattern.includes('*') && wildcardMatch(pattern, model)) {
return pattern;
}
}
return null;
}
export const getSupportedModels = (
config: Pick<ChannelConfig, "supported_models" | "deployment_mapper">
): string[] => {
if (config.supported_models && config.supported_models.length > 0) {
return config.supported_models;
}
return Object.keys(config.deployment_mapper || {});
}
Vendored
+4
View File
@@ -35,6 +35,8 @@ type ApiTokenRow = BaseDbRow & {
type ChannelType =
| "azure-openai"
| "openai"
| "azure-openai-audio"
| "openai-audio"
| "claude"
| "claude-to-openai"
| "openai-responses"
@@ -48,6 +50,7 @@ type ChannelConfig = {
endpoint: string;
api_key: string;
api_version?: string;
supported_models?: string[];
deployment_mapper: Record<string, string>;
model_pricing?: Record<string, ModelPricing>;
}
@@ -82,6 +85,7 @@ type ModelPricing = {
input: number;
output: number;
cache?: number;
request?: number;
}
type ApiTokenData = {