feat: 添加 Claude 到 OpenAI 协议转换支持

新增功能:
- 新增 claude-to-openai channel 类型,支持 Claude 协议请求转换为 OpenAI 协议
- 支持流式和非流式响应转换
- 完整的 SSE 事件转换 (message_start, content_block_*, message_delta, message_stop)
- 支持工具调用 (tool_use) 转换

优化:
- 统一在上层完成模型名映射,移除各 provider 重复逻辑
- 添加通配符匹配支持 (如 *claude*, claude-*)
- 简化代码,移除冗余变量

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
dreamhunter2333andClaude Opus 4.5 committed 2026-01-31 02:15:41 +08:00
1 parent 59149af970
commit c75dade7df
10 files changed
+692 -56

No files matched your search

+1
View File
@@ -34,6 +34,7 @@ const channelTypes = [
{ value: 'azure-openai', label: 'Azure OpenAI' },
{ value: 'openai', label: 'OpenAI' },
{ value: 'claude', label: 'Claude' },
{ value: 'claude-to-openai', label: 'Claude → OpenAI' },
{ value: 'openai-responses', label: 'OpenAI Responses' },
{ value: 'azure-openai-responses', label: 'Azure OpenAI Responses' },
]
+1 -1
View File
@@ -6,7 +6,7 @@ export interface Channel {
export interface ChannelConfig {
name: string
type: 'openai' | 'azure-openai' | 'claude' | 'openai-responses' | 'azure-openai-responses'
type: 'openai' | 'azure-openai' | 'claude' | 'claude-to-openai' | 'openai-responses' | 'azure-openai-responses'
endpoint: string
api_key: string
api_version?: string
+6 -12
View File
@@ -115,25 +115,19 @@ export default {
requestBody: any,
saveUsage: (usage: Usage) => Promise<void>,
): Promise<Response> {
// 准备请求数据
const reqJson = requestBody
// model 已在上层完成映射,直接作为 deploymentName 使用
const { model: deploymentName, stream } = requestBody;
// 强制包含使用数据
const { model: modelName, stream } = reqJson;
if (stream) {
reqJson.stream_options = {
...(reqJson.stream_options || {}),
requestBody.stream_options = {
...(requestBody.stream_options || {}),
include_usage: true,
}
}
// 获取部署名称
const deploymentName = config.deployment_mapper[modelName]
if (!deploymentName) {
throw new Error(`Model ${modelName} not supported`)
}
// 构建目标请求
const proxyRequest = buildProxyRequest(c.req.raw, reqJson, config, deploymentName)
const proxyRequest = buildProxyRequest(c.req.raw, requestBody, config, deploymentName)
// 发送请求并获取响应
const response = await fetch(proxyRequest)
+4 -10
View File
@@ -130,22 +130,16 @@ export default {
requestBody: any,
saveUsage: (usage: Usage) => Promise<void>,
): Promise<Response> {
const reqJson = requestBody
const { model: modelName, stream } = reqJson
const { stream } = requestBody
const deploymentName = config.deployment_mapper[modelName]
if (!deploymentName) {
throw new Error(`Model ${modelName} not supported`)
}
// model 已在上层完成映射
reqJson.model = deploymentName
const proxyRequest = buildProxyRequest(c.req.raw, reqJson, config)
const proxyRequest = buildProxyRequest(c.req.raw, requestBody, config)
const response = await fetch(proxyRequest)
if (stream) {
const [streamForClient, streamForServer] = response.body?.tee() || []
c.executionCtx.waitUntil(handleStreamResponse(c, streamForServer, reqJson, saveUsage))
c.executionCtx.waitUntil(handleStreamResponse(c, streamForServer, requestBody, saveUsage))
return new Response(streamForClient, {
headers: response.headers,
status: response.status,
+3 -12
View File
@@ -264,24 +264,15 @@ export default {
requestBody: any,
saveUsage: (usage: Usage) => Promise<void>,
): Promise<Response> {
// Parse request body
const reqJson = requestBody
const { model: modelName, stream } = reqJson;
const { stream } = requestBody;
// Validate model support
const deploymentName = config.deployment_mapper[modelName]
if (!deploymentName) {
throw new Error(`Model ${modelName} not supported`)
}
// Map model name
reqJson.model = deploymentName
// model 已在上层完成映射
// Note: Unlike OpenAI, Claude's non-streaming response already includes usage data,
// so we don't need to force stream=true for usage tracking
// Build and send proxy request
const proxyRequest = buildProxyRequest(c.req.raw, reqJson, config)
const proxyRequest = buildProxyRequest(c.req.raw, requestBody, config)
const response = await fetch(proxyRequest)
// Handle streaming response
+603
View File
@@ -0,0 +1,603 @@
import { Context } from "hono"
type OpenAIUsageLike = {
prompt_tokens?: number;
completion_tokens?: number;
total_tokens?: number;
input_tokens?: number;
output_tokens?: number;
}
type OpenAIStreamChoice = {
index?: number;
delta?: {
role?: string;
content?: string | Array<{ type?: string; text?: string }>;
tool_calls?: Array<{
index?: number;
id?: string;
type?: string;
function?: {
name?: string;
arguments?: string;
};
}>;
};
finish_reason?: string | null;
}
type OpenAIStreamChunk = {
id?: string;
model?: string;
choices?: OpenAIStreamChoice[];
usage?: OpenAIUsageLike;
}
const mapFinishReasonToClaude = (finishReason?: string | null): string | null => {
switch (finishReason) {
case "stop":
return "end_turn";
case "length":
return "max_tokens";
case "tool_calls":
return "tool_use";
default:
return null;
}
}
const extractSystemText = (system: any): string | null => {
if (typeof system === "string") {
return system;
}
if (Array.isArray(system)) {
let systemText = "";
for (const block of system) {
if (block && typeof block === "object" && block.type === "text" && typeof block.text === "string") {
systemText += block.text;
}
}
return systemText || null;
}
return null;
}
const extractTextFromContent = (content: any): string => {
if (typeof content === "string") {
return content;
}
if (Array.isArray(content)) {
return content
.map(part => (part && typeof part === "object" && typeof part.text === "string") ? part.text : "")
.join("");
}
return "";
}
const buildProxyRequest = (
request: Request,
reqJson: any,
config: ChannelConfig
): Request => {
const targetUrl = new URL("v1/chat/completions", config.endpoint);
const targetHeaders = new Headers(request.headers);
targetHeaders.delete("Authorization");
targetHeaders.delete("x-api-key");
targetHeaders.set("Authorization", `Bearer ${config.api_key}`);
return new Request(targetUrl, {
method: request.method,
headers: targetHeaders,
body: JSON.stringify(reqJson),
});
}
const convertClaudeToOpenAIRequest = (reqJson: any): any => {
const openaiReq: any = {
model: reqJson.model,
stream: reqJson.stream,
max_tokens: reqJson.max_tokens,
temperature: reqJson.temperature,
top_p: reqJson.top_p,
};
if (Array.isArray(reqJson.stop_sequences) && reqJson.stop_sequences.length > 0) {
openaiReq.stop = reqJson.stop_sequences;
}
const messages: any[] = [];
const systemText = extractSystemText(reqJson.system);
if (systemText) {
messages.push({
role: "system",
content: systemText,
});
}
if (Array.isArray(reqJson.messages)) {
for (const msg of reqJson.messages) {
const openaiMsg: any = {
role: msg?.role,
};
const content = msg?.content;
if (typeof content === "string") {
openaiMsg.content = content;
} else if (Array.isArray(content)) {
const textParts: Array<{ type: "text"; text: string }> = [];
const toolCalls: any[] = [];
for (const block of content) {
if (!block || typeof block !== "object") continue;
const blockType = block.type;
if (blockType === "text") {
if (typeof block.text === "string") {
textParts.push({ type: "text", text: block.text });
}
continue;
}
if (blockType === "tool_use") {
const id = typeof block.id === "string" ? block.id : "";
const name = typeof block.name === "string" ? block.name : "";
let inputArgs = "{}";
try {
inputArgs = JSON.stringify(block.input ?? {});
} catch (error) {
inputArgs = "{}";
}
toolCalls.push({
id,
type: "function",
function: {
name,
arguments: inputArgs,
},
});
continue;
}
if (blockType === "tool_result") {
const toolUseId = typeof block.tool_use_id === "string" ? block.tool_use_id : "";
const toolContent = extractTextFromContent(block.content);
messages.push({
role: "tool",
content: toolContent,
tool_call_id: toolUseId,
});
continue;
}
}
if (toolCalls.length > 0) {
openaiMsg.tool_calls = toolCalls;
}
if (textParts.length === 1) {
openaiMsg.content = textParts[0].text;
} else if (textParts.length > 0) {
openaiMsg.content = textParts;
}
}
messages.push(openaiMsg);
}
}
openaiReq.messages = messages;
if (Array.isArray(reqJson.tools)) {
openaiReq.tools = reqJson.tools.map((tool: any) => ({
type: "function",
function: {
name: tool?.name,
description: tool?.description,
parameters: tool?.input_schema,
},
}));
}
return openaiReq;
}
const convertOpenAIResponseToClaude = (resJson: any): any => {
const choice = Array.isArray(resJson?.choices) ? resJson.choices[0] : undefined;
const message = choice?.message ?? {};
const contentBlocks: any[] = [];
const textContent = extractTextFromContent(message?.content);
if (textContent) {
contentBlocks.push({
type: "text",
text: textContent,
});
}
if (Array.isArray(message?.tool_calls)) {
for (const toolCall of message.tool_calls) {
const id = typeof toolCall?.id === "string" ? toolCall.id : "";
const name = typeof toolCall?.function?.name === "string" ? toolCall.function.name : "";
let input = {};
const args = toolCall?.function?.arguments;
if (typeof args === "string") {
try {
input = JSON.parse(args);
} catch (error) {
input = {};
}
} else if (args && typeof args === "object") {
input = args;
}
contentBlocks.push({
type: "tool_use",
id,
name,
input,
});
}
}
const usage = resJson?.usage ?? {};
const inputTokens = usage.prompt_tokens ?? usage.input_tokens ?? 0;
const outputTokens = usage.completion_tokens ?? usage.output_tokens ?? 0;
return {
id: resJson?.id ?? "",
type: "message",
role: "assistant",
model: resJson?.model ?? "",
content: contentBlocks,
stop_reason: mapFinishReasonToClaude(choice?.finish_reason ?? null),
stop_sequence: null,
usage: {
input_tokens: inputTokens,
output_tokens: outputTokens,
},
};
}
const normalizeUsage = (raw?: OpenAIUsageLike): Usage | null => {
if (!raw) return null;
const promptTokens = raw.prompt_tokens ?? raw.input_tokens;
const completionTokens = raw.completion_tokens ?? raw.output_tokens;
const totalTokens = raw.total_tokens ?? (
(promptTokens ?? 0) + (completionTokens ?? 0)
);
if (promptTokens == null && completionTokens == null && totalTokens == null) {
return null;
}
return {
prompt_tokens: promptTokens,
completion_tokens: completionTokens,
total_tokens: totalTokens,
};
}
const createClaudeStreamTransformer = (
saveUsage: (usage: Usage) => Promise<void>
): TransformStream<Uint8Array, Uint8Array> => {
const decoder = new TextDecoder('utf-8');
const encoder = new TextEncoder();
let buffer = "";
let messageStarted = false;
let messageStopped = false;
let messageId = "";
let model = "";
let textBlockIndex: number | null = null;
let nextContentIndex = 0;
let stopReason: string | null = null;
let outputTokens = 0;
let usageToSave: Usage | null = null;
const toolBlocks = new Map<number, { contentIndex: number; id: string; name: string }>();
const openBlocks: number[] = [];
const openBlockSet = new Set<number>();
const enqueueEvent = (
controller: TransformStreamDefaultController<Uint8Array>,
eventType: string,
data: any
) => {
const payload = `event: ${eventType}\ndata: ${JSON.stringify(data)}\n\n`;
controller.enqueue(encoder.encode(payload));
};
const startMessage = (controller: TransformStreamDefaultController<Uint8Array>, chunk?: OpenAIStreamChunk) => {
if (messageStarted) return;
messageId = chunk?.id ?? messageId ?? "";
model = chunk?.model ?? model ?? "";
messageStarted = true;
enqueueEvent(controller, "message_start", {
type: "message_start",
message: {
id: messageId || "",
type: "message",
role: "assistant",
model: model || "",
content: [],
stop_reason: null,
stop_sequence: null,
usage: {
input_tokens: 0,
output_tokens: 0,
},
},
});
};
const startContentBlock = (
controller: TransformStreamDefaultController<Uint8Array>,
index: number,
contentBlock: any
) => {
if (!openBlockSet.has(index)) {
openBlockSet.add(index);
openBlocks.push(index);
}
enqueueEvent(controller, "content_block_start", {
type: "content_block_start",
index,
content_block: contentBlock,
});
};
const ensureTextBlock = (controller: TransformStreamDefaultController<Uint8Array>) => {
if (textBlockIndex == null) {
textBlockIndex = nextContentIndex++;
startContentBlock(controller, textBlockIndex, {
type: "text",
text: "",
});
}
return textBlockIndex;
};
const ensureToolBlock = (
controller: TransformStreamDefaultController<Uint8Array>,
toolIndex: number,
id: string,
name: string
) => {
const existing = toolBlocks.get(toolIndex);
if (existing) {
if (id && !existing.id) existing.id = id;
if (name && !existing.name) existing.name = name;
return existing.contentIndex;
}
const contentIndex = nextContentIndex++;
toolBlocks.set(toolIndex, {
contentIndex,
id,
name,
});
startContentBlock(controller, contentIndex, {
type: "tool_use",
id: id || "",
name: name || "",
input: {},
});
return contentIndex;
};
const handleFinish = (controller: TransformStreamDefaultController<Uint8Array>, finishReason?: string | null) => {
if (messageStopped) return;
if (!messageStarted) {
startMessage(controller);
}
if (finishReason != null) {
stopReason = mapFinishReasonToClaude(finishReason);
}
for (const index of openBlocks) {
enqueueEvent(controller, "content_block_stop", {
type: "content_block_stop",
index,
});
}
enqueueEvent(controller, "message_delta", {
type: "message_delta",
delta: {
stop_reason: stopReason,
},
usage: {
output_tokens: outputTokens,
},
});
enqueueEvent(controller, "message_stop", {
type: "message_stop",
});
messageStopped = true;
};
const processLine = (rawLine: string, controller: TransformStreamDefaultController<Uint8Array>) => {
const line = rawLine.trim();
if (!line || !line.startsWith("data:")) return;
const jsonContent = line.replace(/^data:\s*/, "").trim();
if (!jsonContent || jsonContent === "[DONE]") {
if (jsonContent === "[DONE]") {
handleFinish(controller, undefined);
}
return;
}
let chunkJson: OpenAIStreamChunk | null = null;
try {
chunkJson = JSON.parse(jsonContent) as OpenAIStreamChunk;
} catch (error) {
console.error("Error parsing OpenAI stream chunk:", error);
return;
}
if (!chunkJson) return;
if (chunkJson.usage) {
const normalized = normalizeUsage(chunkJson.usage);
if (normalized) {
usageToSave = normalized;
outputTokens = normalized.completion_tokens ?? outputTokens;
}
}
const choice = Array.isArray(chunkJson.choices) ? chunkJson.choices[0] : undefined;
if (!choice) return;
const delta = choice.delta ?? {};
if (!messageStarted && (delta.role === "assistant" || delta.content || delta.tool_calls)) {
startMessage(controller, chunkJson);
}
if (delta.content) {
const textBlocks = Array.isArray(delta.content)
? delta.content
: [{ type: "text", text: delta.content }];
for (const part of textBlocks) {
const text = typeof part?.text === "string" ? part.text : "";
if (!text) continue;
const index = ensureTextBlock(controller);
enqueueEvent(controller, "content_block_delta", {
type: "content_block_delta",
index,
delta: {
type: "text_delta",
text,
},
});
}
}
if (Array.isArray(delta.tool_calls)) {
for (const toolCall of delta.tool_calls) {
const toolIndex = typeof toolCall?.index === "number" ? toolCall.index : 0;
const id = typeof toolCall?.id === "string" ? toolCall.id : "";
const name = typeof toolCall?.function?.name === "string" ? toolCall.function.name : "";
const argumentsDelta = typeof toolCall?.function?.arguments === "string"
? toolCall.function.arguments
: "";
const contentIndex = ensureToolBlock(controller, toolIndex, id, name);
if (argumentsDelta) {
enqueueEvent(controller, "content_block_delta", {
type: "content_block_delta",
index: contentIndex,
delta: {
type: "input_json_delta",
partial_json: argumentsDelta,
},
});
}
}
}
if (choice.finish_reason) {
handleFinish(controller, choice.finish_reason);
}
};
return new TransformStream<Uint8Array, Uint8Array>({
async transform(chunk, controller) {
buffer += decoder.decode(chunk, { stream: true });
if (!buffer.includes("\n")) {
return;
}
const lines = buffer.split("\n");
buffer = lines.pop() || "";
for (const rawLine of lines) {
processLine(rawLine, controller);
}
},
async flush(controller) {
if (buffer.trim()) {
processLine(buffer, controller);
}
if (!messageStopped) {
handleFinish(controller, stopReason ?? undefined);
}
if (usageToSave) {
try {
await saveUsage(usageToSave);
} catch (error) {
console.error("Error saving usage data:", error);
}
}
},
});
}
export default {
async fetch(
c: Context<HonoCustomType>,
config: ChannelConfig,
requestBody: any,
saveUsage: (usage: Usage) => Promise<void>,
): Promise<Response> {
const { stream } = requestBody;
// model 已在上层完成映射
const openaiReq = convertClaudeToOpenAIRequest(requestBody);
if (stream) {
openaiReq.stream_options = {
...(openaiReq.stream_options || {}),
include_usage: true,
};
}
const proxyRequest = buildProxyRequest(c.req.raw, openaiReq, config);
const response = await fetch(proxyRequest);
if (stream) {
if (!response.ok || !response.body) {
return response;
}
const transformedStream = response.body.pipeThrough(
createClaudeStreamTransformer(saveUsage)
);
const headers = new Headers(response.headers);
headers.set("content-type", "text/event-stream; charset=utf-8");
headers.delete("content-length");
return new Response(transformedStream, {
headers,
status: response.status,
statusText: response.statusText,
});
}
if (response.ok) {
try {
const resJson = await response.clone().json();
const usage = normalizeUsage(resJson?.usage);
if (usage) {
await saveUsage(usage);
}
const claudeResp = convertOpenAIResponseToClaude(resJson);
const headers = new Headers(response.headers);
headers.set("content-type", "application/json");
headers.delete("content-length");
return new Response(JSON.stringify(claudeResp), {
headers,
status: response.status,
statusText: response.statusText,
});
} catch (error) {
console.error("Error converting OpenAI response:", error);
}
}
return response;
}
}
+19 -7
View File
@@ -4,9 +4,10 @@ import { contentJson, fromHono, OpenAPIRoute } from 'chanfana';
import azureOpenaiProxy from "./azure-openai-proxy"
import openaiProxy from "./openai-proxy"
import claudeProxy from "./claude-proxy"
import claudeToOpenaiProxy from "./claude-to-openai-proxy"
import openaiResponsesProxy from "./openai-responses-proxy"
import azureOpenaiResponsesProxy from "./azure-openai-responses-proxy"
import utils from "../utils"
import utils, { findDeploymentMapping } from "../utils"
import { TokenUtils } from "../admin/token_utils"
import { CONSTANTS } from "../constants"
import { z } from "zod";
@@ -25,6 +26,7 @@ const providerMap: Record<
"azure-openai": azureOpenaiProxy.fetch,
"openai": openaiProxy.fetch,
"claude": claudeProxy.fetch,
"claude-to-openai": claudeToOpenaiProxy.fetch,
"openai-responses": openaiResponsesProxy.fetch,
"azure-openai-responses": azureOpenaiResponsesProxy.fetch,
};
@@ -130,16 +132,18 @@ class ProxyEndpoint extends OpenAPIRoute {
}
// Filter channels that support the requested model
const availableChannels: Array<{ key: string, config: ChannelConfig }> = [];
const availableChannels: Array<{ key: string, config: ChannelConfig, mapping: { pattern: string, deployment: string } }> = [];
for (const row of channelsResult.results) {
const config = JSON.parse(row.value) as ChannelConfig;
// Check if channel supports the model (has deployment mapping for it)
if (config.deployment_mapper && config.deployment_mapper[model]) {
const mapping = findDeploymentMapping(config.deployment_mapper, model);
if (mapping) {
availableChannels.push({
key: row.key,
config: config
config: config,
mapping: mapping
});
}
}
@@ -157,6 +161,9 @@ class ProxyEndpoint extends OpenAPIRoute {
const targetChannelKey = selectedChannel.key;
const targetChannelConfig = selectedChannel.config;
// 统一在上层完成模型名映射
requestBody.model = selectedChannel.mapping.deployment;
if (!targetChannelConfig.type) {
return c.text("Channel type invalid", 400);
}
@@ -235,7 +242,7 @@ class ResponsesProxyEndpoint extends OpenAPIRoute {
}
const allowedTypes: ChannelType[] = ["openai-responses", "azure-openai-responses"];
const availableChannels: Array<{ key: string, config: ChannelConfig }> = [];
const availableChannels: Array<{ key: string, config: ChannelConfig, mapping: { pattern: string, deployment: string } }> = [];
for (const row of channelsResult.results) {
const config = JSON.parse(row.value) as ChannelConfig;
@@ -244,10 +251,12 @@ class ResponsesProxyEndpoint extends OpenAPIRoute {
continue;
}
if (config.deployment_mapper && config.deployment_mapper[model]) {
const mapping = findDeploymentMapping(config.deployment_mapper, model);
if (mapping) {
availableChannels.push({
key: row.key,
config: config
config: config,
mapping: mapping
});
}
}
@@ -264,6 +273,9 @@ class ResponsesProxyEndpoint extends OpenAPIRoute {
const targetChannelKey = selectedChannel.key;
const targetChannelConfig = selectedChannel.config;
// 统一在上层完成模型名映射
requestBody.model = selectedChannel.mapping.deployment;
const proxyFetch = providerMap[targetChannelConfig.type || ""];
if (!proxyFetch) {
return c.text("Channel type not supported", 400);
+6 -14
View File
@@ -103,28 +103,20 @@ export default {
requestBody: any,
saveUsage: (usage: Usage) => Promise<void>,
): Promise<Response> {
// 准备请求数据
const reqJson = requestBody
const { stream } = requestBody;
// 强制包含使用数据
const { model: modelName, stream } = reqJson;
if (stream) {
reqJson.stream_options = {
...(reqJson.stream_options || {}),
requestBody.stream_options = {
...(requestBody.stream_options || {}),
include_usage: true,
}
}
// 检查模型是否受支持
const deploymentName = config.deployment_mapper[modelName]
if (!deploymentName) {
throw new Error(`Model ${modelName} not supported`)
}
// 替换模型名称为映射后的名称
reqJson.model = deploymentName
// model 已在上层完成映射
// 构建目标请求
const proxyRequest = buildProxyRequest(c.req.raw, reqJson, config)
const proxyRequest = buildProxyRequest(c.req.raw, requestBody, config)
// 发送请求并获取响应
const response = await fetch(proxyRequest)
+48
View File
@@ -61,9 +61,57 @@ export const saveSetting = async (
return true;
}
/**
* 通配符匹配函数
* 支持 * 匹配任意字符
* 例如: "claude-*" 匹配 "claude-opus-4", "*claude*" 匹配 "my-claude-model"
*/
export const wildcardMatch = (pattern: string, str: string): boolean => {
// 精确匹配优先
if (pattern === str) return true;
// 没有通配符则不匹配
if (!pattern.includes('*')) return false;
// 将通配符模式转换为正则表达式
const regexPattern = pattern
.split('*')
.map(s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
.join('.*');
const regex = new RegExp(`^${regexPattern}$`, 'i');
return regex.test(str);
}
/**
* 从 deployment_mapper 中查找匹配的部署名称
* 支持精确匹配和通配符匹配
* 返回 { pattern, deployment } 或 null
*/
export const findDeploymentMapping = (
deploymentMapper: Record<string, string> | undefined,
model: string
): { pattern: string; deployment: string } | null => {
if (!deploymentMapper) return null;
// 1. 精确匹配优先
if (deploymentMapper[model]) {
return { pattern: model, deployment: deploymentMapper[model] };
}
// 2. 通配符匹配
for (const pattern of Object.keys(deploymentMapper)) {
if (pattern.includes('*') && wildcardMatch(pattern, model)) {
return { pattern, deployment: deploymentMapper[pattern] };
}
}
return null;
}
export default {
getJsonObjectValue,
getJsonSetting,
getSetting,
saveSetting,
wildcardMatch,
findDeploymentMapping,
}
Vendored
+1
View File
@@ -36,6 +36,7 @@ type ChannelType =
| "azure-openai"
| "openai"
| "claude"
| "claude-to-openai"
| "openai-responses"
| "azure-openai-responses"
| undefined