logancyang_obsidian-copilot/src/settings/v2/utils/modelActions.ts
Zero Liu 8658282aa9
chore(lint): enable 4 type-aware quick-win rules and fix violations (#2424)
Turns on no-unsafe-enum-comparison, no-base-to-string,
no-redundant-type-constituents, and restrict-template-expressions.
Fixes all 24 violations across 13 source files.
2026-05-13 21:57:33 -07:00

194 lines
5.7 KiB
TypeScript

import { CustomModel } from "@/aiParams";
import { ChatModelProviders, SettingKeyProviders } from "@/constants";
import { getDecryptedKey } from "@/encryptionService";
import { GitHubCopilotProvider } from "@/LLMProviders/githubCopilot/GitHubCopilotProvider";
import ProjectManager from "@/LLMProviders/projectManager";
import { logError, logWarn } from "@/logger";
import { parseModelsResponse, StandardModel } from "@/settings/providerModels";
import { err2String, getProviderInfo, safeFetch } from "@/utils";
import { getApiKeyForProvider } from "@/utils/modelUtils";
export interface FetchModelsResult {
success: boolean;
models: StandardModel[];
error?: string;
}
export interface AddModelResult {
success: boolean;
alreadyExists: boolean;
verificationFailed: boolean;
verificationError?: string;
}
/**
* Fetch models for a provider
*/
export async function fetchModelsForProvider(
provider: SettingKeyProviders
): Promise<FetchModelsResult> {
try {
// Special handling for GitHub Copilot
if (provider === ChatModelProviders.GITHUB_COPILOT) {
const copilotProvider = GitHubCopilotProvider.getInstance();
const response = await copilotProvider.listModels();
const models = parseModelsResponse(provider, response);
return { success: true, models };
}
// Standard API key based providers
let apiKey = getApiKeyForProvider(provider);
if (!apiKey) {
return { success: false, models: [], error: "API key not configured" };
}
apiKey = await getDecryptedKey(apiKey);
let url = getProviderInfo(provider).listModelURL;
if (!url) {
return { success: false, models: [], error: "Provider does not support model listing" };
}
let headers: Record<string, string> = {
Authorization: `Bearer ${apiKey}`,
};
if (provider === ChatModelProviders.GOOGLE) {
url += `?key=${apiKey}`;
headers = {};
} else if (provider === ChatModelProviders.ANTHROPIC) {
headers = {
"x-api-key": apiKey,
"anthropic-version": "2023-06-01",
};
}
const tryFetch = async (useSafeFetch: boolean) => {
const controller = new AbortController();
const timeoutId = window.setTimeout(() => controller.abort(), 3000);
try {
const response = await (useSafeFetch ? safeFetch : fetch)(url, {
headers,
signal: controller.signal,
method: "GET",
});
if (!response.ok) {
const msg = err2String(await response.json());
logError(msg);
throw new Error(`Failed to fetch models: ${response.statusText} \n detail: ` + msg);
}
return response;
} finally {
window.clearTimeout(timeoutId);
}
};
let response;
try {
response = await tryFetch(false);
} catch (firstError) {
logWarn("First fetch attempt failed, trying with safeFetch...");
try {
response = await tryFetch(true);
} catch (error) {
const msg =
"\nwithout CORS Error: " +
err2String(firstError) +
"\nwith CORS Error: " +
err2String(error);
throw new Error(msg);
}
}
const rawData = await response.json();
const models = parseModelsResponse(provider, rawData);
return { success: true, models };
} catch (error) {
logError(`Error fetching models for ${provider}:`, error);
return { success: false, models: [], error: err2String(error) };
}
}
/**
* Verify and add a model to activeModels
*/
export async function verifyAndAddModel(
model: { id: string; name: string; provider: SettingKeyProviders },
activeModels: CustomModel[],
options?: { skipVerification?: boolean }
): Promise<AddModelResult> {
const skipVerification = options?.skipVerification ?? false;
// Check if model already exists
const existingModel = activeModels.find(
(m) => m.name === model.name && (m.provider as SettingKeyProviders) === model.provider
);
const alreadyExists = Boolean(existingModel);
// Build CustomModel
const apiKey =
model.provider === ChatModelProviders.GITHUB_COPILOT
? undefined
: getApiKeyForProvider(model.provider);
const customModel: CustomModel = {
name: model.name,
provider: model.provider,
apiKey,
enabled: true,
};
// Verify model if not skipped
let verificationFailed = false;
let verificationError: string | undefined;
if (!skipVerification) {
try {
await ProjectManager.instance.getCurrentChainManager().chatModelManager.ping(customModel);
} catch (error) {
verificationFailed = true;
verificationError = err2String(error);
// For GitHub Copilot models, a "not supported" 400 typically means the user
// hasn't enabled this model on their GitHub settings page. Append the policy
// terms (which include an activation link) to guide the user.
if (
(customModel.provider as ChatModelProviders) === ChatModelProviders.GITHUB_COPILOT &&
verificationError.toLowerCase().includes("not supported")
) {
// Reason: policy cache is keyed by model.id, not customModel.name (display name)
const terms = GitHubCopilotProvider.getInstance().getPolicyTerms(model.id);
if (terms) {
verificationError += `\n\n${terms}`;
}
}
logError("Model verification failed:", error);
}
}
return {
success: true,
alreadyExists,
verificationFailed,
verificationError,
};
}
/**
* Build CustomModel object for adding to activeModels
*/
export function buildCustomModel(model: {
id: string;
name: string;
provider: SettingKeyProviders;
}): CustomModel {
return {
name: model.name,
provider: model.provider,
enabled: true,
};
}