243 lines
7.8 KiB
TypeScript
243 lines
7.8 KiB
TypeScript
import type { AiProviderKind, AiProviderResponse, AiProviderStatus, AiRelayMode } from "../api/types";
|
|
|
|
export type AiProviderFilter = AiProviderStatus | "all";
|
|
export type AiProviderListState = "loading" | "ready" | "error";
|
|
export type AiProviderViewState = "api" | "local-development" | "saving" | "error";
|
|
|
|
export interface AiProviderFormState {
|
|
id: string;
|
|
name: string;
|
|
kind: AiProviderKind;
|
|
baseUrl: string;
|
|
apiKeyRef: string;
|
|
apiKeyConfigured: boolean;
|
|
modelsText: string;
|
|
defaultModel: string;
|
|
relayMode: AiRelayMode;
|
|
timeoutMs: string;
|
|
redactionPolicy: string;
|
|
}
|
|
|
|
export interface AiProviderKindDefaults {
|
|
id: string;
|
|
name: string;
|
|
kind: AiProviderKind;
|
|
baseUrl: string;
|
|
apiKeyRef: string;
|
|
modelsText: string;
|
|
defaultModel: string;
|
|
relayMode: AiRelayMode;
|
|
timeoutMs: string;
|
|
redactionPolicy: string;
|
|
requirement: string;
|
|
advancedNote: string;
|
|
}
|
|
|
|
export const aiProviderKindDefaults: Record<AiProviderKind, AiProviderKindDefaults> = {
|
|
"openai-compatible": {
|
|
id: "ai.openai-compatible",
|
|
name: "OpenAI Compatible",
|
|
kind: "openai-compatible",
|
|
baseUrl: "https://relay.example.test/v1",
|
|
apiKeyRef: "secret://providers/openai-compatible",
|
|
modelsText: "gpt-5.6-luna",
|
|
defaultModel: "gpt-5.6-luna",
|
|
relayMode: "relay",
|
|
timeoutMs: "30000",
|
|
redactionPolicy: "default",
|
|
requirement: "需要平台侧 API key / secret 引用;Base URL 可按网关修改。",
|
|
advancedNote: "OpenAI 兼容网关通常只差 Base URL,模型可以保存后再发现。"
|
|
},
|
|
openai: {
|
|
id: "ai.openai",
|
|
name: "OpenAI",
|
|
kind: "openai",
|
|
baseUrl: "https://api.openai.com/v1",
|
|
apiKeyRef: "secret://providers/openai",
|
|
modelsText: "gpt-5.6-terra, gpt-5.6-luna",
|
|
defaultModel: "gpt-5.6-terra",
|
|
relayMode: "relay",
|
|
timeoutMs: "30000",
|
|
redactionPolicy: "default",
|
|
requirement: "官方 SDK/API 使用 API key;平台保存 secret 引用,不在页面保存真实密钥。",
|
|
advancedNote: "官方 OpenAI API 使用 Bearer API key 与 /v1 endpoint;模型列表可按账号权限调整。"
|
|
},
|
|
claude: {
|
|
id: "ai.claude",
|
|
name: "Claude",
|
|
kind: "claude",
|
|
baseUrl: "https://api.anthropic.com/v1",
|
|
apiKeyRef: "secret://providers/anthropic",
|
|
modelsText: "claude-sonnet-5, claude-haiku-4-5-20251001",
|
|
defaultModel: "claude-sonnet-5",
|
|
relayMode: "relay",
|
|
timeoutMs: "30000",
|
|
redactionPolicy: "default",
|
|
requirement: "Anthropic 请求使用 API key;版本头由平台适配层处理。",
|
|
advancedNote: "保持平台中介调用,避免插件或前端接触 Anthropic key。"
|
|
},
|
|
gemini: {
|
|
id: "ai.gemini",
|
|
name: "Gemini",
|
|
kind: "gemini",
|
|
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
|
|
apiKeyRef: "secret://providers/gemini",
|
|
modelsText: "gemini-3.5-flash, gemini-2.5-flash",
|
|
defaultModel: "gemini-3.5-flash",
|
|
relayMode: "relay",
|
|
timeoutMs: "30000",
|
|
redactionPolicy: "default",
|
|
requirement: "Google Gemini 入门使用 API key;平台保存 secret 引用。",
|
|
advancedNote: "Base URL 使用 Google Generative Language API;模型名按项目可用模型调整。"
|
|
},
|
|
ollama: {
|
|
id: "ai.ollama",
|
|
name: "Ollama Local",
|
|
kind: "ollama",
|
|
baseUrl: "http://127.0.0.1:11434/v1",
|
|
apiKeyRef: "",
|
|
modelsText: "gpt-oss:20b",
|
|
defaultModel: "gpt-oss:20b",
|
|
relayMode: "local",
|
|
timeoutMs: "30000",
|
|
redactionPolicy: "default",
|
|
requirement: "本地 Ollama 默认不需要 API key;重点是本机/内网 Base URL 和模型名。",
|
|
advancedNote: "Ollama OpenAI 兼容接口会忽略 dummy key;平台本地模式不要求用户填写密钥引用。"
|
|
},
|
|
custom: {
|
|
id: "ai.custom",
|
|
name: "Custom Provider",
|
|
kind: "custom",
|
|
baseUrl: "https://provider.example.test/v1",
|
|
apiKeyRef: "secret://providers/custom",
|
|
modelsText: "custom-model",
|
|
defaultModel: "custom-model",
|
|
relayMode: "relay",
|
|
timeoutMs: "30000",
|
|
redactionPolicy: "default",
|
|
requirement: "自定义服务至少需要平台 secret 引用、Base URL 和一个模型名。",
|
|
advancedNote: "用于非标准协议或私有网关;保存前请确认服务兼容平台适配层。"
|
|
}
|
|
};
|
|
|
|
export interface AiProviderMetrics {
|
|
total: number;
|
|
active: number;
|
|
disabled: number;
|
|
models: number;
|
|
}
|
|
|
|
export interface AiProviderActionState {
|
|
providerId: string;
|
|
label: string;
|
|
success: boolean;
|
|
message: string;
|
|
}
|
|
|
|
export interface AiProviderPageInitialState {
|
|
providers?: AiProviderResponse[];
|
|
selectedId?: string;
|
|
listState?: AiProviderListState;
|
|
listError?: string;
|
|
source?: AiProviderViewState;
|
|
action?: AiProviderActionState | null;
|
|
}
|
|
|
|
export function emptyAiProviderForm(): AiProviderFormState {
|
|
return aiProviderFormFromDefaults("openai");
|
|
}
|
|
|
|
export function aiProviderToForm(provider?: AiProviderResponse): AiProviderFormState {
|
|
if (!provider) {
|
|
return emptyAiProviderForm();
|
|
}
|
|
return {
|
|
id: provider.id,
|
|
name: provider.name,
|
|
kind: provider.kind,
|
|
baseUrl: provider.baseUrl,
|
|
apiKeyRef: "",
|
|
apiKeyConfigured: provider.apiKeyConfigured,
|
|
modelsText: provider.models.join(", "),
|
|
defaultModel: provider.defaultModel ?? "",
|
|
relayMode: provider.relayMode,
|
|
timeoutMs: String(provider.timeoutMs),
|
|
redactionPolicy: provider.redactionPolicy
|
|
};
|
|
}
|
|
|
|
export function aiProviderFormFromDefaults(kind: AiProviderKind): AiProviderFormState {
|
|
const defaults = aiProviderKindDefaults[kind];
|
|
return {
|
|
id: "",
|
|
name: defaults.name,
|
|
kind: defaults.kind,
|
|
baseUrl: defaults.baseUrl,
|
|
apiKeyRef: defaults.apiKeyRef,
|
|
apiKeyConfigured: false,
|
|
modelsText: defaults.modelsText,
|
|
defaultModel: defaults.defaultModel,
|
|
relayMode: defaults.relayMode,
|
|
timeoutMs: defaults.timeoutMs,
|
|
redactionPolicy: defaults.redactionPolicy
|
|
};
|
|
}
|
|
|
|
export function applyAiProviderKindDefaults(current: AiProviderFormState, kind: AiProviderKind): AiProviderFormState {
|
|
const defaults = aiProviderKindDefaults[kind];
|
|
return {
|
|
...current,
|
|
id: current.id,
|
|
name: defaults.name,
|
|
kind: defaults.kind,
|
|
baseUrl: defaults.baseUrl,
|
|
apiKeyRef: defaults.apiKeyRef,
|
|
apiKeyConfigured: false,
|
|
modelsText: defaults.modelsText,
|
|
defaultModel: defaults.defaultModel,
|
|
relayMode: defaults.relayMode,
|
|
timeoutMs: defaults.timeoutMs,
|
|
redactionPolicy: defaults.redactionPolicy
|
|
};
|
|
}
|
|
|
|
export function completeAiProviderForm(form: AiProviderFormState): AiProviderFormState {
|
|
const defaults = aiProviderKindDefaults[form.kind];
|
|
const modelsText = form.modelsText.trim() || defaults.modelsText;
|
|
const models = modelsText
|
|
.split(",")
|
|
.map((model) => model.trim())
|
|
.filter(Boolean);
|
|
return {
|
|
...form,
|
|
id: generatedAiProviderId(form),
|
|
name: form.name.trim() || defaults.name,
|
|
baseUrl: form.baseUrl.trim() || defaults.baseUrl,
|
|
apiKeyRef: form.apiKeyConfigured && !form.apiKeyRef.trim() ? "" : form.apiKeyRef.trim() || defaults.apiKeyRef,
|
|
modelsText,
|
|
defaultModel: form.defaultModel.trim() || models[0] || defaults.defaultModel,
|
|
relayMode: form.relayMode || defaults.relayMode,
|
|
timeoutMs: form.timeoutMs.trim() || defaults.timeoutMs,
|
|
redactionPolicy: form.redactionPolicy.trim() || defaults.redactionPolicy
|
|
};
|
|
}
|
|
|
|
export function generatedAiProviderId(form: AiProviderFormState): string {
|
|
if (form.id.trim()) {
|
|
return form.id.trim();
|
|
}
|
|
return aiProviderKindDefaults[form.kind].id;
|
|
}
|
|
|
|
export function summarizeAiProviders(providers: AiProviderResponse[]): AiProviderMetrics {
|
|
return providers.reduce<AiProviderMetrics>(
|
|
(metrics, provider) => ({
|
|
total: metrics.total + 1,
|
|
active: metrics.active + (provider.status === "active" ? 1 : 0),
|
|
disabled: metrics.disabled + (provider.status === "disabled" ? 1 : 0),
|
|
models: metrics.models + provider.models.length
|
|
}),
|
|
{ total: 0, active: 0, disabled: 0, models: 0 }
|
|
);
|
|
}
|