import {
	CLOUDFLARE_AI_GATEWAY_ANTHROPIC_BASE_URL,
	CLOUDFLARE_AI_GATEWAY_COMPAT_BASE_URL,
	CLOUDFLARE_AI_GATEWAY_OPENAI_BASE_URL,
	CLOUDFLARE_WORKERS_AI_BASE_URL,
} from "./providers/cloudflare.js";
import type { AnthropicMessagesCompat, Api, Model, ModelThinkingLevel, OpenAICompletionsCompat } from "./types.js";

export const MODELS_DEV_API_URL = "https://models.dev/api.json";

export interface ModelsDevModel {
	id?: string;
	name?: string;
	tool_call?: boolean;
	reasoning?: boolean;
	status?: string;
	limit?: {
		context?: number;
		output?: number;
	};
	cost?: {
		input?: number;
		output?: number;
		cache_read?: number;
		cache_write?: number;
	};
	modalities?: {
		input?: string[];
		output?: string[];
	};
	provider?: {
		npm?: string;
	};
}

export interface ModelsDevProviderData {
	models?: Record<string, ModelsDevModel>;
}

const COPILOT_STATIC_HEADERS = {
	"User-Agent": "GitHubCopilotChat/0.35.0",
	"Editor-Version": "vscode/1.107.0",
	"Editor-Plugin-Version": "copilot-chat/0.35.0",
	"Copilot-Integration-Id": "vscode-chat",
} as const;

const KIMI_STATIC_HEADERS = {
	"User-Agent": "KimiCLI/1.5",
} as const;

const ZAI_TOOL_STREAM_UNSUPPORTED_MODELS = new Set(["glm-4.5", "glm-4.5-air", "glm-4.5-flash", "glm-4.5v"]);

const EAGER_TOOL_INPUT_STREAMING_UNSUPPORTED_ANTHROPIC_MODELS = new Set([
	"github-copilot:claude-haiku-4.5",
	"github-copilot:claude-sonnet-4",
	"github-copilot:claude-sonnet-4.5",
]);

const DEEPSEEK_V4_THINKING_LEVEL_MAP: Partial<Record<ModelThinkingLevel, string | null>> = {
	minimal: null,
	low: null,
	medium: null,
	high: "high",
	xhigh: "max",
	max: null,
};

const KIMI_K3_THINKING_LEVEL_MAP: Partial<Record<ModelThinkingLevel, string | null>> = {
	off: null,
	minimal: null,
	low: null,
	medium: null,
	high: null,
	xhigh: null,
	max: "max",
};

const DEEPSEEK_V4_COMPAT: OpenAICompletionsCompat = {
	requiresReasoningContentOnAssistantMessages: true,
	thinkingFormat: "deepseek",
};

const ZAI_THINKING_COMPAT: OpenAICompletionsCompat = {
	supportsReasoningEffort: false,
	thinkingFormat: "zai",
};

const OPENAI_RESPONSES_NONE_REASONING_MODELS = new Set([
	"gpt-5.1",
	"gpt-5.2",
	"gpt-5.3-codex",
	"gpt-5.4",
	"gpt-5.4-mini",
	"gpt-5.4-nano",
	"gpt-5.5",
	"gpt-5.6",
	"gpt-5.6-sol",
	"gpt-5.6-terra",
	"gpt-5.6-luna",
]);

function getAnthropicMessagesCompat(provider: string, modelId: string): AnthropicMessagesCompat | undefined {
	return EAGER_TOOL_INPUT_STREAMING_UNSUPPORTED_ANTHROPIC_MODELS.has(`${provider}:${modelId}`)
		? { supportsEagerToolInputStreaming: false }
		: undefined;
}

function getBedrockBaseUrl(modelId: string): string {
	if (modelId.startsWith("eu.")) return "https://bedrock-runtime.eu-central-1.amazonaws.com";
	if (modelId.startsWith("au.")) return "https://bedrock-runtime.ap-southeast-2.amazonaws.com";
	if (modelId.startsWith("jp.")) return "https://bedrock-runtime.ap-northeast-1.amazonaws.com";
	return "https://bedrock-runtime.us-east-1.amazonaws.com";
}

function mergeThinkingLevelMap(model: Model<Api>, map: NonNullable<Model<Api>["thinkingLevelMap"]>): void {
	model.thinkingLevelMap = { ...model.thinkingLevelMap, ...map };
}

