Files
browser/platform_web/contracts/aiProviders.ts
T
2026-07-20 16:42:33 +08:00

247 lines
8.0 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;
baseUrlConfigured: boolean;
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: "",
baseUrlConfigured: provider.baseUrlConfigured,
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,
baseUrlConfigured: false,
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,
baseUrlConfigured: false,
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() || (form.baseUrlConfigured ? "" : 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 }
);
}