function supportsOpenAiXhigh(modelId: string): boolean {
	return (
		modelId.includes("gpt-5.2") ||
		modelId.includes("gpt-5.3") ||
		modelId.includes("gpt-5.4") ||
		modelId.includes("gpt-5.5") ||
		modelId.includes("gpt-5.6") ||
		modelId.includes("gpt-6")
	);
}

function isGoogleThinkingApi(model: Model<Api>): boolean {
	return model.api === "google-generative-ai" || model.api === "google-vertex";
}

function isGemini3ProModel(modelId: string): boolean {
	return /gemini-3(?:\.\d+)?-pro/.test(modelId.toLowerCase());
}

function isGemini3FlashModel(modelId: string): boolean {
	return /gemini-3(?:\.\d+)?-flash/.test(modelId.toLowerCase());
}

function isGemma4Model(modelId: string): boolean {
	return /gemma-?4/.test(modelId.toLowerCase());
}

export function applyThinkingLevelMetadata(model: Model<Api>): void {
	if (
		(model.api === "openai-responses" || model.api === "azure-openai-responses") &&
		(model.id.startsWith("gpt-5") || model.id.startsWith("gpt-6"))
	) {
		mergeThinkingLevelMap(model, { off: null });
	}
	if (
		model.api === "openai-responses" &&
		model.provider === "openai" &&
		OPENAI_RESPONSES_NONE_REASONING_MODELS.has(model.id)
	) {
		mergeThinkingLevelMap(model, { off: "none" });
	}
	if (supportsOpenAiXhigh(model.id)) {
		mergeThinkingLevelMap(model, { xhigh: "xhigh" });
	}
	if (model.id.includes("gpt-5.6") || model.id.includes("gpt-6")) {
		mergeThinkingLevelMap(model, { minimal: null, max: "max" });
	}
	if (
		model.id.includes("opus-4-6") ||
		model.id.includes("opus-4.6") ||
		model.id.includes("sonnet-4-6") ||
		model.id.includes("sonnet-4.6")
	) {
		mergeThinkingLevelMap(model, { max: "max" });
	}
	if (
		model.id.includes("opus-4-7") ||
		model.id.includes("opus-4.7") ||
		model.id.includes("opus-4-8") ||
		model.id.includes("opus-4.8") ||
		model.id.includes("opus-5") ||
		model.id.includes("sonnet-5")
	) {
		mergeThinkingLevelMap(model, { xhigh: "xhigh", max: "max" });
	}
	if (model.id.includes("fable-5") || model.id.includes("mythos-5")) {
		mergeThinkingLevelMap(model, { off: null, xhigh: "xhigh", max: "max" });
	}
	if (model.id.includes("mythos-preview")) {
		mergeThinkingLevelMap(model, { off: null, max: "max" });
	}
	if (
		model.api === "openai-completions" &&
		(model.id.includes("deepseek-v4") || model.id === "deepseek-flash" || model.id.endsWith("/deepseek-flash"))
	) {
		mergeThinkingLevelMap(model, DEEPSEEK_V4_THINKING_LEVEL_MAP);
	}
	const kimiK3Id = model.id.toLowerCase();
	if (!model.thinkingLevelMap && (/^k3(-|$)/.test(kimiK3Id) || /(^|\/)kimi-k3(-|$)/.test(kimiK3Id))) {
		mergeThinkingLevelMap(model, KIMI_K3_THINKING_LEVEL_MAP);
	}
	if (isGoogleThinkingApi(model) && isGemini3ProModel(model.id)) {
		mergeThinkingLevelMap(model, { off: null, minimal: null, low: "LOW", medium: null, high: "HIGH" });
	}
	if (isGoogleThinkingApi(model) && isGemini3FlashModel(model.id)) {
		mergeThinkingLevelMap(model, { off: null });
	}
	if (isGoogleThinkingApi(model) && isGemma4Model(model.id)) {
		mergeThinkingLevelMap(model, { off: null, minimal: "MINIMAL", low: null, medium: null, high: "HIGH" });
	}
}

export function parseModelsDevData(data: Record<string, unknown>): Model<Api>[] {
	const models: Model<Api>[] = [];
	const dataObj = data as Record<string, ModelsDevProviderData | undefined>;

	// Amazon Bedrock
	if (dataObj["amazon-bedrock"]?.models) {
		for (const [modelId, model] of Object.entries(dataObj["amazon-bedrock"].models)) {
			if (model.tool_call !== true) continue;
			if (modelId.startsWith("ai21.jamba") || modelId.startsWith("mistral.mistral-7b-instruct-v0")) continue;

			models.push({
				id: modelId,
				name: model.name || modelId,
				api: "bedrock-converse-stream",
				provider: "amazon-bedrock",
				baseUrl: getBedrockBaseUrl(modelId),
				reasoning: model.reasoning === true,
				input: (model.modalities?.input?.includes("image") ? ["text", "image"] : ["text"]) as ("text" | "image")[],
				cost: {
					input: model.cost?.input || 0,
					output: model.cost?.output || 0,
					cacheRead: model.cost?.cache_read || 0,
					cacheWrite: model.cost?.cache_write || 0,
				},
				contextWindow: model.limit?.context || 4096,
				maxTokens: model.limit?.output || 4096,
			});
		}
	}

	// Anthropic
	if (dataObj.anthropic?.models) {
		for (const [modelId, model] of Object.entries(dataObj.anthropic.models)) {
			if (model.tool_call !== true) continue;

			models.push({
				id: modelId,
				name: model.name || modelId,
				api: "anthropic-messages",
				provider: "anthropic",
				baseUrl: "https://api.anthropic.com",
				reasoning: model.reasoning === true,
				input: model.modalities?.input?.includes("image") ? ["text", "image"] : ["text"],
				cost: {
					input: model.cost?.input || 0,
					output: model.cost?.output || 0,
					cacheRead: model.cost?.cache_read || 0,
					cacheWrite: model.cost?.cache_write || 0,
				},
				contextWindow: model.limit?.context || 4096,
				maxTokens: model.limit?.output || 4096,
			});
		}
	}

	// Google
	const googleUnsupportedApiModelPattern = /(^|[-_.])(live|deep-research|computer-use)($|[-_.])/i;
	if (dataObj.google?.models) {
		for (const [modelId, model] of Object.entries(dataObj.google.models)) {
			if (model.tool_call !== true) continue;
			if (googleUnsupportedApiModelPattern.test(modelId)) continue;
			if (model.modalities?.output?.includes("image")) continue;

			models.push({
				id: modelId,
				name: model.name || modelId,
				api: "google-generative-ai",
				provider: "google",
				baseUrl: "https://generativelanguage.googleapis.com/v1beta",
				reasoning: model.reasoning === true,
				input: model.modalities?.input?.includes("image") ? ["text", "image"] : ["text"],
				cost: {
					input: model.cost?.input || 0,
					output: model.cost?.output || 0,
					cacheRead: model.cost?.cache_read || 0,
					cacheWrite: model.cost?.cache_write || 0,
				},
				contextWindow: model.limit?.context || 4096,
				maxTokens: model.limit?.output || 4096,
			});
		}
	}

	// OpenAI
	if (dataObj.openai?.models) {
		for (const [modelId, model] of Object.entries(dataObj.openai.models)) {
			if (model.tool_call !== true) continue;

			models.push({
				id: modelId,
				name: model.name || modelId,
				api: "openai-responses",
				provider: "openai",
				baseUrl: "https://api.openai.com/v1",
				reasoning: model.reasoning === true,
				input: model.modalities?.input?.includes("image") ? ["text", "image"] : ["text"],
				cost: {
					input: model.cost?.input || 0,
					output: model.cost?.output || 0,
					cacheRead: model.cost?.cache_read || 0,
					cacheWrite: model.cost?.cache_write || 0,
				},
				contextWindow: model.limit?.context || 4096,
				maxTokens: model.limit?.output || 4096,
			});
		}
	}

	// Groq
	if (dataObj.groq?.models) {
		for (const [modelId, model] of Object.entries(dataObj.groq.models)) {
			if (model.tool_call !== true) continue;

			models.push({
				id: modelId,
				name: model.name || modelId,
				api: "openai-completions",
				provider: "groq",
				baseUrl: "https://api.groq.com/openai/v1",
				reasoning: model.reasoning === true,
				input: model.modalities?.input?.includes("image") ? ["text", "image"] : ["text"],
				cost: {
					input: model.cost?.input || 0,
					output: model.cost?.output || 0,
					cacheRead: model.cost?.cache_read || 0,
					cacheWrite: model.cost?.cache_write || 0,
				},
				contextWindow: model.limit?.context || 4096,
				maxTokens: model.limit?.output || 4096,
			});
		}
	}

	// Cerebras
	if (dataObj.cerebras?.models) {
		for (const [modelId, model] of Object.entries(dataObj.cerebras.models)) {
			if (model.tool_call !== true) continue;

			models.push({
				id: modelId,
				name: model.name || modelId,
				api: "openai-completions",
				provider: "cerebras",
				baseUrl: "https://api.cerebras.ai/v1",
				reasoning: model.reasoning === true,
				input: model.modalities?.input?.includes("image") ? ["text", "image"] : ["text"],
				cost: {
					input: model.cost?.input || 0,
					output: model.cost?.output || 0,
					cacheRead: model.cost?.cache_read || 0,
					cacheWrite: model.cost?.cache_write || 0,
				},
				contextWindow: model.limit?.context || 4096,
				maxTokens: model.limit?.output || 4096,
			});
		}
	}

	// Cloudflare Workers AI
	if (dataObj["cloudflare-workers-ai"]?.models) {
		for (const [modelId, model] of Object.entries(dataObj["cloudflare-workers-ai"].models)) {
			if (model.tool_call !== true) continue;

			models.push({
				id: modelId,
				name: model.name || modelId,
				api: "openai-completions",
				provider: "cloudflare-workers-ai",
				baseUrl: CLOUDFLARE_WORKERS_AI_BASE_URL,
				reasoning: model.reasoning === true,
				input: model.modalities?.input?.includes("image") ? ["text", "image"] : ["text"],
				cost: {
					input: model.cost?.input || 0,
					output: model.cost?.output || 0,
					cacheRead: model.cost?.cache_read || 0,
					cacheWrite: model.cost?.cache_write || 0,
				},
				contextWindow: model.limit?.context || 4096,
				maxTokens: model.limit?.output || 4096,
				compat: { sendSessionAffinityHeaders: true },
			});
		}
	}

	// Cloudflare AI Gateway
	if (dataObj["cloudflare-ai-gateway"]?.models) {
		for (const [prefixedId, model] of Object.entries(dataObj["cloudflare-ai-gateway"].models)) {
			if (model.tool_call !== true) continue;

			const slashIdx = prefixedId.indexOf("/");
			if (slashIdx === -1) continue;
			const upstream = prefixedId.slice(0, slashIdx);
			const nativeId = prefixedId.slice(slashIdx + 1);

			let api: "anthropic-messages" | "openai-completions" | "openai-responses";
			let baseUrl: string;
			let id: string;
			if (upstream === "openai") {
				api = "openai-responses";
				baseUrl = CLOUDFLARE_AI_GATEWAY_OPENAI_BASE_URL;
				id = nativeId;
			} else if (upstream === "anthropic") {
				api = "anthropic-messages";
				baseUrl = CLOUDFLARE_AI_GATEWAY_ANTHROPIC_BASE_URL;
				id = nativeId;
			} else if (upstream === "workers-ai") {
				api = "openai-completions";
				baseUrl = CLOUDFLARE_AI_GATEWAY_COMPAT_BASE_URL;
				id = prefixedId;
			} else {
				continue;
			}

			const compat = upstream === "workers-ai" ? { sendSessionAffinityHeaders: true } : undefined;

			models.push({
				id,
				name: model.name || id,
				api,
				provider: "cloudflare-ai-gateway",
				baseUrl,
				reasoning: model.reasoning === true,
				input: model.modalities?.input?.includes("image") ? ["text", "image"] : ["text"],
				cost: {
					input: model.cost?.input || 0,
					output: model.cost?.output || 0,
					cacheRead: model.cost?.cache_read || 0,
					cacheWrite: model.cost?.cache_write || 0,
				},
				contextWindow: model.limit?.context || 4096,
				maxTokens: model.limit?.output || 4096,
				...(compat ? { compat } : {}),
			});
		}
	}

	// xAI
	if (dataObj.xai?.models) {
		for (const [modelId, model] of Object.entries(dataObj.xai.models)) {
			if (model.tool_call !== true) continue;

			models.push({
				id: modelId,
				name: model.name || modelId,
				api: "openai-completions",
				provider: "xai",
				baseUrl: "https://api.x.ai/v1",
				reasoning: model.reasoning === true,
				input: model.modalities?.input?.includes("image") ? ["text", "image"] : ["text"],
				cost: {
					input: model.cost?.input || 0,
					output: model.cost?.output || 0,
					cacheRead: model.cost?.cache_read || 0,
					cacheWrite: model.cost?.cache_write || 0,
				},
				contextWindow: model.limit?.context || 4096,
				maxTokens: model.limit?.output || 4096,
			});
		}
	}

	// zAI
	if (dataObj["zai-coding-plan"]?.models) {
		for (const [modelId, model] of Object.entries(dataObj["zai-coding-plan"].models)) {
			if (model.tool_call !== true) continue;
			const supportsImage = model.modalities?.input?.includes("image");

			models.push({
				id: modelId,
				name: model.name || modelId,
				api: "openai-completions",
				provider: "zai",
				baseUrl: "https://api.z.ai/api/coding/paas/v4",
				reasoning: model.reasoning === true,
				input: supportsImage ? ["text", "image"] : ["text"],
				cost: {
					input: model.cost?.input || 0,
					output: model.cost?.output || 0,
					cacheRead: model.cost?.cache_read || 0,
					cacheWrite: model.cost?.cache_write || 0,
				},
				compat: {
					supportsDeveloperRole: false,
					thinkingFormat: ZAI_THINKING_COMPAT.thinkingFormat,
					...(!ZAI_TOOL_STREAM_UNSUPPORTED_MODELS.has(modelId) ? { zaiToolStream: true } : {}),
				},
				contextWindow: model.limit?.context || 4096,
				maxTokens: model.limit?.output || 4096,
			});
		}
	}

	// Mistral
	if (dataObj.mistral?.models) {
		for (const [modelId, model] of Object.entries(dataObj.mistral.models)) {
			if (model.tool_call !== true) continue;

			models.push({
				id: modelId,
				name: model.name || modelId,
				api: "mistral-conversations",
				provider: "mistral",
				baseUrl: "https://api.mistral.ai",
				reasoning: model.reasoning === true,
				input: model.modalities?.input?.includes("image") ? ["text", "image"] : ["text"],
				cost: {
					input: model.cost?.input || 0,
					output: model.cost?.output || 0,
					cacheRead: model.cost?.cache_read || 0,
					cacheWrite: model.cost?.cache_write || 0,
				},
				contextWindow: model.limit?.context || 4096,
				maxTokens: model.limit?.output || 4096,
			});
		}
	}

	// Hugging Face
	if (dataObj.huggingface?.models) {
		for (const [modelId, model] of Object.entries(dataObj.huggingface.models)) {
			if (model.tool_call !== true) continue;

			models.push({
				id: modelId,
				name: model.name || modelId,
				api: "openai-completions",
				provider: "huggingface",
				baseUrl: "https://router.huggingface.co/v1",
				reasoning: model.reasoning === true,
				input: model.modalities?.input?.includes("image") ? ["text", "image"] : ["text"],
				cost: {
					input: model.cost?.input || 0,
					output: model.cost?.output || 0,
					cacheRead: model.cost?.cache_read || 0,
					cacheWrite: model.cost?.cache_write || 0,
				},
				compat: {
					supportsDeveloperRole: false,
				},
				contextWindow: model.limit?.context || 4096,
				maxTokens: model.limit?.output || 4096,
			});
		}
	}

	// Fireworks
	if (dataObj["fireworks-ai"]?.models) {
		for (const [modelId, model] of Object.entries(dataObj["fireworks-ai"].models)) {
			if (model.tool_call !== true) continue;

			models.push({
				id: modelId,
				name: model.name || modelId,
				api: "anthropic-messages",
				provider: "fireworks",
				baseUrl: "https://api.fireworks.ai/inference",
				reasoning: model.reasoning === true,
				input: model.modalities?.input?.includes("image") ? ["text", "image"] : ["text"],
				cost: {
					input: model.cost?.input || 0,
					output: model.cost?.output || 0,
					cacheRead: model.cost?.cache_read || 0,
					cacheWrite: model.cost?.cache_write || 0,
				},
				contextWindow: model.limit?.context || 4096,
				maxTokens: model.limit?.output || 4096,
			});
		}
	}

	// OpenCode and OpenCode Go
	const opencodeVariants = [
		{ key: "opencode", provider: "opencode" as const, basePath: "https://opencode.ai/zen" },
		{ key: "opencode-go", provider: "opencode-go" as const, basePath: "https://opencode.ai/zen/go" },
	];

	for (const variant of opencodeVariants) {
		if (!dataObj[variant.key]?.models) continue;

		for (const [modelId, model] of Object.entries(dataObj[variant.key]!.models!)) {
			if (model.tool_call !== true) continue;
			if (model.status === "deprecated") continue;
			if (modelId === "gpt-5.3-codex-spark") continue;

			const npm = model.provider?.npm;
			let api: Api;
			let baseUrl: string;
			let compat: OpenAICompletionsCompat | undefined;

			if (npm === "@ai-sdk/openai") {
				api = "openai-responses";
				baseUrl = `${variant.basePath}/v1`;
			} else if (npm === "@ai-sdk/anthropic") {
				api = "anthropic-messages";
				baseUrl = variant.basePath;
			} else if (npm === "@ai-sdk/google") {
				api = "google-generative-ai";
				baseUrl = `${variant.basePath}/v1`;
			} else if (npm === "@ai-sdk/alibaba") {
				api = "openai-completions";
				baseUrl = `${variant.basePath}/v1`;
				compat = { cacheControlFormat: "anthropic" };
			} else {
				api = "openai-completions";
				baseUrl = `${variant.basePath}/v1`;
			}

			if (variant.provider === "opencode-go") {
				if (modelId === "minimax-m2.7" || (npm === "@ai-sdk/anthropic" && modelId.startsWith("qwen"))) {
					api = "openai-completions";
					baseUrl = `${variant.basePath}/v1`;
				}
				if (modelId === "qwen3.5-plus" || modelId === "qwen3.6-plus") {
					api = "openai-completions";
					baseUrl = `${variant.basePath}/v1`;
					compat = { ...(compat ?? {}), thinkingFormat: "qwen" };
				}
			}

			models.push({
				id: modelId,
				name: model.name || modelId,
				api,
				provider: variant.provider,
				baseUrl,
				reasoning: model.reasoning === true,
				input: model.modalities?.input?.includes("image") ? ["text", "image"] : ["text"],
				cost: {
					input: model.cost?.input || 0,
					output: model.cost?.output || 0,
					cacheRead: model.cost?.cache_read || 0,
					cacheWrite: model.cost?.cache_write || 0,
				},
				...(compat ? { compat } : {}),
				contextWindow: model.limit?.context || 4096,
				maxTokens: model.limit?.output || 4096,
			});
		}
	}

	// GitHub Copilot
	if (dataObj["github-copilot"]?.models) {
		for (const [modelId, model] of Object.entries(dataObj["github-copilot"].models)) {
			if (model.tool_call !== true) continue;
			if (model.status === "deprecated") continue;

			const isCopilotClaude = modelId.startsWith("claude-");
			const needsResponsesApi = modelId.startsWith("gpt-5") || modelId.startsWith("oswe");

			const api: Api = isCopilotClaude
				? "anthropic-messages"
				: needsResponsesApi
					? "openai-responses"
					: "openai-completions";

			const anthropicCompat =
				api === "anthropic-messages" ? getAnthropicMessagesCompat("github-copilot", modelId) : undefined;

			const copilotModel: Model<Api> = {
				id: modelId,
				name: model.name || modelId,
				api,
				provider: "github-copilot",
				baseUrl: "https://api.individual.githubcopilot.com",
				reasoning: model.reasoning === true,
				input: model.modalities?.input?.includes("image") ? ["text", "image"] : ["text"],
				cost: {
					input: model.cost?.input || 0,
					output: model.cost?.output || 0,
					cacheRead: model.cost?.cache_read || 0,
					cacheWrite: model.cost?.cache_write || 0,
				},
				contextWindow: model.limit?.context || 128000,
				maxTokens: model.limit?.output || 8192,
				headers: { ...COPILOT_STATIC_HEADERS },
				...(anthropicCompat ? { compat: anthropicCompat } : {}),
				...(api === "openai-completions"
					? {
							compat: {
								supportsStore: false,
								supportsDeveloperRole: false,
								supportsReasoningEffort: false,
							},
						}
					: {}),
			};

			models.push(copilotModel);
		}
	}

	// MiniMax
	const minimaxVariants = [
		{ key: "minimax", provider: "minimax" as const, baseUrl: "https://api.minimax.io/anthropic" },
		{ key: "minimax-cn", provider: "minimax-cn" as const, baseUrl: "https://api.minimaxi.com/anthropic" },
	];

	for (const { key, provider, baseUrl } of minimaxVariants) {
		if (dataObj[key]?.models) {
			for (const [modelId, model] of Object.entries(dataObj[key]!.models!)) {
				if (model.tool_call !== true) continue;

				models.push({
					id: modelId,
					name: model.name || modelId,
					api: "anthropic-messages",
					provider,
					baseUrl,
					reasoning: model.reasoning === true,
					input: model.modalities?.input?.includes("image") ? ["text", "image"] : ["text"],
					cost: {
						input: model.cost?.input || 0,
						output: model.cost?.output || 0,
						cacheRead: model.cost?.cache_read || 0,
						cacheWrite: model.cost?.cache_write || 0,
					},
					contextWindow: model.limit?.context || 4096,
					maxTokens: model.limit?.output || 4096,
				});
			}
		}
	}

	// Kimi for Coding
	if (dataObj["kimi-for-coding"]?.models) {
		const kimiModels = dataObj["kimi-for-coding"].models;
		const hasCanonicalModel = Object.hasOwn(kimiModels, "kimi-for-coding");
		const kimiAliases = new Set(["k2p5", "k2p6"]);

		for (const [modelId, model] of Object.entries(kimiModels)) {
			if (model.tool_call !== true) continue;
			if (kimiAliases.has(modelId) && hasCanonicalModel) continue;

			const normalizedId = kimiAliases.has(modelId) ? "kimi-for-coding" : modelId;
			const normalizedName = kimiAliases.has(modelId) ? "Kimi For Coding" : model.name || normalizedId;

			models.push({
				id: normalizedId,
				name: normalizedName,
				api: "anthropic-messages",
				provider: "kimi-coding",
				baseUrl: "https://api.kimi.com/coding",
				headers: { ...KIMI_STATIC_HEADERS },
				reasoning: model.reasoning === true,
				input: model.modalities?.input?.includes("image") ? ["text", "image"] : ["text"],
				cost: {
					input: model.cost?.input || 0,
					output: model.cost?.output || 0,
					cacheRead: model.cost?.cache_read || 0,
					cacheWrite: model.cost?.cache_write || 0,
				},
				contextWindow: model.limit?.context || 4096,
				maxTokens: model.limit?.output || 4096,
			});
		}
	}

	// Moonshot AI
	const moonshotVariants = [
		{ key: "moonshotai", provider: "moonshotai" as const, baseUrl: "https://api.moonshot.ai/v1" },
		{ key: "moonshotai-cn", provider: "moonshotai-cn" as const, baseUrl: "https://api.moonshot.cn/v1" },
	];
	const moonshotCompat: OpenAICompletionsCompat = {
		supportsStore: false,
		supportsDeveloperRole: false,
		supportsReasoningEffort: false,
		maxTokensField: "max_tokens",
		supportsStrictMode: false,
	};

	for (const { key, provider, baseUrl } of moonshotVariants) {
		if (!dataObj[key]?.models) continue;

		for (const [modelId, model] of Object.entries(dataObj[key]!.models!)) {
			if (model.tool_call !== true) continue;

			models.push({
				id: modelId,
				name: model.name || modelId,
				api: "openai-completions",
				provider,
				baseUrl,
				reasoning: model.reasoning === true,
				input: model.modalities?.input?.includes("image") ? ["text", "image"] : ["text"],
				cost: {
					input: model.cost?.input || 0,
					output: model.cost?.output || 0,
					cacheRead: model.cost?.cache_read || 0,
					cacheWrite: model.cost?.cache_write || 0,
				},
				contextWindow: model.limit?.context || 4096,
				maxTokens: model.limit?.output || 4096,
				compat: moonshotCompat,
			});
		}
	}

	// Xiaomi MiMo
	const xiaomiVariants = [
		{ provider: "xiaomi" as const, baseUrl: "https://api.xiaomimimo.com/anthropic" },
		{ provider: "xiaomi-token-plan-cn" as const, baseUrl: "https://token-plan-cn.xiaomimimo.com/anthropic" },
		{ provider: "xiaomi-token-plan-ams" as const, baseUrl: "https://token-plan-ams.xiaomimimo.com/anthropic" },
		{ provider: "xiaomi-token-plan-sgp" as const, baseUrl: "https://token-plan-sgp.xiaomimimo.com/anthropic" },
	];

	if (dataObj.xiaomi?.models) {
		for (const { provider, baseUrl } of xiaomiVariants) {
			for (const [modelId, model] of Object.entries(dataObj.xiaomi.models)) {
				if (model.tool_call !== true) continue;

				models.push({
					id: modelId,
					name: model.name || modelId,
					api: "anthropic-messages",
					provider,
					baseUrl,
					reasoning: model.reasoning === true,
					input: model.modalities?.input?.includes("image") ? ["text", "image"] : ["text"],
					cost: {
						input: model.cost?.input || 0,
						output: model.cost?.output || 0,
						cacheRead: model.cost?.cache_read || 0,
						cacheWrite: model.cost?.cache_write || 0,
					},
					contextWindow: model.limit?.context || 4096,
					maxTokens: model.limit?.output || 4096,
				});
			}
		}
	}

	// Apply post-processing and known fixes
	applyModelsDevOverrides(models);

	// Mirror OpenAI responses models to Azure OpenAI
	const azureOpenAiModels: Model<Api>[] = models
		.filter((model) => model.provider === "openai" && model.api === "openai-responses")
		.map((model) => ({
			...model,
			api: "azure-openai-responses",
			provider: "azure-openai-responses",
			baseUrl: "",
		}));
	models.push(...azureOpenAiModels);

	// Apply thinking level metadata
	for (const model of models) {
		applyThinkingLevelMetadata(model);
	}

	return models;
}

function applyModelsDevOverrides(models: Model<Api>[]): void {
	// models.dev prices the Z.AI coding plan at zero, because that plan is billed by
	// subscription. A zero price is true for the wallet and useless for comparing two
	// models, so the published per-token prices stand in. Source: artificialanalysis.ai,
	// retrieved 2026-09-11.
	const zaiListPrices: Record<string, { input: number; output: number }> = {
		"glm-5.3-flash": { input: 0.15, output: 0.5 },
		"glm-5.3": { input: 1.4, output: 4.4 },
	};
	for (const candidate of models) {
		if (candidate.provider !== "zai") continue;
		const listPrice = zaiListPrices[candidate.id];
		if (!listPrice) continue;
		candidate.cost.input = listPrice.input;
		candidate.cost.output = listPrice.output;
	}

	// Fix Claude Opus 4.5 cache pricing from models.dev
	const opus45 = models.find((m) => m.provider === "anthropic" && m.id === "claude-opus-4-5");
	if (opus45) {
		opus45.cost.cacheRead = 0.5;
		opus45.cost.cacheWrite = 6.25;
	}

	for (const candidate of models) {
		if (candidate.provider === "amazon-bedrock" && candidate.id.includes("anthropic.claude-opus-4-6-v1")) {
			candidate.cost.cacheRead = 0.5;
			candidate.cost.cacheWrite = 6.25;
		}

		if (
			(candidate.provider === "anthropic" ||
				candidate.provider === "opencode" ||
				candidate.provider === "opencode-go" ||
				candidate.provider === "github-copilot") &&
			(candidate.id === "claude-opus-4-6" ||
				candidate.id === "claude-sonnet-4-6" ||
				candidate.id === "claude-opus-4.6" ||
				candidate.id === "claude-sonnet-4.6")
		) {
			candidate.contextWindow = 1000000;
		}

		if (
			(candidate.provider === "opencode" || candidate.provider === "opencode-go") &&
			(candidate.id === "claude-sonnet-4-5" || candidate.id === "claude-sonnet-4")
		) {
			candidate.contextWindow = 200000;
		}

		if ((candidate.provider === "opencode" || candidate.provider === "opencode-go") && candidate.id === "gpt-5.4") {
			candidate.contextWindow = 272000;
			candidate.maxTokens = 128000;
		}

		if (candidate.provider === "openai" && (candidate.id === "gpt-5.4" || candidate.id === "gpt-5.5")) {
			candidate.contextWindow = 272000;
			candidate.maxTokens = 128000;
		}

		if (
			candidate.api === "openai-completions" &&
			(candidate.id.includes("deepseek-v4") ||
				candidate.id === "deepseek-flash" ||
				candidate.id.endsWith("/deepseek-flash"))
		) {
			candidate.compat = {
				...candidate.compat,
				...DEEPSEEK_V4_COMPAT,
			};
			mergeThinkingLevelMap(candidate, DEEPSEEK_V4_THINKING_LEVEL_MAP);
		}
	}

	const minimaxDirectSupportedIds = new Set(["MiniMax-M2.7", "MiniMax-M2.7-highspeed"]);

	for (const candidate of models) {
		if (
			(candidate.provider === "minimax" || candidate.provider === "minimax-cn") &&
			minimaxDirectSupportedIds.has(candidate.id)
		) {
			candidate.contextWindow = 204800;
			candidate.maxTokens = 131072;
		}
	}

	for (let i = models.length - 1; i >= 0; i--) {
		const candidate = models[i];
		if (
			(candidate.provider === "minimax" || candidate.provider === "minimax-cn") &&
			!minimaxDirectSupportedIds.has(candidate.id)
		) {
			models.splice(i, 1);
		}
	}
}

export async function fetchModelsDevModels(
	fetchFn: typeof fetch = fetch,
	url: string = MODELS_DEV_API_URL,
): Promise<Model<Api>[]> {
	const response = await fetchFn(url);
	if (!response.ok) {
		throw new Error(`Failed to fetch models from ${url}: ${response.status} ${response.statusText}`);
	}
	const data = (await response.json()) as Record<string, unknown>;
	return parseModelsDevData(data);
}
